diff --git a/src/spessasynth_lib/sequencer/sequencer.js b/src/spessasynth_lib/sequencer/sequencer.js index ec19405..cfd46cc 100644 --- a/src/spessasynth_lib/sequencer/sequencer.js +++ b/src/spessasynth_lib/sequencer/sequencer.js @@ -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) */ /** @@ -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; @@ -79,6 +80,51 @@ export class Sequencer */ onSongEnded = {}; + /** + * Fires on tempo change + * @type {Object} + */ + 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 @@ -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); /** @@ -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} @@ -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} @@ -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} @@ -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) @@ -369,6 +398,26 @@ export class Sequencer this._sendMessage(WorkletSequencerMessageType.changeSong, false); } + /** + * @param type {Object} + * @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 @@ -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) { @@ -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) { @@ -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; @@ -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); + } } } @@ -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; @@ -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) { diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js index 765819e..781eb66 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js @@ -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 diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js b/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js index 8c83598..7846836 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js @@ -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); @@ -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) diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js b/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js index da14d45..8a408a7 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js @@ -39,5 +39,6 @@ export const WorkletSequencerReturnMessageType = { timeChange: 3, // newAbsoluteTime pause: 4, // no data getMIDI: 5, // midiData - midiError: 6 // errorMSG + midiError: 6, // errorMSG + tempoChange: 7 // newTempoBPM }; \ No newline at end of file diff --git a/src/website/js/manager/manager.js b/src/website/js/manager/manager.js index 77c12e6..628709d 100644 --- a/src/website/js/manager/manager.js +++ b/src/website/js/manager/manager.js @@ -500,7 +500,7 @@ class Manager this.settingsUI.addSequencer(this.seq); // play the midi - //this.seq.play(true); + this.seq.play(true); } async downloadDLSRMI()