Export & loudness
Export is fully offline: the song renders faster than real time into a WAV, and every render also measures its own loudness. The AI uses that loudness number to master to a target without ever leaving the listening loop.
Both renderers take the song builder (define), the render options, and an
optional ExportControls:
import { renderToWav, renderStems } from "@barline/runtime";import build from "./my-song"; // the song's default export: (song: Song) => void
const { blob, lufs } = await renderToWav(build, { bpm: 132 });RenderOptions is { bpm: number; key?: string; bars?: number } — bars
overrides the render length (default: one pass of the arrangement).
Staying responsive — ExportControls
Section titled “Staying responsive — ExportControls”A long render stays responsive because progress is reported as it goes and the work can be cancelled mid-flight (ADR-021 — the encode is chunked so the main thread never blocks):
interface ExportControls { onProgress?(progress: RenderProgress): void; signal?: AbortSignal; // abort to cancel a render in progress}
interface RenderProgress { phase: "render" | "encode"; // the offline DSP pass, then the WAV write fraction: number; // 0..1 within the current phase stemIndex?: number; // stems only: 1-based track index stemCount?: number; // stems only: total tracks track?: string; // stems only: track being rendered}renderToWav(define, opts, controls?) — one master WAV
Section titled “renderToWav(define, opts, controls?) — one master WAV”Renders the whole arrangement through the master chain into a single file and measures its loudness.
const { blob, lufs } = await renderToWav(build, { bpm: 132 }, { onProgress: (p) => console.log(p.phase, `${Math.round(p.fraction * 100)}%`),});// blob: the master WAV; lufs: LoudnessResultRenderResult is { blob: Blob; lufs: LoudnessResult }.
renderStems(define, opts, controls?) — per-track stems
Section titled “renderStems(define, opts, controls?) — per-track stems”Renders each track in isolation (still through its own device chain) and returns one result per track, each with its own loudness.
const controller = new AbortController();const stems = await renderStems(build, { bpm: 132 }, { signal: controller.signal });for (const { track, blob, lufs } of stems) { console.log(track, lufs.integrated.toFixed(1), "LUFS");}// stems: { track: string; blob: Blob; lufs: LoudnessResult }[]Loudness — ITU-R BS.1770-4
Section titled “Loudness — ITU-R BS.1770-4”Every render measures loudness with a true BS.1770-4 meter, so the numbers match what a mastering engineer’s meter would read.
interface LoudnessResult { integrated: number; // whole-track loudness — the mastering-loop target shortTerm: number; // the final ~3 s — catches drops/outro level truePeak: number; // max inter-sample peak, dBTP — keep under ~ -1}integratedis the loudness of the entire track. This is the number you master toward — push it to your genre target and stop.shortTermis the loudness of the last ~3 seconds, so you can tell whether the track lands hot or drops out at the end.truePeakis the BS.1770-4 inter-sample (true) peak in dBTP, measured with 4× oversampling — the peak a DAC or a lossy transcode actually produces, which a plain sample-peak misses. Keep it under about −1 dBTP so the master survives MP3/AAC encoding without clipping.
get_status (and so the listening loop) surfaces these
as integrated_lufs, short_term_lufs, and true_peak, plus master —
instantaneous spectral features of the live master in the same units as a
reference track’s features. That closes the mastering loop: the AI renders or
plays, reads the LUFS / dBTP / spectral delta, adjusts the master chain, and goes
again. A typical techno target is around -8 to -6 LUFS integrated at −1 dBTP.
The master chain
Section titled “The master chain”By default the master bus runs three effects — DEFAULT_MASTER_CHAIN:
glue // bus/glue compressor — glues the mix, makeup gaintilt // single-knob air<->weight tilt EQ — "air" on topmaximizer // brickwall ceiling — the last gain stage before the meterglue and maximizer are what move the integrated LUFS: the compressor levels
the mix and the maximizer raises the ceiling. tilt shapes tone without touching
loudness much. See Effects for every parameter.
Replace the whole chain with setMasterEffects — it is a full replacement, not
a merge, so include everything you still want:
import { fx } from "@barline/core";import type { Song } from "@barline/runtime";
export const config = { bpm: 132, key: "A minor" };
export default function (song: Song) { // ... tracks ...
// push for a hot, glued master, then re-measure with renderToWav song.setMasterEffects([ fx.glue({ threshold: -16, ratio: 2.5, makeup: 1.5 }), fx.tilt({ tilt: 1.2 }), fx.maximizer({ threshold: -0.8, lookahead: 3, release: 80 }), ]);}Render is deterministic offline: the FAUST reverbs (space/hall/plate) and
the convolver resolve to fixed impulse responses rather than fx.reverb’s
real-time tail, so the same song always renders to the same WAV — and the same
LUFS. That is what makes the mastering loop reproducible.