Skip to content

Commit

Permalink
Fixed everything
Browse files Browse the repository at this point in the history
- Fixed volume balance
- Removed sample limit
- Added soundFont selection
- Samples now load at the start again to reduce stutter
- a lot of changes to the code
  • Loading branch information
spessasus committed Jul 30, 2023
1 parent be62cad commit 45c23d1
Show file tree
Hide file tree
Showing 20 changed files with 458 additions and 185 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/node_modules/
/soundfonts/
/disabled_sf2s/
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ SoundFont2 based realtime synthetizer and MIDI visualizer written in JavaScript.
- Written in pure JavaScript using WebAudio API (Express.js is only used for the file server)

## Limitations
- Max 4 Samples per note. It's probably my bad coding or it's too much for the browser. Either way, it may cause problems with some instruments, but the program tries to find the samples that matter the most. I might consider writing custom AudioWorklet to get around this.
- The program currently supports a limited amount of generators and no modulators. There are some problems with the volume balancing. This program is still in it's early development, so it might not sound as good as other synthetizers (e.g. FluidSynth)
- The program currently supports a limited amount of generators and no modulators. This program is still in it's early development, so it might not sound as good as other synthetizers (e.g. FluidSynth)

## Installation
***Chrome is highly recommended!***
Expand All @@ -24,7 +23,7 @@ SoundFont2 based realtime synthetizer and MIDI visualizer written in JavaScript.

**Requires Node.js**
1. Download the code as zip and extract or use `git clone https://github.com/spessasus/SpessaSynth`
2. Put your file named `soundfont.sf2` into the `soundfonts` folder. (SoundFont selection coming soon)
2. Put your soundfonts into the `soundfonts` folder. (you can select soundfonts in the program)
3. Double click the `start.bat`
4. Enjoy!

Expand Down
25 changes: 23 additions & 2 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,39 @@ input[type="file"] {
transition: width ease 0.5s;
}

#file_upload {
.midi_and_sf_controller {
position: relative;
display: block;
width: fit-content;
margin: auto;
padding: 6px 12px;
}

#file_upload
{
padding: 6px 0;
border-radius: 5px;
cursor: pointer;
background-color: #222;
font-weight: bolder;
}

#sf_selector option
{
background-color: var(--top-color);
text-align: center;
}

#sf_selector
{
display: block;
border: none;
font-size: 15px;
background: var(--top-color);
text-align: center;
width: 100%;
margin-top: 5px;
}

/*Center*/
#note_canvas
{
Expand Down
10 changes: 7 additions & 3 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
<div id="title_wrapper">
<div id="progress_bar"></div>
<h1 id="title">SpessaSynth: MIDI Soundfont2 Player</h1>
<label id="file_upload"> Upload a MIDI file
<input type="file" accept="audio/parsedMidi" id="midi_file_input"><br/>
</label>
<div class="midi_and_sf_controller">
<label id="file_upload"> Upload a MIDI file
<input type="file" accept="audio/parsedMidi" id="midi_file_input"><br/>
</label>
<select id="sf_selector">
</select>
</div>
</div>

<div id="keyboard_selector">
Expand Down
112 changes: 83 additions & 29 deletions src/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,54 @@ function startMidi(midiFile)
});
}

