AudioQueue Output fails playing audio almost immediately?

On macOS Sequoia, I'm having the hardest time getting this basic audio output to work correctly. I'm compiling in XCode using C99, and when I run this, I get audio for a split second, and then nothing, indefinitely.

Any ideas what could be going wrong?

Here's a minimum code example to demonstrate:

#include <AudioToolbox/AudioToolbox.h>
#include <stdint.h>

#define RENDER_BUFFER_COUNT 2
#define RENDER_FRAMES_PER_BUFFER 128
// mono linear PCM audio data at 48kHz
#define RENDER_SAMPLE_RATE 48000
#define RENDER_CHANNEL_COUNT 1
#define RENDER_BUFFER_BYTE_COUNT (RENDER_FRAMES_PER_BUFFER * RENDER_CHANNEL_COUNT * sizeof(f32))

void RenderAudioSaw(float* outBuffer, uint32_t frameCount, uint32_t channelCount)
{
    static bool isInverted = false;
    float scalar = isInverted ? -1.f : 1.f;
    for (uint32_t frame = 0; frame < frameCount; ++frame)
    {
        for (uint32_t channel = 0; channel < channelCount; ++channel)
        {
            // series of ramps, alternating up and down.
            outBuffer[frame * channelCount + channel] = 0.1f * scalar * ((float)frame / frameCount);
        }
    }
    
    isInverted = !isInverted;
}

AudioStreamBasicDescription coreAudioDesc = { 0 };
AudioQueueRef coreAudioQueue = NULL;
AudioQueueBufferRef coreAudioBuffers[RENDER_BUFFER_COUNT] = { NULL };

void coreAudioCallback(void* unused, AudioQueueRef queue, AudioQueueBufferRef buffer)
{
    // 0's here indicate no fancy packet magic
    AudioQueueEnqueueBuffer(queue, buffer, 0, 0);
}

int main(void)
{
    const UInt32 BytesPerSample = sizeof(float);
    
    coreAudioDesc.mSampleRate = RENDER_SAMPLE_RATE;
    coreAudioDesc.mFormatID = kAudioFormatLinearPCM;
    coreAudioDesc.mFormatFlags = kLinearPCMFormatFlagIsFloat | kLinearPCMFormatFlagIsPacked;
    coreAudioDesc.mBytesPerPacket = RENDER_CHANNEL_COUNT * BytesPerSample;
    coreAudioDesc.mFramesPerPacket = 1;
    coreAudioDesc.mBytesPerFrame = RENDER_CHANNEL_COUNT * BytesPerSample;
    coreAudioDesc.mChannelsPerFrame = RENDER_CHANNEL_COUNT;
    coreAudioDesc.mBitsPerChannel = BytesPerSample * 8;
    
    coreAudioQueue = NULL;
    
    OSStatus result;

    // most of the 0 and NULL params here are for compressed sound formats etc.
    result = AudioQueueNewOutput(&coreAudioDesc, &coreAudioCallback, NULL, 0, 0, 0, &coreAudioQueue);
    
    if (result != noErr)
    {
        assert(false == "AudioQueueNewOutput failed!");
        abort();
    }
    
    for (int i = 0; i < RENDER_BUFFER_COUNT; ++i)
    {
        uint32_t bufferSize = coreAudioDesc.mBytesPerFrame * RENDER_FRAMES_PER_BUFFER;
        result = AudioQueueAllocateBuffer(coreAudioQueue, bufferSize, &(coreAudioBuffers[i]));
        if (result != noErr)
        {
            assert(false == "AudioQueueAllocateBuffer failed!");
            abort();
        }
    }
    
    for (int i = 0; i < RENDER_BUFFER_COUNT; ++i)
    {
        RenderAudioSaw(coreAudioBuffers[i]->mAudioData, RENDER_FRAMES_PER_BUFFER, RENDER_CHANNEL_COUNT);
        coreAudioBuffers[i]->mAudioDataByteSize = coreAudioBuffers[i]->mAudioDataBytesCapacity;
        
        AudioQueueEnqueueBuffer(coreAudioQueue, coreAudioBuffers[i], 0, 0);
    }

    AudioQueueStart(coreAudioQueue, NULL);

    sleep(10); // some time to hear the audio
    
    AudioQueueStop(coreAudioQueue, true);
    AudioQueueDispose(coreAudioQueue, true);
    
    return 0;
}

instead of sleeping for 10 (what? seconds?) try running a runloop for a while:

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);

It gets a bit tedious during development if you always have to wait until your program is done (even if it isn't working), so you can set up a runloop dispatch source before hand. Hitting any key then return should exit your program. This example is Objective-C++, I'm sure you can adapt it to C, or just make your main file a .mm file.

dispatch_source_t source = dispatch_source_create(
            DISPATCH_SOURCE_TYPE_READ, // dispatch_source_type_t  _Nonnull type,
            STDIN_FILENO,              // uintptr_t handle,
            0,                         // uintptr_t mask, (unused)
            DISPATCH_TARGET_QUEUE_DEFAULT);
    dispatch_source_set_event_handler(source, ^{
        std::cout << "exiting\n";
        exit(0);
    });
    
    dispatch_activate(source);
    do {
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
    } while (true);

Thanks for the suggestion.

I should have clarified that the standard sleep() function takes seconds, it's just to minimize code amount.

Sadly, this does not resolve the issue.

AudioQueue Output fails playing audio almost immediately?
 
 
Q