Apply PR #262's device-buffer fix for clicks caused by audio underruns. Reconfigure on both output-device and hardware sample-rate changes, respect the device's frame range, and read back the actual buffer size. Avoid rewriting unchanged sizes when buffer changes notify the format listener. Remove listeners before teardown, including failed initialization, so they cannot configure a device that the AudioUnit has already released. Add hardware-independent regression coverage for switching, rate changes, scope filtering, limits, synchronous notifications, diagnostic overrides, absent devices, and listener cleanup. Record the original gameplay evidence and the additional silent Core Audio validation in the audio reference. Validation: full host tests, Mac host build, docs checks, and the iOS source compile check (178 files) passed. Silent USB/speaker switching and speaker rates of 44.1/96/48 kHz verified 221/480/240 frames. HDMI switching and gameplay listening after these listener changes remain unvalidated. Co-authored-by: parsifal295 <130349765+parsifal295@users.noreply.github.com>
157 lines
9.2 KiB
C
157 lines
9.2 KiB
C
// 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 <AudioToolbox/AudioToolbox.h>
|
|
#include <assert.h>
|
|
#include <time.h>
|
|
|
|
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;
|
|
}
|