- Zig 87%
- Nix 13%
zig-chime #
Plays short audio files — a terminal's bell, a notification, an alert — through the system's sound output, from Zig, for applications that want a sound played and nothing more. It is a thin layer over a set of codec and sound-server libraries, so that an application does not have to wire them together itself.
The API documentation is generated from the doc comments and published at https://jeff.jcollie.page/zig-chime/.
const chime = @import("chime");
var bell = try chime.Sound.load(gpa, io, std.Io.Dir.cwd(), "bell.flac", .{});
defer bell.deinit(gpa);
var player = try chime.Player.init(gpa, io, .{ .name = "my app", .environ = init.minimal.environ });
defer player.deinit();
defer player.release(&bell);
_ = try player.play(&bell, 0.8); // returns at once
How it works #
An application loads each audio file once into a Sound, decoded into memory,
and plays it through a Player, its connection to the system's sound output.
GLOSSARY.md defines the language and docs/adr/ records the decisions behind
it.
- Formats: WAV, FLAC (native and in Ogg), Opus in Ogg, and MP3, recognized by their contents rather than their file names. Metadata is ignored.
- Loading:
Sound.loadreads a file;Sound.fromBytesdecodes one already in memory, such as a default sound built in with@embedFile. Anything that decodes to more than 30 seconds is refused witherror.SoundTooLong, unlessmax_duration_mssays otherwise, so that a misconfigured path cannot fill memory with an hour of audio. - Playing:
playtakes a Sound and a volume from 0 to 1 and returns at once. Playing a Sound that is already playing does nothing and returns.dropped. Different Sounds can play at the same time. The first Play of a Sound connects to the sound server, and that is where an error can come from;preparedoes it beforehand, so that an application learns at once that there is no sound output and the first Play starts without the wait. - Releasing: a Player keeps a stream open for each Sound it has played or
prepared,
paused between Plays so that the next one starts at once.
releasecloses it, and has to come before the Sound is freed; builds with safety checks assert that it did.deinitstops everything immediately. - Output: PipeWire on Linux and WASAPI on Windows, each stream marked as a notification so the system routes it and sets its volume as one. The stream runs at the Sound's own rate and channel count and the system converts to the device's, so this library has no resampler and no mixer. The sound server's own thread pulls the samples; the Player starts no thread or task of its own. macOS is not supported yet, and building for it fails at compile time.
- Testing without a sound device:
PlayerWith(backend.Null)is a Player whose streams play nothing and are driven by hand withpull, which is how this library is tested and how an application using it can be.
What is decoded:
| Format | What | Not |
|---|---|---|
| WAV | 8, 16, 24 and 32-bit integer and 32 and 64-bit float PCM, plain or WAVE_FORMAT_EXTENSIBLE, RIFF or RIFX |
A-law, mu-law, ADPCM, RF64 |
| FLAC | Native and Ogg, every bit depth and up to eight channels. The MD5 is not checked. | |
| Opus | Ogg Opus with channel mapping family 0 or 1 (up to 7.1), with the pre-skip, end trimming and output gain applied | Ambisonics and unlabeled channel layouts |
| MP3 | MPEG-1, MPEG-2 and MPEG-2.5, Layers I to III, with the encoder's delay and padding trimmed where a LAME tag gives them | Trimming without a LAME tag, which nothing records the delay for |
Ogg Vorbis is recognized and refused with error.UnsupportedFormat.
Dependencies #
| Library | Used for |
|---|---|
| zig-riff | WAV |
| zig-ogg | Ogg, for FLAC and Opus |
| zig-flac | FLAC |
| zig-opus | Opus |
| zig-mp3 | MP3 |
| zig-pipewire | Sound output on Linux |
| zig-wsapi | Sound output on Windows |
Requirements #
- Zig 0.17
- Linux with PipeWire, or Windows
Using it in a project #
zig fetch --save git+https://git.jcollie.dev/jeff/zig-chime.git
const chime = b.dependency("chime", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("chime", chime.module("chime"));
Only the target's own sound-output library is fetched: zig-pipewire for Linux, zig-wsapi for Windows.
Getting the source #
git clone https://git.jcollie.dev/jeff/zig-chime.git
Building #
The toolchain comes from the Nix flake:
$ nix develop
$ zig build test # the unit tests
$ zig build check # compile everything without running it
$ zig build docs # the API documentation, into zig-out/docs
$ zig build docs-serve # serve it at http://localhost:8000/
$ zig build play -- --volume 0.5 bell.flac # hear a file through the real output
$ zig build test -Dtarget=x86_64-windows -fwine # the Windows build's tests, under Wine
The dependencies are fetched by Zig the first time. CI fetches them through Nix
instead, from build.zig.zon.nix, and hands them to Zig with --system:
$ zig build test --system "$(nix build --no-link --print-out-paths .#zig-deps)"
The tests decode the fixtures in src/tests/data/, one per format, and drive
the Player through backend.Null, so they need no sound device.
zig build play is the end-to-end check: it plays each file it is given
through PipeWire or WASAPI and fails if a Play does not finish.
What is where #
| Path | What |
|---|---|
src/root.zig |
The module: Sound, Player, PlayerWith, backend |
src/Sound.zig |
Loading and decoding into memory, and the length limit |
src/decode.zig, src/decode/ |
Recognizing a format by its contents, and one decoder per format |
src/player.zig |
The Player: one stream per Sound, the drop rule, volume, release |
src/backend/ |
PipeWire, WASAPI, and the null backend for tests |
tools/play.zig |
zig build play |
tools/docs_server.zig |
zig build docs-serve |
Issues and contributions #
Report issues and submit pull requests at the Tangled mirror,
https://tangled.org/jcollie.dev/zig-chime. CONTRIBUTING.md covers what a
change needs.
License #
MIT. The project follows the REUSE specification;
reuse lint checks it.
References cited #
Microsoft Corporation. "Extensible Wave-Format Descriptors." Microsoft Learn,
June 2020.
https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/extensible-wave-format-descriptors.
WAVE_FORMAT_EXTENSIBLE, and that the first two bytes of its sub-format GUID
are the format tag a plain header would carry.
Nilsson, Martin. ID3 tag version 2.4.0 - Main Structure. id3.org, November 2000. https://id3.org/id3v2.4.0-structure. The tag header's syncsafe size and footer flag, which say how far to step to find what follows a tag.
Terriberry, T., R. Lee, and R. Giles. Ogg Encapsulation for the Opus Audio Codec. RFC 7845. RFC Editor, April 2016. https://www.rfc-editor.org/info/rfc7845. The identification header, the pre-skip, the output gain, channel mapping families 0 and 1, and trimming the end of a stream to its last granule position.
Valin, JM., K. Vos, and T. Terriberry. Definition of the Opus Audio Codec. RFC 6716. RFC Editor, September 2012. https://www.rfc-editor.org/info/rfc6716. The 120 ms bound on a packet, which sizes the decode buffer.
van Beurden, Martijn Q. C., and Andrew Weaver. Free Lossless Audio Codec (FLAC). RFC 9639. RFC Editor, December 2024. https://www.rfc-editor.org/info/rfc9639. FLAC's channel order, which for up to eight channels is already the WAVE order.
Xiph.Org Foundation. Vorbis I Specification. July 2020. https://xiph.org/vorbis/doc/Vorbis_I_spec.html. The Vorbis channel orders, which Opus mapping family 1 uses and which are rearranged into the WAVE order here.