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 }