Plays short audio files such as a terminal bell through PipeWire on Linux and WASAPI on Windows.
  • Zig 87.2%
  • Nix 12.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 63971dca3f
All checks were successful
test / test (push) Successful in 9m4s
test / docs (push) Successful in 7m7s
Open a Sound's stream ahead of its first Play with Player.prepare
An application can now find out when it loads its Sounds that there is no
sound output, rather than at the first Play, and the first Play no longer
waits on the sound server. A stream is opened paused, so a prepared Sound is
off the server's schedule until it is played.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013GnDsfLDmzoHCS7p8w1dKf
2026-10-14 15:59:36 -05:00
.forgejo/workflows Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00
docs/adr Open a Sound's stream ahead of its first Play with Player.prepare 2026-10-14 15:59:36 -05:00
LICENSES Scaffold zig-chime 2026-10-14 10:50:54 -05:00
src Open a Sound's stream ahead of its first Play with Player.prepare 2026-10-14 15:59:36 -05:00
tools Decode Sounds and play them through PipeWire and WASAPI 2026-10-14 14:24:12 -05:00
.gitignore Scaffold zig-chime 2026-10-14 10:50:54 -05:00
build.zig Decode Sounds and play them through PipeWire and WASAPI 2026-10-14 14:24:12 -05:00
build.zig.zon Depend on the decoders and the sound-output libraries 2026-10-14 11:15:02 -05:00
build.zig.zon.nix Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00
CONTRIBUTING.md Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00
flake.lock Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00
flake.nix Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00
GLOSSARY.md Scaffold zig-chime 2026-10-14 10:50:54 -05:00
README.md Open a Sound's stream ahead of its first Play with Player.prepare 2026-10-14 15:59:36 -05:00
REUSE.toml Trim MP3 delay and padding, fetch dependencies through Nix, test under Wine 2026-10-14 14:33:07 -05:00

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.load reads a file; Sound.fromBytes decodes one already in memory, such as a default sound built in with @embedFile. Anything that decodes to more than 30 seconds is refused with error.SoundTooLong, unless max_duration_ms says otherwise, so that a misconfigured path cannot fill memory with an hour of audio.
  • Playing: play takes 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; prepare does 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. release closes it, and has to come before the Sound is freed; builds with safety checks assert that it did. deinit stops 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 with pull, 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.