2015-04-03 9 views
5

ich die neue AVAudioEngine in iOS 8.completionHandler von AVAudioPlayerNode.scheduleFile() zu früh genannt

Es ist wie die completionHandler von player.scheduleFile schaut zu verwenden versuche() aufgerufen wird vor die Tondatei hat fertig spielen.

Ich verwende eine Sounddatei mit einer Länge von 5s - und die println() -Message erscheint etwa 1 Sekunde vor dem Ende des Sounds.

Mache ich etwas falsch oder missverstehe ich die Idee eines AbschlussesHandler?

Danke! Hier


ist ein Code:

class SoundHandler { 
    let engine:AVAudioEngine 
    let player:AVAudioPlayerNode 
    let mainMixer:AVAudioMixerNode 

    init() { 
     engine = AVAudioEngine() 
     player = AVAudioPlayerNode() 
     engine.attachNode(player) 
     mainMixer = engine.mainMixerNode 

     var error:NSError? 
     if !engine.startAndReturnError(&error) { 
      if let e = error { 
       println("error \(e.localizedDescription)") 
      } 
     } 

     engine.connect(player, to: mainMixer, format: mainMixer.outputFormatForBus(0)) 
    } 

    func playSound() { 
     var soundUrl = NSBundle.mainBundle().URLForResource("Test", withExtension: "m4a") 
     var soundFile = AVAudioFile(forReading: soundUrl, error: nil) 

     player.scheduleFile(soundFile, atTime: nil, completionHandler: { println("Finished!") }) 

     player.play() 
    } 
} 

Antwort

6

Das scheint wie ein Fehler, sollten wir eine Radar-Einreichung einreichen! http://bugreport.apple.com

In der Zwischenzeit habe ich festgestellt, wenn Sie stattdessen scheduleBuffer:atTime:options:completionHandler: verwenden, wird der Rückruf wie erwartet (nach der Wiedergabe beendet) ausgelöst.

Beispielcode:

AVAudioFile *file = [[AVAudioFile alloc] initForReading:_fileURL commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:nil]; 
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat frameCapacity:(AVAudioFrameCount)file.length]; 
[file readIntoBuffer:buffer error:&error]; 

[_player scheduleBuffer:buffer atTime:nil options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{ 
    // reminder: we're not on the main thread in here 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     NSLog(@"done playing, as expected!"); 
    }); 
}]; 
+0

Works als Behelfslösung. Vielen Dank! – Oliver

+0

Schöne Workaround. –

+0

Liebe es. Klappt wunderbar! –

6

ich das gleiche Verhalten zu sehen.

Aus meiner Erfahrung glaube ich, dass der Rückruf aufgerufen wird, sobald der Puffer/Segment/Datei "geplant" wurde, nicht wenn es fertig ist zu spielen.

Obwohl die Dokumentation explizit besagt: "aufgerufen, nachdem der Puffer vollständig abgespielt hat oder der Player gestoppt ist. Kann Null sein."

Also ich denke, es ist entweder ein Fehler oder eine falsche Dokumentation. Keine Ahnung, welche

4

Sie können jederzeit die Zukunft Zeit berechnen, wenn Audio-Wiedergabe wird abgeschlossen, mit AVAudioTime. Das aktuelle Verhalten ist nützlich, da es die Planung zusätzlicher Puffer/Segmente/Dateien unterstützt, die vom Rückruf abgespielt werden, bevor das Ende des aktuellen Puffers/Segments/der Datei beendet wird, wodurch eine Lücke in der Audiowiedergabe vermieden wird. Dadurch können Sie einen einfachen Loop-Player ohne viel Arbeit erstellen. Hier ein Beispiel:

class Latch { 
    var value : Bool = true 
} 

func loopWholeFile(file : AVAudioFile, player : AVAudioPlayerNode) -> Latch { 
    let looping = Latch() 
    let frames = file.length 

    let sampleRate = file.processingFormat.sampleRate 
    var segmentTime : AVAudioFramePosition = 0 
    var segmentCompletion : AVAudioNodeCompletionHandler! 
    segmentCompletion = { 
     if looping.value { 
      segmentTime += frames 
      player.scheduleFile(file, atTime: AVAudioTime(sampleTime: segmentTime, atRate: sampleRate), completionHandler: segmentCompletion) 
     } 
    } 
    player.scheduleFile(file, atTime: AVAudioTime(sampleTime: segmentTime, atRate: sampleRate), completionHandler: segmentCompletion) 
    segmentCompletion() 
    player.play() 

    return looping 
} 

