AVAudioRecorder and AVAudioPlayer audio have different length - swift

I record with AVAudioRecorder, then pause it, print the currentTime, stop it and close it.
When I play the audio and print the duration of it, it is always different than the currentTime.
How is that possible? I don't do anything with the audio after pausing it besides saving it so how is there a different length?
recorder.pause()
print(recorder.currentTime) //e.g. 1.14
recorder.stop()
print(Manager.getAudioFileLength(url)) //e.g. 1.31
do {
try player = AVAudioPlayer(contentsOfURL: url)
}
catch let error as NSError {
NSLog("error: \(error)")
}
print(player.duration) //e.g. 1.25
And in Manager
class Manager {
static func getAudioFileLength(url : NSURL) -> Float64 {
let audioAsset = AVURLAsset(URL: url)
let audioDuration = audioAsset.duration
let audioDurationSeconds = CMTimeGetSeconds(audioDuration)
return audioDurationSeconds
}
}

From the Apple Documentation of AVAsset (superclass of AVURLAsset):
If providesPreciseDurationAndTiming is false, a best-available estimate of the duration is returned. ... If providesPreciseDurationAndTiming is true the absolute precision, at whatever processing cost that entails–and cost can be significant for certain media formats, such as .mpg.

Related

AVMutableComposition goes silent every once in a while

I'm trying to create a an audio loop in Swift and it works very well 9/10 times. But then suddenly, the 10th time (or so) the insertTimeRange function seems to fail in some way.
I can see that the player has the correct length, but instead of taking the full 60 seconds of audio from every loop, it just seems to take a very, very short part of it and loop every minute. Illustration of the problem with five loops:
.----------.----------.----------.----------.----------
(short but hearable audio = ., complete silence = -)
Unfortunately, it doesn't throw any error. Here's how I create the composition:
private func createComposition(audioURL: URL, minutesToLoop: UInt) -> AVMutableComposition? {
// Initiate new composition
let composition = AVMutableComposition()
// Add one audio track (channel) to the composition
let compositionAudioTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: 0)
// Create AVAsset from audio file (mp3 -> React Native bundle string -> URL -> AVAsset)
let asset = AVURLAsset(url: audioURL)
// Extract (now compatible) audio track from AVAsset
let track = asset.tracks(withMediaType: AVMediaType.audio)[0]
// Create a time range from 0 - 60 seconds that we can use to cut that out from the track
let timeRange = CMTimeRangeMake(start: .zero, duration: CMTimeMakeWithSeconds(60, preferredTimescale: 600))
if (compositionAudioTrack != nil) {
// Repeat for as many minutes as user specified in the app
for _ in 0...(minutesToLoop - 1) {
do {
// Take first 60 seconds from the audio file (timeRange, of: track) and paste over and over again exactly at the end of the track (at: composition.duration)
try compositionAudioTrack!.insertTimeRange(timeRange, of: track, at: composition.duration)
} catch {
print("FAILED TO MERGE AUDIO")
return nil
}
}
}
return composition
}
For future AVMutableComposition lovers, I found the solution. You needed to add a boolean to your asset options, like so:
let asset = AVURLAsset(url: audioURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])

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

How to fix 'appendingPathComponent' is unavailable: Use appendingPathComponent on URL error

I'm working on a old Swift tutorial (Swift 2.0) that's posted on Ray Wenderlich's web site (https://www.raywenderlich.com/2185-how-to-make-a-letter-word-game-with-uikit-and-swift-part-3-3) and I'm running into an error when I tried to re-setup a function called "preloadAudioEffects" in Swift 4.2 . The error? appendingPathComponent' is unavailable: Use appendingPathComponent on URL instead.
I've tried to rename the old Swift code [Ex: NSBundle to Bundle , stringByAppendingPathComponent to appendingPathComponent()], but I'm still running into some syntax issues due to my inexperience with Swift.
This is the original code:
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
//1 get the file path URL
let soundPath = NSBundle.mainBundle().resourcePath!.stringByAppendingPathComponent(effect)
let soundURL = NSURL.fileURLWithPath(soundPath)
//2 load the file contents
var loadError:NSError?
let player = AVAudioPlayer(contentsOfURL: soundURL, error: &loadError)
assert(loadError == nil, "Load sound failed")
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
}
}
And this is what I've tried to do via following the suggestions in Xcode:
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
//1 get the file path URL
let soundPath = Bundle.main.resourcePath!.appendingPathComponent(effect)
let soundURL = NSURL.fileURL(withPath: soundPath)
//2 load the file contents
var loadError:NSError?
let player = AVAudioPlayer(contentsOfURL: soundURL, error: &loadError)
assert(loadError == nil, "Load sound failed")
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
}
}
Get the full path to the sound file and convert it to a URL by using NSURL.fileURLWithPath().
Call AVAudioPlayer(contentsOfURL:error:) to load a sound file in an audio player.
Set the numberOfLoops to zero so that the sound won’t loop at all. Call prepareToPlay() to preload the audio buffer for that sound.
Finally, save the player object in the audio dictionary, using the name of the file as the dictionary key.
Just replace resourcePath with resourceURL
let soundURL = Bundle.main.resourceURL!.appendingPathComponent(effect)
and you have to wrap the AVAudioPlayer initializer in a try block
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
let soundURL = Bundle.main.resourceURL!.appendingPathComponent(effect)
//2 load the file contents
do {
let player = try AVAudioPlayer(contentsOf: soundURL)
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
} catch { print(error) }
}
}

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

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

