Skip to content

aqs-stream.c

This is the source for the fromAudioDevice Stream implementation for Audio Queue Services, used for live audio capture on macOS and iOS.

Instructions

See live-spot-stream.c.

Code

Available in this TrulyNatural SDK installation at ~/Sensory/TrulyNaturalSDK/7.6.1/sample/c/src/aqs-stream.{c,h}

aqs-stream.h

/* Sensory Confidential
 * Copyright (C)2019-2025 Sensory, Inc. https://sensory.com/
 *
 * TrulyHandsfree SDK custom stream header. See aqs-stream.c.
 *------------------------------------------------------------------------------
 */

typedef enum {
  STREAM_LATENCY_LOW,  /* low latency, high CPU overhead          */
  STREAM_LATENCY_HIGH, /* higher latency, with lower CPU overhead */
} StreamLatency;

SnsrStream
streamFromAQS(unsigned int rate, SnsrStreamMode mode, StreamLatency latency);

aqs-stream.c

/* Sensory Confidential
 * Copyright (C)2018-2025 Sensory, Inc. https://sensory.com/
 *
 *------------------------------------------------------------------------------
 * SnsrStream provider for Audio Queue Services on Darwin.
 * Currently capture-only.
 *------------------------------------------------------------------------------
 */

#include <snsr.h>

#include <assert.h>

#include <pthread.h>
#include <string.h>
#include <stdio.h>

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


#include "aqs-stream.h"

/* Initial size of the circular capture buffer. 500 ms at 16kHz */
#define CAPTURE_MINSIZE  16000
/* Maximum size of the circular capture buffer.  10 s  at 16kHz */
#define CAPTURE_MAXSIZE 320000

#define PERIOD_LOW_LATENCY  0.015
#define PERIOD_HIGH_LATENCY 0.200
#define BUFFER_SIZE_SECONDS 2.0

/* 10 ms at 16 kHz */
#define MIN_BUFFER_SIZE     320
/* 5 s at 16 kHz */
#define MAX_BUFFER_SIZE  160000
#define MIN_BUFFER_COUNT    4

typedef struct {
  SnsrStream           capture;       /* Captured audio buffer                */
  const char          *initErrorMsg;  /* NULL if initialization ok            */
  AudioStreamBasicDescription dataFormat;  /* recording format description    */
  AudioQueueRef        queue;              /* OS-level recording queue        */
  AudioQueueBufferRef *buffer;             /* queue buffers                   */
  pthread_mutex_t      lock;               /* protects capture stream         */
  pthread_cond_t       notEmpty;           /* signals capture stream          */
  size_t               bufferCount;        /* number of buffers in *buffer    */
  UInt32               bufferByteSize;     /* size of one queue buffer        */
  unsigned             isRunning:1;        /* 0 to wind down recording        */
} ProviderData;


static SnsrRC
ossErrorMessage(SnsrStream b, OSStatus s)
{
  const char *msg;
  switch (s) {
  case kAudioServicesNoError:
    msg = NULL;
  case kAudioServicesUnsupportedPropertyError:
    msg = "The property is not supported.";
    break;
  case kAudioServicesBadPropertySizeError:
    msg = "The size of the property data was not correct.";
    break;
  case kAudioServicesBadSpecifierSizeError:
    msg = "The size of the specifier data was not correct.";
    break;
  case kAudioServicesSystemSoundClientTimedOutError:
    msg = "System sound client message timed out.";
    break;
  case kAudioServicesSystemSoundUnspecifiedError:
  default:
    snsrStream_setDetail(b, "An unspecified error occurred, code %i.", (int)s);
    return SNSR_RC_ERROR;
  }
  snsrStream_setDetail(b, "%s", msg);
  return SNSR_RC_ERROR;
}


