From 99fc4e003d99a352e75e350b7986b03410a83e04 Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Wed, 30 Sep 2026 07:23:58 +0100
Subject: [PATCH 1/7] fix(engine): keep chunk sources fed during slowed
playback
The lookahead gate compared _scheduledTo, in source seconds, against the
output playhead. With SoundTouch stretching, sources play at 1x and the
worklet buffers the surplus, so below 1x the output playhead lags what
the sources have consumed. The gate overstated its margin by (1 - rate)
seconds per second until the sources ran dry and the next chunk was
scheduled late and skipped. At 0.75x that happened about fifty seconds in.
The gate now measures against ctxTimeToSourceTime, which already follows
the source rate, so it also holds in the tape-effect fallback.
Reported and diagnosed by @goermezer in #701.
Closes #722
---
static/js/chunkedAudioEngine.js | 8 +-
tests/js/slowed-scheduling.test.mjs | 198 ++++++++++++++++++++++++++++
2 files changed, 205 insertions(+), 1 deletion(-)
create mode 100644 tests/js/slowed-scheduling.test.mjs
diff --git a/static/js/chunkedAudioEngine.js b/static/js/chunkedAudioEngine.js
index 4a03d45d..3438208f 100644
--- a/static/js/chunkedAudioEngine.js
+++ b/static/js/chunkedAudioEngine.js
@@ -603,7 +603,13 @@ export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {
// loop.start when the playhead crosses it (bounded by one rAF frame).
const limit = (loop.enabled && loop.end > loop.start) ? loop.end : _duration;
if (_scheduledTo >= limit) return;
- if (_scheduledTo - _getCurrentTime() < LOOKAHEAD_SEC) {
+ // Measured against the sources, not the output playhead. With SoundTouch
+ // stretching, sources play at 1x and the worklet buffers the surplus, so
+ // below 1x the output playhead falls behind what the sources have used up.
+ // Gating on it overstated the margin by (1 - rate) seconds every second
+ // until the sources ran dry and the next chunk landed late and skipped
+ // (#722). In the tape-effect fallback the two clocks agree.
+ if (_scheduledTo - ctxTimeToSourceTime(ctx.currentTime) < LOOKAHEAD_SEC) {
_filling = true;
_scheduleNext().finally(() => { _filling = false; });
}
diff --git a/tests/js/slowed-scheduling.test.mjs b/tests/js/slowed-scheduling.test.mjs
new file mode 100644
index 00000000..3dd6553c
--- /dev/null
+++ b/tests/js/slowed-scheduling.test.mjs
@@ -0,0 +1,198 @@
+// Slowed playback must never let the chunk sources run dry (#722).
+//
+// With SoundTouch stretching, sources play at 1x and the worklet buffers what
+// it has not yet released, so below 1x the sources are used up faster than the
+// output playhead moves. The lookahead gate compared against the output
+// playhead, overstated its margin by (1 - rate) seconds every second, and at
+// 0.75x the sources ran dry about fifty seconds in.
+//
+// So this plays a two minute track against a fake clock, one tick every 100 ms,
+// and checks at every tick that the scheduled sources still reach past "now".
+// The bug needs minutes of playback to show, which the 6 second e2e fixture
+// cannot provide, hence a node test with a synthetic WAV.
+
+let passed = 0;
+let failed = 0;
+
+function check(name, condition, detail = '') {
+ if (condition) {
+ passed++;
+ console.log(`PASS ${name}`);
+ } else {
+ failed++;
+ console.log(`FAIL ${name}${detail ? ` -- ${detail}` : ''}`);
+ }
+}
+
+const RATE = 8000;
+const TRACK_SEC = 120;
+const PLAY_SEC = 100;
+const STEP_SEC = 0.1;
+
+class FakeParam {
+ constructor(value = 0) { this.value = value; }
+}
+
+class FakeNode {
+ constructor(ctx, type) {
+ this.ctx = ctx;
+ this.type = type;
+ ctx.nodes.push(this);
+ }
+
+ connect(destination) { return destination; }
+ disconnect() {}
+}
+
+class FakeGain extends FakeNode {
+ constructor(ctx) {
+ super(ctx, 'gain');
+ this.gain = {
+ value: 1,
+ setTargetAtTime: (value) => { this.gain.value = value; },
+ };
+ }
+}
+
+class FakeAnalyser extends FakeNode {
+ constructor(ctx) {
+ super(ctx, 'analyser');
+ this.fftSize = 1024;
+ }
+}
+
+// Records where each source starts and how much of its buffer it plays, which
+// is all the check below needs to know when the feed runs out.
+class FakeSource extends FakeNode {
+ constructor(ctx) {
+ super(ctx, 'source');
+ this.playbackRate = new FakeParam(1);
+ this.buffer = null;
+ }
+
+ start(when, offset = 0) {
+ const played = (this.buffer.duration - offset) / this.playbackRate.value;
+ this.ctx.fedUntil = Math.max(this.ctx.fedUntil, when + played);
+ }
+
+ stop() {}
+}
+
+class FakeContext {
+ constructor({ worklet }) {
+ this.nodes = [];
+ this.currentTime = 0;
+ this.sampleRate = 44100;
+ this.state = 'running';
+ this.fedUntil = 0;
+ this.destination = new FakeNode(this, 'destination');
+ this.audioWorklet = worklet ? { addModule: async () => {} } : null;
+ }
+
+ createGain() { return new FakeGain(this); }
+ createAnalyser() { return new FakeAnalyser(this); }
+ createBufferSource() { return new FakeSource(this); }
+ createBuffer(channels, length, sampleRate) {
+ const data = Array.from({ length: channels }, () => new Float32Array(length));
+ return { duration: length / sampleRate, getChannelData: (channel) => data[channel] };
+ }
+ async resume() {}
+ async close() {}
+ addEventListener() {}
+ removeEventListener() {}
+}
+
+class FakeWorkletNode extends FakeNode {
+ constructor(ctx) {
+ super(ctx, 'worklet');
+ this.parameters = new Map([['tempo', new FakeParam(1)]]);
+ this.port = { postMessage() {} };
+ queueMicrotask(() => this.port.onmessage?.({ data: { type: 'latency', frames: 6541 } }));
+ }
+}
+
+let pendingTick = null;
+globalThis.window = { AudioContext: FakeContext };
+globalThis.AudioWorkletNode = FakeWorkletNode;
+globalThis.requestAnimationFrame = (cb) => { pendingTick = cb; return 1; };
+globalThis.cancelAnimationFrame = () => { pendingTick = null; };
+
+// Mono 16-bit silence. Only the length matters, and a low rate keeps it small.
+function wavBytes(seconds) {
+ const dataBytes = seconds * RATE * 2;
+ const bytes = new ArrayBuffer(44 + dataBytes);
+ const view = new DataView(bytes);
+ const text = (offset, value) => {
+ for (let i = 0; i < value.length; i++) view.setUint8(offset + i, value.charCodeAt(i));
+ };
+ text(0, 'RIFF');
+ view.setUint32(4, bytes.byteLength - 8, true);
+ text(8, 'WAVE');
+ text(12, 'fmt ');
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, 1, true);
+ view.setUint32(24, RATE, true);
+ view.setUint32(28, RATE * 2, true);
+ view.setUint16(32, 2, true);
+ view.setUint16(34, 16, true);
+ text(36, 'data');
+ view.setUint32(40, dataBytes, true);
+ return new Uint8Array(bytes);
+}
+
+const WAV = wavBytes(TRACK_SEC);
+globalThis.fetch = async (_url, options = {}) => {
+ const match = /bytes=(\d+)-(\d+)/.exec(options.headers?.Range || '');
+ const start = match ? Number(match[1]) : 0;
+ const end = match ? Math.min(Number(match[2]), WAV.length - 1) : WAV.length - 1;
+ const slice = WAV.slice(start, end + 1);
+ return {
+ ok: true,
+ status: match ? 206 : 200,
+ headers: { get: (name) => name === 'Content-Range' ? `bytes ${start}-${end}/${WAV.length}` : null },
+ arrayBuffer: async () => slice.buffer,
+ };
+};
+
+const { createChunkedAudioEngine } = await import('../../static/js/chunkedAudioEngine.js');
+
+// Enough turns of the event loop for a chunk fetch and decode to settle.
+const settle = async () => {
+ for (let i = 0; i < 5; i++) await new Promise((resolve) => setImmediate(resolve));
+};
+
+async function verify(label, { worklet, rate }) {
+ const ctx = new FakeContext({ worklet });
+ const engine = createChunkedAudioEngine([{ name: 'vocals', url: '/vocals.wav' }], { context: ctx });
+ check(`${label}: engine becomes ready`, await engine.ready);
+ check(
+ `${label}: runs on the expected path`,
+ engine.supportsPitchShift() === worklet,
+ );
+ engine.setPlaybackRate(rate);
+ await engine.play();
+
+ let dryAt = null;
+ while (ctx.currentTime < PLAY_SEC) {
+ ctx.currentTime += STEP_SEC;
+ const tick = pendingTick;
+ pendingTick = null;
+ tick?.();
+ await settle();
+ if (dryAt === null && ctx.fedUntil < ctx.currentTime) dryAt = ctx.currentTime;
+ }
+ check(
+ `${label}: sources never run dry over ${PLAY_SEC} s`,
+ dryAt === null,
+ `ran dry at ${dryAt?.toFixed(1)} s`,
+ );
+ engine.destroy();
+}
+
+await verify('SoundTouch at 1x', { worklet: true, rate: 1 });
+await verify('SoundTouch at 0.75x', { worklet: true, rate: 0.75 });
+await verify('tape effect at 0.75x', { worklet: false, rate: 0.75 });
+
+console.log(`\n${passed}/${passed + failed} checks passed`);
+process.exit(failed === 0 ? 0 : 1);
From 8349df41db275b0e7c7b073863336932949ed7fe Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Wed, 30 Sep 2026 08:48:45 +0100
Subject: [PATCH 2/7] fix(engine): stop the tempo stage replaying drum attacks
below 1x
Below 1x each WSOLA sequence plays 70 ms of input but advances only
70 * tempo, so consecutive sequences overlap and an attack in the
overlap is heard twice. At 0.75x kicks came out as a ~50 ms flam.
The shared tempo stage now marks attacks on the sample-to-sample
difference, and when the next sequence would replay one it carries
straight on from where the last sequence ended instead. That splices
identical samples, so it is seamless. The extra input is borrowed, at
most 100 ms, and paid back by later sequences stretching slightly more.
Only the shared tempo stage does this. The pitch stages are aligned
sample for sample with the unpitched drums, and borrowing there would
pull the band off the kit.
Measured on a full mix at 0.75x: 93 attacks in, 104 out before, 93 out
after.
Closes #728
---
static/vendor/soundtouch-processor.js | 154 +++++++++++++++++++++++++-
tests/js/tempo-attacks.test.mjs | 148 +++++++++++++++++++++++++
2 files changed, 296 insertions(+), 6 deletions(-)
create mode 100644 tests/js/tempo-attacks.test.mjs
diff --git a/static/vendor/soundtouch-processor.js b/static/vendor/soundtouch-processor.js
index 52eb564a..689c2b21 100644
--- a/static/vendor/soundtouch-processor.js
+++ b/static/vendor/soundtouch-processor.js
@@ -173,6 +173,103 @@ class Biquad {
// actually land, and four biquads on one bus cost nothing worth counting.
const BUTTERWORTH_Q8 = [0.50979558, 0.60134489, 0.89997622, 2.56291545];
+// Attacks the tempo stage must not play twice (#728).
+//
+// Below 1x a sequence plays 70 ms of input but moves on by only 70 * tempo, so
+// consecutive sequences overlap and whatever sits in the overlap is heard
+// twice. Sustained sound hides that. A kick does not: at 0.75x it came out as a
+// flam about 50 ms long. When the next sequence would replay an attack, the
+// chain instead carries straight on from where the last sequence stopped, which
+// splices identical samples and so is seamless, and borrows the time that costs.
+// Later sequences stretch a little harder to pay it back.
+//
+// Nothing is split or filtered, so unlike the attack path this file once had,
+// the output is still built only from whole grains of the input.
+const ATTACK_FRAME_MS = 2; // energy frame the detector works in
+const ATTACK_HISTORY_MS = 20; // what an attack is measured against
+const ATTACK_RATIO = 4; // 6 dB above the recent level
+const ATTACK_FLOOR = 1e-6; // ignore noise rising out of near silence
+const ATTACK_GAP_MS = 30; // one attack per kick, not one per frame of it
+// An attack is marked this far ahead of the frame that crossed the threshold,
+// so the few milliseconds of rise before it count as part of the attack.
+const ATTACK_LEAD_MS = 4;
+// Borrowed input the chain may be ahead of its nominal clock by. The whole
+// mix, click included, goes through this one stage, so borrowing moves
+// everything together and the only effect is the playhead trailing the audio,
+// by this at most, until it is paid back.
+const MAX_BORROW_MS = 100;
+// Share of a sequence's advance used to pay the loan back, so repayment is
+// spread thinly rather than stretching one sequence hard.
+const REPAY_SHARE = 0.25;
+
+/**
+ * Marks attacks in a sample stream, by absolute sample position.
+ *
+ * Measured on the sample-to-sample difference rather than on the signal: the
+ * difference is dominated by high frequencies, which is what an attack is made
+ * of, while a bass line or a pad barely moves it. Plain energy missed most of
+ * the attacks in a full mix and placed the rest 8 to 30 ms late.
+ *
+ * Positions are absolute so they survive the FIFO compacting under them.
+ */
+class AttackTracker {
+ constructor(sr) {
+ this.frameLen = Math.max(1, Math.round(ATTACK_FRAME_MS * sr / 1000));
+ this.alpha = ATTACK_FRAME_MS / ATTACK_HISTORY_MS;
+ this.gap = Math.round(ATTACK_GAP_MS * sr / 1000);
+ this.lead = Math.round(ATTACK_LEAD_MS * sr / 1000);
+ this.clear();
+ }
+
+ clear() {
+ this.written = 0; // absolute position of the next sample pushed
+ this.frameAcc = 0;
+ this.frameFill = 0;
+ this.prevL = 0;
+ this.prevR = 0;
+ this.level = 0;
+ this.lastAttack = -Infinity;
+ this.attacks = [];
+ }
+
+ push(l, r, n) {
+ for (let i = 0; i < n; i++) {
+ const dl = l[i] - this.prevL, dr = r[i] - this.prevR;
+ this.prevL = l[i];
+ this.prevR = r[i];
+ this.frameAcc += dl * dl + dr * dr;
+ if (++this.frameFill < this.frameLen) continue;
+ const e = this.frameAcc / this.frameLen;
+ const start = this.written + i + 1 - this.frameLen - this.lead;
+ if (e > ATTACK_RATIO * this.level && e > ATTACK_FLOOR && start - this.lastAttack >= this.gap) {
+ this.attacks.push(start);
+ this.lastAttack = start;
+ }
+ this.level += this.alpha * (e - this.level);
+ this.frameAcc = 0;
+ this.frameFill = 0;
+ }
+ this.written += n;
+ }
+
+ /** True if an attack starts in [from, to), absolute positions. */
+ any(from, to) {
+ // Sorted, and below 1x the unread backlog, and so this list, keeps growing.
+ for (const a of this.attacks) {
+ if (a >= to) return false;
+ if (a >= from) return true;
+ }
+ return false;
+ }
+
+ /** Forget attacks the chain has consumed past. */
+ drop(before) {
+ let k = 0;
+ while (k < this.attacks.length && this.attacks[k] < before) k++;
+ if (k) this.attacks.splice(0, k);
+ }
+}
+
/**
* One WSOLA time-stretch chain: stereo in, stereo out, its own buffers.
*
@@ -183,7 +280,13 @@ const BUTTERWORTH_Q8 = [0.50979558, 0.60134489, 0.89997622, 2.56291545];
* folding, only attenuate whatever the fold produced.
*/
class Wsola {
- constructor(sr) {
+ /**
+ * `keepAttacks` must stay off for the pitch stages. Borrowing moves a chain's
+ * clock, and a pitch chain is aligned sample for sample with the unpitched
+ * drums beside it, so there it would pull the band away from the kit. It is
+ * safe on the shared tempo stage only because everything shares that clock.
+ */
+ constructor(sr, { keepAttacks = false } = {}) {
this.ovLen = Math.round(OVERLAP_MS * sr / 1000);
this.seekLen = Math.round(SEEK_MS * sr / 1000);
this.seqLen = Math.round(SEQUENCE_MS * sr / 1000);
@@ -209,6 +312,12 @@ class Wsola {
this._primed = false;
this.postFilter = null;
+
+ this._attacks = keepAttacks ? new AttackTracker(sr) : null;
+ this._maxBorrow = Math.round(MAX_BORROW_MS * sr / 1000);
+ this._head = 0; // absolute position of inL's read head
+ this._borrowed = 0; // input consumed ahead of the nominal clock
+ this._continue = false; // next sequence starts exactly where this one ended
}
clear() {
@@ -217,11 +326,23 @@ class Wsola {
this._carryL.fill(0); this._carryR.fill(0);
this._advanceRem = 0;
this._primed = false;
+ this._attacks?.clear();
+ this._head = 0;
+ this._borrowed = 0;
+ this._continue = false;
}
push(l, r, n) {
this.inL.push(l, 0, n);
this.inR.push(r, 0, n);
+ this._attacks?.push(l, r, n);
+ }
+
+ _consume(n) {
+ this.inL.consume(n);
+ this.inR.consume(n);
+ this._head += n;
+ this._attacks?.drop(this._head);
}
/** Run sequences until `want` output samples are queued, or input runs out. */
@@ -236,7 +357,10 @@ class Wsola {
_sequence(tempo) {
const ovLen = this.ovLen, midLen = this.midLen, seqLen = this.seqLen;
const outLen = ovLen + midLen;
- const bestOff = findBestOffset(this._carryL, ovLen, this.inL, this.seekLen);
+ // After carrying straight on, the carry is the very samples at the head,
+ // so offset 0 splices them onto themselves. A search could land elsewhere.
+ const bestOff = this._continue ? 0 : findBestOffset(this._carryL, ovLen, this.inL, this.seekLen);
+ this._continue = false;
for (let i = 0; i < ovLen; i++) {
const w = i / ovLen;
@@ -257,14 +381,32 @@ class Wsola {
this.outL.push(this._tmpL, 0, outLen);
this.outR.push(this._tmpR, 0, outLen);
- const exact = (seqLen - ovLen) * tempo + this._advanceRem;
+ const nominal = (seqLen - ovLen) * tempo;
+ const repay = this._attacks ? Math.min(this._borrowed, nominal * REPAY_SHARE) : 0;
+ const exact = nominal - repay + this._advanceRem;
const whole = Math.floor(exact);
+
+ // Input this sequence played in full ends at `played`; the carry after it
+ // is faded out under the next sequence's head. The next sequence starts no
+ // earlier than `whole`, so an attack in between would be heard twice.
+ if (this._attacks && tempo < 1) {
+ const played = bestOff + outLen;
+ const cost = played - nominal;
+ if (this._borrowed + cost <= this._maxBorrow
+ && this._attacks.any(this._head + whole, this._head + played + ovLen)) {
+ this._borrowed += cost;
+ this._continue = true;
+ this._consume(played);
+ return;
+ }
+ }
+
+ this._borrowed -= repay;
this._advanceRem = exact - whole;
// The similarity search is local to this sequence. Folding bestOff into
// the nominal advance makes content-dependent offsets accumulate into a
// clock error, which is especially audible against unpitched drums.
- this.inL.consume(whole);
- this.inR.consume(whole);
+ this._consume(whole);
}
}
@@ -459,7 +601,7 @@ class SoundTouchProcessor extends AudioWorkletProcessor {
constructor() {
super();
- this._tempo = new Wsola(sampleRate);
+ this._tempo = new Wsola(sampleRate, { keepAttacks: true });
// Silence every bus is primed with, before its own alignment pad, so they
// share one clock and each chain can produce from its first block.
this._commonPrime = this._tempo.needed + PRIME_CUSHION;
diff --git a/tests/js/tempo-attacks.test.mjs b/tests/js/tempo-attacks.test.mjs
new file mode 100644
index 00000000..cc6c1b14
--- /dev/null
+++ b/tests/js/tempo-attacks.test.mjs
@@ -0,0 +1,148 @@
+// Kicks must not come out doubled at slowed speeds (#728).
+//
+// Below 1x consecutive WSOLA sequences overlap, and an attack in the overlap is
+// played twice. At 0.75x a kick came out as a flam roughly 50 ms long. The
+// tempo stage now carries straight on past an attack instead of replaying it,
+// and borrows the time that costs.
+//
+// Ground truth comes from the signal, not from a detector: a synthetic track of
+// 40 kicks over a sustained chord, so the output must hold exactly 40 attacks.
+// The processor is driven exactly as an AudioWorklet would drive it.
+//
+// Run: node tests/js/tempo-attacks.test.mjs
+
+import { readFileSync } from 'node:fs';
+
+const SR = 44100;
+const BLOCK = 128;
+const TEMPO = 0.75;
+const KICKS = 40;
+const KICK_EVERY = 0.5;
+const INPUT_COUNT = 13;
+const ZERO_INPUT = 6;
+// The processor's own MAX_BORROW_MS, in output time, plus a few milliseconds
+// of measurement slack.
+const MAX_SHIFT = 0.1 / TEMPO + 0.005;
+
+let passed = 0;
+let failed = 0;
+
+function check(name, condition, detail = '') {
+ if (condition) {
+ passed++;
+ console.log(`PASS ${name}`);
+ } else {
+ failed++;
+ console.log(`FAIL ${name}${detail ? ` -- ${detail}` : ''}`);
+ }
+}
+
+const SRC = readFileSync(new URL('../../static/vendor/soundtouch-processor.js', import.meta.url), 'utf8');
+
+function load() {
+ let Cls = null;
+ new Function('sampleRate', 'AudioWorkletProcessor', 'registerProcessor', SRC)(
+ SR,
+ class { constructor() { this.port = { onmessage: null, postMessage() {} }; } },
+ (_name, c) => { Cls = c; },
+ );
+ return Cls;
+}
+
+/** A kick is a 3 kHz click and a 60 Hz thump, over an A minor chord that never stops. */
+function track() {
+ const n = Math.round((KICKS * KICK_EVERY + 0.5) * SR);
+ const x = new Float32Array(n);
+ for (let i = 0; i < n; i++) {
+ const t = i / SR;
+ x[i] = 0.08 * (Math.sin(2 * Math.PI * 220 * t) + Math.sin(2 * Math.PI * 261.63 * t) + Math.sin(2 * Math.PI * 329.63 * t));
+ }
+ for (let k = 0; k < KICKS; k++) {
+ const at = Math.round((0.25 + k * KICK_EVERY) * SR);
+ for (let i = 0; i < 0.15 * SR && at + i < n; i++) {
+ const t = i / SR;
+ const click = t < 0.003 ? 0.5 * Math.sin(2 * Math.PI * 3000 * t) * (1 - t / 0.003) : 0;
+ x[at + i] += click + 0.6 * Math.sin(2 * Math.PI * 60 * t) * Math.exp(-t / 0.04);
+ }
+ }
+ return x;
+}
+
+function stretch(x) {
+ const Processor = load();
+ const p = new Processor();
+ const total = Math.round(x.length / TEMPO) + SR;
+ const out = new Float32Array(Math.ceil(total / BLOCK) * BLOCK);
+ const params = { tempo: new Float32Array([TEMPO]) };
+ for (let pos = 0; pos < out.length; pos += BLOCK) {
+ const block = new Float32Array(BLOCK);
+ if (pos < x.length) block.set(x.subarray(pos, Math.min(pos + BLOCK, x.length)));
+ const inputs = Array.from({ length: INPUT_COUNT }, (_, k) => (k === ZERO_INPUT ? [block, block] : []));
+ const l = new Float32Array(BLOCK);
+ const r = new Float32Array(BLOCK);
+ p.process(inputs, [[l, r]], params);
+ out.set(l, pos);
+ }
+ return out;
+}
+
+/**
+ * Attack times in seconds: peaks of the click band's 1 ms envelope above a
+ * third of the loudest, at least 10 ms apart. The chord is too low to reach
+ * it, so each peak is a click, whether first played or replayed.
+ */
+function attacks(y) {
+ const hop = Math.round(SR * 0.001);
+ const env = [];
+ for (let i = hop; i + hop <= y.length; i += hop) {
+ let e = 0;
+ for (let j = 0; j < hop; j++) {
+ const d = y[i + j] - y[i + j - 1];
+ e += d * d;
+ }
+ env.push(e);
+ }
+ const top = Math.max(...env);
+ const out = [];
+ for (let f = 1; f + 1 < env.length; f++) {
+ if (env[f] < top / 3 || env[f] < env[f - 1] || env[f] < env[f + 1]) continue;
+ const t = (f + 1) * hop / SR;
+ if (out.length && t - out.at(-1) < 0.01) continue;
+ out.push(t);
+ }
+ return out;
+}
+
+const input = track();
+check('the probe sees every kick in the input', attacks(input).length === KICKS, `saw ${attacks(input).length}`);
+
+const found = attacks(stretch(input));
+check(
+ `every kick comes out once at ${TEMPO}x`,
+ found.length === KICKS,
+ `heard ${found.length} attacks for ${KICKS} kicks`,
+);
+
+// Borrowed time is paid back, so each kick lands within the loan of where an
+// exact clock puts it. The first kick fixes the stage's latency.
+const shifts = found.slice(0, KICKS).map((t, k) => t - found[0] - (k * KICK_EVERY) / TEMPO);
+const worst = Math.max(...shifts.map(Math.abs));
+check(
+ 'no kick drifts further than the borrow limit',
+ worst <= MAX_SHIFT,
+ `worst ${(worst * 1000).toFixed(1)} ms`,
+);
+check(
+ 'the last kick is back on the clock',
+ Math.abs(shifts.at(-1)) <= MAX_SHIFT,
+ `${(shifts.at(-1) * 1000).toFixed(1)} ms`,
+);
+
+// Borrowing moves a chain's clock. The pitch stages are aligned sample for
+// sample with the unpitched drums, so only the shared tempo stage may do it.
+const enabled = [...SRC.matchAll(/new Wsola\([^)]*keepAttacks: true/g)].length;
+const tempoStage = /this\._tempo = new Wsola\(sampleRate, \{ keepAttacks: true \}\)/.test(SRC);
+check('only the shared tempo stage keeps attacks', enabled === 1 && tempoStage, `enabled in ${enabled} places`);
+
+console.log(`\n${passed}/${passed + failed} checks passed`);
+process.exit(failed === 0 ? 0 : 1);
From 662f5cf83b26758feb9f79fb933188b6bbce1b04 Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Wed, 30 Sep 2026 10:15:29 +0100
Subject: [PATCH 3/7] feat(engine): stretch slowed playback with Signalsmith
Stretch
WSOLA slows audio by repeating overlapping 82 ms fragments, heard as an
echo on everything sustained. The shared tempo stage now uses the
Signalsmith Stretch WASM core (MIT, vendored from signalsmith-stretch
1.3.2) at 40 ms blocks, chosen by ear against 30, 60 and 120 ms.
- The core is its own worklet module, added before the processor. The
processor instantiates it asynchronously and swaps it in at the next
flush, never under audio already playing. If it does not load, WSOLA
stays the tempo stage.
- The processor reports which stage runs and its latency in two parts.
Latency is the priming plus the core's input side, divided by the
tempo, plus its output side; measured end to end it lands within 1 ms.
Both engines take it from one shared module, tempoStage.js, instead of
each hard-coding WSOLA's.
- script-src gains 'wasm-unsafe-eval', in the server CSP and the Tauri
CSP. It allows WebAssembly compilation only, not JS eval. The CSP test
now pins script-src to exactly 'self' and 'wasm-unsafe-eval'.
On 20 s of a full mix at 0.75x (55 attacks in): WSOLA 60 attacks out
with 8 close pairs, Signalsmith 61 with 2.
Closes #729
---
app/main.py | 6 +-
desktop/src-tauri/tauri.conf.json | 2 +-
static/js/audioEngine.js | 30 +++--
static/js/chunkedAudioEngine.js | 30 +++--
static/js/tempoStage.js | 58 +++++++++
static/vendor/signalsmith-stretch.js | 53 ++++++++
static/vendor/soundtouch-processor.js | 136 ++++++++++++++++++++-
tests/e2e/tempo-stage.spec.mjs | 42 +++++++
tests/js/pitch-shift.test.mjs | 2 +-
tests/js/signalsmith-stage.test.mjs | 166 ++++++++++++++++++++++++++
tests/js/tempo-attacks.test.mjs | 2 +-
tests/test_csp.py | 10 +-
12 files changed, 499 insertions(+), 38 deletions(-)
create mode 100644 static/js/tempoStage.js
create mode 100644 static/vendor/signalsmith-stretch.js
create mode 100644 tests/e2e/tempo-stage.spec.mjs
create mode 100644 tests/js/signalsmith-stage.test.mjs
diff --git a/app/main.py b/app/main.py
index eb050740..2035d665 100644
--- a/app/main.py
+++ b/app/main.py
@@ -915,7 +915,9 @@ def download_logs_zip() -> StreamingResponse:
# Content-Security-Policy. Defense-in-depth so an injected string in the webview
# can't run script (and, in the desktop app, reach the exposed Tauri IPC) — #171.
# script-src has no 'unsafe-inline'/'eval': all JS is same-origin modules and the
-# inline scripts/onclick were moved out. 'unsafe-inline' is allowed for *styles*
+# inline scripts/onclick were moved out. 'wasm-unsafe-eval' lets WebAssembly
+# compile, which the Signalsmith tempo stage in the audio worklet needs (#729);
+# it does not re-enable JS eval, new Function or string timers. 'unsafe-inline' is allowed for *styles*
# only (the UI sets many style attributes). Allowances:
# connect-src -> same-origin API/SSE, the GitHub update check, Tauri IPC,
# Wikidata/Wikipedia for the artist box the now-playing card
@@ -926,7 +928,7 @@ def download_logs_zip() -> StreamingResponse:
# style/font -> the Google Fonts
_CSP = (
"default-src 'self'; "
- "script-src 'self'; "
+ "script-src 'self' 'wasm-unsafe-eval'; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com data:; "
"img-src 'self' data: blob: https:; "
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index 5604ffd7..8ad86950 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -24,7 +24,7 @@
}
],
"security": {
- "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self' ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
+ "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self' ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
}
},
"bundle": {
diff --git a/static/js/audioEngine.js b/static/js/audioEngine.js
index 0100ac06..d2616ed4 100644
--- a/static/js/audioEngine.js
+++ b/static/js/audioEngine.js
@@ -24,6 +24,7 @@ import {
} from "./pitchBus.js";
import { createPlaybackContext } from "./audioContext.js";
import { createTickLoop } from "./tickLoop.js";
+import { loadStretchWorklet, pipelineLatencySeconds as stretchLatency } from "./tempoStage.js";
export function createAudioEngine(stems, { onTime, onEnded, context } = {}) {
// Mobile/iOS only starts audio from a context resumed inside a user gesture.
@@ -53,15 +54,22 @@ export function createAudioEngine(stems, { onTime, onEnded, context } = {}) {
// Reported by the processor, because deriving it here would mean keeping a
// copy of its buffering constants in sync by hand.
let _workletLatencyFrames = 0;
+ // Which tempo stage the processor is running, and its latency (#729).
+ let _tempoStage = null;
const _workletReady = (ctx.audioWorklet
- ? ctx.audioWorklet.addModule('/vendor/soundtouch-processor.js').then(() => {
+ ? loadStretchWorklet(ctx.audioWorklet).then(() => {
stNode = new AudioWorkletNode(ctx, 'soundtouch-processor', {
numberOfInputs: INPUT_COUNT,
numberOfOutputs: 1,
outputChannelCount: [2],
});
stNode.port.onmessage = (event) => {
- if (event?.data?.type === 'latency') _workletLatencyFrames = event.data.frames || 0;
+ const data = event?.data;
+ if (data?.type === 'latency') _workletLatencyFrames = data.frames || 0;
+ else if (data?.type === 'tempoStage') _tempoStage = data;
+ else if (data?.type === 'tempoStageFailed') {
+ console.warn('[tempoStage] Signalsmith core failed to start, keeping WSOLA:', data.message);
+ }
};
// The worklet loads asynchronously, so anything set before it arrived
// would otherwise be dropped. Re-apply the current value now.
@@ -187,12 +195,6 @@ export function createAudioEngine(stems, { onTime, onEnded, context } = {}) {
// (startCtxTime > ctx.currentTime), which would otherwise make this go
// negative -- the playhead must sit still at the start until the audio enters.
// A no-op for a normal start, where startCtxTime == the moment play() ran.
- const wsolaLatencySeconds = () => {
- const needed = Math.round(0.012 * ctx.sampleRate)
- + Math.round(0.028 * ctx.sampleRate)
- + Math.round(0.082 * ctx.sampleRate);
- return Math.floor(needed / 128) * 128 / ctx.sampleRate;
- };
const anyLaneTransposed = () => {
for (const t of tracks.values()) {
if (t.visualOnly) continue;
@@ -202,11 +204,13 @@ export function createAudioEngine(stems, { onTime, onEnded, context } = {}) {
};
const pipelineLatencySeconds = () => {
if (!stNode) return 0;
- // The pitch buses only buffer once something is actually transposed. Until
- // then the worklet hands its input straight back, with no delay to correct.
- const pitchLatency = anyLaneTransposed() ? _workletLatencyFrames / ctx.sampleRate : 0;
- const tempoStages = Math.abs(_playbackRate - 1) >= 1e-3 ? 1 : 0;
- return pitchLatency + tempoStages * wsolaLatencySeconds();
+ return stretchLatency({
+ workletFrames: _workletLatencyFrames,
+ stage: _tempoStage,
+ rate: _playbackRate,
+ transposed: anyLaneTransposed(),
+ sampleRate: ctx.sampleRate,
+ });
};
const now = () => playing
? Math.max(
diff --git a/static/js/chunkedAudioEngine.js b/static/js/chunkedAudioEngine.js
index 3438208f..a85475b0 100644
--- a/static/js/chunkedAudioEngine.js
+++ b/static/js/chunkedAudioEngine.js
@@ -190,6 +190,7 @@ import {
} from "./pitchBus.js";
import { createPlaybackContext } from "./audioContext.js";
import { createTickLoop } from "./tickLoop.js";
+import { loadStretchWorklet, pipelineLatencySeconds as stretchLatency } from "./tempoStage.js";
export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {}) {
const AC = window.AudioContext || window.webkitAudioContext;
@@ -226,15 +227,22 @@ export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {
// Reported by the processor, because deriving it here would mean keeping a
// copy of its buffering constants in sync by hand.
let _workletLatencyFrames = 0;
+ // Which tempo stage the processor is running, and its latency (#729).
+ let _tempoStage = null;
const _workletReady = (ctx.audioWorklet
- ? ctx.audioWorklet.addModule('/vendor/soundtouch-processor.js').then(() => {
+ ? loadStretchWorklet(ctx.audioWorklet).then(() => {
stNode = new AudioWorkletNode(ctx, 'soundtouch-processor', {
numberOfInputs: INPUT_COUNT,
numberOfOutputs: 1,
outputChannelCount: [2],
});
stNode.port.onmessage = (event) => {
- if (event?.data?.type === 'latency') _workletLatencyFrames = event.data.frames || 0;
+ const data = event?.data;
+ if (data?.type === 'latency') _workletLatencyFrames = data.frames || 0;
+ else if (data?.type === 'tempoStage') _tempoStage = data;
+ else if (data?.type === 'tempoStageFailed') {
+ console.warn('[tempoStage] Signalsmith core failed to start, keeping WSOLA:', data.message);
+ }
};
// The worklet loads asynchronously, so anything set before it arrived
// would otherwise be dropped. Re-apply the current value now.
@@ -335,12 +343,6 @@ export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {
// schedule chunk 0 without an async await after ready() completes.
const _cache = new Map();
- const _wsolaLatencySeconds = () => {
- const needed = Math.round(0.012 * ctx.sampleRate)
- + Math.round(0.028 * ctx.sampleRate)
- + Math.round(0.082 * ctx.sampleRate);
- return Math.floor(needed / 128) * 128 / ctx.sampleRate;
- };
const _anyLaneTransposed = () => {
for (const stem of stemMap.values()) {
if (effectivePitch(stem.name, stem.pitch, stem.pitchable) !== 0) return true;
@@ -349,11 +351,13 @@ export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {
};
const _pipelineLatencySeconds = () => {
if (!stNode) return 0;
- // The pitch buses only buffer once something is actually transposed. Until
- // then the worklet hands its input straight back, with no delay to correct.
- const pitchLatency = _anyLaneTransposed() ? _workletLatencyFrames / ctx.sampleRate : 0;
- const tempoStages = Math.abs(_playbackRate - 1) >= 1e-3 ? 1 : 0;
- return pitchLatency + tempoStages * _wsolaLatencySeconds();
+ return stretchLatency({
+ workletFrames: _workletLatencyFrames,
+ stage: _tempoStage,
+ rate: _playbackRate,
+ transposed: _anyLaneTransposed(),
+ sampleRate: ctx.sampleRate,
+ });
};
// Ducking either side of a bus change. Both buses are delayed by the same
diff --git a/static/js/tempoStage.js b/static/js/tempoStage.js
new file mode 100644
index 00000000..43842988
--- /dev/null
+++ b/static/js/tempoStage.js
@@ -0,0 +1,58 @@
+// Loading the worklet, and how late its output runs, for both audio engines.
+//
+// Both engines drive the same processor and used to carry their own copy of
+// its latency. Signalsmith Stretch (#729) made that a formula that depends on
+// the tempo and on which stage loaded, and a third copy of it would drift.
+
+const CORE_URL = "/vendor/signalsmith-stretch.js";
+const PROCESSOR_URL = "/vendor/soundtouch-processor.js";
+
+/**
+ * Add the Signalsmith core, then the processor.
+ *
+ * The core is optional. A browser without WebAssembly in worklets, or a CSP
+ * without 'wasm-unsafe-eval', fails here, and the processor then keeps WSOLA
+ * as its tempo stage. Only the processor failing fails the whole thing.
+ */
+export async function loadStretchWorklet(audioWorklet) {
+ try {
+ await audioWorklet.addModule(CORE_URL);
+ } catch (err) {
+ console.warn("[tempoStage] Signalsmith core did not load, keeping WSOLA:", err);
+ }
+ await audioWorklet.addModule(PROCESSOR_URL);
+}
+
+// WSOLA's tempo stage, as the engines have always counted it.
+function wsolaLatencySeconds(sampleRate) {
+ const needed = Math.round(0.012 * sampleRate)
+ + Math.round(0.028 * sampleRate)
+ + Math.round(0.082 * sampleRate);
+ return Math.floor(needed / 128) * 128 / sampleRate;
+}
+
+/**
+ * Seconds between a source sample entering the worklet and it being heard.
+ *
+ * @param {object} p
+ * @param {number} p.workletFrames the processor's `latency` message: the
+ * priming every bus goes through once the worklet is doing anything
+ * @param {object|null} p.stage the processor's latest `tempoStage` message
+ * @param {number} p.rate playback rate
+ * @param {boolean} p.transposed whether any lane is off its own key
+ * @param {number} p.sampleRate
+ */
+export function pipelineLatencySeconds({ workletFrames, stage, rate, transposed, sampleRate }) {
+ const tempoActive = Math.abs(rate - 1) >= 1e-3;
+ // The pitch buses only buffer once something is actually transposed. Until
+ // then the worklet hands its input straight back, with no delay to correct.
+ const pitchSeconds = transposed ? workletFrames / sampleRate : 0;
+ if (!tempoActive) return pitchSeconds;
+ if (stage?.stage === "signalsmith") {
+ // The priming and the core's input side are counted in input samples, so
+ // they stretch with the tempo; its output side does not. Measured against
+ // the processor end to end, this lands within a millisecond.
+ return ((workletFrames + stage.inputFrames) / rate + stage.outputFrames) / sampleRate;
+ }
+ return pitchSeconds + wsolaLatencySeconds(sampleRate);
+}
diff --git a/static/vendor/signalsmith-stretch.js b/static/vendor/signalsmith-stretch.js
new file mode 100644
index 00000000..d0460010
--- /dev/null
+++ b/static/vendor/signalsmith-stretch.js
@@ -0,0 +1,53 @@
+// Signalsmith Stretch 1.3.2, the WASM core only (#729).
+//
+// From the npm package signalsmith-stretch@1.3.2, file SignalsmithStretch.mjs,
+// lines 3 to 16: the Emscripten loader with the WASM embedded. Nothing in it is
+// changed. The package's own AudioWorkletNode wrapper is left out, because it
+// stretches only buffers it owns and ignores the rate on live input, which is
+// all StemDeck has. soundtouch-processor.js drives the core directly instead.
+//
+// Loaded as its own worklet module ahead of soundtouch-processor.js. Modules in
+// one AudioWorkletGlobalScope share its global object, which is how the loader
+// reaches the processor without an import (tests load the processor as a plain
+// function body, where an import would not parse).
+//
+// Upstream: https://signalsmith-audio.co.uk/code/stretch/
+//
+// MIT License
+//
+// Copyright (c) 2022 Geraint Luff / Signalsmith Audio Ltd.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+var SignalsmithStretch = (() => {
+ var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined;
+
+ return (
+function(moduleArg = {}) {
+ var moduleRtn;
+
+var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var crypto=globalThis?.crypto||{getRandomValues:array=>{for(var i=0;iDate.now()};var moduleOverrides=Object.assign({},Module);var arguments_=[];var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";var readAsync,readBinary;if(ENVIRONMENT_IS_SHELL){readBinary=f=>{if(typeof readbuffer=="function"){return new Uint8Array(readbuffer(f))}let data=read(f,"binary");assert(typeof data=="object");return data};readAsync=f=>new Promise((resolve,reject)=>{setTimeout(()=>resolve(readBinary(f)))});globalThis.clearTimeout??=id=>{};globalThis.setTimeout??=f=>f();arguments_=globalThis.arguments||globalThis.scriptArgs;if(typeof quit=="function"){quit_=(status,toThrow)=>{setTimeout(()=>{if(!(toThrow instanceof ExitStatus)){let toLog=toThrow;if(toThrow&&typeof toThrow=="object"&&toThrow.stack){toLog=[toThrow,toThrow.stack]}err(`exiting due to exception: ${toLog}`)}quit(status)});throw toThrow}}if(typeof print!="undefined"){globalThis.console??={};console.log=print;console.warn=console.error=globalThis.printErr??print}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=console.log.bind(console);var err=console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;var wasmBinary;if(typeof atob=="undefined"){if(typeof global!="undefined"&&typeof globalThis=="undefined"){globalThis=global}globalThis.atob=function(input){var keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{enc1=keyStr.indexOf(input.charAt(i++));enc2=keyStr.indexOf(input.charAt(i++));enc3=keyStr.indexOf(input.charAt(i++));enc4=keyStr.indexOf(input.charAt(i++));chr1=enc1<<2|enc2>>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}while(ifilename.startsWith(dataURIPrefix);function findWasmBinary(){var f="data:application/octet-stream;base64,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fyAAKAIIIQMgBCgCACEFAkACQCABQQRPBEAgAyAFckEDcQ0BA0AgAygCACAFKAIARw0CIAVBBGohBSADQQRqIQMgAUEEayIBQQNLDQALCyABRQ0BCwNAIAMtAAAiCSAFLQAAIgpGBEAgBUEBaiEFIANBAWohAyABQQFrIgENAQwCCwsgCSAKawwBC0EACyIBDQBBACEBIAAoAgwiAyAEKAIEIgVGDQBBf0EBIAMgBUkbIQELIAZBEGokACABRSEDCyAEQRBqJAAgAEEgaiQAIANFBEAjAEEQayIFJABBuggQHCEEAn8gAi0AC0EHdgRAIAIoAgQMAQsgAi0AC0H/AHELIQMCfwJ/IwBBEGsiByQAIAhBBGohASADIARqIgBB9////wdNBEACQCAAQQtJBEAgAUIANwIAIAFBADYCCCABIAEtAAtBgAFxIABB/wBxcjoACyABIAEtAAtB/wBxOgALDAELIABBC08EfyAAQQhqQXhxIgYgBkEBayIGIAZBC0YbBUEKC0EBaiIGEAchCCABIAEoAghBgICAgHhxIAZB/////wdxcjYCCCABIAEoAghBgICAgHhyNgIIIAEgCDYCACABIAA2AgQLIAdBEGokACABDAELEA0ACyIALQALQQd2BEAgACgCAAwBCyAACyIAQboIIAQQGyAAIARqIgACfyACLQALQQd2BEAgAigCAAwBCyACCyADEBsgACADaiECIwBBEGsiACQAIABBADoAD0EBIQQDQCAEBEAgAiAALQAPOgAAIARBAWshBCACQQFqIQIMAQsLIABBEGokACAFQRBqJAACfyABLQALQQd2BEAgASgCAAwBC0EACxoQDgALIAhBEGokACAHLAAPQQBIBEAgBygCBBAECyMAQRBrIgIkACMAQRBrIgEkACACQQxqQQQQACIABH9B3DQgADYCAEF/BUEACyABKAIMGiABQRBqJAAEQEHcNCgCABoQDgALIAIoAgwhASACQRBqJABBkDRBASABQf////8HcCICIAJBAU0bNgIAQZw0QgA3AgBBlDRCADcCAEGoNEIANwMAQbA0QgA3AwAgB0EQaiQAQbg0QgA3AgBBwDRBADYCAEHENEIANwIAQcw0QQA2AgBB0DRCADcCAEHYNEEANgIAC+gBAQR/IwBBEGsiBiQAIwBBIGsiAyQAIwBBEGsiBCQAIAQg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";return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}var binary=tryParseAsDataURI(file);if(binary){return binary}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["e"];updateMemoryViews();addOnInit(wasmExports["f"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}var info=getWasmImports();wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var __abort_js=()=>abort("");var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){abortOnCannotGrowMemory(requestedSize)}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.5/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}abortOnCannotGrowMemory(requestedSize)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var _random_get=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var keepRuntimeAlive=()=>true;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var wasmImports={d:__abort_js,c:__emscripten_memcpy_js,b:_emscripten_resize_heap,a:_random_get};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["f"])();var _setBuffers=Module["_setBuffers"]=(a0,a1)=>(_setBuffers=Module["_setBuffers"]=wasmExports["h"])(a0,a1);var _blockSamples=Module["_blockSamples"]=()=>(_blockSamples=Module["_blockSamples"]=wasmExports["i"])();var _intervalSamples=Module["_intervalSamples"]=()=>(_intervalSamples=Module["_intervalSamples"]=wasmExports["j"])();var _inputLatency=Module["_inputLatency"]=()=>(_inputLatency=Module["_inputLatency"]=wasmExports["k"])();var _outputLatency=Module["_outputLatency"]=()=>(_outputLatency=Module["_outputLatency"]=wasmExports["l"])();var _reset=Module["_reset"]=()=>(_reset=Module["_reset"]=wasmExports["m"])();var _presetDefault=Module["_presetDefault"]=(a0,a1)=>(_presetDefault=Module["_presetDefault"]=wasmExports["n"])(a0,a1);var _presetCheaper=Module["_presetCheaper"]=(a0,a1)=>(_presetCheaper=Module["_presetCheaper"]=wasmExports["o"])(a0,a1);var _configure=Module["_configure"]=(a0,a1,a2,a3)=>(_configure=Module["_configure"]=wasmExports["p"])(a0,a1,a2,a3);var _setTransposeFactor=Module["_setTransposeFactor"]=(a0,a1)=>(_setTransposeFactor=Module["_setTransposeFactor"]=wasmExports["q"])(a0,a1);var _setTransposeSemitones=Module["_setTransposeSemitones"]=(a0,a1)=>(_setTransposeSemitones=Module["_setTransposeSemitones"]=wasmExports["r"])(a0,a1);var _setFormantFactor=Module["_setFormantFactor"]=(a0,a1)=>(_setFormantFactor=Module["_setFormantFactor"]=wasmExports["s"])(a0,a1);var _setFormantSemitones=Module["_setFormantSemitones"]=(a0,a1)=>(_setFormantSemitones=Module["_setFormantSemitones"]=wasmExports["t"])(a0,a1);var _setFormantBase=Module["_setFormantBase"]=a0=>(_setFormantBase=Module["_setFormantBase"]=wasmExports["u"])(a0);var _seek=Module["_seek"]=(a0,a1)=>(_seek=Module["_seek"]=wasmExports["v"])(a0,a1);var _process=Module["_process"]=(a0,a1)=>(_process=Module["_process"]=wasmExports["w"])(a0,a1);var _flush=Module["_flush"]=a0=>(_flush=Module["_flush"]=wasmExports["x"])(a0);var _main=Module["_main"]=(a0,a1)=>(_main=Module["_main"]=wasmExports["y"])(a0,a1);Module["UTF8ToString"]=UTF8ToString;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function callMain(){var entryFunction=_main;var argc=0;var argv=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);if(shouldRunNow)callMain();postRun()}{doRun()}}var shouldRunNow=true;run();moduleRtn=readyPromise;
+
+
+ return moduleRtn;
+}
+);
+})();
+
+globalThis.SignalsmithStretchCore = SignalsmithStretch;
diff --git a/static/vendor/soundtouch-processor.js b/static/vendor/soundtouch-processor.js
index 689c2b21..d5b4a510 100644
--- a/static/vendor/soundtouch-processor.js
+++ b/static/vendor/soundtouch-processor.js
@@ -410,6 +410,99 @@ class Wsola {
}
}
+// The shared tempo stage, when the Signalsmith core loaded (#729).
+//
+// WSOLA slows audio down by repeating overlapping fragments of it, which is
+// heard as an echo on anything sustained. Signalsmith Stretch works in the
+// frequency domain and repeats nothing. 40 ms blocks were chosen by ear against
+// 30, 60 and the library's 120 ms default: 120 softened a quarter of the drum
+// attacks in a full mix, 30 blurred the low end.
+const SIGNALSMITH_BLOCK_MS = 40;
+const SIGNALSMITH_INTERVAL_MS = 10;
+// Largest input one render quantum can ask for is 128 * the tempo maximum.
+const SIGNALSMITH_MAX_BLOCK = 128 * 4;
+
+/**
+ * The same shape as a Wsola chain, as far as the processor uses one: push,
+ * fill, clear and an output FIFO pair. Anything else would mean a second code
+ * path through process().
+ *
+ * The core stretches by whatever ratio it is handed, so each quantum feeds it
+ * 128 * tempo input samples for 128 output samples. The fraction carries over,
+ * so the rate is exact on average and nothing drifts against the click.
+ */
+class SignalsmithTempo {
+ constructor(core, sr) {
+ this._core = core;
+ core._main();
+ core._configure(
+ 2,
+ Math.round(SIGNALSMITH_BLOCK_MS * sr / 1000),
+ Math.round(SIGNALSMITH_INTERVAL_MS * sr / 1000),
+ false,
+ );
+ core._reset();
+ this.inputLatency = core._inputLatency();
+ this.outputLatency = core._outputLatency();
+ this._len = SIGNALSMITH_MAX_BLOCK;
+ this._ptr = core._setBuffers(2, this._len);
+ this._heap = null;
+ this.inL = new FloatFifo();
+ this.inR = new FloatFifo();
+ this.outL = new FloatFifo();
+ this.outR = new FloatFifo();
+ this._carry = 0;
+ }
+
+ // Views onto the core's buffers, rebuilt only if its memory was replaced.
+ _views() {
+ const buffer = this._core.HEAP8.buffer;
+ if (!this._heap || this._heap.buffer !== buffer) {
+ this._heap = new Float32Array(buffer, this._ptr, this._len * 4);
+ }
+ return this._heap;
+ }
+
+ clear() {
+ this.inL.clear(); this.inR.clear();
+ this.outL.clear(); this.outR.clear();
+ this._core._reset();
+ this._carry = 0;
+ }
+
+ push(l, r, n) {
+ this.inL.push(l, 0, n);
+ this.inR.push(r, 0, n);
+ }
+
+ fill(tempo, want) {
+ const block = 128;
+ while (this.outL.avail < want) {
+ const exact = block * tempo + this._carry;
+ const take = Math.min(Math.floor(exact), this._len);
+ if (this.inL.avail < take) return;
+ this._carry = exact - take;
+ const heap = this._views();
+ const len = this._len;
+ for (let i = 0; i < take; i++) {
+ heap[i] = this.inL.peek(i);
+ heap[len + i] = this.inR.peek(i);
+ }
+ this.inL.consume(take);
+ this.inR.consume(take);
+ this._core._process(take, block);
+ const out = this._views();
+ this.outL.push(out, 2 * len, block);
+ this.outR.push(out, 3 * len, block);
+ }
+ }
+
+ /** Output samples between a sample entering and leaving, at this tempo. */
+ latencyFrames(tempo) {
+ return this.inputLatency / tempo + this.outputLatency;
+ }
+}
+
/** FIFO sample at `idx`, falling back to the carried history for idx < 0. */
function sampleAt(fifo, hist, idx) {
if (idx >= 0) return fifo.peek(idx);
@@ -601,10 +694,15 @@ class SoundTouchProcessor extends AudioWorkletProcessor {
constructor() {
super();
- this._tempo = new Wsola(sampleRate, { keepAttacks: true });
+ // WSOLA stays as the tempo stage until the Signalsmith core is ready, and
+ // for good if it never is: a browser without WebAssembly in worklets, or a
+ // CSP that blocks it. Its sizes also set the priming below either way.
+ this._wsola = new Wsola(sampleRate, { keepAttacks: true });
+ this._tempo = this._wsola;
+ this._signalsmith = null;
// Silence every bus is primed with, before its own alignment pad, so they
// share one clock and each chain can produce from its first block.
- this._commonPrime = this._tempo.needed + PRIME_CUSHION;
+ this._commonPrime = this._wsola.needed + PRIME_CUSHION;
this._chains = new Array(INPUT_COUNT).fill(null);
this._idle = new Array(INPUT_COUNT).fill(0);
this._unpitchedL = new FloatFifo();
@@ -629,13 +727,39 @@ class SoundTouchProcessor extends AudioWorkletProcessor {
this.port.postMessage({
type: 'latency',
frames: this._commonPrime
- + alignmentPad(this._tempo.ovLen + this._tempo.midLen, 1),
+ + alignmentPad(this._wsola.ovLen + this._wsola.midLen, 1),
+ });
+ }
+ this._reportTempoStage();
+
+ // Loaded as its own worklet module ahead of this one; see
+ // signalsmith-stretch.js. Instantiating is asynchronous, so the stage is
+ // only swapped in at the next flush, never under audio already playing.
+ const core = globalThis.SignalsmithStretchCore;
+ if (typeof core === 'function') {
+ core().then((module) => {
+ this._signalsmith = new SignalsmithTempo(module, sampleRate);
+ }).catch((err) => {
+ this.port?.postMessage({ type: 'tempoStageFailed', message: String(err?.message || err) });
});
}
}
+ // The tempo stage's own latency, which the engine adds when tempo is not 1.
+ // Signalsmith's depends on the tempo, so it is sent as its two parts.
+ _reportTempoStage() {
+ if (!this.port) return;
+ const ss = this._tempo === this._signalsmith ? this._signalsmith : null;
+ this.port.postMessage({
+ type: 'tempoStage',
+ stage: ss ? 'signalsmith' : 'wsola',
+ inputFrames: ss ? ss.inputLatency : 0,
+ outputFrames: ss ? ss.outputLatency : 0,
+ });
+ }
+
_primeUnpitched() {
- const outLen = this._tempo.ovLen + this._tempo.midLen;
+ const outLen = this._wsola.ovLen + this._wsola.midLen;
const pad = new Float32Array(this._commonPrime + alignmentPad(outLen, 1));
this._unpitchedL.clear();
this._unpitchedR.clear();
@@ -644,6 +768,10 @@ class SoundTouchProcessor extends AudioWorkletProcessor {
}
_flush() {
+ if (this._signalsmith && this._tempo !== this._signalsmith) {
+ this._tempo = this._signalsmith;
+ this._reportTempoStage();
+ }
this._tempo.clear();
this._mixFifoL.clear();
this._mixFifoR.clear();
diff --git a/tests/e2e/tempo-stage.spec.mjs b/tests/e2e/tempo-stage.spec.mjs
new file mode 100644
index 00000000..bb412935
--- /dev/null
+++ b/tests/e2e/tempo-stage.spec.mjs
@@ -0,0 +1,42 @@
+// The Signalsmith tempo stage loads in a real browser (#729).
+//
+// tests/js/signalsmith-stage.test.mjs checks the stage itself in node. What
+// only a browser can show is that it gets that far at all: the core is a
+// WebAssembly module compiled inside the audio worklet, which the page's CSP
+// blocks unless script-src allows 'wasm-unsafe-eval'. Blocked, the processor
+// quietly keeps WSOLA and every other test still passes, so this watches the
+// processor say which stage it is running.
+
+import { test, expect } from "@playwright/test";
+import { openStudio } from "./helpers.mjs";
+
+async function recordWorkletMessages(page) {
+ await page.addInitScript(() => {
+ window.__workletMessages = [];
+ const Native = window.AudioWorkletNode;
+ window.AudioWorkletNode = class extends Native {
+ constructor(...args) {
+ super(...args);
+ this.port.addEventListener("message", (event) => window.__workletMessages.push(event.data));
+ }
+ };
+ });
+}
+
+test("slowed playback runs on the Signalsmith stage", async ({ page }) => {
+ const failures = [];
+ page.on("console", (msg) => {
+ if (msg.type() === "warning" && msg.text().includes("[tempoStage]")) failures.push(msg.text());
+ });
+ await recordWorkletMessages(page);
+ await openStudio(page);
+
+ await page.locator("#t-speed-075").click();
+ await page.locator("#t-play").click();
+
+ await expect.poll(
+ () => page.evaluate(() => window.__workletMessages.filter((m) => m?.type === "tempoStage").at(-1)?.stage),
+ { timeout: 10000 },
+ ).toBe("signalsmith");
+ expect(failures).toEqual([]);
+});
diff --git a/tests/js/pitch-shift.test.mjs b/tests/js/pitch-shift.test.mjs
index 91d64423..ba47f468 100644
--- a/tests/js/pitch-shift.test.mjs
+++ b/tests/js/pitch-shift.test.mjs
@@ -619,7 +619,7 @@ const Proc = load();
const starved = load(SRC
.replace('const PRIME_CUSHION = 512;', 'const PRIME_CUSHION = 0;')
.replace('const OUTPUT_RESERVE = 512;', 'const OUTPUT_RESERVE = 0;')
- .replace('this._commonPrime = this._tempo.needed + PRIME_CUSHION;', 'this._commonPrime = 0;'));
+ .replace('this._commonPrime = this._wsola.needed + PRIME_CUSHION;', 'this._commonPrime = 0;'));
let starvedPartials = 0;
for (const [pitch, tempo] of [[2, 1], [-2, 1], [5, 1], [-5, 1]]) {
starvedPartials += artefacts(starved, pitch, tempo).partial;
diff --git a/tests/js/signalsmith-stage.test.mjs b/tests/js/signalsmith-stage.test.mjs
new file mode 100644
index 00000000..9be327dc
--- /dev/null
+++ b/tests/js/signalsmith-stage.test.mjs
@@ -0,0 +1,166 @@
+// The Signalsmith tempo stage (#729).
+//
+// WSOLA slowed audio by repeating overlapping fragments, heard as an echo on
+// everything sustained. The processor now stretches with Signalsmith Stretch
+// when its core loads, and keeps WSOLA when it does not. Checked here, with the
+// processor driven exactly as an AudioWorklet drives it:
+//
+// - the stage is swapped in at a flush, and reported, and only then
+// - the latency the engines compute from that report is the latency measured
+// - kicks come out once each and on time, so nothing drifts
+// - with no core, WSOLA stays, so playback never depends on WebAssembly
+//
+// Run: node tests/js/signalsmith-stage.test.mjs
+
+import { readFileSync } from 'node:fs';
+import { pipelineLatencySeconds } from '../../static/js/tempoStage.js';
+
+const BLOCK = 128;
+const INPUT_COUNT = 13;
+const ZERO_INPUT = 6;
+
+let passed = 0;
+let failed = 0;
+
+function check(name, condition, detail = '') {
+ if (condition) {
+ passed++;
+ console.log(`PASS ${name}`);
+ } else {
+ failed++;
+ console.log(`FAIL ${name}${detail ? ` -- ${detail}` : ''}`);
+ }
+}
+
+const vendor = (name) => readFileSync(new URL(`../../static/vendor/${name}`, import.meta.url), 'utf8');
+const PROCESSOR = vendor('soundtouch-processor.js');
+const CORE = vendor('signalsmith-stretch.js');
+
+/** A processor, with or without the core in its global scope, and its messages. */
+async function create(sr, { core }) {
+ if (core) new Function(CORE)();
+ else delete globalThis.SignalsmithStretchCore;
+ let Cls = null;
+ const messages = [];
+ new Function('sampleRate', 'AudioWorkletProcessor', 'registerProcessor', PROCESSOR)(
+ sr,
+ class { constructor() { this.port = { postMessage: (m) => messages.push(m) }; } },
+ (_name, c) => { Cls = c; },
+ );
+ const p = new Cls();
+ // Instantiating the core is asynchronous; the engine's play() resets the
+ // processor well after that, which is what this stands in for.
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ p.port.onmessage({ data: { type: 'reset' } });
+ return { p, messages };
+}
+
+/** Run `input` through at `tempo` into the unpitched input; returns the left output. */
+function run(p, input, tempo, outLength) {
+ const out = new Float32Array(Math.ceil(outLength / BLOCK) * BLOCK);
+ const params = { tempo: new Float32Array([tempo]) };
+ for (let pos = 0; pos < out.length; pos += BLOCK) {
+ const block = new Float32Array(BLOCK);
+ if (pos < input.length) block.set(input.subarray(pos, Math.min(pos + BLOCK, input.length)));
+ const inputs = Array.from({ length: INPUT_COUNT }, (_, k) => (k === ZERO_INPUT ? [block, block] : []));
+ const l = new Float32Array(BLOCK);
+ p.process(inputs, [[l, new Float32Array(BLOCK)]], params);
+ out.set(l, pos);
+ }
+ return out;
+}
+
+const lastOf = (messages, type) => messages.filter((m) => m.type === type).at(-1);
+
+// --- the stage is swapped in, and says so ---
+{
+ const { p, messages } = await create(44100, { core: true });
+ run(p, new Float32Array(BLOCK), 0.75, BLOCK);
+ const stage = lastOf(messages, 'tempoStage');
+ check('the core is swapped in at the first flush', stage?.stage === 'signalsmith', `stage ${stage?.stage}`);
+ check('its latency is reported in two parts', stage?.inputFrames > 0 && stage?.outputFrames > 0);
+}
+
+// --- no core: WSOLA stays ---
+{
+ const { p, messages } = await create(44100, { core: false });
+ run(p, new Float32Array(BLOCK), 0.75, BLOCK);
+ check('without the core, WSOLA stays the tempo stage', lastOf(messages, 'tempoStage')?.stage === 'wsola');
+}
+
+// --- latency: what the engines compute is what comes out ---
+for (const sr of [44100, 48000]) {
+ for (const tempo of [0.75, 0.9]) {
+ const { p, messages } = await create(sr, { core: true });
+ const at = Math.round(sr * 0.5);
+ const input = new Float32Array(sr);
+ input[at] = 1;
+ const out = run(p, input, tempo, sr * 2);
+ let peak = 0;
+ let peakAt = 0;
+ for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) > peak) { peak = Math.abs(out[i]); peakAt = i; }
+ const measured = (peakAt - at / tempo) / sr;
+ const computed = pipelineLatencySeconds({
+ workletFrames: lastOf(messages, 'latency').frames,
+ stage: lastOf(messages, 'tempoStage'),
+ rate: tempo,
+ transposed: false,
+ sampleRate: sr,
+ });
+ const error = Math.abs(measured - computed) * 1000;
+ check(
+ `the computed latency matches at ${sr} Hz, ${tempo}x`,
+ error <= 1,
+ `measured ${(measured * 1000).toFixed(1)} ms, computed ${(computed * 1000).toFixed(1)} ms`,
+ );
+ }
+}
+
+// --- kicks come out once, and on time ---
+{
+ const sr = 44100;
+ const tempo = 0.75;
+ const kicks = 40;
+ const every = 0.5;
+ const n = Math.round((kicks * every + 0.5) * sr);
+ const x = new Float32Array(n);
+ for (let i = 0; i < n; i++) {
+ const t = i / sr;
+ x[i] = 0.08 * (Math.sin(2 * Math.PI * 220 * t) + Math.sin(2 * Math.PI * 261.63 * t) + Math.sin(2 * Math.PI * 329.63 * t));
+ }
+ for (let k = 0; k < kicks; k++) {
+ const at = Math.round((0.25 + k * every) * sr);
+ for (let i = 0; i < 0.15 * sr && at + i < n; i++) {
+ const t = i / sr;
+ const click = t < 0.003 ? 0.5 * Math.sin(2 * Math.PI * 3000 * t) * (1 - t / 0.003) : 0;
+ x[at + i] += click + 0.6 * Math.sin(2 * Math.PI * 60 * t) * Math.exp(-t / 0.04);
+ }
+ }
+ const { p } = await create(sr, { core: true });
+ const y = run(p, x, tempo, Math.round(n / tempo) + sr);
+
+ // Peaks of the click band's 1 ms envelope above a third of the loudest.
+ const hop = Math.round(sr * 0.001);
+ const env = [];
+ for (let i = hop; i + hop <= y.length; i += hop) {
+ let e = 0;
+ for (let j = 0; j < hop; j++) e += (y[i + j] - y[i + j - 1]) ** 2;
+ env.push(e);
+ }
+ const top = Math.max(...env);
+ const found = [];
+ for (let f = 1; f + 1 < env.length; f++) {
+ if (env[f] < top / 3 || env[f] < env[f - 1] || env[f] < env[f + 1]) continue;
+ const t = (f + 1) * hop / sr;
+ if (found.length && t - found.at(-1) < 0.01) continue;
+ found.push(t);
+ }
+ check(`every kick comes out once at ${tempo}x`, found.length === kicks, `heard ${found.length} for ${kicks}`);
+ // The input advance carries its fraction, so the last kick lands where an
+ // exact clock puts it, not a few samples further each quantum.
+ const drift = found.at(-1) - found[0] - ((kicks - 1) * every) / tempo;
+ check('the last kick is on the clock', Math.abs(drift) <= 0.002, `${(drift * 1000).toFixed(2)} ms`);
+}
+
+console.log(`\n${passed}/${passed + failed} checks passed`);
+process.exit(failed === 0 ? 0 : 1);
diff --git a/tests/js/tempo-attacks.test.mjs b/tests/js/tempo-attacks.test.mjs
index cc6c1b14..7ddcfcb2 100644
--- a/tests/js/tempo-attacks.test.mjs
+++ b/tests/js/tempo-attacks.test.mjs
@@ -141,7 +141,7 @@ check(
// Borrowing moves a chain's clock. The pitch stages are aligned sample for
// sample with the unpitched drums, so only the shared tempo stage may do it.
const enabled = [...SRC.matchAll(/new Wsola\([^)]*keepAttacks: true/g)].length;
-const tempoStage = /this\._tempo = new Wsola\(sampleRate, \{ keepAttacks: true \}\)/.test(SRC);
+const tempoStage = /this\._wsola = new Wsola\(sampleRate, \{ keepAttacks: true \}\)/.test(SRC);
check('only the shared tempo stage keeps attacks', enabled === 1 && tempoStage, `enabled in ${enabled} places`);
console.log(`\n${passed}/${passed + failed} checks passed`);
diff --git a/tests/test_csp.py b/tests/test_csp.py
index 53833ff5..9b8e1a11 100644
--- a/tests/test_csp.py
+++ b/tests/test_csp.py
@@ -40,7 +40,11 @@ def test_connect_src_allows_the_artist_and_lyrics_hosts_and_nothing_wider():
def test_script_src_stays_locked():
# Lock #171's intent: loosening connect-src must not weaken the XSS defense.
- script = _csp_directive("script-src")
+ script = _csp_directive("script-src").split()[1:]
assert "'self'" in script
- assert "unsafe-inline" not in script
- assert "unsafe-eval" not in script
+ assert "'unsafe-inline'" not in script
+ assert "'unsafe-eval'" not in script
+ # 'wasm-unsafe-eval' compiles WebAssembly and nothing else: JS eval,
+ # new Function and string timers stay blocked. The Signalsmith tempo stage
+ # needs it (#729). Anything beyond it here is a loosening to justify.
+ assert set(script) == {"'self'", "'wasm-unsafe-eval'"}
From d78fa34c69ab3e5b4e29602d55850a21bf87db59 Mon Sep 17 00:00:00 2001
From: "Tha.Les" <31625914+thcp@users.noreply.github.com>
Date: Thu, 1 Oct 2026 13:04:54 +0100
Subject: [PATCH 4/7] Fix the bugs reported against 0.19.0 (#737)
* fix(backend): start on CPU when torch is installed but cannot load
available_torch_devices() caught only ImportError. A torch whose DLLs
fail to load raises OSError instead (WinError 127 from a CUDA DLL left
behind on Windows, #723), which killed startup when the device setting
was "auto" and made every GET /api/settings a 500 otherwise.
The probe now catches any failure, falls back to ["cpu"], logs the
cause once with its traceback, and remembers it. /api/settings carries
it as torch_error, and Settings replaces the device description with a
warning in red: separation cannot run on any device until torch is
repaired, CPU included, so a quiet fallback would hide the real state.
tests/test_torch_unloadable.py installs an import hook that makes
`import torch` raise that OSError; the probe and settings tests fail on
the old code.
Closes #730
* fix(library): favourite a track from its library row
The Now Playing heart was the only way to favourite, and daw.css hides
that whole card below 1460 px. A maximised laptop window is under that,
so there was no way to favourite at all, and no way to un-favourite
from the Favorites view.
- Library and Favorites rows get a heart next to the delete button.
- Every heart goes through toggleFavorite(), which saves, repaints the
Now Playing heart when it is the open track, and re-renders the list,
so the hearts cannot disagree with each other or the store.
- Row actions float over the row's right edge on hover or keyboard
focus. In the flow the hidden delete button took about 29 px from
every title at rest; a second button would have taken it again.
Delete was also invisible under keyboard focus before.
- A favourite shows a small heart in its subline at rest.
tests/e2e/favorite-reach.spec.mjs runs at 1280 and 1600 px; all four
fail on the old code.
Closes #724
* fix(desktop): offer older NVIDIA cards a torch build with their kernels
A CUDA 13 driver was offered cu128 alone. cu128 installs torch 2.8,
whose builds start at sm_61 on Windows and sm_70 on Linux
(TORCH_CUDA_ARCH_LIST in pytorch's release/2.8 .ci scripts), so a
GTX 970 (sm_52) installed it, failed verification with "no kernel image
is available", and dropped to CPU.
- A card below compute capability 7 is never offered cu128; it gets
cu124 then cu118, the torch 2.6 builds, which carry sm_50 on both
platforms (release/2.6 scripts), and an sm_50 binary runs on 5.x.
- A CUDA 13 driver on a newer card keeps cu128 first, with cu124 and
cu118 behind it instead of CPU.
New tests: a_pre_volta_card_is_never_offered_cu128 (the old code
returned ["cu128"] for 5.2 under 13.0) and
a_cuda_13_driver_falls_back_to_the_2_6_builds. The published-wheel
check now covers caps 5.2 and 6.1.
Closes #732
* fix(ui): show a track's key once, as one label that fits
The analysis strip said the key three times: "D maj" on the Key card,
"Major" under it, and "Major" again on a Scale card of its own, while
the Key card was too narrow for a longer label.
- keyLabel.js formatKey() turns the stored key and scale into one
label, "B minor" or "B harmonic minor", from a translated template
and mode names in all ten languages (plus the European Portuguese
spelling). Note names stay letters everywhere, as DAWs write them.
- The Scale card, the Major/Minor sub-line and an unused confidence
label are gone; the confidence ring stays.
- Key takes the Scale card's grid column, so the row keeps the seven
edges the presence row lines up with. Measured at a 950 px window it
still has 115 px for a label that can need 155, so the key wraps to a
second line rather than clip; a short key stays on one line.
- Display only: stored key, scale and key_confidence are unchanged, so
nothing needs re-analysis.
tests/js/key-label.test.mjs checks every language; the new
tests/e2e/key-card.spec.mjs checks en, de and fr at 1366 and 950 px
for the label and that it is not clipped, and fails on the old code.
Closes #736
* fix(desktop): stop retrying CUDA setup on a GPU no build supports
A CPU result born from a failure is re-probed on every launch (#247),
which is right for a network drop. For a GPU that every offered CUDA
build refuses with "no kernel image is available", it meant downloading
and installing CUDA torch, failing verification and restoring CPU torch
on every launch: six cycles in the #723 reporter's setup.log.
- verify_cuda_torch() now says why it failed; the no-kernel case is
matched on CUDA's exact wording, so driver, memory or install problems
are never mistaken for it.
- When every candidate installed and was refused for that reason, setup
records cuda-unsupported-gpu. Any other mix keeps today's reasons.
- The setup screen treats that reason as settled and says plainly why
the GPU is not used. A new app version re-runs setup anyway, which is
when different builds could be on offer.
New tests: a_gpu_every_build_refuses_is_settled_as_unsupported and
only_cudas_own_wording_counts_as_no_kernel_image.
Closes #733
* fix(desktop): put the bundled torch back exactly when CUDA fails
The CUDA install laid the CUDA wheels over the CPU ones, and the restore
laid the CPU wheels back over those, both with --ignore-installed.
Overlaying never removes files only the CUDA wheel has. On Windows torch
loads every DLL in torch/lib, so the leftover c10_cuda.dll of one torch
version failed to link against the restored CPU DLLs, and `import torch`
raised WinError 127 for good while setup reported success (#723).
New module torchswap:
- Before the first real CUDA install the bundled torch is moved aside
into .stemdeck-torch-backup next to site-packages. What belongs to
torch is read from pip's RECORD files; on a real install that is only
torch, functorch, torchgen, torchaudio, torio, torchvision and their
dist-infos.
- A manifest is written and synced before anything moves, so a crash at
any point is recoverable; the next setup run restores it first.
- Each candidate installs into clean directories; a failure deletes the
CUDA build and renames the original back, with no network. Success
deletes the backup.
After any restore `import torch` is now checked. Without a backup (the
move failed, or the install broke before this fix) the CPU wheels are
reinstalled, and if torch still does not import, everything torch owns
is deleted and they are installed once more into clean directories.
That is what repairs an install already carrying the stray DLL.
Whether a working CUDA build is already present is now asked once,
before anything moves, instead of inside each install.
Eight new tests on real temp directories, including the reporter's case
(no c10_cuda.dll after rollback, c10.dll byte for byte) and a snapshot
interrupted part way.
Closes #731
* fix(analyze): stop reporting minor keys as their relative major
Key detection multiplied each key's profile correlation by how loud its
root was. In a minor song the relative major's root is the minor chord's
third and sits in most of a typical loop's chords, so it is usually
louder: every i-VI-III-VII loop came out as its relative major at up to
100% confidence ("Plug in Baby", B minor, read D major). Harmonic minor
was never reported, and confidence changed with the input's level.
- Keys are ranked by correlation alone, which is level-invariant.
- The keys close to the best (within 0.1) and their relatives are then
weighed against the song's edges: how well the first and, when the
whole song was analysed, last 4 s of music fit each key's tonic triad
and bass note. A song starts and ends on its tonic far more often than
anywhere else, which is what a whole-song histogram cannot show.
- A minor key is reported as harmonic minor when its raised seventh
outweighs the flat seventh 1.3 to 1.
- After separation the key is detected again from the stems: harmony
stems without drums, the bass stem as the bass line, and the whole
song. Any failure keeps the estimate from the mix.
- Chroma uses 93 ms frames: 4.1 s instead of 8.6 s on a 9.5 min track's
stems, same score. The stems pass takes about 1 s per minute of audio.
Measured on tests/keyset.py, 33 synthetic songs with known keys (chord
loops over a bass line, 11 progressions in 3 keys), key and scale both
right:
before 21 of 33 (27 keys right)
after 30 of 33 from the mix, 30 of 33 from stems
The three misses are vi-IV-I-V, which starts and ends on its minor
chord. A real 9.5 min track (E minor) reads the same before and after.
Already analysed tracks keep their stored key; nothing is re-analysed.
tests/test_key_detection.py scores the set and the reporter's loop, and
fails on the old detector.
Closes #726
* fix(pipeline): count the stems key pass inside post
A separate "key" stage timing broke the timings' order, which
tests/test_identify_regressions.py holds as a contract: new entries may
only follow the existing stages. The pass is short and already inside
post, so it is counted there.
Refs #726
* test(desktop): check the torch rollback on a real torch install
An ignored test, run by hand: copies a real site-packages torch (1.2 GB
here), moves it aside, lays a CUDA build with a stray c10_cuda.dll into
its place, rolls back, and compares every file's SHA-256 with the
original. Passed against the dev venv's torch 2.6.0+cpu, which then
imported and ran a tensor op from the restored copy.
Refs #731
---------
Co-authored-by: Thales <>
---
app/core/config.py | 25 ++
app/main.py | 4 +
app/pipeline/analyze.py | 352 +++++++++++++++++++----
app/pipeline/runner.py | 8 +-
desktop/src-tauri/src/main.rs | 345 ++++++++++++++++++++--
desktop/src-tauri/src/torchswap.rs | 444 +++++++++++++++++++++++++++++
desktop/ui/setup.js | 15 +-
static/css/daw.css | 51 +++-
static/index.html | 9 -
static/js/catalog.js | 89 ++++--
static/js/i18n.js | 101 ++++++-
static/js/job.js | 9 +-
static/js/keyLabel.js | 35 +++
static/js/player.js | 4 -
tests/e2e/favorite-reach.spec.mjs | 74 +++++
tests/e2e/key-card.spec.mjs | 64 +++++
tests/js/key-label.test.mjs | 61 ++++
tests/keyset.py | 106 +++++++
tests/test_key_detection.py | 156 ++++++++++
tests/test_torch_unloadable.py | 87 ++++++
20 files changed, 1896 insertions(+), 143 deletions(-)
create mode 100644 desktop/src-tauri/src/torchswap.rs
create mode 100644 static/js/keyLabel.js
create mode 100644 tests/e2e/favorite-reach.spec.mjs
create mode 100644 tests/e2e/key-card.spec.mjs
create mode 100644 tests/js/key-label.test.mjs
create mode 100644 tests/keyset.py
create mode 100644 tests/test_key_detection.py
create mode 100644 tests/test_torch_unloadable.py
diff --git a/app/core/config.py b/app/core/config.py
index 104f67cb..63ebf7c4 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -28,11 +28,23 @@ def _env_path_opt(name: str) -> Path | None:
return Path(raw).expanduser().resolve() if raw else None
+# Why torch could not be loaded at the last probe, or None if it loaded (#730).
+# Refreshed on every probe rather than decided once, so a repaired install
+# clears it without a restart.
+_torch_load_error: str | None = None
+
+
+def torch_load_error() -> str | None:
+ """The last probe's reason torch is installed but unusable, if any."""
+ return _torch_load_error
+
+
def available_torch_devices() -> list[str]:
"""Compute devices this machine can actually use, best-first. CPU is always
present; cuda/mps depend on the hardware + installed torch build. The
Settings UI uses this to disable options that aren't available/detected so
a user can't pick an impossible device."""
+ global _torch_load_error
devices: list[str] = []
try:
import torch
@@ -41,8 +53,21 @@ def available_torch_devices() -> list[str]:
devices.append("cuda")
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
devices.append("mps")
+ _torch_load_error = None
except ImportError:
pass
+ except Exception as exc: # noqa: BLE001 -- any failure here must not stop the server
+ # Installed but unloadable: on Windows a CUDA DLL left behind by a
+ # half-reverted install raises OSError (WinError 127) from torch's own
+ # DLL loader (#723). Catching only ImportError let that kill startup
+ # and every /api/settings call. The server has to come up so it can
+ # say what is wrong. Logged loudly once per distinct cause, because
+ # separation cannot run on any device until torch is repaired, CPU
+ # included, and a quiet fallback would hide exactly that.
+ reason = f"{type(exc).__name__}: {exc}"
+ if reason != _torch_load_error:
+ logger.error("torch is installed but could not be loaded: %s", reason, exc_info=True)
+ _torch_load_error = reason
devices.append("cpu")
return devices
diff --git a/app/main.py b/app/main.py
index 2035d665..4d5fde4f 100644
--- a/app/main.py
+++ b/app/main.py
@@ -39,6 +39,7 @@
available_torch_devices,
configure_portable_environment,
ensure_runtime_dirs,
+ torch_load_error,
)
from app.core.logging_setup import configure_logging
from app.core.process import process_exists as _process_exists
@@ -393,6 +394,9 @@ def _settings_payload() -> dict[str, object]:
"demucs_device": get_demucs_device_choice(),
"demucs_device_resolved": get_demucs_device(),
"demucs_devices_available": available_torch_devices(),
+ # Set when torch is installed but cannot load, which stops separation
+ # on every device (#730). Read after the probe above refreshed it.
+ "torch_error": torch_load_error(),
}
diff --git a/app/pipeline/analyze.py b/app/pipeline/analyze.py
index 60d60c13..2acbb3f1 100644
--- a/app/pipeline/analyze.py
+++ b/app/pipeline/analyze.py
@@ -54,6 +54,34 @@
# and minor is the better default when the call is genuinely ambiguous.
_MINOR_TIE_BREAK_FRAC = 0.05
+# How much the song's edges count against the correlation when choosing
+# between close candidates, a key and its relative above all. Tuned on
+# tests/keyset.py: the reporter's
+# i-VI-III-VII leads its relative major by up to 0.22 in correlation and
+# trails it by about 0.8 in edge fit, and in every song detected correctly the
+# edges agree with the correlation, so weighing them in never flips those.
+_EDGE_WEIGHT = 0.4
+# Keys this close to the best correlation, and their relatives, are all
+# candidates when the edges are weighed in. A loop like i-VII-VI-VII puts the
+# VII's own key ahead of the tonic's by a hair, and neither is the other's
+# relative.
+_FAMILY_WINDOW = 0.1
+# Without edges, a relative pair this close in correlation counts as a tie.
+_RELATIVE_MARGIN = 0.15
+# Harmonic minor is reported when the raised seventh outweighs the flat one by
+# this much and reaches this share of the loudest pitch class. On
+# tests/keyset.py the ratio is at most 0.77 for natural minor and at least 1.54
+# for harmonic minor, on the mix and on stems; the floor only keeps two
+# near-silent pitch classes from deciding it.
+_HARMONIC_RATIO = 1.3
+_HARMONIC_FLOOR = 0.1
+# A correlation gap this wide over the nearest unrelated key is full
+# confidence; an edge margin this wide settles a relative pair outright.
+_CONFIDENCE_FULL_GAP = 0.25
+_EDGE_CONFIDENT_MARGIN = 0.3
+# How much of the start and end of the music counts as its edges.
+_EDGE_SECONDS = 4.0
+
def _correlate(profile: tuple[float, ...], chroma: list[float], shift: int) -> float:
n = len(profile)
@@ -68,66 +96,131 @@ def _correlate(profile: tuple[float, ...], chroma: list[float], shift: int) -> f
return num / (denom_p * denom_c)
-def _detect_key(chroma_mean: list[float]) -> tuple[str, str, int]:
- """Find the best-matching key by combining profile correlation with
- root prominence. The Pearson correlation alone is fooled by relative
- keys whose diatonic notes happen to overlap with the song's loud
- pitches but whose own tonic is weak (e.g. picking A minor for an
- E-minor song because E is its 5th and D is its 4th). Weighting by
- the candidate root's chroma value forces the algorithm to also
- confirm 'is this proposed tonic actually loud in the audio?'.
- Logs the chroma vector and top-5 candidates for diagnostics.
+def _key_candidates(chroma_mean: list[float]) -> list[tuple[float, int, str]]:
+ """Every key's Pearson correlation with its profile, best first, as
+ (correlation, root, "maj" | "min")."""
+ out = []
+ for shift in range(12):
+ out.append((_correlate(_MAJOR_PROFILE, chroma_mean, shift), shift, "maj"))
+ out.append((_correlate(_MINOR_PROFILE, chroma_mean, shift), shift, "min"))
+ out.sort(key=lambda c: c[0], reverse=True)
+ return out
+
+
+def _relative(root: int, mode: str) -> tuple[int, str]:
+ return ((root + 9) % 12, "min") if mode == "maj" else ((root + 3) % 12, "maj")
+
+
+def _tonic_fit(root: int, mode: str, edges: list[tuple[list[float], list[float]]]) -> float:
+ """How much the song's edges sound like this key's home chord.
+
+ Each edge is (chroma, bass chroma) averaged over a few seconds at the start
+ or the end of the music, each scaled to a maximum of 1. A song starts and
+ ends on its tonic far more often than on any other chord, and that is the
+ one thing a whole-song histogram cannot show: a minor loop such as
+ i-VI-III-VII uses exactly its relative major's notes (#726).
+ """
+ triad = {root, (root + (4 if mode == "maj" else 3)) % 12, (root + 7) % 12}
+ score = 0.0
+ for harmony, bass in edges:
+ inside = sum(harmony[i] for i in triad) / 3
+ outside = sum(harmony[i] for i in range(12) if i not in triad) / 9
+ score += inside - outside + bass[root]
+ return score / len(edges)
+
+
+def _detect_key(
+ chroma_mean: list[float],
+ edges: list[tuple[list[float], list[float]]] | None = None,
+) -> tuple[str, str, int]:
+ """Find the key: profile correlation narrows it to a few close candidates,
+ and the song's edges decide between them, a key and its relative above all.
+
+ It used to multiply each key's correlation by how loud its root was. In a
+ minor song the relative major's root is the minor chord's third, and is
+ also in two of the other three chords of a typical loop, so it is usually
+ the louder one: every i-VI-III-VII progression came out as its relative
+ major, at up to 100% confidence (#726). Correlation alone is scale
+ invariant and does not have that pull; what it cannot do is tell relative
+ keys apart, which is the edges' job.
+
+ `edges` is optional: without it a near-tie between a key and its relative
+ falls back on the pop/rock minor prior.
Returns (label, scale_name, confidence_pct).
- - label: e.g. "G# maj"
- - scale_name: "Major" or "Natural Minor"
- - confidence_pct: 0-100, derived from the gap between the winning
- candidate and the runner-up, normalized so a clear
- win ranks high and a near-tie ranks low."""
- raw: list[tuple[float, float, str, int]] = [] # (weighted, pearson, label, root_idx)
- for shift in range(12):
- root_strength = chroma_mean[shift]
- pearson_maj = _correlate(_MAJOR_PROFILE, chroma_mean, shift)
- pearson_min = _correlate(_MINOR_PROFILE, chroma_mean, shift)
- # Multiplicative root weighting. Pearson can be negative; when
- # it is, a low-chroma root makes things less negative (closer to
- # zero), which is actually the desired ordering.
- raw.append((pearson_maj * root_strength, pearson_maj, f"{_PITCHES[shift]} maj", shift))
- raw.append((pearson_min * root_strength, pearson_min, f"{_PITCHES[shift]} min", shift))
- raw.sort(key=lambda x: x[0], reverse=True)
-
- # Diagnostic log: chroma profile + top 5 candidates with both raw
- # and weighted scores. Lets us see what the algorithm is "hearing".
+ - label: e.g. "G# maj"
+ - scale_name: "Major", "Natural Minor" or "Harmonic Minor"
+ - confidence_pct: 0-100
+ """
+ candidates = _key_candidates(chroma_mean)
+ best = candidates[0]
+ rel_root, rel_mode = _relative(best[1], best[2])
+ rel = next(c for c in candidates if c[1] == rel_root and c[2] == rel_mode)
+
chroma_str = ", ".join(f"{_PITCHES[i]}={chroma_mean[i]:.3f}" for i in range(12))
- top5_str = ", ".join(
- f"{label}={weighted:+.3f}(p{pearson:+.2f}*r{chroma_mean[idx]:.2f})"
- for weighted, pearson, label, idx in raw[:5]
- )
+ top5_str = ", ".join(f"{_PITCHES[r]} {m}={p:+.3f}" for p, r, m in candidates[:5])
logger.debug("chroma: %s", chroma_str)
logger.debug("key candidates (top 5): %s", top5_str)
- # Pick best major and best minor for the tie-break, both by the
- # weighted score.
- best_maj = next(c for c in raw if c[2].endswith("maj"))
- best_min = next(c for c in raw if c[2].endswith("min"))
-
- gap = abs(best_maj[0] - best_min[0])
- threshold = max(abs(best_maj[0]), abs(best_min[0])) * _MINOR_TIE_BREAK_FRAC
- # Near-tie -> prefer minor (pop/rock prior); clear winner -> use it.
- winner = (best_maj if best_maj[0] > best_min[0] else best_min) if gap > threshold else best_min
-
- # Confidence: gap between the winner and the runner-up that *isn't*
- # the relative major/minor of the winner (those will always be near-
- # ties with the algorithm's profile-correlation approach, so they
- # tell us nothing about real ambiguity). Normalize so a healthy 0.15
- # gap = 100% confident; tiny gap = 0%.
- runner_up = next(c for c in raw if c[2] != winner[2])
- confidence_score = winner[0] - runner_up[0]
- confidence_pct = max(0, min(100, round(confidence_score / 0.15 * 100)))
-
- label = winner[2]
- scale_name = "Major" if label.endswith("maj") else "Natural Minor"
- return label, scale_name, confidence_pct
+ winner = best
+ # How clearly the winner was separated from its closest rival, 0-1.
+ clarity = 1.0
+ if edges:
+ by_key = {(c[1], c[2]): c for c in candidates}
+ pool = set()
+ for c in candidates:
+ if best[0] - c[0] <= _FAMILY_WINDOW:
+ pool.add((c[1], c[2]))
+ pool.add(_relative(c[1], c[2]))
+ scored = sorted(
+ (
+ (by_key[k][0] + _EDGE_WEIGHT * _tonic_fit(k[0], k[1], edges), by_key[k])
+ for k in pool
+ ),
+ key=lambda s: s[0],
+ reverse=True,
+ )
+ logger.debug(
+ "with edges: %s",
+ ", ".join(f"{_PITCHES[c[1]]} {c[2]}={score:.3f}" for score, c in scored[:5]),
+ )
+ winner = scored[0][1]
+ runner = scored[1][0] if len(scored) > 1 else scored[0][0] - _EDGE_CONFIDENT_MARGIN
+ clarity = min(1.0, (scored[0][0] - runner) / _EDGE_CONFIDENT_MARGIN)
+ elif best[0] - rel[0] < _RELATIVE_MARGIN:
+ # Near-tie with nothing else to go on: prefer minor, the pop/rock
+ # prior ("Come As You Are" hammers the open D of an E minor song).
+ threshold = max(abs(best[0]), abs(rel[0])) * _MINOR_TIE_BREAK_FRAC
+ if best[0] - rel[0] <= threshold and rel[2] == "min":
+ winner = rel
+ clarity = 0.5
+
+ root, mode = winner[1], winner[2]
+ label = f"{_PITCHES[root]} {mode}"
+
+ scale_name = "Major"
+ if mode == "min":
+ # The raised seventh is the harmonic minor's signature (a major V);
+ # the natural minor's is the flat seventh. Only called when the raised
+ # one clearly dominates and is actually sounding.
+ raised = chroma_mean[(root + 11) % 12]
+ flat = chroma_mean[(root + 10) % 12]
+ peak = max(chroma_mean) or 1.0
+ harmonic = raised > _HARMONIC_RATIO * flat and raised > _HARMONIC_FLOOR * peak
+ scale_name = "Harmonic Minor" if harmonic else "Natural Minor"
+
+ # Confidence: how clearly the key family won, against the best candidate
+ # that is neither the winner nor its relative (relative keys correlate
+ # alike by construction, so they say nothing about the family), then
+ # scaled by how clearly the pair itself was separated.
+ # With edges, the margin over the runner-up already covers both, since
+ # every close candidate was in the running.
+ if edges:
+ return label, scale_name, round(clarity * 100)
+ others = [c for c in candidates if (c[1], c[2]) not in {(best[1], best[2]), (rel[1], rel[2])}]
+ family_gap = best[0] - others[0][0]
+ confidence = min(1.0, max(0.0, family_gap / _CONFIDENCE_FULL_GAP)) * clarity
+ return label, scale_name, round(confidence * 100)
def _measure_loudness(y: object, sr: int) -> tuple[float | None, float | None]:
@@ -261,12 +354,11 @@ def analyze(job: Job, source: Path) -> tuple[int | None, str | None]:
# chroma_cqt is constant-Q based — better pitch resolution than
# chroma_stft, especially in the bass register where the open
# strings of a guitar live.
- chroma = librosa.feature.chroma_cqt(y=y_harmonic, sr=sr)
- chroma_mean = chroma.mean(axis=1).tolist()
- if any(chroma_mean):
- key, scale, key_confidence = _detect_key(chroma_mean)
- else:
- key, scale, key_confidence = None, None, None
+ # The decode stops at 180 s, so the song's last chord is only among
+ # the evidence when the whole song fitted.
+ whole_song = y.size < 179.5 * sr
+ detected = detect_key_from_audio(y_harmonic, None, sr, whole_song=whole_song)
+ key, scale, key_confidence = detected if detected else (None, None, None)
# LUFS / peak. Computed on the same 22 kHz mono buffer; this
# loses a few dB of accuracy vs full-sample-rate stereo, but
@@ -310,3 +402,143 @@ def analyze(job: Job, source: Path) -> tuple[int | None, str | None]:
logger.exception("analyze failed for job %s", job.id)
_set(job, stage="Analysis skipped")
return None, None
+
+
+def _key_evidence(
+ harmony: object,
+ bass: object | None,
+ sr: int,
+ whole_song: bool,
+) -> tuple[list[float], list[tuple[list[float], list[float]]]]:
+ """The whole-song chroma, and the chroma and bass chroma at the edges of
+ the music, for _detect_key.
+
+ `bass` is the separated bass stem when there is one. Without it, the low
+ three octaves of `harmony` stand in for it. The end of the song is only an
+ edge when `whole_song`: a decode capped part way through ends mid-song.
+ """
+ import librosa
+ import numpy as np
+
+ # 93 ms frames. Key needs no finer: measured on a 9.5 min track's stems,
+ # 512 took 8.6 s for both chromas and 2048 took 4.1 s, with the same
+ # score on tests/keyset.py.
+ hop = 2048
+ chroma = librosa.feature.chroma_cqt(y=harmony, sr=sr, hop_length=hop)
+ low = bass if bass is not None else harmony
+ bass_chroma = librosa.feature.chroma_cqt(
+ y=low, sr=sr, hop_length=hop, fmin=librosa.note_to_hz("C1"), n_octaves=3
+ )
+ level = librosa.feature.rms(y=harmony if bass is None else harmony + bass, hop_length=hop)[0]
+ frames = min(chroma.shape[1], bass_chroma.shape[1], level.shape[0])
+ chroma, bass_chroma, level = chroma[:, :frames], bass_chroma[:, :frames], level[:frames]
+
+ # Music, not the silence or count-in before it: frames within 20 dB of
+ # the loudest.
+ active = np.flatnonzero(level > 0.1 * (level.max() or 1.0))
+ span = max(1, int(_EDGE_SECONDS * sr / hop))
+
+ def region(a: int, b: int) -> tuple[list[float], list[float]]:
+ h = chroma[:, a:b].mean(axis=1)
+ lo = bass_chroma[:, a:b].mean(axis=1)
+ return (h / (h.max() or 1.0)).tolist(), (lo / (lo.max() or 1.0)).tolist()
+
+ edges = []
+ if active.size:
+ start = int(active[0])
+ edges.append(region(start, start + span))
+ if whole_song:
+ end = int(active[-1]) + 1
+ edges.append(region(max(0, end - span), end))
+ return chroma.mean(axis=1).tolist(), edges
+
+
+def detect_key_from_audio(
+ harmony: object, bass: object | None, sr: int, whole_song: bool = True
+) -> tuple[str, str, int] | None:
+ """Key, scale and confidence from mono audio, or None for silence."""
+ chroma_mean, edges = _key_evidence(harmony, bass, sr, whole_song)
+ if not any(chroma_mean):
+ return None
+ return _detect_key(chroma_mean, edges)
+
+
+# Stems that carry harmony. Vocals count for half: a melody leans on the tonic
+# at phrase ends, but it also dwells on passing notes.
+_HARMONY_STEMS = {"guitar": 1.0, "piano": 1.0, "other": 1.0, "vocals": 0.5}
+# The stems are decoded whole up to this long, so the end of the song counts.
+_STEM_KEY_MAX_SECONDS = 600.0
+
+
+def refine_key_from_stems(job: Job, stems_dir: Path) -> None:
+ """Detect the key again from the separated stems, and keep it if it works.
+
+ The first estimate comes from the full mix before separation, so the key
+ shows while separation runs. The stems are better evidence: the bass line
+ on its own says which note is home, drums no longer smear the chroma, and
+ the whole song, including its last chord, is available rather than the
+ first 180 s. Any failure keeps the first estimate.
+ """
+ import numpy as np
+
+ sr = 22050
+ try:
+ harmony = None
+ heard = []
+ longest = 0
+ for name, weight in _HARMONY_STEMS.items():
+ path = stems_dir / f"{name}.wav"
+ if not path.is_file():
+ continue
+ loaded = _load_audio_ffmpeg(path, sr=sr, duration=_STEM_KEY_MAX_SECONDS)
+ if loaded is None:
+ continue
+ y = loaded[0] * weight
+ longest = max(longest, y.size)
+ harmony = y if harmony is None else _sum_padded(harmony, y)
+ heard.append(name)
+ if harmony is None:
+ logger.info("key from stems: no harmony stems for job %s, keeping %s", job.id, job.key)
+ return
+ bass = None
+ bass_path = stems_dir / "bass.wav"
+ if bass_path.is_file():
+ loaded = _load_audio_ffmpeg(bass_path, sr=sr, duration=_STEM_KEY_MAX_SECONDS)
+ if loaded is not None:
+ bass = loaded[0]
+ harmony, bass = _pad_pair(harmony, bass)
+ if bass is None:
+ logger.info(
+ "key from stems: no bass stem for job %s, using the harmony's low end", job.id
+ )
+ whole = longest < (_STEM_KEY_MAX_SECONDS - 1) * sr
+ result = detect_key_from_audio(harmony.astype(np.float32), bass, sr, whole_song=whole)
+ if result is None:
+ return
+ key, scale, confidence = result
+ if key != job.key or scale != job.scale:
+ logger.info(
+ "key from stems (%s%s): %s %s %s%% (mix said %s %s)",
+ "+".join(heard),
+ "+bass" if bass is not None else "",
+ key,
+ scale,
+ confidence,
+ job.key,
+ job.scale,
+ )
+ _set(job, key=key, scale=scale, key_confidence=confidence)
+ except Exception:
+ logger.exception("key from stems failed for job %s, keeping %s", job.id, job.key)
+
+
+def _sum_padded(a: object, b: object) -> object:
+ a, b = _pad_pair(a, b)
+ return a + b
+
+
+def _pad_pair(a: object, b: object) -> tuple[object, object]:
+ import numpy as np
+
+ n = max(a.size, b.size) # type: ignore[attr-defined]
+ return np.pad(a, (0, n - a.size)), np.pad(b, (0, n - b.size)) # type: ignore[attr-defined]
diff --git a/app/pipeline/runner.py b/app/pipeline/runner.py
index 83a70b0f..ab484902 100644
--- a/app/pipeline/runner.py
+++ b/app/pipeline/runner.py
@@ -21,7 +21,7 @@
from app.core.redact import redact
from app.core.registry import is_upload, set_proc
from app.core.registry import persist as persist_registry
-from app.pipeline.analyze import analyze
+from app.pipeline.analyze import analyze, refine_key_from_stems
from app.pipeline.beatgrid import compute_beat_grid
from app.pipeline.collect import (
cleanup_source,
@@ -222,6 +222,12 @@ def _run_common(job: Job, source: Path, job_dir: Path) -> None:
cleanup_source(job_dir)
job.stems = [{"name": name, "url": f"/api/jobs/{job.id}/stems/{name}.wav"} for name in found]
_check_cancel(job)
+ # The key was first estimated from the mix, before separation. The stems
+ # are better evidence, the bass line above all (#726). Keeps the first
+ # estimate on any failure. Counted in "post", not as a stage of its own:
+ # the timings' order is a contract (tests/test_identify_regressions.py).
+ refine_key_from_stems(job, stems_dir)
+ _check_cancel(job)
_set(job, stage="Mixing tracks...")
original_path = make_original_track(job, job_dir, stems_dir)
if original_path is not None:
diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs
index b1ad729c..442e0c0f 100644
--- a/desktop/src-tauri/src/main.rs
+++ b/desktop/src-tauri/src/main.rs
@@ -1,6 +1,8 @@
mod certs;
mod dragout;
mod dropin;
+#[cfg(any(not(target_os = "macos"), test))]
+mod torchswap;
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
@@ -1945,6 +1947,27 @@ fn ensure_torch_device(
#[cfg(not(target_os = "macos"))]
{
+ let site = torch_site_packages(&python);
+ if site.is_none() {
+ append_to_setup_log(
+ &data_dir,
+ "could not locate site-packages; torch swaps overlay",
+ );
+ }
+ // A swap that was interrupted (crash, forced quit, a rollback that did
+ // not finish) is put back before anything else looks at torch.
+ if let Some(site) = site.as_deref().filter(|s| torchswap::has_backup(s)) {
+ match torchswap::restore(site) {
+ Ok(()) => append_to_setup_log(
+ &data_dir,
+ "restored the bundled torch left aside by an interrupted swap",
+ ),
+ Err(e) => append_to_setup_log(
+ &data_dir,
+ &format!("could not restore an interrupted torch swap ({e})"),
+ ),
+ }
+ }
let setup = match detect_nvidia_gpu(&data_dir) {
Some((gpu_name, cuda_version, compute_cap)) => {
// Try each candidate wheel in turn. A build that installs but
@@ -1959,7 +1982,20 @@ fn ensure_torch_device(
// reads in a bug report and sends them after the wrong thing
// (#502: the install had been killed mid-download).
let mut cuda_installed = false;
- for tag in &candidates {
+ // One entry per candidate tried: None when it did not install,
+ // otherwise what verification said. Decides the reason below.
+ let mut outcomes: Vec