Playback and Export
Flow plays audio in real time and exports to WAV, MIDI, MusicXML, and LilyPond files. Real-time playback and WAV/MIDI export live in @audio; notation export/import is opt-in via use "@notation-io".
Real-time playback uses IAudioBackend, which probe-selects the best available platform backend:
- macOS — CoreAudio (
AudioToolbox.frameworkP/Invoke,CoreAudioBackend).playblocks until the full buffer has been rendered by the device (drain fixed 2026-06-10). - Windows — WASAPI (
NAudio.Wasapi,WasapiBackend). Audible end-to-end; HUMAN-UAT pending. - Linux — PulseAudio (
libpulse-simpleP/Invoke,PulseAudioSimpleBackend). Also works on PipeWire via PA’s compatibility layer.
Playing Audio
play (blocking)
Plays a buffer or sequence and blocks until playback finishes:
use "@std"
use "@audio"
Buffer tone = (createSineTone 0.5 440.0 0.5)
(play tone)Open in playgroundYou can also pass a sequence directly (it renders with a sine synth):
timesig 4/4 {
Sequence mel = | C4 D4 E4 F4 |
(play mel)
}Open in playgroundstream (non-blocking)
Plays audio asynchronously so your script continues executing:
use "@audio"
Buffer buf = (createSineTone 5.0 440.0 0.5)
(stream buf)
Note: returns immediately; audio plays in the backgroundOpen in playgroundstream also works with sequences.
loop
Loops a buffer indefinitely (non-blocking), or a specific number of times:
(loop buf) Note: forever (until (stop) is called)
(loop buf 4) Note: 4 timesOpen in playgroundpreview
Low-quality preview (mono, 22050 Hz) for fast iteration:
(preview buf)Open in playgroundstop
Stops any currently playing audio:
(stop)Open in playgroundAudio Devices
List Devices
use "@std"
use "@audio"
String[] devices = (audioDevices)
(print (str devices))Open in playgroundThe PulseAudio Simple API doesn’t enumerate sinks, so audioDevices() currently returns an empty list. For now, select an output device with the --device CLI flag on flow run / flow play.
Set Device
Bool success = (setAudioDevice "pulse")Open in playgroundCheck Availability
Bool available = (isAudioAvailable)
(print (str available))Open in playgroundIf the audio backend is unavailable, play / stream / loop become no-ops with a warning — your WAV/MIDI exports still work. For headless renders and CI, set FLOW_SUPPRESS_PLAYBACK=1 to route playback to a capture buffer instead of the audio device.
WAV Export
writeWav and exportWav write 16/24/32-bit PCM at the buffer’s sample rate. Parent directories are auto-created. The 16/24-bit paths apply TPDF (Triangular Probability Density Function) dither at 1 LSB — the dither RNG is seeded deterministically per export, so consecutive writes of the same buffer produce byte-identical WAVs.
Basic Export
use "@audio"
Buffer buf = (createSineTone 1.0 440.0 0.5)
(writeWav "output.wav" buf)Open in playgroundCustom Bit Depth
(writeWav "output_16.wav" buf 16) Note: default
(writeWav "output_24.wav" buf 24)
(writeWav "output_32.wav" buf 32)Open in playgroundexportWav (buffer-first)
The buffer-first variant is also available:
(exportWav buf "output.wav")
(exportWav buf "output_24.wav" 24)Open in playgroundWAV Loading
Load an existing WAV back into a buffer (supports 16/24/32-bit PCM; auto-resamples to 44100 Hz). Two optional overloads apply varispeed pitch-shift at load time — identity short-circuits at semitones=0 / ratio=1.0:
use "@audio"
Buffer loaded = (loadWav "sample.wav")
Buffer up5 = (loadWav "sample.wav" 5) Note: +5 semitones (Int)
Buffer halfRate = (loadWav "sample.wav" 0.5) Note: half-speed = down one octave
Int frames = (getFrames loaded)
Int channels = (getChannels loaded)
Note: works with effects pipeline like any other buffer
Buffer processed = loaded -> gain -6dB -> reverb 0.2Open in playgroundMIDI Export
Export a Song to a Standard MIDI File (Format 1, multi-track) via DryWetMidi. Tempo, time signature, and key from the enclosing musical context are preserved. Each unique sequence name in the song becomes its own track (plus a conductor track for tempo / timesig); track names route to General MIDI program numbers by prefix:
| Sequence name prefix | GM program | Channel |
|---|---|---|
violin* / viola* / cello* / contrabass* | 40 / 41 / 42 / 43 | per-track |
piano* | 0 | per-track |
brass* / horn* | 56 | per-track |
sax* | 65 | per-track |
flute* | 73 | per-track |
string* (synth) | 48 | per-track |
organ* | 19 | per-track |
bell* | 14 | per-track |
drum* | 0 | channel 9 (GM percussion) |
use "@std"
use "@audio"
tempo 140 {
timesig 3/4 {
key Gmajor {
section waltz {
| G4q B4q D5q |
| D5h G4q |
}
section ending { | G4h. | }
Song song = [waltz waltz ending]
(writeMidi "my_waltz.mid" song)
}
}
}Open in playgroundTPQN auto-elevates to the LCM of any tuplet denominators (default 480, hard cap 9600). Voice-block polyphony exports as overlapping NoteOn events at the parent’s tick. Non-12-TET tunings fire a one-shot stderr advisory; per-note pitch-bend export is on the v1.6+ backlog.
MIDI Import (CLI)
MIDI import is a CLI subcommand rather than an in-language builtin — it emits round-trip-friendly .flow source from a .mid:
flow midi2flow input.mid # writes input.flow next to source
flow midi2flow input.mid -o tune.flow # explicit output path
flow midi2flow input.mid --no-sustain # omit sustain-pedal blocks
flow midi2flow input.mid --sfz # prefer SFZ instruments for orchestral GM
flow midi2flow input.mid --dump # also write a diagnostic dump Notation Export & Import
Opt in to MusicXML / LilyPond / ABC / MML with use "@notation-io". All four builtins write or read text formats; none ship audio dependencies.
use "@std"
use "@audio"
use "@notation-io"
tempo 120 {
timesig 4/4 {
key Cmajor {
section verse {
Sequence mel = | C4 E4 G4 C5 |
}
Song song = [verse]
(writeMusicXML "verse.musicxml" song) Note: MusicXML 3.1 partwise (MuseScore-compatible)
(writeLilyPond "verse.ly" song) Note: LilyPond 2.24+
}
}
}
Note: ABC import — single tune returns Section, multi-tune (X:1/X:2/...) returns Array[Section]
Section tune = (abc "X:1\nT:Demo\nM:4/4\nK:Cmaj\nC D E F |")
Note: PC-98 MML import — returns a single Sequence
Sequence riff = (mml "T120 L4 O4 cdefga>c")Open in playgroundBoth exports preserve articulations, microtonal cent offsets (as <alter> decimals in MusicXML; as % +Nc comments in LilyPond), voice-block polyphony (per-note <voice>N</voice> tags / sibling << { } \\ { } >> voices), and dynamics. Imports are charitable — unknown ornaments / opcodes drop with a one-shot stderr advisory rather than erroring.
Beat-Synced Live Reload
flow watch <script> quantizes file-watch reloads to the next bar boundary and applies a 64-sample crossfade between the old and new render. Failed renders keep the previous version playing — your speakers don’t go silent when you typo.
flow watch piece.flow Complete Render-to-File Workflow
use "@std"
use "@audio"
use "@notation-io"
tempo 120 {
timesig 4/4 {
key Cmajor {
section intro {
Sequence melody = | C4 E4 G4 C5 |
}
section verse {
Sequence lead = | E4 E4 F4 G4 |
}
section chorus {
Sequence lead = | I IV V I |
}
Song mySong = [intro verse*2 chorus]
Buffer raw = (renderSong mySong "piano")
Buffer mix = raw -> reverb 0.3 -> fadeIn 0.2 -> fadeOut 0.5
(writeWav "my_song.wav" mix)
(writeMidi "my_song.mid" mySong)
(writeMusicXML "my_song.musicxml" mySong)
(writeLilyPond "my_song.ly" mySong)
Int frames = (getFrames mix)
Int duration = (div frames 44100)
(print $"duration: ~{duration}s")
}
}
}Open in playgroundPlayback Architecture
- Flow uses
IAudioBackendas a platform abstraction for real-time playback. AudioPlaybackManagerdetects and instantiates the best available backend at startup (probe order: WebAudio on WASM, CoreAudio on macOS, WASAPI on Windows, PulseAudio on Linux).- The Linux backend (
PulseAudioSimpleBackend) usesPA_SAMPLE_FLOAT32LEand supports 1–8 channels. Also works on PipeWire via PA’s compatibility layer. - Audio renders to stereo float buffers at 44100 Hz by default.
Function Reference
| Function | Signature | Description |
|---|---|---|
play | (Buffer) -> Void | Play buffer (blocking) |
play | (Sequence) -> Void | Render and play sequence (blocking) |
stream | (Buffer) -> Void | Play asynchronously (non-blocking) |
stream | (Sequence) -> Void | Render and stream sequence |
loop | (Buffer) -> Void | Loop indefinitely |
loop | (Buffer, Int) -> Void | Loop N times |
preview | (Buffer) -> Void | Low-quality mono preview (22050 Hz) |
stop | () -> Void | Stop all playback |
audioDevices | () -> String[] | List available devices (empty under PulseAudio Simple API) |
setAudioDevice | (String) -> Bool | Select device by name |
isAudioAvailable | () -> Bool | Check backend availability |
writeWav | (String, Buffer) -> Void | Path-first WAV export (16-bit) |
writeWav | (String, Buffer, Int) -> Void | Path-first with bit depth (16/24/32) |
exportWav | (Buffer, String) -> Void | Buffer-first WAV export (16-bit) |
exportWav | (Buffer, String, Int) -> Void | Buffer-first with bit depth |
loadWav | (String) -> Buffer | Load WAV file (auto-resample to 44100 Hz) |
loadWav | (String, Int) -> Buffer | Load with semitone varispeed |
loadWav | (String, Double) -> Buffer | Load with ratio varispeed |
writeMidi | (String, Song) -> Void | Export Song to .mid (SMF Format 1, multi-track) |
writeMusicXML | (String, Song) -> Void | Export Song to MusicXML 3.1 (requires @notation-io) |
writeLilyPond | (String, Song) -> Void | Export Song to LilyPond 2.24+ (requires @notation-io) |
abc | (String) -> Section\|Array[Section] | Import ABC 2.1 source (requires @notation-io) |
mml | (String) -> Sequence | Import PC-98 MML source (requires @notation-io) |
See Also
- Audio and Synthesis - Creating and synthesizing audio
- Effects - Processing audio before export
- Song Structure - Creating songs to render and export
- Voices and Tracks - Lower-level multi-track rendering