Flow

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.framework P/Invoke, CoreAudioBackend). play blocks 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-simple P/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 playground

You 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 playground

stream (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 background
Open in playground

stream 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 times
Open in playground

preview

Low-quality preview (mono, 22050 Hz) for fast iteration:

(preview buf)
Open in playground

stop

Stops any currently playing audio:

(stop)
Open in playground

Audio Devices

List Devices

use "@std"
use "@audio"

String[] devices = (audioDevices)
(print (str devices))
Open in playground

The 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 playground

Check Availability

Bool available = (isAudioAvailable)
(print (str available))
Open in playground

If 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 playground

Custom Bit Depth

(writeWav "output_16.wav" buf 16)        Note: default
(writeWav "output_24.wav" buf 24)
(writeWav "output_32.wav" buf 32)
Open in playground

exportWav (buffer-first)

The buffer-first variant is also available:

(exportWav buf "output.wav")
(exportWav buf "output_24.wav" 24)
Open in playground

WAV 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.2
Open in playground

MIDI 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 prefixGM programChannel
violin* / viola* / cello* / contrabass*40 / 41 / 42 / 43per-track
piano*0per-track
brass* / horn*56per-track
sax*65per-track
flute*73per-track
string* (synth)48per-track
organ*19per-track
bell*14per-track
drum*0channel 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 playground

TPQN 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 playground

Both 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 playground

Playback Architecture

  • Flow uses IAudioBackend as a platform abstraction for real-time playback.
  • AudioPlaybackManager detects 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) uses PA_SAMPLE_FLOAT32LE and 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

FunctionSignatureDescription
play(Buffer) -> VoidPlay buffer (blocking)
play(Sequence) -> VoidRender and play sequence (blocking)
stream(Buffer) -> VoidPlay asynchronously (non-blocking)
stream(Sequence) -> VoidRender and stream sequence
loop(Buffer) -> VoidLoop indefinitely
loop(Buffer, Int) -> VoidLoop N times
preview(Buffer) -> VoidLow-quality mono preview (22050 Hz)
stop() -> VoidStop all playback
audioDevices() -> String[]List available devices (empty under PulseAudio Simple API)
setAudioDevice(String) -> BoolSelect device by name
isAudioAvailable() -> BoolCheck backend availability
writeWav(String, Buffer) -> VoidPath-first WAV export (16-bit)
writeWav(String, Buffer, Int) -> VoidPath-first with bit depth (16/24/32)
exportWav(Buffer, String) -> VoidBuffer-first WAV export (16-bit)
exportWav(Buffer, String, Int) -> VoidBuffer-first with bit depth
loadWav(String) -> BufferLoad WAV file (auto-resample to 44100 Hz)
loadWav(String, Int) -> BufferLoad with semitone varispeed
loadWav(String, Double) -> BufferLoad with ratio varispeed
writeMidi(String, Song) -> VoidExport Song to .mid (SMF Format 1, multi-track)
writeMusicXML(String, Song) -> VoidExport Song to MusicXML 3.1 (requires @notation-io)
writeLilyPond(String, Song) -> VoidExport Song to LilyPond 2.24+ (requires @notation-io)
abc(String) -> Section\|Array[Section]Import ABC 2.1 source (requires @notation-io)
mml(String) -> SequenceImport PC-98 MML source (requires @notation-io)

See Also