|
|
import { Synthetizer } from "../synthetizer/synthetizer.js"; |
|
|
import { messageTypes } from "../midi_parser/midi_message.js"; |
|
|
import { workletMessageType } from "../synthetizer/worklet_system/message_protocol/worklet_message.js"; |
|
|
import { |
|
|
SongChangeType, |
|
|
WorkletSequencerMessageType, |
|
|
WorkletSequencerReturnMessageType |
|
|
} from "./worklet_sequencer/sequencer_message.js"; |
|
|
import { SpessaSynthWarn } from "../utils/loggin.js"; |
|
|
import { DUMMY_MIDI_DATA, MIDIData } from "../midi_parser/midi_data.js"; |
|
|
import { BasicMIDI } from "../midi_parser/basic_midi.js"; |
|
|
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; |
|
|
import { DEFAULT_SEQUENCER_OPTIONS } from "./default_sequencer_options.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class Sequencer |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onError; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onTextEvent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
midiData; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
songListData = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onSongChange = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onTimeChange = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onSongEnded = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onTempoChange = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMetaEvent = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
currentTempo = 120; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
songIndex = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getMIDIResolve = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hasDummyData = true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isFinished = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
duration = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pausedTime = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(midiBinaries, synth, options = DEFAULT_SEQUENCER_OPTIONS) |
|
|
{ |
|
|
this.ignoreEvents = false; |
|
|
this.synth = synth; |
|
|
this.highResTimeOffset = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.absoluteStartTime = this.synth.currentTime; |
|
|
|
|
|
this.synth.sequencerCallbackFunction = this._handleMessage.bind(this); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._preservePlaybackState = options?.preservePlaybackState ?? false; |
|
|
|
|
|
if (this._skipToFirstNoteOn === false) |
|
|
{ |
|
|
|
|
|
this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, false); |
|
|
} |
|
|
|
|
|
if (this._preservePlaybackState === true) |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, true); |
|
|
} |
|
|
|
|
|
this.loadNewSongList(midiBinaries, options?.autoPlay ?? true); |
|
|
|
|
|
window.addEventListener("beforeunload", this.resetMIDIOut.bind(this)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_loop = true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get loop() |
|
|
{ |
|
|
return this._loop; |
|
|
} |
|
|
|
|
|
set loop(value) |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.setLoop, [value, this._loopsRemaining]); |
|
|
this._loop = value; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_loopsRemaining = -1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get loopsRemaining() |
|
|
{ |
|
|
return this._loopsRemaining; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set loopsRemaining(val) |
|
|
{ |
|
|
this._loopsRemaining = val; |
|
|
this._sendMessage(WorkletSequencerMessageType.setLoop, [this._loop, val]); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_playbackRate = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get playbackRate() |
|
|
{ |
|
|
return this._playbackRate; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set playbackRate(value) |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value); |
|
|
this.highResTimeOffset *= (value / this._playbackRate); |
|
|
this._playbackRate = value; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_shuffleSongs = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get shuffleSongs() |
|
|
{ |
|
|
return this._shuffleSongs; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set shuffleSongs(value) |
|
|
{ |
|
|
this._shuffleSongs = value; |
|
|
if (value) |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOn]); |
|
|
} |
|
|
else |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOff]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get skipToFirstNoteOn() |
|
|
{ |
|
|
return this._skipToFirstNoteOn; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set skipToFirstNoteOn(val) |
|
|
{ |
|
|
this._skipToFirstNoteOn = val; |
|
|
this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, this._skipToFirstNoteOn); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get preservePlaybackState() |
|
|
{ |
|
|
return this._preservePlaybackState; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set preservePlaybackState(val) |
|
|
{ |
|
|
this._preservePlaybackState = val; |
|
|
this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, val); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get currentTime() |
|
|
{ |
|
|
|
|
|
if (this.pausedTime !== undefined) |
|
|
{ |
|
|
return this.pausedTime; |
|
|
} |
|
|
|
|
|
return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate; |
|
|
} |
|
|
|
|
|
set currentTime(time) |
|
|
{ |
|
|
if (!this._preservePlaybackState) |
|
|
{ |
|
|
this.unpause(); |
|
|
} |
|
|
this._sendMessage(WorkletSequencerMessageType.setTime, time); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get currentHighResolutionTime() |
|
|
{ |
|
|
if (this.pausedTime !== undefined) |
|
|
{ |
|
|
return this.pausedTime; |
|
|
} |
|
|
const highResTimeOffset = this.highResTimeOffset; |
|
|
const absoluteStartTime = this.absoluteStartTime; |
|
|
|
|
|
|
|
|
const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate; |
|
|
|
|
|
let currentPerformanceTime = highResTimeOffset + performanceElapsedTime; |
|
|
const currentAudioTime = this.currentTime; |
|
|
|
|
|
const smoothingFactor = 0.01 * this._playbackRate; |
|
|
|
|
|
|
|
|
const timeDifference = currentAudioTime - currentPerformanceTime; |
|
|
this.highResTimeOffset += timeDifference * smoothingFactor; |
|
|
|
|
|
|
|
|
currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime; |
|
|
return currentPerformanceTime; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get paused() |
|
|
{ |
|
|
return this.pausedTime !== undefined; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addOnSongChangeEvent(callback, id) |
|
|
{ |
|
|
this.onSongChange[id] = callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addOnSongEndedEvent(callback, id) |
|
|
{ |
|
|
this.onSongEnded[id] = callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addOnTimeChangeEvent(callback, id) |
|
|
{ |
|
|
this.onTimeChange[id] = callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addOnTempoChangeEvent(callback, id) |
|
|
{ |
|
|
this.onTempoChange[id] = callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addOnMetaEvent(callback, id) |
|
|
{ |
|
|
this.onMetaEvent[id] = callback; |
|
|
} |
|
|
|
|
|
resetMIDIOut() |
|
|
{ |
|
|
if (!this.MIDIout) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
for (let i = 0; i < 16; i++) |
|
|
{ |
|
|
this.MIDIout.send([messageTypes.controllerChange | i, 120, 0]); |
|
|
this.MIDIout.send([messageTypes.controllerChange | i, 123, 0]); |
|
|
} |
|
|
this.MIDIout.send([messageTypes.reset]); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_sendMessage(messageType, messageData = undefined) |
|
|
{ |
|
|
this.synth.post({ |
|
|
channelNumber: -1, |
|
|
messageType: workletMessageType.sequencerSpecific, |
|
|
messageData: { |
|
|
messageType: messageType, |
|
|
messageData: messageData |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nextSong() |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.forwards]); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
previousSong() |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.backwards]); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setSongIndex(index) |
|
|
{ |
|
|
const clamped = Math.max(Math.min(this.songsAmount - 1, index), 0); |
|
|
this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.index, clamped]); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_callEvents(type, params) |
|
|
{ |
|
|
for (const key in type) |
|
|
{ |
|
|
const callback = type[key]; |
|
|
try |
|
|
{ |
|
|
callback(params); |
|
|
} |
|
|
catch (e) |
|
|
{ |
|
|
SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_handleMessage(messageType, messageData) |
|
|
{ |
|
|
if (this.ignoreEvents) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
switch (messageType) |
|
|
{ |
|
|
case WorkletSequencerReturnMessageType.midiEvent: |
|
|
|
|
|
|
|
|
|
|
|
let midiEventData = messageData; |
|
|
if (this.MIDIout) |
|
|
{ |
|
|
if (midiEventData[0] >= 0x80) |
|
|
{ |
|
|
this.MIDIout.send(midiEventData); |
|
|
return; |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.songChange: |
|
|
this.songIndex = messageData[0]; |
|
|
const songChangeData = this.songListData[this.songIndex]; |
|
|
this.midiData = songChangeData; |
|
|
this.hasDummyData = false; |
|
|
this.absoluteStartTime = 0; |
|
|
this.duration = this.midiData.duration; |
|
|
this._callEvents(this.onSongChange, songChangeData); |
|
|
|
|
|
if (messageData[1] === true) |
|
|
{ |
|
|
this.unpause(); |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.timeChange: |
|
|
|
|
|
const time = this.synth.currentTime - messageData; |
|
|
this._callEvents(this.onTimeChange, time); |
|
|
this._recalculateStartTime(time); |
|
|
if (this.paused && this._preservePlaybackState) |
|
|
{ |
|
|
this.pausedTime = time; |
|
|
} |
|
|
else |
|
|
{ |
|
|
this.unpause(); |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.pause: |
|
|
this.pausedTime = this.currentTime; |
|
|
this.isFinished = messageData; |
|
|
if (this.isFinished) |
|
|
{ |
|
|
this._callEvents(this.onSongEnded, undefined); |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.midiError: |
|
|
if (this.onError) |
|
|
{ |
|
|
this.onError(messageData); |
|
|
} |
|
|
else |
|
|
{ |
|
|
throw new Error("Sequencer error: " + messageData); |
|
|
} |
|
|
return; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.getMIDI: |
|
|
if (this._getMIDIResolve) |
|
|
{ |
|
|
this._getMIDIResolve(BasicMIDI.copyFrom(messageData)); |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.metaEvent: |
|
|
|
|
|
|
|
|
|
|
|
const event = messageData[0]; |
|
|
switch (event.messageStatusByte) |
|
|
{ |
|
|
case messageTypes.setTempo: |
|
|
event.messageData.currentIndex = 0; |
|
|
const bpm = 60000000 / readBytesAsUintBigEndian(event.messageData, 3); |
|
|
event.messageData.currentIndex = 0; |
|
|
this.currentTempo = Math.round(bpm * 100) / 100; |
|
|
if (this.onTempoChange) |
|
|
{ |
|
|
this._callEvents(this.onTempoChange, this.currentTempo); |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.text: |
|
|
case messageTypes.lyric: |
|
|
case messageTypes.copyright: |
|
|
case messageTypes.trackName: |
|
|
case messageTypes.marker: |
|
|
case messageTypes.cuePoint: |
|
|
case messageTypes.instrumentName: |
|
|
case messageTypes.programName: |
|
|
let lyricsIndex = -1; |
|
|
if (event.messageStatusByte === messageTypes.lyric) |
|
|
{ |
|
|
lyricsIndex = Math.min( |
|
|
this.midiData.lyricsTicks.indexOf(event.ticks), |
|
|
this.midiData.lyrics.length - 1 |
|
|
); |
|
|
} |
|
|
let sentStatus = event.messageStatusByte; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.midiData.isKaraokeFile && ( |
|
|
event.messageStatusByte === messageTypes.text || |
|
|
event.messageStatusByte === messageTypes.lyric |
|
|
)) |
|
|
{ |
|
|
lyricsIndex = Math.min( |
|
|
this.midiData.lyricsTicks.indexOf(event.ticks), |
|
|
this.midiData.lyricsTicks.length |
|
|
); |
|
|
sentStatus = messageTypes.lyric; |
|
|
} |
|
|
if (this.onTextEvent) |
|
|
{ |
|
|
this.onTextEvent(event.messageData, sentStatus, lyricsIndex, event.ticks); |
|
|
} |
|
|
break; |
|
|
} |
|
|
this._callEvents(this.onMetaEvent, messageData); |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.loopCountChange: |
|
|
this._loopsRemaining = messageData; |
|
|
if (this._loopsRemaining === 0) |
|
|
{ |
|
|
this._loop = false; |
|
|
} |
|
|
break; |
|
|
|
|
|
case WorkletSequencerReturnMessageType.songListChange: |
|
|
this.songListData = messageData; |
|
|
break; |
|
|
|
|
|
default: |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_recalculateStartTime(time) |
|
|
{ |
|
|
this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate; |
|
|
this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getMIDI() |
|
|
{ |
|
|
return new Promise(resolve => |
|
|
{ |
|
|
this._getMIDIResolve = resolve; |
|
|
this._sendMessage(WorkletSequencerMessageType.getMIDI, undefined); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadNewSongList(midiBuffers, autoPlay = true) |
|
|
{ |
|
|
this.pause(); |
|
|
|
|
|
this.midiData = DUMMY_MIDI_DATA; |
|
|
this.hasDummyData = true; |
|
|
this.duration = 99999; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sanitizedMidis = midiBuffers.map(m => |
|
|
{ |
|
|
if (m.binary !== undefined) |
|
|
{ |
|
|
return m; |
|
|
} |
|
|
return BasicMIDI.copyFrom(m); |
|
|
}); |
|
|
this._sendMessage(WorkletSequencerMessageType.loadNewSongList, [sanitizedMidis, autoPlay]); |
|
|
this.songIndex = 0; |
|
|
this.songsAmount = midiBuffers.length; |
|
|
if (this.songsAmount > 1) |
|
|
{ |
|
|
this.loop = false; |
|
|
} |
|
|
if (autoPlay === false) |
|
|
{ |
|
|
this.pausedTime = this.currentTime; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
connectMidiOutput(output) |
|
|
{ |
|
|
this.resetMIDIOut(); |
|
|
this.MIDIout = output; |
|
|
this._sendMessage(WorkletSequencerMessageType.changeMIDIMessageSending, output !== undefined); |
|
|
this.currentTime -= 0.1; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pause() |
|
|
{ |
|
|
if (this.paused) |
|
|
{ |
|
|
SpessaSynthWarn("Already paused"); |
|
|
return; |
|
|
} |
|
|
this.pausedTime = this.currentTime; |
|
|
this._sendMessage(WorkletSequencerMessageType.pause); |
|
|
} |
|
|
|
|
|
unpause() |
|
|
{ |
|
|
this.pausedTime = undefined; |
|
|
this.isFinished = false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
play(resetTime = false) |
|
|
{ |
|
|
if (this.isFinished) |
|
|
{ |
|
|
resetTime = true; |
|
|
} |
|
|
this._recalculateStartTime(this.pausedTime || 0); |
|
|
this.unpause(); |
|
|
this._sendMessage(WorkletSequencerMessageType.play, resetTime); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stop() |
|
|
{ |
|
|
this._sendMessage(WorkletSequencerMessageType.stop); |
|
|
} |
|
|
} |