Muestras de audio productor de múltiples hilos OSX.

Esta pregunta es un seguimiento de una pregunta anterior ( subprocesos productores de audio con subproceso de consumidor OSX AudioComponent y callback en C ), incluido un ejemplo de prueba, que funciona y se comporta como se esperaba pero no responde a la pregunta. He reformulado sustancialmente la pregunta y he vuelto a codificar el ejemplo, de modo que solo contenga el código C simple. (Descubrí que pocas porciones de código de Objective-C en el ejemplo anterior solo causaron confusión y distrajeron al lector de lo que es esencial en la pregunta).

Con el fin de aprovechar los múltiples núcleos de procesador y hacer que el subproceso de renderizado del CoreAudio sea ​​lo más liviano posible, la rutina del productor de las muestras de LPCM claramente tiene que “sentarse” en un subproceso diferente, fuera de la prioridad real de la cal. procesar hilo / callback. Debe alimentar las muestras a un búfer circular ( TPCircularBuffer en este ejemplo), desde el cual el sistema progtwigría la extracción de datos en cuantos de inNumberFrames.

El Grand Central Dispatch API ofrece una solución simple, que he deducido de algunas investigaciones individuales (incluida la encoding de prueba y error). Esta solución es elegante, ya que no bloquea nada ni entra en conflicto entre los modelos push y pull. Sin embargo, el GCD, que se supone que se encarga de “subprocesos” no cumple con los requisitos específicos de paralelización para los subprocesos de trabajo del código del productor, por lo que tuve que generar explícitamente un número de subprocesos POSIX, dependiendo del número de núcleos lógicos disponibles. Aunque los resultados ya son notables en términos de acelerar el cálculo, todavía me siento un poco incómodo al mezclar POSIX y GCD. En particular, se aplica a la variable wait_interval y se calcula correctamente, no mediante la predicción de cuántas muestras de PCM puede necesitar el subproceso de procesamiento para el siguiente ciclo.

Aquí está el código (pseudo) acortado y simplificado para mi progtwig de prueba , en plain-C.

Declaración del controlador:

#include "TPCircularBuffer.h" #include  #include  #include  #include  #include  typedef struct { TPCircularBuffer buffer; AudioComponentInstance toneUnit; Float64 sampleRate; AudioStreamBasicDescription streamFormat; Float32* f; //array of updated frequencies Float32* a; //array of updated amps Float32* prevf; //array of prev. frequencies Float32* preva; //array of prev. amps Float32* val; int* arg; int* previous_arg; UInt32 frames; int state; Boolean midif; //wip } MyAudioController; MyAudioController gen; dispatch_semaphore_t mSemaphore; Boolean multithreading, NF; typedef struct data{ int tid; int cpuCount; }data; 

Gestión del controlador:

 void setup (void){ // Initialize circular buffer TPCircularBufferInit(&(self->buffer), kBufferLength); // Create the semaphore mSemaphore = dispatch_semaphore_create(0); // Setup audio createToneUnit(&gen); } void dealloc (void) { // Release buffer resources TPCircularBufferCleanup(&buffer); // Clean up semaphore dispatch_release(mSemaphore); // dispose of audio if(gen.toneUnit){ AudioOutputUnitStop(gen.toneUnit); AudioUnitUninitialize(gen.toneUnit); AudioComponentInstanceDispose(gen.toneUnit); } } 

Llamada del despachador (lanzando la cola del productor desde el hilo principal):

 void dproducer (Boolean on, Boolean multithreading, Boolean NF) { if (on == true) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ if((multithreading)||(NF)) producerSum(on); else producer(on); }); } return; } 

