para - ¿Cómo programar un secuenciador de audio preciso en tiempo real en el iPhone?
garageband para windows (9)
Al medir el tiempo transcurrido para la parte "Hacer algo de trabajo" en el ciclo y restar esta duración del siguiente Tiempo, mejora en gran medida 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
}
}
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.
En mi último intento probé el motor de sonido CocosDenshion que usa openAL y permite cargar sonidos en múltiples buffers y luego reproducirlos cuando sea necesario. Aquí está el código básico:
en eso:
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 creo el motor de sonido, cargo algunos sonidos en diferentes buffers y luego establezco el ciclo del 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 y funciona como debería, PERO el momento es inestable. Tan pronto como sucede algo más (ig dibujando en una vista) desaparece.
Como entiendo 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. Ig "idrum" tiene un ritmo estable perfecto incluso en 180 bpm cuando se realiza el zoom y el dibujo. Entonces debe haber una solución.
¿Alguien tiene alguna idea?
Gracias por cualquier ayuda de antemano!
Atentamente,
Walchy
Gracias por tu respuesta. Me llevó un paso más allá, pero desafortunadamente no al objetivo. Aquí esta 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 tiempo y creo un nuevo tema.
- (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 ciclo de secuencia, configuro la prioridad del hilo lo más alto posible y entro en un ciclo 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 todavía es inestable si sucede algo más, especialmente cosas de GUI.
¿Hay alguna forma de obtener respuestas en tiempo real con NSThread en el iPhone?
Atentamente,
Walchy
Estoy haciendo algo similar con 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 proyecto de ejemplo que usa io remoto se puede encontrar here eche un vistazo al argumento de devolución de llamada enTimeStamp.
EDITAR: Ejemplo de funcionamiento de este enfoque (y en la tienda de aplicaciones, se puede encontrar here )
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 me ha resultado muy mach_absolute_time
usar mach_absolute_time
.
Puedes leer un poco sobre lo que hace here pero a falta de eso es que devuelve el tiempo con una 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
struct 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 quisiéramos marcar cada 16ma nota, podrías hacer algo como esto:
uint64_t interval = (1000 * 1000 * 1000) / 16;
uint64_t nextTime = currentTime + interval;
En este punto, currentTime
contendría una cierta cantidad de nanosegundos, y usted querría que marque cada vez interval
pasaban los nanosegundos, que almacenamos en nextTime
. Luego puede configurar un ciclo 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 de mach_timebase_info
es un poco confuso, pero una vez que lo tienes 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. Podrías poner todo el código anterior en su propio método llamado run
, y comenzarlo con algo como:
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
Todo el código que ve aquí es una simplificación de un proyecto de código abierto, puede verlo y ejecutarlo usted mismo here , si eso es de alguna ayuda. Aclamaciones.
NSTimer no tiene absolutamente ninguna garantía sobre cuándo 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.
Opté por utilizar una AudioUnit de RemoteIO y un hilo de fondo que llena los búferes de swing (un búfer para lectura, uno para escritura que luego se intercambia) utilizando 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.
Aclamaciones,
Ben
Pensé que un mejor enfoque para la gestión del tiempo sería tener una configuración de bpm (120, por ejemplo) y salir 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 nos fijamos en cualquier aplicación de secuencia, todos 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 de ninguna manera, pero creo que este enfoque le ahorrará muchos dolores de cabeza en el futuro.
Realmente, la manera más precisa de abordar el tiempo es contar muestras de audio y hacer lo que sea necesario cuando haya pasado una cierta cantidad de muestras. 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.
Si construir su secuencia antes de tiempo no es una limitación, puede obtener un tiempo preciso usando una composición de AVMutable. Esto reproduciría 4 sonidos espaciados uniformemente durante 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];
Crédito original para esta solución: http://forum.theamazingaudioengine.com/discussion/638#Item_5
Y más detalles: sincronización precisa con AVMutableComposition
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.
Solo contar los búferes de devolución de llamada de 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.