Der obige Code plant die gesamte Datei zweimal vor player.play() aufrufen. Wenn jedes Segment kurz vor dem Abschluss steht, plant es in Zukunft eine weitere ganze Datei, um Lücken in der Wiedergabe zu vermeiden. Zum Stoppen der Looping, Sie den Rückgabewert verwenden, ein Latch, wie folgt aus:

let looping = loopWholeFile(file, player) 
sleep(1000) 
looping.value = false 
player.stop() 
0

Ja, es etwas genannt wird erhalten, bevor die Datei (oder Puffer) abgeschlossen. Wenn Sie [myNode stop] im Completion-Handler aufrufen, wird die Datei (oder der Puffer) nicht vollständig abgeschlossen. Wenn Sie jedoch [myEngine stop] aufrufen, wird die Datei (oder der Puffer) bis zum Ende abgeschlossen.

1

Mein Fehlerbericht wurde geschlossen, da "wie vorgesehen funktioniert", aber Apple hat mich auf neue Variationen des scheduleFile hingewiesen, Methoden scheduleSegment und scheduleBuffer in iOS 11.Diese fügen Sie ein completionCallbackType Argument, die Sie verwenden können, um festzulegen, dass Sie den Abschluss Rückruf wollen, wenn die Wiedergabe abgeschlossen ist:

[self.audioUnitPlayer 
      scheduleSegment:self.audioUnitFile 
      startingFrame:sampleTime 
      frameCount:(int)sampleLength 
      atTime:0 
      completionCallbackType:AVAudioPlayerNodeCompletionDataPlayedBack 
      completionHandler:^(AVAudioPlayerNodeCompletionCallbackType callbackType) { 
    // do something here 
}]; 

Die documentation sagt nichts darüber, wie das funktioniert, aber ich es getestet und es funktioniert für mich.

Ich habe für iOS diese Abhilfe wurde unter Verwendung von 8-10:

- (void)playRecording { 
    [self.audioUnitPlayer scheduleSegment:self.audioUnitFile startingFrame:sampleTime frameCount:(int)sampleLength atTime:0 completionHandler:^() { 
     float totalTime = [self recordingDuration]; 
     float elapsedTime = [self recordingCurrentTime]; 
     float remainingTime = totalTime - elapsedTime; 
     [self performSelector:@selector(doSomethingHere) withObject:nil afterDelay:remainingTime]; 
    }]; 
} 

- (float)recordingDuration { 
    float duration = duration = self.audioUnitFile.length/self.audioUnitFile.processingFormat.sampleRate; 
    if (isnan(duration)) { 
     duration = 0; 
    } 
    return duration; 
} 

- (float)recordingCurrentTime { 
    AVAudioTime *nodeTime = self.audioUnitPlayer.lastRenderTime; 
    AVAudioTime *playerTime = [self.audioUnitPlayer playerTimeForNodeTime:nodeTime]; 
    AVAudioFramePosition sampleTime = playerTime.sampleTime; 
    if (sampleTime == 0) { return self.audioUnitLastKnownTime; } // this happens when the player isn't playing 
    sampleTime += self.audioUnitStartingFrame; // if we trimmed from the start, or changed the location with the location slider, the time before that point won't be included in the player time, so we have to track it ourselves and add it here 
    float time = sampleTime/self.audioUnitFile.processingFormat.sampleRate; 
    self.audioUnitLastKnownTime = time; 
    return time; 
} 
0
// audioFile here is our original audio 

audioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: { 
     print("scheduleFile Complete") 

     var delayInSeconds: Double = 0 

     if let lastRenderTime = self.audioPlayerNode.lastRenderTime, let playerTime = self.audioPlayerNode.playerTime(forNodeTime: lastRenderTime) { 

      if let rate = rate { 
       delayInSeconds = Double(audioFile.length - playerTime.sampleTime)/Double(audioFile.processingFormat.sampleRate)/Double(rate!) 
      } else { 
       delayInSeconds = Double(audioFile.length - playerTime.sampleTime)/Double(audioFile.processingFormat.sampleRate) 
      } 
     } 

     // schedule a stop timer for when audio finishes playing 
     DispatchTime.executeAfter(seconds: delayInSeconds) { 
      audioEngine.mainMixerNode.removeTap(onBus: 0) 
      // Playback has completed 
     } 

    })