CHICIO CODING

Dirty clean code. Creative Stuff. Stuff.

Mp3ID3Tagger: a native macOS app to edit the ID3 tag of your mp3 files written using RxSwift and RxCocoa

The third of a short series of post in which I describe my two latest project: ID3TagEditor and Mp3ID3Tagger. In this post I will talk about Mp3ID3Tagger, a macOS application to edit id3 tag of your mp3 files.


In this previous post I described the reason why I develop Mp3ID3Tagger, a macOS app to edit the id3 tag of your mp3 files that leverage on the power of ID3TagEditor. Below you can find the app logo.

MP3ID3Tagger macOS app RxSwift

So how did I develop MP3ID3Tagger? I was about to start the development following the classic approach to develop an app on every Apple OS: Model View Controller and plain Swift. But then I though: “This is the perfect project to test one of the last programming technique I recently learned: Reactive Programming/Reaactive Extensions with RxSwift and RxCocoa!!!!!! In this way I can also try to use a different architectural pattern: the Model View ViewModel (MVVM):sunglasses:. What kind of architectural pattern is the MVVM? What are Reactive Programming, Reactive Extensions, RxSwift and RxCocoa???
Let’s start from the first one. The MVVM is an architectural pattern invented by the Microsoft software engineers Ken Cooper and Ted Peters. As for other architecture patterns I described in the past, the MVVM is useful to clearly separate the UI development from the business logic. The main components of the MVVM are:

  • the Model, that usually represents the business logic of the application.
  • the View, as in the other architectural pattern, the view is the structure, layout, and appearance of what a user sees on the screen.
  • the View model, that usually represents an abstraction of the view exposing public properties and commands.
  • the Binder interprets bindings defined in the View, observes the View Model for changes in state and updates the View and finally observes the View for changes in state and updates the View Model.

From the definition above we see that the MVVM needs something to bind the view to the view model in a platform independent way. This is why we need RxSwift, RxCocoa and Reactive Extensions (usually called ReactiveX). What are they? Let’s see some quote for the definitions:

Reactive Extensions (also known as ReactiveX or Rx) is a set of tools allowing imperative programming languages to operate on sequences of data regardless of whether the data is synchronous or asynchronous. It provides a set of sequence operators that operate on each item in the sequence. …. ReactiveX is API for asynchronous programming with observable streams … RxSwift is the Swift version of ReactiveX (Rx) …. RxCocoa is a framework that helps make Cocoa APIs used in iOS and OS X easier to use with reactive techniques ….

The main components of RxSwift are:

  • Observables, that are something which emit notifications of change, and Observers, that are something which subscribe to an Observable, in order to be notified when it has changed
  • Subjects, that are entity that act both as an Observable and as an Observer
  • Operator, that are basically functions that work on Observable and return Observable

So RxSwift and RxCocoa let us create an abstraction from the platform specific UI implementation and let us implement our ViewModel by working in an event-driven way: the ViewModel only works with streams of data that comes from Observable and Subjects of RxSwift. RxCocoa gives us an abstraction over Cocoa and Cocoa Touch specific components and let us work with generic observable UI component. This basically means that:

  • RxSwift and RxCocoa are our Binder of the MVVM
  • the Various View and View Controllers are the View of the MVVM
  • the ID3TagEditor will be the Model of the MVVM
  • the ViewModel will connect the View and the ID3TagEditor Model in a platform UI independent way

With this architecture we can also think about using the same Model and ViewModel on different platform. So if in the future I will develop an iOS version of Mp3ID3Tagger, I will only have to develop the View part. So let’s start to see how I implemented Mp3ID3Tagger, the app subject of this post. Let’s start from the UI to see how MP3ID3Tagger does look like. The app has only one screen where the user can input its the data he/she want to insert into the tag. There is a button on the left to select the cover and all the textual/numeric values on the left. The values that could be set from a list are implemented as NSPopUpButton components.

MP3ID3Tagger interface

The first building block is the ViewModel base class. This class is useful to centralize the setup of a disposeBag. The DisposeBag it’s an RxSwift component that keeps a reference to all the Disposable you add to it. The Observable are Disposable, so you can add them to it to have an ARC-like behaviour: when the DisposeBag will be released all the Disposable instances it keeps will be released as well. So by having the ViewModel base class all the ViewModel will have a disposeBag by default where they will add their disposables. As we have seen before the app just one screen, so there’s just one ViewModel subclass to represent that screen, the Mp3ID3TaggerViewModel class. This class has 4 properties:

  • id3TagReader, of type ID3TagReader. This class has the responsibility to read a tag from an mp3 file when an openAction occurs. So ID3TagReader will be a subscriber of the openAction observable.
  • id3TagWriter, of type ID3TagWriter. This class has the responsibility to save a new tag to the mp3 file currently opened (the last openAction value) when a saveAction occurs. So ID3TagWriter will be a subscriber of the saveAction observable.
  • form, of type Form. This class has the responsibility to fill the fields of the form on the UI with values of the ID3tag read by the id3TagReader when an mp3 file has been opened. It has also the responsibility to collect the data contained in the form so that the id3TagWriter can write them when a saveAction occurs.
  • saveResult, of type PublishSubject<Bool>. This subject publishes the result of a save action made by the id3TagWriter.
class Mp3ID3TaggerViewModel: ViewModel {
    let id3TagReader: ID3TagReader
    let id3TagWriter: ID3TagWriter
    let form: Form
    let saveResult: PublishSubject<Bool>
    
    init(openAction: Observable<String>, saveAction: Observable<Void>) {
        self.id3TagReader = ID3TagReader(id3TagEditor: ID3TagEditor(), openAction: openAction)
        self.id3TagWriter = ID3TagWriter(id3TagEditor: ID3TagEditor(), saveAction: saveAction)
        self.form = Form()
        self.saveResult = PublishSubject<Bool>()
        super.init()

        id3TagReader.read { [unowned self] id3Tag in
            self.form.fillFields(using: id3Tag)
        }
        
        id3TagWriter.write(input: Observable.combineLatest(form.readFields(), openAction)) { result in
            self.saveResult.onNext(result)
        }
    }
} 

Now we can see the details of all these collaborators of our view model. Let’s start from the ID3TagReader. This class keeps a reference to an instance of the ID3TagEditor. Its main function is read(_ finish: @escaping (ID3Tag?) -> ()). In this function there is the subscribe to the openAction observable received at construction time (passed by the Mp3ID3TaggerViewModel). Each new value received from the openAction is a path to a new mp3 file. This path is passed to the ID3TagEditor instance that read of the ID3 tag of the song. If everything goes well, the tag is returned to the caller by using the finish closure. If you remember the Mp3ID3TaggerViewModel class, in this finish closure the form class is called that execute the fill of the fields (we will see below how it does this operation).

class ID3TagReader {
    private let id3TagEditor: ID3TagEditor
    private let openAction: Observable<String>
    private let disposeBag: DisposeBag
    
    init(id3TagEditor: ID3TagEditor, openAction: Observable<String>) {
        self.id3TagEditor = id3TagEditor
        self.openAction = openAction
        self.disposeBag = DisposeBag()
    }
    
    func read(_ finish: @escaping (ID3Tag?) -> ()) {
        openAction.subscribe(onNext: { [unowned self] path in
            do {
                let id3Tag = try self.id3TagEditor.read(from: path)
                finish(id3Tag)
            } catch {
                finish(nil)
            }
        }).disposed(by: disposeBag)
    }
}

Then we have the ID3TagWriter class. Like the ID3TagReader, this class keeps a reference to an instance of the ID3TagEditor. Its main function is write(input: Observable<(ID3Tag, String)>, _ finish: @escaping (Bool) -> ()). This function takes two parameters:

  • input of type Observable<(ID3Tag, String)>. This is an observable on a tuple composed by the path of an mp3 file and an ID3 tag
  • finish of type (Bool) -> ()

Inside this function there’s the subscription to the saveAction observable received at construction time from the Mp3ID3TaggerViewModel class. This observable is combined with the input observable received as parameter and described above and a new subscription to the result of the combination is created: each time we receive a path to an mp3 file, an ID3 tag and a save action is triggered the ID3TagEditor instance is used to write the ID3 tag to the mp3 file. The called of the write function of the ID3TagWriter is notified of the result of the operation by calling the finish operation.

class ID3TagWriter {
    private let id3TagEditor: ID3TagEditor
    private let saveAction: Observable<Void>
    private let disposeBag: DisposeBag
    
    init(id3TagEditor: ID3TagEditor, saveAction: Observable<Void>) {
        self.id3TagEditor = id3TagEditor
        self.saveAction = saveAction
        self.disposeBag = DisposeBag()
    }
    
    func write(input: Observable<(ID3Tag, String)>, _ finish: @escaping (Bool) -> ()) {
        saveAction
            .withLatestFrom(input)
            .subscribe(onNext: { [unowned self] event in
                do {
                    try self.id3TagEditor.write(tag: event.0, to: event.1)
                    finish(true)
                } catch {
                    finish(false)
                }
            })
            .disposed(by: disposeBag)
    }
}

Now let’s see the Form class and its collaborators. This class has 5 collaborators. Each collaborator represents a subset of the form fields. This fields are represented as Variable subject of the specific type of the fields. In this way we are able to publish new values (by using the value property) to this observable and at the same time observe their values. In fact in this class there are two functions:

  • readFields(), that creates an observable from the fields observables by combining them using the Rx operator combineLatest
  • fillFields(using id3Tag: ID3Tag?), that sets the value of the fields observables with the received id3 tag (read by the ID3TagReader when an mp3 file has been opened)

Below you can find the Form class with all the implementations also for its collaborators. In this way it’s easy to note what I stated above: the set of all the Variable fields of this classes matches the set of the UI components that we saw in the screenshot of the app that you saw above. One last important thing to note: the class AttachedPictureField forces the type of the attached picture to be saved to FrontCover. In this way the ID3TagEditor will write the ID3 tag with the correct data to display the album cover on my renault clio!!! :relieved:

class Form {
    let basicSongFields: BasicSongFields
    let versionField: VersionField
    let trackPositionInSetFields: TrackPositionInSetFields
    let genreFields: GenreFields
    let attachedPictureField: AttachedPictureField
    
    init() {
        self.basicSongFields = BasicSongFields()
        self.versionField = VersionField()
        self.trackPositionInSetFields = TrackPositionInSetFields()
        self.genreFields = GenreFields()
        self.attachedPictureField = AttachedPictureField()
    }
    
    func readFields() -> Observable<ID3Tag> {
        return Observable.combineLatest(
            versionField.validVersion,
            basicSongFields.observe(),
            trackPositionInSetFields.trackPositionInSet,
            genreFields.genre,
            attachedPictureField.observeAttachPictureCreation()
        ) { (version, basicFields, trackPositionInSet, genre, image) -> ID3Tag in
            return ID3Tag(
                version: version,
                artist: basicFields.artist,
                albumArtist: basicFields.albumArtist,
                album: basicFields.album,
                title: basicFields.title,
                year: basicFields.year,
                genre: genre,
                attachedPictures: image,
                trackPosition: trackPositionInSet
            )
        }
    }
    
    func fillFields(using id3Tag: ID3Tag?) {
        fillBasicFieldsUsing(id3Tag: id3Tag)
        fillVersionFieldUsing(id3Tag: id3Tag)
        fillTrackPositionFieldsUsing(id3Tag: id3Tag)
        fillGenreFieldsUsing(id3Tag: id3Tag)
        fillAttachedPictureUsing(id3Tag: id3Tag)
    }
    
    private func fillBasicFieldsUsing(id3Tag: ID3Tag?) {
        basicSongFields.title.value = id3Tag?.title
        basicSongFields.artist.value = id3Tag?.artist
        basicSongFields.album.value = id3Tag?.album
        basicSongFields.albumArtist.value = id3Tag?.albumArtist
        basicSongFields.year.value = id3Tag?.year
    }
    
    private func fillVersionFieldUsing(id3Tag: ID3Tag?) {
        if let version = id3Tag?.properties.version.rawValue {
            versionField.version.value = Int(version)
        }
    }
    
    private func fillTrackPositionFieldsUsing(id3Tag: ID3Tag?) {
        if let trackPosition = id3Tag?.trackPosition {
            trackPositionInSetFields.trackPosition.value = String(trackPosition.position)
            fillTotalTracksFieldUsing(id3Tag: id3Tag)
        }
    }
    
    private func fillTotalTracksFieldUsing(id3Tag: ID3Tag?) {
        if let totalTracks = id3Tag?.trackPosition?.totalTracks {
            trackPositionInSetFields.totalTracks.value = String(totalTracks)
        }
    }
    
    private func fillGenreFieldsUsing(id3Tag: ID3Tag?) {
        if let genre = id3Tag?.genre {
            genreFields.genreIdentifier.value = genre.identifier?.rawValue
            genreFields.genreDescription.value = genre.description
        }
    }
    
    private func fillAttachedPictureUsing(id3Tag: ID3Tag?) {
        if let validAttachedPictures = id3Tag?.attachedPictures, validAttachedPictures.count > 0 {
            attachedPictureField.attachedPicture.value = ImageWithType(data: validAttachedPictures[0].art,
                                                                       format: validAttachedPictures[0].format)
        }
    }
}

....

typealias BasicSongFieldsValues = (title: String?, artist: String?, album: String?, albumArtist: String?, year: String?)

class BasicSongFields {
    let title: Variable<String?>
    let artist: Variable<String?>
    let album: Variable<String?>
    let albumArtist: Variable<String?>
    let year: Variable<String?>
    
    init() {
        self.title = Variable<String?>(nil)
        self.artist = Variable<String?>(nil)
        self.album = Variable<String?>(nil)
        self.albumArtist = Variable<String?>(nil)
        self.year = Variable<String?>(nil)
    }
    
    func observe() -> Observable<BasicSongFieldsValues> {
        return Observable.combineLatest(
            title.asObservable(),
            artist.asObservable(),
            album.asObservable(),
            albumArtist.asObservable(),
            year.asObservable()
        ) { title, artist, album, albumArtist, year in
            return BasicSongFieldsValues(title: title,
                                         artist: artist,
                                         album: album,
                                         albumArtist: albumArtist,
                                         year: year)
        }
    }
}

....

class VersionField {
    let version: Variable<Int?>
    let validVersion: Observable<ID3Version>

    init() {
        self.version = Variable<Int?>(3)
        self.validVersion = version.asObservable().map { (versionSelected) -> ID3Version in
            return ID3Version(rawValue: UInt8(versionSelected ?? 0)) ?? .version3
        }
    }
}

....

class TrackPositionInSetFields {
    let trackPosition: Variable<String?>
    let totalTracks: Variable<String?>
    let trackPositionInSet: Observable<TrackPositionInSet?>
    
    init() {
        self.trackPosition = Variable<String?>(nil)
        self.totalTracks = Variable<String?>(nil)
        self.trackPositionInSet = Observable.combineLatest(
            trackPosition.asObservable(),
            totalTracks.asObservable()
        ) { (trackPosition, totalTracks) -> TrackPositionInSet? in
            if let validTrackPositionAsString = trackPosition,
                let validTrackPosition = Int(validTrackPositionAsString) {
                return TrackPositionInSet(position: validTrackPosition,
                                          totalTracks: TrackPositionInSetFields.convertToNumber(totalTracks: totalTracks))
            }
            return nil
        }
    }
    
    private static func convertToNumber(totalTracks: String?) -> Int? {
        if let validTotalTracks = totalTracks {
            return Int(validTotalTracks)
        }
        return nil
    }
}

....

class GenreFields {
    let genreIdentifier: Variable<Int?>
    let genreDescription: Variable<String?>
    let genre: Observable<Genre?>
    
    init() {
        self.genreIdentifier = Variable<Int?>(nil)
        self.genreDescription = Variable<String?>(nil)
        self.genre = Observable.combineLatest(
            genreIdentifier.asObservable(),
            genreDescription.asObservable()
        ) { (genreIdentifier, genreDescription) -> Genre? in
            if let validGenre = genreIdentifier,
                let validId3Genre = ID3Genre(rawValue: validGenre) {
                return Genre(genre: validId3Genre, description: genreDescription)
            }
            return nil
        }
    }
}

....

class AttachedPictureField {
    let attachedPicture: Variable<ImageWithType?>

    init() {
        self.attachedPicture = Variable<ImageWithType?>(nil)
    }

    func observeAttachPictureCreation() -> Observable<[AttachedPicture]?> {
        return attachedPicture
            .asObservable()
            .map({ imageWithType in
                if let validImageWithType = imageWithType {
                    return [AttachedPicture(art: validImageWithType.data,
                                            type: .FrontCover,
                                            format: validImageWithType.format)]
                } else {
                    return nil
                }
            })
    }
}

Now it’s time to see the view controller of the app that basically corresponds to the View of the MVVM. Its name is Mp3ID3TaggerViewController. This controller will implement a protocol I defined: the BindableView protocol. This protocol represents the View part in the MVVM architecture. This protocol must be implemented only by subclasses of the NSViewController. The protocol contains a property and a function. The viewModel forces the class (the View) to have a property that represents its ViewModel. The function bindViewModel is where the View and the View model are bound together. The bindViewModel must be called inside one the lifecycle methods of the NSViewController.

protocol BindableView where Self: NSViewController {
    associatedtype ViewModelType
    var viewModel: ViewModelType! { get set }
    func bindViewModel()
}

If we look at the implementation of the bindViewModel method, we can see where something “magical” is happening :crystal_ball:: an instance of Mp3ID3TaggerViewModel class is created and the UI components that represents the various field of the form are bounded to the view model fields by using the custom opertator <->. So this operator let us define the what is called two way binding or bidirectional binding using RxSwift:

  • each Variable field of the view model is bounded to a field on the UI. This basically means that each value we set a in the value property of a Variable field will be displayed on the UI Cocoa specific field.

  • each value inserted in the UI Cocoa specific field will be set in the corresponding Variable field on the view model.

