2009-05-25 20 views
22

Quiero programar un secuenciador de audio simple en el iphone, pero no puedo obtener un tiempo preciso. Los últimos días probé todas las técnicas de audio posibles en el iPhone, comenzando desde AudioServicesPlaySystemSound y AVAudioPlayer y OpenAL hasta AudioQueues.¿Cómo programar un secuenciador de audio preciso en tiempo real en el iPhone?

En mi último intento probé el motor de sonido CocosDenshion que usa openAL y permite cargar sonidos en múltiples búferes y luego reproducirlos cuando sea necesario. Aquí está el código básico:

init:

int channelGroups[1]; 
channelGroups[0] = 8; 
soundEngine = [[CDSoundEngine alloc] init:channelGroups channelGroupTotal:1]; 

int i=0; 
for(NSString *soundName in [NSArray arrayWithObjects:@"base1", @"snare1", @"hihat1", @"dit", @"snare", nil]) 
{ 
    [soundEngine loadBuffer:i fileName:soundName fileType:@"wav"]; 
    i++; 
} 

[NSTimer scheduledTimerWithTimeInterval:0.14 target:self selector:@selector(drumLoop:) userInfo:nil repeats:YES]; 

En la inicialización se crea el motor de sonido, cargue algunos sonidos a diferentes tampones y luego establecer el bucle secuenciador con NSTimer.

bucle de audio:

- (void)drumLoop:(NSTimer *)timer 
{ 
for(int track=0; track<4; track++) 
{ 
    unsigned char note=pattern[track][step]; 
    if(note) 
     [soundEngine playSound:note-1 channelGroupId:0 pitch:1.0f pan:.5 gain:1.0 loop:NO]; 
} 

if(++step>=16) 
    step=0; 

} 

Eso es todo y funciona como debería, pero el tiempo es inestable e inestable. Tan pronto como sucede algo más (por ejemplo, dibujar en una vista) se desincroniza.

Según tengo entendido, el motor de sonido y openAL los búferes están cargados (en el código de inicio) y luego están listos para comenzar inmediatamente con alSourcePlay(source); - ¿entonces el problema puede ser con NSTimer?

Ahora hay docenas de aplicaciones de secuenciador de sonido en la tienda de aplicaciones y tienen un horario preciso. Yo G. "idrum" tiene un ritmo estable perfecto incluso en 180 bpm cuando se hace zoom y dibujo. Entonces debe haber una solución.

¿Alguien tiene alguna idea?

¡Gracias por cualquier ayuda con anticipación!

Saludos,

Walchy


Gracias por su respuesta. Me llevó un paso más allá, pero desafortunadamente no al objetivo. Esto es lo que hice:

nextBeat=[[NSDate alloc] initWithTimeIntervalSinceNow:0.1]; 
[NSThread detachNewThreadSelector:@selector(drumLoop:) toTarget:self withObject:nil]; 

En la inicialización almaceno el tiempo para el siguiente latido y crear un nuevo hilo.

- (void)drumLoop:(id)info 
{ 
    [NSThread setThreadPriority:1.0]; 

    while(1) 
    { 
     for(int track=0; track<4; track++) 
     { 
      unsigned char note=pattern[track][step]; 
      if(note) 
       [soundEngine playSound:note-1 channelGroupId:0 pitch:1.0f pan:.5 gain:1.0 loop:NO]; 
     } 

     if(++step>=16) 
      step=0;  

     NSDate *newNextBeat=[[NSDate alloc] initWithTimeInterval:0.1 sinceDate:nextBeat]; 
     [nextBeat release]; 
     nextBeat=newNextBeat; 
     [NSThread sleepUntilDate:nextBeat]; 
    } 
} 

En el bucle de secuencia I establece la prioridad del hilo lo más alto posible y entra en un bucle infinito. Después de reproducir los sonidos, calculo el siguiente tiempo absoluto para el siguiente tiempo y envío el hilo a dormir hasta esta vez.

