Skip to content

Commit

Permalink
add tempo change monitoring
Browse files Browse the repository at this point in the history
addresses #95
  • Loading branch information
spessasus committed Jan 7, 2025
1 parent 712729a commit 47be27b
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 84 deletions.
211 changes: 134 additions & 77 deletions src/spessasynth_lib/sequencer/sequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { BasicMIDI } from "../midi_parser/basic_midi.js";

/**
* sequencer.js
* purpose: plays back the midi file decoded by midi_loader.js, including support for multi-channel midis (adding channels when more than 1 midi port is detected)
* purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis
* (adding channels when more than one midi port is detected)
*/

/**
Expand Down Expand Up @@ -63,7 +64,7 @@ export class Sequencer
* Fires on text event
* @type {function}
* @param data {Uint8Array} the data text
* @param type {number} the status byte of the message (the meta status byte)
* @param type {number} the status byte of the message (the meta-status byte)
* @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
*/
onTextEvent;
Expand All @@ -79,6 +80,51 @@ export class Sequencer
*/
onSongEnded = {};

/**
* Fires on tempo change
* @type {Object<string, function(number)>}
*/
onTempoChange = {};

/**
* Current song's tempo in BPM
* @type {number}
*/
currentTempo = 120;
/**
* Current song index
* @type {number}
*/
songIndex = 0;
/**
* @type {function(BasicMIDI)}
* @private
*/
_getMIDIResolve = undefined;
/**
* Indicates if the current midiData property has fake data in it (not yet loaded)
* @type {boolean}
*/
hasDummyData = true;
/**
* Indicates whether the sequencer has finished playing a sequence
* @type {boolean}
*/
isFinished = false;
/**
* The current sequence's length, in seconds
* @type {number}
*/
duration = 0;

/**
* Indicates if the sequencer is paused.
* Paused if a number, undefined if playing
* @type {undefined|number}
* @private
*/
pausedTime = undefined;

/**
* Creates a new Midi sequencer for playing back MIDI files
* @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files
Expand All @@ -97,47 +143,6 @@ export class Sequencer
*/
this.absoluteStartTime = this.synth.currentTime;

/**
* @type {function(BasicMIDI)}
* @private
*/
this._getMIDIResolve = undefined;

/**
* Controls the playback's rate
* @type {number}
*/
this._playbackRate = 1;

this.songIndex = 0;

/**
* Indicates if the current midiData property has dummy data in it (not yet loaded)
* @type {boolean}
*/
this.hasDummyData = true;

this._loop = true;

/**
* Indicates whether the sequencer has finished playing a sequence
* @type {boolean}
*/
this.isFinished = false;

/**
* Indicates if the sequencer is paused.
* Paused if a number, undefined if playing
* @type {undefined|number}
*/
this.pausedTime = undefined;

/**
* The current sequence's length, in seconds
* @type {number}
*/
this.duration = 0;

this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);

/**
Expand Down Expand Up @@ -167,6 +172,49 @@ export class Sequencer
window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
}

/**
* Internal loop marker
* @type {boolean}
* @private
*/
_loop = true;

get loop()
{
return this._loop;
}

set loop(value)
{
this._sendMessage(WorkletSequencerMessageType.setLoop, value);
this._loop = value;
}

/**
* Controls the playback's rate
* @type {number}
* @private
*/
_playbackRate = 1;

/**
* @returns {number}
*/
get playbackRate()
{
return this._playbackRate;
}

/**
* @param value {number}
*/
set playbackRate(value)
{
this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
this.highResTimeOffset *= (value / this._playbackRate);
this._playbackRate = value;
}

/**
* Indicates if the sequencer should skip to first note on
* @return {boolean}
Expand Down Expand Up @@ -230,17 +278,6 @@ export class Sequencer
this._sendMessage(WorkletSequencerMessageType.setTime, time);
}

get loop()
{
return this._loop;
}

set loop(value)
{
this._sendMessage(WorkletSequencerMessageType.setLoop, value);
this._loop = value;
}

/**
* Use for visualization as it's not affected by the audioContext stutter
* @returns {number}
Expand Down Expand Up @@ -271,24 +308,6 @@ export class Sequencer
return currentPerformanceTime;
}

/**
* @returns {number}
*/
get playbackRate()
{
return this._playbackRate;
}

/**
* @param value {number}
*/
set playbackRate(value)
{
this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
this.highResTimeOffset *= (value / this._playbackRate);
this._playbackRate = value;
}

