Play audio from "paused" point after restarting the track with AVAudioPlayer - ios

I'm working on a music player in Swift. The user can play a track, pause the track, or move a slider to choose a place to start. These three functions work.
However, if a user plays a track, listens for 1:00, and hits pause: when they click play again, it will restart the track from 0:00 instead of respecting where they paused.
I am using AVAudioPlayer as a media player -- according to the documentation, .play() allows you to specify a start-point using atTime.
So I tried to use atTime and have it start where the slider is -- if a listener listens for 1:00, I want atTime to dynamically pick that up. I try to establish this time value in a var.
class MusicViewController: UIViewController {
// removed imports and viewdidload to condense
#IBAction func playDownload(_ sender: Any) {
// removed file download code to condense
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl)
guard let player = audioPlayer else { return }
//here's where I try to capture a time value from the slider's progress
let checkTime = TimeInterval(Slider.value)
player.prepareToPlay()
player.play(atTime: checkTime)
} catch let error {
print(error.localizedDescription)
}
// updates slider with progress
Slider.value = 0.0
Slider.maximumValue = Float((audioPlayer?.duration)!)
audioPlayer.play()
timer = Timer.scheduledTimer(timeInterval: 0.0001, target: self, selector: #selector(self.updateSlider), userInfo: nil, repeats: true)
}
#IBAction func pause(_ sender: Any) {
if audioPlayer.isPlaying {
audioPlayer.pause()
} else {
audioPlayer.play()
}
}
#IBOutlet var Slider: UISlider!
#objc func updateSlider() {
Slider.value = Float(audioPlayer.currentTime)
print("Changing works")
}
}
So I am trying to capture progress and have .play respect that -- this code will run, but it does not work. No errors but after hitting play, it will go to the end of the track immediately now. No good.
I am new to Swift - any idea where I went wrong?

Related

AVAudioPlayer doesn't stop while playing

