iOS: trimming audio files with Swift
iOS: trimming audio files
I’ve written about how to record audio on iOS using Swift.
But, how do you trim the recording?
Introduction
One way to trim an audio file is to use AVFoundation’s AVAssetExportSession. You create an export session instance, set its parameters, and then tell it to export the asset (your audio file) according to those parameters.
In the recorder project, I saved the URL of the recording in the soundFileURL instance variable. To use AVAssetExportSession we need to create an AVAsset from it. Here is a simple action that creates the asset and then call the export function I will discuss next.
1 2 3 4 5 6 7 8 |
@IBAction func trim() { if let asset = AVAsset.assetWithURL(self.soundFileURL) as? AVAsset { exportAsset(asset, fileName: "trimmed.m4a") } // Swift 4 update: // let asset = AVAsset(url: self.soundFileURL) } |
Now to define the export func.
You create the exporter from your asset and desired file format. Here I’m using the Apple lossless format.
Then I set the exporter’s outputURL property to a file URL in the documents directory. This will be the location of the trimmed audio file.
I create a core media time range using CMTimeRangeFromTimeToTime that specifies the time offsets for the beginning and ending for the trimmed file. Here I just hard code the values, but of course you’d use a slider or a waveform view to choose the time boundaries.
While you’re there, you can also specify an AVMutableAudioMix for the volume. You can even specify a volume ramp.
Once the exporter’s properties are set, you call exportAsynchronouslyWithCompletionHandler to do the actual work. You can check the status of the export in the completion handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func exportAsset(asset:AVAsset, fileName:String) { let documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] let trimmedSoundFileURL = documentsDirectory.URLByAppendingPathComponent(fileName) print("saving to \(trimmedSoundFileURL.absoluteString)") let filemanager = NSFileManager.defaultManager() if filemanager.fileExistsAtPath(trimmedSoundFileURL.absoluteString) { println("sound exists") } let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) exporter.outputFileType = AVFileTypeAppleM4A exporter.outputURL = trimmedSoundFileURL let duration = CMTimeGetSeconds(asset.duration) if (duration < 5.0) { println("sound is not long enough") return } // e.g. the first 5 seconds let startTime = CMTimeMake(0, 1) let stopTime = CMTimeMake(5, 1) let exportTimeRange = CMTimeRangeFromTimeToTime(startTime, stopTime) exporter.timeRange = exportTimeRange // do it exporter.exportAsynchronouslyWithCompletionHandler({ switch exporter.status { case AVAssetExportSessionStatus.Failed: println("export failed \(exporter.error)") case AVAssetExportSessionStatus.Cancelled: println("export cancelled \(exporter.error)") default: println("export complete") } }) } |
Or using Swift 4 syntax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
func exportAsset(_ asset: AVAsset, fileName: String) { print("\(#function)") let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let trimmedSoundFileURL = documentsDirectory.appendingPathComponent(fileName) print("saving to \(trimmedSoundFileURL.absoluteString)") if FileManager.default.fileExists(atPath: trimmedSoundFileURL.absoluteString) { print("sound exists, removing \(trimmedSoundFileURL.absoluteString)") do { if try trimmedSoundFileURL.checkResourceIsReachable() { print("is reachable") } try FileManager.default.removeItem(atPath: trimmedSoundFileURL.absoluteString) } catch { print("could not remove \(trimmedSoundFileURL)") print(error.localizedDescription) } } print("creating export session for \(asset)") if let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) { exporter.outputFileType = AVFileType.m4a exporter.outputURL = trimmedSoundFileURL let duration = CMTimeGetSeconds(asset.duration) if duration < 5.0 { print("sound is not long enough") return } // e.g. the first 5 seconds let startTime = CMTimeMake(0, 1) let stopTime = CMTimeMake(5, 1) exporter.timeRange = CMTimeRangeFromTimeToTime(startTime, stopTime) // do it exporter.exportAsynchronously(completionHandler: { print("export complete \(exporter.status)") switch exporter.status { case AVAssetExportSessionStatus.failed: if let e = exporter.error { print("export failed \(e)") } case AVAssetExportSessionStatus.cancelled: print("export cancelled \(String(describing: exporter.error))") default: print("export complete") } }) } else { print("cannot create AVAssetExportSession for asset \(asset)") } } |
Groovy, huh?
Summary
To trim an audio (or video) file, use AVFoundation’s AVAssetExportSession.
Thanks for good example.
In the next post, could you make an example about Streaming music with Swift ?
I’m start to learn swift and build up my first app about Streaming Music Online. Then i get stuck in the process to get the duration of mp3 file from URL, still looking for the solution for it.
Regards,
You should play your streaming audio with AVPlayer. Instantiate AVPlayer with new player item, call play() func and when AVPlayer actually starts playing (isPlating == true) you can grab duration of your mp3 file.
Below is an extension to AVPlayer.
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
Hi. You mentioned that we can use slider or a waveform view to choose the time boundaries. I have been searching for a tutorial about that for a week and i couldn’t find anything. Could you please explain that too? i would really appreciate it.
UISliders are pretty well documented. I think a “range” slider would be better. NMRangeSlider is one open source example.
SCWaveformView is an open source waveform view.
Thank you very much!
Is it possible to change the pitch and rate in the method “exportAsset” ? If it is possible how should i do this?
Your code works fine. But could you tell why audio quality degrades?? Thanks.
i m trying to trim an mp3 file that is locally saved.
The code runs correctly as per my use case, let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough)
exportSession?.outputFileType = AVFileTypeQuickTimeMovie
I exported as the above formats.
But i keep getting the exported outfile duration as zero
Below is the code
i m trying to trim an mp3 file that is locally saved.
The code runs correctly as per my use case, let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough)
exportSession?.outputFileType = AVFileTypeQuickTimeMovie
I exported as the above formats.
But i keep getting the exported outfile duration as zero
Below is the code
func exportAsset(asset:AVAsset, fileName:String) {
// var audioUrl : URL = URL.init(string: “ipod-library://item/item.mp3?id=2928197610297287611”)!
// var avAsset: AVAsset = AVAsset(url: audioUrl)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let trimmedSoundFileURL = documentsDirectory.appendingPathComponent(fileName)
print(“saving to \(trimmedSoundFileURL.absoluteString)”)
let filemanager = FileManager.default
if filemanager.fileExists(atPath: trimmedSoundFileURL.absoluteString) {
print(“sound exists”)
}
let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough)
exportSession?.outputFileType = AVFileTypeQuickTimeMovie
exportSession?.outputURL = trimmedSoundFileURL
let duration = CMTimeGetSeconds(asset.duration)
if (duration Void in
if exportSession?.status == .completed {
print(“AVAssetExportSessionStatusCompleted”)
print(exportSession?.asset)
// play trimmed song
}
else if exportSession?.status == .failed {
// a failure may happen because of an event out of your control
// for example, an interruption like a phone call comming in
// make sure and handle this case appropriately
print(“AVAssetExportSessionStatusFailed”)
}
else {
print(“Export Session Status: \(exportSession?.status)”)
}
})
}
func playTrimmedSong() {
let url: URL = URL.init(string: “file:///var/mobile/Containers/Data/Application/B57F6319-9412-4238-900F-85CE0A4EB343/Documents/trimmed4.m4a”)!
let avAsset: AVAsset = AVAsset(url: url)
print(avAsset.duration.seconds)
}
</code
Hello,
I have an important voice memo recorded in 20171108 on my iphone. Unfortunally i didn’t save in Itunes or in iCloud
Today 20171202 i trimmed this voice memo
Trimmed version (20171202) look like overwrite original version (20171108)
I used application ibrowse to find voice memo file, but in the folder Recording i just have trimmed version not original version.
I try to use assetmanifest.plist but i’m not confortable with code and function you exposed.
Could you please help to fix my issue?
Look at the first example:
As you see, I’m specifying a new filename for the trimmed sound file.
Hello,
Thank you for quick reply.
I’m a novice in code so i need your help.
I need a procedure with step by step, could you help me please?
Where i put this code? In which file?
This code will be execute?
Below is what i understood:
1- In the forlder Recording add new file with this code:
@IBAction
func trim() {
if let asset = AVAsset.assetWithURL(self.soundFileURL) as? AVAsset {
exportAsset(asset, fileName: “20171108 195140.m4a”)
}
}
2- 20171108 195140.m4a it’s a trimmed filename
3- If i rename “20171108 195140.m4a” to “20171203_NewFileName.m4a” how i do the link between trimmed file and new file
Can i share my files with you please?
Thank you in advance
Hello Gene,
Could you please help me ?
Hi,
I’m using AudioKit and range slider for trimming audio. The audio in counted samples. Range slider in CGFloat, and trimming in calculated in seconds. The problem I keep facing is that seconds in the slider don’t correspond the samples in the audio. Can you advise the right way if solving it? Or may be recommend a good library for it. Thanks in advance.
Screenshot: https://ibb.co/jAyeT0
When I try to pull the file and play it using MediaPlayer, it plays the full music. I checked the file and I’m pretty sure it’s the trimmed one that is saved. Please help.
I just tried my example again and it does trim it. Try renaming it to make sure you’re using the right file.
Of course, you have the code I’m using, so if you find a bug, let me know!