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 }