feat(align): warped MIDI rendering and peaks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca5942956a
commit
63b32a488d
4 files changed
+149
-22
No files matched your search
+36
-22
@@ -5,7 +5,7 @@ import { songToScore } from "../score/gp.ts";
|
||||
const TAIL_SECONDS = 2;
|
||||
|
||||
/** Offline-render sink: AlphaSynth requires an ISynthOutput but exportAudio never uses it. */
|
||||
function dummyOutput(sampleRate: number): at.synth.ISynthOutput {
|
||||
export function dummyOutput(sampleRate: number): at.synth.ISynthOutput {
|
||||
const ev = { on() {}, off() {} };
|
||||
return {
|
||||
sampleRate, open() {}, play() {}, destroy() {}, pause() {}, addSamples() {}, resetSamples() {}, activate() {},
|
||||
@@ -18,6 +18,39 @@ function songSeconds(song: Song): number {
|
||||
return (song.bars * song.timeSig[0] * (4 / song.timeSig[1]) * 60) / song.bpm;
|
||||
}
|
||||
|
||||
/** Render a MidiFile offline. With `frames`, output is exactly that long (zero-padded or truncated); else full length. */
|
||||
export function renderMidiFile(
|
||||
midi: at.midi.MidiFile, sf2: Uint8Array, sampleRate: number,
|
||||
opts: { trackVolume?: Map<number, number>; frames?: number } = {},
|
||||
): Audio {
|
||||
const synth = new at.synth.AlphaSynth(dummyOutput(sampleRate), 500);
|
||||
const eo = new at.synth.AudioExportOptions();
|
||||
eo.soundFonts = [sf2];
|
||||
eo.sampleRate = sampleRate;
|
||||
eo.useSyncPoints = false;
|
||||
if (opts.trackVolume) eo.trackVolume = opts.trackVolume;
|
||||
const exporter = synth.exportAudio(eo, midi, [], new Map());
|
||||
const cap = opts.frames ?? Infinity;
|
||||
const Ls: Float32Array[] = [], Rs: Float32Array[] = [];
|
||||
let pos = 0;
|
||||
while (pos < cap) {
|
||||
const chunk = exporter.render(500);
|
||||
if (!chunk) break;
|
||||
const s = chunk.samples;
|
||||
const frames = Math.min(s.length >> 1, cap - pos);
|
||||
const l = new Float32Array(frames), r = new Float32Array(frames);
|
||||
for (let i = 0; i < frames; i++) { l[i] = s[2 * i]!; r[i] = s[2 * i + 1]!; }
|
||||
Ls.push(l); Rs.push(r);
|
||||
pos += frames;
|
||||
}
|
||||
synth.destroy();
|
||||
const n = opts.frames ?? pos;
|
||||
const L = new Float32Array(n), R = new Float32Array(n);
|
||||
let o = 0;
|
||||
for (let k = 0; k < Ls.length; k++) { L.set(Ls[k]!, o); R.set(Rs[k]!, o); o += Ls[k]!.length; }
|
||||
return { sampleRate, channels: [L, R] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render each track in isolation with alphaSynth. Isolation: `AudioExportOptions.trackVolume` is keyed by MIDI
|
||||
* channel (despite the doc's "track index"), so every channel not owned by the target track is muted (volume 0).
|
||||
@@ -36,27 +69,8 @@ export function renderStems(song: Song, sf2: Uint8Array, sampleRate = 44100): Ma
|
||||
const trackVolume = new Map<number, number>();
|
||||
for (let ch = 0; ch < 16; ch++) if (!own.has(ch)) trackVolume.set(ch, 0);
|
||||
|
||||
const synth = new at.synth.AlphaSynth(dummyOutput(sampleRate), 500);
|
||||
const opts = new at.synth.AudioExportOptions();
|
||||
opts.soundFonts = [sf2];
|
||||
opts.sampleRate = sampleRate;
|
||||
opts.useSyncPoints = false;
|
||||
opts.trackVolume = trackVolume;
|
||||
const exporter = synth.exportAudio(opts, midi, [], new Map());
|
||||
|
||||
const L = new Float32Array(total), R = new Float32Array(total);
|
||||
let pos = 0;
|
||||
for (;;) {
|
||||
const chunk = exporter.render(500);
|
||||
if (!chunk) break;
|
||||
const s = chunk.samples;
|
||||
const frames = Math.min(s.length >> 1, total - pos);
|
||||
for (let i = 0; i < frames; i++) { L[pos + i] = s[2 * i]!; R[pos + i] = s[2 * i + 1]!; }
|
||||
pos += frames;
|
||||
if (pos >= total) break;
|
||||
}
|
||||
synth.destroy();
|
||||
stems.set(track.name, { sampleRate, channels: [L, R] });
|
||||
const audio = renderMidiFile(midi, sf2, sampleRate, { trackVolume, frames: total });
|
||||
stems.set(track.name, audio);
|
||||
}
|
||||
return stems;
|
||||
}
|
||||
Reference in new issue
Block a user