236 lines
7.0 KiB
JavaScript
236 lines
7.0 KiB
JavaScript
/**
|
|
* @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<string, Float32Array>} _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)
|