/** * @typedef {Object} AppendDeltaMessage * @property {"appendDelta"} type * @property {ResponseAudioDelta} delta */ /** * @typedef {Object} DeleteItemMessage * @property {"deleteItem"} type * @property {string} item_id */ /** * @typedef {Object} ClearMessage * @property {"clear"} type */ /** * @typedef {Object} SetSourceRateMessage * @property {"setSourceRate"} type * @property {number} hz */ /** * @typedef {Object} NowPlayingMessage * @property {"nowPlaying"} type * @property {string|null} item_id * @property {number} played_ms */ /** * @typedef {Object} MuteMessage * @property {"mute"} type */ /** * @typedef {Object} UnmuteMessage * @property {"unmute"} type */ /** * @typedef {AppendDeltaMessage | DeleteItemMessage | ClearMessage | SetSourceRateMessage | MuteMessage | UnmuteMessage} PlaybackMessage */ /** * @typedef {Object} Chunk * @property {string} item_id * @property {Int16Array} data * @property {number} off */ class AudioPlaybackWorklet extends AudioWorkletProcessor { constructor() { super() /** @type {number} */ this.srcRate = 24000 /** @type {number} */ this.dstRate = sampleRate /** @type {number} */ this.step = this.srcRate / this.dstRate /** @type {Chunk[]} */ this.queue = [] /** @type {Chunk|null} */ this.cur = null /** @type {number} */ this.hold = 0 /** @type {number} */ this.phase = 0 /** @type {number|undefined} */ this._x0 = undefined /** @type {number|undefined} */ this._x1 = undefined /** @type {string|null} */ this._x0ItemId = null /** @type {string|null} */ this._x1ItemId = null /** @type {string|null} */ this._nextItemId = null /** @type {string|null} */ this.nowItemId = null /** @type {number} */ this.nowItemSamples = 0 /** @type {number} */ this._notifyFrames = 0 /** @type {boolean} */ this.muted = false this.port.onmessage = (e) => this._onMessage(/** @type {PlaybackMessage} */ (e.data)) } /** @param {PlaybackMessage} msg */ _onMessage(msg) { if (!msg || !msg.type) return if (msg.type === "appendDelta" && msg.delta && msg.delta.pcmInt16 instanceof Int16Array) { this.queue.push({ item_id: msg.delta.item_id, data: msg.delta.pcmInt16, off: 0 }) return } if (msg.type === "deleteItem") { const id = msg.item_id this.queue = this.queue.filter((ch) => ch.item_id !== id) if (this.cur && this.cur.item_id === id) { this.cur = null this.hold = 0 } if (this.nowItemId === id) { this.nowItemId = null this.nowItemSamples = 0 this._postNowPlaying() } return } if (msg.type === "clear") { this.queue.length = 0 this.cur = null this.hold = 0 this.phase = 0 this._x0 = undefined this._x1 = undefined this._x0ItemId = null this._x1ItemId = null this._nextItemId = null this.nowItemId = null this.nowItemSamples = 0 this._notifyFrames = 0 this._postNowPlaying() return } if (msg.type === "setSourceRate" && Number.isFinite(msg.hz) && msg.hz > 0) { this.srcRate = msg.hz | 0 this.step = this.srcRate / this.dstRate return } if (msg.type === "mute") { this.muted = true return } if (msg.type === "unmute") { this.muted = false return } } /** @returns {boolean} */ _ensureCurrent() { if (this.cur == null) { if (this.queue.length === 0) return false this.cur = this.queue.shift() || null if (this.cur == null) return false } return true } /** @returns {number} */ _nextInt16() { for (;;) { if (!this._ensureCurrent()) { this._nextItemId = null this.hold = 0 return 0 } const d = this.cur.data const o = this.cur.off | 0 if (o < d.length) { const s = d[o] this.cur.off = o + 1 this.hold = s this._nextItemId = this.cur.item_id return s } this.cur = null } } _postNowPlaying() { /** @type {NowPlayingMessage} */ const m = { type: "nowPlaying", item_id: this.nowItemId, played_ms: Math.max(0, Math.floor((this.nowItemSamples * 1000) / this.srcRate)), } this.port.postMessage(m) } /** * @param {Float32Array[][]} _inputs * @param {Float32Array[][]} outputs * @param {Record} _parameters * @returns {boolean} */ process(_inputs, outputs, _parameters) { const out = outputs[0] if (!out || out.length === 0) return true const ch0 = out[0] const N = ch0.length if (this._x1 === undefined) { this._x1 = this._nextInt16() this._x1ItemId = this._nextItemId this._x0 = this._x1 this._x0ItemId = this._x1ItemId this.phase = 0 this.nowItemId = this._x0ItemId this.nowItemSamples = 0 this._postNowPlaying() } const common = () => { this.phase += this.step while (this.phase >= 1) { this.phase -= 1 this._x0 = this._x1 this._x0ItemId = this._x1ItemId this._x1 = this._nextInt16() this._x1ItemId = this._nextItemId if (this.nowItemId !== this._x0ItemId) { this.nowItemId = this._x0ItemId this.nowItemSamples = 0 this._postNowPlaying() } if (this.nowItemId) this.nowItemSamples += 1 } } if (this.muted) { for (let i = 0; i < N; i++) { ch0[i] = 0 for (let c = 1; c < out.length; c++) out[c][i] = 0 common() } } else { for (let i = 0; i < N; i++) { const yI16 = /** @type {number} */ (this._x0) + this.phase * /** @type {number} */ (this._x1 - /** @type {number} */ (this._x0)) const yF32 = Math.max(-1, Math.min(1, yI16 / 32768)) ch0[i] = yF32 for (let c = 1; c < out.length; c++) out[c][i] = yF32 common() } } this._notifyFrames += N if (this._notifyFrames >= this.dstRate / 20) { this._postNowPlaying() this._notifyFrames = 0 } return true } } registerProcessor("audio-playback-worklet", AudioPlaybackWorklet)