Swift: Playing audio using AVPlayer - Audio is not playing, cannot hear audio - swift

I am using AVPlayer to play an mp3 audio file only. I am using a url that I tested and works fine. I needed to use the AVPlayer because I needed to setup a UISlider programmatically and AVPlayer is convenient. The UISlider works and updates as the audio plays. The audio might be be playing but I cannot hear the sound. I say this because the UISlider is working.
Update: You can hear the audio when building the app on a simulator. Issue occurs when building it on device - mine is XS MAX.
Link to screen recordong ->
Visit: https://streamable.com/nkbn8
I have tried using the same URL with AVAudioPlayer and audio plays and you can hear it.
private func setupAudioContent() {
let urlString = "https://s3.amazonaws.com/kargopolov/kukushka.mp3"
if let url = NSURL(string: urlString) {
audioPlayer = AVPlayer(url: url as URL)
let playerLayer = AVPlayerLayer(player: audioPlayer)
self.layer.addSublayer(playerLayer)
playerLayer.frame = self.frame
audioPlayer?.play()
audioPlayer?.volume = 1.0
audioPlayer?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
let interval = CMTime(value: 1, timescale: 2)
audioPlayer?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
let currentTime = CMTimeGetSeconds(progressTime)
let currentTimeSecondsString = String(format: "%02d", Int(currentTime.truncatingRemainder(dividingBy: 60)))
let currentTimeMinutesString = String(format: "%02d", Int(currentTime / 60))
self.currentTimeLabel.text = "\(currentTimeMinutesString):\(currentTimeSecondsString)"
if let duration = self.audioPlayer?.currentItem?.duration {
let durationsSeconds = CMTimeGetSeconds(duration)
self.audioSlider.value = Float(currentTime / durationsSeconds)
}
})
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "currentItem.loadedTimeRanges" {
isAudioPlaying = true
if let duration = audioPlayer?.currentItem?.duration {
let seconds = CMTimeGetSeconds(duration)
let secondsText = Int(seconds) % 60
let minutesText = String(format: "%02d", Int(seconds) / 60)
audioLengthLabel.text = "\(minutesText):\(secondsText)"
}
}
}
#objc func handleSliderChange() {
if let duration = audioPlayer?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
let value = Float64(audioSlider.value) * totalSeconds
let seekTime = CMTime(value: Int64(value), timescale: 1)
audioPlayer?.seek(to: seekTime, completionHandler: { (completedSeek) in
})
}
}
Expected result: Hear Audio playing
Actual result: cannot hear audio playing. Seems like audio is playing just no sound.

When using AVPlayer you should make sure your device is not on silent mode as that will cause to not output audio even though your volume is at max.
If you would like to keep your device on silent mode and still play the audio you can use the following code before your .play():
do {
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
}
catch {
// report for an error
print(error)
}

Related

Change BPM in real time with AVAudioEngine using Swift

Hello I am trying to implement simple audio app using AVAudioEngine, which plays short wav audio files in a loop at some bpm, that can be changed in real time (by slider or something).
Current solution logic:
set bpm=60
create audioFile from sample.wav
calculate bufferSize: AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm))
set bufferSize to audioBuffer
load file audioFile into audioBuffer.
schedule audioBuffer to play
This solution works, but the issue is - if I want to change bpm I need to recreate buffer with different bufferSize, so it will not be in real time, since I need to stop player and reschedule buffer with different bufferSize.
Any thoughts how it can be done ?
Thanks in advance !
Code (main part):
var bpm:Float = 30
let engine = AVAudioEngine()
var player = AVAudioPlayerNode()
var audioBuffer: AVAudioPCMBuffer?
var audioFile: AVAudioFile?
override func viewDidLoad() {
super.viewDidLoad()
audioFile = loadfile(from: "sound.wav")
audioBuffer = tickBuffer(audioFile: audioFile!)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFile?.processingFormat)
do {
engine.prepare()
try engine.start()
} catch {
print(error)
}
}
private func loadfile(from fileName: String) -> AVAudioFile? {
let path = Bundle.main.path(forResource: fileName, ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
let audioFile = try AVAudioFile(forReading: url)
return audioFile
} catch {
print("Error loading buffer1 \(error)")
}
return nil
}
func tickBuffer(audioFile: AVAudioFile) -> AVAudioPCMBuffer {
let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm))
let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: periodLength)!
try! audioFile.read(into: buffer)
buffer.frameLength = periodLength
return buffer
}
func play() {
player.scheduleBuffer(audioBuffer, at: nil, options: .loops, completionHandler: nil)
player.play()
}
func stop() {
player.stop()
}

