For a piece of our project at work we needed to record audio in MP3 format. The builtin AVFoundation
supports decoding MP3 format but it’s a proprietary format when it comes to encoding. So we decided to record with M4A format with the framework and the convert it to MP3 afterward. To do that I found a compiled library of mp3lame
with Objective-C wrappers, the repo was created in 2015. I cloned it, made some minor changes and created a small class which has a single method to do the conversion. This function returns a Publisher
with <URL, Error>
type.
The original git, the old one, you can find it here https://github.com/lixing123/ExtAudioFileConverter. To create my project downloaded it and picked up files I wanted, which were, ExtAudioConverter.h
, ExtAudioConverter.m
, lame.h
and libmp3lame.a
. After dropping them into my project, Xcode asked me to create a bridging-header file, I happily agreed to its kind offer and imported the newly added ExtAudioConverter.h
to it:
/// MP3Converter-Bridging-Header.h
#include "ExtAudioConverter.h"
Next step was adding AudioToolbox
framework from (after selecting the target from the left pan) Frameworks, Libraries, and Embedded Content section in General tab.
The last step to setup the environment was adding Library Search Path in Build Settings. Since I moved the files into a folder named Components, my path looked like this $(PROJECT_DIR)/Components
.
Next step was modifying wrappers -(void)validateInput:(ExtAudioConverterSettings*)settings
function because it was not returning a value if the settings were not compatible, it was just exiting the function. I changed the return type to bool
so I could check the retuned value and decide if the call was successful or failed.
/// Original code
if (!valid) {
NSLog(@"the file format and data format pair is not valid");
exit(-1);
}
/// Modified to:
if (!valid) {
NSLog(@"The file format and data format pair is not valid");
return false;
} else {
return true;
}
The next step was creating this small converter class which is self explanatory:
final class MP3Converter {
enum MP3ConverterError: Error, LocalizedError {
case invalidSettings
var errorDescription: String? {
switch self {
case .invalidSettings:
return "The settings are not correct for output."
}
}
}
func convert(input: URL, output: URL) -> AnyPublisher<URL, Error> {
let converter = ExtAudioConverter()
converter.inputFile = input.path
converter.outputFile = output.path
converter.outputFileType = kAudioFileMP3Type
/// To convert to MP3 you should set the outputFormatID to kAudioFormatMPEGLayer3, the default value will fail.
converter.outputFormatID = kAudioFormatMPEGLayer3
let result = converter.convert()
if result {
return Just(output)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return Fail(error: MP3ConverterError.invalidSettings)
.eraseToAnyPublisher()
}
}
}
And here you, the usage example:
import UIKit
import AVFoundation
import Combine
class ViewController: UIViewController {
var player: AVPlayer?
var playerItem: AVPlayerItem?
var disposable = Set<AnyCancellable>()
@IBOutlet weak var convertButton: UIButton!
@IBOutlet weak var statusLabel: UILabel!
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
override func viewDidLoad() {
super.viewDidLoad()
statusLabel.numberOfLines = 0
statusLabel.lineBreakMode = .byWordWrapping
}
@IBAction func convertAction(_ sender: UIButton) {
self.statusLabel.text = "Converting..."
let input = Bundle.main.url(forResource: "sample4", withExtension: "m4a")!
let output = getDocumentsDirectory().appendingPathComponent("converted.mp3")
let converter = MP3Converter()
convertButton.isEnabled = false
/// Run it in a different queue and update the UI when it's done executing, otherwise your UI will freeze.
DispatchQueue.global(qos: .userInteractive).async {
converter.convert(input: input, output: output)
.receive(on: DispatchQueue.global())
.sink(receiveCompletion: { result in
DispatchQueue.main.async {
if case .failure(let error) = result {
self.statusLabel.text = "Conversion failed: \n\(error.localizedDescription)"
self.convertButton.isEnabled = true
}
}
}, receiveValue: { result in
DispatchQueue.main.async {
let playerItem: AVPlayerItem = AVPlayerItem(url: result)
self.player = AVPlayer(playerItem: playerItem)
self.player?.play()
self.statusLabel.text = "File saved as converted.mp3 in \nthe documents directory."
}
}).store(in: &self.disposable)
}
}
}
You can download the sourcecode from github https://github.com/maysamsh/Swift-MP3Converter