2014-12-05 9 views
5

Ho cercato di utilizzare AVAudioEngine per programmare più file audio per riprodurre in perfetta sincronia, ma ascoltando l'output sembra esserci un leggero ritardo tra i nodi di input. Il motore audio è implementata utilizzando il seguente grafico:AVAudioEngine più AVAudioInputNodes non riproducono in perfetta sincrono

// 
//AVAudioPlayerNode1 --> 
//AVAudioPlayerNode2 --> 
//AVAudioPlayerNode3 --> AVAudioMixerNode --> AVAudioUnitVarispeed ---> AvAudioOutputNode 
//AVAudioPlayerNode4 -->           | 
//AVAudioPlayerNode5 -->          AudioTap 
//  |               
//AVAudioPCMBuffers  
// 

e sto usando il seguente codice per caricare i campioni e li programmare allo stesso tempo:

- (void)scheduleInitialAudioBlock:(SBScheduledAudioBlock *)block { 
    for (int i = 0; i < 5; i++) { 
     NSString *path = [self assetPathForChannel:i trackItem:block.trackItem]; //this fetches the right audio file path to be played 
     AVAudioPCMBuffer *buffer = [self bufferFromFile:path]; 
     [block.buffers addObject:buffer]; 
    } 

    AVAudioTime *time = [[AVAudioTime alloc] initWithSampleTime:0 atRate:1.0]; 
    for (int i = 0; i < 5; i++) { 
     [inputNodes[i] scheduleBuffer:block.buffers[i] 
            atTime:time 
            options:AVAudioPlayerNodeBufferInterrupts 
         completionHandler:nil]; 
    } 
} 

- (AVAudioPCMBuffer *)bufferFromFile:(NSString *)filePath { 
    NSURL *fileURl = [NSURL fileURLWithPath:filePath]; 
    NSError *error; 
    AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:fileURl commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:&error]; 
    if (error) { 
     return nil; 
    } 

    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFile.processingFormat frameCapacity:audioFile.length]; 
    [audioFile readIntoBuffer:buffer frameCount:audioFile.length error:&error]; 

    if (error) { 
     return nil; 
    } 

    return buffer; 
} 

ho notato il problema è solo per i dispositivi, sto provando con un iPhone5s, ma non riesco a capire perché i file audio non siano sincronizzati, ogni aiuto sarebbe molto apprezzato.

** ** RISPOSTA

Abbiamo finito l'ordinamento la questione con il seguente codice:

AVAudioTime *startTime = nil; 

for (AVAudioPlayerNode *node in inputNodes) { 
    if(startTime == nil) { 
     const float kStartDelayTime = 0.1; // sec 
     AVAudioFormat *outputFormat = [node outputFormatForBus:0]; 
     AVAudioFramePosition startSampleTime = node.lastRenderTime.sampleTime + kStartDelayTime * outputFormat.sampleRate; 
     startTime = [AVAudioTime timeWithSampleTime:startSampleTime atRate:outputFormat.sampleRate]; 
    } 

    [node playAtTime:startTime]; 
} 

Questo ha dato ogni AVAudioInputNode abbastanza tempo per caricare i buffer e di tutti i nostri problemi audio sincronizzazione fisse. Spero che questo aiuti gli altri!

+0

Bene, hai più nodi giocatore e su ognuno devi chiama il metodo 'play'.La mia ipotesi è che questa chiamata inizi immediatamente l'elaborazione e il lancio di materiale in audio thread. Poichè ogni 'play' è chiamato in momenti diversi probabilmente sta causando questo ritardo. Se il mio ragionamento è corretto e se è possibile avviare i nodi giocatore in un batch non lo so. In realtà sto utilizzando il ticket di supporto per gli sviluppatori Apple in cui ho fatto una domanda simile. Se avrò una risposta definitiva, tornerò su questa domanda. – Kegluneq

+0

Il problema non è affatto il *** playAt: ***. È esattamente pensato per il lancio in gruppo. Devi solo usare un tempo di ancoraggio concreto. Vedere la mia risposta in basso ... – mramosch

+0

Quindi, nonostante la soluzione in ANSWER il problema rimarrà se un dispositivo richiede più di 0,1 secondi per ottenere il suo atto insieme. Questi sono numeri magici purché non li pianifichino correttamente ... – mramosch

