Skip to content

Instruments & outputs

Every track has exactly one output: the thing that turns its notes into sound. It’s pure data — a tagged union, discriminated by kind — so the runtime maps it to the actual voice. Five kinds: synth, sampler, soundfont, midi, and external for hardware with an audio return.

import type { Song } from "@barline/runtime";
output: { kind: "synth", synth: { id: "kick", params: { volume: -12 } } }

synth.id names a built-in voice; synth.params is an optional Record<string, number | string> of per-instrument knobs (e.g. { volume: -12, note: 86 }). The nine drum/synth primitives:

idRole
kickThe four-on-the-floor anchor
hatClosed hi-hat tick
clapOff-beat backbeat clap
metalMetallic perc / ride hit
bassSub / rumble bass
leadMono lead line
kick909Punchier 909-style kick
acid303Squelchy 303-style mono
stabChord/organ stab

Nine voices are FAUST-compiled and 16-voice polyphonic — referenced by the same synth.id. The runtime lazily loads only the devices named by the song. If an artifact is unavailable it falls back to a Tone PolySynth, so playback never breaks:

idRole
subtractiveClassic subtractive poly
supersawDetuned supersaw stack
fmFM operator voice
pluckKarplus-style pluck
acid303 diode-ladder mono
hardkickDistorted hard-techno kick
prismDual-engine flagship synth: independent shapes/filters, cross-mod, routing, unison, drive, and space
drumForgeUnified kick/snare/clap/hat/percussion synthesis family
resonatorPlayable modal physical model with mallet and bow excitation
const lead = song.track({
name: "lead",
output: { kind: "synth", synth: { id: "supersaw", params: { volume: -8 } } },
});

Every FAUST voice supports deterministic pitch bend, pressure, timbre, glide, and legato. Attach those values to pattern events with express():

import { express, notes, beats } from "@barline/core";
const phrase = express(
(_event, index) => ({
pressure: index % 3 === 0 ? 0.8 : 0.3,
timbre: index / 8,
pitchBend: index % 2 === 0 ? 0 : 0.08,
glide: 0.04,
legato: index > 0,
}),
notes(["F3", "Ab3", "C4", "Eb4"], beats(0.5)),
);

Expression is pure event data. The generated FAUST processor queues complete timestamped note/expression packets on the audio thread, so live and offline rendering use the same timing model.

output: {
kind: "sampler",
samples: { "C3": "/api/uploads/<id>", "C4": "/api/uploads/<id>" },
}

samples is a note → URL map. The runtime pitches each zone to fill the gaps, so a few well-placed root notes cover a whole range. URLs are uploaded assets at /api/uploads/<id> — see the Library for how to upload them.

output: { kind: "soundfont", font: "<id>", program: 0 }

font is an uploaded .sf2/.sf3 (an /api/uploads/<id>); program is a General MIDI melodic program number, 0..127 (0 = acoustic grand piano). Upload soundfonts the same way as samples — both live in the unified Library.

output: { kind: "midi", channel: 1, port: "IAC Driver Bus 1" }

Sends note events out over Web MIDI on channel (1..16). port is optional — omit it to use the default output port. When an explicitly named port disappears, Barline reports it offline and sends nothing; it never silently reroutes the performance to another connected device.

Audio Setup can also run MIDI Clock as a leader or follower. The Sync Hub supports Start/Continue/Stop and Song Position Pointer at 24 PPQN and shows measured BPM, jitter, and drift. Arming follower mode confirms audio ownership before external transport can start playback. Start resets to zero; Continue resumes the retained Song Position Pointer, including sub-bar positions. A leader starting after a seek sends SPP plus Continue. Ableton Link is not part of the current contract.

external — MIDI out plus a calibrated audio return

Section titled “external — MIDI out plus a calibrated audio return”
output: {
kind: "external",
channel: 1,
port: "USB MIDI Interface",
returnRoute: "external-1",
hardwareLatencyMs: 1.4,
}

An external output sends the same timestamped note stream as midi, then binds the track to a logical return name. In Audio Setup, map external-1 to the physical input/output pair and run loopback calibration. Physical device ids and measured offsets stay machine-local; the song remains portable. Changing either physical side revokes the old calibration and print approval until the new route is measured.

The measured route plus optional fixed converter/device delay enters the normal sample latency ledger and graph delay compensation. The return is monitored through the track’s device/effect/mixer path.

Offline render cannot summon hardware. Choose External return print in the Record panel to capture an exact-frame managed take first. The printed clip can then be edited, exported, or source-frozen like any internal instrument. Because a hardware return is shared, per-play .through() chains are ignored on midi and external tracks; use the track chain or process the printed audio instead.

A song file is export const config plus a default function the runtime calls with a live song. Here are several tracks across kinds:

import type { Song } from "@barline/runtime";
import { fx, notes, bars, beats, r } from "@barline/core";
export const config = { bpm: 132, key: "A minor" };
export default function (song: Song) {
const kick = song.track({
name: "kick",
output: { kind: "synth", synth: { id: "kick909" } },
effects: [fx.distortion({ drive: 0.3, wet: 0.5 })],
});
const keys = song.track({
name: "keys",
output: { kind: "soundfont", font: "<id>", program: 4 }, // electric piano
});
const drums = song.track({
name: "drums",
output: {
kind: "sampler",
samples: { C2: "/api/uploads/<id>", D2: "/api/uploads/<id>" },
},
});
song.section("intro", bars(4), () => {
kick.play(r`x . . . x . . . x . . . x . . .`);
drums.play(r`. . x . . . x .`);
keys.play(notes(["C3", "Eb3", "G3"], beats(1)));
});
song.arrange([{ section: "intro" }]);
}

Once an instrument is making sound, shape it with the track’s effects chain.