UNPKG

wam-community

Version:

A collection of prebuilt Web Audio Modules ready for use

536 lines (490 loc) 17.4 kB
/** @typedef {import('@webaudiomodules/api').WamEvent} WamEvent */ /** @typedef {import('@webaudiomodules/api').WamEventType} WamEventType */ /** @typedef {import('@webaudiomodules/api').WamAutomationEvent} WamAutomationEvent */ /** @typedef {import('@webaudiomodules/api').WamTransportEvent} WamTransportEvent */ /** @typedef {import('@webaudiomodules/api').WamMidiEvent} WamMidiEvent */ /** @typedef {import('@webaudiomodules/api').WamSysexEvent} WamSysexEvent */ /** @typedef {import('@webaudiomodules/api').WamMpeEvent} WamMpeEvent */ /** @typedef {import('@webaudiomodules/api').WamOscEvent} WamOscEvent */ /** @typedef {import('@webaudiomodules/api').WamInfoEvent} WamInfoEvent */ /** @typedef {import('@webaudiomodules/api').WamParameterData} WamParameterData */ /** @typedef {import('@webaudiomodules/api').WamTransportData} WamTransportData */ /** @typedef {import('@webaudiomodules/api').WamMidiData} WamMidiData */ /** @typedef {import('@webaudiomodules/api').WamBinaryData} WamBinaryData */ /** @typedef {import('@webaudiomodules/api').WamInfoData} WamInfoData */ /** @typedef {import('@webaudiomodules/api').AudioWorkletGlobalScope} AudioWorkletGlobalScope */ /** @typedef {typeof import('./types').RingBuffer} RingBufferConstructor */ /** @typedef {import('./types').RingBuffer} RingBuffer */ /** @typedef {import('./types').TypedArrayConstructor} TypedArrayConstructor */ /** @typedef {import('./types').WamEventRingBuffer} IWamEventRingBuffer */ /** @typedef {typeof import('./types').WamEventRingBuffer} WamEventRingBufferConstructor */ /** @typedef {import('./types').WamSDKBaseModuleScope} WamSDKBaseModuleScope */ /** * @param {string} [moduleId] * @returns {WamEventRingBufferConstructor} */ const getWamEventRingBuffer = (moduleId) => { /** @type {AudioWorkletGlobalScope} */ // @ts-ignore const audioWorkletGlobalScope = globalThis; /** * @implements {IWamEventRingBuffer} */ class WamEventRingBuffer { /** * Default number of additional bytes allocated * per event (to support variable-size event objects) * * @type {number} */ static DefaultExtraBytesPerEvent = 64; /** * Number of bytes required for WamEventBase * {uint32} total event size in bytes * {uint8} encoded event type * {float64} time * * @type {number} */ static WamEventBaseBytes = 4 + 1 + 8; /** * Number of bytes required for WamAutomationEvent * {WamEventBaseBytes} common event properties * {uint16} encoded parameter id * {float64} value * {uint8} normalized * * @type {number} */ static WamAutomationEventBytes = WamEventRingBuffer.WamEventBaseBytes + 2 + 8 + 1; /** * Number of bytes required for WamTransportEvent * {WamEventBaseBytes} common event properties * {uint32} current bar * {float64} currentBarStarted * {float64} tempo * {uint8} time signature numerator * {uint8} time signature denominator * {uint8} playing flag * * @type {number} */ static WamTransportEventBytes = WamEventRingBuffer.WamEventBaseBytes + 4 + 8 + 8 + 1 + 1 + 1; /** * Number of bytes required for WamMidiEvent or WamMpeEvent * {WamEventBaseBytes} common event properties * {uint8} status byte * {uint8} data1 byte * {uint8} data2 byte * * @type {number} */ static WamMidiEventBytes = WamEventRingBuffer.WamEventBaseBytes + 1 + 1 + 1; /** * Number of bytes required for WamSysexEvent or WamOscEvent * (total number depends on content of message / size of byte array) * {WamEventBaseBytes} common event properties * {uint32} number of bytes in binary array * {uint8[]} N bytes in binary array depending on message * * @type {number} */ static WamBinaryEventBytes = WamEventRingBuffer.WamEventBaseBytes + 4; // + N /** * Returns a SharedArrayBuffer large enough to safely store * the specified number of events. Specify 'maxBytesPerEvent' * to support variable-size binary event types like sysex or osc. * * @param {RingBufferConstructor} RingBuffer * @param {number} eventCapacity * @param {number} [maxBytesPerEvent=undefined] * @returns {SharedArrayBuffer} */ static getStorageForEventCapacity(RingBuffer, eventCapacity, maxBytesPerEvent = undefined) { if (maxBytesPerEvent === undefined) maxBytesPerEvent = WamEventRingBuffer.DefaultExtraBytesPerEvent; else maxBytesPerEvent = Math.max(maxBytesPerEvent, WamEventRingBuffer.DefaultExtraBytesPerEvent); const capacity = (Math.max( WamEventRingBuffer.WamAutomationEventBytes, WamEventRingBuffer.WamTransportEventBytes, WamEventRingBuffer.WamMidiEventBytes, WamEventRingBuffer.WamBinaryEventBytes, ) + maxBytesPerEvent) * eventCapacity; return RingBuffer.getStorageForCapacity(capacity, Uint8Array); } /** * Provides methods for encoding / decoding WamEvents to / from * a UInt8Array RingBuffer. Specify 'maxBytesPerEvent' * to support variable-size binary event types like sysex or osc. * * @param {RingBufferConstructor} RingBuffer * @param {SharedArrayBuffer} sab * @param {string[]} parameterIds * @param {number} [maxBytesPerEvent=undefined] */ constructor(RingBuffer, sab, parameterIds, maxBytesPerEvent = undefined) { /** @type {Record<string, number>} */ this._eventSizeBytes = {}; /** @type {Record<string, number>} */ this._encodeEventType = {}; /** @type {Record<number, string>} */ this._decodeEventType = {}; /** @type {WamEventType[]} */ const wamEventTypes = ['wam-automation', 'wam-transport', 'wam-midi', 'wam-sysex', 'wam-mpe', 'wam-osc', 'wam-info']; wamEventTypes.forEach((type, encodedType) => { let byteSize = 0; switch (type) { case 'wam-automation': byteSize = WamEventRingBuffer.WamAutomationEventBytes; break; case 'wam-transport': byteSize = WamEventRingBuffer.WamTransportEventBytes; break; case 'wam-mpe': case 'wam-midi': byteSize = WamEventRingBuffer.WamMidiEventBytes; break; case 'wam-osc': case 'wam-sysex': case 'wam-info': byteSize = WamEventRingBuffer.WamBinaryEventBytes; break; default: break; } this._eventSizeBytes[type] = byteSize; this._encodeEventType[type] = encodedType; this._decodeEventType[encodedType] = type; }); /** @type {number} */ this._parameterCode = 0; /** @type {{[parameterId: string]: number}} */ this._parameterCodes = {}; /** @type {{[parameterId: string]: number}} */ this._encodeParameterId = {}; /** @type {{[parameterId: number]: string}} */ this._decodeParameterId = {}; this.setParameterIds(parameterIds); /** @type {SharedArrayBuffer} */ this._sab = sab; if (maxBytesPerEvent === undefined) maxBytesPerEvent = WamEventRingBuffer.DefaultExtraBytesPerEvent; else maxBytesPerEvent = Math.max(maxBytesPerEvent, WamEventRingBuffer.DefaultExtraBytesPerEvent); /** @type {number} */ this._eventBytesAvailable = Math.max( WamEventRingBuffer.WamAutomationEventBytes, WamEventRingBuffer.WamTransportEventBytes, WamEventRingBuffer.WamMidiEventBytes, WamEventRingBuffer.WamBinaryEventBytes, ) + maxBytesPerEvent; /** @type {ArrayBuffer} */ this._eventBytes = new ArrayBuffer(this._eventBytesAvailable); /** @type {DataView} */ this._eventBytesView = new DataView(this._eventBytes); /** @type {RingBuffer} */ this._rb = new RingBuffer(this._sab, Uint8Array); /** @type {Uint8Array} */ this._eventSizeArray = new Uint8Array(this._eventBytes, 0, 4); /** @type {DataView} */ this._eventSizeView = new DataView(this._eventBytes, 0, 4); } /** * Write common WamEvent properties to internal buffer. * * @private * @param {number} byteSize total size of event in bytes * @param {string} type * @param {number} time * @returns {number} updated byte offset */ _writeHeader(byteSize, type, time) { let byteOffset = 0; this._eventBytesView.setUint32(byteOffset, byteSize); byteOffset += 4; this._eventBytesView.setUint8(byteOffset, this._encodeEventType[type]); byteOffset += 1; this._eventBytesView.setFloat64(byteOffset, Number.isFinite(time) ? time : -1); byteOffset += 8; return byteOffset; } /** * Write WamEvent to internal buffer. * * @private * @param {WamEvent} event * @returns {Uint8Array} */ _encode(event) { let byteOffset = 0; const { type, time } = event; switch (event.type) { case 'wam-automation': { if (!(event.data.id in this._encodeParameterId)) break; const byteSize = this._eventSizeBytes[type]; byteOffset = this._writeHeader(byteSize, type, time); /** * @type {WamAutomationEvent} * @property {WamAutomationData} data */ const { data } = event; const encodedParameterId = this._encodeParameterId[data.id]; const { value, normalized } = data; this._eventBytesView.setUint16(byteOffset, encodedParameterId); byteOffset += 2; this._eventBytesView.setFloat64(byteOffset, value); byteOffset += 8; this._eventBytesView.setUint8(byteOffset, normalized ? 1 : 0); byteOffset += 1; } break; case 'wam-transport': { const byteSize = this._eventSizeBytes[type]; byteOffset = this._writeHeader(byteSize, type, time); /** * @type {WamTransportEvent} * @property {WamTransportData} data */ const { data } = event; const { currentBar, currentBarStarted, tempo, timeSigNumerator, timeSigDenominator, playing } = data; this._eventBytesView.setUint32(byteOffset, currentBar); byteOffset += 4; this._eventBytesView.setFloat64(byteOffset, currentBarStarted); byteOffset += 8; this._eventBytesView.setFloat64(byteOffset, tempo); byteOffset += 8; this._eventBytesView.setUint8(byteOffset, timeSigNumerator); byteOffset += 1; this._eventBytesView.setUint8(byteOffset, timeSigDenominator); byteOffset += 1; this._eventBytesView.setUint8(byteOffset, playing ? 1 : 0); byteOffset += 1; } break; case 'wam-mpe': case 'wam-midi': { const byteSize = this._eventSizeBytes[type]; byteOffset = this._writeHeader(byteSize, type, time); /** * @type {WamMidiEvent | WamMpeEvent} * @property {WamMidiData} data */ const { data } = event; const { bytes } = data; let b = 0; while (b < 3) { this._eventBytesView.setUint8(byteOffset, bytes[b]); byteOffset += 1; b++; } } break; case 'wam-osc': case 'wam-sysex': case 'wam-info': { /** @type {Uint8Array | null} */ let bytes = null; if (event.type === 'wam-info') { /** * @type {WamInfoEvent} * @property {WamInfoData} data */ const { data } = event; bytes = (new TextEncoder()).encode(data.instanceId); } else { /** * @type {WamSysexEvent | WamOscEvent} * @property {WamBinaryData} data */ const { data } = event; bytes = data.bytes; } const numBytes = bytes.length; const byteSize = this._eventSizeBytes[type]; byteOffset = this._writeHeader(byteSize + numBytes, type, time); this._eventBytesView.setUint32(byteOffset, numBytes); byteOffset += 4; const bytesRequired = byteOffset + numBytes; // eslint-disable-next-line no-console if (bytesRequired > this._eventBytesAvailable) console.error(`Event requires ${bytesRequired} bytes but only ${this._eventBytesAvailable} have been allocated!`); const buffer = new Uint8Array(this._eventBytes, byteOffset, numBytes); buffer.set(bytes); byteOffset += numBytes; } break; default: break; } return new Uint8Array(this._eventBytes, 0, byteOffset); } /** * Read WamEvent from internal buffer. * * @private * @returns {WamEvent | false} Decoded WamEvent */ _decode() { let byteOffset = 0; const type = this._decodeEventType[this._eventBytesView.getUint8(byteOffset)]; byteOffset += 1; let time = this._eventBytesView.getFloat64(byteOffset); if (time === -1) time = undefined; byteOffset += 8; switch (type) { case 'wam-automation': { const encodedParameterId = this._eventBytesView.getUint16(byteOffset); byteOffset += 2; const value = this._eventBytesView.getFloat64(byteOffset); byteOffset += 8; const normalized = !!this._eventBytesView.getUint8(byteOffset); byteOffset += 1; if (!(encodedParameterId in this._decodeParameterId)) break; const id = this._decodeParameterId[encodedParameterId]; /** @type {WamAutomationEvent} */ const event = { type, time, data: { id, value, normalized, }, }; return event; } case 'wam-transport': { const currentBar = this._eventBytesView.getUint32(byteOffset); byteOffset += 4; const currentBarStarted = this._eventBytesView.getFloat64(byteOffset); byteOffset += 8; const tempo = this._eventBytesView.getFloat64(byteOffset); byteOffset += 8; const timeSigNumerator = this._eventBytesView.getUint8(byteOffset); byteOffset += 1; const timeSigDenominator = this._eventBytesView.getUint8(byteOffset); byteOffset += 1; const playing = (this._eventBytesView.getUint8(byteOffset) == 1); byteOffset += 1; /** @type {WamTransportEvent} */ const event = { type, time, data: { currentBar, currentBarStarted, tempo, timeSigNumerator, timeSigDenominator, playing }, }; return event; } case 'wam-mpe': case 'wam-midi': { /** @type {[number, number, number]} */ const bytes = [0, 0, 0]; let b = 0; while (b < 3) { bytes[b] = this._eventBytesView.getUint8(byteOffset); byteOffset += 1; b++; } /** @type {WamMidiEvent | WamMpeEvent} */ const event = { type, time, data: { bytes }, }; return event; } case 'wam-osc': case 'wam-sysex': case 'wam-info': { const numBytes = this._eventBytesView.getUint32(byteOffset); byteOffset += 4; const bytes = new Uint8Array(numBytes); bytes.set(new Uint8Array(this._eventBytes, byteOffset, numBytes)); byteOffset += numBytes; if (type === 'wam-info') { const instanceId = (new TextDecoder()).decode(bytes); const data = { instanceId }; return { type, time, data }; } else { const data = { bytes }; return { type, time, data }; } } default: break; } // eslint-disable-next-line no-console // console.warn('Failed to decode event!'); return false; } /** * Write WamEvents to the ring buffer, returning * the number of events successfully written. * * @param {WamEvent[]} events * @returns {number} */ write(...events) { const numEvents = events.length; let bytesAvailable = this._rb.availableWrite; let numSkipped = 0; let i = 0; while (i < numEvents) { const event = events[i]; const bytes = this._encode(event); const eventSizeBytes = bytes.byteLength; let bytesWritten = 0; if (bytesAvailable >= eventSizeBytes) { if (eventSizeBytes === 0) numSkipped++; else bytesWritten = this._rb.push(bytes); } else break; bytesAvailable -= bytesWritten; i++; } return i - numSkipped; } /** * Read WamEvents from the ring buffer, returning * the list of events successfully read. * * @returns {WamEvent[]} */ read() { if (this._rb.empty) return []; const events = []; let bytesAvailable = this._rb.availableRead; let bytesRead = 0; while (bytesAvailable > 0) { bytesRead = this._rb.pop(this._eventSizeArray); bytesAvailable -= bytesRead; const eventSizeBytes = this._eventSizeView.getUint32(0); const eventBytes = new Uint8Array(this._eventBytes, 0, eventSizeBytes - 4); bytesRead = this._rb.pop(eventBytes); bytesAvailable -= bytesRead; const decodedEvent = this._decode(); if (decodedEvent) events.push(decodedEvent); } return events; } /** * In case parameter set changes, update the internal mappings. * May result in some invalid automation events, which will be * ignored. Note that this must be called on all corresponding * WamEventRingBuffers on both threads. * @param {string[]} parameterIds */ setParameterIds(parameterIds) { this._encodeParameterId = {}; this._decodeParameterId = {}; parameterIds.forEach((parameterId) => { let parameterCode = -1 if (parameterId in this._parameterCodes) parameterCode = this._parameterCodes[parameterId]; else { parameterCode = this._generateParameterCode(); this._parameterCodes[parameterId] = parameterCode; } this._encodeParameterId[parameterId] = parameterCode; this._decodeParameterId[parameterCode] = parameterId; }); } /** * Generates a numeric parameter code in a range suitable for * encoding as uint16. * * @returns {number} */ _generateParameterCode() { if (this._parameterCode > 65535) throw Error('Too many parameters have been registered!'); return this._parameterCode++; } } if (audioWorkletGlobalScope.AudioWorkletProcessor) { /** @type {WamSDKBaseModuleScope} */ const ModuleScope = audioWorkletGlobalScope.webAudioModules.getModuleScope(moduleId); if (!ModuleScope.WamEventRingBuffer) ModuleScope.WamEventRingBuffer = WamEventRingBuffer; } return WamEventRingBuffer; }; export default getWamEventRingBuffer;