Audio playback progress as UISlider in Swift - ios

I've seen some posts about accomplishing this in Objective-C but I've been unable to do the same via Swift.
Specifically, I can't figure out how to implement addPeriodicTimeObserverForInterval in the below.
var player : AVAudioPlayer! = nil
#IBAction func playAudio(sender: AnyObject) {
playButton.selected = !(playButton.selected)
if playButton.selected {
let fileURL = NSURL(string: toPass)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
startTime.text = "\(player.currentTime)"
endTime.text = NSString(format: "%.1f", player.duration)
} else {
player.stop()
}
Any assistance would be appreciated.

Thanks to the suggestion of Dare above, here's how I accomplished this:
var updater : CADisplayLink! = nil
#IBAction func playAudio(sender: AnyObject) {
playButton.selected = !(playButton.selected)
if playButton.selected {
updater = CADisplayLink(target: self, selector: Selector("trackAudio"))
updater.frameInterval = 1
updater.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
let fileURL = NSURL(string: toPass)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
startTime.text = "\(player.currentTime)"
theProgressBar.minimumValue = 0
theProgressBar.maximumValue = 100 // Percentage
} else {
player.stop()
}
}
func trackAudio() {
var normalizedTime = Float(player.currentTime * 100.0 / player.duration)
theProgressBar.value = normalizedTime
}
#IBAction func cancelClicked(sender: AnyObject) {
player.stop()
updater.invalidate()
dismissViewControllerAnimated(true, completion: nil)
}

Just to elaborate on my previous comment, this is how I implemented it and it seems to work pretty well. Any Swift corrections are more than welcome, I'm still an Obj-C guy for now.
#IBAction func playAudio(sender: AnyObject) {
var playing = false
if let currentPlayer = player {
playing = player.playing;
}else{
return;
}
if !playing {
let filePath = NSBundle.mainBundle().pathForResource("3e6129f2-8d6d-4cf4-a5ec-1b51b6c8e18b", ofType: "wav")
if let path = filePath{
let fileURL = NSURL(string: path)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
displayLink = CADisplayLink(target: self, selector: ("updateSliderProgress"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode!)
}
} else {
player.stop()
displayLink.invalidate()
}
}
func updateSliderProgress(){
var progress = player.currentTime / player.duration
timeSlider.setValue(Float(progress), animated: false)
}
*I set time slider's range between 0 and 1 on a storyboard

Specifically for Swift I was able to handle it like this:
I set the maximum value of the scrubSlider to the duration of the music file(.mp3) that was loaded in this method
override func viewDidLoad() {
super.viewDidLoad()
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("bach", ofType: "mp3")!))
scrubSlider.maximumValue = Float(player.duration)
} catch {
//Error
}
_ = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ViewController.updateScrubSlider), userInfo: nil, repeats: true)
}
I player was set to play the music at the time set by the value of the scrubber.
#IBAction func scrub(sender: AnyObject) {
player.currentTime = NSTimeInterval(scrubSlider.value)
}

Syntax has now changed in Swift 4:
updater = CADisplayLink(target: self, selector: #selector(self.trackAudio))
updater.preferredFramesPerSecond = 1
updater.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
And the function (I have previously set the progressSlider.maxValue to player.duration):
#objc func trackAudio() {
progressSlider.value = Float(player!.currentTime)
}

Related

Autopan in iOS with AVAudioPlayer smoothly

