import type { Song, Track } from '../types.ts'; import type { Instrument, NoteSpec, RenderProgress } from '../instruments/types.ts'; import { TempoMap } from '../midi/time.ts'; // Offline rendering: convert a track's notes to seconds under the tempo // map, hand them to the instrument's render, and level the result. Mixing // applies volume, mute, and solo, so those controls never cause a // re-render. export const RENDER_TAIL_SEC = 2; export function noteSpecsForTrack(song: Song, track: Track): { notes: NoteSpec[]; lengthSec: number } { const tm = new TempoMap(song.ppq, song.tempo, song.timeSig); let lengthSec = 0; const notes: NoteSpec[] = track.notes.map((n) => { const timeSec = tm.tickToSec(n.tick); const endSec = tm.tickToSec(n.tick + n.duration); lengthSec = Math.max(lengthSec, endSec); return { timeSec, durationSec: Math.max(0.01, endSec - timeSec), pitch: n.pitch, velocity: Math.max(0, Math.min(1, n.velocity / 127)) }; }); return { notes, lengthSec: lengthSec + RENDER_TAIL_SEC }; } export interface RenderTrackOpts { signal?: AbortSignal; onProgress?: (p: RenderProgress) => void; } export async function renderTrackBuffer(song: Song, track: Track, instrument: Instrument, sampleRate: number, quality: 'preview' | 'full' = 'preview', opts: RenderTrackOpts = {}): Promise { const { notes, lengthSec } = noteSpecsForTrack(song, track); const lengthSamples = Math.max(1, Math.ceil(lengthSec * sampleRate)); const { audio: buf } = await instrument.renderTrack({ notes, sampleRate, lengthSamples, quality, settings: track.settings ?? {}, signal: opts.signal, onProgress: opts.onProgress }); // Level each track to a common peak so mixing is predictable. let peak = 0; for (let i = 0; i < buf.length; i++) peak = Math.max(peak, Math.abs(buf[i])); if (peak > 0) { const s = 0.5 / peak; const out = new Float32Array(buf.length); for (let i = 0; i < buf.length; i++) out[i] = buf[i] * s; return out; } return buf; } export function audibleTracks(tracks: Track[]): Set { const solo = tracks.filter((t) => t.solo); const audible = new Set(); for (const t of tracks) { if (solo.length > 0 ? t.solo : !t.mute) audible.add(t.id); } return audible; } export function mixTracks(tracks: Track[], buffers: Map): Float32Array { const audible = audibleTracks(tracks); let length = 1; for (const t of tracks) { const b = buffers.get(t.id); if (b && audible.has(t.id)) length = Math.max(length, b.length); } const out = new Float32Array(length); for (const t of tracks) { if (!audible.has(t.id)) continue; const b = buffers.get(t.id); if (!b) continue; for (let i = 0; i < b.length; i++) out[i] += b[i] * t.volume; } let peak = 0; for (let i = 0; i < out.length; i++) peak = Math.max(peak, Math.abs(out[i])); if (peak > 0.98) { const s = 0.98 / peak; for (let i = 0; i < out.length; i++) out[i] *= s; } return out; } export function trackRenderKey(song: Song, track: Track): string { // FNV-1a over everything that affects the rendered audio. const parts = [String(song.ppq), track.instrumentId, JSON.stringify(song.tempo), JSON.stringify(track.settings ?? {})]; for (const n of track.notes) parts.push(`${n.tick},${n.duration},${n.pitch},${n.velocity}`); let h = 0x811c9dc5; const s = parts.join(';'); for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0).toString(36) + ':' + s.length.toString(36); } export function encodeWavMono16(samples: Float32Array, sampleRate: number): Uint8Array { const n = samples.length; const out = new Uint8Array(44 + n * 2); const dv = new DataView(out.buffer); const str = (off: number, s: string) => { for (let i = 0; i < s.length; i++) out[off + i] = s.charCodeAt(i); }; str(0, 'RIFF'); dv.setUint32(4, 36 + n * 2, true); str(8, 'WAVE'); str(12, 'fmt '); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true); dv.setUint32(24, sampleRate, true); dv.setUint32(28, sampleRate * 2, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true); str(36, 'data'); dv.setUint32(40, n * 2, true); for (let i = 0; i < n; i++) { const v = Math.max(-1, Math.min(1, samples[i])); dv.setInt16(44 + i * 2, Math.round(v * 32767), true); } return out; }