How to record a video and make it slow motion

I am working on an iPhone app for school and need some help. The app should record video, make it slow motion (about 2x), then save it to the photo library. So far I have everything except how to make the video slow motion. I know it can be done as there is already an app in the App Store that does it.
How can I take a video I've saved to a temp url and adjust the speed before saving it to the photo library?
If you need to export your video then you need to use the AVMutableComposition Class
Then add your video as an AVAsset to an AVMutableComposition and scale it with:
- (void)scaleTimeRange:(CMTimeRange)timeRange toDuration:(CMTime)duration
Finally you export it using AVAssetExportSession Class
I written a code that makes your video in "slow motion" and saves it in Photos Library. "Main Thing This Code Works In Swift 5". Creating "Slow motion" video in iOS swift is not easy, that I came across many "slow motion" that came to know not working or some of the codes in them are depreciated. And so I finally figured a way to make slow motion in Swift.
This code can be used for 120fps are greater than that too. Just add the url of your video and make it slow
Here is the "code snippet I created for achieving slow motion"
func slowMotion(pathUrl: URL) {
let videoAsset = AVURLAsset.init(url: pathUrl, options: nil)
let currentAsset = AVAsset.init(url: pathUrl)
let vdoTrack = currentAsset.tracks(withMediaType: .video)[0]
let mixComposition = AVMutableComposition()
let compositionVideoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
let videoInsertError: Error? = nil
var videoInsertResult = false
do {
try compositionVideoTrack?.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.duration),
of: videoAsset.tracks(withMediaType: .video)[0],
at: .zero)
videoInsertResult = true
} catch let videoInsertError {
}
if !videoInsertResult || videoInsertError != nil {
//handle error
return
}
var duration: CMTime = .zero
duration = CMTimeAdd(duration, currentAsset.duration)
//MARK: You see this constant (videoScaleFactor) this helps in achieving the slow motion that you wanted. This increases the time scale of the video that makes slow motion
// just increase the videoScaleFactor value in order to play video in higher frames rates(more slowly)
let videoScaleFactor = 2.0
let videoDuration = videoAsset.duration
compositionVideoTrack?.scaleTimeRange(
CMTimeRangeMake(start: .zero, duration: videoDuration),
toDuration: CMTimeMake(value: videoDuration.value * Int64(videoScaleFactor), timescale: videoDuration.timescale))
compositionVideoTrack?.preferredTransform = vdoTrack.preferredTransform
let dirPaths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).map(\.path)
let docsDir = dirPaths[0]
let outputFilePath = URL(fileURLWithPath: docsDir).appendingPathComponent("slowMotion\(UUID().uuidString).mp4").path
if FileManager.default.fileExists(atPath: outputFilePath) {
do {
try FileManager.default.removeItem(atPath: outputFilePath)
} catch {
}
}
let filePath = URL(fileURLWithPath: outputFilePath)
let assetExport = AVAssetExportSession(
asset: mixComposition,
presetName: AVAssetExportPresetHighestQuality)
assetExport?.outputURL = filePath
assetExport?.outputFileType = .mp4
assetExport?.exportAsynchronously(completionHandler: {
switch assetExport?.status {
case .failed:
print("asset output media url = \(String(describing: assetExport?.outputURL))")
print("Export session faiied with error: \(String(describing: assetExport?.error))")
DispatchQueue.main.async(execute: {
// completion(nil);
})
case .completed:
print("Successful")
let outputURL = assetExport!.outputURL
print("url path = \(String(describing: outputURL))")
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL!)
}) { saved, error in
if saved {
print("video successfully saved in photos gallery view video in photos gallery")
}
if (error != nil) {
print("error in saing video \(String(describing: error?.localizedDescription))")
}
}
DispatchQueue.main.async(execute: {
// completion(_filePath);
})
case .none:
break
case .unknown:
break
case .waiting:
break
case .exporting:
break
case .cancelled:
break
case .some(_):
break
}
})
}
slowmoVideo is an OSS project which appears to do this very nicely, though I don't know that it would work on an iPhone.
It does not simply make your videos play at 0.01× speed. You can
smoothly slow down and speed up your footage, optionally with motion
blur. How does slow motion work? slowmoVideo tries to find out where
pixels move in the video (this information is called Optical Flow),
and then uses this information to calculate the additional frames.