/* Native, explicit-device HAL bench client. Never changes a default device or unmutes a profile. */ #include #include #include #include #include #include #include #include #include #include #define REQUIRE(value) do { if (!(value)) { fprintf(stderr, "Failed at line %d: %s\n", __LINE__, #value); exit(1); } } while (0) static AudioUnit unit; static bool initialized, running, changed_format; static AudioStreamID target_stream; static AudioStreamBasicDescription original_format, selected_format; static struct { double phase; _Atomic uint64_t frames; _Atomic bool invalid_buffer; } render; int nxs_hid_bench_main(int count, char **arguments); static AudioObjectPropertyAddress address(AudioObjectPropertySelector selector, AudioObjectPropertyScope scope) { return (AudioObjectPropertyAddress){selector, scope, kAudioObjectPropertyElementMain}; } static void get(AudioObjectID object, AudioObjectPropertySelector selector, AudioObjectPropertyScope scope, void *data, UInt32 size) { AudioObjectPropertyAddress property = address(selector, scope); OSStatus result = AudioObjectGetPropertyData(object, &property, 0, NULL, &size, data); if (result != noErr) fprintf(stderr, "Property read failed: object %u, selector %08x, status %d\n", object, selector, (int)result); REQUIRE(result == noErr); } static void *array(AudioObjectID object, AudioObjectPropertySelector selector, AudioObjectPropertyScope scope, UInt32 *size) { AudioObjectPropertyAddress property = address(selector, scope); REQUIRE(AudioObjectGetPropertyDataSize(object, &property, 0, NULL, size) == noErr && *size != 0); void *data = malloc(*size); REQUIRE(data != NULL); get(object, selector, scope, data, *size); return data; } static CFStringRef uid(AudioDeviceID device) { CFStringRef value; get(device, kAudioDevicePropertyDeviceUID, kAudioObjectPropertyScopeGlobal, &value, sizeof(value)); return value; } static void defaults(CFStringRef values[2]) { const AudioObjectPropertySelector roles[] = {kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDefaultSystemOutputDevice}; for (size_t index = 0; index < 2; ++index) { AudioDeviceID device; get(kAudioObjectSystemObject, roles[index], kAudioObjectPropertyScopeGlobal, &device, sizeof(device)); values[index] = uid(device); } } static AudioDeviceID select_device(const char *serial, char selected_uid[512]) { REQUIRE(strlen(serial) == 16); UInt32 bytes; AudioDeviceID *devices = array(kAudioObjectSystemObject, kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, &bytes); AudioDeviceID selected = kAudioObjectUnknown; unsigned int matches = 0; for (size_t index = 0; index < bytes / sizeof(*devices); ++index) { CFStringRef identifier = uid(devices[index]); char text[512]; REQUIRE(CFStringGetCString(identifier, text, sizeof(text), kCFStringEncodingUTF8)); CFRelease(identifier); if (strstr(text, serial)) { selected = devices[index]; memcpy(selected_uid, text, strlen(text) + 1u); ++matches; } } free(devices); REQUIRE(matches == 1 && selected != kAudioObjectUnknown); UInt32 transport; get(selected, kAudioDevicePropertyTransportType, kAudioObjectPropertyScopeGlobal, &transport, sizeof(transport)); REQUIRE(transport == kAudioDeviceTransportTypeUSB); printf("{\"kind\":\"audio_device\",\"device_id\":%u,\"uid\":\"%s\"}\n", selected, selected_uid); return selected; } static void print_format(const char *kind, const AudioStreamBasicDescription *format) { printf("{\"kind\":\"%s\",\"sample_rate\":%.9g,\"format_id\":%u,\"flags\":%u,\"channels\":%u,\"bits\":%u,\"bytes_per_frame\":%u}\n", kind, format->mSampleRate, format->mFormatID, format->mFormatFlags, format->mChannelsPerFrame, format->mBitsPerChannel, format->mBytesPerFrame); } static bool same_format(const AudioStreamBasicDescription *a, const AudioStreamBasicDescription *b) { return a->mSampleRate == b->mSampleRate && a->mFormatID == b->mFormatID && a->mFormatFlags == b->mFormatFlags && a->mChannelsPerFrame == b->mChannelsPerFrame && a->mBitsPerChannel == b->mBitsPerChannel && a->mBytesPerFrame == b->mBytesPerFrame && a->mFramesPerPacket == b->mFramesPerPacket && a->mBytesPerPacket == b->mBytesPerPacket; } static OSStatus set_physical(const AudioStreamBasicDescription *format) { AudioObjectPropertyAddress property = address(kAudioStreamPropertyPhysicalFormat, kAudioObjectPropertyScopeGlobal); return AudioObjectSetPropertyData(target_stream, &property, 0, NULL, sizeof(*format), format); } static void cleanup(void) { if (running) AudioOutputUnitStop(unit); if (initialized) AudioUnitUninitialize(unit); if (unit) AudioComponentInstanceDispose(unit); if (changed_format) { AudioStreamBasicDescription current; AudioObjectPropertyAddress property = address(kAudioStreamPropertyPhysicalFormat, kAudioObjectPropertyScopeGlobal); UInt32 size = sizeof(current); if (AudioObjectGetPropertyData(target_stream, &property, 0, NULL, &size, ¤t) == noErr && same_format(¤t, &selected_format)) { OSStatus result = set_physical(&original_format); if (result != noErr) fprintf(stderr, "Original target format restore failed: %d\n", (int)result); } else { fprintf(stderr, "Target format changed externally; not replacing it during cleanup.\n"); } } } static OSStatus fill(void *context, AudioUnitRenderActionFlags *flags, const AudioTimeStamp *time, UInt32 bus, UInt32 frames, AudioBufferList *buffers) { (void)context; (void)flags; (void)time; (void)bus; if (buffers->mNumberBuffers != 1 || buffers->mBuffers[0].mNumberChannels != 2 || !buffers->mBuffers[0].mData || buffers->mBuffers[0].mDataByteSize < frames * 2u * sizeof(float)) { atomic_store_explicit(&render.invalid_buffer, true, memory_order_relaxed); for (UInt32 index = 0; index < buffers->mNumberBuffers; ++index) if (buffers->mBuffers[index].mData) memset(buffers->mBuffers[index].mData, 0, buffers->mBuffers[index].mDataByteSize); return noErr; } float *samples = buffers->mBuffers[0].mData; for (UInt32 frame = 0; frame < frames; ++frame) { float sample = (float)(0.125 * sin(render.phase)); render.phase += 6.2831853071795864769 * 1000.0 / 48000.0; if (render.phase >= 6.2831853071795864769) render.phase -= 6.2831853071795864769; samples[frame * 2u] = samples[frame * 2u + 1u] = sample; } buffers->mBuffers[0].mDataByteSize = frames * 2u * sizeof(float); atomic_fetch_add_explicit(&render.frames, frames, memory_order_relaxed); return noErr; } static void control(const char *serial, const char *operation) { char *arguments[] = {"hid-bench", (char *)serial, (char *)operation}; REQUIRE(nxs_hid_bench_main(3, arguments) == 0); } static void verify_muted(const char *serial) { control(serial, "inspect"); } int main(int count, char **arguments) { if (count != 3 && count != 5 && count != 6) { fprintf(stderr, "Usage: audio-bench inspect SERIAL | audio-bench run SERIAL BITS SECONDS\n"); return 2; } bool run = !strcmp(arguments[1], "run"); bool stress = count == 6 && !strcmp(arguments[5], "stress"); REQUIRE((run && (count == 5 || stress)) || (!strcmp(arguments[1], "inspect") && count == 3)); REQUIRE(atexit(cleanup) == 0); const char *serial = arguments[2]; CFStringRef before[2]; defaults(before); char selected_uid[512]; AudioDeviceID device = select_device(serial, selected_uid); CFStringRef identifier = uid(device); REQUIRE(!CFEqual(identifier, before[0]) && !CFEqual(identifier, before[1])); CFRelease(identifier); UInt32 bytes; AudioStreamID *streams = array(device, kAudioDevicePropertyStreams, kAudioObjectPropertyScopeOutput, &bytes); REQUIRE(bytes == sizeof(*streams)); target_stream = streams[0]; free(streams); get(target_stream, kAudioStreamPropertyPhysicalFormat, kAudioObjectPropertyScopeGlobal, &original_format, sizeof(original_format)); print_format("original_physical_format", &original_format); AudioStreamRangedDescription *formats = array(target_stream, kAudioStreamPropertyAvailablePhysicalFormats, kAudioObjectPropertyScopeGlobal, &bytes); unsigned long bits = run ? strtoul(arguments[3], NULL, 10) : 0; bool found = false; for (size_t index = 0; index < bytes / sizeof(*formats); ++index) { print_format("available_physical_format", &formats[index].mFormat); if (!found && formats[index].mFormat.mFormatID == kAudioFormatLinearPCM && !(formats[index].mFormat.mFormatFlags & kAudioFormatFlagIsNonMixable) && formats[index].mFormat.mBitsPerChannel == bits && formats[index].mFormat.mChannelsPerFrame == 2 && formats[index].mSampleRateRange.mMinimum <= 48000 && formats[index].mSampleRateRange.mMaximum >= 48000) { selected_format = formats[index].mFormat; selected_format.mSampleRate = 48000; found = true; } } free(formats); if (!run) return 0; REQUIRE(found && (bits == 16 || bits == 24)); unsigned long seconds = strtoul(arguments[4], NULL, 10); REQUIRE(seconds >= 1 && seconds <= 120 && (!stress || seconds >= 6)); verify_muted(serial); changed_format = !same_format(&selected_format, &original_format); if (changed_format) REQUIRE(set_physical(&selected_format) == noErr); struct timespec delay = {.tv_nsec = 10000000}; bool ready = false; for (unsigned int attempt = 0; attempt < 100; ++attempt) { AudioStreamBasicDescription current; get(target_stream, kAudioStreamPropertyPhysicalFormat, kAudioObjectPropertyScopeGlobal, ¤t, sizeof(current)); if (same_format(¤t, &selected_format)) { ready = true; break; } nanosleep(&delay, NULL); } REQUIRE(ready); AudioComponentDescription description = {.componentType = kAudioUnitType_Output, .componentSubType = kAudioUnitSubType_HALOutput, .componentManufacturer = kAudioUnitManufacturer_Apple}; AudioComponent component = AudioComponentFindNext(NULL, &description); REQUIRE(component != NULL && AudioComponentInstanceNew(component, &unit) == noErr); UInt32 enabled = 1, disabled = 0; REQUIRE(AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &enabled, sizeof(enabled)) == noErr); REQUIRE(AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &disabled, sizeof(disabled)) == noErr); REQUIRE(AudioUnitSetProperty(unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &device, sizeof(device)) == noErr); AudioStreamBasicDescription client = {.mSampleRate = 48000, .mFormatID = kAudioFormatLinearPCM, .mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, .mBytesPerPacket = 8, .mFramesPerPacket = 1, .mBytesPerFrame = 8, .mChannelsPerFrame = 2, .mBitsPerChannel = 32}; REQUIRE(AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &client, sizeof(client)) == noErr); AURenderCallbackStruct callback = {.inputProc = fill}; REQUIRE(AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callback, sizeof(callback)) == noErr); REQUIRE(AudioUnitInitialize(unit) == noErr); initialized = true; AudioStreamBasicDescription virtual_format; get(target_stream, kAudioStreamPropertyVirtualFormat, kAudioObjectPropertyScopeGlobal, &virtual_format, sizeof(virtual_format)); print_format("prestart_virtual_format", &virtual_format); fflush(stdout); OSStatus start_result = AudioOutputUnitStart(unit); if (start_result != noErr) fprintf(stderr, "AudioOutputUnitStart status: %d (0x%08x)\n", (int)start_result, (unsigned int)start_result); REQUIRE(start_result == noErr); running = true; AudioStreamBasicDescription actual; get(target_stream, kAudioStreamPropertyPhysicalFormat, kAudioObjectPropertyScopeGlobal, &actual, sizeof(actual)); REQUIRE(same_format(&actual, &selected_format)); print_format("running_physical_format", &actual); fflush(stdout); for (unsigned long second = 0; second < seconds; ++second) { struct timespec wait = {.tv_sec = 1}; nanosleep(&wait, NULL); REQUIRE(!atomic_load_explicit(&render.invalid_buffer, memory_order_relaxed)); if (stress && second == 1) control(serial, "stress-muted"); if (stress && second == seconds - 2) control(serial, "restore-muted"); verify_muted(serial); } REQUIRE(AudioOutputUnitStop(unit) == noErr); running = false; REQUIRE(atomic_load_explicit(&render.frames, memory_order_relaxed) >= 48000u); CFStringRef after[2]; defaults(after); REQUIRE(CFEqual(before[0], after[0]) && CFEqual(before[1], after[1])); for (size_t index = 0; index < 2; ++index) { CFRelease(before[index]); CFRelease(after[index]); } printf("{\"kind\":\"audio_run_complete\",\"bits\":%lu,\"frames\":%llu,\"default_uids_unchanged\":true}\n", bits, (unsigned long long)atomic_load_explicit(&render.frames, memory_order_relaxed)); return 0; }