iOS AVPlayer won't play short video in iPhone 7

I am trying to play mp4 video file continuously. But it is not played short video about 2 seconds. With videos more than 2 seconds(6 seconds) is worked well. It is happened only in iPhone 7 in device and simulator. But with other devices it is worked well also.
My code is here:
//Stop player before starting new play
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
playerLayer?.removeFromSuperlayer()
playerLayer = nil
guard let url = Bundle.main.url(forResource: video, withExtension: "mp4") else {
return
}
try? AVAudioSession.sharedInstance().setCategory(.playAndRecord)
try? AVAudioSession.sharedInstance().setActive(true)
player = AVPlayer(url: url)
player?.volume = 1.0
playerLayer = AVPlayerLayer(player: player)
videoContainerView.layer.addSublayer(playerLayer)
playerLayer?.frame = videoContainerView.bounds
//track player progress
let interval = CMTime(value: 1, timescale: 2)
player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak player] (progressTime) in
let seconds = CMTimeGetSeconds(progressTime)
guard let duration = player?.currentItem?.duration else {
return
}
let durationSeconds = CMTimeGetSeconds(duration)
if seconds >= durationSeconds {
// Seek and play again
player?.seek(to: CMTime.zero, completionHandler: { (completedSeek) in
player?.play()
})
}
})
player?.play()

AVAudioPlayer does not think it is playing when it is

I call the superPlay function to start audio playback a little while later I call the superPlay function again to stop the playback. This does not work though because player.isPlaying is false even knowing the sound is clearly playing in an infinite loop. I have no idea why please help!
func superPlay(timeInterval: TimeInterval, soundName: String) {
do {
alarmSound = soundName
//set up audio session
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: alarmSound, withExtension: "mp3")
player = try! AVAudioPlayer(contentsOf: url!)
player.numberOfLoops = -1
//Start AVAudioPlayer
print(timeInterval)
print(time)
let playbackDelay = timeInterval // must be ≥ 0
if player.isPlaying {
player.stop()
} else {
player.play(atTime: player.deviceCurrentTime + playbackDelay) //time is a TimeInterval after which the audio will start
}
}
catch {
print(error)
}
}
I have spent a couple more days debugging this now. What is happening is the original play is being assigned to a specific AudioPlayer ID for example the print is: "Optional(<AVAudioPlayer: 0x600002802280>)"
When I call the function again to stop the play the AVAudioPlayer is assigned a different ID therefore it does not find that the old player is still playing and moves forward with playing a new sound on top of the old sound.
I am not sure how to store the AVAudioPlayer ID and then call the function so that it checks the store Player for if it is ".isPlaying"??
This happen because you initialize it before stopping current audio. Try this way:-
func superPlay(timeInterval: TimeInterval, soundName: String) {
// If audio is playing already then stop it.
if let audioPlayer = player {
if player.isplaying {
player.stop()
}
}
// Initilize audio player object with new sound
do {
alarmSound = soundName
//set up audio session
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: alarmSound, withExtension: "mp3")
player = try! AVAudioPlayer(contentsOf: url!)
player.numberOfLoops = -1
//Start AVAudioPlayer
print(timeInterval)
print(time)
let playbackDelay = timeInterval // must be ≥ 0
player.play(atTime: player.deviceCurrentTime + playbackDelay) //time is a TimeInterval after which the audio will start
}
catch {
print(error)
}
}

