2015-04-03 1 views
5

Je suis en train d'utiliser la nouvelle AVAudioEngine dans iOS 8.completionHandler de AVAudioPlayerNode.scheduleFile() est appelée trop tôt

Il ressemble à la completionHandler de player.scheduleFile() est appelée avant le fichier audio a fini de jouer. J'utilise un fichier son d'une longueur de 5s - et le println() -Message apparaît autour de 1 seconde avant la fin du son. Est-ce que je fais quelque chose de mal ou est-ce que je me méprends sur l'idée d'un achèvement de la main?

Merci!


Voici un 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() 
    } 
} 

Répondre

6

Cela semble être un bug, nous devrions déposer une soumission radar! En attendant, comme solution de contournement, j'ai remarqué que si vous utilisez à la place scheduleBuffer:atTime:options:completionHandler: le rappel est déclenché comme prévu (après la fin de la lecture).

code Exemple:

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

Fonctionne comme une solution de contournement. Merci! – Oliver

+0

Belle solution de contournement. –

+0

J'adore ça. Fonctionne comme un charme! –

6

Je vois le même comportement. De mon expérience, je crois que le rappel est appelé une fois que le tampon/segment/fichier a été "programmé", pas quand il a fini de jouer.

Bien que le document indique explicitement: "Appelé après la fin de la lecture de la mémoire tampon ou l'arrêt du lecteur. Donc je pense que c'est un bug ou une documentation incorrecte. Aucune idée qui

4

Vous pouvez toujours calculer le temps futur lorsque la lecture audio complètera, en utilisant AVAudioTime. Le comportement actuel est utile car il prend en charge la planification de tampons/segments/fichiers supplémentaires à lire à partir du rappel avant la fin du tampon/segment/fichier en cours, ce qui évite une lacune dans la lecture audio. Cela vous permet de créer un lecteur de boucle simple sans beaucoup de travail. Voici un exemple:

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 
} 

Le code ci-dessus planifie le fichier entier deux fois avant d'appeler player.play(). Au fur et à mesure que chaque segment se rapproche de la fin, il planifie un autre fichier entier dans le futur, pour éviter les écarts dans la lecture. Pour arrêter le bouclage, utilisez la valeur de retour, un verrou, comme ceci:

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

Oui, il ne s'appelle un peu avant que le fichier (ou tampon) est terminée. Si vous appelez [myNode stop] depuis le gestionnaire de complétion, le fichier (ou le tampon) ne sera pas complètement terminé. Cependant, si vous appelez [myEngine stop], le fichier (ou le tampon) se terminera à la fin

1

Mon rapport de bogue a été fermé car "fonctionne comme prévu", mais Apple m'a indiqué de nouvelles variantes du fichier scheduleFile, Méthodes scheduleSegment et scheduleBuffer dans iOS 11.Ceux-ci ajoutent un argument completionCallbackType que vous pouvez utiliser pour indiquer que vous voulez que le rappel d'achèvement lorsque la lecture est terminée:

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

Le documentation ne dit rien sur la façon dont cela fonctionne, mais je l'ai testé et il fonctionne pour moi.

Je l'ai utilisé cette solution de contournement pour iOS 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 
     } 

    })