lib/sys/src/coreaudio.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const builtin = @import("builtin");
4 const thread = @import("thread.zig");
5
6 pub const supported = builtin.os.tag == .macos and builtin.cpu.arch.endian() == .little;
7
8 pub const sample_format_s16_native: u8 = 1;
9 pub const buffer_count: usize = 3;
10 pub const maximum_frames_per_buffer: usize = 65_536;
11
12 const status_ok: i32 = 0;
13 const audio_format_linear_pcm = fourCC("lpcm");
14 const audio_format_flag_signed_integer: u32 = 1 << 2;
15 const audio_format_flag_packed: u32 = 1 << 3;
16 const audio_queue_property_is_running = fourCC("aqrn");
17 const audio_hardware_property_default_output_device = fourCC("dOut");
18 const audio_object_property_scope_global = fourCC("glob");
19 const audio_object_property_element_main: u32 = 0;
20 const audio_object_system: u32 = 1;
21 const audio_object_unknown: u32 = 0;
22 const all_buffers_free: u8 = (@as(u8, 1) << @intCast(buffer_count)) - 1;
23
24 pub const Error = error{
25 UnsupportedPlatform,
26 UnsupportedFormat,
27 CapacityOverflow,
28 ChannelLimitExceeded,
29 ClientNameLimitExceeded,
30 FrameLimitExceeded,
31 BufferLimitExceeded,
32 QueueCreationFailed,
33 BufferAllocationFailed,
34 PropertyListenerFailed,
35 EnqueueFailed,
36 StartFailed,
37 StopFailed,
38 OutOfMemory,
39 };
40
41 pub const Limits = struct {
42 client_name_bytes: usize = 255,
43 channels: u8 = 2,
44 };
45
46 pub const Spec = struct {
47 format: u8 = sample_format_s16_native,
48 channels: u8 = 2,
49 rate: u32 = 48_000,
50 frames_per_buffer: usize = 256,
51 };
52
53 pub const StorageLimits = struct {
54 frames_per_buffer: usize,
55 channels: u8,
56 };
57
58 pub const CapacityError = error{
59 CapacityOverflow,
60 ChannelLimitExceeded,
61 FrameLimitExceeded,
62 };
63
64 pub const Capacity = struct {
65 frames_per_buffer: usize,
66 frame_bytes: u32,
67 buffer_bytes: u32,
68 foreign_audio_bytes: usize,
69 callback_state_bytes: usize,
70
71 pub fn derive(limits: StorageLimits) CapacityError!Capacity {
72 if (limits.frames_per_buffer == 0 or limits.frames_per_buffer > maximum_frames_per_buffer) {
73 return error.FrameLimitExceeded;
74 }
75 if (limits.channels == 0) return error.ChannelLimitExceeded;
76
77 const frame_bytes = std.math.mul(
78 usize,
79 @sizeOf(i16),
80 @as(usize, limits.channels),
81 ) catch return error.CapacityOverflow;
82 if (frame_bytes > std.math.maxInt(u32)) return error.CapacityOverflow;
83
84 const buffer_bytes = std.math.mul(
85 usize,
86 limits.frames_per_buffer,
87 frame_bytes,
88 ) catch return error.CapacityOverflow;
89 if (buffer_bytes > std.math.maxInt(u32)) return error.CapacityOverflow;
90
91 const foreign_audio_bytes = std.math.mul(
92 usize,
93 buffer_count,
94 buffer_bytes,
95 ) catch return error.CapacityOverflow;
96
97 return .{
98 .frames_per_buffer = limits.frames_per_buffer,
99 .frame_bytes = @intCast(frame_bytes),
100 .buffer_bytes = @intCast(buffer_bytes),
101 .foreign_audio_bytes = foreign_audio_bytes,
102 .callback_state_bytes = @sizeOf(State),
103 };
104 }
105 };
106
107 const CoreAudioLimits = Limits;
108 const CoreAudioCapacity = Capacity;
109
110 pub const Storage = struct {
111 phase: alloc_phase.capacity.Phase,
112 capacity: CoreAudioCapacity,
113 state: *State,
114
115 pub const Limits: type = StorageLimits;
116 pub const Capacity: type = CoreAudioCapacity;
117 pub const Exhaustion = error{BufferLimitExceeded};
118 pub const InitError = std.mem.Allocator.Error || CapacityError;
119
120 pub const claim: alloc_phase.capacity.Declaration = .{
121 .source = .{
122 .id = "sys.coreaudio_playback_storage",
123 .kind = .phase_static,
124 .limit_source = .caller,
125 .storage = .{
126 .covered = &.{
127 .{
128 .id = "one_stable_coreaudio_callback_state",
129 .lifetime = .steady,
130 .detail = "one stable CoreAudio callback state",
131 },
132 },
133 .excluded = &.{
134 "caller-owned interleaved PCM frames",
135 "AudioQueue objects and framework-allocated playback buffers",
136 "CoreAudio device, converter, and callback thread storage",
137 },
138 },
139 .capacity = .{
140 .inputs = &.{},
141 .type_selectors = &.{
142 alloc_phase.capacity.bindType(State, "state"),
143 },
144 .nodes = &.{
145 .{ .constant = 1 },
146 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
147 },
148 .assertions = &.{.{
149 .scope = .closure_total,
150 .measure = .retained,
151 .relation = .exact,
152 .expression = 1,
153 }},
154 },
155 .overload = .{
156 .kind = .reject_before_mutation,
157 .detail = "oversize playback payloads reject before a free queue buffer is acquired",
158 },
159 .risks = .{
160 .transitive = .{
161 .status = .open,
162 .detail = "playback synchronization enters the host thread implementation",
163 },
164 .foreign = .{
165 .status = .open,
166 .detail = "AudioQueue owns three bounded buffers and internal device state",
167 },
168 },
169 .obligations = &.{
170 .{ .key = "sys_coreaudio_playback_capacity_capacity_model", .role = .capacity_model },
171 .{ .key = "sys_coreaudio_playback_capacity_overload", .role = .overload },
172 .{ .key = "sys_coreaudio_playback_oom_retry", .role = .custom },
173 },
174 },
175 .bindings = .{
176 .owner = @This(),
177 .seal = .{
178 .family = alloc_phase.capacity.selector(@This().activate),
179 .premise = .{
180 .class = .checked_semantic_fact,
181 .authority = .checker,
182 },
183 },
184 .teardown = .{
185 .family = alloc_phase.capacity.selector(@This().deinit),
186 .premise = .{
187 .class = .checked_semantic_fact,
188 .authority = .checker,
189 },
190 },
191 },
192 };
193
194 pub fn init(allocator: std.mem.Allocator, limits: StorageLimits) InitError!Storage {
195 const capacity = try CoreAudioCapacity.derive(limits);
196 const state = try allocator.create(State);
197 state.* = .{};
198 return .{
199 .phase = .initialization,
200 .capacity = capacity,
201 .state = state,
202 };
203 }
204
205 pub fn activate(self: *Storage) void {
206 std.debug.assert(self.phase == .initialization);
207 self.phase = .steady;
208 }
209
210 pub fn validatePayload(self: *Storage, byte_count: usize) Exhaustion!void {
211 std.debug.assert(self.phase == .steady);
212 if (byte_count > self.capacity.buffer_bytes) return error.BufferLimitExceeded;
213 }
214
215 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
216 std.debug.assert(self.phase != .teardown);
217 self.phase = .teardown;
218 allocator.destroy(self.state);
219 self.state = undefined;
220 }
221 };
222
223 comptime {
224 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
225 }
226
227 const CoreAudioStorage = Storage;
228
229 pub const Playback = struct {
230 queue: AudioQueueRef,
231 storage: CoreAudioStorage,
232
233 pub const Limits: type = CoreAudioLimits;
234 pub const Capacity: type = CoreAudioCapacity;
235 pub const Storage: type = CoreAudioStorage;
236
237 pub fn open(
238 allocator: std.mem.Allocator,
239 limits: CoreAudioLimits,
240 spec: Spec,
241 name: []const u8,
242 ) Error!Playback {
243 if (comptime !supported) return error.UnsupportedPlatform;
244 if (name.len > limits.client_name_bytes) return error.ClientNameLimitExceeded;
245 if (spec.channels == 0 or spec.channels > limits.channels) {
246 return error.ChannelLimitExceeded;
247 }
248 if (spec.format != sample_format_s16_native or spec.rate == 0) {
249 return error.UnsupportedFormat;
250 }
251
252 var storage = CoreAudioStorage.init(allocator, .{
253 .frames_per_buffer = spec.frames_per_buffer,
254 .channels = spec.channels,
255 }) catch |err| return switch (err) {
256 error.OutOfMemory => error.OutOfMemory,
257 error.CapacityOverflow => error.CapacityOverflow,
258 error.ChannelLimitExceeded => error.ChannelLimitExceeded,
259 error.FrameLimitExceeded => error.FrameLimitExceeded,
260 };
261 errdefer storage.deinit(allocator);
262
263 const format = streamDescription(spec, storage.capacity);
264
265 var optional_queue: ?AudioQueueRef = null;
266 if (AudioQueueNewOutput(
267 &format,
268 bufferComplete,
269 storage.state,
270 null,
271 null,
272 0,
273 &optional_queue,
274 ) != status_ok) return error.QueueCreationFailed;
275 const queue = optional_queue orelse return error.QueueCreationFailed;
276 errdefer _ = AudioQueueDispose(queue, 1);
277
278 if (AudioQueueAddPropertyListener(
279 queue,
280 audio_queue_property_is_running,
281 runningChanged,
282 storage.state,
283 ) != status_ok) return error.PropertyListenerFailed;
284
285 for (&storage.state.buffers) |*slot| {
286 var optional_buffer: ?*AudioQueueBuffer = null;
287 if (AudioQueueAllocateBuffer(
288 queue,
289 storage.capacity.buffer_bytes,
290 &optional_buffer,
291 ) != status_ok) return error.BufferAllocationFailed;
292 slot.* = optional_buffer orelse return error.BufferAllocationFailed;
293 }
294
295 storage.activate();
296 return .{
297 .queue = queue,
298 .storage = storage,
299 };
300 }
301
302 pub fn close(self: *Playback, allocator: std.mem.Allocator) void {
303 _ = AudioQueueDispose(self.queue, 1);
304 self.storage.deinit(allocator);
305 self.* = undefined;
306 }
307
308 pub fn writeFrames(self: *Playback, bytes: []const u8) Error!void {
309 try self.storage.validatePayload(bytes.len);
310 if (bytes.len == 0) return;
311 if (bytes.len % self.storage.capacity.frame_bytes != 0) {
312 return error.BufferLimitExceeded;
313 }
314
315 const state = self.storage.state;
316 const buffer = acquireBuffer(state) catch return error.StartFailed;
317 var submitted = false;
318 defer if (!submitted) releaseBuffer(state, buffer);
319
320 std.debug.assert(bytes.len <= buffer.data_capacity);
321 const target: [*]u8 = @ptrCast(buffer.data);
322 @memcpy(target[0..bytes.len], bytes);
323 buffer.data_size = @intCast(bytes.len);
324
325 if (AudioQueueEnqueueBuffer(self.queue, buffer, 0, null) != status_ok) {
326 return error.EnqueueFailed;
327 }
328 submitted = true;
329
330 state.mutex.lock();
331 const needs_start = !state.started;
332 if (needs_start) {
333 state.started = true;
334 state.running = true;
335 }
336 state.mutex.unlock();
337
338 if (needs_start and AudioQueueStart(self.queue, null) != status_ok) {
339 state.mutex.lock();
340 state.failed = true;
341 state.started = false;
342 state.running = false;
343 state.condition.broadcast();
344 state.mutex.unlock();
345 _ = AudioQueueStop(self.queue, 1);
346 return error.StartFailed;
347 }
348 }
349
350 pub fn drain(self: *Playback) Error!void {
351 const state = self.storage.state;
352 state.mutex.lock();
353 const should_stop = state.started;
354 state.mutex.unlock();
355 if (!should_stop) return;
356
357 if (AudioQueueStop(self.queue, 0) != status_ok) return error.StopFailed;
358
359 state.mutex.lock();
360 defer state.mutex.unlock();
361 while (state.running) state.condition.wait(&state.mutex);
362 std.debug.assert(state.free_mask == all_buffers_free);
363 }
364 };
365
366 pub fn available() bool {
367 if (comptime !supported) return false;
368
369 const address = AudioObjectPropertyAddress{
370 .selector = audio_hardware_property_default_output_device,
371 .scope = audio_object_property_scope_global,
372 .element = audio_object_property_element_main,
373 };
374 var output_device: u32 = audio_object_unknown;
375 var byte_count: u32 = @sizeOf(@TypeOf(output_device));
376 return AudioObjectGetPropertyData(
377 audio_object_system,
378 &address,
379 0,
380 null,
381 &byte_count,
382 &output_device,
383 ) == status_ok and
384 byte_count == @sizeOf(@TypeOf(output_device)) and
385 output_device != audio_object_unknown;
386 }
387
388 const State = struct {
389 mutex: thread.Mutex = .{},
390 condition: thread.Condition = .{},
391 buffers: [buffer_count]?*AudioQueueBuffer = @splat(null),
392 free_mask: u8 = all_buffers_free,
393 running: bool = false,
394 started: bool = false,
395 failed: bool = false,
396 };
397
398 const AudioQueue = opaque {};
399 const AudioQueueRef = *AudioQueue;
400
401 const AudioQueueBuffer = extern struct {
402 data_capacity: u32,
403 data: *anyopaque,
404 data_size: u32,
405 user_data: ?*anyopaque,
406 packet_description_capacity: u32,
407 packet_descriptions: ?*anyopaque,
408 packet_description_count: u32,
409 };
410
411 const AudioStreamBasicDescription = extern struct {
412 sample_rate: f64,
413 format_id: u32,
414 format_flags: u32,
415 bytes_per_packet: u32,
416 frames_per_packet: u32,
417 bytes_per_frame: u32,
418 channels_per_frame: u32,
419 bits_per_channel: u32,
420 reserved: u32,
421 };
422
423 const AudioObjectPropertyAddress = extern struct {
424 selector: u32,
425 scope: u32,
426 element: u32,
427 };
428
429 const AudioQueueOutputCallback = *const fn (
430 user_data: ?*anyopaque,
431 queue: AudioQueueRef,
432 buffer: *AudioQueueBuffer,
433 ) callconv(.c) void;
434
435 const AudioQueuePropertyListener = *const fn (
436 user_data: ?*anyopaque,
437 queue: AudioQueueRef,
438 property: u32,
439 ) callconv(.c) void;
440
441 extern fn AudioQueueNewOutput(
442 format: *const AudioStreamBasicDescription,
443 callback: AudioQueueOutputCallback,
444 user_data: ?*anyopaque,
445 callback_run_loop: ?*anyopaque,
446 callback_run_loop_mode: ?*anyopaque,
447 flags: u32,
448 queue: *?AudioQueueRef,
449 ) i32;
450 extern fn AudioQueueDispose(queue: AudioQueueRef, immediate: u8) i32;
451 extern fn AudioQueueAllocateBuffer(
452 queue: AudioQueueRef,
453 byte_count: u32,
454 buffer: *?*AudioQueueBuffer,
455 ) i32;
456 extern fn AudioQueueEnqueueBuffer(
457 queue: AudioQueueRef,
458 buffer: *AudioQueueBuffer,
459 packet_description_count: u32,
460 packet_descriptions: ?*const anyopaque,
461 ) i32;
462 extern fn AudioQueueStart(queue: AudioQueueRef, start_time: ?*const anyopaque) i32;
463 extern fn AudioQueueStop(queue: AudioQueueRef, immediate: u8) i32;
464 extern fn AudioQueueGetProperty(
465 queue: AudioQueueRef,
466 property: u32,
467 output: *anyopaque,
468 byte_count: *u32,
469 ) i32;
470 extern fn AudioQueueAddPropertyListener(
471 queue: AudioQueueRef,
472 property: u32,
473 listener: AudioQueuePropertyListener,
474 user_data: ?*anyopaque,
475 ) i32;
476 extern fn AudioObjectGetPropertyData(
477 object: u32,
478 address: *const AudioObjectPropertyAddress,
479 qualifier_byte_count: u32,
480 qualifier: ?*const anyopaque,
481 output_byte_count: *u32,
482 output: *anyopaque,
483 ) i32;
484
485 fn acquireBuffer(state: *State) error{Failed}!*AudioQueueBuffer {
486 state.mutex.lock();
487 defer state.mutex.unlock();
488 while (state.free_mask == 0 and !state.failed) {
489 state.condition.wait(&state.mutex);
490 }
491 if (state.failed) return error.Failed;
492
493 var index: usize = 0;
494 while (index < buffer_count) : (index += 1) {
495 const bit = bufferBit(index);
496 if (state.free_mask & bit == 0) continue;
497 state.free_mask &= ~bit;
498 return state.buffers[index].?;
499 }
500 unreachable;
501 }
502
503 fn releaseBuffer(state: *State, buffer: *AudioQueueBuffer) void {
504 state.mutex.lock();
505 defer state.mutex.unlock();
506 for (state.buffers, 0..) |candidate, index| {
507 if (candidate != buffer) continue;
508 const bit = bufferBit(index);
509 std.debug.assert(state.free_mask & bit == 0);
510 state.free_mask |= bit;
511 state.condition.signal();
512 return;
513 }
514 unreachable;
515 }
516
517 fn bufferComplete(
518 optional_state: ?*anyopaque,
519 _: AudioQueueRef,
520 buffer: *AudioQueueBuffer,
521 ) callconv(.c) void {
522 const state: *State = @ptrCast(@alignCast(optional_state.?));
523 releaseBuffer(state, buffer);
524 }
525
526 fn runningChanged(
527 optional_state: ?*anyopaque,
528 queue: AudioQueueRef,
529 property: u32,
530 ) callconv(.c) void {
531 std.debug.assert(property == audio_queue_property_is_running);
532 var running: u32 = 0;
533 var byte_count: u32 = @sizeOf(@TypeOf(running));
534 const status = AudioQueueGetProperty(queue, property, &running, &byte_count);
535
536 const state: *State = @ptrCast(@alignCast(optional_state.?));
537 state.mutex.lock();
538 defer state.mutex.unlock();
539 state.running = status == status_ok and
540 byte_count == @sizeOf(@TypeOf(running)) and
541 running != 0;
542 if (!state.running) state.started = false;
543 state.condition.broadcast();
544 }
545
546 fn bufferBit(index: usize) u8 {
547 std.debug.assert(index < buffer_count);
548 return @as(u8, 1) << @intCast(index);
549 }
550
551 fn streamDescription(spec: Spec, capacity: Capacity) AudioStreamBasicDescription {
552 std.debug.assert(spec.rate != 0);
553 std.debug.assert(spec.channels != 0);
554 std.debug.assert(capacity.frame_bytes == @sizeOf(i16) * @as(u32, spec.channels));
555 return .{
556 .sample_rate = @floatFromInt(spec.rate),
557 .format_id = audio_format_linear_pcm,
558 .format_flags = audio_format_flag_signed_integer | audio_format_flag_packed,
559 .bytes_per_packet = capacity.frame_bytes,
560 .frames_per_packet = 1,
561 .bytes_per_frame = capacity.frame_bytes,
562 .channels_per_frame = spec.channels,
563 .bits_per_channel = 16,
564 .reserved = 0,
565 };
566 }
567
568 fn fourCC(comptime text: []const u8) u32 {
569 if (text.len != 4) @compileError("fourCC requires four bytes");
570 return @as(u32, text[0]) << 24 |
571 @as(u32, text[1]) << 16 |
572 @as(u32, text[2]) << 8 |
573 @as(u32, text[3]);
574 }
575
576 test "coreaudio playback capacity bounds three queue buffers" {
577 comptime {
578 alloc_phase.capacity.record(
579 alloc_phase.capacity.witness(Storage, "sys_coreaudio_playback_capacity_capacity_model"),
580 );
581 }
582 comptime {
583 alloc_phase.capacity.record(
584 alloc_phase.capacity.witness(Storage, "sys_coreaudio_playback_capacity_overload"),
585 );
586 }
587
588 const capacity = try Capacity.derive(.{
589 .frames_per_buffer = 256,
590 .channels = 2,
591 });
592 try std.testing.expectEqual(@as(u32, 4), capacity.frame_bytes);
593 try std.testing.expectEqual(@as(u32, 1024), capacity.buffer_bytes);
594 try std.testing.expectEqual(@as(usize, 3072), capacity.foreign_audio_bytes);
595 try std.testing.expectEqual(@sizeOf(State), capacity.callback_state_bytes);
596
597 try std.testing.expectError(
598 error.FrameLimitExceeded,
599 Capacity.derive(.{
600 .frames_per_buffer = maximum_frames_per_buffer + 1,
601 .channels = 2,
602 }),
603 );
604 }
605
606 fn checkStorageInitFailures(allocator: std.mem.Allocator) !void {
607 var storage = try Storage.init(allocator, .{
608 .frames_per_buffer = 256,
609 .channels = 2,
610 });
611 storage.deinit(allocator);
612 }
613
614 test "coreaudio playback storage retries after allocation failure" {
615 comptime {
616 alloc_phase.capacity.record(
617 alloc_phase.capacity.witness(Storage, "sys_coreaudio_playback_oom_retry"),
618 );
619 }
620
621 try std.testing.checkAllAllocationFailures(
622 std.testing.allocator,
623 checkStorageInitFailures,
624 .{},
625 );
626
627 var storage = try Storage.init(std.testing.allocator, .{
628 .frames_per_buffer = 256,
629 .channels = 2,
630 });
631 defer storage.deinit(std.testing.allocator);
632 storage.activate();
633 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.phase);
634 try std.testing.expectError(
635 error.BufferLimitExceeded,
636 storage.validatePayload(@as(usize, storage.capacity.buffer_bytes) + 1),
637 );
638 }