Lab

arrangement-engine

Plays an arrangement on the audio clock: its audio clips, its notes through each track's instrument, its device chains, sends and returns, and an offline render to a buffer, with a hook for the transport, the playhead and the meters

pnpm dlx shadcn@latest add @audio-ui/arrangement-engine
import { START_SECONDS, createArrangementEngine, renderArrangement, renderTrackClips } from "@/lib/arrangement-engine";
Fileslib/arrangement-engine.ts, hooks/use-arrangement-engine.ts
Depends ondrum-synthesis, arrangement, use-arrangement, clip-player, device-library, mixer-engine, rack, drum-rack, instrument-rack, voice-pool, web-audio
npm@audio-ui/engine, @audio-ui/react
Used byarrangement-view, block-audio-editor, block-device-chain, block-mixer, block-timeline, mixer-view, use-midi-playback
ExportKindWhat it is
START_SECONDSconstHow far ahead a play is scheduled, so every part of the engine has heard of it before its first frame.
MetronomeSettingsinterfaceWhat the metronome does.
ArrangementEngineOptionsinterface
ArrangementEngineinterface
createArrangementEnginefunctionPlays an arrangement on a context: a clip player for each audio track, an instrument with a voice per held note for each MIDI track, each track's device chain, and the mixer they all go through, on one transport counted in frames of the audio clock.
RenderOptionsinterface
renderArrangementfunctionRenders the arrangement's main output to a buffer, faster than real time, through the same engine that plays it: the same frames come out.
renderTrackClipsfunctionWhat a track's clips play over [from, to) of the arrangement, and nothing else: their fades, gains and crossfades, without the track's devices, its mix, its automation or anything sent to it.

Usage

arrangement-engine plays the document useArrangement holds: each audio track through a clip player, each MIDI track through the instrument of its chain with a voice for each note it holds (so a monophonic instrument plays chords), every device chain, every send and return, and the main track, on one transport counted in frames of the audio clock. The document comes in as a value; the engine follows it while it plays, and the interface never touches the audio.

import { useArrangement } from "@/hooks/use-arrangement";
import { useArrangementEngine } from "@/hooks/use-arrangement-engine";

const arrangement = useArrangement({ duration, initialTracks, tempo });
const engine = useArrangementEngine({
  doc: arrangement.doc,
  metronome: { on: clicking, rhythm }, // heard in playback, never in an export
  recordings, // a memoized Map of recording id to AudioBuffer
});

<Button onClick={() => (engine.playing ? engine.stop() : engine.play(cursor))}>Play</Button>;
<TimelinePlayhead source={engine.position} />; // seconds, as the speakers play them
<Meter source={useAudioLevels(engine.meter(track.id) ?? NONE)} />;
  • engine.noteOn(track, pitch, velocity) and engine.noteOff(track, pitch) play a note on a MIDI track's instrument now, through its chain, as a key of a clip's piano roll does.
  • play(from) starts 12 ms from now (START_SECONDS), so every track has heard of it before its first frame, round the document's loop when it is on; a note already sounding where it starts plays from there. stop() stops the clips and the notes and lets the effects' tails ring.
  • position is a live source of the seconds the speakers play: the audio clock, less the output's latency and the engine's delay compensation, so the playhead is where the sound is. The engine stops itself at the end of the arrangement.
  • status is the context's sample rate, its latency in seconds (the output's and the delay compensation's), and its state: what an engine line shows.
  • A device switched off passes the sound on unchanged, as late as it would make it, so switching it moves nothing in time.

renderArrangement(doc, recordings, { from, to, sampleRate }) renders the main output to a buffer through the same engine on an offline context, faster than real time, scheduling the notes a second at a time with onProgress as it goes: the same frames come out as play, and a neutral clip comes out bit for bit.

To a file

audio-encode writes rendered sound to a WAV, FLAC or MP3 file. It encodes in chunks with a paint between two, so a long file never freezes the page, and loads each format's encoder (WebAssembly for FLAC and MP3) the first time it is used.

pnpm dlx shadcn@latest add @audio-ui/audio-encode
import { renderArrangement } from "@/lib/arrangement-engine";
import { downloadFile, encodeAudio } from "@audio-ui/engine/files";

const rendered = await renderArrangement(doc, recordings, { from: 0, sampleRate: 48_000, to: 30 });
if (rendered) {
  const file = await encodeAudio([rendered.getChannelData(0), rendered.getChannelData(1)], {
    bitrate: 320,
    format: "mp3",
    onProgress: (done) => console.log(`${Math.round(done * 100)}%`),
    sampleRate: rendered.sampleRate,
  });
  downloadFile(file, "arrangement.mp3");
}
  • format is "wav", "flac" or "mp3". bitDepth is 16 (the default) or 24 for WAV and FLAC, or 32 for a WAV of floats, which keeps what goes past full scale; anywhere else a sample past full scale is clipped. bitrate is an MP3's, in kbps, 128 by default.
  • An MP3 comes out at the nearest rate MP3 has: 48 kHz for a render at 96 kHz.
Built by Ouest Labs, the source code is available on GitHub.
RSS