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.
Complete export jobs are serialized to bound browser CPU and memory pressure. Probe and collection songs receive an explicit context, never connect to Destination, and use silent per-song instrument collectors. Barline never swaps Tone’s ambient live context, so preparation cannot send MIDI, touch hardware returns, or rebind live playback while a bounce is running. Queueing, preload, render, and encode all honor the same cancellation signal.
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 makes the delivery window explicit:
interface RenderOptions { bpm: number; key?: string; sourceRevision?: string; // authoritative project revision sampleRate?: number; // 8–192 kHz tailSeconds?: number | "auto" | "none"; // default "auto" bars?: number; barStart?: number; // inclusive barEnd?: number; // exclusive allowHardwareSilence?: boolean; // default false}The default window is one complete arrangement pass. "auto" tail follows the
longest conservative track/send/return/master path, including declared device
tails, and is capped at 120 seconds to protect browser memory.
Master and measurement renders fail when an unfrozen MIDI or external track has
no offline audio. Print/freeze the hardware first. allowHardwareSilence: true
is an explicit diagnostic escape hatch and records every omitted route in
provenance.
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 while queued, preparing assets, rendering, or encoding (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 wav?: { bitDepth?: 16 | 24 | "float32"; dither?: "none" | "tpdf"; };}
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}Cancellation does not detach an async song definition or preload from the serialization lock. Already-started work settles before the next export begins, so a canceled graph cannot overlap its successor.
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; provenance: RenderProvenance }.
Float WAV preserves finite over-range samples; integer delivery clips to its
representable range. TPDF is available for 16/24-bit quantization and disabled
for float.
renderStems(define, opts, controls?) — explicit stem topology
Section titled “renderStems(define, opts, controls?) — explicit stem topology”Renders targets sequentially in fresh offline graphs. Choose a topology instead of relying on an ambiguous meaning of “stem”:
| Topology | Signal delivered |
|---|---|
post-master | Selected track, sends/returns, and master chain |
pre-master | Selected track and sends/returns, bypassing master effects |
pre-fader | Selected track before its mixer fader; sends disabled |
post-fader | Selected track after fader/pan; sends and master disabled |
return | Selected return after its fader; all feeding sends active |
return-pre-fader | Selected return before its fader |
const controller = new AbortController();const stems = await renderStems( build, { bpm: 132, sampleRate: 48000 }, { signal: controller.signal, topology: "pre-master", names: ["kick", "bass"], wav: { bitDepth: 24, dither: "tpdf" }, },);for (const { track, topology, blob, lufs, provenance } of stems) { console.log(track, lufs.integrated.toFixed(1), "LUFS");}For long or high-rate sessions, set retainStems: false and consume each
completed stem through onStem. This keeps only one encoded stem live at a
time while preserving sequential progress and per-stem provenance.
requireOfflineAudio: true rejects MIDI and unprinted external tracks instead
of silently producing empty freeze audio.
Render provenance
Section titled “Render provenance”Every master and stem records:
- authoritative source revision, runtime graph/musical fingerprint, and render timestamp;
- sample rate/channels, tempo/key, bar window, musical duration, and tail;
- topology/target plus WAV bit depth and dither;
- referenced assets and devices;
- every path’s intrinsic, compensation, and total latency samples;
- BS.1770 loudness;
- actual FAUST/worklet/instrument fallbacks plus structured asset, stretch, and device degradations used by that render.
Use renderProvenanceBlob(result.provenance) to download the JSON sidecar. The
Studio export menu saves it beside WAV and lossy masters, source-selection
renders, and stem sets. Managed bounce/freeze assets retain it in metadata, and
MCP render/measure tools return it to the agent.
Source-linked freeze
Section titled “Source-linked freeze”Studio can freeze a track or return from the Export menu. It renders the target
pre-fader to float WAV, uploads and pins the immutable asset, then writes a
freeze property into the target’s normal TypeScript:
freeze: { asset: "user/<hash>", bpm: 132, bars: 32, sourceRevision: "<project fingerprint>", sampleRate: 48000, headTrimSamples: 144,}Playback substitutes the cache at the same compensated musical sample while leaving fader, pan, mute/solo, meters, and sends live. Delete the property (or choose Unfreeze) to restore the untouched source graph. External tracks must first be printed through a calibrated return.
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. Source fingerprints, fallback disclosure, and provenance make that
mastering loop auditable as well as reproducible.