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 (a single track recorded to WAV). They commit in different directions — MIDI becomes a pattern you can edit, audio becomes a sample in your Library.

  • MIDI take: the notes played over the transport while it loops, kept as played-note events you can fold into a pattern.
  • Audio take: one track, recorded as 16-bit PCM mono WAV. On stop it uploads to your Library via POST /api/uploads and is auto-named recording-NN is the next free index, so takes never clobber each other.

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.

An audio take is just a Library sample once it uploads, so it lives at /api/uploads/<id> like any other upload. Wire it 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.