/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / audio / render.ts
108 lines · 4.0 KBBlameHistoryRaw
1import type { Song, Track } from '../types.ts';
2import type { Instrument, NoteSpec } from '../instruments/types.ts';
3import { TempoMap } from '../midi/time.ts';
5// Offline rendering: convert a track's notes to seconds under the tempo
6// map, hand them to the instrument's render, and level the result. Mixing
7// applies volume, mute, and solo, so those controls never cause a
8// re-render.
10export const RENDER_TAIL_SEC = 2;
12export function noteSpecsForTrack(song: Song, track: Track): { notes: NoteSpec[]; lengthSec: number } {
13 const tm = new TempoMap(song.ppq, song.tempo, song.timeSig);
14 let lengthSec = 0;
15 const notes: NoteSpec[] = track.notes.map((n) => {
16 const timeSec = tm.tickToSec(n.tick);
17 const endSec = tm.tickToSec(n.tick + n.duration);
18 lengthSec = Math.max(lengthSec, endSec);
19 return { timeSec, durationSec: Math.max(0.01, endSec - timeSec), pitch: n.pitch, velocity: Math.max(0, Math.min(1, n.velocity / 127)) };
20 });
21 return { notes, lengthSec: lengthSec + RENDER_TAIL_SEC };
24export async function renderTrackBuffer(song: Song, track: Track, instrument: Instrument, sampleRate: number): Promise<Float32Array> {
25 const { notes, lengthSec } = noteSpecsForTrack(song, track);
26 const lengthSamples = Math.max(1, Math.ceil(lengthSec * sampleRate));
27 const buf = await instrument.render({ notes, sampleRate, lengthSamples });
28 // Level each track to a common peak so mixing is predictable.
29 let peak = 0;
30 for (let i = 0; i < buf.length; i++) peak = Math.max(peak, Math.abs(buf[i]));
31 if (peak > 0) {
32 const s = 0.5 / peak;
33 const out = new Float32Array(buf.length);
34 for (let i = 0; i < buf.length; i++) out[i] = buf[i] * s;
35 return out;
36 }
37 return buf;
40export function audibleTracks(tracks: Track[]): Set<string> {
41 const solo = tracks.filter((t) => t.solo);
42 const audible = new Set<string>();
43 for (const t of tracks) {
44 if (solo.length > 0 ? t.solo : !t.mute) audible.add(t.id);
45 }
46 return audible;
49export function mixTracks(tracks: Track[], buffers: Map<string, Float32Array>): Float32Array {
50 const audible = audibleTracks(tracks);
51 let length = 1;
52 for (const t of tracks) {
53 const b = buffers.get(t.id);
54 if (b && audible.has(t.id)) length = Math.max(length, b.length);
55 }
56 const out = new Float32Array(length);
57 for (const t of tracks) {
58 if (!audible.has(t.id)) continue;
59 const b = buffers.get(t.id);
60 if (!b) continue;
61 for (let i = 0; i < b.length; i++) out[i] += b[i] * t.volume;
62 }
63 let peak = 0;
64 for (let i = 0; i < out.length; i++) peak = Math.max(peak, Math.abs(out[i]));
65 if (peak > 0.98) {
66 const s = 0.98 / peak;
67 for (let i = 0; i < out.length; i++) out[i] *= s;
68 }
69 return out;
72export function trackRenderKey(song: Song, track: Track): string {
73 // FNV-1a over everything that affects the rendered audio.
74 const parts = [String(song.ppq), track.instrumentId, JSON.stringify(song.tempo)];
75 for (const n of track.notes) parts.push(`${n.tick},${n.duration},${n.pitch},${n.velocity}`);
76 let h = 0x811c9dc5;
77 const s = parts.join(';');
78 for (let i = 0; i < s.length; i++) {
79 h ^= s.charCodeAt(i);
80 h = Math.imul(h, 0x01000193);
81 }
82 return (h >>> 0).toString(36) + ':' + s.length.toString(36);
85export function encodeWavMono16(samples: Float32Array, sampleRate: number): Uint8Array {
86 const n = samples.length;
87 const out = new Uint8Array(44 + n * 2);
88 const dv = new DataView(out.buffer);
89 const str = (off: number, s: string) => { for (let i = 0; i < s.length; i++) out[off + i] = s.charCodeAt(i); };
90 str(0, 'RIFF');
91 dv.setUint32(4, 36 + n * 2, true);
92 str(8, 'WAVE');
93 str(12, 'fmt ');
94 dv.setUint32(16, 16, true);
95 dv.setUint16(20, 1, true);
96 dv.setUint16(22, 1, true);
97 dv.setUint32(24, sampleRate, true);
98 dv.setUint32(28, sampleRate * 2, true);
99 dv.setUint16(32, 2, true);
100 dv.setUint16(34, 16, true);
101 str(36, 'data');
102 dv.setUint32(40, n * 2, true);
103 for (let i = 0; i < n; i++) {
104 const v = Math.max(-1, Math.min(1, samples[i]));
105 dv.setInt16(44 + i * 2, Math.round(v * 32767), true);
106 }
107 return out;
↑↓ move↵openescclose