I am trying to build autopan(move the audio between left, middle and right channel) with AVaudioPlayer to play my music files. I could get it through AVAudioPlayer.pan method. Implemented to get AUTO pan by using timer in my swift code. Now the issue is ,audio is not playing smoothly and it breaks in between.
Here is my present code ,
class AudioPlayer: UIViewController {
var player = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
prepareAudioSession()
}
func prepareAudioSession() {
let audioFileName = "LPNumb"
let audioFileExtension = "mp3"
guard let filePath = Bundle.main.path(forResource: audioFileName, ofType: audioFileExtension) else {
print("Audio file not found at specified path")
return
}
do {
let alertSound = URL(fileURLWithPath: filePath)
try? player = AVAudioPlayer(contentsOf: alertSound)
} catch {
print(error)
}
}
#IBAction func play(_ sender: AnyObject){
player.play()
if player.isPlaying{
_ = Timer.scheduledTimer(timeInterval: 0.50, target: self, selector: #selector(self.update1), userInfo: nil, repeats: true)
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.update2), userInfo: nil, repeats: true)
_ = Timer.scheduledTimer(timeInterval: 1.50, target: self, selector: #selector(self.update3), userInfo: nil, repeats: true)
}
}
#objc func update1() {
player.pan = 0
}
#objc func update2() {
player.pan = 1
}
#objc func update3() {
player.pan = -1
}
}
I want to make the output audio as MONO and require audio to be played AUTOPANNED smoothly.
I think, you need make just one Timer for this task.Take a look through the code:
import UIKit
import AVFoundation
enum PanState {
case up
case down
}
class ViewController: UIViewController {
var audioPlayer: AVAudioPlayer?
var timerForPan: Timer?
var pan: Double = 0.0
var panState: PanState = .up
override func viewDidLoad() {
super.viewDidLoad()
if let path = Bundle.main.path(forResource: "LPNumb", ofType: "mp3") {
let record = URL(fileURLWithPath: path)
setupRecordToPlayer(from: record)
}
setTimers()
}
func setTimers() {
timerForPan = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updatePan), userInfo: nil, repeats: true)
}
func setupRecordToPlayer(from url: URL) {
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
} catch let error {
debugPrint(error.localizedDescription)
}
}
#IBAction func playButtonPressed(_ sender: UIButton) {
audioPlayerToPlay()
}
#objc func updatePan() {
if audioPlayer?.isPlaying ?? false {
switch panState {
case .up:
self.pan += 0.1
if pan >= 1 {
self.panState = .down
}
case .down:
self.pan -= 0.1
if pan <= -1 {
self.panState = .up
}
}
audioPlayer?.pan = Float(self.pan)
}
}
/// Prepare Audio player to play
func audioPlayerToPlay() {
audioPlayer?.prepareToPlay()
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
} catch {
print(error.localizedDescription)
}
audioPlayer?.play()
}
}

AVPlayer progress with UISlider Swift

I'm trying to use a slider to control Audio and everything works fine, but when I try to make the slider value equal to the player current time it crashes.
However, if I print something inside the updateSlider function, it appears and works fine.
override func viewDidLoad()
{
songTitle.text = mainSongTitle
background.image = image
mainImageView.image = image
downloadFileFromURL(url: URL(string: previewURL)!)
var time = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateSlider), userInfo: nil, repeats: true)
}
func downloadFileFromURL(url : URL)
{
var downloadTask = URLSessionDownloadTask()
downloadTask = URLSession.shared.downloadTask(with: url, completionHandler:
{
customURL , response , error in
self.play(url: customURL!)
self.slider.maximumValue = Float(player.duration)
})
downloadTask.resume()
}
func play(url : URL)
{
do
{
player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
player.play()
}
catch
{
print(error)
}
}
#IBAction func PlayPressed(_ sender: Any)
{
if player.isPlaying
{
player.pause()
}
else
{
player.play()
}
}
#IBAction func ChangerTimePlayer(_ sender: Any)
{
player.stop()
player.currentTime = TimeInterval(slider.value)
player.prepareToPlay()
player.play()
}
func updateSlider()
{
slider.value = Float(player.currentTime)
}
Maybe you should do something like:
func updateSlider(){
slider.value = Float(player.currentTime/player.duration)
}
Your download completion task needs to be called on the main thread to safely use UIKit (your slider):
DispatchQueue.main.async {
self.play(url: customURL!)
self.slider.maximumValue = Float(player.duration)
}

AVPlayer layer showing blank screen when application running or idle for 15 minutes

