audio
import audio from "audio";
import fs from "fs";
import io from "io";
fn main() -> i32 {
let data: string = fs.read_file("ding.wav") or { return 1; };
let ding: audio.Sound = audio.decode_wav(data) or {
io.println("cannot read the sound: {err}");
return 1;
};
audio.play(ding) or { io.println("no sound: {err}"); };
return 0;
}audio plays sound on the system’s default output. The system mixes your
program with every other one, and the user sets its volume in their own
mixer (Windows’ Volume Mixer, their desktop’s sound settings, Leivos’s top
bar), where it shows by its name. There are two ways to play:
- a clip: a short sound you have in memory, played once, at once, over whatever else is playing: clicks, notifications, game effects;
- a stream: samples you write as you make them: music from a decoder, a synthesizer, a game’s own mix.
Sounds and formats
A Sound is 16-bit samples in memory: rate frames a second, channels
samples a frame (1 or 2, left then right), samples the data. It is a
view: it owns nothing.
pub struct Sound { rate: i64; channels: i64; samples: []const i16; }
pub struct Format { rate: i64; channels: i64; sample: Sample = Sample.I16; }
pub enum Sample : u8 { I16 = 1; F32 = 2; }A stream’s Format says what you will write: 16-bit integers (full scale
±32767) or f32 (full scale ±1.0). Any rate from 8000 to 96000 and one
or two channels work; the system converts to what the device takes.
Clips
| Function | Does |
|---|---|
play(sound, gain = 1.0) | Play sound once and return at once; the first play loads it, later plays reuse it |
load(sound) -> Clip | Hand the sound to the system (copied: the Sound may go away after) |
c.play(gain = 1.0) | Play the clip from its start, over anything playing, itself included |
c.stop() | Stop every play of the clip |
c.free() | Stop it and release it |
play keeps the clip of each sound it has played, so use it for sounds
that live as long as the program (a decoded file you keep, a sound in a
global). For a sound you are about to drop, load it and free the clip.
A clip is at most MAX_CLIP_SECONDS (10) long: use a stream for longer
sounds. A program plays up to MAX_VOICES (8) clips at once; a ninth
stops the one that started first. (On Leivos the limit is the machine’s:
16 clips over every program.)
var click: audio.Clip = audio.load(click_sound) or { return 1; };
click.play(0.5) or { };
// ...
click.free();Streams
| Function | Does |
|---|---|
open(format, buffer_ms = 100) -> Stream | A stream on the default output, paused, holding buffer_ms of sound |
s.start(), s.pause() | Play, or stop where it is (what is written stays) |
s.write_i16(samples), s.write_f32(samples) | Queue all of samples, waiting while the buffer is full |
s.try_write_i16(samples) -> i64, s.try_write_f32(samples) -> i64 | Queue the whole frames that fit now, never waiting; how many |
s.free_frames() -> i64 | Frames a try_write would take now |
s.played() -> i64 | Frames played since open |
s.drain() | Wait until everything written is played, then pause |
s.set_volume(gain) | The stream’s level, 0.0 silent to 1.0 as written; changes ramp smoothly |
s.handle() -> i64 | A handle to watch with ui.watch, ready when the stream can take more |
s.close() | Stop at once and release the stream (drain first to hear the end) |
The buffer is the latency: what you write is heard up to buffer_ms
later. A stream starts paused so you can fill it first: write what fits,
then start.
import audio from "audio";
import math from "math";
var block: [960]i16; // 10 ms of 48 kHz stereo
fn main() -> i32 {
var s: audio.Stream = audio.open(audio.Format { rate: 48000; channels: 2; }) or { return 1; };
var t: i64 = 0;
while (t < 48000 * 2) { // two seconds of 440 Hz
for (i in 0..480) {
let v: f64 = math.sin_f64(math.TAU * 440.0 * ((t + i) as f64) / 48000.0) * 8000.0;
block[i * 2] = v as i16;
block[i * 2 + 1] = v as i16;
}
s.write_i16(block[0..960]) or { return 1; };
if (t == 0) { s.start() or { return 1; }; } // the first block is in: play
t = t + 480;
}
s.drain() or { return 1; };
s.close();
return 0;
}The samples are interleaved: frame n is samples[n * channels] (left)
and samples[n * channels + 1] (right). A slice whose length isn’t a
whole number of frames is a broken contract (a trap), as is writing f32
to an I16 stream.
If your program writes too slowly the stream plays silence until the next
frames come, then goes on; played() counts only what was really
played. A waiting write on a paused stream that the samples can never
fit into would wait forever: it traps instead, telling you to start
first.
Feeding a stream from a window
write_i16 waits, which would freeze a window. In a ui program, watch
the stream’s handle and write what fits each time it is ready:
import audio from "audio";
import ui from "ui";
var music: audio.Stream;
var next_block: [4800]i16;
fn feed(h: i64) {
let free: i64 = music.free_frames() or { return; };
let n: i64 = fill(next_block[0..4800], free); // your samples, up to `free` frames
music.try_write_i16(next_block[0..n * 2]) or { };
}music = audio.open(audio.Format { rate: 48000; channels: 2; }) or { return 1; };
music.start() or { return 1; };
ui.watch(music.handle(), feed) or { return 1; };WAV files
| Function | Does |
|---|---|
decode_wav(data) -> Sound or AudioError | The samples of a WAV file as a Sound, in new memory |
free_wav(sound) | Release what decode_wav allocated (and the clip play kept of it) |
decode_wav reads PCM WAV at 8, 16, 24 or 32 bits and 32-bit float WAV,
mono or stereo, at 8000 to 96000 Hz; anything else is InvalidWav. The
reading itself is the wav module’s, which needs no audio
system: use it alone when you only read WAV files.
Errors
pub enum AudioError : u8 {
NoDevice = 1: "no audio output device";
Unavailable = 2: "the audio system is not available";
Unsupported = 3: "audio format not supported by the device";
DeviceLost = 4: "the audio device went away";
TooLong = 5: "sound too long for a clip";
InvalidWav = 6: "not a playable WAV file";
OutOfMemory = 7: "out of memory";
}A program that plays sound should go on without it: on a server, in a
container or on a machine without speakers, open and load return
NoDevice or Unavailable and nothing else changes. When the user
switches outputs (plugs in headphones, picks another device), a stream
moves to the new default by itself, with a short gap (on macOS, not yet:
a stream stays on the output it opened on); DeviceLost means no output
is left.
A gain outside 0.0..1.0, a rate or channel count out of range, or using a
stream after close are broken contracts and trap.
Platforms
| Platform | Through |
|---|---|
| Windows | WASAPI, the system’s audio engine |
| Linux | ALSA’s default device, which reaches PipeWire or PulseAudio; needs libasound.so.2 (without it, Unavailable) |
| macOS | CoreAudio’s audio queues |
| Leivos | audiod, Leivos’s sound server |
The audio library is audio100 (.dll, .so, .dylib), next to tape
and next to your program like the other runtime libraries; on Leivos it
is compiled into your program.
Use a stream from the thread that opened it. Recording, choosing among
several outputs, and sounds embedded with #resource sound are not there
yet.
Last modified: