Swift background music delay - swift

I'm trying to make background music loop seamlessly while having a part that doesn't loop, but there is a delay when switching the AVAudioPlayers. The delay happens when songA stops and songB starts.
Here is the code.
lazy var songA: AVAudioPlayer? = {
guard let url = Bundle.main.url(forResource: "A", withExtension: "wav") else {
return nil
}
do {
let player = try AVAudioPlayer(contentsOf: url)
let x = player
x.delegate=self
player.numberOfLoops = 0
player.prepareToPlay()
return player
} catch {
return nil
}
}()
lazy var songB: AVAudioPlayer? = {
guard let url = Bundle.main.url(forResource: "B", withExtension: "wav") else {
return nil
}
do {
let player = try AVAudioPlayer(contentsOf: url)
player.numberOfLoops = -1
player.prepareToPlay()
return player
} catch {
return nil
}
}()
func multiTrack(songA: AVAudioPlayer, songB: AVAudioPlayer) {
masterStop()
songA.play()
while songA.isPlaying==true {
if songA.isPlaying==false{
songB.play()
songA.stop()
songA.prepareToPlay()
}
}
songB.play()
songA.stop()
songA.prepareToPlay()
songA.currentTime=0
}

Related

How to play several sound in a row

The goal is to play several sounds one after another (getReady -> nextExercise -> burpees).
The problem is that only the first one is being played
How it should work:
I call playGetReady() from WorkoutTabataViewController
I plays the first sound
After the first sound is finished, automatically "audioPlayerDidFinishPlaying()" is being called
It triggers "playNextSound()" func, which playing next sound
But audioPlayerDidFinishPlaying() is not being called. Or am I missing something and it should work differently?
class AudioPlayerManager: AVAudioPlayerDelegate {
var description: String
static let shared = AudioPlayerManager()
var audioPlayer: AVAudioPlayer?
var workoutVC: WorkoutTabataViewController?
var mainVC: MainTabataViewController?
var currentSound = 0
let urls: [URL]
init() {
self.description = ""
//First sound
let getReady = Bundle.main.path(forResource: "Get ready", ofType: "mp3")!
let urlGetReady = URL(fileURLWithPath: getReady)
//Second sound
let nextExercise = Bundle.main.path(forResource: "Next Exercise", ofType: "mp3")!
let urlNextExercise = URL(fileURLWithPath: nextExercise)
//Third sound
let burpees = Bundle.main.path(forResource: "Burpees", ofType: "mp3")!
let urlBurpees = URL(fileURLWithPath: burpees)
urls = [urlGetReady, urlNextExercise, urlBurpees]
}
func playGetReady() {
do {
audioPlayer = try AVAudioPlayer(contentsOf: urls[currentSound])
audioPlayer?.delegate = self
audioPlayer?.play()
} catch {
print(error)
}
}
func playNextSound() {
currentSound += 1
if currentSound < urls.count {
do {
audioPlayer = try AVAudioPlayer(contentsOf: urls[currentSound])
audioPlayer?.delegate = self
audioPlayer?.play()
} catch {
print(error)
}
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if flag {
playNextSound()
}
}
}
Your audio manager class is not introspectable. Say #objc func audioPlayerDidFinishPlaying or, better, make it an NSObject.

Modify code to play and download mp3 at the same time, not download mp3 then play it in Swift/SwiftUI

I have created this code to play/stop/rewind tracks that will be downloaded at the device and stored in cache. The only problem is that it starts playing only when the track will be fully downloaded. Before that the app freezes completely. It's not a problem with small mp3, but what if it will be 30 minutes audio? I need to modify this code to start audio playing immediately and download the track in the background. Seek help here because there is no guides how to do it with AVAudioPlayer. Thanks.
import Foundation
import AVFoundation
import AVFAudio
final class AudioManager: ObservableObject {
// static let shared = AudioManager()
var player: AVAudioPlayer?
#Published private(set) var isDownloading = false
#Published private(set) var isPlaying: Bool = false {
didSet {
print(isPlaying, "isPlaying")
}
}
#Published private(set) var isLooping: Bool = false
#MainActor func startPlayer(track: String) async {
guard let fileURL = URL(string: track) else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let songName = fileURL.lastPathComponent
var soundData: Data
let tracksFolderUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last!.appendingPathComponent("tracks")
let trackUrl = tracksFolderUrl.appendingPathComponent(songName)
if FileManager.default.fileExists(atPath: trackUrl.path) {
// Load local data if it exists
print("Loading data from \(trackUrl)")
soundData = try Data(contentsOf: trackUrl)
} else {
//… otherwise load from network
isDownloading = true
print("Downloading data from \(fileURL)")
(soundData, _) = try await URLSession.shared.data(from: fileURL)
//… then save to disk
try FileManager.default.createDirectory(at: tracksFolderUrl, withIntermediateDirectories: true)
print("Saving data to \(trackUrl)")
try soundData.write(to: trackUrl)
isDownloading = false
}
self.player = try AVAudioPlayer(data: soundData)
guard let player = player else { return }
player.prepareToPlay()
player.play()
isPlaying = true
}
catch {
print(error)
}
}
func playPause() {
guard let player = player else {
print("Audio player not found")
return
}
if player.isPlaying {
player.pause()
isPlaying = false
} else {
player.play()
isPlaying = true
}
}
func stop() {
guard let player = player else {
print("Audio player not found")
return
}
if player.isPlaying {
player.stop()
isPlaying = false
}
}
func toggleLoop() {
guard let player = player else { return }
player.numberOfLoops = player.numberOfLoops == 0 ? -1 : 0
isLooping = player.numberOfLoops != 0
print("isLooping", isLooping)
}
}
Your current code will suspend while
(soundData, _) = try await URLSession.shared.data(from: fileURL)
is downloading data.
To stream files initialise the AVPlayer with the URL:
AVPlayer(url: url)
If you also want to download the data to save locally you can do that separately.
To get the duration of the track:
player.currentItem?.duration
to get the current playback position:
player.currentTime()
Apple's documentation is here.

