Skip to content

Record

The Record panel captures what you play. It has two kinds of take: a MIDI take (the notes you played, quantized to the section’s grid) and an audio take (mic/line input or an external-instrument return captured on exact sample frames). They commit in different directions: MIDI becomes a pattern you can edit; audio becomes immutable managed assets plus readable clip/take source.

  • MIDI take: the notes played over the transport while it loops, kept as played-note events you can fold into a pattern.
  • Audio take: mono or stereo input captured by an AudioWorklet at scheduled sample-frame start/stop boundaries. Count-in, pre-roll, destination, calibrated/manual timing correction, safe monitoring, and punch length are explicit.
  • TakeStack: a loop punch split into immutable 24-bit pass assets. Preview the lanes, choose the source pass, then commit all pass ids plus the selected range to a small TypeScript module.
  • External return print: the same exact-frame path fed by a calibrated logical return route from an external output track.

Open Audio Setup before a serious take. It keeps machine-specific facts out of the song while giving the recorder one trustworthy clock and route model:

  • choose the input and optional output device;
  • probe the real channel count, sample rates, browser processing, and reported input/base/output latency;
  • run the five-click acoustic loopback calibration and inspect confidence;
  • apply a visible manual trim when measured hardware says it is needed;
  • name a logical return such as external-1.

Input monitoring unlocks only after you approve a measured headphone route with at least 55% calibration confidence and an effective round trip at or below 35 ms. Bluetooth/headset routes stay blocked because their latency is high and variable. The local v3 profile records which physical input resolved behind default, the current output-route generation, and a verified physical output identity. Connecting, removing, changing devices, switching the OS default, or being unable to verify the output revokes approval until that physical path is inspected, calibrated, and approved again. A physical device id is never written into song code.

MIDI capture rides the bar clock, so it needs somewhere to land: a section with bars and a running transport. A stopped transport disables the control — there is no grid to quantize against. Give the take a home first:

import type { Song } from "@barline/runtime";
import { bars, beats, r } from "@barline/core";
export const config = { bpm: 128, key: "A minor" };
export default function (song: Song) {
const lead = song.track({
name: "lead",
output: { kind: "synth", synth: { id: "lead" } },
});
song.section("verse", bars(4), () => {
// play into this section while it loops — the take quantizes here
lead.play(r`x . x . x . x .`);
});
song.arrange([{ section: "verse", repeat: 2 }]);
}

Loop the section, arm the track, and play. The captured notes come back as a played-note take you can hand-edit into a notes([...]) literal in the section, the same write-back surface the Clip detail piano roll edits.

A finished MIDI take writes a recording-N.ts pattern file you own as code. Pick a track and hit → arrangement and the studio commits it as real structure in one edit: it adds the import, inserts a named section that plays the take —

import { recording1 } from "./recording-1";
song.section("jam-1", bars(4), (t) => {
t.tracks["bass"]?.play(recording1);
});

— and appends { section: "jam-1" } to song.arrange([...]). The take is now an addressable part of the song (it appears in the Arrange and Session views), not a loose file to hand-wire. The edit goes through the same Yjs path as every keystroke, so the arrangement array is regenerated cleanly (never comma-spliced) and the result always parses.

The local stage starts heartbeating while the take is armed, including count-in and pre-roll before the first audio chunk. Canceling an arm invalidates that specific attempt, so a late permission or count-in completion from an older take cannot attach itself to a newer recording.

On commit, an audio take is pinned in the Library and inserted as a clip in the section, track, and beat selected before arming. It lives at /api/uploads/<id> like any other immutable upload. You can still wire or rearrange it manually in two ways.

As a clip — drop the take onto a track as an audio block with track.clip({ url }), called inside a section body:

export default function (song: Song) {
const recording = "/api/uploads/<id>"; // the take's url from your Library
const vox = song.track({
name: "vox",
output: { kind: "synth", synth: { id: "lead" } },
});
song.section("intro", bars(8), () => {
vox.clip({ url: recording, at: beats(0), beats: beats(16) });
});
song.arrange([{ section: "intro" }]);
}

clip() takes the same Clip options as any audio block — at, beats, loop, gain, and stretch / semitones if you want the take time-stretched or pitch-shifted to fit. at and beats are Beats, so wrap bare numbers with beats() or bars().

As a sampler — map the take to a note and play it back pitched across the keyboard:

export default function (song: Song) {
const chop = song.track({
name: "chop",
output: {
kind: "sampler",
samples: { C3: "/api/uploads/<id>" }, // the recording-N take
},
});
song.section("loop", bars(2), () => {
chop.play(r`x . . . x . x .`);
});
song.arrange([{ section: "loop", repeat: 4 }]);
}

Either way the take is a normal upload from there — content-addressed, served immutably, and shared with every other sample in your Library. Record captures the performance; the rest of the studio treats it like any other clip.

Set Punch length to a number of bars to schedule an exact start and stop on the shared audio clock. Set Loop passes above one to record the same window repeatedly. The worklet marks pass boundaries on sample frames rather than splitting a timer-based recording afterward. Multi-pass punches must start on a bar boundary; Barline loops that arrangement range for every pass instead of continuing into later bars.

After the loop, TakeStack shows one preview lane per pass. Committing pass 2, for example:

  1. uploads and pins every pass as its own immutable managed asset;
  2. writes take-stack-<id>.ts with destination, pass asset ids, and comp.pass: 1;
  3. atomically imports the module’s resolved asset, source range, and musical duration into the audible track.clip() region.

Changing comp.pass in the generated module changes playback without copying or overwriting another performance. Unequal recovered pass lengths derive their own end and clip duration from the selected pass. The current UI chooses whole passes; arbitrary word-by-word comp painting remains outside this bounded tracking workflow.

Define the portable intent in code:

const hardware = song.track({
name: "hardware",
output: {
kind: "external",
channel: 1,
port: "USB MIDI Interface",
returnRoute: "external-1",
},
});

Bind and calibrate external-1 in Audio Setup, then choose External return print in Record. Barline sends timestamped MIDI, includes measured return latency in graph compensation, and captures the return into a managed audio take. Once printed, that hardware performance can be exported or source-frozen like any internal track.

Chunks are staged in IndexedDB before upload. Device loss, processor failure, navigation, storage pressure, upload interruption, or source-document drift preserves the local recording for recovery instead of silently discarding it. The active capture and its finalizer heartbeat an owner lease; another tab ignores live work and atomically claims only interrupted or expired stages. Manual stop, scheduled punch completion, cancel, and failure share one bounded finalizer. Multipart upload resumes from completed parts, failed publication releases assets that never entered source, and source insertion happens only while the original song document is still current.