risposta

2

Ho utilizzato il ticket di supporto per sviluppatori Apple per i miei problemi con AVAudioEngine in cui un problema era (è) esattamente uguale al tuo. Ho ottenuto questo codice per provare:

AudioTimeStamp myAudioQueueStartTime = {0}; 
UInt32 theNumberOfSecondsInTheFuture = 5; 
Float64 hostTimeFreq = CAHostTimeBase::GetFrequency(); 
UInt64 startHostTime = CAHostTimeBase::GetCurrentTime()+theNumberOfSecondsInTheFuture*hostTimeFreq; 
myAudioQueueStartTime.mFlags = kAudioTimeStampHostTimeValid; 
myAudioQueueStartTime.mHostTime = startHostTime; 
AVAudioTime *time = [AVAudioTime timeWithAudioTimeStamp:&myAudioQueueStartTime sampleRate:_file.processingFormat.sampleRate]; 

A parte il gioco di pianificazione in Skynet era invece di 5 secondi nel futuro, ancora non sincronizzare due AVAudioPlayerNodes (quando sono passato GetCurrentTime() per un valore arbitrario per gestire sino a gioca i nodi).

Quindi non essere in grado di sincronizzare due o più nodi insieme è un bug (confermato dal supporto Apple). Generalmente, se non devi usare qualcosa che è stato introdotto con AVAudioEngine (e non sai come tradurlo in AUGraph), ti consiglio di usare AUGraph. È un po 'più overhead da implementare, ma tu hai più controllo su di esso.

+0

Abbiamo finito per ordinare il problema con il seguente codice: 'AVAudioTime * startTime = nil; per (nodo AVAudioPlayerNode * in inputNodes) { if (startTime == nil) { const float kStartDelayTime = 0,1; // sec AVAudioFormat * outputFormat = [node outputFormatForBus: 0]; AVAudioFramePosition startSampleTime = node.lastRenderTime.sampleTime + kStartDelayTime * outputFormat.sampleRate; startTime = [AVAudioTime timeWithSampleTime: startSampleTime atRate: outputFormat.sampleRate]; } [node playAtTime: startTime]; } ' –

+0

Invece di riprodurre tutti gli AVInputNode insieme, li ho programmati per 0,1 secondi in futuro, consentendo un tempo sufficiente per caricare i buffer su ciascun canale. Questo risolve il problema. –

+0

Il problema non è affatto il *** playAt: ***. È esattamente pensato per il lancio in gruppo. Devi solo usare un tempo di ancoraggio concreto. Vedere la mia risposta di seguito ... – mramosch

7

Problema:

Beh, il problema è che si recupera la tua player.lastRenderTime in ogni corsa del ciclo for, prima playAt:

Quindi, in effetti ne riceverai uno diverso n ow -time per ogni giocatore!

Il modo in cui si farlo si potrebbe anche iniziare a tutti i giocatori nel ciclo con gioco: o playAtTime: nil !!! Si verificherebbe lo stesso risultato con una perdita di sincronizzazione ...

Per lo stesso motivo il lettore a corto-of-sync in modi diversi su dispositivi diversi, a seconda della velocità della macchina ;-) I suoi ora -Times sono numeri magici casuali - così, don' Supponiamo che funzioneranno sempre se solo capiteranno di lavorare nel tuo scenario. Anche il più piccolo ritardo a causa di un ciclo corsa occupato o CPU buttarti out-of-sync di nuovo ...

Soluzione:

Quello che realmente dovete fare è quello di ottenere ONE istantanea discreta di now = player.lastRenderTime prima del ciclo e utilizzare questa stessa ancora per ottenere un avvio sincronizzato in batch per tutto il lettore.

In questo modo non è nemmeno necessario ritardare l'avvio del lettore. Certo, il sistema ritaglierà alcuni dei fotogrammi principali - (ma ovviamente la stessa quantità per ogni giocatore ;-) - per compensare la differenza tra il tuo ora (che in realtà è già passato e passato) e l'attuale Playtime (che resta ancora da fare in un futuro molto prossimo) ma alla fine ricominciare tutto il lettore esattamente in sincrono come se effettivamente fossi davvero li ha iniziato a ora in passato. Questi fotogrammi ritagliati non sono quasi mai visibili e avrai tranquillità per quanto riguarda la reattività ...