AVAudioEngine doesn't playback a sound

I am trying to play with AVAudioEngine to playback the wav file. I tried to do it in a few different ways, but nothing work.
Try 1
...
private var audioEngine: AVAudioEngine = AVAudioEngine()
private var mixer: AVAudioMixerNode = AVAudioMixerNode()
private var audioFilePlayer: AVAudioPlayerNode = AVAudioPlayerNode()
func Play1() {
guard let filePath = Bundle.main.url(forResource: "testwav", withExtension: "wav", subdirectory: "res") else {
print("file not found")
return
}
print("\(filePath)")
guard let audioFile = try? AVAudioFile(forReading: filePath) else{ return }
let audioFormat = audioFile.processingFormat
let audioFrameCount = UInt32(audioFile.length)
guard let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount) else{ return }
do{
try audioFile.read(into: audioFileBuffer)
} catch{
print("over")
}
let mainMixer = audioEngine.mainMixerNode
audioEngine.attach(audioFilePlayer)
audioEngine.connect(audioFilePlayer, to:mainMixer, format: audioFileBuffer.format)
audioEngine.connect(mainMixer, to:audioEngine.outputNode, format: audioFileBuffer.format)
try? audioEngine.start()
audioFilePlayer.play()
audioFilePlayer.scheduleBuffer(audioFileBuffer, at: nil, options:AVAudioPlayerNodeBufferOptions.loops)
}
...
Try 2
...
private var audioEngine: AVAudioEngine = AVAudioEngine()
private var mixer: AVAudioMixerNode = AVAudioMixerNode()
private var audioFilePlayer: AVAudioPlayerNode = AVAudioPlayerNode()
func Play2() {
DispatchQueue.global(qos: .background).async {
self.audioEngine.attach(self.mixer)
self.audioEngine.connect(self.mixer, to: self.audioEngine.outputNode, format: nil)
// !important - start the engine *before* setting up the player nodes
try! self.audioEngine.start()
let audioPlayer = AVAudioPlayerNode()
self.audioEngine.attach(audioPlayer)
// Notice the output is the mixer in this case
self.audioEngine.connect(audioPlayer, to: self.mixer, format: nil)
guard let fileUrl = Bundle.main.url(forResource: "testwav", withExtension: "wav", subdirectory: "res") else {
// guard let url = Bundle.main.url(forResource: "audiotest", withExtension: "mp3", subdirectory: "res") else {
print("mp3 not found")
return
}
do {
let file = try AVAudioFile(forReading: fileUrl)
audioPlayer.scheduleFile(file, at: nil, completionHandler: nil)
audioPlayer.play(at: nil)
} catch let error {
print(error.localizedDescription)
}
}
}
...
...
private var audioEngine: AVAudioEngine = AVAudioEngine()
private var mixer: AVAudioMixerNode = AVAudioMixerNode()
private var audioFilePlayer: AVAudioPlayerNode = AVAudioPlayerNode()
func Play3() {
DispatchQueue.global(qos: .background).async {
self.audioEngine = AVAudioEngine()
_ = self.audioEngine.mainMixerNode
self.audioEngine.prepare()
do {
try self.audioEngine.start()
} catch {
print(error)
}
guard let url = Bundle.main.url(forResource: "testwav", withExtension: "wav", subdirectory: "res") else {
// guard let url = Bundle.main.url(forResource: "audiotest", withExtension: "mp3", subdirectory: "res") else {
print("mp3 not found")
return
}
let player = AVAudioPlayerNode()
player.volume = 1.0
do {
let audioFile = try AVAudioFile(forReading: url)
let format = audioFile.processingFormat
print(format)
self.audioEngine.attach(player)
self.audioEngine.connect(player, to: self.audioEngine.mainMixerNode, format: format)
player.scheduleFile(audioFile, at: nil, completionHandler: nil)
} catch let error {
print(error.localizedDescription)
}
player.play()
}
}
...
Also should be mentioned that there are no errors, while debugging I see that all the methods are executed and everything is ok, but I don't hear sound playback...
What am I doing wrong?
Try to activate your audio session with the following method:
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions = []) throws.
Please note that if another active audio session has higher priority than yours (for example, a phone call), and neither audio session allows mixing, attempting to activate your audio session fails. Deactivating an audio session that has running audio objects stops them, deactivates the session, and return an AVAudioSession.ErrorCode.isBusy error.