static void
audioCallback(void *privateData,
              AudioQueueRef inAQ,
              AudioQueueBufferRef inBuffer,
              const AudioTimeStamp *inStartTime,
              UInt32 inNumPackets,
              const AudioStreamPacketDescription *inPacketDesc)
{
  ProviderData *d = (ProviderData *)privateData;
  size_t written;

  if (!inNumPackets && d->dataFormat.mBytesPerPacket)
    inNumPackets = inBuffer->mAudioDataByteSize / d->dataFormat.mBytesPerPacket;
  if (inNumPackets) {
    pthread_mutex_lock(&d->lock);
    written = snsrStreamWrite(d->capture, inBuffer->mAudioData,
                              d->dataFormat.mBytesPerPacket, inNumPackets);
    if (written < inNumPackets)
      snsrStream_setRC(d->capture, SNSR_RC_BUFFER_OVERRUN);
    pthread_cond_broadcast(&d->notEmpty);
    pthread_mutex_unlock(&d->lock);
  }
  if (d->isRunning)
    AudioQueueEnqueueBuffer(d->queue, inBuffer, 0, NULL);
}


static OSStatus
audioInit(ProviderData *d, double sampleRate,
          double chunkSizeSeconds, double totalSizeSeconds)
{
  AudioStreamBasicDescription *f = &d->dataFormat;
  OSStatus s;
  int i;

  /* Recording format: 16-bit LPCM at 16 kHz */
  memset(f, 0, sizeof(*f));
  f->mFormatID = kAudioFormatLinearPCM;
  f->mSampleRate = sampleRate;
  f->mChannelsPerFrame = 1;
  f->mBitsPerChannel = 16;
  f->mBytesPerPacket = f->mBytesPerFrame =
    f->mChannelsPerFrame * sizeof(SInt16);
  f->mFramesPerPacket = 1;
  f->mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;

  /* Create recording queue */
  s = AudioQueueNewInput(f, audioCallback, d, NULL,
                         kCFRunLoopCommonModes, 0, &d->queue);
  if (s != kAudioServicesNoError) return s;

  /* Derive appropriate audio queue buffer size */
  d->bufferByteSize =
    (UInt32)(f->mSampleRate * f->mBytesPerPacket * chunkSizeSeconds);
  if (d->bufferByteSize > MAX_BUFFER_SIZE) d->bufferByteSize = MAX_BUFFER_SIZE;
  if (d->bufferByteSize < MIN_BUFFER_SIZE) d->bufferByteSize = MIN_BUFFER_SIZE;
  d->bufferCount = ceil(totalSizeSeconds / chunkSizeSeconds);
  if (d->bufferCount < MIN_BUFFER_COUNT) d->bufferCount = MIN_BUFFER_COUNT;

  /* Allocate audio buffers */
  d->buffer = malloc(sizeof(*d->buffer) * d->bufferCount);
  for (i = 0; i < d->bufferCount && s == kAudioServicesNoError; i++)
    s = AudioQueueAllocateBuffer(d->queue, d->bufferByteSize, d->buffer + i);

  return s;
}


/*------------------------------------------------------------------------------
 */

static SnsrRC
streamOpen(SnsrStream b)
{
  ProviderData *d = (ProviderData *)snsrStream_getData(b);
  OSStatus s;
  int i;

  if (d->initErrorMsg) {
    snsrStream_setDetail(b, "%s", d->initErrorMsg);
    return SNSR_RC_NOT_FOUND;
  }
  snsrStreamOpen(d->capture);
  s = kAudioServicesNoError;
  for (i = 0; i < d->bufferCount && s == kAudioServicesNoError; i++)
    s = AudioQueueEnqueueBuffer(d->queue, d->buffer[i], 0, NULL);
  if (s == kAudioServicesNoError)
    s = AudioQueueStart(d->queue, NULL);
  if (s != kAudioServicesNoError)
    return ossErrorMessage(b, s);
  d->isRunning = 1;
  return snsrStreamRC(b);
}


