Skip to content

Volodymyr-13/SwiftTagLib

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SwiftTagLib

This library is meant as a metadata focused and distilled replacement for what SFBAudioEngine does. SFBAudioEngine utilizes CXXTagLib which wraps taglib.

Usage

import SwiftTagLib

// create audio file from url: reads it's metadata & properties
let url = URL(fileURLWithPath: "./path/to/audiofile.mp3")
var audioFile = try AudioFile(url: url)

// get metadata values
print("song:", audioFile.metadata.artist ?? "_", "-", audioFile.metadata.title ?? "_")
print("bitrate:", audioFile.properties.bitrate, "duration:", aduioFile.properties.duration)
let picture = audioFile.metadata.attachedPictures.first

// set metadata values
audioFile.metadata.title = "Song name"
audioFile.metadata.lyrics = .none
audioFile.metadata.attachedPictures = [
    .init(data: Data(), kind: .frontCover, description: "album cover")
]

// write metadata changes
try audioFile.write()

Note on C++ Interoprability

This library is done using Swift C++ Interop (there are some limitations to consider). Understanding Objective-C and Swift interoperability. Currently supporting reading from most of the same types as SFBAudioEngine in C++ Bridge.

Differences from SFBAudioEngine and explanation of how things work.

taglib

taglib v2.0.2 integrated directly as git submodule into it's own target which also includes utfcpp v4.0.6 as git submodule.

CxxTagLibBridge

SwiftTagLib

protocol AudioFileImplementation {
    init(_ fileName: std.string)

    func readMetadata(
        _ metadata: UnsafeMutablePointer<AudioMetadata>!,
        _ properties: UnsafeMutablePointer<AudioProperties>!,
        _ errorDescription: UnsafeMutablePointer<std.string>!
    ) -> MetadataReadingOutcome

    func writeMetadata(
        _ metadata: UnsafeMutablePointer<AudioMetadata>!,
        _ errorDescription: UnsafeMutablePointer<std.string>!
    ) -> MetadataWritingOutcome
}

All of the equivalent types in Swift conform to CxxRepresentable protocol:

protocol CxxRepresentable {
    associatedtype CxxRepresentation
    init(cxxRepresentation: CxxRepresentation)
    var cxxRepresentation: CxxRepresentation { get }
}

AudioMetadata.hpp also stores a bitmask of it's TagSource with Swift equivalent OptionSet TagSource.

TagSource was quite useful during testing to understand the common errors when reading from or writing to Tag's.

Format detection is done only in Swift as this doesn't really touch taglib or require Objective-C or C++. There's AudioFile protocol:

protocol AudioFormat {
    static var supportedPathExtensions: Set<String> { get }
    static var supportedMIMETypes: Set<String> { get }
    static func isFormatSupported(_ fileHandle: FileHandle) -> Bool

    /// import escaping.
    typealias ImplementationClass = CxxTagLibBridge.AudioFile
    associatedtype Implementation: AudioFileImplementation
}

extension AudioFormat {
    static var implementationMetatype: Implementation.Type { Implementation.self }
}

And AudioFile.Format enum which provides us with:

var formatMetatype: any AudioFormat.Type { ... }
var implementationMetatype: AudioFileImplementation.Type { formatMetatype.implementationMetatype }

The detection of format is done much alike in SFBAudioEngine by calculating the score for each one in AudioFile.Format.Detector.

There's just 3 types protocols for file header format/layout defined: BasicHeader Layout/Format, CompositeHeader Layout/Format and CustomHeader Layout/Format.

There's also two helper structs for this to work:

  • BinaryData struct to read UTF8 String's easily from data obtained from FileHandle.
  • ID3v2ShallowHeader to check for ID3v2, this mostly exact copy from SFBAudioEngine but done in Swift.

File header format/layout by categories:

Tests

There's some audio file samples in repository and Tests are executed using them.

  • LanguageBarrierTraversalTest.swift is a suite of a sanity checks that we can go from Swift to C++ type and back and it behaves as expected.
  • AudioFileTests.swift is a suite of test check that interaction operations with metadata behave as expected with some caveats.
    • Reading metadata expects that metadata read from file doesn't differ between reads.
    • Same self overwrite expects that writing metadata read from file to copy of that file produces same metadata when read again after writing.
    • Writing metadata expects that semi-random metadata is written to file when read again after writing.
    • Erasing metadata expects that erased metadata isn't there when read again after writing.

Here's a caveat:

for keyPath in Self.stringProperties {
    let string = UUID().uuidString.replacingOccurrences(of: "-", with: "")
    /// ID3v1 imposes 128 bytes limitation on fields
    /// which is 30 characters: or approximately 28 ASCII characters + 2 bytes of language code.
    /// Going over this character limit currently only fails test for mp3 file,
    /// indicating the limit depends on actual format implementation
    metadata[keyPath: keyPath] = String(string.prefix(28))
}

Not everything is reproduced and there's some performance caveats

  • There's one ugly problem i encountered and rather than solved just worked around it;
    • going from Swift.Array<T> to std::vector<T> and vice versa, here and here, i actually created an Issue for that .

    it can be improved but this will introduce some use of unsafe pointers, and i'm quite unsure what's worse unsafe memory access in between Swift and C++ or slowly building up a std::vector in loop via push_back method.

  • Not all metadata fields are covered/supported, i ignored supporting ReplayGain and Sorting/Grouping metadata entries, here's the Issue for that.

Notes:

  • WMA file format is supported by taglib here, but not SFBAudioEngine, can probably try to support it even though it's outdated.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published