Swift 3: AVAudioPlayer not playing sound

I have a struct with my audio player in it:
struct MT_Audio {
func playAudio(_ fileName:String, _ fileExtension:String, _ atVolume:Float) {
var audioPlayer = AVAudioPlayer()
if let audioPath = Bundle.main.path(forResource: fileName, ofType: fileExtension) {
let audioURL = URL(string:audioPath)
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioURL!)
audioPlayer.volume = atVolume
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
}
}
}
//I'm calling it in viewDidLoad like this:
guard let fileURL = Bundle.main.url(forResource:"heartbeat-01a", withExtension: "mp3")
else {
print("can't find file")
return
}
let myAudioPlayer = MT_Audio() //<--RESOLVED THE ISSUE BY MAKING THIS A PROPERTY OF THE VIEWCONTROLLER
myAudioPlayer.playAudio("heartbeat-01a", "mp3", 1.0)
Since it doesn't crash and burn on the guard I know the file is there. I've also put a break point in after the try and I am getting to the audio player. When I go to the actual file and click on it in Xcode it plays. This fails on both the sim and device. Any help would be appreciated.
Looks like your audioPlayer is only stored within your playAudio function.
Try to keep the audioPlayer as an variable inside your class like this:
struct MT_Audio {
var audioPlayer: AVAudioPlayer?
mutating func playAudio(_ fileName:String, _ fileExtension:String, _ atVolume:Float) {
// is now member of your struct -> var audioPlayer = AVAudioPlayer()
if let audioPath = Bundle.main.path(forResource: fileName, ofType: fileExtension) {
let audioURL = URL(string:audioPath)
do {
let audioPlayer = try AVAudioPlayer(contentsOf: audioURL!)
audioPlayer.volume = atVolume
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
}
}
}

AVPlayer audio buffering in swift 3 source disconnected observer

I have app that plays AAC audio stream. Everything works fine, but when I disconnect stream and connect again after one second audio stop playing after half minute. When i don't reconnect i have error after one- two minutes. To reconnect i must stop AVPlayer and start again. I want to reconnect stream or show message immediately after player stops play music. How can I do that? Moreover i have another question: I convert my code to swift 3 and I have problem with one line:
fileprivate var playerItem = AVPlayerItem?()
error: cannot invoke initializer without argument
How i can fix that? Maybe this is the problem?
My Radio Player class:
import Foundation
import AVFoundation
import UIKit
protocol errorMessageDelegate {
func errorMessageChanged(_ newVal: String)
}
protocol sharedInstanceDelegate {
func sharedInstanceChanged(_ newVal: Bool)
}
class RadioPlayer : NSObject {
static let sharedInstance = RadioPlayer()
var instanceDelegate:sharedInstanceDelegate? = nil
var sharedInstanceBool = false {
didSet {
if let delegate = self.instanceDelegate {
delegate.sharedInstanceChanged(self.sharedInstanceBool)
}
}
}
fileprivate var player = AVPlayer(url: URL(string: Globals.radioURL)!)
// fileprivate var playerItem = AVPlayerItem?()
fileprivate var isPlaying = false
var errorDelegate:errorMessageDelegate? = nil
var errorMessage = "" {
didSet {
if let delegate = self.errorDelegate {
delegate.errorMessageChanged(self.errorMessage)
}
}
}
override init() {
super.init()
errorMessage = ""
let asset: AVURLAsset = AVURLAsset(url: URL(string: Globals.radioURL)!, options: nil)
let statusKey = "tracks"
asset.loadValuesAsynchronously(forKeys: [statusKey], completionHandler: {
var error: NSError? = nil
DispatchQueue.main.async(execute: {
let status: AVKeyValueStatus = asset.statusOfValue(forKey: statusKey, error: &error)
if status == AVKeyValueStatus.loaded{
let playerItem = AVPlayerItem(asset: asset)
self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true
} else {
self.errorMessage = error!.localizedDescription
print(error!)
}
})
})
NotificationCenter.default.addObserver(
forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: nil,
queue: nil,
using: { notification in
print("Status: Failed to continue")
self.errorMessage = NSLocalizedString("STREAM_INTERUPT", comment:"Stream was interrupted")
})
print("Initializing new player")
}
func resetPlayer() {
errorMessage = ""
let asset: AVURLAsset = AVURLAsset(url: URL(string: Globals.radioURL)!, options: nil)
let statusKey = "tracks"
asset.loadValuesAsynchronously(forKeys: [statusKey], completionHandler: {
var error: NSError? = nil
DispatchQueue.main.async(execute: {
let status: AVKeyValueStatus = asset.statusOfValue(forKey: statusKey, error: &error)
if status == AVKeyValueStatus.loaded{
let playerItem = AVPlayerItem(asset: asset)
// playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &ItemStatusContext)
self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true
} else {
self.errorMessage = error!.localizedDescription
print(error!)
}
})
})
}
func bufferFull() -> Bool {
return bufferAvailableSeconds() > 45.0
}
func bufferAvailableSeconds() -> TimeInterval {
// Check if there is a player instance
if ((player.currentItem) != nil) {
// Get current AVPlayerItem
let item: AVPlayerItem = player.currentItem!
if (item.status == AVPlayerItemStatus.readyToPlay) {
let timeRangeArray: NSArray = item.loadedTimeRanges as NSArray
if timeRangeArray.count < 1 { return(CMTimeGetSeconds(kCMTimeInvalid)) }
let aTimeRange: CMTimeRange = (timeRangeArray.object(at: 0) as AnyObject).timeRangeValue
// let startTime = CMTimeGetSeconds(aTimeRange.end)
let loadedDuration = CMTimeGetSeconds(aTimeRange.duration)
return (TimeInterval)(loadedDuration);
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
func play() {
player.play()
isPlaying = true
print("Radio is \(isPlaying ? "" : "not ")playing")
}
func pause() {
player.pause()
isPlaying = false
print("Radio is \(isPlaying ? "" : "not ")playing")
}
func currentlyPlaying() -> Bool {
return isPlaying
}
}
I will be grateful for help ;)
For the second issue fileprivate var playerItem = AVPlayerItem?()
write this and it should work fileprivate var playerItem: AVPlayerItem?.
For the first issue
when I disconnect stream and connect again after one second audio stop
playing after half minute. When i don't reconnect i have error after
one- two minutes. To reconnect i must stop AVPlayer and start again. I
want to reconnect stream or show message immediately after player
stops play music. How can I do that?
I don't get what's wrong ? You pause the player by pressing the button then you press the button again and after one - two minutes it stops by itself ?
I have tested the same class today and it works just fine, even after the connection is lost to the server (when the connection resumes you can click the play button and it will play)
I'll leave you my code here, give it a try
import Foundation
import AVFoundation
import UIKit
protocol errorMessageDelegate {
func errorMessageChanged(newVal: String)
}
protocol sharedInstanceDelegate {
func sharedInstanceChanged(newVal: Bool)
}
class RadioPlayer : NSObject {
static let sharedInstance = RadioPlayer()
var instanceDelegate:sharedInstanceDelegate? = nil
var sharedInstanceBool = false {
didSet {
if let delegate = self.instanceDelegate {
delegate.sharedInstanceChanged(newVal: self.sharedInstanceBool)
}
}
}
private var player = AVPlayer(url: NSURL(string: "<# YOUR STREAM HERE #>")! as URL)
private var playerItem: AVPlayerItem?
private var isPlaying = false
var errorDelegate:errorMessageDelegate? = nil
var errorMessage = "" {
didSet {
if let delegate = self.errorDelegate {
delegate.errorMessageChanged(newVal: self.errorMessage)
}
}
}
override init() {
super.init()
errorMessage = ""
let asset: AVURLAsset = AVURLAsset(url: NSURL(string: "<# YOUR STREAM HERE #>")! as URL, options: nil)
let statusKey = "tracks"
asset.loadValuesAsynchronously(forKeys: [statusKey], completionHandler: {
var error: NSError? = nil
DispatchQueue.main.async(execute: {
let status: AVKeyValueStatus = asset.statusOfValue(forKey: statusKey, error: &error)
if status == AVKeyValueStatus.loaded{
let playerItem = AVPlayerItem(asset: asset)
self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true
} else {
self.errorMessage = error!.localizedDescription
print(error!)
}
})
})
NotificationCenter.default.addObserver(
forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: nil,
queue: nil,
using: { notification in
print("Status: Failed to continue")
self.errorMessage = "Stream was interrupted"
})
print("Initializing new player")
}
func resetPlayer() {
errorMessage = ""
let asset: AVURLAsset = AVURLAsset(url: NSURL(string: "<# YOUR STREAM HERE #>")! as URL, options: nil)
let statusKey = "tracks"
asset.loadValuesAsynchronously(forKeys: [statusKey], completionHandler: {
var error: NSError? = nil
DispatchQueue.main.async(execute: {
let status: AVKeyValueStatus = asset.statusOfValue(forKey: statusKey, error: &error)
if status == AVKeyValueStatus.loaded{
let playerItem = AVPlayerItem(asset: asset)
//playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &ItemStatusContext)
self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true
} else {
self.errorMessage = error!.localizedDescription
print(error!)
}
})
})
}
func bufferFull() -> Bool {
return bufferAvailableSeconds() > 45.0
}
func bufferAvailableSeconds() -> TimeInterval {
// Check if there is a player instance
if ((player.currentItem) != nil) {
// Get current AVPlayerItem
let item: AVPlayerItem = player.currentItem!
if (item.status == AVPlayerItemStatus.readyToPlay) {
let timeRangeArray: NSArray = item.loadedTimeRanges as NSArray
if timeRangeArray.count < 1 { return(CMTimeGetSeconds(kCMTimeInvalid)) }
let aTimeRange: CMTimeRange = (timeRangeArray.object(at: 0) as AnyObject).timeRangeValue
//let startTime = CMTimeGetSeconds(aTimeRange.end)
let loadedDuration = CMTimeGetSeconds(aTimeRange.duration)
return (TimeInterval)(loadedDuration)
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
func play() {
player.play()
isPlaying = true
print("Radio is \(isPlaying ? "" : "not ")playing")
}
func pause() {
player.pause()
isPlaying = false
print("Radio is \(isPlaying ? "" : "not ")playing")
}
func currentlyPlaying() -> Bool {
return isPlaying
}
}