In this way the View Model is completely decoupled from the View part (in this case the NSViewController). This means that we can reuse the same ViewModel to create other versions of Mp3ID3Tagger for other platforms. This is absolutely fantastic!!!!! :heart_eyes::relaxed:. Last but not least in the controller we have also some other functions:

  • open(_ sender: Any?) and save(_ sender: Any?) that manage the open an mp3 file and save of the same file
  • bindSaveAction() that observe the result of a save action
  • openImage(imageUrl: URL) and bindAttachedPictureField() that manage the bind and the subscription to an open action of an image to be used as front cover for the id3 tag.
infix operator <-> : DefaultPrecedence

func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
    let bindToUIDisposable = variable.asObservable()
        .bind(to: property)
    let bindToVariable = property
        .subscribe(onNext: { n in
            variable.value = n
        }, onCompleted:  {
            bindToUIDisposable.dispose()
        })
    
    return CompositeDisposable(bindToUIDisposable, bindToVariable)
}

....

class Mp3ID3TaggerViewController: NSViewController, BindableView {
    private let disposeBag: DisposeBag = DisposeBag()
    private let openAction: PublishSubject<String> = PublishSubject<String>()
    private let saveAction: PublishSubject<Void> = PublishSubject<Void>()
    private let stringToID3ImageExtensionAdapter = StringToID3ImageExtensionAdapter()
    var viewModel: Mp3ID3TaggerViewModel!
    @IBOutlet weak var versionPopUpbutton: NSPopUpButton!
    @IBOutlet weak var fileNameLabel: NSTextField!
    @IBOutlet weak var titleTextField: NSTextField!
    @IBOutlet weak var artistTextField: NSTextField!
    @IBOutlet weak var albumTextField: NSTextField!
    @IBOutlet weak var albumArtistField: NSTextField!
    @IBOutlet weak var yearTextField: NSTextField!
    @IBOutlet weak var trackPositionTextField: NSTextField!
    @IBOutlet weak var totalTracksTextField: NSTextField!
    @IBOutlet weak var genrePopUpMenu: NSPopUpButton!
    @IBOutlet weak var genreDescriptionTextField: NSTextField!
    @IBOutlet weak var imageSelectionButton: NSButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.bindViewModel()
    }
    
    func bindViewModel() {
        viewModel = Mp3ID3TaggerViewModel(openAction: openAction.asObservable(), saveAction: saveAction.asObservable())
        (titleTextField.rx.text <-> viewModel.form.basicSongFields.title).disposed(by: disposeBag)
        (artistTextField.rx.text <-> viewModel.form.basicSongFields.artist).disposed(by: disposeBag)
        (albumTextField.rx.text <-> viewModel.form.basicSongFields.album).disposed(by: disposeBag)
        (albumArtistField.rx.text <-> viewModel.form.basicSongFields.albumArtist).disposed(by: disposeBag)
        (yearTextField.rx.text <-> viewModel.form.basicSongFields.year).disposed(by: disposeBag)
        (versionPopUpbutton.rx.selectedItemTag <-> viewModel.form.versionField.version).disposed(by: disposeBag)
        (trackPositionTextField.rx.text <-> viewModel.form.trackPositionInSetFields.trackPosition).disposed(by: disposeBag)
        (totalTracksTextField.rx.text <-> viewModel.form.trackPositionInSetFields.totalTracks).disposed(by: disposeBag)
        (genrePopUpMenu.rx.selectedItemTag <-> viewModel.form.genreFields.genreIdentifier).disposed(by: disposeBag)
        (genreDescriptionTextField.rx.text <-> viewModel.form.genreFields.genreDescription).disposed(by: disposeBag)
        self.bindAttachedPictureField()
        self.bindSaveAction()
    }
    
    private func bindAttachedPictureField() {
        viewModel
            .form
            .attachedPictureField
            .attachedPicture
            .asObservable()
            .filter({ $0 != nil })
            .subscribe(onNext: { self.imageSelectionButton.image = NSImage(data: $0!.data) })
            .disposed(by: disposeBag)
        imageSelectionButton.rx.tap.subscribe(onNext: { tap in
            NSOpenPanel.display(in: self.view.window!,
                                fileTypes: ["png", "jpg", "jpeg"],
                                title: "Select an Image file",
                                onOkResponse: self.openImage)
        }).disposed(by: disposeBag)
    }
    
    private func bindSaveAction() {
        viewModel.saveResult
            .asObservable()
            .subscribe(onNext: { (result) in
                let alert = NSAlert()
                alert.addButton(withTitle: "Ok")
                alert.messageText = result ? "Mp3 saved correctly!" : "Error during save!"
                alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
            })
            .disposed(by: disposeBag)
    }
    
    private func openImage(imageUrl: URL) {
        if let image = try? Data(contentsOf: imageUrl) {
            let type = self.stringToID3ImageExtensionAdapter.adapt(format: imageUrl.pathExtension)
            self.viewModel.form.attachedPictureField.attachedPicture.value = ImageWithType(data: image, format: type)
            self.imageSelectionButton.image = NSImage(data: image)
        }
    }

    @IBAction func open(_ sender: Any?) {
        NSOpenPanel.display(in: self.view.window!,
                            fileTypes: ["mp3"],
                            title: "Select an MP3 file",
                            onOkResponse: {
                                self.openAction.onNext($0.path)
                                self.fileNameLabel.stringValue = $0.lastPathComponent
        })
    }
    
    @IBAction func save(_ sender: Any?) {
        saveAction.onNext(())
    }
} 

We’re done with Mp3ID3Tagger. I hope you liked my architectural choices and how I developed it by leveraging the power of RxSwift and RxCocoa :sunglasses::relieved:. Obviously don’t forget to see the official Mp3ID3Tagger repo
and obviously to download the Mp3ID3Tagger app from this link and use it!!! :heartpulse::sparkling_heart:

ID3TagEditor: a Swift framework to read and write ID3 tag of your mp3 files for macOS, iOS, tvOS and watchOS

The second of a short series of post in which I describe my two latest project: ID3TagEditor and Mp3ID3Tagger. In this post I will describe how I created ID3TagEditor.


In this previous post I described the reason why I developed ID3TagEditor, a swift library to edit ID3 tag of mp3 files with support for macOS, iOS, watchOS and tvOS. In this post I will described how I developed it. Below you can find the library logo.

ID3TagEditor logo

But before going deeper in the details of ID3TagEditor it useful to know how the ID3 tag standard works (you can find the full reference on the official site). The definition reported on it for the ID3 standard is:

An ID3 tag is a data container within an MP3 audio file stored in a prescribed format

This definition means that an ID3 tag is basically a chunk of information stored at the beginning of an mp3 file. The standard defines the format that any developer can use to read and write this information. Let’s see an example of an ID3 tag using a hex editor.

ID3 tag example

A tag is composed by an header and a series of frames. The tag header has a size of 10 bytes contains the following information (for both v2 and v3):

  • ID3 tag file identifier, 3 bytes, usually represented as “ID3”
  • tag version, 2 bytes, a couple of number that represent the major version and the revision version (e.g. 0x03 0x00)
  • flags, 1 bytes, contains three configurations flags represented as %abc00000 (bit to 1)
  • size, 4 bytes. Quoting the ID3 standard the size is:

the size of the complete tag after unsychronisation, including padding, excluding the header but not excluding the extended header. The ID3v2 tag size is encoded with four bytes where the most significant bit (bit 7) is set to zero in every byte, making a total of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is represented as $00 00 02 01. …. Only 28 bits(representing up to 256MB) are used in the size description…

ID3 tag header

A frame is composed of an header and a custom content. The frame header contains the following information, that change in size between versions:

  • frame id, 3 bytes in version 2 and 4 bytes in version 3
  • size, 3 bytes in version 2 and 4 bytes in version 3 that the describe the total size of the frame excluding the header
  • option flags, 2 bytes available only in version 3

So the frame header size is 10 bytes in version 3 and 6 bytes in version 2. After the header there is the custom specific frame flags/options and the frame content. Below you can find an example of a frame in a version 3 tag.

ID3 frame example

Last but not least at the end of the ID3 tag there are also 2 KB of offset (you can see it in the previous images, that series of endless 0x00 at the end of the tag :relieved:). How does ID3TagEditor read and write all this information? The main api of the framework are two simple methods:

/**
 Read the ID3 tag contained in the mp3 file.

 - parameter path: path of the mp3 file to be parsed.

 - throws: Could throw `InvalidFileFormat` if an mp3 file doesn't exists at the specified path.

 - returns: an ID3 tag or nil, if a tag doesn't exists in the file.
 */
public func read(from path: String) throws -> ID3Tag?

/**
 Writes the mp3 to a new file or overwrite it with the new ID3 tag.

 - parameter tag: the ID3 tag that written in the mp3 file.
 - parameter path: path of the mp3 file where we will write the tag.
 - parameter newPath: path where the file with the new tag will be written. **If nil, the mp3 file will be overwritten**.
 If nothing is passed, the file will be overwritten at its current location.

 - throws: Could throw `TagTooBig` (tag size > 256 MB) or `InvalidTagData` (no data set to be written in the
 ID3 tag).
 */
public func write(tag: ID3Tag, to path: String, andSaveTo newPath: String? = nil) throws

So the ID3TagEditor framework has two main parts: one for read/parse an mp3 file and one for write an ID3 tag to the mp3 file.
Let’s start from the read/parsing part. The main entry point of the library is the class ID3TagParser that is instantiated from a ID3TagParserFactory. Its main function is the called parse. As the name suggest it parses the various frames. Before that there are three operation:

  • the version of the tag is extracted by a collaborator called ID3TagVersionParser
  • a check if a tag is available in the mp3 file loaded. This check is done by a collaborator named ID3TagPresence
  • the size of the tag is extracted by a collaborator called ID3TagSizeParser
....
 
func parse(mp3: Data) -> ID3Tag? {
    let version = tagVersionParser.parse(mp3: mp3 as Data)
    if (tagPresence.isTagPresentIn(mp3: mp3 as Data, version: version)) {
        let id3Tag = ID3Tag(version: version, size: 0)
        parseTagSizeFor(mp3: mp3 as NSData, andSaveInId3Tag: id3Tag)
        parseFramesFor(mp3: mp3 as NSData, id3Tag: id3Tag)
        return id3Tag
    }
    return nil
}

....

The parsing of each frame is done in the function parseFramesFor.

....
private func parseFramesFor(mp3: NSData, id3Tag: ID3Tag) {
    var currentFramePosition = id3TagConfiguration.headerSize();
    while currentFramePosition < id3Tag.properties.size {
        let frame = getFrameFrom(mp3: mp3, position: currentFramePosition, version: id3Tag.properties.version)
        frameContentParser.parse(frame: frame, id3Tag: id3Tag)
        currentFramePosition += frame.count;
    }
}

private func getFrameFrom(mp3: NSData, position: Int, version: ID3Version) -> Data {
    let frameSize = frameSizeParser.parse(mp3: mp3, framePosition: position, version: version)
    let frame = mp3.subdata(with: NSMakeRange(position, frameSize))
    return frame
}
....

