Autopan in iOS with AVAudioPlayer smoothly - ios

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()
}
}

Related

Swift timer won't work after invalidating and reinitiating

I'm using Timer() for indicating current audio position in audio player
func startTimer() {
print("PlayerController: startTimer()")
if itemPlayTimer != nil {
return
}
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription?(currentTime)
}
when user pause player app invalidating timer
func stopTimer() {
itemPlayTimer?.invalidate()
itemPlayTimer = nil
}
But after calling startTimer() again selector won't call
The reason was that the timer starts executing in other thread, not in main thread.
change your functions in this way:
func startTimer() {
print("PlayerController: startTimer()")
if itemPlayTimer == nil {
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
}
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription?(currentTime)
}
func stopTimer() {
if itemPlayTimer != nil {
itemPlayTimer?.invalidate()
itemPlayTimer = nil
}
}
Use the following code. I have used An AVPlayer with a sample Video to demonstrate pausing/playing with Timer. Logic is same for audio player.
Just replace AVPlayer with your audioPlayer.
Key logic here is to properly manage the state of Playing/not playing and checking timer for nil etc.
As indicated in this Answer
startTimer() starts the timer only if it's nil and stopTimer() stops
it only if it's not nil.
You have only to take care of stopping the timer before
creating/starting a new one.
I have implemented this in a sample project and is working 100%.
Carefully See the function pauseTapped(_ sender: UIButton)
See sample gif At end of Code
import UIKit
import AVKit
class TimerVC: UIViewController {
///A container view for displaying the AVPlayer.
#IBOutlet weak var playerView: UIView!
/// A button to play and pause the video
#IBOutlet weak var btnPause: UIButton!
///to maintain the status of AVPlayer playing or not
var flagPlaying = true
///An AVPlayer for displaying and playing the video
var player: AVPlayer?
///to show the current time to user
#IBOutlet weak var lblCurrentTime: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//add an AVPlayer with sample URL link
addVideoPlayer( playerView: playerView)
}
///Timer
var itemPlayTimer: Timer?
#objc func startTimer() {
if itemPlayTimer != nil {
return
}
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
///update time label
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription(currentTime())
}
func updateTimeDescription( _ currentTime :CMTime ) {
self.lblCurrentTime.text = "\(currentTime.seconds)"
}
///To Pause and play the video
#IBAction func pauseTapped(_ sender: UIButton) {
if flagPlaying {
//pause
if itemPlayTimer != nil {
player?.pause()
itemPlayTimer?.invalidate()
itemPlayTimer = nil
flagPlaying = false
DispatchQueue.main.async {
self.btnPause.setTitle("Play", for: .normal)
}
}
}else {
//not playing
if itemPlayTimer == nil {
player?.play()
startTimer()
flagPlaying = true
DispatchQueue.main.async {
self.btnPause.setTitle("Pause", for: .normal)
}
}
}
}
private func addVideoPlayer(playerView: UIView) {
//let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer.init(url: URL.init(string: "http://techslides.com/demos/sample-videos/small.mp4")!)
let layer: AVPlayerLayer = AVPlayerLayer(player: player)
layer.backgroundColor = UIColor.white.cgColor
layer.frame = CGRect(x: 0, y: 0, width: playerView.frame.width, height: playerView.frame.height)
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerView.layer.sublayers?.forEach({$0.removeFromSuperlayer()})
playerView.layer.addSublayer(layer)
flagPlaying = true
player?.play()
startTimer()
}
}
Working Example
Let me know if you need any help

Loop vibration in Swift

I'm playing a sound using AVPlayer. I'm trying to let the iPhone vibrate during this sound.
I'm able to create a vibration with:
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
But I'm having problems trying to loop this vibration if I use:
vibrationTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(ViewController.loopVibration), userInfo: nil, repeats: true)
#objc func loopVibration() {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
}
The func loopVibration() gets called every two seconds but it doesn't vibrate.
Not sure why this isn't working for you--the following sample works for me on my test device (iPhone X, iOS 11.2).
The ViewController sample below includes outlets for playing single sounds, playing single vibrations, looping sounds and looping vibrations. The "Chirp" sound is a 1s wav file.
Note that since this is a sample, the code below doesn't dispose of the system sounds, nor invalidate timers if the ViewController is hidden. Make sure to manage your system resources appropriately if you adapt this.
//
// ViewController.swift
//
import UIKit
import AudioToolbox
class ViewController: UIViewController {
static let soundId: SystemSoundID? = {
guard let soundURL = Bundle.main.url(forResource: "Chirp", withExtension: "wav") else {
return nil
}
var soundId: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &soundId)
return soundId
}()
static let timerInterval: TimeInterval = 2
var soundTimer: Timer?
var vibrationTimer: Timer?
var playCount = 0 {
didSet {
playCountLabel.text = String(playCount)
}
}
func invalidateTimers() {
if let vibrationTimer = vibrationTimer {
vibrationTimer.invalidate()
}
if let soundTimer = soundTimer {
soundTimer.invalidate()
}
}
#IBOutlet weak var playCountLabel: UILabel!
#IBAction func playSound(_ sender: Any) {
if let soundId = ViewController.soundId {
AudioServicesPlaySystemSound(soundId)
playCount += 1
}
}
#IBAction func loopSound(_ sender: Any) {
invalidateTimers()
soundTimer = Timer.scheduledTimer(timeInterval: ViewController.timerInterval, target: self, selector: #selector(playSound(_:)), userInfo: nil, repeats: true)
}
#IBAction func vibrate(_ sender: Any) {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
playCount += 1
}
#IBAction func loopVibrate(_ sender: Any) {
invalidateTimers()
soundTimer = Timer.scheduledTimer(timeInterval: ViewController.timerInterval, target: self, selector: #selector(vibrate(_:)), userInfo: nil, repeats: true)
}
#IBAction func cancelAll(_ sender: Any) {
invalidateTimers()
}
}
It's not permitted to use continuous vibration, your app might be rejected. If you still want to do it you can try something like this:
func vibrate() {
for i in 0...13 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(Double(i) * 0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
self.vibrate()
})
}
}

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)
}

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)
}
}
}

Audio playback progress as UISlider in Swift

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)
}

Resources