AVAudioPlayerNode skips a lot of sounds when stopped and started again - swift

I'm scheduling a buffer periodically using scheduleBuffer function. The completion handler schedules the next sound when current one is done, like so:
func scheduleSounds() {
if !isPlaying { return }
while beatsScheduled < beatsToScheduleAhead {
// Returns AVAudioTime of the next sound, based on sample time
// first sound is always sample time 0, then increments.
let nextTime = getNextTime()
player.scheduleBuffer(
soundBuffer,
at: nextTime,
options: AVAudioPlayerNodeBufferOptions(rawValue: 0),
completionHandler: {
self.queue!.sync {
print("Finished playing a sound!")
scheduleSounds()
}
}
)
beatsScheduled += 1
if !playerStarted {
player.play(at: nil)
playerStarted = true
}
}
}
When I stop and then start the player/engine there is a delay before the sound starts playing and after logging which sounds play when I noticed that there are couple of sounds that the engine does not play. My suspicion is that the player nodes think that the sample time has already passed relative to the player node time. I think I might not be resetting or stopping something properly, here's how I start and stop the sounds:
func start() {
do {
try engine.start()
isPlaying = true
queue!.sync() {
self.scheduleSounds()
}
} catch {
print("\(error)")
}
}
func stop() {
isPlaying = false
player.stop()
engine.stop()
playerStarted = false
}
So the problem is how do I make sure that there are no sounds that AVAudioPlayerNode skips before playing the sound?

Related

Playing 2 sounds in succession ... but before 2nd sound finishes trying to stop the entire 2 sound sequence

Playing 2 sounds in succession ... but before 2nd sound finishes, trying to stop the entire 2 sound sequence.
Note that it does not matter where I choose to stop the sequence - even, e.g., while the 1st sound is playing.
Here are the playSound and stopSound + 1 helper func code snippets:
func playSound(theSoundName: String) {
guard let url = Bundle.main.url(forResource: "audio/" + theSoundName,
withExtension: "mp3") else {
print("sound not found")
return
}
itsSoundPlayer = setupAudioPlayer(theURL: url)
if theSoundName == "roar" {
itsSoundPlayer?.numberOfLoops = -1 // forever
}
itsSoundPlayer?.play()
} // playSound
func stopSound() {
itsSoundPlayer?.stop() // stops whatever is playing via playSound(...)
} // stopSound
func setupAudioPlayer(theURL: URL) -> AVAudioPlayer? {
do {
// Make this App ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let soundPlayer = try AVAudioPlayer(contentsOf: theURL)
return soundPlayer
}
catch let error as NSError {
print("error: \(error.localizedDescription)")
return nil
}
} // setupAudioPlayer
Okay, so the above are the basic building blocks ... now for a specific example of their use:
func attaBoy() {
stopSound() // stop whatever is playing via playSound(...)
playSound(theSoundName: "attaboy") // then play the new sound
// give "attaboy" time to finish before returning to "roar"
let theDelay = Double(2.0)
DispatchQueue.main.asyncAfter(deadline: .now() + theDelay) {
self.playSound(theSoundName: "roar") // replay
}
} // attaBoy
At an undetermined point when either attaboy or roar is playing, I want to call stopSound(). This stoppage could occur while attaboy is playing or while roar is playing.
I've tried to use the above code as is ... but when I try to stop the 2-sound sequence while attaboy is playing, attaboy stops as it should, but roar still plays.
Is there some other approach I should try ?

How to update MPNowPlayingInfoCenter/MPRemoteCommandCenter play/pause controls with AVAudioPlayerNode?

I am making a simple music player app that just plays audio using AVAudioEngine. When I pause the AVAudioPlayerNode, it does not update the play/pause control of the MPNowPlayingInfoCenter/MPRemoteCommandCenter. How do I update it?
I dont want to pause the entire AVAudioEngine and then resume to update the MPNowPlayingInfoCenter/MPRemoteCommandCenter play/pause control since it lags up my UI and there's a delay when playing back audio.
Does anyone have any idea or solution regarding this topic? The documentation is absolutely atrocious and no one knows a goddamn thing about AVAudioEngine. Anyone?
func pause() {
playerNode.pause()
audioEngine.pause()
displayLink?.isPaused = true
DispatchQueue.main.async {
self.musicPlayerControlsManager.isPlaying = false
}
}
func playPlayerNode() {
if !audioEngine.attachedNodes.isEmpty && audioFile != nil {
DispatchQueue.main.async {
self.musicPlayerControlsManager.isPlaying = true
self.displayLink?.isPaused = false
}
do {
try audioEngine.start()
playerNode.play()
} catch {
print(error.localizedDescription)
}
}
}

How to set NowPlaying properties with a AVQueuePlayer in Swift?