/**
* true if paused, false if playing or stopped
* @returns {boolean}
Expand Down Expand Up @@ -328,6 +347,16 @@ export class Sequencer
this.onTimeChange[id] = callback;
}

/**
* Adds a new event that gets called when the tempo changes
* @param callback {function(number)} the new tempo, in BPM
* @param id {string} must be unique
*/
addOnTempoChangeEvent(callback, id)
{
this.onTempoChange[id] = callback;
}

resetMIDIOut()
{
if (!this.MIDIout)
Expand Down Expand Up @@ -369,6 +398,26 @@ export class Sequencer
this._sendMessage(WorkletSequencerMessageType.changeSong, false);
}

/**
* @param type {Object<string, function>}
* @param params {any}
* @private
*/
_callEvents(type, params)
{
Object.entries(type).forEach((callback) =>
{
try
{
callback[1](params);
}
catch (e)
{
SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e);
}
});
}

/**
* @param {WorkletSequencerReturnMessageType} messageType
* @param {any} messageData
Expand Down Expand Up @@ -411,7 +460,7 @@ export class Sequencer
this.hasDummyData = false;
this.absoluteStartTime = 0;
this.duration = this.midiData.duration;
Object.entries(this.onSongChange).forEach((callback) => callback[1](songChangeData));
this._callEvents(this.onSongChange, songChangeData);
// if is auto played, unpause
if (messageData[2] === true)
{
Expand All @@ -429,7 +478,7 @@ export class Sequencer
case WorkletSequencerReturnMessageType.timeChange:
// message data is absolute time
const time = this.synth.currentTime - messageData;
Object.entries(this.onTimeChange).forEach((callback) => callback[1](time));
this._callEvents(this.onTimeChange, time);
this._recalculateStartTime(time);
if (this.paused && this._preservePlaybackState)
{
Expand All @@ -446,7 +495,7 @@ export class Sequencer
this.isFinished = messageData;
if (this.isFinished)
{
Object.entries(this.onSongEnded).forEach((callback) => callback[1]());
this._callEvents(this.onSongEnded, undefined);
}
break;

Expand All @@ -466,6 +515,14 @@ export class Sequencer
{
this._getMIDIResolve(BasicMIDI.copyFrom(messageData));
}
break;

case WorkletSequencerReturnMessageType.tempoChange:
this.currentTempo = messageData;
if (this.onTempoChange)
{
this._callEvents(this.onTempoChange, this.currentTempo);
}
}
}

Expand Down Expand Up @@ -499,7 +556,7 @@ export class Sequencer
loadNewSongList(midiBuffers, autoPlay = true)
{
this.pause();
// add some dummy data
// add some fake data
this.midiData = DUMMY_MIDI_DATA;
this.hasDummyData = true;
this.duration = 99999;
Expand Down Expand Up @@ -549,7 +606,7 @@ export class Sequencer

/**
* Starts the playback
* @param resetTime {boolean} If true, time is set to 0s
* @param resetTime {boolean} If true, time is set to 0 s
*/
play(resetTime = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,15 @@ export function _processEvent(event, trackIndex)
break;

case messageTypes.setTempo:
this.oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision);
let tempoBPM = getTempo(event);
this.oneTickToSeconds = 60 / (tempoBPM * this.midiData.timeDivision);
if (this.oneTickToSeconds === 0)
{
this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
tempoBPM = 120;
}
this.post(WorkletSequencerReturnMessageType.tempoChange, Math.floor(tempoBPM * 100) / 100);
break;

// recongized but ignored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ export function _processTick()
let current = this.currentTime;
while (this.playedTime < current)
{
// find next event
// find the next event
let trackIndex = this._findFirstEventIndex();
let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
this._processEvent(event, trackIndex);

this.eventIndex[trackIndex]++;

// find next event
// find the next event
trackIndex = this._findFirstEventIndex();
if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
{
// song has ended
// the song has ended
if (this.loop)
{
this.setTimeTicks(this.midiData.loop.start);
Expand All @@ -43,7 +43,7 @@ export function _processTick()
this.setTimeTicks(this.midiData.loop.start);
return;
}
// if song has ended
// if the song has ended
else if (current >= this.duration)
{
if (this.loop && this.currentLoopCount > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ export const WorkletSequencerReturnMessageType = {
timeChange: 3, // newAbsoluteTime<number>
pause: 4, // no data
getMIDI: 5, // midiData<MIDI>
midiError: 6 // errorMSG<string>
midiError: 6, // errorMSG<string>
tempoChange: 7 // newTempoBPM<number>
};
2 changes: 1 addition & 1 deletion src/website/js/manager/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ class Manager
this.settingsUI.addSequencer(this.seq);

// play the midi
//this.seq.play(true);
this.seq.play(true);
}

async downloadDLSRMI()
Expand Down

0 comments on commit 47be27b

Please sign in to comment.