How does the parsing for each frame work? How does ID3TagEditor recognize the correct frame and execute the correct parsing based on the frame type? The answer is inside the ID3FrameContentParser class, used inside the parseFramesFor(mp3: NSData, id3Tag: ID3Tag) function. This class uses the Command Pattern to launch the correct parsing operations for the current frame type. The list of frame parsing operations is stored inside inside a dictionary where the key is the FrameType enum. This enum generically identify the frame type, and is mapped to the correct ID3 frame identifier for each version in the ID3FrameConfiguration function frameTypeFor(identifier: frameIdentifier, version: version). As you can see below the extraction of the frame identifier is done in the getFrameTypeFrom(frame: Data, version: ID3Version) -> FrameType.

 class ID3FrameContentParser: FrameContentParser {
     private let frameContentParsingOperations: [FrameType : FrameContentParsingOperation]
     private var id3FrameConfiguration: ID3FrameConfiguration
 
     init(frameContentParsingOperations: [FrameType : FrameContentParsingOperation],
          id3FrameConfiguration: ID3FrameConfiguration) {
         self.frameContentParsingOperations = frameContentParsingOperations
         self.id3FrameConfiguration = id3FrameConfiguration
     }
 
     func parse(frame: Data, id3Tag: ID3Tag) {
         let frameType = getFrameTypeFrom(frame: frame, version: id3Tag.properties.version)
         if (isAValid(frameType: frameType)) {
             frameContentParsingOperations[frameType]?.parse(frame: frame, id3Tag: id3Tag)
         }
     }
 
     private func getFrameTypeFrom(frame: Data, version: ID3Version) -> FrameType {
         let frameIdentifierSize = id3FrameConfiguration.identifierSizeFor(version: version)
         let frameIdentifierData = [UInt8](frame.subdata(in: Range(0...frameIdentifierSize - 1)))
         let frameIdentifier = toString(frameIdentifier: frameIdentifierData)
         let frameType = id3FrameConfiguration.frameTypeFor(identifier: frameIdentifier, version: version)
         return frameType
     }
 
     private func isAValid(frameType: FrameType) -> Bool {
         return frameType != .Invalid
     }
 
     private func toString(frameIdentifier: [UInt8]) -> String {
         return frameIdentifier.reduce("") { (convertedString, byte) -> String in
             return convertedString + String(Character(UnicodeScalar(byte)))
         }
     }
 }

If we want to go deeper we can have a look at the ID3FrameContentParsingOperationFactory. This class initialize the classes used as command to parse the various type of frames. I will talk about their implementation details in other posts (because this classes contain a lot of cool swift stuff that I can use to write a lot of other posts :smirk:).

class ID3FrameContentParsingOperationFactory {
    static func make() -> [FrameType : FrameContentParsingOperation] {
        let paddingRemover = PaddingRemoverUsingTrimming()
        let id3FrameConfiguration = ID3FrameConfiguration()
        return [
            .Artist: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover, 
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.artist = frameContentWithoutPadding
            },
            .AlbumArtist: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.albumArtist = frameContentWithoutPadding
            },
            .Album: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.album = frameContentWithoutPadding
            },
            .Title: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.title = frameContentWithoutPadding
            },
            .AttachedPicture: AttachedPictureFrameContentParsingOperation(
                    id3FrameConfiguration: id3FrameConfiguration,
                    pictureTypeAdapter: ID3PictureTypeAdapter(
                            id3FrameConfiguration: ID3FrameConfiguration(),
                            id3AttachedPictureFrameConfiguration: ID3AttachedPictureFrameConfiguration()
                    )
            ),
            .Year: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.year = frameContentWithoutPadding
            },
            .Genre: ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.genre = ID3GenreStringAdapter().adapt(genre: frameContentWithoutPadding)
            },
            .TrackPosition : ID3FrameStringContentParsingOperation(
                    paddingRemover: paddingRemover,
                    id3FrameConfiguration: id3FrameConfiguration
            ) { (id3Tag: ID3Tag, frameContentWithoutPadding: String) in
                id3Tag.trackPosition = ID3TrackPositionStringAdapter().adapt(trackPosition: frameContentWithoutPadding)
            }
        ]
    }
}

Let’s see instead how ID3TagEditor write a new tag to an mp3 file. The creation of the tag is done by the ID3TagCreator class used inside the Mp3WithID3TagBuilder class, the one that execute the real write on the mp3 file with the new tag on disk. The main function of the ID3TagCreator class is create(id3Tag: ID3Tag) throws -> Data. Inside this function the frames are created from the data passed to the framework as an ID3Tag class. If all the the frame validation goes well a new tag header is created and again, if the tag header is valid (the size of the tag is valid), a new Data object is returned to the Mp3WithID3TagBuilder class and is written to the mp3 file.

class ID3TagCreator {
    private let id3FrameCreatorsChain: ID3FrameCreatorsChain
    private let uInt32ToByteArrayAdapter: UInt32ToByteArrayAdapter
    private let id3TagConfiguration: ID3TagConfiguration

    ....

    func create(id3Tag: ID3Tag) throws -> Data {
        var frames = id3FrameCreatorsChain.createFrames(id3Tag: id3Tag, tag: [UInt8]())
        if thereIsNotValidDataIn(frames: frames) {
            throw ID3TagEditorError.InvalidTagData
        }
        frames.append(contentsOf: createFramesEnd())
        let header = createTagHeader(contentSize: frames.count, id3Tag: id3Tag);
        let tag = header + frames
        if (isTooBig(tag: tag)) {
            throw ID3TagEditorError.TagTooBig
        }
        return Data(bytes: tag)
    }
    
    ....
}    

How are the frames data created? The answer is inside the ID3FrameCreatorsChain and the ID3FrameCreatorsChainFactory classes. The factory class creates a Chain of responsibility, where each subclass of the ID3FrameCreatorsChain class is a specialization with the responsibility to write a specific frame type. At the end of the chain an [Uint8] array is returned. This is basically an array of bytes, that is then converted into a Data object at the end of the create(id3Tag: ID3Tag) throws -> Data of the ID3TagCreator class (where also the tag header is added as we saw before). Below you can find the chain creation contained in the ID3FrameCreatorsChainFactory class (again, we will see the details of the various classes contained in the chain in other future posts :stuck_out_tongue_winking_eye: This framework contains too much cool swift stuff :flushed:) . One important thing to note: the ID3AttachedPicturesFramesCreator class is able to create attached picture frames that sets the type of the cover to one from the list defined in the ID3 standard. In this way I can use my ID3TagEditor framework to tag the mp3 with the correct data that I need to display the mp3 files cover on the media nav system of my clio!!! :relieved:

class ID3FrameCreatorsChainFactory {
    static func make() -> ID3FrameCreatorsChain {
        let paddingAdder = PaddingAdderToEndOfContentUsingNullChar()
        let frameConfiguration = ID3FrameConfiguration()
        let uInt32ToByteArrayAdapter = UInt32ToByteArrayAdapterUsingUnsafePointer()
        let frameContentSizeCalculator = ID3FrameContentSizeCalculator(
                uInt32ToByteArrayAdapter: uInt32ToByteArrayAdapter
        )
        let frameFlagsCreator = ID3FrameFlagsCreator()
        let frameFromStringUTF16ContentCreator = ID3FrameFromStringContentCreator(
                frameContentSizeCalculator: frameContentSizeCalculator,
                frameFlagsCreator: frameFlagsCreator,
                stringToBytesAdapter: ID3UTF16StringToByteAdapter(paddingAdder: paddingAdder,
                                                                  frameConfiguration: frameConfiguration)
        )
        let frameFromStringISO88591ContentCreator = ID3FrameFromStringContentCreator(
            frameContentSizeCalculator: frameContentSizeCalculator,
            frameFlagsCreator: frameFlagsCreator,
            stringToBytesAdapter: ID3ISO88591StringToByteAdapter(paddingAdder: paddingAdder,
                                                                 frameConfiguration: frameConfiguration)
        )
        let albumFrameCreator = ID3AlbumFrameCreator(
                frameCreator: frameFromStringUTF16ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let albumArtistCreator = ID3AlbumArtistFrameCreator(
                frameCreator: frameFromStringUTF16ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let artistFrameCreator = ID3ArtistFrameCreator(
                frameCreator: frameFromStringUTF16ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let titleFrameCreator = ID3TitleFrameCreator(
                frameCreator: frameFromStringUTF16ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let attachedPictureFrameCreator = ID3AttachedPicturesFramesCreator(
                attachedPictureFrameCreator: ID3AttachedPictureFrameCreator(
                        id3FrameConfiguration: frameConfiguration,
                        id3AttachedPictureFrameConfiguration: ID3AttachedPictureFrameConfiguration(),
                        frameContentSizeCalculator: frameContentSizeCalculator,
                        frameFlagsCreator: frameFlagsCreator
                )
        )
        let yearFrameCreator = ID3YearFrameCreator(
                frameCreator: frameFromStringISO88591ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let genreFrameCreator = ID3GenreFrameCreator(
                frameCreator: frameFromStringISO88591ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        let trackPositionFrameCreator = ID3TrackPositionFrameCreator(
                frameCreator: frameFromStringISO88591ContentCreator,
                id3FrameConfiguration: frameConfiguration
        )
        albumFrameCreator.nextCreator = albumArtistCreator
        albumArtistCreator.nextCreator = artistFrameCreator
        artistFrameCreator.nextCreator = titleFrameCreator
        titleFrameCreator.nextCreator = yearFrameCreator
        yearFrameCreator.nextCreator = genreFrameCreator
        genreFrameCreator.nextCreator = trackPositionFrameCreator
        trackPositionFrameCreator.nextCreator = attachedPictureFrameCreator
        return albumFrameCreator
    }
}

That’s it!!! This is the general structure of the ID3TagEditor framework. If you want to discover more about this framework you can have a look at my github repo and start to make some contribution :heart::purple_heart:. Obviously, you must also continue to read my blog and wait for the other posts about other implementation details I promised above (if you’re too lazy to go see by yourself :kissing_heart::satisfied:).

The birth of ID3TagEditor and Mp3ID3Tagger and my journey into the ID3 tag standard

The first of a short series of post in which I describe my two latest project: ID3TagEditor and Mp3ID3Tagger. In this post I will talk about why I started to develop them.


Recently I bought a new car. After a lot of searches I finally decided to buy the Renault Clio 2017 1.5 dci. I love this car. It has been a big step forward on my previous car. One of its most interesting feature is its media entertainment system: the Media Nav Evolution system. This system has a 7’’ touchscreen with map integration and a basic smartphone integration with Siri voice recognition and phone call support.

media nav clio

One of the thing that caught my attention was the possibility to start to listen to my mp3 collection while I’m driving (on my previous car I had a standard cd player). So I prepared an usb key with some of my mp3 and I started to listen to them. I suddenly made a great discovery: some of my songs were displayed on the touchscreen with information about the album and they were displaying the cover of the album!!!!! :heart_eyes:. I though: “Whoahh this is very cool!! I need to start to fill my mp3 with all this information. I want to see the cover of the album for each mp3 I have!!!!!!!”. This is exactly the moment where my journey into the development of ID3TagEditor and Mp3ID3Tagger started, but I was not yet aware of it :grin:. So I sat in front of my MacBook, I opened iTunes and I started to tag my mp3 files. I saved them on a usb key and I went to my car to test them. The result was the following:

mp3 no cover

What the hell is going on??!!?! :angry: The title and the album where displayed but the cover was not show on the screen. So I got back to my desk and I started to download some native macOS app that let the user edit what I discovered was called ID3 tag. None of them worked as expected. Then I found an app called Mp3Tag. This is Windows application that runs also on macOS using Wine. So I downloaded it and tried to tag some mp3. I put them on a usb key and then…

mp3 with cover

Mp3Tag was working as expected :relieved:. But then I started to ask myself: “What is doing Mp3Tag that the other native macOS application are not doing?”. The only way to discover the reason behind this mystery was to compare an mp3 tagged with Mp3Tag with an mp3 tagged with one of the other applications. So I opened with my favourite hex editor HexFiend an mp3 tagged with iTunes and another one tagged with Mp3Tag and I compared them…

mp3 compare itunes mp3tag

Yep, a single byte could make a big difference :open_mouth:. The fact is that the ID3 standard accept multiple types of attached picture for an mp3: front cover, back cover, icon, artist photo ecc. The type of picture is represented as a byte just after the MIME type in the attached picture frame of the ID3 standard. The problem is that iTunes and other “mp3 tagger” native macOS applications don’t let the user modify the type of the attached picture. All this application set the byte to 0x00 that in the ID3 standard corresponds to the “Other” cover type. But the Media Nav Evolution system of my Renault Clio is able to read only attached picture frames inserted in the tag with a specific type, for example 0x03 “Cover (Front)”, that is the default type inserted by Mp3Tag. The attached picture frames that have the 0x00 “Other” type are discarded :unamused:. My next question was: “How is it possible that there’s not a native macOS app? There are only cross platform/web solution. I want an app that a real Apple fag would be happy to use…“:apple::stuck_out_tongue:. Honestly, I didn’t find it. So I started to think: “I could develop this app, using some modern framework/programming paradigm I studied in the last months…In this way I have a chance to create my first macOS app and add some other interesting projects to my Github profile…and in this way I can work on a project where I don’t have to launch the commands npm install or npm run build hundred times in a hour…”. Here we are after two months of work with the public release of:

  • ID3TagEditor, a pure Swift framework (only Apple Foundation framework dependencies) to read/modify ID3 tag of your mp3 files with support for the following Apple platforms: macOS, iOS, watchOS and macOS (so the entire Apple ecosystem :grin:)
  • Mp3ID3Tagger, a native macOS app written in Swift using the reactive programming paradigm and in particular its Rx (Reactive Extensions) variant with the frameworks RxSwift and RxCocoa (Rx????!?!?!? WHATTTT?!?!?!? :cold_sweat:).

If you are still interested in knowing the details about the development of this two projects, you can follow the links below:

mp3id3tagger id3tageditor

I hope you will see how much love and passion I put into this projects and I also hope you will find all the technical details inside them interesting :sparkling_heart:.

Clean Code: data structures vs objects and the law of demeter

In this post I will talk about clean code: data structures, objects vs procedural and the law of demeter.


In this previous post I described what Clean Code is and what does it mean to use meaningful names in your code. This time I will talk about Data structures and objects. But wait, do we really know the definition of them? Let’s see what Uncle Bob says about them in its Clean Code book:

Objects hide their data behind abstractions and expose functions that operate on that data. Data structures expose their data and have no meaningful functions.

It’s easy to see that they are opposites. A lot of programmers are convinced of the fact that in software development everything should be an object. If you think about the nature of objects you will see that there are times where you just want simple plain data structures that you can manipulate in a procedural way. This is a consequence of the fact that adding new functions to an object may require much more work, because maybe you need to modify all the objects of the same type to add a new function. This give us the following definitions, stated by Uncle Bob in its Clean Code book:

Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. Object oriented code, on the other hand, makes it easy to add new classes without chaging existing functions.

Procedural code makes it hard to add new data structures because all the functions must change. Object oriented code makes it hard to add new functions because all the classes must change.

This set of rules must guide us when we have to choose between objects vs procedural implementation:

  • do you expect to add more and more functions to your code, and keep unmodified the data structures you use? Ok, let’s go with procedural code :relaxed:.
  • do you expect to add new types without adding new functions to your code? Well, use objects :bowtie:.

Related to the object oriented programming, Uncle Bob talks about the law of demeter that says a module should not know about innards of the objects it manipulates. The focus of this law is to improve the decoupling of objects. More precisely its definition is:

A method f of a class C should only call the methods of these:

  • C
  • an object created by f
  • an object passed as argument to f
  • an object held in a instance variable of C

This bring us to talk about what is called train wreck: concatenation of function/properties calls. The difference between objects and data structures gives us a clear understand of when a train wreck is really dangerous:

  • if in a method you’re working with data structures, law of demeter is not applied to them because is natural for a data structure to expose their internal structure.
  • if in a method you’re working with objects, then you should consider concatenation of method calls as violation of the law of demeter

The next time you will write a piece of code try to consider these concepts and how they can improve your code.

Clean code uncle bob superman

Blender tutorial: introduction to basics of modeling - part 1

In this new post of the Blender tutorial series I will talk about the fundamental of modeling in Blender.


In the previous post of the “Blender tutorial” series we talked about how we can select and transforms objects. In this new post thing will start to get more interesting: we will start to talk about modeling. First of all let’s see which kind of primitives mesh we can create with blender.
We can start by opening a new file and removing the default cube mesh by selecting it and pressing the “x” key. Now we are ready to add new primitive meshes. We can do it in two ways:

  • from the Tools -> Create menu in the 3D window
  • from the Add -> Mesh menu at the bottom of the 3D window, when we are in object or edit mode

Blender support a series of basic primitives, from which we can start modeling more complex meshes:

  • plane
  • cube
  • circle
  • UV sphere
  • ico sphere
  • cylinder
  • cone
  • torus

When we choose a primitive mesh to be created, it will be placed where the 3D cursor is pointing at the moment of creation. After the creation of the mesh, a new panel at the bottom left of the 3D window will be shown. In this panel we can customize the properties of the mesh.

blender modeling create primitive meshes

As we said before, we can start from a primitive mesh and model a more complex one. So the first thing we need to learn is how we can select vertices, edges and faces. How can we do that? First, we need to choose edit mode, by selecting the mesh and by choosing it from the editing/interaction mode menu component in the 3D window (or alternately by pressing the “tab” key). After that, we can choose the selection mode between vertices, edge or faces in the bottom bar of the 3D window or by pressing “ctrl + tab” keys.

blender select edges faces vertices

After selecting the edges, vertices or faces that we want to modify, 3D axis will be shown near your selection. They are similar to the ones shown for transformations. By dragging one of these axes the selcted vertices, edges or faces will move accordingly to the direction of the dragging.
We can improve the selection of edges by pressing “ctrl + alt + right click” keys while we are in edge selection mode: in this way we will be able to select edges loops, a series of connected closed edges series.
We can also improve the selection by using the more/less option to select entire levels of edges/vertices/faces.

blender modified mesh

By selecting individual edges, vertices and faces you can start modeling you meshes. Anyway, sometimes you will need to be able to do a more soft modeling than the one we already described above in this article (for example while working on organic objects. This is why it is possible to use the proportional editing. When we use this kind of modeling, the modification on a vertices/edge/face will influence the other element around with a proportional falloff. You can activate the proportional editing using the menu Mesh -> Proportional Editing in edit mode or using the dedicated button in the bottom bar of the 3D window.

blender modeling proportional editing

Below you can find an example of two meshes modified with and without the proportional editing enable.It’s easy to see the difference.

blender modeling proportional editing example

In the next post we will continue to talk about the fundamental of modeling.