Se ti occorrono questi fotogrammi - a causa di clic uditivi o artefatti all'avvio di file/segmenti/buffer - beh, shift il tuo ora per il futuro avviando il tuo giocatore in ritardo. Ma, naturalmente, si otterrà questo piccolo ritardo dopo aver colpito il pulsante di avvio - anche se naturalmente ancora in perfetta sincronia ...

Conclusione:

Il punto qui è quello di avere un unico riferimento ora -time per tutti i giocatori e per chiamare la playAtTime: ora metodi il più presto possibile dopo la cattura questa ora-riferimento. Maggiore è il divario, maggiore sarà la porzione di trame principali ritagliate, a meno che non sia fornito un ragionevole ritardo di avvio e lo si aggiunga al ora -time, che naturalmente causa una mancanza di risposta in forma di avvio ritardato dopo aver colpito il tuo pulsante Start.

E sempre essere consapevoli del fatto che - qualunque ritardo su qualsiasi dispositivo è prodotto dai meccanismi di buffering audio - è FUNZIONA NON effetto la sincronicità di qualsiasi quantità di giocatore, se fatto in proprio, di cui sopra modo! È NON ritardare l'audio, sia! Solo la finestra che ti consente di ascoltare l'audio verrà aperta in un secondo momento ...


Occorre considerare che:

  • Se si va per il non-ritardato (super-reattiva) opzione di avvio e per qualsiasi motivo per cui si verifica un grande ritardo (tra il cattura di ora e l'inizio effettivo del lettore), sarà clip-off una grande parte di primo piano (fino a circa ~ 300ms/0,3 sec) della vostra audio. Ciò significa che quando avvii il tuo lettore inizierà subito ma non riprenderà dalla posizione che hai messo in pausa di recente ma piuttosto (fino a ~ 300 ms) più avanti nel tuo audio. Quindi la percezione acustica è che il gioco in pausa taglia fuori una parte del tuo audio mentre sei in movimento nonostante tutto sia perfettamente sincronizzato.
  • Come il ritardo di avvio che fornite nel playAtTime : ora + myProvidedDelay chiamata di metodo è un valore costante fisso (che non ottiene regolato in modo dinamico per ospitare ritardo buffering o altra variabile parametri al sistema pesante carico) andando anche per l'opzione ritardata con un tempo di ritardo previsto più piccolo di circa ~ 300ms possono causare un ritaglio delle principali audio-campioni se il dispositivo-dipendente preparazione tempo supera il tempo di ritardo previsto.
  • La quantità massima di ritaglio non aumenta (di progettazione) di questi ~ 300 ms. Per dimostrare basta forzare un ritaglio controllato (esatto per il campione) dei telai principali, ad es. aggiungendo un tempo di ritardo negativo a ora e si percepirà una crescente porzione audio ridotta aumentando questo valore negativo . Ogni valore negativo più grande di ~ 300 ms ottiene rettificato a ~ 300 ms. Quindi un ritardo negativo disponibile di 30 secondi si causare lo stesso comportamento come un valore negativo di 10, 6, 3 o 1 secondi, e naturalmente anche compresi negativi 0,8, 0,5 secondi giù a ~ 0,3

Questo esempio è utile per scopi dimostrativi, ma i valori di ritardo negativo non devono essere utilizzati nel codice di produzione.


ATTENZIONE:

La cosa più importante di tutto in una configurazione multi-giocatore è quello di mantenere il vostro player.pause in sincronia. A giugno 2016 non esiste ancora una strategia di uscita sincronizzata in AVAudioPlayerNode.

Solo un piccolo metodo di look-up o il logout qualcosa alla console in-tra due player.pause chiamate potrebbe costringere quest'ultimo uno da eseguire uno o più frame/campione (s) entro e non oltre il precedente. Quindi il tuo giocatore non si fermerebbe nella stessa posizione relativa nel tempo. E soprattutto - diversi dispositivi comporterebbero un comportamento diverso ...

Se ora li avvii nel modo sopra citato (sincronizzato), queste posizioni del giocatore attuale fuori sincrono della tua ultima pausa otterranno sicuramente forza -sync'ed al tuo nuovo now -posizionamento ad ogni playAtTime: - che essenzialmente significa che stai propagando il campione/i frame persi in futuro ad ogni nuovo avvio del tuo lettore. Questo naturalmente aggiunge a ogni nuovo ciclo di avvio/pausa e allarga il divario. Fatelo cinquanta o cento volte e ottenete già un buon effetto delay senza usare un effetto-unità audio ;-)

Dato che non abbiamo alcun controllo (su questo sistema) su questo fattore, l'unico rimedio è per mettere tutte le chiamate a player.pause diritte una dopo l'altra in una sequenza stretta senza nulla tra loro, come si può vedere negli esempi di seguito. Non gettarli in un ciclo for o qualcosa di simile - questa sarebbe una garanzia per finire fuori sincrono alla prossima pausa/avvio del tuo lettore ...

Se mantenere queste chiamate insieme è un Una soluzione al 100% perfetta o il run-loop sotto un grande carico della CPU potrebbe per caso interferire e forzare-separare le chiamate di pausa l'una dall'altra e causare frame drop - non so - almeno in settimane a fare i conti con l'API di AVAudioNode I non potrebbe in alcun modo forzare il mio multigiocatore per uscire fuori sincrono - tuttavia, non mi sento ancora molto comodo o sicuro con questa soluzione di pausa casuale non sincronizzata, ...


Codice-esempio e alternativa:

Se il motore è già in esecuzione hai un @property lastRenderTime in AVAudioNode - superclasse del lettore - Questo è il tuo biglietto per il 100% del campione-frame sync accurate. ..

AVAudioFormat *outputFormat = [playerA outputFormatForBus:0]; 

const float kStartDelayTime = 0.0; // seconds - in case you wanna delay the start 

AVAudioFramePosition now = playerA.lastRenderTime.sampleTime; 

AVAudioTime *startTime = [AVAudioTime timeWithSampleTime:(now + (kStartDelayTime * outputFormat.sampleRate)) atRate:outputFormat.sampleRate]; 

[playerA playAtTime: startTime]; 
[playerB playAtTime: startTime]; 
[playerC playAtTime: startTime]; 
[playerD playAtTime: startTime]; 

[player... 

Tra l'altro - si può ottenere lo stesso risultato esatto campione-frame 100% con le classi/AVAudioRecorder AVAudioPlayer ...

NSTimeInterval startDelayTime = 0.0; // seconds - in case you wanna delay the start 
NSTimeInterval now = playerA.deviceCurrentTime; 

NSTimeIntervall startTime = now + startDelayTime; 

[playerA playAtTime: startTime]; 
[playerB playAtTime: startTime]; 
[playerC playAtTime: startTime]; 
[playerD playAtTime: startTime]; 

[player... 

Senza startDelayTime il primo 100-200ms di tutti i giocatori avranno tagliato fuori perché il comando di start ha effettivamente il suo tempo per il ciclo di esecuzione, anche se i giocatori hanno già iniziato (o meglio, stato programmato) 100 % in sincronizzazione a ora. Ma con un startDelayTime = 0,25 sei a posto. E non dimenticare mai prepareToPlay i tuoi giocatori in anticipo in modo che al momento dell'avvio non sia necessario eseguire alcun buffering o configurazione aggiuntivi: avviarli ragazzi ;-)

+0

Io uso la tua tecnica di snapshotting (singolo valore lastCurrentTime') per avviare più 'AVAudioPlayerNode's in sincronizzazione. Tuttavia sembra che la riproduzione sia ritardata di circa ~ 250 ms. Qualche idea ? – Vince

+0

Prima di tutto, ottima risposta. Sei ancora attivo su Stack Overflow? Sto avendo difficoltà con questo problema esatto. La tua risposta mi ha portato nella giusta direzione, ma ho bisogno di quei primi ~ 200 ms di audio e stanno ancora tagliando. Potresti dare una mano? – AntiStrike12

+0

Cosa intendi con 'ancora si stanno tagliando ...' - 'ancora' nonostante cosa? Nonostante l'impostazione di kStartDelayTime? – mramosch