This library is meant as a metadata focused and distilled replacement for what SFBAudioEngine
does.
SFBAudioEngine
utilizes CXXTagLib
which wraps taglib
.
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()
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
v2.0.2
integrated directly as git submodule into it's own target which also includes utfcpp
v4.0.6
as git submodule.
- Instead of reading metadata into dictionary, for each kind of
Tag
in file a new instance ofAudioMetadata
is created from thatTag
and then overlayed withvoid AudioMetadata::overlay(const AudioMetadata layer)
. AbstractAudioFile
serves as generic interface for files requiring:- that
FileType
is a subclass ofTagLib::File
, - implementation for
void read_metadata_implementation(FileType &file, AudioMetadata *metadata)
, - implementation for
void write_metadata_implementation(FileType &file, AudioMetadata *metadata)
.
this allows for somewhat streamlined and simple implementation of each AudioFile itself, while the interface exposed to
Swift
looks like this:MetadataReadingOutcome readMetadata(AudioMetadata *metadata, AudioProperties *properties, std::string *errorDescription)
MetadataWritingOutcome writeMetadata(AudioMetadata *metadata, std::string *errorDescription)
- that
AudioFile.hpp
is just and import umbrella for audio file implementations (all of them arestruct
's):AudioFile
aside from field also holdsusing AttachedPictures = std::vector<Picture>
andusing AdditionalMetadataPair = std::pair<std::string, std::string>
.- Interaction with
Tag
's happens much alike inSFBAudioEngine
in categories onAudioMetadata
: - The
AudioMetadata.Picture
also has bunch of categories for converting images:
- On the
Swift
side of thingsAudioFileImplementation
serves as common interface for all those audio files which inherit fromAbstractAudioFile.hpp
inCxxTagLibBridge
and conformances are defined after the fact.
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
}
AudioFile.Properties
is aSwift
equivalent ofAudioProperties.hpp
.AudioFile.Metadata
is aSwift
equivalent ofAudioMetadata.hpp
.AudioFile.Metadata.AttachedPicture.hpp
isSwift
equivalent ofAudioMetadata.Picture
.AudioFile.Metadata.AdditionalMetadataPair
isSwift
equivalent ofusing AdditionalMetadataPair = std::pair<std::string, std::string>
.
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 toTag
'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 readUTF8
String
's easily from data obtained fromFileHandle
.ID3v2ShallowHeader
to check forID3v2
, this mostly exact copy fromSFBAudioEngine
but done inSwift
.
File header format/layout by categories:
BasicHeader
:CompositeHeader
:CustomHeader
:
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 fromSwift
toC++
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))
}
- There's one ugly problem i encountered and rather than solved just worked around it;
- going from
Swift.Array<T>
tostd::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
andC++
or slowly building up astd::vector
inloop
viapush_back
method. - going from
- Not all metadata fields are covered/supported, i ignored supporting
ReplayGain
andSorting
/Grouping
metadata entries, here's the Issue for that.