Skip to content

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).

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: LoudnessResult

RenderResult 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 }[]

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
}
  • integrated is the loudness of the entire track. This is the number you master toward — push it to your genre target and stop.
  • shortTerm is the loudness of the last ~3 seconds, so you can tell whether the track lands hot or drops out at the end.
  • truePeak is 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.

By default the master bus runs three effects — DEFAULT_MASTER_CHAIN:

glue // bus/glue compressor — glues the mix, makeup gain
tilt // single-knob air<->weight tilt EQ — "air" on top
maximizer // brickwall ceiling — the last gain stage before the meter

glue 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.