0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 1import type { Song, Track } from '../types.ts';
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 2import type { Instrument, NoteSpec, RenderProgress } from '../instruments/types.ts';
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 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 };
22}
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 24export interface RenderTrackOpts {
25 signal?: AbortSignal;
26 onProgress?: (p: RenderProgress) => void;
27}
29export async function renderTrackBuffer(song: Song, track: Track, instrument: Instrument, sampleRate: number, quality: 'preview' | 'full' = 'preview', opts: RenderTrackOpts = {}): Promise<Float32Array> {
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 30 const { notes, lengthSec } = noteSpecsForTrack(song, track);
31 const lengthSamples = Math.max(1, Math.ceil(lengthSec * sampleRate));
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 32 const { audio: buf } = await instrument.renderTrack({ notes, sampleRate, lengthSamples, quality, settings: track.settings ?? {}, signal: opts.signal, onProgress: opts.onProgress });
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 33 // Level each track to a common peak so mixing is predictable.
34 let peak = 0;
35 for (let i = 0; i < buf.length; i++) peak = Math.max(peak, Math.abs(buf[i]));
36 if (peak > 0) {
37 const s = 0.5 / peak;
38 const out = new Float32Array(buf.length);
39 for (let i = 0; i < buf.length; i++) out[i] = buf[i] * s;
40 return out;
41 }
42 return buf;
43}
45export function audibleTracks(tracks: Track[]): Set<string> {
46 const solo = tracks.filter((t) => t.solo);
47 const audible = new Set<string>();
48 for (const t of tracks) {
49 if (solo.length > 0 ? t.solo : !t.mute) audible.add(t.id);
50 }
51 return audible;
52}
54export function mixTracks(tracks: Track[], buffers: Map<string, Float32Array>): Float32Array {
55 const audible = audibleTracks(tracks);
56 let length = 1;
57 for (const t of tracks) {
58 const b = buffers.get(t.id);
59 if (b && audible.has(t.id)) length = Math.max(length, b.length);
60 }
61 const out = new Float32Array(length);
62 for (const t of tracks) {
63 if (!audible.has(t.id)) continue;
64 const b = buffers.get(t.id);
65 if (!b) continue;
66 for (let i = 0; i < b.length; i++) out[i] += b[i] * t.volume;
67 }
68 let peak = 0;
69 for (let i = 0; i < out.length; i++) peak = Math.max(peak, Math.abs(out[i]));
70 if (peak > 0.98) {
71 const s = 0.98 / peak;
72 for (let i = 0; i < out.length; i++) out[i] *= s;
73 }
74 return out;
75}
77export function trackRenderKey(song: Song, track: Track): string {
78 // FNV-1a over everything that affects the rendered audio.
78851f6Replace instrawave with simulated instruments driven by manifestsJeremy Magland 79 const parts = [String(song.ppq), track.instrumentId, JSON.stringify(song.tempo), JSON.stringify(track.settings ?? {})];
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 80 for (const n of track.notes) parts.push(`${n.tick},${n.duration},${n.pitch},${n.velocity}`);
81 let h = 0x811c9dc5;
82 const s = parts.join(';');
83 for (let i = 0; i < s.length; i++) {
84 h ^= s.charCodeAt(i);
85 h = Math.imul(h, 0x01000193);
86 }
87 return (h >>> 0).toString(36) + ':' + s.length.toString(36);
88}
90export function encodeWavMono16(samples: Float32Array, sampleRate: number): Uint8Array {
91 const n = samples.length;
92 const out = new Uint8Array(44 + n * 2);
93 const dv = new DataView(out.buffer);
94 const str = (off: number, s: string) => { for (let i = 0; i < s.length; i++) out[off + i] = s.charCodeAt(i); };
95 str(0, 'RIFF');
96 dv.setUint32(4, 36 + n * 2, true);
97 str(8, 'WAVE');
98 str(12, 'fmt ');
99 dv.setUint32(16, 16, true);
100 dv.setUint16(20, 1, true);
101 dv.setUint16(22, 1, true);
102 dv.setUint32(24, sampleRate, true);
103 dv.setUint32(28, sampleRate * 2, true);
104 dv.setUint16(32, 2, true);
105 dv.setUint16(34, 16, true);
106 str(36, 'data');
107 dv.setUint32(40, n * 2, true);
108 for (let i = 0; i < n; i++) {
109 const v = Math.max(-1, Math.min(1, samples[i]));
110 dv.setInt16(44 + i * 2, Math.round(v * 32767), true);
111 }
112 return out;
113}