A simple MP3 converter for iOS

iPod Touch

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.

Frameworks

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.

Build Settings

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

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.