I'm working on developing an app that plays narration by playing sentence-by-sentence sound file one after another.
With below code, it played as expected. However, after adding "Stop" button to stop what's playing, I found that "Stop" button didn't stop the sound.
I tested the "Stop" button before pressing "Play" button, which worked no problem (message was printed). However, after pressing "Play" and while NarrationPlayer is playing, "Stop" button didn't work (no message was printed).
Any idea what's wrong?
import UIKit
import AVFoundation
class ViewController: UIViewController,AVAudioPlayerDelegate {
var NarrationPlayer:AVAudioPlayer = AVAudioPlayer()
var soundlist: [String] = []
var counter = 0
}
func playSound(_ soundfile: String) {
let NarPath = Bundle.main.path(forResource: soundfile, ofType:"mp3")!
let NarUrl = URL(fileURLWithPath: NarPath)
do {
NarrationPlayer = try AVAudioPlayer(contentsOf: NarUrl)
NarrationPlayer.delegate = self
} catch{
print(error)
}
NarrationPlayer.play()
}
#IBAction func play(_ sender: Any) {
soundlist.append("a")
soundlist.append("b")
soundlist.append("c")
playSound("first")
while counter < soundlist.count{
if NarrationPlayer.isPlaying == true{
}
else{
playSound(soundlist[counter])
counter += 1
}
}
}
#IBAction func StopPlay(_ sender: Any) {
print("stop button worked")
}
The problem you're running into is that this line here:
while counter < soundlist.count{
is hogging the main thread, keeping any click on your "Stop Playing" button from actually firing.
You've set a delegate method though, and one of the very handy things you can do here is increment your counter and play your next sound file each time a sound file finishes up.
Something like this:
func playSound(_ soundfile: String) {
let NarPath = Bundle.main.path(forResource: soundfile, ofType:"mp3")!
let NarUrl = URL(fileURLWithPath: NarPath)
do {
NarrationPlayer = try AVAudioPlayer(contentsOf: NarUrl)
NarrationPlayer.delegate = self
} catch{
print(error)
}
NarrationPlayer.play()
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
{
playSound(self.soundlist[counter])
counter += 1
}
#IBAction func play(_ sender: Any) {
playSound("first")
soundlist.append("a")
soundlist.append("b")
soundlist.append("c")
}
One last piece of advice:
Change the name NarrationPlayer to narrationPlayer. Variables in Swift, like in Objective-C, should start with lowercase (also known as lowerCamelCase).

How to play an audio file sample

Okay I'm trying to do something similar to iTunes not sure if it's still the same. It's when you click a song and it gives a sample of the audio file. This is my code looks.
The music file is like 2-3min long. I got the start time to start at 42sec seconds. However, the song finishes to the end. I'm trying to make the audio file a sample of 30sec. So it should start at 42sec and end at 1min and 12sec.
Would appreciate any help thanks.
Every time your audio sample starts playing, you can create a Timer object which will make your player stop in a given amount of time.
var audioPlayer = AVAudioPlayer()
var timer: Timer?
func prepareMusic() {
....
// Your code to start playing sample
audioPlayer.currentTime = 42
audioPlayer.play()
// Here we are stopping previous timer if there was any, and creating new one for 30 seconds. It will make player stop.
timer?.invalidate()
timer = Timer(fire: Date.init(timeIntervalSinceNow: 30), interval: 0, repeats: false) { (timer) in
if self.audioPlayer.isPlaying {
self.audioPlayer.stop()
}
}
RunLoop.main.add(timer!, forMode: .defaultRunLoopMode)
}
func musicButton(sender: UIButton) {
....
// If sample is stopped by user — stop timer as well
if audioPlayer.isPlaying {
audioPlayer.stop()
timer?.invalidate()
}
}
There is one more edge case I can think of — if you hide/close view controller, you might also want to stop that timer.
override func viewWillDisappear(_ animated: Bool) {
timer?.invalidate()
}

How To Make UISlider match Audio progress

I am creating a simple music app, and I was wondering how I can make a UiSlider to follow the progress of a audio file. Here's my project so far:
Code:
import UIKit
import AVFoundation
class SongDetailViewController: UITableViewController {
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "Song Name", ofType: "mp3")!))
audioPlayer.prepareToPlay()
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
}
}
catch {
print(error)
}
}
// Buttons
// Dismiss
#IBAction func dismiss(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
// Play
#IBAction func play(_ sender: Any) {
audioPlayer.stop()
audioPlayer.play()
}
// Pause
#IBAction func pause(_ sender: Any) {
audioPlayer.pause()
}
// Restart
#IBAction func restart(_ sender: Any) {
audioPlayer.currentTime = 0
}
}
I'm wanting to create the uislider similar to the Apple Music app where it follows the audio file's progress and whenever the user slides the ball thing (lol) it goes to that time of the song. If you could give me some code to complete this, that would be amazing!
Please keep in mind that I am fairly new to coding and am still learning swift, so keep it simple :-) Thanks again!
If you switch to using an AVPlayer, you can add a periodicTimeObserver to your AVPlayer. In the example below you'll get a callback every 1/30 second…
let player = AVPlayer(url: Bundle.main.url(forResource: "Song Name", withExtension: "mp3")!)
player.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 30), queue: .main) { time in
let fraction = CMTimeGetSeconds(time) / CMTimeGetSeconds(player.currentItem!.duration)
self.slider.value = fraction
}
Where you create an audioPlayer in your code, replace with the code above.
Using AVAudioPlayer you could create a periodic timer that fires several times a second (up to 60 times/second - any more would be a waste) and updates your slider based on your audio player's currentTime property. To sync the update with screen refresh you could use a CADisplayLink timer.
Edit:
This part of my answer doesn't work:
It should also be possible to set up a Key Value Observer on your
AVAudioPlayers currentTime property so that each time the value
changes your observer fires. (I haven't tried this, but it should
work.)

AVPlayer does not update currentPlaybackTime when seeking backwards