I would like to create a custom video player using AVPlayer() and AVPlayerLayer() classes.
My code working fine for application fresh start but goes wrong and showing blank screen when the application running or idle for more than 15 minutes.
When the occurrences this issue all classes of my application having AVPlayerLayer showing blank screen. I don't know why this happens.
AVPlayer() and AVPlayerlayer() instances are initialized as below.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
dispatch_async(dispatch_get_main_queue(), {
let playerItem = AVPlayerItem(URL: self.itemUrl)
self.avPlayer = AVPlayer(playerItem: playerItem)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(VideoPreviewView.restartVideoFromBeginning),
name: AVPlayerItemDidPlayToEndTimeNotification,
object: self.avPlayer.currentItem)
self.avPlayerLayer = AVPlayerLayer(player: self.avPlayer)
self.view.layer.addSublayer(self.avPlayerLayer)
self.avPlayerLayer.frame = self.view.bounds
self.avPlayer.pause()
})
}
}
Play function
func playVideo(sender: AnyObject) {
avPlayer.play()
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
slider.hidden=false
myTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(VideoPreviewView.updateSlider), userInfo: nil, repeats: true)
slider.addTarget(self, action: #selector(VideoPreviewView.sliderValueDidChange(_: )), forControlEvents: UIControlEvents.ValueChanged)
slider.minimumValue = 0.0
slider.continuous = true
pauseButton.hidden=false
playButton.hidden=true
closeViewButton.hidden = false
}
Restart video
func restartVideoFromBeginning() {
let seconds: Int64 = 0
let preferredTimeScale: Int32 = 1
let seekTime: CMTime = CMTimeMake(seconds, preferredTimeScale)
avPlayer?.seekToTime(seekTime)
avPlayer?.pause()
pauseButton.hidden=true
playButton.hidden = false
closeViewButton.hidden=false
}
func updateSlider() {
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
else{
print("Not playing.")
}
currentTime = Float(CMTimeGetSeconds(avPlayer.currentTime()))
duration = avPlayer.currentItem!.asset.duration
totalDuration = Float(CMTimeGetSeconds(duration))
slider.value = currentTime // Setting slider value as current time
slider.maximumValue = totalDuration // Setting maximum value as total duration of the video
}
func sliderValueDidChange(sender: UISlider!) {
timeInSecond=slider.value
newtime = CMTimeMakeWithSeconds(Double(timeInSecond), 1)// Setting new time using slider value
avPlayer.seekToTime(newtime)
}
#IBAction func closeViewAction(sender: AnyObject) {
pauseButton.hidden=true
self.avPlayer.pause()
self.avPlayerLayer.removeFromSuperlayer()
self.dismissViewControllerAnimated(true, completion: nil)
}

Play audio while downloading without downloading file twice?

I would like to write an iOS app that plays an audio file as soon as there is enough data while continuing to download. It seems that you can either download and play when the download is finishing (Using NSURLProtocol to implement play while downloading for AVPlayer on iOS) or continuously stream without getting to save a file (Playing audio file while I download it). Is there any way to download and play at the same time without downloading two copies of the file?
You can do that with the help of AVAssetResourceLoader and AVPlayer
Here is the link of the tutorial
http://leshkoapps.com/wordpress/audio-streaming-and-caching-in-ios-using-avassetresourceloader-and-avplayer/
And here is the Github repo for that
https://github.com/leshkoapps/AVAssetResourceLoader
here you can use below functions without download (Streaming)
import AVFoundation
var progressTimer:Timer?
{
willSet {
progressTimer?.invalidate()
}
}
var playerStream: AVPlayer?
var playerItem: AVPlayerItem?
func playerStream(urlStream : String)
{
if let playerStream = playerStream
{
if playerStream.isPlaying
{
stopProgressTimer()
playerStream.pause()
}
else
{
startProgressTimer()
playerStream.play()
}
}
else
{
if let urlStr = urlStream.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
{
if let TempURL = URL.init(string: urlStr)
{
playerItem = AVPlayerItem(url: TempURL)
playerStream = AVPlayer(playerItem: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
}
}
}
}
func playerItemDidPlayToEndTime() {
stopProgressTimer()
self.playProgressView.progress = 0.0
if let playerStream = self.playerStream
{
playerStream.replaceCurrentItem(with: playerItem)
playerStream.seek(to: kCMTimeZero)
// playerStream.seek(to: .zero) swift 4.0
}
}
func stopProgressTimer() {
progressTimer?.invalidate()
progressTimer = nil
}
func startProgressTimer()
{
if #available(iOS 10.0, *) {
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){_ in
self.updateProgressTimer()
}
}
else {
progressTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateProgressTimer), userInfo: nil, repeats: true)
}
}
#objc func updateProgressTimer()
{
if let playerItem = playerItem
{
if let pa = playerStream
{
let floatTime = Float(CMTimeGetSeconds(pa.currentTime()))
let floatTimeDu = Float(CMTimeGetSeconds(playerItem.duration))
playProgressView.progress = Double(floatTime / floatTimeDu)
}
}
}