Rutina del productor enhebrable:

 void producerSum(Boolean on) { int rc; int num = getCPUnum(); pthread_t threads[num]; data thread_args[num]; void* resulT; static Float32 frames [FR_MAX]; Float32 wait_interval; int bytesToCopy; Float32 floatmax; while(on){ wait_interval = FACT*(gen.frames)/(gen.sampleRate); Float32 damp = 1./(Float32)(gen.frames); bytesToCopy = gen.frames*sizeof(Float32); memset(frames, 0, FR_MAX*sizeof(Float32)); availableBytes = 0; fbuffW = (Float32**)calloc(num + 1, sizeof(Float32*)); for (int i=0; i<num; ++i) { fbuffW[i] = (Float32*)calloc(gen.frames, sizeof(Float32)); thread_args[i].tid = i; thread_args[i].cpuCount = num; rc = pthread_create(&threads[i], NULL, producerTN, (void *) &thread_args[i]); } for (int i=0; i<num; ++i) rc = pthread_join(threads[i], &resulT); for(UInt32 samp = 0; samp < gen.frames; samp++) for(int i = 0; i < num; i++) frames[samp] += fbuffW[i][samp]; //code for managing producer state and GUI updates { ... } float *head = TPCircularBufferHead(&(gen.buffer), &availableBytes); memcpy(head,(const void*)frames,MIN(bytesToCopy, availableBytes));//copies frames to head TPCircularBufferProduce(&(gen.buffer),MIN(bytesToCopy,availableBytes)); dispatch_semaphore_wait(mSemaphore, dispatch_time(DISPATCH_TIME_NOW, wait_interval * NSEC_PER_SEC)); if(gen.state == stopped){gen.state = idle; on = false;} for(int i = 0; i <= num; i++) free(fbuffW[i]); free(fbuffW); } return; } 

Un solo hilo productor puede tener este aspecto:

 void *producerT (void *TN) { Float32 samples[FR_MAX]; data threadData; threadData = *((data *)TN); int tid = threadData.tid; int step = threadData.cpuCount; int *ret = calloc(1,sizeof(int)); do_something(tid, step, &samples); { … } return (void*)ret; } 

Aquí está la callback de procesamiento (subproceso de consumidor en tiempo real CoreAudio):

 static OSStatus audioRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { MyAudioController *THIS = (MyAudioController *)inRefCon; // An event happens in the render thread- signal whoever is waiting if (THIS->state == active) dispatch_semaphore_signal(mSemaphore); // Mono audio rendering: we only need one target buffer const int channel = 0; Float32* targetBuffer = (Float32 *)ioData->mBuffers[channel].mData; memset(targetBuffer,0,inNumberFrames*sizeof(Float32)); // Pull samples from circular buffer int32_t availableBytes; Float32 *buffer = TPCircularBufferTail(&THIS->buffer, &availableBytes); //copy circularBuffer content to target buffer int bytesToCopy = ioData->mBuffers[channel].mDataByteSize; memcpy(targetBuffer, buffer, MIN(bytesToCopy, availableBytes)); { … }; TPCircularBufferConsume(&THIS->buffer, availableBytes); THIS->frames = inNumberFrames; return noErr; } 

Grand Central Dispatch ya se encarga de las operaciones de envío a múltiples núcleos y subprocesos de procesadores. En el procesamiento o procesamiento de audio en tiempo real típico, uno nunca necesita esperar en una señal o semáforo, ya que la tasa de consumo del búfer circular es muy predecible y se desplaza muy lentamente con el tiempo. La API AVAudioSession (si está disponible) y la unidad de audio y la callback le permiten establecer y determinar el tamaño del búfer de callback y, por lo tanto, la velocidad máxima a la que puede cambiar el búfer circular. Por lo tanto, puede enviar todas las operaciones de procesamiento en un temporizador, procesar el número exacto necesario por período de temporizador y dejar que el tamaño y el estado del búfer compensen cualquier fluctuación en el tiempo de envío de hilos.

En reproducciones de audio extremadamente largas, es posible que desee medir la desviación entre las operaciones del temporizador y el consumo de audio en tiempo real (frecuencia de muestreo) y ajustar la cantidad de muestras procesadas o el desplazamiento del temporizador.