ryanfrancesconi/spfk-metadata-xmp
A Swift package for reading and writing [Adobe XMP](https://www.adobe.com/devnet/xmp.html) metadata embedded in audio and video files on macOS. Built on top of the Adobe XMP SDK (via bundled `XMPCore` and `XMPFiles` binary frameworks) with a Swift-native API layer.
Overview
SPFKMetadataXMP provides two main components:
XMP— A thread-safeactorsingleton for reading and writing raw XMP XML strings to/from media files. Manages the Adobe XMP SDK lifecycle. Theparseandwritemethods arenonisolated, enabling true concurrent file operations without actor serialization.XMPMetadata— ASendablestruct that parses XMP XML into strongly-typed properties focused on timecode, markers, and media metadata.
Supported File Formats
The XMP SDK supports reading and writing metadata for common media containers including AIF, M4A, MP3, MP4, and WAV. Raw AAC containers are read-only (no XMP write support).
Key Types
XMP
Singleton actor that wraps the Adobe XMP C++ SDK. Handles SDK initialization via mutex-protected C++ lifecycle management. File I/O methods are nonisolated — they bypass the actor's serial executor because the underlying C++ calls use stack-local SXMPFiles / SXMPMeta instances with no shared state.
// Read XMP from a file (synchronous, nonisolated)
let xmlString = try XMP.shared.parse(url: fileURL)
// Write XMP to a file (synchronous, nonisolated)
try XMP.shared.write(string: xmlString, to: fileURL)
// Concurrent reads are safe — each call gets its own C++ objects
try await withThrowingTaskGroup(of: XMPMetadata.self) { group in
for url in urls {
group.addTask { try XMPMetadata(url: url) }
}
// ...
}XMPMetadata
Parses XMP XML into structured properties. Can be initialized from a file URL, an XML string, or an AEXMLDocument.
// From a file (synchronous, thread-safe)
let metadata = try XMPMetadata(url: fileURL)
// From an XML string
let metadata = try XMPMetadata(xml: xmlString)
// Access parsed properties
metadata.title // dc:title
metadata.frameRate // TimecodeFrameRate (from timecode or nominal rate)
metadata.startTimecodeResolved // Timecode (prefers altTimecode over startTimecode)
metadata.markers // [XMPMarker]
metadata.duration // TimeInterval
metadata.audioSampleRate // Double
metadata.audioChannelType // String (e.g. "Stereo")
metadata.videoFieldOrder // String (e.g. "Progressive")
metadata.nominalFrameRate // Float (e.g. 25.0)
metadata.creatorTool // String (e.g. "Adobe Premiere Pro 2022.0")
metadata.createDate // String
metadata.startTimeScale // CMTimeScale
metadata.startTimeSampleSize // CMTimeValueXMPMarker
Represents a single marker from the XMP xmpDM:Tracks data, with frame-based and time-based positioning.
let marker = XMPMarker(
name: "Hit",
comment: "impact sound",
startFrame: 48,
durationInFrames: 5,
frameRate: .fps25
)
marker.time // 1.92 (seconds)
marker.duration // 0.2 (seconds)
marker.startTimecode // Timecode valueFrameRate
Maps XMP timecode format strings (e.g. "25Timecode", "2997DropTimecode") to TimecodeFrameRate values. Supports 23.976, 24, 25, 29.97 (drop and non-drop), 30, 50, 59.94 (drop and non-drop), and 60 fps.
XMPElement
A String-backed enum representing XMP namespace elements (rdf:RDF, xmpDM:Tracks, dc:title, etc.) with a type-safe AEXMLElement subscript for XML traversal.
Thread Safety
The package is designed for concurrent use across multiple files:
- SDK initialization (
SXMPMeta::Initialize,SXMPFiles::Initialize) is protected by astd::mutexin the C++ layer, ensuring safe one-time setup even under concurrent access. parse()andwrite()arenonisolatedon theXMPactor. Each call creates stack-localSXMPFilesandSXMPMetaC++ objects with no shared mutable state, so multiple files can be read or written in parallel.XMPMetadataisSendable— all properties are value types, immutable after initialization. Instances can be safely passed across concurrency domains.- Same-file writes are not internally serialized. The caller is responsible for not writing to the same file from multiple threads concurrently.
terminate()andisInitializedremain actor-isolated to prevent teardown during active operations.
Architecture
SPFKMetadataXMP (Swift)
|-- XMP.swift Actor: SDK lifecycle + nonisolated file I/O
|-- XMPMetadata.swift XMP XML parser -> structured metadata
|-- XMPMarker.swift Marker data type with time calculations
|-- XMPElement.swift XMP namespace element enum + AEXML subscript
|-- FrameRate.swift XMP timecode format -> TimecodeFrameRate mapping
SPFKMetadataXMPC (Objective-C++ / C++)
|-- XMPFile.mm ObjC++ bridge to XMPUtil C++ functions
|-- XMPLifecycle.mm ObjC++ bridge to SDK init/terminate
|-- XMPLifecycleCXX.cpp Mutex-protected C++ SDK lifecycle
|-- XMPUtil.cpp C++ XMP read/write (stack-local SXMPFiles/SXMPMeta)
Frameworks/
|-- XMPCore.xcframework Adobe XMP Core SDK binary
|-- XMPFiles.xcframework Adobe XMP Files SDK binaryDependencies
| Package | Purpose | |---------|---------| | spfk-base | Foundation extensions, logging, error utilities | | spfk-time | CMTime utilities, SwiftTimecode re-export | | spfk-utils | AEXML XML parsing, string extensions | | spfk-testing | Test infrastructure (test target only) |
Future API Opportunities
The Adobe XMP SDK exposes ~300+ methods across TXMPMeta, TXMPFiles, TXMPIterator, and TXMPUtils. This package currently uses a small subset (open/read/write/serialize). Below are capabilities worth exploring.
Direct Property Access
GetProperty / SetProperty with type-specific variants (_Bool, _Int, _Float, _Date, _Int64). Would allow reading or modifying individual XMP fields without full XML round-tripping. Also GetLocalizedText / SetLocalizedText for locale-aware dc:title handling.
Property Iterator
TXMPIterator walks the XMP property tree node-by-node. Useful for discovery/inspection tools or memory-efficient traversal of large XMP packets without parsing the entire DOM.
Structured Property Composition
TXMPUtils::ComposeArrayItemPath, ComposeStructFieldPath, ComposeQualifierPath — build canonical XMP paths for nested structures. Avoids manual string construction for complex property access.
Template-Based Bulk Updates
TXMPUtils::ApplyTemplate merges XMP from one SXMPMeta into another with configurable merge modes (replace, add, clear). Could enable batch metadata stamping across files.
File Format Detection
TXMPFiles::CheckFileFormat identifies format from file content (not extension). More robust than extension-based routing.
Sidecar XMP Support
The SDK can read/write .xmp sidecar files for formats that don't support embedded XMP. Could extend support to formats like raw AAC.
Progress Callbacks
SetProgressCallback on TXMPFiles for monitoring long read/write operations. Useful for batch processing UI feedback.
Associated Resources
GetAssociatedResources finds related files (sidecars, thumbnails). IsMetadataWritable checks write support before attempting.
Audio-Specific Namespaces
Built-in constants for kXMP_NS_BWF (Broadcast Wave), kXMP_NS_iXML, kXMP_NS_DM (Dynamic Media), plus RegisterNamespace for custom schemas.
Serialization Options
Compact output, pretty-print, read-only packets, exact packet sizing, padding control — fine-grained control over XML output format.
Requirements
- Platforms: macOS 13+
- Swift: 6.2+
- C++20 (for the XMP SDK bridge layer)
About
Spongefork (SPFK) is the personal software projects of Ryan Francesconi. Dedicated to creative sound manipulation, his first application, Spongefork, was released in 1999 for macOS 8. From 2016 to 2025 he was the lead macOS developer at Audio Design Desk.
Package Metadata
Repository: ryanfrancesconi/spfk-metadata-xmp
Default branch: main
README: README.md