I have an AVQueuePlayer that gets songs from a Firebase Storage via their URL and plays them in sequence.
static func playQueue() {
for song in songs {
guard let url = song.url else { return }
lofiSongs.append(AVPlayerItem(url: url))
}
if queuePlayer == nil {
queuePlayer = AVQueuePlayer(items: lofiSongs)
} else {
queuePlayer?.removeAllItems()
lofiSongs.forEach { queuePlayer?.insert($0, after: nil) }
}
queuePlayer?.seek(to: .zero) // In case we added items back in
queuePlayer?.play()
}
And this works great.
I can also make the lock screen controls appear and use the play pause button like this:
private static func setRemoteControlActions() {
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [self] event in
queuePlayer?.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [self] event in
if queuePlayer?.rate == 1.0 {
queuePlayer?.pause()
return .success
}
return .commandFailed
}
}
The problem comes with setting the metadata of the player (name, image, etc).
I know it can be done once by setting MPMediaItemPropertyTitle and MPMediaItemArtwork, but how would I change it when the next track loads?
I'm not sure if my approach works for AVQueueplayer, but for playing live streams with AVPlayer you can "listen" to metadata receiving.
extension ViewController: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
//look for metadata in groups
}
}
I added the AVPlayerItemMetadataOutputPushDelegate via an extension to my ViewController.
I also found this post.
I hope this gives you a lead to a solution. As said I'm not sure how this works with AVQueuePlayer.

How to keep AVMIDIPlayer playing?

I'm trying to use Apple's AVMIDIPlayer object for playing a MIDI file. It seems easy enough in Swift, using the following code:
let midiFile:NSURL = NSURL(fileURLWithPath:"/path/to/midifile.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOf: midiFile as URL, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
midiPlayer?.play {
print("finished playing")
}
And it plays for about 0.05 seconds. I presume I need to frame it in some kind of loop. I've tried a simple solution:
while stillGoing {
midiPlayer?.play {
let stillGoing = false
}
}
which works, but ramps up the CPU massively. Is there a better way?
Further to the first comment, I've tried making a class, and while it doesn't flag any errors, it doesn't work either.
class midiPlayer {
var player: AVMIDIPlayer?
func play(file: String) {
let myURL = URL(string: file)
do {
try self.player = AVMIDIPlayer.init(contentsOf: myURL!, soundBankURL: nil)
self.player?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
self.player?.play()
}
func stop() {
self.player?.stop()
}
}
// main
let myPlayer = midiPlayer()
let midiFile = "/path/to/midifile.mid"
myPlayer.play(file: midiFile)
You were close with your loop. You just need to give the CPU time to go off and do other things instead of constantly checking to see if midiPlayer is finished yet. Add a call to usleep() in your loop. This one checks every tenth of a second:
let midiFile:NSURL = NSURL(fileURLWithPath:"/Users/steve/Desktop/Untitled.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOfURL: midiFile, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
var stillGoing = true
while stillGoing {
midiPlayer?.play {
print("finished playing")
stillGoing = false
}
usleep(100000)
}
You need to ensure that the midiPlayer object exists until it's done playing. If the above code is just in a single function, midiPlayer will be destroyed when the function returns because there are no remaining references to it. Typically you would declare midiPlayer as a property of an object, like a subclassed controller.
Combining Brendan and Steve's answers, the key is sleep or usleep and sticking the play method outside the loop to avoid revving the CPU.
player?.play({return})
while player!.isPlaying {
sleep(1) // or usleep(10000)
}
The original stillGoing value works, but there is also an isPlaying method.
.play needs something between its brackets to avoid hanging forever after completion.
Many thanks.

Radio Streaming AVPlayer latency (delay) is to high swift 3

In my app I play audio live streaming and the delay is very important. I'm using AVPlayer but it takes 5-6 sec to start and I need it 3 sec max of delay. How can I do it to start playing faster and reduce that delay?
Set a small buffer will do the job? how to set it up with AVPlayer?
This is my RadioPlayer class:
import Foundation
import AVFoundation
class RadioPlayer {
static let sharedInstance = RadioPlayer()
private var player = AVPlayer()
private var isPlaying = false
private var language: LanguageDOM?
func play() {
player.play()
isPlaying = true
}
func pause() {
player.pause()
isPlaying = false
}
func toggle() {
if isPlaying == true {
pause()
} else {
play()
}
}
func currentTimePlaying() -> CMTime {
return player.currentTime()
}
func changeLanguage(nlanguage: LanguageDOM){
self.pause()
self.language = nlanguage
player = AVPlayer(url: NSURL(string: nlanguage.url)! as URL)
self.play()
}
func currentlyPlaying() -> Bool {
return isPlaying
}
func currentLanguage() -> LanguageDOM {
return self.language!
}
func currentLanguageId() -> Int {
if self.language == nil {
return -1
}
else {
return language!.id
}
}
}
I'm assuming your network is fast enough to load the necessary buffer for 3 second delays.
What you want to look at is the -prerollAtRate: of AVPlayer. When used properly, it will allow for minimal latency between when you press play, and when you hear the sound. It requires however that part of the song be downloaded already for processing.
As for AVAudioSession, it is not what you're looking for, AVPlayer is the correct class for you.
If AVPlayer isn't fast enough I suggest looking into BASS which is a low level C like audio library built upon AudioUnits framework which allow for precise and fast control over your stream.