pnpm dlx shadcn@latest add @audio-ui/arrangement-engine
import { START_SECONDS, createArrangementEngine, renderArrangement, renderTrackClips } from "@/lib/arrangement-engine";| Files | lib/arrangement-engine.ts, hooks/use-arrangement-engine.ts |
|---|---|
| Depends on | drum-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 by | arrangement-view, block-audio-editor, block-device-chain, block-mixer, block-timeline, mixer-view, use-midi-playback |
| Export | Kind | What it is |
|---|---|---|
START_SECONDS | const | How far ahead a play is scheduled, so every part of the engine has heard of it before its first frame. |
MetronomeSettings | interface | What the metronome does. |
ArrangementEngineOptions | interface | |
ArrangementEngine | interface | |
createArrangementEngine | function | Plays 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. |
RenderOptions | interface | |
renderArrangement | function | Renders the arrangement's main output to a buffer, faster than real time, through the same engine that plays it: the same frames come out. |
renderTrackClips | function | What 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)andengine.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.positionis 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.statusis 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");
}formatis"wav","flac"or"mp3".bitDepthis 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.bitrateis 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.