Something went wrong. Try again.
Plays short audio files such as a terminal bell through PipeWire on Linux and WASAPI on Windows.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! An audio file decoded once into memory, ready to be played any number of//! times.//!//! A Sound belongs to whoever loaded it, not to any Player, so it can be loaded//! -- and a bad file refused -- before there is a connection to the sound//! output at all. A Player that has played it holds a stream for it until the//! Sound is released with `Player.release`, which has to happen before `deinit`.
const std = @import("std");const Allocator = std.mem.Allocator;
const decode = @import("decode.zig");
const Sound = @This();
/// Interleaved, `channels` to a frame, nominally within -1 to 1.samples: []f32,/// Frames a second, as the file was recorded. The system resamples to the/// output device.rate: u32,/// As the file has them, in the WAVE order: front left, front right, centre,/// LFE, back left, back right, then the sides.channels: u16,/// How many Players hold a stream for this Sound. Counted only where safety/// checks are on, which is where `deinit` asserts it has reached zero.holders: if (std.debug.runtime_safety) u32 else void = if (std.debug.runtime_safety) 0 else {},
pub const Options = struct { /// The longest Sound that will be loaded, in milliseconds; anything that /// decodes to more is refused with `error.SoundTooLong` rather than held /// in memory. Thirty seconds is far more than an alert needs, and an hour /// of stereo at 48 kHz would be over a gigabyte. max_duration_ms: u32 = 30_000, /// The largest file `load` will read, for the same reason. max_file_bytes: usize = 64 << 20,};
pub const Format = decode.Format;pub const DecodeError = decode.Error;pub const LoadError = std.Io.Dir.ReadFileAllocError || DecodeError;
/// Reads and decodes the file at `path`, relative to `dir`.pub fn load(gpa: Allocator, io: std.Io, dir: std.Io.Dir, path: []const u8, options: Options) LoadError!Sound { const bytes = dir.readFileAlloc(io, path, gpa, .limited(options.max_file_bytes)) catch |err| switch (err) { error.StreamTooLong => return error.SoundTooLong, else => |e| return e, }; defer gpa.free(bytes); return fromBytes(gpa, bytes, options);}
/// Decodes a file already in memory, such as one built in with `@embedFile`./// `bytes` is not kept.pub fn fromBytes(gpa: Allocator, bytes: []const u8, options: Options) DecodeError!Sound { const decoded = try decode.decode(gpa, bytes, options.max_duration_ms); return .{ .samples = decoded.samples, .rate = decoded.rate, .channels = decoded.channels };}
pub fn deinit(sound: *Sound, gpa: Allocator) void { if (std.debug.runtime_safety) std.debug.assert(sound.holders == 0); // release it from every Player first gpa.free(sound.samples); sound.* = undefined;}
/// Samples per channel.pub fn frames(sound: Sound) usize { return sound.samples.len / sound.channels;}
/// How long the Sound plays for, in milliseconds.pub fn durationMs(sound: Sound) u64 { return @as(u64, sound.frames()) * std.time.ms_per_s / sound.rate;}
const testing = std.testing;
test "fromBytes keeps nothing of the input" { const bytes = try testing.allocator.dupe(u8, @embedFile("tests/data/tone-s16-mono-44100.wav")); var sound = try fromBytes(testing.allocator, bytes, .{}); testing.allocator.free(bytes); defer sound.deinit(testing.allocator); try testing.expectEqual(@as(usize, 4410), sound.frames()); try testing.expectEqual(@as(u64, 100), sound.durationMs());}
test "a Sound longer than the limit is refused" { try testing.expectError(error.SoundTooLong, fromBytes( testing.allocator, @embedFile("tests/data/tone-s16-mono-44100.wav"), .{ .max_duration_ms = 50 }, ));}
test "load reads from a directory" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(testing.io, .{ .sub_path = "bell.flac", .data = @embedFile("tests/data/tone-stereo-44100.flac") });
var sound = try load(testing.allocator, testing.io, tmp.dir, "bell.flac", .{}); defer sound.deinit(testing.allocator); try testing.expectEqual(@as(u16, 2), sound.channels);
try testing.expectError(error.SoundTooLong, load(testing.allocator, testing.io, tmp.dir, "bell.flac", .{ .max_file_bytes = 100 })); try testing.expectError(error.FileNotFound, load(testing.allocator, testing.io, tmp.dir, "missing.wav", .{}));}