Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any lower-latency (than Mach semaphores) means of synchronization for audio programming on OS X?

I'm currently using a TPCircularBuffer to synchronize decoding audio data from an external library (libxmp: http://xmp.sourceforge.net/) and playing it back via the AudioUnits API on OS X.

Mach semaphores are used to signal when the buffer needs to be refilled.

However, there seems to be a "gap" in the audio (and the audio seems to play more slowly than usual) when the semaphore is triggered.

Is there any lower-latency method of synchronization that can be used in this case?

A proof-of-concept is here: https://gist.github.com/douglas-carmichael/cda1117e42e917397ed7

This is the structure I am passing down to the callback:

struct StreamData
{
    TPCircularBuffer ringBuffer;
    semaphore_t semaphore;
    int fillThreshold;
};

Here is how I am creating the semaphore:

// Initialize our semaphore
mach_port_t self = mach_task_self();
kern_return_t ret = semaphore_create(self, &ourStream.semaphore, SYNC_POLICY_FIFO, 0);
if (ret != KERN_SUCCESS)
{
NSLog(@"Semaphore creation failed. Error <%d, %s>", ret, mach_error_string(ret));
return 0;
}

Here is the playback loop:

// Start our playback loop
struct xmp_frame_info ourFrameInfo;
int err = true;
while (xmp_play_frame(myContext) == 0)
{
 xmp_get_frame_info(myContext, &ourFrameInfo);
 if (ourFrameInfo.loop_count > 0)
 break;

 /* Are we getting a buffer overrun? */
  if (err != false)
  {
  err = TPCircularBufferProduceBytes(&ourStream.ringBuffer, ourFrameInfo.buffer, ourFrameInfo.buffer_size);
  }
  semaphore_wait(ourStream.semaphore);
 }

Here is the rendering callback:

static OSStatus renderModuleCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
                                 const AudioTimeStamp *inTimeStamp,
                                 UInt32 inBusNumber,
                                 UInt32 inBufferFrames,
                                 AudioBufferList *ioData)
{

struct StreamData *ourStream = inRefCon;

/* Initialize our variable for how much is available */
int bytesAvailable = 0;

/* Grab the data from the circular buffer into a temporary buffer */
SInt16 *ourBuffer = TPCircularBufferTail(&ourStream->ringBuffer, &bytesAvailable);

/* Do we have enough data? */
/* Note: fillThreshold is the maximum output buffer size for the device. */

if (bytesAvailable < ourStream->fillThreshold)
{
    semaphore_signal(ourStream->semaphore);
}

 /* memcpy() the data to the audio output */
 memcpy(ioData->mBuffers[0].mData, ourBuffer, bytesAvailable);

 /* Clear that section of the buffer */
 TPCircularBufferConsume(inRefCon, bytesAvailable);

 return noErr;
 }
like image 567
dcarmich Avatar asked Dec 05 '25 20:12

dcarmich


1 Answers

There appear to be Apple DTS recommendations not do anything that could lock or allocate memory inside a short real-time Audio Unit render callback, possibly even including semaphores and mach signaling.

Instead, an app can repeatedly poll the lock-free circular fifo in another thread (not the render callback thread). Given that both the sample rate and size of fifo are known, a polling rate clearly faster than the fifo empty-to-threshold rate should work, be fairly efficient, and require no locks. Latency can be controlled by varying both the threshold level and corresponding polling rate. An repeating NSTimer or CADisplayLink (or Open GL frame render) timer may be suitable for polling.

Note that the circular buffer needs to be larger than the fill threshold so the the fill routine has sufficient time to work asynchronous to the audio callback. And, of course, the worse case fill rate has to be faster than the best case fifo emptying rate.

like image 82
hotpaw2 Avatar answered Dec 08 '25 12:12

hotpaw2



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!