De nuevo, esto funciona y funciona de manera más estable que mis intentos sin NSThread, pero sigue siendo inestable si sucede algo más, especialmente cosas de GUI.

¿Hay alguna manera de obtener respuestas en tiempo real con NSThread en el iPhone?

Saludos,

Walchy

+0

Creo @Walchy está fuera para el almuerzo ... Wonder cuales forma en que finalmente eligió hacerlo? –

+0

Lamentablemente, no puedo publicar código de muestra, pero para la aplicación Reproducción en la tienda de aplicaciones, utilizamos una devolución de llamada para proporcionar los datos de audio que se leen de los archivos. Cada archivo tiene las mismas características de audio, por lo que para proporcionar un tiempo preciso, simplemente saltamos a ese punto de muestra en los datos del archivo de audio.Es súper preciso (descarga la aplicación y obtén el juego gratis del día). Podemos crear secciones de bucle y saltar a secciones en el archivo, todo mientras se reproducen más de 20 pistas a la vez (todavía no se ha alcanzado un límite de seguimiento). –

Respuesta

8

NSTimer tiene absolutamente ninguna garantía sobre cuando se dispara.Se programa para un tiempo de fuego en el runloop, y cuando el runloop se acerca a los temporizadores, ve si alguno de los temporizadores está vencido. Si es así, ejecuta sus selectores. Excelente para una amplia variedad de tareas; inútil para este.

El primer paso aquí es que necesita mover el procesamiento de audio a su propio hilo y salir del hilo de la interfaz de usuario. Para el tiempo, puedes construir tu propio motor de sincronización usando enfoques C normales, pero comenzaría mirando CAAnimation y especialmente CAMediaTiming.

Tenga en cuenta que hay muchas cosas en Cocoa que están diseñadas solo para ejecutarse en el hilo principal. Por ejemplo, no haga ningún trabajo de IU en un hilo de fondo. En general, lea los documentos con cuidado para ver lo que dicen sobre la seguridad del hilo. Pero en general, si no hay mucha comunicación entre los hilos (que no debería haber en la mayoría de los casos IMO), los hilos son bastante fáciles en Cocoa. Mire NSThread.

6

estoy haciendo algo similar con la salida remoteIO. No confío en NSTimer. utilizo la marca de tiempo proporcionada en la devolución de llamada de render para calcular todos mis tiempos. No sé cuán precisa es la tasa de hz del iPhone, pero estoy seguro de que está muy cerca de 44100hz, así que solo calculo cuando debería cargar el siguiente tiempo basado en el número de muestra actual.

un ejemplo de proyecto que utiliza io remoto se puede encontrar here eche un vistazo al argumento de devolución de llamada enTimeStamp.

EDIT: Ejemplo de este enfoque de trabajo (y en la tienda de aplicaciones, se puede encontrar here)

+0

, por supuesto, necesitará un bpm, pero para programar su próxima reproducción de muestra, tendrá que encontrar las muestras hasta el siguiente tiempo (esto será definido por sus bpm) y usar la marca de tiempo actual para hacer esto. –

+0

Muchas gracias por las publicaciones. – griotspeak

+0

¿Qué parte de la estructura AudioTimeStamp usas para calcular el tiempo? He estado usando mSampleTime. ¿Alguna opinión sobre cuál es mejor usar y por qué? –

0

pensé un mejor enfoque para la gestión del tiempo sería tener un entorno lpm (120, por ejemplo), y vete de eso en su lugar. Las mediciones de minutos y segundos son casi inútiles al escribir/hacer aplicaciones de música/música.

Si miras cualquier aplicación de secuencia, todas pasan por tiempos en lugar de tiempo. En el lado opuesto, si observas un editor de formas de onda, usa minutos y segundos.

No estoy seguro de la mejor manera de implementar este código por ningún medio, pero creo que este enfoque le ahorrará muchos dolores de cabeza en el futuro.

