UNPKG

spessasynth_lib

Version:

MIDI and SoundFont2/DLS library for the browsers with no compromises

654 lines (585 loc) 24.7 kB
import { consoleColors } from "../utils/other.js"; import { ALL_CHANNELS_OR_DIFFERENT_ACTION, BasicMIDI, loadSoundFont, masterParameterType, MIDI, MIDI_CHANNEL_COUNT, SpessaSynthCoreUtils as util, SpessaSynthLogging, SpessaSynthProcessor, SpessaSynthSequencer, SynthesizerSnapshot } from "spessasynth_core"; import { returnMessageType, workletMessageType } from "./worklet_message.js"; import { WORKLET_PROCESSOR_NAME } from "./worklet_url.js"; import { workletKeyModifierMessageType } from "./key_modifier_manager.js"; import { WorkletSoundfontManagerMessageType } from "./sfman_message.js"; import { SongChangeType, SpessaSynthSequencerMessageType, SpessaSynthSequencerReturnMessageType } from "../sequencer/sequencer_message.js"; import { fillWithDefaults } from "../utils/fill_with_defaults.js"; import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js"; import { MIDIData } from "../sequencer/midi_data.js"; // a worklet processor wrapper for the synthesizer core class WorkletSpessaProcessor extends AudioWorkletProcessor { /** * If the worklet is alive * @type {boolean} */ alive = true; /** * Instead of 18 stereo outputs, there's one with 32 channels (no effects) * @type {boolean} */ oneOutputMode = false; /** * Creates a new worklet synthesis system. contains all channels * @param options {{ * processorOptions: { * midiChannels: number, * soundfont: ArrayBuffer, * enableEventSystem: boolean, * startRenderingData: StartRenderingDataConfig * }}} */ constructor(options) { super(); const opts = options.processorOptions; // one output is indicated by setting midiChannels to 1 this.oneOutputMode = opts.midiChannels === 1; // prepare synthesizer connections /** * @param t {returnMessageType} * @param d {any} */ const postSyn = (t, d) => { // noinspection JSCheckFunctionSignatures this.postMessageToMainThread({ messageType: t, messageData: d }); }; // start rendering data const startRenderingData = opts?.startRenderingData; /** * The snapshot that synth was restored from * @type {SynthesizerSnapshot|undefined} * @private */ const snapshot = startRenderingData?.snapshot; // noinspection JSUnresolvedReference /** * Initialize the synthesis engine * @type {SpessaSynthProcessor} */ this.synthesizer = new SpessaSynthProcessor( sampleRate, // AudioWorkletGlobalScope { effectsEnabled: !this.oneOutputMode, // one output mode disables effects enableEventSystem: opts?.enableEventSystem, // enable message port? midiChannels: MIDI_CHANNEL_COUNT, // midi channel count (16) initialTime: currentTime // AudioWorkletGlobalScope, sync with audioContext time } ); this.synthesizer.onEventCall = (t, d) => { postSyn(returnMessageType.eventCall, { eventName: t, eventData: d }); }; this.synthesizer.onChannelPropertyChange = (p, n) => postSyn(returnMessageType.channelPropertyChange, [n, p]); this.synthesizer.onMasterParameterChange = (t, v) => postSyn(returnMessageType.masterParameterChange, [t, v]); const bank = loadSoundFont(opts.soundfont); this.synthesizer.soundfontManager.reloadManager(bank); this.synthesizer.processorInitialized.then(() => { // initialize the sequencer engine this.sequencer = new SpessaSynthSequencer(this.synthesizer); const postSeq = (type, data) => { this.postMessageToMainThread({ messageType: returnMessageType.sequencerSpecific, messageData: { messageType: type, messageData: data } }); }; // receive messages from the main thread this.port.onmessage = e => this.handleMessage(e.data); // sequencer events this.sequencer.onMIDIMessage = m => { postSeq(SpessaSynthSequencerReturnMessageType.midiEvent, m); }; this.sequencer.onTimeChange = t => { postSeq(SpessaSynthSequencerReturnMessageType.timeChange, t); }; this.sequencer.onPlaybackStop = p => { postSeq(SpessaSynthSequencerReturnMessageType.pause, p); }; this.sequencer.onSongChange = (i, a) => { postSeq(SpessaSynthSequencerReturnMessageType.songChange, [i, a]); }; this.sequencer.onMetaEvent = (e, i) => { postSeq(SpessaSynthSequencerReturnMessageType.metaEvent, [e, i]); }; this.sequencer.onLoopCountChange = c => { postSeq(SpessaSynthSequencerReturnMessageType.loopCountChange, c); }; this.sequencer.onSongListChange = l => { const midiDataList = l.map(s => new MIDIData(s)); this.postMessageToMainThread({ messageType: returnMessageType.sequencerSpecific, messageData: { messageType: SpessaSynthSequencerReturnMessageType.songListChange, messageData: midiDataList } }); }; if (snapshot !== undefined) { this.synthesizer.applySynthesizerSnapshot(snapshot); } // if sent, start rendering if (startRenderingData) { util.SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info); if (startRenderingData.parsedMIDI) { if (startRenderingData?.loopCount !== undefined) { this.sequencer.loopCount = startRenderingData?.loopCount; this.sequencer.loop = true; } else { this.sequencer.loop = false; } // set voice cap to unlimited this.synthesizer.voiceCap = Infinity; /** * set options * @type {SequencerOptions} */ const seqOptions = fillWithDefaults( startRenderingData.sequencerOptions, DEFAULT_SEQUENCER_OPTIONS ); this.sequencer.skipToFirstNoteOn = seqOptions.skipToFirstNoteOn; this.sequencer.preservePlaybackState = seqOptions.preservePlaybackState; this.sequencer.playbackRate = seqOptions.initialPlaybackRate; // autoplay is ignored try { // cloned objects don't have methods this.sequencer.loadNewSongList([BasicMIDI.copyFrom(startRenderingData.parsedMIDI)]); } catch (e) { console.error(e); postSeq(SpessaSynthSequencerReturnMessageType.midiError, e); } } } this.postReady(); }); } postReady() { this.postMessageToMainThread({ messageType: returnMessageType.isFullyInitialized, messageData: undefined }); } /** * @param data {WorkletReturnMessage} */ postMessageToMainThread(data) { this.port.postMessage(data); } /** * @this {WorkletSpessaProcessor} * @param message {WorkletMessage} */ handleMessage(message) { const data = message.messageData; const channel = message.channelNumber; let channelObject; if (channel >= 0) { channelObject = this.synthesizer.midiAudioChannels[channel]; if (channelObject === undefined) { util.SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`); return; } } switch (message.messageType) { case workletMessageType.midiMessage: this.synthesizer.processMessage(...data); break; case workletMessageType.customCcChange: // custom controller change channelObject.setCustomController(data[0], data[1]); break; case workletMessageType.ccReset: if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.synthesizer.resetAllControllers(); } else { channelObject.resetControllers(); } break; case workletMessageType.setChannelVibrato: if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { for (let i = 0; i < this.synthesizer.midiAudioChannels.length; i++) { const chan = this.synthesizer.midiAudioChannels[i]; if (data.rate === ALL_CHANNELS_OR_DIFFERENT_ACTION) { chan.disableAndLockGSNRPN(); } else { chan.setVibrato(data.depth, data.rate, data.delay); } } } else if (data.rate === ALL_CHANNELS_OR_DIFFERENT_ACTION) { channelObject.disableAndLockGSNRPN(); } else { channelObject.setVibrato(data.depth, data.rate, data.delay); } break; case workletMessageType.stopAll: if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.synthesizer.stopAllChannels(data === 1); } else { channelObject.stopAllNotes(data === 1); } break; case workletMessageType.killNotes: this.synthesizer.voiceKilling(data); break; case workletMessageType.muteChannel: channelObject.muteChannel(data); break; case workletMessageType.addNewChannel: this.synthesizer.createMidiChannel(true); break; case workletMessageType.debugMessage: console.debug(this.synthesizer); break; case workletMessageType.setMasterParameter: /** * @type {masterParameterType} */ const type = data[0]; const value = data[1]; this.synthesizer.setMasterParameter(type, value); break; case workletMessageType.setDrums: channelObject.setDrums(data); break; case workletMessageType.transpose: if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.synthesizer.transposeAllChannels(data[0], data[1]); } else { channelObject.transposeChannel(data[0], data[1]); } break; case workletMessageType.highPerformanceMode: this.synthesizer.highPerformanceMode = data; break; case workletMessageType.lockController: if (data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION) { channelObject.setPresetLock(data[1]); } else { channelObject.lockedControllers[data[0]] = data[1]; } break; case workletMessageType.sequencerSpecific: const seq = this.sequencer; const messageData = data.messageData; const messageType = data.messageType; switch (messageType) { default: break; case SpessaSynthSequencerMessageType.loadNewSongList: try { /** * @type {(BasicMIDI|{binary: ArrayBuffer, altName: string})[]} */ const sList = messageData[0]; const songMap = sList.map(s => { if (s.duration) { // cloned objects don't have methods return BasicMIDI.copyFrom(s); } return new MIDI(s.binary, s.altName); }); seq.loadNewSongList(songMap, messageData[1]); } catch (e) { console.error(e); this.postMessageToMainThread({ messageType: returnMessageType.sequencerSpecific, messageData: { messageType: SpessaSynthSequencerReturnMessageType.midiError, messageData: e } }); } break; case SpessaSynthSequencerMessageType.pause: seq.pause(); break; case SpessaSynthSequencerMessageType.play: seq.play(messageData); break; case SpessaSynthSequencerMessageType.stop: seq.stop(); break; case SpessaSynthSequencerMessageType.setTime: seq.currentTime = messageData; break; case SpessaSynthSequencerMessageType.changeMIDIMessageSending: seq.sendMIDIMessages = messageData; break; case SpessaSynthSequencerMessageType.setPlaybackRate: seq.playbackRate = messageData; break; case SpessaSynthSequencerMessageType.setLoop: const [loop, count] = messageData; seq.loop = loop; if (count === ALL_CHANNELS_OR_DIFFERENT_ACTION) { seq.loopCount = Infinity; } else { seq.loopCount = count; } break; case SpessaSynthSequencerMessageType.changeSong: switch (messageData[0]) { case SongChangeType.forwards: seq.nextSong(); break; case SongChangeType.backwards: seq.previousSong(); break; case SongChangeType.shuffleOff: seq.shuffleMode = false; seq.songIndex = seq.shuffledSongIndexes[seq.songIndex]; break; case SongChangeType.shuffleOn: seq.shuffleMode = true; seq.shuffleSongIndexes(); seq.songIndex = 0; seq.loadCurrentSong(); break; case SongChangeType.index: seq.songIndex = messageData[1]; seq.loadCurrentSong(); break; } break; case SpessaSynthSequencerMessageType.getMIDI: this.postMessageToMainThread({ messageType: returnMessageType.sequencerSpecific, messageData: { messageType: SpessaSynthSequencerReturnMessageType.getMIDI, messageData: seq.midiData } }); break; case SpessaSynthSequencerMessageType.setSkipToFirstNote: seq.skipToFirstNoteOn = messageData; break; case SpessaSynthSequencerMessageType.setPreservePlaybackState: seq.preservePlaybackState = messageData; } break; case workletMessageType.soundFontManager: try { const sfManager = this.synthesizer.soundfontManager; const type = data[0]; const messageData = data[1]; let font; switch (type) { case WorkletSoundfontManagerMessageType.addNewSoundFont: font = loadSoundFont(messageData[0]); sfManager.addNewSoundFont(font, messageData[1], messageData[2]); this.postMessageToMainThread({ messageType: returnMessageType.isFullyInitialized, messageData: undefined }); break; case WorkletSoundfontManagerMessageType.reloadSoundFont: font = loadSoundFont(messageData); sfManager.reloadManager(font); this.postMessageToMainThread({ messageType: returnMessageType.isFullyInitialized, messageData: undefined }); break; case WorkletSoundfontManagerMessageType.deleteSoundFont: sfManager.deleteSoundFont(messageData); break; case WorkletSoundfontManagerMessageType.rearrangeSoundFonts: sfManager.rearrangeSoundFonts(messageData); } } catch (e) { this.postMessageToMainThread({ messageType: returnMessageType.soundfontError, messageData: e }); } break; case workletMessageType.keyModifierManager: /** * @type {workletKeyModifierMessageType} */ const keyMessageType = data[0]; const man = this.synthesizer.keyModifierManager; const keyMessageData = data[1]; switch (keyMessageType) { default: return; case workletKeyModifierMessageType.addMapping: man.addMapping(...keyMessageData); break; case workletKeyModifierMessageType.clearMappings: man.clearMappings(); break; case workletKeyModifierMessageType.deleteMapping: man.deleteMapping(...keyMessageData); } break; case workletMessageType.requestSynthesizerSnapshot: const snapshot = SynthesizerSnapshot.createSynthesizerSnapshot(this.synthesizer); this.postMessageToMainThread({ messageType: returnMessageType.synthesizerSnapshot, messageData: snapshot }); break; case workletMessageType.setLogLevel: SpessaSynthLogging(data[0], data[1], data[2], data[3]); break; case workletMessageType.setEffectsGain: this.synthesizer.reverbGain = data[0]; this.synthesizer.chorusGain = data[1]; break; case workletMessageType.destroyWorklet: this.alive = false; this.synthesizer.destroySynthProcessor(); delete this.synthesizer; delete this.sequencer.midiData; delete this.sequencer; break; default: util.SpessaSynthWarn("Unrecognized event:", data); break; } } // noinspection JSUnusedGlobalSymbols /** * the audio worklet processing logic * @param inputs {Float32Array[][]} required by WebAudioAPI * @param outputs {Float32Array[][]} the outputs to write to, only the first two channels of each are populated * @returns {boolean} true unless it's not alive */ process(inputs, outputs) { if (!this.alive) { return false; } // process sequencer this.sequencer.processTick(); if (this.oneOutputMode) { const out = outputs[0]; // 1 output with 32 channels. // channels are ordered as follows: // midiChannel1L, midiChannel1R, // midiChannel2L, midiChannel2R // and so on /** * @type {Float32Array[][]} */ const channelMap = []; for (let i = 0; i < 32; i += 2) { channelMap.push([out[i], out[i + 1]]); } this.synthesizer.renderAudioSplit( [], [], // effects are disabled channelMap ); } else { // 18 outputs, each a stereo one // 0: reverb // 1: chorus // 2: channel 1 // 3: channel 2 // and so on this.synthesizer.renderAudioSplit( outputs[0], // reverb outputs[1], // chorus outputs.slice(2) ); } return true; } } // noinspection JSUnresolvedReference registerProcessor(WORKLET_PROCESSOR_NAME, WorkletSpessaProcessor); util.SpessaSynthInfo("%cProcessor successfully registered!", consoleColors.recognized);