spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
372 lines (328 loc) • 9.46 kB
JavaScript
import { _addNewMidiPort, _processEvent } from "./process_event.js";
import { _findFirstEventIndex, processTick } from "./process_tick.js";
import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from "./song_control.js";
import { _playTo, _recalculateStartTime, play, setTimeTicks } from "./play.js";
import { messageTypes, midiControllers } from "../midi/midi_message.js";
import { sendMIDICC, sendMIDIMessage, sendMIDIPitchWheel, sendMIDIProgramChange, sendMIDIReset } from "./events.js";
import { SpessaSynthWarn } from "../utils/loggin.js";
import { MIDI_CHANNEL_COUNT } from "../synthetizer/synth_constants.js";
class SpessaSynthSequencer
{
/**
* All the sequencer's songs
* @type {BasicMIDI[]}
*/
songs = [];
/**
* Current song index
* @type {number}
*/
songIndex = 0;
/**
* shuffled song indexes
* @type {number[]}
*/
shuffledSongIndexes = [];
/**
* the synth to use
* @type {SpessaSynthProcessor}
*/
synth;
/**
* if the sequencer is active
* @type {boolean}
*/
isActive = false;
/**
* If the event should instead be sent back to the main thread instead of synth
* @type {boolean}
*/
sendMIDIMessages = false;
/**
* sequencer's loop count
* @type {number}
*/
loopCount = Infinity;
/**
* event's number in this.events
* @type {number[]}
*/
eventIndex = [];
/**
* tracks the time that has already been played
* @type {number}
*/
playedTime = 0;
/**
* The (relative) time when the sequencer was paused. If it's not paused, then it's undefined.
* @type {number}
*/
pausedTime = undefined;
/**
* Absolute playback startTime, bases on the synth's time
* @type {number}
*/
absoluteStartTime = 0;
/**
* Currently playing notes (for pausing and resuming)
* @type {{
* midiNote: number,
* channel: number,
* velocity: number
* }[]}
*/
playingNotes = [];
/**
* controls if the sequencer loops (defaults to true)
* @type {boolean}
*/
loop = true;
/**
* controls if the songs are ordered randomly
* @type {boolean}
*/
shuffleMode = false;
/**
* the current track data
* @type {BasicMIDI}
*/
midiData = undefined;
/**
* midi port number for the corresponding track
* @type {number[]}
*/
midiPorts = [];
midiPortChannelOffset = 0;
/**
* stored as:
* Record<midi port, channel offset>
* @type {Record<number, number>}
*/
midiPortChannelOffsets = {};
/**
* @type {boolean}
*/
skipToFirstNoteOn = true;
/**
* If true, seq will stay paused when seeking or changing the playback rate
* @type {boolean}
*/
preservePlaybackState = false;
/**
* Called on a MIDI message if sending MIDI messages is enabled
* @type {function(message: number[])}
*/
onMIDIMessage;
/**
* Called when the time changes
* @type {function(newTime: number)}
*/
onTimeChange;
/**
* Calls when sequencer stops the playback
* @type {function(isFinished: boolean)}
*/
onPlaybackStop;
/**
* Calls after the songs have been processed but before the playback begins
* @type {function(newSongList: BasicMIDI[])}
*/
onSongListChange;
/**
* Calls when the song is changed (for example, in a playlist)
* @type {function(songIndex: number, autoPlay: boolean)}
*/
onSongChange;
/**
* Calls when a meta-event occurs
* @type {function(e: MIDIMessage, trackIndex: number)}
*/
onMetaEvent;
/**
* Calls when the loop count changes (usually decreases)
* @type {function(count: number)}
*/
onLoopCountChange;
/**
* @param spessasynthProcessor {SpessaSynthProcessor}
*/
constructor(spessasynthProcessor)
{
this.synth = spessasynthProcessor;
this.absoluteStartTime = this.synth.currentSynthTime;
}
/**
* Controls the playback's rate
* @type {number}
* @private
*/
_playbackRate = 1;
// noinspection JSUnusedGlobalSymbols
/**
* @param value {number}
*/
set playbackRate(value)
{
const time = this.currentTime;
this._playbackRate = value;
this.currentTime = time;
}
get currentTime()
{
// return the paused time if it's set to something other than undefined
if (this.pausedTime !== undefined)
{
return this.pausedTime;
}
return (this.synth.currentSynthTime - this.absoluteStartTime) * this._playbackRate;
}
set currentTime(time)
{
if (!this.midiData)
{
return;
}
if (time > this.duration || time < 0)
{
// time is 0
if (this.skipToFirstNoteOn)
{
this.setTimeTicks(this.midiData.firstNoteOn - 1);
}
else
{
this.setTimeTicks(0);
}
return;
}
if (this.skipToFirstNoteOn)
{
if (time < this.firstNoteTime)
{
this.setTimeTicks(this.midiData.firstNoteOn - 1);
return;
}
}
this.stop();
this.playingNotes = [];
const wasPaused = this.paused && this.preservePlaybackState;
this.pausedTime = undefined;
this?.onTimeChange?.(time);
if (this.midiData.duration === 0)
{
SpessaSynthWarn("No duration!");
this?.onPlaybackStop?.(true);
return;
}
this._playTo(time);
this._recalculateStartTime(time);
if (wasPaused)
{
this.pause();
}
else
{
this.play();
}
}
/**
* true if paused, false if playing or stopped
* @returns {boolean}
*/
get paused()
{
return this.pausedTime !== undefined;
}
/**
* Pauses the playback
* @param isFinished {boolean}
*/
pause(isFinished = false)
{
if (this.paused)
{
SpessaSynthWarn("Already paused");
return;
}
this.pausedTime = this.currentTime;
this.stop();
this?.onPlaybackStop?.(isFinished);
}
/**
* Stops the playback
*/
stop()
{
this.clearProcessHandler();
// disable sustain
for (let i = 0; i < 16; i++)
{
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
}
this.synth.stopAllChannels();
if (this.sendMIDIMessages)
{
for (let note of this.playingNotes)
{
this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]);
}
for (let c = 0; c < MIDI_CHANNEL_COUNT; c++)
{
this.sendMIDICC(c, midiControllers.allNotesOff, 0);
}
}
}
loadCurrentSong(autoPlay = true)
{
let index = this.songIndex;
if (this.shuffleMode)
{
index = this.shuffledSongIndexes[this.songIndex];
}
this.loadNewSequence(this.songs[index], autoPlay);
}
_resetTimers()
{
this.playedTime = 0;
this.eventIndex = Array(this.tracks.length).fill(0);
}
setProcessHandler()
{
this.isActive = true;
}
clearProcessHandler()
{
this.isActive = false;
}
shuffleSongIndexes()
{
const indexes = this.songs.map((_, i) => i);
this.shuffledSongIndexes = [];
while (indexes.length > 0)
{
const index = indexes[Math.floor(Math.random() * indexes.length)];
this.shuffledSongIndexes.push(index);
indexes.splice(indexes.indexOf(index), 1);
}
}
}
// Web MIDI sending
SpessaSynthSequencer.prototype.sendMIDIMessage = sendMIDIMessage;
SpessaSynthSequencer.prototype.sendMIDIReset = sendMIDIReset;
SpessaSynthSequencer.prototype.sendMIDICC = sendMIDICC;
SpessaSynthSequencer.prototype.sendMIDIProgramChange = sendMIDIProgramChange;
SpessaSynthSequencer.prototype.sendMIDIPitchWheel = sendMIDIPitchWheel;
SpessaSynthSequencer.prototype.assignMIDIPort = assignMIDIPort;
SpessaSynthSequencer.prototype._processEvent = _processEvent;
SpessaSynthSequencer.prototype._addNewMidiPort = _addNewMidiPort;
SpessaSynthSequencer.prototype.processTick = processTick;
SpessaSynthSequencer.prototype._findFirstEventIndex = _findFirstEventIndex;
SpessaSynthSequencer.prototype.loadNewSequence = loadNewSequence;
SpessaSynthSequencer.prototype.loadNewSongList = loadNewSongList;
SpessaSynthSequencer.prototype.nextSong = nextSong;
SpessaSynthSequencer.prototype.previousSong = previousSong;
SpessaSynthSequencer.prototype.play = play;
SpessaSynthSequencer.prototype._playTo = _playTo;
SpessaSynthSequencer.prototype.setTimeTicks = setTimeTicks;
SpessaSynthSequencer.prototype._recalculateStartTime = _recalculateStartTime;
export { SpessaSynthSequencer };