I have an NSTimer set up that fires every 0.1 seconds, in the callback I fetch currentTime() and use it to update the label with the duration of the video.
When I am seeking forwards, by setting the rate to 3, this timer keeps up, but when I set the rate to -3, the video keeps up, but the currentTime() still returns the same value from when I started seeking. This occurs until I stop seeking and then currentTime() returns the correct time
How can I fetch the current time the video is at, which will work when seeking backwards?
Edit: Here is the code I use (translated from Xamarin C#):
class VideoPlayer: UIView {
var player: AVPlayer!
var wasPaused: Bool!
func play(url: String) {
// set the URL to the Video Player
let streamingURL: NSURL = NSURL(string: url)!
player = AVPlayer(URL: streamingURL)
let playerLayer = AVPlayerLayer(layer: player)
layer.insertSublayer(playerLayer, atIndex: 0)
// Reset the state
player.seekToTime(CMTime(seconds: 0, preferredTimescale: 600))
// Start a timer to move the scrub label
NSTimer(timeInterval: 0.1, target: self, selector: #selector(playbackTimeUpdated), userInfo: nil, repeats: true)
}
func playbackTimeUpdated() {
// This one is not correct when seeking backwards
let time = player.currentTime().seconds;
// Use the time to adjust a UIProgressView
}
// Gets called when the reverse button is released
func reverseTouchUp() {
player.rate = 1
}
// Gets called when the reverse button is pressed
func reverseTouchDown()
{
player.rate = -3;
}
}
Try CMTimeGetSeconds(player.currentTime()) instead of player.currentTime().seconds. It works for me.
Also check that you timer is actually running (add NSLog calls to it for example), maybe you just blocking its thread.

Resume sound in AVAudioPlayer with swift

I am trying to play a clip of sound that can be paused, resumed or stopped. My pause button works, and the action function for this button is shown below. However I cannot get my resume function to work. When I press the resume button no audio is played. I have read around online and the only tips I can find are to use the prepareToPlay() function and to set shortStartTimeDelay to a value greater than 0.0. I have tried both of these to no avail.
timeAtPause is a global variable of type NSTimeInterval
The action function for the pause button is as follows:
#IBAction func pauseAllAudio(sender: UIButton) {
timeAtPause = audioPlayer.currentTime
audioPlayer.pause()
}
The action function for the resume button is as follows:
#IBAction func resumeAllAudio(sender: UIButton) {
let shortStartDelay = 0.01
audioPlayer.prepareToPlay()
audioPlayer.playAtTime(timeAtPause + shortStartDelay)
}
Any tips on how to resume the audio would be really appreciated.
Thank you for your time.
You should not use playAtTime() to resume the AVAudioPlayer, as documentation states:
Plays a sound asynchronously, starting at a specified point in the
audio output device’s timeline.
and
Use this method to precisely synchronize the playback of two or more
AVAudioPlayer objects.
And, even if you use it, it should be used in conjunction with deviceCurrentTime plus the time in seconds to have the delay. In one word, it's not meant to be used to resume the paused player. Instead, just use play() to resume the playback.
You probably did what I did. Upon calling my play() function, I created a new player every time:
// The Wrong Way
#IBAction func playAction(sender: AnyObject) {
// do and catch omitted for brevity
player = try AVAudioPlayer(contentsOf: goodURL)
player.play()
}
However this should be done at some other initialization time, such as when you have the user choose the file to play.
Or, if you're hard-coding the file to play, you could initialize at an earlier time, such as viewDidLoad().
Then, once your AVAudioPlayer is initialized, you can call .play() and pause() on it and they will work correctly. And .play() will resume after a pause.
To simply pause and restart at the same point in the audio file, the following works:
#IBAction func playAction(sender: AnyObject) {
player.play()
}
#IBAction func pauseAction(sender: AnyObject) {
player.pause()
}
I found these to be helpful. If you are doing this in Swift, I did this a little differently. I created an AudioPlayer class after importing AVFoundation and used these two functions to pause and resume the music. In the SwiftUI call, I used an #State boolean that toggled between the button states. Here is the code that I used:
import AVFoundation
class MusicPlayer {
static let shared = MusicPlayer()
var audioPlayer: AVAudioPlayer?
// Pause music
func pauseBackgroundMusic() {
guard let audioPlayer = audioPlayer else { return }
audioPlayer.pause()
}
// Resume music
func resumeBackgroundMusic() {
guard let audioPlayer = audioPlayer else { return }
audioPlayer.play()
}
}
And in the View:
Define an #State variable:
#State var pauseMode = false
And call the music player as follows in the Button action code:
Button(action: {
if pauseMode {
MusicPlayer.shared.resumeBackgroundMusic()
self.pauseMode.toggle()
} else {
MusicPlayer.shared.pauseBackgroundMusic()
self.pauseMode.toggle()
}
})
{
Image(systemName: "playpause")
}
FYI - I posted this in SwiftUI since newer programmers (and Apple) seem to be moving in this direction. I figured this might help someone (like me!) who came to this question seeking a Swift / SwiftUI answer.

Resources