Read and write audio metadata in Swift, powered by TagLib 2.3.1.
TagLibSwift wraps the TagLib C++ library as a Swift package for iOS and macOS. TagLib's source is compiled from vendored source by SwiftPM β no prebuilt binary, no CMake step, and full iOS device and simulator support out of the box.
The public API mirrors TagLib's own model β open a file, then reach its tag, audioProperties, properties, and pictures, and save() β layered over TagLib's C++ types via Swift/C++ interop.
- π Read & write tags for MP3/ID3v2, FLAC, MP4/M4A, Ogg Vorbis, WAV/AIFF, APE, WavPack, and more.
- π§± TagLib-faithful API β a single
AudioFilehandle exposingtag,audioProperties,properties,pictures, andsave(), mirroringTagLib::FileRef. - ποΈ Universal metadata β the format-independent
PropertyMapexposes every text tag (ALBUMARTIST,COMPOSER,DISCNUMBER,BPM, β¦) as a plain[String: [String]]. - πΌοΈ Cover art β read and write embedded pictures as
Data. - 𧬠Generic complex properties β read/write any complex-property key (
PICTURE,GENERALOBJECT, β¦) as typedComplexValuemaps. - ποΈ Audio properties β bitrate, length, sample rate, channels, plus
bitsPerSampleand MPEG version/layer where the format provides them. - πΎ In-memory I/O β open from and serialize back to
Data, no file path required. - π§© Escape hatch β drop down to the raw
TagLib.FileReffor format-specific work when you need it. - β Tested β 34 tests across MP3/FLAC/M4A/Ogg (path and in-memory) with 100% line coverage of the Swift API.
| Minimum | |
|---|---|
| Swift | 5.9 |
| Xcode | 15 |
| iOS | 13.0 |
| macOS | 10.15 |
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/jeonghi/TagLibSwift.git", from: "0.1.0")
]Then add the product to your target. TagLibSwift uses Swift/C++ interoperability, so any target that depends on it must enable C++ interop:
.target(
name: "MyApp",
dependencies: [.product(name: "TagLibSwift", package: "TagLibSwift")],
swiftSettings: [.interoperabilityMode(.Cxx)]
)In Xcode, add the package via File βΈ Add Package Dependenciesβ¦, then set Build Settings βΈ C++ and Objective-C Interoperability to C++/Objective-C++ on the consuming target.
Because Swift/C++ interop is not yet ABI-stable, treat adopting TagLibSwift as a deliberate, toolchain-pinned choice. The API rides the value-type interop path (no imported reference types), which preserves the iOS 13 / macOS 10.15 floor.
import TagLibSwift
import Foundation
guard let file = AudioFile(path: "/path/to/song.flac") else { return }
// Read common tags
print(file.tag.title, "β", file.tag.artist)
// Read audio properties
if let audio = file.audioProperties {
print(audio.bitrate, "kbps,", audio.lengthInSeconds, "s")
}
// Write common tags
file.tag.title = "New Title"
file.tag.year = 2026
// Universal, format-independent text tags
var props = file.properties
props["COMPOSER"] = ["Jane Doe"]
let unsupported = file.setProperties(props) // returns any keys this format rejected
// Cover art
file.setPictures([
Picture(data: try Data(contentsOf: coverURL),
mimeType: "image/png",
description: "cover",
pictureType: .frontCover)
])
try file.save()setProperties(_:) returns the keys the format could not store (empty means everything was written) β so partial writes are never silent.
Open audio straight from bytes (e.g. a download) and serialize the edited bytes back out β no temp file needed:
guard let file = AudioFile(data: mp3Data, fileExtension: "mp3") else { return }
file.tag.title = "New Title"
try file.save() // writes back into the in-memory stream
let updated: Data = try file.serialized() // reopen with AudioFile(data:fileExtension:)serialized() throws AudioFileError.notMemoryBacked on a path-backed file.
Beyond cover art, any complex-property key is reachable as typed ComplexValue maps:
for key in file.complexPropertyKeys { // e.g. "PICTURE", "GENERALOBJECT"
let maps = file.complexProperties(key) // [[String: ComplexValue]]
// ComplexValue: .string / .int / .bool / .data / .stringList / .unsupported
}
file.setComplexProperties("PICTURE", newMaps) // then save()pictures / setPictures(_:) are a thin, typed layer over the PICTURE key.
| Member | TagLib equivalent | Notes |
|---|---|---|
AudioFile(path:) |
FileRef(path) |
Returns nil if the file could not be parsed. |
AudioFile(data:fileExtension:) |
FileRef(IOStream*) |
Opens from an in-memory Data buffer via a ByteVectorStream. |
try file.serialized() |
ByteVectorStream::data() |
In-memory bytes after save(); throws for path-backed files. |
file.isValid / file.isNull |
FileRef::isNull |
Validity of the parsed file. |
file.tag |
FileRef::tag() |
Live view: title, artist, album, comment, genre, year, track, isEmpty. Setters persist on save(). |
file.audioProperties |
FileRef::audioProperties() |
Optional; lengthInSeconds, lengthInMilliseconds, bitrate, sampleRate, channels, bitsPerSample?, mpegVersion?, mpegLayer?. |
file.properties / setProperties(_:) |
FileRef::properties() / setProperties() |
PropertyMap = [String: [String]]. setProperties returns rejected keys. |
file.complexPropertyKeys / complexProperties(_:) / setComplexProperties(_:_:) |
FileRef::complexProperty* |
Any complex-property key as [[String: ComplexValue]]. |
file.pictures / setPictures(_:) |
complex property "PICTURE" |
Cover art as Picture values (typed layer over the PICTURE key). |
try file.save() |
FileRef::save() |
Throws AudioFileError.saveFailed. |
When the typed API doesn't cover a format-specific need (e.g. individual ID3v2 frames), reach the underlying TagLib type:
let ref = file.fileRef // TagLib.FileRef β use TagLib's C++ API directlyHow far the Swift surface covers TagLib. Because the whole library is compiled in
and reachable through file.fileRef, everything TagLib can do is accessible β
the checklist distinguishes what's wrapped in an idiomatic Swift API from what you
reach through the raw escape hatch.
Legend: β
high-level Swift API Β· πΆ reachable via raw file.fileRef (C++ interop), not pre-wrapped Β· β not available
| TagLib capability | Status | Swift API |
|---|---|---|
| Basic tags β title, artist, album, comment, genre, year, track | β | file.tag |
Universal text tags β PropertyMap (ALBUMARTIST, COMPOSER, DISCNUMBER, BPM, LYRICS, β¦, any key) |
β | file.properties / setProperties(_:) |
| Rejected-key reporting on write | β | setProperties(_:) return value |
Cover art / pictures β complex PICTURE property |
β | file.pictures / setPictures(_:) |
Generic complex properties β any key as typed ComplexValue maps |
β | file.complexPropertyKeys / complexProperties(_:) / setComplexProperties(_:_:) |
| Audio properties β length, bitrate, sample rate, channels | β | file.audioProperties |
Extended audio props β bitsPerSample (FLAC/WAV/AIFF/MP4/APE/WavPack/TrueAudio/DSF/DSDIFF), MPEG version/layer |
β | file.audioProperties.bitsPerSample / .mpegVersion / .mpegLayer |
| In-memory / IOStream / ByteVector I/O (no file path) | β | AudioFile(data:fileExtension:) / try file.serialized() |
| Save to disk | β | try file.save() |
| File validity β isNull / isValid | β | file.isValid / file.isNull |
| All TagLib file formats (auto-detected) β the API above works on every supported format | β | AudioFile(path:) |
| Format-specific tag content β ID3v2 / MP4 / Xiph / APE / ASF metadata | β | file.properties (text) Β· complexProperties(_:) (binary/structured) |
| Format-specific tag classes β raw ID3v2 frames (chapters, USLT lyrics, POPM rating, TXXXβ¦), MP4 atoms, Xiph/APE/ASF objects | πΆ | file.fileRef |
| Per-version tag strip/remove (ID3v1 vs ID3v2, etc.) | πΆ | file.fileRef |
The high-level API now covers TagLib's portable metadata model end to end β
base tags, the universal PropertyMap (every text tag), generic complex properties
(cover art and any other key), extended audio properties, and in-memory I/O β
across all formats. The remaining πΆ rows are, by design, thin: the content of
format-specific tags is already reachable through properties (text) and
complexProperties (binary/structured), so wrapping each raw frame/atom class
(hundreds of format-specific APIs) and per-version tag stripping stay reachable
through the raw file.fileRef escape hatch rather than being pre-wrapped.
A multiplatform (iOS + macOS) SwiftUI demo lives in Examples/TagLibSwiftDemo/. Open Examples/TagLibSwiftDemo/TagLibSwiftDemo.xcodeproj in Xcode and run β it exercises the full API: opening an AudioFile, editing tag.*, reading audioProperties, editing the PropertyMap (showing rejected keys on save), viewing/setting cover art (pictures / setPictures), and save(). It ships a tiny tagged sample MP3 that it copies to a writable temp location before opening (the bundle is read-only), plus a .fileImporter to open other files.
The demo target enables C++ interop via the build setting SWIFT_OBJC_INTEROP_MODE = objcxx β required for import TagLibSwift to compile. The project is generated from Project.swift with tuist (tuist generate), but the committed .xcodeproj is standalone and opens/builds without tuist.
MP3 (ID3v1/ID3v2), FLAC, Ogg (Vorbis/Opus/Speex/FLAC), MP4/M4A, WAV, AIFF, APE, WavPack, Musepack, TrueAudio, ASF/WMA, Matroska, DSF/DSDIFF, and tracker formats (MOD/IT/S3M/XM) β everything TagLib 2.3.1 supports.
TagLibSwift vendors TagLib 2.3.1. The upstream source lives in-repo, so consumers need no submodules to build. Maintainers bumping the vendored TagLib version should follow UPDATING.md.
swift testThe base tests use a committed MP3 fixture (regenerate with Tests/TagLibSwiftTests/Resources/create_test_mp3.sh, requires ffmpeg). The multi-format tests generate their FLAC/M4A/Ogg fixtures automatically at runtime, skipping cleanly if ffmpeg or a required encoder is unavailable.
TagLibSwift is released under the MIT License. TagLib is licensed under LGPL v2.1.