Unable to play downloaded HLS content while internet is not available

I am working on Downloading and playing HLS content, To download the HLS I am using following code
func downloadTask() {
let videoUrl = URL(string: "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
downloadSession = AVAssetDownloadURLSession(configuration: configuration!, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
destinationUrl = documentsDirectoryURL.appendingPathComponent(videoUrl.lastPathComponent)
var urlComponents = URLComponents(
url: videoUrl,
resolvingAgainstBaseURL: false
)!
urlComponents.scheme = "https"
do {
let asset = try AVURLAsset(url: urlComponents.url!)
asset.resourceLoader.setDelegate(self, queue: DispatchQueue(label: "com.example.AssetResourceLoaderDelegateQueue"))
if #available(iOS 10.0, *) {
assetDownloadTask = downloadSession!
.makeAssetDownloadTask(
asset: asset,
assetTitle: "RG-TVVideo",
assetArtworkData: nil,
options: nil
)
APP_DELEGATE.isProgressRunning = true
assetDownloadTask?.resume()
} else {
// Fallback on earlier versions
}
} catch { print("Erorr while parsing the URL.") }
}
Download finished
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
if #available(iOS 11.0, *) {
let storageManager = AVAssetDownloadStorageManager.shared()
let newPolicy = AVMutableAssetDownloadStorageManagementPolicy()
newPolicy.expirationDate = Date()
newPolicy.priority = .important
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(location.relativePath)
storageManager.setStorageManagementPolicy(newPolicy, for: assetURL)
UserDefaults.standard.set(location.relativePath, forKey: "videoPath")
strDownloadStatus = "5"
let dictVideoInfo = ["strDownloadStatus" : "5","VideoID":self.strID]
// Here I am Storing Downloaded location in to database
DBManager.shared.updateVideoStatus(strVideoID: APP_DELEGATE.arrTempVideoIds.object(at: 0) as! String, strStatus: "5", strSavePath: location.relativePath) { (status) in }
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name.init("UpdateProgress"), object: self.percentageComplete, userInfo: dictVideoInfo)
}
}
}
Now I am trying to get Video path from the location which is stored in Database and trying to play it offline(Without Internet) using following code
func setLocalPlayer(strDownloadPath: String) {
let strDownloadPath = “”
//Getting path from database
DBManager.shared.getDownloadedPath(videoID: VideoID) { (strPath) in
strDownloadPath = strPath
}
activityIndicator.isHidden = false
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(strDownloadPath)
let asset = AVURLAsset(url: assetURL)
// if let cache = asset.assetCache, cache.isPlayableOffline {
// let videoAsset = AVURLAsset(url: assetURL)
asset.resourceLoader.preloadsEligibleContentKeys = true
asset.resourceLoader.setDelegate(self, queue: DispatchQueue(label: "com.example.AssetResourceLoaderDelegateQueue"))
let playerItem = AVPlayerItem(asset: asset)
avPlayer = AVPlayer(playerItem: playerItem)
avPlayerLayer = AVPlayerLayer()
avPlayerLayer.frame = CGRect(x: 0, y: 0, width: playerContainer.frame.width, height: playerContainer.frame.height)
avPlayerLayer.videoGravity = .resize
avPlayerLayer.player = avPlayer
playerContainer.layer.addSublayer(avPlayerLayer)
let interval = CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = avPlayer?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
self.updateVideoPlayerState()
if self.avPlayer != nil {
self.bufferState()
}
})
self.slider.setThumbImage(UIImage(named: "slider_dot"), for: UIControl.State.normal)
resetTimer()
avPlayer.play()
isPlaying = true
// }
}
NOTE: This code is working fine when internet is on
I have referred following links
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/HTTPLiveStreaming/HTTPLiveStreaming.html
https://assist-software.net/snippets/how-play-encrypted-http-live-streams-offline-avfoundation-ios-using-swift-4
Downloading and playing offline HLS Content - iOS 10
Please guide what I am doing wrong.
Thanks
Well, I don't know if it's your error, but for further readings :
Don't do newPolicy.expirationDate = Date() it's a mistake. According to Advances in HTTP Live Streaming 2017 WWDC session, it will delete your file as soon as possible.
Before playing your offline playback, you can check if it's still on your device in Settings -> General -> Storage -> MyApp
The expiration date property is there in case your asset at some point
becomes no longer eligible to be played. For instance, you may find
that you may be in a situation where a particular show may be leaving
your catalog, you no longer have rights to stream it.
If that's the case you can set the expiration date and it will be sort of bumped up
in the deletion queue. So, using it is fairly straight forward.

