lib/sys/src/audio.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const capabilities = @import("capabilities.zig");
  4 const coreaudio = @import("coreaudio.zig");
  5 const pulse = @import("pulse.zig");
  6 
  7 const backend = if (builtin.os.tag == .macos) coreaudio else pulse;
  8 
  9 pub const required_capabilities = capabilities.host(&.{.audio});
 10 
 11 pub const Error = error{
 12     UnsupportedPlatform,
 13     OpenFailed,
 14     ConfigureFailed,
 15     Underrun,
 16     WriteFailed,
 17     OutOfMemory,
 18 };
 19 
 20 pub const channels: u8 = 2;
 21 pub const PlaybackLimits = backend.Playback.Limits;
 22 pub const PlaybackCapacity = backend.Playback.Capacity;
 23 pub const PlaybackStorage = backend.Playback.Storage;
 24 
 25 pub const default_playback_supported = backend.supported and builtin.cpu.arch.endian() == .little;
 26 
 27 pub const PlaybackDevice = struct {
 28     playback: backend.Playback,
 29 
 30     pub fn openDefault(
 31         allocator: std.mem.Allocator,
 32         limits: PlaybackLimits,
 33         sample_rate: u32,
 34         frames_per_buffer: usize,
 35         name: []const u8,
 36     ) Error!PlaybackDevice {
 37         if (comptime !default_playback_supported) return error.UnsupportedPlatform;
 38         const playback = try openBackend(
 39             allocator,
 40             limits,
 41             sample_rate,
 42             frames_per_buffer,
 43             name,
 44         );
 45         return .{ .playback = playback };
 46     }
 47 
 48     pub fn drain(self: *PlaybackDevice) void {
 49         self.playback.drain() catch {};
 50     }
 51 
 52     pub fn close(self: *PlaybackDevice, allocator: std.mem.Allocator) void {
 53         self.playback.close(allocator);
 54     }
 55 
 56     pub fn writeInterleaved(
 57         self: *PlaybackDevice,
 58         samples: [*]const i16,
 59         frames: usize,
 60     ) Error!usize {
 61         const writable_frames = if (comptime builtin.os.tag == .macos)
 62             @min(frames, self.playback.storage.capacity.frames_per_buffer)
 63         else
 64             frames;
 65         const sample_count = std.math.mul(
 66             usize,
 67             writable_frames,
 68             channels,
 69         ) catch return error.WriteFailed;
 70         const bytes = std.mem.sliceAsBytes(samples[0..sample_count]);
 71         if (comptime builtin.os.tag == .macos) {
 72             self.playback.writeFrames(bytes) catch return error.WriteFailed;
 73         } else {
 74             self.playback.writeFrames(bytes) catch |err| return switch (err) {
 75                 error.OutOfMemory => error.OutOfMemory,
 76                 else => error.WriteFailed,
 77             };
 78         }
 79         return writable_frames;
 80     }
 81 
 82     pub fn prepare(_: *PlaybackDevice) void {}
 83 
 84     pub fn capacity(self: *const PlaybackDevice) PlaybackCapacity {
 85         return self.playback.storage.capacity;
 86     }
 87 };
 88 
 89 pub fn defaultPlaybackSupported() bool {
 90     return default_playback_supported and backend.available();
 91 }
 92 
 93 fn openBackend(
 94     allocator: std.mem.Allocator,
 95     limits: PlaybackLimits,
 96     sample_rate: u32,
 97     frames_per_buffer: usize,
 98     name: []const u8,
 99 ) Error!backend.Playback {
100     if (comptime builtin.os.tag == .macos) {
101         return coreaudio.Playback.open(
102             allocator,
103             limits,
104             .{
105                 .format = coreaudio.sample_format_s16_native,
106                 .channels = channels,
107                 .rate = sample_rate,
108                 .frames_per_buffer = frames_per_buffer,
109             },
110             name,
111         ) catch |err| return switch (err) {
112             error.OutOfMemory => error.OutOfMemory,
113             error.UnsupportedPlatform => error.UnsupportedPlatform,
114             error.QueueCreationFailed => error.OpenFailed,
115             else => error.ConfigureFailed,
116         };
117     }
118 
119     return pulse.Playback.open(
120         allocator,
121         limits,
122         .{
123             .format = pulse.sample_format_s16le,
124             .channels = channels,
125             .rate = sample_rate,
126         },
127         name,
128     ) catch |err| return switch (err) {
129         error.OutOfMemory => error.OutOfMemory,
130         error.UnsupportedPlatform => error.UnsupportedPlatform,
131         error.CapacityOverflow,
132         error.ChannelLimitExceeded,
133         error.ClientNameLimitExceeded,
134         error.ControlPayloadCapacityExceeded,
135         error.StreamRejected,
136         error.ServerError,
137         => error.ConfigureFailed,
138         else => error.OpenFailed,
139     };
140 }
141 
142 pub fn openDefaultPlayback(
143     allocator: std.mem.Allocator,
144     limits: PlaybackLimits,
145     sample_rate: u32,
146     frames_per_buffer: usize,
147     name: []const u8,
148 ) Error!PlaybackDevice {
149     return PlaybackDevice.openDefault(allocator, limits, sample_rate, frames_per_buffer, name);
150 }
151 
152 test "playback support follows native backend availability" {
153     if (comptime !default_playback_supported) {
154         try std.testing.expect(!defaultPlaybackSupported());
155         return;
156     }
157     _ = defaultPlaybackSupported();
158 }
159 
160 test "live device playback writes interleaved frames" {
161     if (comptime !default_playback_supported) return error.SkipZigTest;
162     if (!backend.available()) return error.SkipZigTest;
163 
164     const allocator = std.testing.allocator;
165     var device = openDefaultPlayback(
166         allocator,
167         .{ .client_name_bytes = "sys-test".len, .channels = channels },
168         48_000,
169         256,
170         "sys-test",
171     ) catch return error.SkipZigTest;
172     defer device.close(allocator);
173 
174     const samples = @as([(256 * @as(usize, channels))]i16, @splat(0));
175     for (0..8) |_| {
176         const written = try device.writeInterleaved(&samples, 256);
177         try std.testing.expectEqual(@as(usize, 256), written);
178     }
179     device.drain();
180 }