+0

¿Cómo crees que vas a medir esto? 'NSTimer' sufre de Jitter, al igual que cualquier otro mecanismo que dependa de un hilo programado .. Esta es la razón por la cual la única forma de que funcione es cronometrando contra el reloj de muestra de salida. – marko

4

Opté por utilizar una AudioUnit de RemoteIO y una cadena de fondo que llena los búferes de columpios (un búfer para lectura, uno para escritura que luego se intercambia) usando la API de AudioFileServices. Los búferes se procesan y mezclan en el subproceso AudioUnit. El subproceso AudioUnit señala el subproceso bgnd cuando debería comenzar a cargar el siguiente buffer de oscilación. Todo el procesamiento se realizó en C y se utilizó la API posix thread. Todo el material de UI estaba en ObjC.

IMO, el enfoque AudioUnit/AudioFileServices ofrece el mayor grado de flexibilidad y control.

Saludos,

Ben

1

realmente la forma más precisa para acercarse tiempo lo es para contar las muestras de audio y hacer lo que hay que hacer cuando un cierto número de muestras ha pasado. La frecuencia de muestreo de salida es la base para todo lo relacionado con el sonido de todos modos, así que este es el reloj maestro.

No tiene que verificar cada muestra, hacer esto cada par de mseg será suficiente.

3

Has tenido algunas buenas respuestas aquí, pero pensé que ofrecería algún código para una solución que funcionaba para mí.Cuando comencé a investigar esto, en realidad busqué la forma de ejecutar los bucles en los juegos y encontré una buena solución que ha sido muy eficiente para mí al usar mach_absolute_time.

Puede leer un poco sobre lo que hace here pero a falta de eso es que devuelve tiempo con precisión de nanosegundos. Sin embargo, el número que devuelve no es bastante tiempo, varía con la CPU que tiene, por lo que primero debe crear una estructura mach_timebase_info_data_t y luego usarla para normalizar la hora.

// Gives a numerator and denominator that you can apply to mach_absolute_time to 
// get the actual nanoseconds 
mach_timebase_info_data_t info; 
mach_timebase_info(&info); 

uint64_t currentTime = mach_absolute_time(); 

currentTime *= info.numer; 
currentTime /= info.denom; 

Y si queríamos que marque cada nota 16a, se podría hacer algo como esto:

uint64_t interval = (1000 * 1000 * 1000)/16; 
uint64_t nextTime = currentTime + interval; 

En este punto, currentTime contendría un número de nanosegundos, y que te gustaría que para marcar cada vez que pasó interval nanosegundos, que almacenamos en nextTime. A continuación, puede configurar un bucle while, algo como esto:

while (_running) { 
    if (currentTime >= nextTime) { 
     // Do some work, play the sound files or whatever you like 
     nextTime += interval; 
    } 

    currentTime = mach_absolute_time(); 
    currentTime *= info.numer; 
    currentTime /= info.denom; 
} 

El material mach_timebase_info es un poco confuso, pero una vez que llegue allí, funciona muy bien. Ha sido extremadamente eficiente para mis aplicaciones. También vale la pena señalar que no querrás ejecutar esto en el hilo principal, por lo que distribuirlo a su propio hilo es prudente. Se puede poner todo el código anterior en su propio método llamado run, y empezar con algo como:

