diff --git a/src/pipeline/run.ts b/src/pipeline/run.ts index 1a9bdf1..bee9768 100644 --- a/src/pipeline/run.ts +++ b/src/pipeline/run.ts @@ -40,6 +40,19 @@ export function copyFingering(timed: TimedNote[], assigned: ScoreNote[], grid: G }); } +/** Sample-wise sum of equally shaped audio buffers. */ +export function sumAudio(...as: Audio[]): Audio { + const first = as[0]!; + return { + sampleRate: first.sampleRate, + channels: first.channels.map((c, k) => { + const o = new Float32Array(c.length); + for (const a of as) { const x = a.channels[k]!; for (let i = 0; i < o.length; i++) o[i]! += x[i]!; } + return o; + }), + }; +} + export async function createPipeline( models: { demucs: Uint8Array; basicPitch: Uint8Array }, ): Promise<(input: PipelineInput, flags: OracleFlags) => Promise> { @@ -63,9 +76,10 @@ export async function createPipeline( } } else { const s = await separate(input.mix); - warnings.push('demucs "other" stem unused'); const mk = (name: string, role: Role, audio: Audio): Job => ({ name, role, audio, truthNotes: null }); - jobs.push(mk("Guitar", "guitar", s.guitar), mk("Bass", "bass", s.bass), mk("Keys", "keys", s.piano), + // Guitar is transcribed from guitar + piano + other: Demucs-6s scatters guitar energy across all three + // (synthetic t1 median guitar F1 0.39–0.60 → 0.72–0.87; real "Last Night on Earth" 0.390 → 0.403). + jobs.push(mk("Guitar", "guitar", sumAudio(s.guitar, s.piano, s.other)), mk("Bass", "bass", s.bass), mk("Keys", "keys", s.piano), mk("Vocals", "vocals", s.vocals), mk("Drums", "drums", s.drums)); } diff --git a/tests/pipeline/run.test.ts b/tests/pipeline/run.test.ts index aab9904..d5c35d8 100644 --- a/tests/pipeline/run.test.ts +++ b/tests/pipeline/run.test.ts @@ -1,7 +1,7 @@ import { test, expect, beforeAll } from "bun:test"; import { join } from "node:path"; import { buildSong, loadSongDir } from "../../src/bench/dataset.ts"; -import { createPipeline, copyFingering } from "../../src/pipeline/run.ts"; +import { createPipeline, copyFingering, sumAudio } from "../../src/pipeline/run.ts"; import { snapTicks, quantize } from "../../src/pipeline/quantize.ts"; import type { TimedNote } from "../../src/core/types.ts"; import { truthFromGp } from "../../src/score/gp.ts"; @@ -47,9 +47,9 @@ test("t1c/1 full pipeline yields all roles", async () => { const roles = new Set(output.tracks.map((t) => t.role)); for (const r of ["guitar", "bass", "keys", "vocals", "drums"]) expect(roles.has(r as never)).toBe(true); expect(gp.length).toBeGreaterThan(0); - expect(warnings.some((w) => w.includes("other"))).toBe(true); + expect(warnings.some((w) => w.includes("other"))).toBe(false); expect(Math.abs(output.grid.bpm - d.truth.bpm)).toBeLessThan(2); - expect(s.guitarMerged.f1!).toBeGreaterThan(0.3); + expect(s.guitarMerged.f1!).toBeGreaterThan(0.6); // guitar from guitar+piano+other stems (was 0.57 from the guitar stem alone) }, SLOW); test("t1d/1 all oracle flags: two guitar tracks, partSplit and merged F1 === 1", async () => { @@ -90,3 +90,12 @@ test("copyFingering matches by snapped tick + pitch (merged cluster, adjacent re copyFingering(timed, assigned, grid); expect(timed.map((x) => [x.string, x.fret])).toEqual([[2, 0], [2, 0], [3, 240], [1, 0]]); }); + +test("sumAudio adds buffers sample-wise", () => { + const a = { sampleRate: 10, channels: [new Float32Array([1, 2]), new Float32Array([0, 1])] }; + const b = { sampleRate: 10, channels: [new Float32Array([3, -2]), new Float32Array([1, 1])] }; + const s = sumAudio(a, b); + expect([...s.channels[0]!]).toEqual([4, 0]); + expect([...s.channels[1]!]).toEqual([1, 2]); + expect(s.sampleRate).toBe(10); +});