2015-09-18 12 views
10

Sto creando un metronomo come parte di un'app più grande e ho alcuni file wav molto brevi da utilizzare come singoli suoni. Vorrei utilizzare AVAudioEngine perché NSTimer ha notevoli problemi di latenza e Core Audio sembra piuttosto scoraggiante da implementare in Swift. Sto tentando quanto segue, ma al momento non sono in grado di implementare i primi 3 passaggi e mi chiedo se c'è un modo migliore.Uso di AVAudioEngine per programmare i suoni per il metronomo a bassa latenza

codice di struttura:

  1. creare un array di URL di file in base alle impostazioni correnti del metronomo (numero di battiti al bar e suddivisioni per battuta; file di A per il beat, il file B per le suddivisioni)
  2. livello di programmazione creare un file wav con il numero appropriato di fotogrammi di silenzio, in base al tempo e la lunghezza delle file, e inserirlo nella matrice tra ciascuno dei suoni
  3. leggere quei file in un unico AudioBuffer o AudioBufferList
  4. 012.

Finora sono stato in grado di giocare un buffer loop (punto 4) di un singolo file audio, ma non sono stato in grado di costruire un buffer da un array di file o di creare il silenzio di programmazione, né ho trovato qualche risposta su StackOverflow che risolve questo problema. Quindi immagino che questo non sia l'approccio migliore.

La mia domanda è: È possibile pianificare una sequenza di suoni con bassa latenza utilizzando AVAudioEngine e quindi ripetere quella sequenza? In caso contrario, quale framework/approccio è più adatto per la programmazione dei suoni durante la codifica in Swift?

+0

Non sono sicuro che questo aiuti, ma provare [TheAmazingAudioEngine] (https://github.com/TheAmazingAudioEngine/TheAmazingAudioEngine). È scritto nell'obiettivo c, ma può essere usato come framework in swift –

+0

Ho guardato brevemente TAAE e potrebbe essere l'opzione migliore, anche se spero che ci sia un approccio più nativo. – blwinters

risposta

2

Sono riuscito a creare un buffer contenente il suono dal file e il silenzio della lunghezza richiesta. Spero che questo vi aiuterà:

// audioFile here – an instance of AVAudioFile initialized with wav-file 
func tickBuffer(forBpm bpm: Int) -> AVAudioPCMBuffer { 
    audioFile.framePosition = 0 // position in file from where to read, required if you're read several times from one AVAudioFile 
    let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60/Double(bpm)) // tick's length for given bpm (sound length + silence length) 
    let buffer = AVAudioPCMBuffer(PCMFormat: audioFile.processingFormat, frameCapacity: periodLength) 
    try! audioFile.readIntoBuffer(buffer) // sorry for forcing try 
    buffer.frameLength = periodLength // key to success. This will append silcence to sound 
    return buffer 
} 

// player – instance of AVAudioPlayerNode within your AVAudioEngine 
func startLoop() { 
    player.stop() 
    let buffer = tickBuffer(forBpm: bpm) 
    player.scheduleBuffer(buffer, atTime: nil, options: .Loops, completionHandler: nil) 
    player.play() 
} 
+0

Ciò è utile, specialmente usando 'buffer.frameLength' per il silenzio, ma non consente ancora un file audio diverso per ogni tipo di battito (cioè il suono A per i downbeats, il suono B per i beat e il suono C per le suddivisioni). Il vero trucco sarebbe quello di creare un buffer contenente un'intera barra di ritmi e suddivisioni e quindi eseguire il ciclo dell'intero buffer/barra. La risposta sopra è perfetta per un metronomo di base o traccia di clic, ma non può supportare un metronomo "reale" che richiede almeno due suoni diversi. – blwinters

+0

Penso che si possa provare a creare un grafico con due istanze di AVAudioPlayerNode (una per ogni tipo di "tick"). Il primo suona come metronomo "base" e l'altro solo su ritmi forti. – 5hrp

+0

Alla fine, questo è l'approccio con cui sono andato e ho ottenuto il singolo scatto funzionante. In futuro, potrò provare più nodi e usare il parametro atTime per creare l'offset necessario. – blwinters

3

Penso che uno dei possibili modi per riprodurre i suoni con il minor errore possibile di tempo è fornire campioni audio direttamente tramite callback. In iOS puoi farlo con AudioUnit.

In questa richiamata è possibile tenere traccia del conteggio dei campioni e sapere a quale campione si è ora. Dal contatore di campioni è possibile passare al valore temporale (utilizzando la frequenza di campionamento) e utilizzarlo per i compiti di alto livello come il metronomo. Se vedi che è il momento di suonare il metronomo, allora inizi a copiare campioni audio da quel suono al buffer.

Questa è una parte teorica senza alcun codice, ma è possibile trovare molti esempi di AudioUnit e la tecnica di callback.

+0

Grazie, dovrò esaminarlo di più. – blwinters

2

di estendere la risposta di 5hrp:

Prendiamo il caso semplice in cui si dispone di due battute, un levare (TONE1) e un battere (Tone2), e li vogliono fuori fase tra loro così l'audio sarà (su, giù, su, giù) per un certo bpm.

Avrete bisogno di due istanze di AVAudioPlayerNode (uno per ogni battito), diamo loro chiamano audioNode1 e audioNode2

La prima battuta si vuole essere in fase, in modo setup come normale:

let buffer = tickBuffer(forBpm: bpm) 
audioNode1player.scheduleBuffer(buffer, atTime: nil, options: .loops, completionHandler: nil) 

quindi per il secondo battito si desidera che sia esattamente fuori fase, o per iniziare a t = bpm/2. per questo è possibile utilizzare un AVAudioTime variabile:

audioTime2 = AVAudioTime(sampleTime: AVAudioFramePosition(AVAudioFrameCount(audioFile2.processingFormat.sampleRate * 60/Double(bpm) * 0.5)), atRate: Double(1)) 

è possibile utilizzare questa variabile nel buffer in questo modo:

audioNode2player.scheduleBuffer(buffer, atTime: audioTime2, options: .loops, completionHandler: nil) 

Ciò giocare sul circuito tuoi due battute, bpm/2 su fase l'una dall'altra!

È facile vedere come generalizzare questo a più battiti, per creare un'intera barra. Non è la soluzione più elegante, però, perché se vuoi dire la 16a nota devi creare 16 nodi.

Problemi correlati