Make a playlist (start next song) in swift

I have created a sound player in swift with AVFoundation. I am trying to start the next song in array when the playing song is finished. I was trying to implement this code
if (audioPlayer.currentTime >= audioPlayer.duration){
var recentSong = songPlaylist[selectedSongNumber + 1]
audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
audioPlayer.play()
}
but I am not being able to implement this code (I do not know where to implement it).Here is my complete code
import UIKit
import AVFoundation
import AVKit
public var audioPlayer = AVPlayer()
public var selectedSongNumber = Int()
public var songPlaylist:[String] = ["song1", "song2"]
public var recentSong = "song1"
let playImage = UIImage(named: "Play.png") as UIImage!
let pauseImage = UIImage(named: "Pause.png") as UIImage!
class FirstViewController: UIViewController {
#IBOutlet weak var musicSlider: UISlider!
#IBOutlet weak var PlayPause: UIButton!
var audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
override func viewDidLoad() {
super.viewDidLoad()
musicSlider.maximumValue = Float(audioPlayer.duration)
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("updateMusicSlider"), userInfo: nil, repeats: true)
if (audioPlayer.currentTime >= audioPlayer.duration){
var recentSong = songPlaylist[selectedSongNumber + 1]
audioPlayer = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath:
NSBundle.mainBundle().pathForResource(recentSong, ofType: "mp3")!), error: nil)
audioPlayer.play()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func PlayPauseButton(sender: AnyObject) {
if (audioPlayer.playing == false){
audioPlayer.play()
PlayPause.setImage(pauseImage, forState: .Normal)
}else{
audioPlayer.pause()
PlayPause.setImage(playImage, forState: .Normal)
}
}
#IBAction func StopButton(sender: AnyObject) {
audioPlayer.stop()
audioPlayer.currentTime = 0
PlayPause.setImage(playImage, forState: .Normal)
}
#IBAction func musicSliderAction(sender: UISlider) {
audioPlayer.stop()
audioPlayer.currentTime = NSTimeInterval(musicSlider.value)
audioPlayer.play()
}
func updateMusicSlider(){
musicSlider.value = Float(audioPlayer.currentTime)
}
}
I am updating my code with something different:
import UIKit
import AVFoundation
class ViewController: UIViewController, AVAudioPlayerDelegate {
var counter = 0
var song = ["1","2","3"]
var player = AVAudioPlayer()
#IBOutlet weak var musicSlider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
musicSlider.value = 0.0
}
func updateMusicSlider(){
musicSlider.value = Float(player.currentTime)
}
#IBAction func playSong(sender: AnyObject) {
music()
}
#IBAction func sliderAction(sender: AnyObject) {
player.stop()
player.currentTime = NSTimeInterval(musicSlider.value)
player.play()
}
func music(){
var audioPath = NSBundle.mainBundle().pathForResource("\(song[counter])", ofType: "mp3")!
var error : NSError? = nil
player = AVAudioPlayer(contentsOfURL: NSURL(string: audioPath), error: &error)
musicSlider.maximumValue = Float(player.duration)
var timer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: Selector("updateMusicSlider"), userInfo: nil, repeats: true)
player.delegate = self
if error == nil {
player.delegate = self
player.prepareToPlay()
player.play()
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool)
{
println("Called")
if flag {
counter++
}
if ((counter + 1) == song.count) {
counter = 0
}
music()
}
}
You can do it this way.
Hope It will help and HERE is sample project for more Info.
You need to implement AVAudioPlayerDelegate Protocol's method:
optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer!, successfully flag: Bool)
Documentation link
Play your next music item here.
But I will not recommend, since AVAudioPlayer can only play one item at a time. You need to instantiate again with another music item after completion. I will suggest you to use AVQueuePlayer. Detaled answer has been given here. Hope it helps!
I created a sound player in swift with AVFoundation. I used progressView to time the song and then when it hits 0.98743 it will update to the next song automatically this is the Github link: https://github.com/ryan-wlr/MusicPlayerIOS
func updateProgressView() {
if (progressView.progress > a.advanced(by: 0.98743)) {
audioPlayerDidFinishPlayeing()
}
if audioPlayer.isPlaying {
let progress = Float(audioPlayer.currentTime/audioPlayer.duration)
progressView.setProgress(progress, animated: true)
}
}

Resources