OutcastID3: A CocoaPod to read and write MP3 ID3 tags

We've just released a new update to Outcast, which adds the ability to read chapter information embedded inside of podcast episodes.

In order to implement this, we looked for an open-source library written in Swift which also support chapters (the CHAP and CTOC information in MP3 files).

We couldn't find any written in Swift (or even Objective-C) that support chapters, so we decided to write our own:

OutcastID3 on CocoaPods

The goals of this were:

  • Support the CHAP and CTOC ID3 frames
  • Be easily extensible (even though it doesn't yet support all frame types, it's extremely support to add support for more frame types)
  • Be somewhat efficient (for example, most of the libraries we reviewed read the entire MP3 file into memory in order to read the ID3 tag, rather than just reading in the ID3 tag bytes).

Reading an ID3 Tag

guard let url = Bundle.main.url(forResource: "MyFile", withExtension: "mp3") else {
    return
}

let mp3 = try MP3File(localUrl: url)
let tag = try x.readID3Tag()

When you read the tag, it not only returns the ID3 frames (in tag.tag), but also the byte offsets for the tag. This makes it easy to strip out or replace the ID3 tag with new information.

To read the actual frame data, you can loop over the frames property and check for the appropriate type:

for frame in tag.tag.frames {
    switch frame {
    case let s as OutcastID3.Frame.StringFrame:
        print("\(s.type.description): \(s.str)")
        
    default:
        break
}

Since most of the frame in ID3 tags are plain strings, the StringFrame class is used for all of these. You can easily check which frame type it corresponds to by using the type enum.

Writing an ID3 Tag

To write an ID3 tag to a file, you construct an OutcastID3.ID3Tag object, and call writeID3Tag().

Two notes about writing tags:

  1. It will output a few file, rather than modifying the input file. If you want to overwrite the existing file, treat the newly-created file as a temporary file and rename it to the input file's filename.
  2. If the input file has no ID3 tag, it is inserted at the head of the file, otherwise, the existing ID3 tag is removed and replaced (as you would expect!).

Adding Support for Additional Tag Types

To add support for additional tag types, dig into the OutcastID3.Frame.RawFrame class.

You can create a new handler class/struct that extends OutcastID3TagFrame, then make a reference to it for the appropriate tag type in OutcastID3.Frame.RawFrame.

You need to implement two methods:

public static func parse(version: OutcastID3.TagVersion, data: Data) -> OutcastID3TagFrame?

This takes all of the raw bytes for an ID3 frame (the length of which is indicated in the frame header. The header is included in data. You can return any frame type (or nil), but I'm not sure why you would return a different frame type.

If this returns nil, the input data is treated as a RawFrame, meaning if you then write the ID3 tag again, the raw bytes from this unparsed frame will be written as-is.

public func frameData(version: OutcastID3.TagVersion) throws -> Data

This method converts the caller into raw bytes that can be written to an MP3 file. You need to build the frame header also, so it's best to use the FrameBuilder helper class.

If an error occurs encoding the frame, you can throw something from  OutcastID3.MP3File.WriteError (or create your own error type).

Pull Requests

Feel free to submit pull requests to http://github.com/CrunchyBagel/OutcastID3 - we think this library is the easiest way to manage ID3 tags in Swift, so would love to see support for all tag types and any other feature enhancements to increase its utility.