Skip to content

Commit

Permalink
Playlist support
Browse files Browse the repository at this point in the history
- added multiple song support
- a handful of fixes
- updated the sequencer's wiki
  • Loading branch information
spessasus committed Sep 19, 2023
1 parent b0cfbad commit 1304e5b
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 127 deletions.
77 changes: 49 additions & 28 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@

<div id="title_wrapper">
<div id="progress_bar"></div>
<h1 id="title">SpessaSynth: SoundFont2 Javascript Synthetizer Online Demo</h1>
<h1 id="title">SpessaSynth: Online Demo</h1>

<div class="midi_and_sf_controller">
<label id="file_upload" for='midi_file_input'> Upload the MIDI file</label>
<input type="file" accept=".mid" id="midi_file_input"><br/>
<label id="file_upload" for='midi_file_input'>Upload your MIDI files</label>
<input type="file" accept=".mid" id="midi_file_input" multiple><br/>

<label id="sf_upload"> Upload the soundfont
<input type="file" accept=".sf2" id="sf_file_input"><br/>
Expand Down Expand Up @@ -114,6 +114,7 @@ <h1 id="title">SpessaSynth: SoundFont2 Javascript Synthetizer Online Demo</h1>
}

document.getElementById("bundled_sf").onclick = () => {
titleMessage.innerText = "Downloading SoundFont...";
fetchFont("soundfonts/GeneralUser_GS.sf2", percent => titleMessage.innerText = `Downloading SF2: ${percent}%`).then(arr => {
try {
window.soundFontParser = new SoundFont2(arr);
Expand All @@ -132,6 +133,7 @@ <h1 id="title">SpessaSynth: SoundFont2 Javascript Synthetizer Online Demo</h1>

/**
* @param midiFile {File}
* @returns {Promise<MIDI>}
*/
async function parseMidi(midiFile)
{
Expand All @@ -149,32 +151,51 @@ <h1 id="title">SpessaSynth: SoundFont2 Javascript Synthetizer Online Demo</h1>
}

/**
* @param midiFile {File}
* @param midiFiles {FileList}
*/
function startMidi(midiFile)
async function startMidi(midiFiles)
{
titleMessage.innerText = `Parsing ${midiFile.name}`;
document.getElementById("file_upload").innerText = midiFile.name;
parseMidi(midiFile).then(parsedMidi => {
if(parsedMidi.midiName.trim().length > 0)
{
titleMessage.style.fontStyle = "italic";
titleMessage.innerText = parsedMidi.midiName;
}
else
{
titleMessage.innerText = TITLE;
}
titleMessage.innerText = `Parsing ${midiFiles[0].name}`;
let fName;
if(midiFiles[0].name.length > 20)
{
fName = midiFiles[0].name.substring(0, 21) + "...";
}
else
{
fName = midiFiles[0].name;
}
if(midiFiles.length > 1)
{
fName += ` and ${midiFiles.length - 1} others`;
}
document.getElementById("file_upload").innerText = fName;
/**
* @type {MIDI[]}
*/
const parsed = [];
for (let i = 0; i < midiFiles.length; i++) {
titleMessage.innerText = `Parsing ${midiFiles[i].name}`;
parsed.push(await parseMidi(midiFiles[i]));
}
titleMessage.style.fontStyle = "italic";
if(parsed[0].midiName.trim().length > 0)
{
titleMessage.innerText = parsed[0].midiName.trim();
}
else
{
titleMessage.innerText = midiFiles[0].name;
}

if(manager.seq)
{
manager.seq.loadNewSequence(parsedMidi);
manager.seq.currentTime = 0;
}
else {
manager.play(parsedMidi);
}
});
if(manager.seq)
{
manager.seq.loadNewSongList(parsed);
manager.seq.currentTime = 0;
}
else {
manager.play(parsed);
}
}

/**
Expand Down Expand Up @@ -221,15 +242,15 @@ <h1 id="title">SpessaSynth: SoundFont2 Javascript Synthetizer Online Demo</h1>
sfInput.onchange = undefined;
if(fileInput.files[0])
{
startMidi(fileInput.files[0]);
startMidi(fileInput.files);
}
else
{
fileInput.onclick = undefined;
fileInput.onchange = e => {
if(e.target.files[0])
{
startMidi(fileInput.files[0]);
startMidi(fileInput.files);
}
}
}
Expand Down
68 changes: 56 additions & 12 deletions src/spessasynth_lib/sequencer/sequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ const MAX_NOTEONS_PER_S = 200;

export class Sequencer {
/**
* Creates a new Midi sequencer for playing back MIDI file
* @param parsedMidi {MIDI} The parsed midi
* Creates a new Midi sequencer for playing back MIDI files
* @param parsedMidis {MIDI[]} List of the parsed midi
* @param synth {Synthetizer} synth to send events to
*/
constructor(parsedMidi, synth)
constructor(parsedMidis, synth)
{
this.ignoreEvents = false;
this.synth = synth;
Expand All @@ -34,7 +34,7 @@ export class Sequencer {
* Absolute playback startTime, bases on the synth's time
* @type {number}
*/
this.absoluteStartTime = this.synth.currentTime;
this.absoluteStartTime = this.now;

/**
* Controls the playback's rate
Expand All @@ -57,7 +57,39 @@ export class Sequencer {

this.noteOnsPerS = 0;

this.loadNewSequence(parsedMidi);
this.loadNewSongList(parsedMidis);
}

/**
* @param parsedMidis {MIDI[]}
*/
loadNewSongList(parsedMidis)
{
this.songs = parsedMidis;
this.songIndex = 0;
this.loadNewSequence(this.songs[this.songIndex]);
}

nextSong()
{
this.songIndex++;
this.songIndex %= this.songs.length;
this.loadNewSequence(this.songs[this.songIndex]);
}

previousSong()
{
this.songIndex--;
if(this.songIndex < 0)
{
this.songIndex = this.songs.length - 1;
}
this.loadNewSequence(this.songs[this.songIndex]);
}

get now()
{
return performance.now() / 1000
}

/**
Expand All @@ -70,7 +102,7 @@ export class Sequencer {
{
return this.pausedTime;
}
return (this.synth.currentTime - this.absoluteStartTime) * this.playbackRate;
return (this.now - this.absoluteStartTime) * this.playbackRate;
}

set currentTime(time)
Expand All @@ -87,7 +119,7 @@ export class Sequencer {
this.playingNotes = [];
this.pausedTime = undefined;
this._playTo(time);
this.absoluteStartTime = this.synth.currentTime - time / this.playbackRate;
this.absoluteStartTime = this.now - time / this.playbackRate;
this.play();
if(this.renderer)
{
Expand Down Expand Up @@ -117,6 +149,7 @@ export class Sequencer {
*/
loadNewSequence(parsedMidi)
{
this.stop();
if (!parsedMidi.tracks) {
throw "No tracks supplied!";
}
Expand Down Expand Up @@ -146,6 +179,7 @@ export class Sequencer {
}

this.synth.resetControllers();
this.play(true);
}

calculateNoteTimes()
Expand Down Expand Up @@ -401,7 +435,7 @@ export class Sequencer {
if(this.paused)
{
// adjust the start time
this.absoluteStartTime = this.synth.currentTime - this.pausedTime;
this.absoluteStartTime = this.now - this.pausedTime;
this.pausedTime = undefined;
}

Expand All @@ -423,7 +457,7 @@ export class Sequencer {
this.playingNotes = [];
this.pausedTime = undefined;
this._playTo(0, ticks);
this.absoluteStartTime = this.synth.currentTime - this.playedTime / this.playbackRate;
this.absoluteStartTime = this.now - this.playedTime / this.playbackRate;
this.play();
if(this.renderer)
{
Expand All @@ -448,7 +482,8 @@ export class Sequencer {
return;
}
let event = this.events[this.eventIndex];
while(this.playedTime <= this.currentTime)
let current = this.currentTime
while(this.playedTime <= current)
{
this._processEvent(event);
++this.eventIndex;
Expand All @@ -459,10 +494,19 @@ export class Sequencer {
this.setTimeTicks(this.midiData.loop.start);
return;
}

if(this.eventIndex >= this.events.length)
else if(this.eventIndex >= this.events.length || current > this.duration)
{
this.pause();
// if(this.loop) {
// this.setTimeTicks(this.midiData.loop.start);
// }
// else
// {
if(this.songs.length > 1)
{
this.nextSong();
}
// }
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/spessasynth_lib/soundfont/chunk/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export class Sample {
this.indexRatio = 1;
this.sampleDataArray = smplArr;
this.sampleLengthSeconds = this.sampleLength / (this.sampleRate * 2);
this.loopAllowed = this.sampleLoopStartIndex !== this.sampleLoopEndIndex;

if (this.sampleLength < 1 || this.sampleName.substring(0, 3).toLowerCase() === "eos") {
return;
Expand Down
13 changes: 8 additions & 5 deletions src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Voice} from "./voice.js";
import {Preset} from "../../soundfont/chunk/presets.js";
import { consoleColors } from '../../utils/other.js'
import { midiControllers } from '../../midi_parser/midi_message.js'
import { DEFAULT_GAIN } from '../synthetizer.js'

const CHANNEL_LOUDNESS = 0.5;

Expand Down Expand Up @@ -123,8 +124,8 @@ export class MidiChannel {
break;

case midiControllers.lsbForControl7MainVolume:
let nevVol = (this.channelVolume << 7) | value;
this.setVolume(nevVol);
let nevVol = ((this.channelVolume & 0x7f) << 7) | value;
this.setVolume(nevVol / 128);
break;

case midiControllers.sustainPedal:
Expand All @@ -142,8 +143,8 @@ export class MidiChannel {
break;

case midiControllers.lsbForControl11ExpressionController:
const expression = (this.channelExpression << 7) | value;
this.setExpression(expression)
const expression = ((this.channelExpression & 0x7f) << 7) | value;
this.setExpression(expression / 16384)
break;

case midiControllers.effects3Depth:
Expand Down Expand Up @@ -230,6 +231,7 @@ export class MidiChannel {

setExpression(val)
{
val = Math.min(1, val);
this.channelExpression = val;
this.gainController.gain.value = this.getGain();
}
Expand Down Expand Up @@ -326,6 +328,7 @@ export class MidiChannel {
}

setVolume(volume) {
volume = Math.min(127, volume);
this.channelVolume = volume / 127;
this.gainController.gain.value = this.getGain();
}
Expand Down Expand Up @@ -546,7 +549,7 @@ export class MidiChannel {
this.channelTuningRatio = 1;
this.channelPitchBendRange = 2;
this.holdPedal = false;
this.gainController.gain.value = 1;
this.gainController.gain.value = this.getGain();
this.chorusController.gain.value = 0;
this.panner.pan.value = 0;
this.pitchBend = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ export class SynthesisModel
WAVETABLE OSCILLATOR
====================
*/
const loop = this.looping !== 0 && sample.loopAllowed;
this.wavetableOscillator = new AudioBufferSourceNode(context, {
buffer: sample.getAudioBuffer(context, offsets.start, offsets.end),
playbackRate: synthesisOptions.getPlaybackRate() * tuningRatio,
loop: this.looping !== 0
loop: loop
});

// set up loop
if (this.looping !== 0)
if (loop)
{
// lsI / (sr * 2)
const loopStartIndex = sample.sampleLoopStartIndex
Expand Down
16 changes: 12 additions & 4 deletions src/spessasynth_lib/synthetizer/synthetizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { midiControllers } from '../midi_parser/midi_message.js'
import { WorkletChannel } from './worklet_channel/worklet_channel.js'

// i mean come on
const VOICES_CAP = 1000;
const VOICES_CAP = 2137;

export const DEFAULT_GAIN = 0.5;
export const DEFAULT_PERCUSSION = 9;
Expand Down Expand Up @@ -386,8 +386,16 @@ export class Synthetizer {
// main volume
if (messageData[2] === 0x04 && messageData[3] === 0x01)
{
const vol = messageData[5] << 7 | messageData[4];
this.volumeController.gain.value = vol / 16383;
if(messageData[4])
{
const vol = messageData[5] << 7 | messageData[4];
this.volumeController.gain.value = vol / 16384 * DEFAULT_GAIN;
}
else
{
const vol = messageData[5];
this.volumeController.gain.value = vol / 127 * DEFAULT_GAIN;
}
}
break;

Expand Down Expand Up @@ -448,7 +456,7 @@ export class Synthetizer {
if(messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10)
{
// this is a roland master volume message
this.volumeController.gain.value = messageData[7] / 100;
this.volumeController.gain.value = messageData[7] / 100 * DEFAULT_GAIN;
console.log(`%cRoland Master Volume control set to: %c${messageData[7]}%c via: %c${arrayToHexString(messageData)}`,
consoleColors.info,
consoleColors.value,
Expand Down
1 change: 0 additions & 1 deletion src/website/css/sequencer_ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@

#sequencer_controls .control_buttons:hover{
cursor: pointer;
filter: brightness(1.5);
}

#sequencer_controls #note_progress_background
Expand Down
Loading

0 comments on commit 1304e5b

Please sign in to comment.