static SnsrRC
streamClose(SnsrStream b)
{
  ProviderData *d = (ProviderData *)snsrStream_getData(b);
  SnsrRC r;

  d->isRunning = 0;
  AudioQueueStop(d->queue, true);

  /* Flush the capture buffer */
  r = snsrStreamRC(d->capture);
  snsrStreamSkip(d->capture, 1, CAPTURE_MAXSIZE);
  snsrStream_setRC(d->capture, SNSR_RC_OK);
  return r == SNSR_RC_OK? snsrStreamRC(b): r;
}


static void
streamRelease(SnsrStream b)
{
  ProviderData *d = (ProviderData *)snsrStream_getData(b);

  snsrRelease(d->capture);
  AudioQueueDispose(d->queue, true);
  pthread_mutex_destroy(&d->lock);
  pthread_cond_destroy(&d->notEmpty);
  free(d->buffer);
  free(d);
}


static size_t
streamRead(SnsrStream b, void *buffer, size_t size)
{
  SnsrRC r;
  ProviderData *d = (ProviderData *)snsrStream_getData(b);
  size_t read = 0;

  pthread_mutex_lock(&d->lock);
  if (snsrStreamRC(d->capture) != SNSR_RC_OK) {
    snsrStream_setRC(b, snsrStreamRC(d->capture));
  } else {
    do {
      read += snsrStreamRead(d->capture, (char *)buffer + read, 1, size - read);
      r = snsrStreamRC(d->capture);
    } while ((r == SNSR_RC_OK || r == SNSR_RC_EOF)
             && read < size
             && !pthread_cond_wait(&d->notEmpty, &d->lock));
    if (r != SNSR_RC_OK) {
      snsrStream_setRC(b, r);
      snsrStream_setDetail(b, "%s", snsrStreamErrorDetail(d->capture));
    } else if (read < size) {
      snsrStream_setRC(b, SNSR_RC_EOF);
    }
  }
  pthread_mutex_unlock(&d->lock);
  return read;
}


static SnsrStream_Vmt ProviderDef = {
  "Darwin Audio Queue Services audio capture",
  &streamOpen, &streamClose, &streamRelease, &streamRead, NULL
};


SnsrStream
streamFromAQS(unsigned int rate,
                  SnsrStreamMode mode, StreamLatency latency)
{
  SnsrStream b;
  ProviderData *d = (ProviderData *)malloc(sizeof(*d));
  OSStatus s;
  int r;

  if (!d) return NULL;
  memset(d, 0, sizeof(*d));
  b = snsrStream_alloc(&ProviderDef, d, 1, 0);
  if (!b) {
    free(d);
    return NULL;
  }
  do {
    d->capture = snsrStreamFromBuffer(CAPTURE_MINSIZE, CAPTURE_MAXSIZE);
    if (!d->capture) {
      snsrStream_setRC(b, SNSR_RC_NO_MEMORY);
      break;
    }
    snsrRetain(d->capture);
    if (mode != SNSR_ST_MODE_READ) {
      snsrStream_setRC(b, SNSR_RC_INVALID_MODE);
      break;
    }
    /* Signalling and mutexes */
    r = pthread_mutex_init(&d->lock, NULL);
    if (!r) r = pthread_cond_init(&d->notEmpty, NULL);
    if (r) {
      snsrStream_setRC(b, SNSR_RC_ERROR);
      snsrStream_setDetail(b, "%s", strerror(r));
      break;
    }

    /* Allocate buffers */
    s = audioInit(d, rate, latency == STREAM_LATENCY_LOW?
                  PERIOD_LOW_LATENCY: PERIOD_HIGH_LATENCY,
                  BUFFER_SIZE_SECONDS);
    if (s != kAudioServicesNoError) {
      snsrStream_setRC(b, ossErrorMessage(b, s));
      break;
    }
  } while (0);

  if (snsrStreamRC(b) != SNSR_RC_OK)
    d->initErrorMsg = strdup(snsrStreamErrorDetail(b));
  return b;
}