// Mac device buffer contract: drive the real setup/listener/close paths with Core Audio property notifications, // including synchronous format notifications from a buffer write and device removal during disposal. No hardware needed. #include #include #include static AudioComponent mock_find(AudioComponent, const AudioComponentDescription *); static OSStatus mock_new(AudioComponent, AudioComponentInstance *); static OSStatus mock_get(AudioUnit, AudioUnitPropertyID, AudioUnitScope, AudioUnitElement, void *, UInt32 *); static OSStatus mock_set(AudioUnit, AudioUnitPropertyID, AudioUnitScope, AudioUnitElement, const void *, UInt32); static OSStatus mock_add(AudioUnit, AudioUnitPropertyID, AudioUnitPropertyListenerProc, void *); static OSStatus mock_remove(AudioUnit, AudioUnitPropertyID, AudioUnitPropertyListenerProc, void *); static OSStatus mock_init(AudioUnit); static OSStatus mock_uninit(AudioUnit); static OSStatus mock_dispose(AudioComponentInstance); #define AudioComponentFindNext mock_find #define AudioComponentInstanceNew mock_new #define AudioUnitGetProperty mock_get #define AudioUnitSetProperty mock_set #define AudioUnitAddPropertyListener mock_add #define AudioUnitRemovePropertyListenerWithUserData mock_remove #define AudioUnitInitialize mock_init #define AudioUnitUninitialize mock_uninit #define AudioComponentInstanceDispose mock_dispose #include "../hle_audio.c" static struct { AudioDeviceID device; Float64 rate; AudioValueRange range; UInt32 frames; AudioUnitPropertyListenerProc device_listener, format_listener; unsigned writes, disposals, input_formats; OSStatus init_error; } unit; static AudioUnit fake_au(void) { return (AudioUnit)&unit; } static void changed(AudioUnitPropertyID id, AudioUnitScope scope, AudioUnitElement el) { AudioUnitPropertyListenerProc cb = id == kAudioOutputUnitProperty_CurrentDevice ? unit.device_listener : unit.format_listener; if (cb) cb(&unit, fake_au(), id, scope, el); } static AudioComponent mock_find(AudioComponent c, const AudioComponentDescription *d) { (void)c; assert(d->componentSubType == kAudioUnitSubType_DefaultOutput); return (AudioComponent)&unit; } static OSStatus mock_new(AudioComponent c, AudioComponentInstance *au) { (void)c; assert(!unit.device_listener && !unit.format_listener); *au = fake_au(); return noErr; } static OSStatus mock_get(AudioUnit au, AudioUnitPropertyID id, AudioUnitScope scope, AudioUnitElement el, void *data, UInt32 *size) { assert(au == fake_au() && el == 0); if (id == kAudioOutputUnitProperty_CurrentDevice) { assert(scope == kAudioUnitScope_Global && *size == sizeof unit.device); memcpy(data, &unit.device, *size); } else if (id == kAudioUnitProperty_StreamFormat) { assert(scope == kAudioUnitScope_Output && *size == sizeof(AudioStreamBasicDescription)); AudioStreamBasicDescription f = {.mSampleRate = unit.rate}; memcpy(data, &f, *size); } else if (id == kAudioDevicePropertyBufferFrameSizeRange) { assert(scope == kAudioUnitScope_Global && *size == sizeof unit.range); memcpy(data, &unit.range, *size); } else { assert(id == kAudioDevicePropertyBufferFrameSize && scope == kAudioUnitScope_Global && *size == sizeof unit.frames); memcpy(data, &unit.frames, *size); } return noErr; } static OSStatus mock_set(AudioUnit au, AudioUnitPropertyID id, AudioUnitScope scope, AudioUnitElement el, const void *data, UInt32 size) { assert(au == fake_au() && el == 0); if (id == kAudioDevicePropertyBufferFrameSize) { assert(unit.device && scope == kAudioUnitScope_Global && size == sizeof unit.frames); UInt32 frames; memcpy(&frames, data, size); assert(frames >= unit.range.mMinimum && frames <= unit.range.mMaximum); unit.frames = frames; assert(++unit.writes < 100); changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); // must not recursively set the same buffer } else if (id == kAudioUnitProperty_StreamFormat) { const AudioStreamBasicDescription *f = data; assert(scope == kAudioUnitScope_Input && size == sizeof *f && f->mSampleRate == RATE && f->mChannelsPerFrame == 2); unit.input_formats++; changed(id, scope, el); // the 48 kHz guest format is not the hardware rate } else { assert(id == kAudioUnitProperty_SetRenderCallback && scope == kAudioUnitScope_Input && size == sizeof(AURenderCallbackStruct)); } return noErr; } static OSStatus mock_add(AudioUnit au, AudioUnitPropertyID id, AudioUnitPropertyListenerProc cb, void *ctx) { assert(au == fake_au() && !ctx); if (id == kAudioOutputUnitProperty_CurrentDevice) unit.device_listener = cb; else { assert(id == kAudioUnitProperty_StreamFormat); unit.format_listener = cb; } return noErr; } static OSStatus mock_remove(AudioUnit au, AudioUnitPropertyID id, AudioUnitPropertyListenerProc cb, void *ctx) { assert(au == fake_au() && !ctx); if (id == kAudioOutputUnitProperty_CurrentDevice) { assert(unit.device_listener == cb); unit.device_listener = NULL; } else { assert(id == kAudioUnitProperty_StreamFormat && unit.format_listener == cb); unit.format_listener = NULL; } return noErr; } static OSStatus mock_init(AudioUnit au) { assert(au == fake_au()); return unit.init_error; } static OSStatus mock_uninit(AudioUnit au) { assert(au == fake_au() && !unit.device_listener && !unit.format_listener); return noErr; } static OSStatus mock_dispose(AudioComponentInstance au) { assert(au == fake_au() && !unit.device_listener && !unit.format_listener); unit.device = kAudioObjectUnknown; changed(kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0); unit.disposals++; return noErr; } uint64_t now_ns(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return (uint64_t)t.tv_sec * 1000000000ull + t.tv_nsec; } static struct { const void *key; void *value; } object; void *objmap_get(const void *key) { assert(key == object.key); return object.value; } void objmap_put(const void *key, void *value) { assert(!object.key); object.key = key; object.value = value; } void *objmap_del(const void *key) { assert(key == object.key); void *value = object.value; object.key = NULL; object.value = NULL; return value; } void hle_signal_system_event(void *g) { (void)g; } void hle_bind_system_event(void *g, int autoclear) { (void)g; (void)autoclear; } module_t *module_of(const void *p) { (void)p; return NULL; } void dm_host_helper_qos(qos_class_t q) { (void)q; } int main(void) { setenv("DM_AUDIO", "1", 1); unsetenv("DM_DBG_AU_FRAMES"); unit.device = 1; unit.rate = 48000; unit.range = (AudioValueRange){15, 4096}; unit.frames = 512; uint32_t out[16] = {0}; uint8_t prm[16]; h_au_InitParam(prm); assert(h_au_OpenDef0(out, prm) == RES_OK); assert(unit.frames == 240 && unit.writes == 1 && unit.input_formats == 1 && unit.device_listener && unit.format_listener); unit.device = 2; unit.rate = 44100; unit.frames = 512; changed(kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0); assert(unit.frames == 221 && unit.writes == 2); // A device's rate can change without a CurrentDevice notification. unit.rate = 96000; changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); assert(unit.frames == 480 && unit.writes == 3); unit.rate = 48000; changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); assert(unit.frames == 240 && unit.writes == 4); unit.rate = 44100; changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0); changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1); assert(unit.frames == 240 && unit.writes == 4); changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); assert(unit.frames == 221 && unit.writes == 5); // Device limits are part of the HAL contract. Request the nearest supported frame count. unit.device = 3; unit.rate = 48000; unit.range.mMinimum = 256; changed(kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0); assert(unit.frames == 256); unit.range = (AudioValueRange){15, 128}; changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); assert(unit.frames == 128); unit.range.mMaximum = 4096; setenv("DM_DBG_AU_FRAMES", "1024", 1); changed(kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0); assert(unit.frames == 1024); unit.device = 4; unit.rate = 44100; changed(kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0); assert(unit.frames == 1024); unsetenv("DM_DBG_AU_FRAMES"); unsigned before = unit.writes; unit.device = kAudioObjectUnknown; changed(kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0); assert(unit.writes == before); unit.device = 1; h_au_Close(out); assert(unit.disposals == 1 && !object.key); // Failure after listener registration follows the same cleanup path as Close. unit.device = 1; unit.rate = 48000; unit.init_error = kAudioUnitErr_FailedInitialization; aout failed = {.ch = 2}; assert(au_create(&failed) == kAudioUnitErr_FailedInitialization && !failed.au && unit.disposals == 2); puts("audio device: switching, rate changes, limits, diagnostic override and listener cleanup passed"); return 0; }