How to play multiple Audio Files simultaneously using AVPlayer?

I am trying to play multiple audio files using 2 AVPlayer instances, but one of the player stops for a fraction of a second rather than playing all audio files simultaneously.
The logic of the program is as follows:
var player: AVPlayer? will stream an audio file from my database. On its own is playing perfectly.
fileprivate var countPlayer: AVPlayer? plays the count number of the current item being played by var player. The count is a sequence of 1 to 8 and for each digit I am storing/sandobxing a .wav file locally such as 1.wav, 2.wav...8.wav.
When current time of var player is at a certain time, countPlayer is triggered and it plays one of the local file 1.wav, 2.wav..etc.
The problem is that when the var countPlayer starts playing, it causes the background AVPlayer, namely var player to stop for a fraction of a second, similar to what's described in this comment:
Play multiple Audio Files with AVPlayer
var player: AVPlayer? //plays the song
fileprivate var countPlayer: AVPlayer? // plays the count number of song
private func addBoundaryTimeObserver(tableIndexPath: IndexPath) {
let mediaItem = mediaArray[tableIndexPath.row]
guard let url = URL(string: mediaItem.mediaAudioUrlStringRepresentation ?? "") else {return}
let playerItem = AVPlayerItem(url: url)
player = AVPlayer(playerItem: playerItem)
var timesToTransverse = [NSValue]()
//convert string representation of times elements to array
let timesRecorded: [String] = mediaItem.timesRecorded.components(separatedBy: ",")
// Build boundary times from arrayOfBeats keys
let timeDoubles: [Double] = timesRecorded.compactMap {timeString in
if let second = Double("\(timeString)") {
return second
}
return nil
}
guard timeDoubles.count > 0 else {return} //unexpected
timesToTransverse = timeDoubles.map { second in
let cmtime = CMTime(seconds: second, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
return NSValue(time: cmtime)
}
guard timesToTransverse.count != 0 else {return}
guard let playerCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? PlayerCell else {return}
startTime = Date().timeIntervalSinceReferenceDate
timeIndex = 0
player?.play()
player?.rate = Float(initialPlaybackRate)
// find the index of time
//reset timeObserverToken
// call a function with the new times sorted
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
timeObserverToken =
player?.addBoundaryTimeObserver(forTimes: timesToTransverse, queue: mainQueue) {
[weak self] in
//because there are no time signature changes, we can simply increment timeIndex with + 1 every time `addBoundaryTimeObserver` completion handler is called and subscript timesToTransverse with timeIndex in order to get the subsequent timeInSeconds
guard let strongSelf = self, strongSelf.timeIndex < timesToTransverse.count else {return}
let timeElement = timesToTransverse[strongSelf.timeIndex]
strongSelf.timeInSeconds = CMTimeGetSeconds(timeElement.timeValue)
//show progress in progressView
let duration = CMTimeGetSeconds(playerItem.duration)
let cmtimeSeconds = CMTime(seconds: strongSelf.timeInSeconds, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
//Total time since timer started, in seconds
strongSelf.timeInSeconds = Date().timeIntervalSinceReferenceDate - strongSelf.startTime
let timeString = String(format: "%.2f", strongSelf.timeInSeconds)
strongSelf.timeString = timeString
//use reminder operator to determine the beat count
let beat = (strongSelf.timeIndex + 1) % 8 == 0 ? 8 : ((strongSelf.timeIndex + 1) % 8)
//play the beat count : 1, 2, ...8
self.preapareToPlayAudio(beatCount: beat)
/*
0: (0 + 1) % 8 = 1
1: (1 + 1) % 8 = 2
6: (6 + 1) % 8 = 7
7: (7 + 1) % 8 = 0
*/
strongSelf.timeIndex += 1
}
}//end addBoundaryTimeObserver
//prepare determine what wav file to play
private func preapareToPlayAudio(beatCount: Int) {
switch beatCount {
case 1:
guard let url = Bundle.main.url(forResource: "1", withExtension: "wav") else {return}
playWith(beatCountURL: url)
//7 more cases go here .....
default: print("unexpected case here")
}
}//end play(beatCount: Int)
private func playWith(beatCountURL: URL) {
let playerItem = AVPlayerItem(url: beatCountURL)
countPlayer = AVPlayer(playerItem: playerItem)
countPlayer?.play()
}
You would be better off using AVAudioPlayerNode, AVAudioMixerNode, AVAudioEngine. Using these classes you won't have problems like you have right now. It's also not that difficult to set up.
You can check out my gist, in order to play the sounds in your Playgrounds you would need to put audio files into Resources folder in Project Navigator:
https://gist.github.com/standinga/24342d23acfe70dc08cbcc994895f32b
The code works without stopping background audio when top sounds are triggered.
Here's also the same code:
import AVFoundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
class AudioPlayer {
var backgroundAudioFile:AVAudioFile
var topAudioFiles: [AVAudioFile] = []
var engine:AVAudioEngine
var backgroundAudioNode: AVAudioPlayerNode
var topAudioAudioNodes = [AVAudioPlayerNode]()
var mixer: AVAudioMixerNode
var timer: Timer!
var urls: [URL] = []
init (_ url: URL, urls: [URL] = []) {
backgroundAudioFile = try! AVAudioFile(forReading: url)
topAudioFiles = urls.map { try! AVAudioFile(forReading: $0) }
engine = AVAudioEngine()
mixer = AVAudioMixerNode()
engine.attach(mixer)
engine.connect(mixer, to: engine.outputNode, format: nil)
self.urls = urls
backgroundAudioNode = AVAudioPlayerNode()
for _ in topAudioFiles {
topAudioAudioNodes += [AVAudioPlayerNode()]
}
}
func start() {
engine.attach(backgroundAudioNode)
engine.connect(backgroundAudioNode, to: mixer, format: nil)
backgroundAudioNode.scheduleFile(backgroundAudioFile, at: nil, completionHandler: nil)
try! engine.start()
backgroundAudioNode.play()
for node in topAudioAudioNodes {
engine.attach(node)
engine.connect(node, to: mixer, format: nil)
try! engine.start()
}
// simulate rescheduling files played on top of background audio
DispatchQueue.global().async { [unowned self] in
for i in 0..<1000 {
sleep(2)
let index = i % self.topAudioAudioNodes.count
let node = self.topAudioAudioNodes[index]
node.scheduleFile(self.topAudioFiles[index], at: nil, completionHandler: nil)
node.play()
}
}
}
}
let bundle = Bundle.main
let beepLow = bundle.url(forResource: "beeplow", withExtension: "wav")!
let beepMid = bundle.url(forResource: "beepmid", withExtension: "wav")!
let backgroundAudio = bundle.url(forResource: "backgroundAudio", withExtension: "wav")!
let audioPlayer = AudioPlayer(backgroundAudio, urls: [beepLow, beepMid])
audioPlayer.start()