[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; 

Todo el código que ves aquí es una simplificación de un proyecto que abra de fuentes, se puede ver y ejecútelo usted mismo here, si eso sirve de ayuda. Aclamaciones.

+0

Tenga en cuenta que el reloj del sistema (por ejemplo, devuelto por 'mach_absolute_time') no es necesariamente sincrónico con el reloj de audio en el que las muestras se sincronizan con el códec. El uso del contador de muestras en la devolución de llamada de audio para generar tiempos de secuenciador es la única manera confiable. – marko

+0

Sí, he descubierto algunas cosas que están mal con este enfoque, incluido el hecho de que se ve afectado por las actualizaciones de reloj de los servidores de la zona horaria, etc. ¿Tiene un enlace/referencia para conectar el contador de muestras en el render de audio? ¿llamar de vuelta? – pwightman

+0

En casi todos los manejadores de procesamiento de audio (incluido CoreAudio) se le da el conteo de muestras, o puede convertirlo de los parámetros de tiempo que proporciona. La consecuencia de esto es que necesita dar servicio a una cola de eventos y procesar los elementos vencidos (y decidir qué hacer con los que ya están en el pasado) en el controlador de renderizado. El SDK de VST tiene un ejemplo que le gustaría ver. Es fundamentalmente muy similar a AU. – marko

1

Una cosa adicional que puede mejorar la capacidad de respuesta en tiempo real es establecer kAudioSessionProperty_PreferredHardwareIOBufferDuration de la sesión de audio en unos pocos milisegundos (como 0.005 segundos) antes de activar su sesión de audio. Esto hará que RemoteIO solicite búferes de devolución de llamada más cortos con más frecuencia (en un hilo en tiempo real). No tome mucho tiempo en estas devoluciones de audio en tiempo real, o matará el hilo de audio y todo el audio de su aplicación.

Simplemente contando los búfers de devolución de llamada RemoteIO más cortos es del orden de 10 veces más preciso y con menor latencia que con un NSTimer. Y contar muestras dentro de un búfer de devolución de llamada de audio para posicionar el inicio de su mezcla de sonido le dará un tiempo relativo de milisegundos.

1

Al medir el tiempo transcurrido para la parte de "hacer un trabajo" en el bucle y restando esta duración de la nextTime mejora considerablemente la precisión:

while (loop == YES) 
{ 
    timerInterval = adjustedTimerInterval ; 

    startTime = CFAbsoluteTimeGetCurrent() ; 

    if (delegate != nil) 
    { 
     [delegate timerFired] ; // do some work 
    } 

    endTime = CFAbsoluteTimeGetCurrent() ; 

    diffTime = endTime - startTime ; // measure how long the call took. This result has to be subtracted from the interval! 

    endTime = CFAbsoluteTimeGetCurrent() + timerInterval-diffTime ; 

    while (CFAbsoluteTimeGetCurrent() < endTime) 
    { 
     // wait until the waiting interval has elapsed 
    } 
} 
1

Si la construcción de su secuencia antes de tiempo no es una limitación, puede obtener el tiempo preciso usando una composición de AVMutable. Esto jugar 4 sonidos uniformemente espaciadas durar 1 segundo:

// setup your composition 

AVMutableComposition *composition = [[AVMutableComposition alloc] init]; 
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey : @YES}; 

for (NSInteger i = 0; i < 4; i++) 
{ 
    AVMutableCompositionTrack* track = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; 
    NSURL *url = [[NSBundle mainBundle] URLForResource:[NSString stringWithFormat:@"sound_file_%i", i] withExtension:@"caf"]; 
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; 
    AVAssetTrack *assetTrack = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; 
    CMTimeRange timeRange = [assetTrack timeRange]; 

    Float64 t = i * 1.0; 
    NSError *error; 
    BOOL success = [track insertTimeRange:timeRange ofTrack:assetTrack atTime:CMTimeMake(t, 4) error:&error]; 
    NSAssert(success && !error, @"error creating composition"); 
} 

AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:composition]; 
self.avPlayer = [[AVPlayer alloc] initWithPlayerItem:playerItem]; 

// later when you want to play 

[self.avPlayer seekToTime:kCMTimeZero]; 
[self.avPlayer play]; 

de crédito original para esta solución: http://forum.theamazingaudioengine.com/discussion/638#Item_5

Y más detalle: precise timing with AVMutableComposition

Cuestiones relacionadas