feat(align): warped MIDI rendering and peaks
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ca5942956a
commit
63b32a488d
@@ -0,0 +1,48 @@
|
||||
import * as at from "@coderline/alphatab";
|
||||
import type { Audio } from "../core/types.ts";
|
||||
import { renderMidiFile } from "../audio/render.ts";
|
||||
import type { RefTrack } from "./smf.ts";
|
||||
import type { Warp } from "./warp.ts";
|
||||
|
||||
const TPS = 1920; // division 960 at a fixed 120 bpm
|
||||
const PEAK = Math.pow(10, -1 / 20);
|
||||
const MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
/**
|
||||
* Render reference tracks to audio with `warp` applied (MIDI time -> audio time). Notes are placed on a fixed-tempo
|
||||
* tick grid so alphaSynth output time equals audio time. Peak-normalised to -1 dBFS; `durationSec` pads/trims.
|
||||
* Pure: no I/O. Notes landing before audio t=0 are clamped to 0 (dropped if they end before it).
|
||||
*/
|
||||
export function renderMidi(
|
||||
tracks: RefTrack[], warp: Warp, sf2: Uint8Array,
|
||||
opts: { sampleRate?: number; mono?: boolean; durationSec?: number } = {},
|
||||
): Audio {
|
||||
const sampleRate = opts.sampleRate ?? 44100;
|
||||
const midi = new at.midi.MidiFile();
|
||||
midi.division = 960;
|
||||
midi.addEvent(new at.midi.TempoChangeEvent(0, 500000));
|
||||
const seen = new Set<number>();
|
||||
let next = 0;
|
||||
for (const t of tracks) {
|
||||
const ch = t.isDrums ? 9 : MELODIC_CHANNELS[next++ % MELODIC_CHANNELS.length]!;
|
||||
if (!seen.has(ch)) { seen.add(ch); midi.addEvent(new at.midi.ProgramChangeEvent(0, 0, ch, t.isDrums ? 0 : t.program)); }
|
||||
for (const n of t.notes) {
|
||||
const on = Math.round(Math.max(0, warp.toAudio(n.onset)) * TPS);
|
||||
const off = Math.max(on + 1, Math.round(warp.toAudio(n.offset) * TPS));
|
||||
if (warp.toAudio(n.offset) <= 0) continue;
|
||||
midi.addEvent(new at.midi.NoteOnEvent(0, on, ch, n.pitch, n.velocity));
|
||||
midi.addEvent(new at.midi.NoteOffEvent(0, off, ch, n.pitch, 0));
|
||||
}
|
||||
}
|
||||
const frames = opts.durationSec === undefined ? undefined : Math.round(opts.durationSec * sampleRate);
|
||||
const out = renderMidiFile(midi, sf2, sampleRate, { frames });
|
||||
let chans = out.channels;
|
||||
if (opts.mono) {
|
||||
const [L, R] = chans as [Float32Array, Float32Array];
|
||||
chans = [L.map((v, i) => (v + R[i]!) / 2)];
|
||||
}
|
||||
let peak = 0;
|
||||
for (const c of chans) for (let i = 0; i < c.length; i++) { const a = Math.abs(c[i]!); if (a > peak) peak = a; }
|
||||
if (peak > 0) { const g = PEAK / peak; for (const c of chans) for (let i = 0; i < c.length; i++) c[i]! *= g; }
|
||||
return { sampleRate, channels: chans };
|
||||
}
|
||||
Reference in New Issue
Block a user