/**
* Fetches and replaces the current manager's font
* @param fontName {string}
*/
function replaceFont(fontName)
{
titleMessage.innerText = "Downloading soundfont...";
fetchFont(fontName, percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px`)
.then(data => {
titleMessage.innerText = "Parsing soundfont...";
setTimeout(() => {
window.soundFontParser = new SoundFont2(data);
progressBar.style.width = "0";

if(window.soundFontParser.presets.length < 1)
{
titleMessage.innerText = "No presets in the soundfont! Check your file?"
return;
}
titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player";

// prompt the user to click if needed
if(!window.audioContextMain)
{
titleMessage.innerText = "Press anywhere to start the app";
return;
}

if(!window.manager) {
// prepare the manager
window.manager = new Manager(audioContextMain, soundFontParser);
}
else
{
window.manager.synth.soundFont = window.soundFontParser;
window.manager.synth.reloadSoundFont();
window.manager.keyboard.reloadSelectors();

if(window.manager.seq)
{
// resets controllers
window.manager.seq.currentTime -= 0.1;
}
}
});
});
}

document.body.onclick = () =>
{
// user has clicked, we can create the ui
Expand All @@ -98,38 +146,44 @@ document.body.onclick = () =>
document.body.onclick = null;
}

titleMessage.innerText = "Downloading soundfont...";
/**
* @type {{name: string, size: number}[]}
*/
let soundFonts = [];

fetchFont("soundfont.sf2", percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px`)
.then(data => {
titleMessage.innerText = "Parsing soundfont...";
setTimeout(() => {
window.soundFontParser = new SoundFont2(data);
// load the list of soundfonts
fetch("soundfonts").then(async r => {
const sfSelector = document.getElementById("sf_selector");

titleMessage.innerText = "SpessaSynth: MIDI Soundfont2 Player";
progressBar.style.width = "0";
soundFonts = JSON.parse(await r.text());
for(let sf of soundFonts)
{
const option = document.createElement("option");
option.innerText = sf.name;
sfSelector.appendChild(option);
}

if(!fileInput.files[0]) {
fileInput.onchange = () => {
if (!fileInput.files[0]) {
return;
}
startMidi(fileInput.files[0]);
fileInput.onchange = null;
};
}
else
{
startMidi(fileInput.files[0]);
}
sfSelector.onchange = e => {
if(window.manager.seq)
{
window.manager.seq.pause();
}
replaceFont(e.target.value);
}

// prompt the user to click if needed
if(!window.audioContextMain)
{
titleMessage.innerText = "Press anywhere to start the app";
// fetch the smallest sf2 first...
replaceFont(soundFonts[0].name)
if(!fileInput.files[0]) {
fileInput.onchange = () => {
if (!fileInput.files[0]) {
return;
}
// prepare the manager
window.manager = new Manager(audioContextMain, soundFontParser);
});
});
startMidi(fileInput.files[0]);
fileInput.onchange = null;
};
}
else
{
startMidi(fileInput.files[0]);
}
})
43 changes: 43 additions & 0 deletions src/js/midi_parser/midi_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export class MIDI{
*/
this.tempoChanges = [{ticks: 0, tempo: 120}];

let loopStart = null;
let loopEnd = null;

/**
* Read all the tracks
* @type {MidiMessage[][]}
Expand Down Expand Up @@ -135,6 +138,38 @@ export class MIDI{
tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
});
}

// check for loop (CC 2/4)
if((statusByte & 0xF0) === 0xB0)
{
// loop start
if(eventData[0] === 2)
{
if(loopStart === null)
{
loopStart = totalTicks;
}
else
{
// this controller has occured more than once, this means that it doesnt indicate the loop
loopStart = 0;
}
}

// loop end
if(eventData[0] === 4)
{
if(loopEnd === null)
{
loopEnd = totalTicks;
}
else
{
// this controller has occured more than once, this means that it doesnt indicate the loop
loopEnd = 0;
}
}
}
}
this.tracks.push(track);
console.log("Parsed", this.tracks.length, "/", this.tracksAmount);
Expand All @@ -143,6 +178,14 @@ export class MIDI{
this.lastEventTick = Math.max(...this.tracks.map(t => t[t.length - 1].ticks));
console.log("MIDI file parsed. Total tick time:", this.lastEventTick);

if(loopStart === null || loopEnd === null || loopEnd === 0)
{
loopStart = 0;
loopEnd = this.lastEventTick
}
this.loop = {start: loopStart, end: loopEnd};
console.log("loop", this.loop);

// get track name
this.midiName = "";
if(this.tracks[0][0].messageStatusByte === 0x03) {
Expand Down
15 changes: 13 additions & 2 deletions src/js/midi_player/sequencer/sequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export class Sequencer {

set currentTime(time)
{
if(time < 0 || time > this.duration)
{
time = 0;
}
this.stop();
this.playingNotes = [];
this.pausedTime = undefined;
Expand Down Expand Up @@ -216,9 +220,15 @@ export class Sequencer {
this._processEvent(event);
++this.eventIndex;

if(this.eventIndex >= this.events.length)
// loop
if(this.eventIndex >= this.events.length || this.midiData.loop.end <= event.ticks)
{
this.currentTime = 0;
this.stop();
this.playingNotes = [];
this.pausedTime = undefined;
this.eventIndex = this.events.findIndex(e => e.ticks >= this.midiData.loop.start);
this.absoluteStartTime = this.synth.currentTime - this.ticksToSeconds(this.events[this.eventIndex].ticks) / this.playbackRate;
this.play();
return;
}

Expand All @@ -238,6 +248,7 @@ export class Sequencer {
if(this.rendererEventIndex >= this.events.length)
{
return;
//this.rendererEventIndex = this.events.findIndex(e => e.ticks >= this.midiData.loop.start);
}
event = this.events[this.rendererEventIndex - 1];

Expand Down
24 changes: 22 additions & 2 deletions src/js/midi_player/synthetizer/midi_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class MidiChannel {

// this.reverbCreator = this.ctx.createConvolver();
// fetch("other/impulse.wav").then(async r => {
// this.reverbCreator.buffer = await this.ctx.decodeAudioData(await r.arrayBuffer());
// this.reverbCreator.sampleData = await this.ctx.decodeAudioData(await r.arrayBuffer());
// })

this.resetControllers();
Expand Down Expand Up @@ -85,6 +85,24 @@ export class MidiChannel {
// this.reverbController.gain.value = value / 127;
// }

/**
* Transposes the channel by given octaves
* @param octaves {number} can be positive and negative
*/
transposeChannel(octaves)
{
function GetTransposeFrequencyMultiplier(transpose) {
if (transpose === 0) {
return 1;
} else if (transpose > 0) {
return 2 ** transpose;
} else if (transpose < 0) {
return 1 / (2 ** Math.abs(transpose));
}
}

}

pressHoldPedal()
{
this.holdPedal = true;
Expand Down Expand Up @@ -141,7 +159,7 @@ export class MidiChannel {
return;
}

let note = new Voice(midiNote, this.panner, this.sf, this.preset, this.vibrato, this.channelTuningRatio, (highPerf ? 2 : 4));
let note = new Voice(midiNote, velocity, this.panner, this.sf, this.preset, this.vibrato, this.channelTuningRatio, (highPerf ? 2 : 4));

// calculate gain
let gain = (velocity / 127);
Expand Down Expand Up @@ -390,6 +408,8 @@ export class MidiChannel {
this.panner.pan.value = 0;
this.pitchBend = 0;

this.trasposeMultiplier = 1;

this.vibrato = {depth: 0, rate: 0, delay: 0};
this.resetParameters();
}
Expand Down
8 changes: 8 additions & 0 deletions src/js/midi_player/synthetizer/synthetizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ export class Synthetizer {
}
}

reloadSoundFont()
{
for(let i = 0; i < 16; i++)
{
this.programChange(i, this.midiChannels[i].preset.program);
}
}

/**
* Sends a sysex
* @param messageData {ShiftableByteArray} the message's data (after F0)
Expand Down
Loading

0 comments on commit 45c23d1

Please sign in to comment.