Files
wehub-resource-sync 8f10353f0c
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
chore: import upstream snapshot with attribution
2026-07-13 12:32:09 +08:00

161 lines
4.8 KiB
JavaScript

'use strict';
// SoundTouch WSOLA time-stretcher - AudioWorkletProcessor
// Adapted from SoundTouch C++ by Olli Parviainen. MIT License.
// Self-contained; no imports required.
const SEQUENCE_MS = 82;
const SEEK_MS = 28;
const OVERLAP_MS = 12;
class FloatFifo {
constructor() {
this._d = new Float32Array(65536);
this._r = 0;
this._w = 0;
}
get avail() { return this._w - this._r; }
clear() { this._r = this._w = 0; }
peek(offset) { return this._d[this._r + offset]; }
consume(n) { this._r = Math.min(this._r + n, this._w); }
shift(dst, dstOff, n) {
n = Math.min(n, this.avail);
for (let i = 0; i < n; i++) dst[dstOff + i] = this._d[this._r++];
return n;
}
push(src, srcOff, n) {
this._ensureRoom(n);
for (let i = 0; i < n; i++) this._d[this._w++] = src[srcOff + i];
}
_compact() {
const av = this.avail;
this._d.copyWithin(0, this._r, this._w);
this._r = 0;
this._w = av;
}
_ensureRoom(n) {
if (this._r > 32768) this._compact();
if (this._w + n > this._d.length) {
this._compact();
if (this._w + n > this._d.length) {
const nd = new Float32Array(Math.max(this._d.length * 2, this._w + n + 4096));
nd.set(this._d.subarray(0, this._w));
this._d = nd;
}
}
}
}
function findBestOffset(ref, refLen, fifo, seekLen) {
let bestOff = 0, bestCorr = -Infinity;
for (let off = 0; off < seekLen; off++) {
let corr = 0;
for (let i = 0; i < refLen; i++) corr += ref[i] * fifo.peek(off + i);
if (corr > bestCorr) { bestCorr = corr; bestOff = off; }
}
return bestOff;
}
class SoundTouchProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{
name: 'tempo',
defaultValue: 1.0,
minValue: 0.25,
maxValue: 4.0,
automationRate: 'k-rate',
}];
}
constructor() {
super();
const sr = sampleRate;
this._ovLen = Math.round(OVERLAP_MS * sr / 1000);
this._seekLen = Math.round(SEEK_MS * sr / 1000);
this._seqLen = Math.round(SEQUENCE_MS * sr / 1000);
this._midLen = this._seqLen - 2 * this._ovLen;
this._inL = new FloatFifo();
this._inR = new FloatFifo();
this._outL = new FloatFifo();
this._outR = new FloatFifo();
// Overlap carry-over buffers (crossfade region saved from previous sequence)
this._carryL = new Float32Array(this._ovLen);
this._carryR = new Float32Array(this._ovLen);
// Pre-allocated temp output (seqLen - ovLen samples per sequence max)
const outPerSeq = this._ovLen + this._midLen;
this._tmpL = new Float32Array(outPerSeq);
this._tmpR = new Float32Array(outPerSeq);
}
process(inputs, outputs, parameters) {
const inp = inputs[0];
const outp = outputs[0];
const frames = 128;
const tempo = parameters.tempo[0];
const inL = inp?.[0] || new Float32Array(frames);
const inR = inp?.[1] || inL;
const outL = outp[0];
const outR = outp[1] || outL;
// Passthrough at 1.0 - zero DSP cost
if (Math.abs(tempo - 1.0) < 0.001) {
outL.set(inL);
if (outR !== outL) outR.set(inR);
return true;
}
this._inL.push(inL, 0, frames);
this._inR.push(inR, 0, frames);
const needed = this._ovLen + this._seekLen + this._seqLen;
while (this._inL.avail >= needed) this._processSeq(tempo);
const got = this._outL.shift(outL, 0, frames);
this._outR.shift(outR, 0, frames);
for (let i = got; i < frames; i++) { outL[i] = 0; if (outR !== outL) outR[i] = 0; }
return true;
}
_processSeq(tempo) {
const ovLen = this._ovLen;
const midLen = this._midLen;
const seqLen = this._seqLen;
const outLen = ovLen + midLen;
const bestOff = findBestOffset(this._carryL, ovLen, this._inL, this._seekLen);
// Crossfade carry-over with the new sequence start
for (let i = 0; i < ovLen; i++) {
const w = i / ovLen;
this._tmpL[i] = this._carryL[i] * (1 - w) + this._inL.peek(bestOff + i) * w;
this._tmpR[i] = this._carryR[i] * (1 - w) + this._inR.peek(bestOff + i) * w;
}
// Copy middle section verbatim
for (let i = 0; i < midLen; i++) {
this._tmpL[ovLen + i] = this._inL.peek(bestOff + ovLen + i);
this._tmpR[ovLen + i] = this._inR.peek(bestOff + ovLen + i);
}
// Save last ovLen samples as new carry-over for next crossfade
for (let i = 0; i < ovLen; i++) {
this._carryL[i] = this._inL.peek(bestOff + ovLen + midLen + i);
this._carryR[i] = this._inR.peek(bestOff + ovLen + midLen + i);
}
this._outL.push(this._tmpL, 0, outLen);
this._outR.push(this._tmpR, 0, outLen);
// Advance input: tempo controls how many input samples map to one output sequence
const advance = Math.round((seqLen - ovLen) * tempo) + bestOff;
this._inL.consume(advance);
this._inR.consume(advance);
}
}
registerProcessor('soundtouch-processor', SoundTouchProcessor);