|
- // ring-player-processor.js — AudioWorklet processor for LiveListenWS
- // Runs on the audio rendering thread, immune to main-thread blocking.
-
- class RingPlayerProcessor extends AudioWorkletProcessor {
- constructor(options) {
- super();
- const ch = options.processorOptions?.channels || 1;
- this._channels = ch;
- // 500ms ring buffer at sampleRate
- this._ringSize = Math.ceil(sampleRate * ch * 0.5);
- this._ring = new Float32Array(this._ringSize);
- this._writePos = 0;
- this._readPos = 0;
- this._started = false;
- this._fadeGain = 1.0;
- this._startThreshold = Math.ceil(sampleRate * ch * 0.2); // 200ms
-
- this.port.onmessage = (e) => {
- if (e.data.type === 'pcm') {
- this._pushSamples(e.data.samples);
- }
- };
- }
-
- _available() {
- return (this._writePos - this._readPos + this._ringSize) % this._ringSize;
- }
-
- _pushSamples(float32arr) {
- const ring = this._ring;
- const size = this._ringSize;
- const n = float32arr.length;
-
- // Overrun: advance read cursor to make room
- const used = this._available();
- const free = size - used - 1;
- if (n > free) {
- this._readPos = (this._readPos + (n - free)) % size;
- }
-
- let w = this._writePos;
- // Fast path: contiguous write
- if (w + n <= size) {
- ring.set(float32arr, w);
- w += n;
- if (w >= size) w = 0;
- } else {
- // Wrap around
- const first = size - w;
- ring.set(float32arr.subarray(0, first), w);
- ring.set(float32arr.subarray(first), 0);
- w = n - first;
- }
- this._writePos = w;
-
- if (!this._started && this._available() >= this._startThreshold) {
- this._started = true;
- }
- }
-
- process(inputs, outputs, parameters) {
- const output = outputs[0];
- const outLen = output[0]?.length || 128;
- const ch = this._channels;
- const ring = this._ring;
- const size = this._ringSize;
-
- if (!this._started) {
- for (let c = 0; c < output.length; c++) output[c].fill(0);
- return true;
- }
-
- const need = outLen * ch;
- const avail = this._available();
-
- if (avail < need) {
- // Underrun: play what we have with fade-out, fill rest with silence
- const have = avail;
- const haveFrames = Math.floor(have / ch);
- const fadeLen = Math.min(64, haveFrames);
- const fadeStart = haveFrames - fadeLen;
- let r = this._readPos;
-
- for (let i = 0; i < haveFrames; i++) {
- let env = this._fadeGain;
- if (i >= fadeStart) {
- env *= 1.0 - (i - fadeStart) / fadeLen;
- }
- for (let c = 0; c < ch; c++) {
- if (c < output.length) {
- output[c][i] = ring[r] * env;
- }
- r = (r + 1) % size;
- }
- }
- this._readPos = r;
-
- // Silence the rest
- for (let i = haveFrames; i < outLen; i++) {
- for (let c = 0; c < output.length; c++) output[c][i] = 0;
- }
- this._fadeGain = 0;
- return true;
- }
-
- // Normal path
- let r = this._readPos;
- const fadeInLen = (this._fadeGain < 1.0) ? Math.min(64, outLen) : 0;
-
- for (let i = 0; i < outLen; i++) {
- let env = 1.0;
- if (i < fadeInLen) {
- env = this._fadeGain + (1.0 - this._fadeGain) * (i / fadeInLen);
- }
- for (let c = 0; c < ch; c++) {
- if (c < output.length) {
- output[c][i] = ring[r] * env;
- }
- r = (r + 1) % size;
- }
- }
- this._readPos = r;
- this._fadeGain = 1.0;
- return true;
- }
- }
-
- registerProcessor('ring-player-processor', RingPlayerProcessor);
|