Playing custom audio with your own player
At a glance
| Item |
Summary |
| Purpose |
Builds an editable audio playlist on sample-buffer rendering APIs, including remote commands and Now Playing metadata. |
| App architecture |
A SwiftUI-owned observable player façade protects playlist state and delegates serialized rendering to a queue-bound serializer. |
| Main patterns |
Observable façade, serial executor, item/source/serializer pipeline, capability protocol, notification bridge. |
| Project style |
Single target with General integration/models and a layered Player folder. |
Project structure
SampleBufferPlayer/
├── SampleBufferPlayerApp.swift
├── ContentView.swift
├── General/
│ ├── PlaylistItem.swift
│ ├── RemoteCommandCenter.swift
│ └── NowPlayingCenter.swift
└── Player/
├── SampleBufferPlayer.swift
├── SampleBufferSerializer.swift
├── SampleBufferItem.swift
└── SampleBufferSource.swift
Structure observations
SampleBufferPlayer is the application-facing API; lower files form a staged media pipeline.
- Remote commands and Now Playing updates live outside the renderer, keeping system integration separate from buffer production.
Overall architecture
flowchart LR
View["ContentView"] --> Player["SampleBufferPlayer"]
Remote["MPRemoteCommandCenter"] --> Adapter["RemoteCommandCenter"]
Adapter --> Player
Player --> Serializer["SampleBufferSerializer serial queue"]
Serializer --> Item["SampleBufferItem"]
Item --> Source["SampleBufferSource"]
Source --> File["AVAudioFile"]
Serializer --> Renderer["AVSampleBufferAudioRenderer"]
Serializer --> Sync["AVSampleBufferRenderSynchronizer"]
Player --> Now["NowPlayingCenter"]
Reference code
SampleBufferPlayer/Player/SampleBufferSerializer.swift:40 — nonpublic playback logic and framework renderers are owned by one serial executor.
private let serializationQueue = DispatchQueue(
label: "sample.buffer.player.serialization.queue")
private let audioRenderer = AVSampleBufferAudioRenderer()
private let renderSynchronizer = AVSampleBufferRenderSynchronizer()
private var items: [SampleBufferItem] = []
Interpretation
The façade owns editable playlist semantics and observable UI values. The serializer owns ordered playback mutations; items own per-play instance state, and sources translate file frames into timestamped CMSampleBuffer values.
Ownership and state
classDiagram
ContentView *-- SampleBufferPlayer : State owns
SampleBufferPlayer *-- Playlist : owns under semaphore
SampleBufferPlayer *-- SampleBufferSerializer : owns
SampleBufferSerializer *-- AVSampleBufferAudioRenderer : owns
SampleBufferSerializer *-- AVSampleBufferRenderSynchronizer : owns
SampleBufferSerializer *-- SampleBufferItem : queue owns
SampleBufferItem *-- SampleBufferSource : lazy owns
RemoteCommandCenter o-- RemoteCommandHandler : weak capture
Ownership evidence
SampleBufferPlayer/ContentView.swift:13 — the root view creates the long-lived observable façade.
struct ContentView: View {
@State private var sampleBufferPlayer = SampleBufferPlayer()
@State private var editMode = EditMode.inactive
}
| Owner |
Object or state |
Relationship |
Mutation authority |
ContentView |
Player and edit mode |
Creates/retains through @State |
View for edit mode; player for playback state |
SampleBufferPlayer |
Persistent playlist/current index |
Owns behind semaphore |
Player façade methods |
SampleBufferSerializer |
Active playback queue/timeline |
Owns behind serialization queue |
Serializer private methods |
SampleBufferItem |
One playback instance/source/error/offset |
Wraps immutable playlist item with mutable playback state |
Item/serializer |
SampleBufferSource |
File cursor and next offset |
Owns file reader |
Source methods |
Class and protocol design
| Type |
Responsibility |
Depends on |
SampleBufferPlayer |
Public-in-app playlist/playback façade and observable state |
Serializer and system integrations |
SampleBufferSerializer |
Serialize queue edits, enqueue buffers, control timeline |
Renderer/synchronizer |
SampleBufferItem |
Bridge a playlist model to a lazily created source |
PlaylistItem, SampleBufferSource |
SampleBufferSource |
Convert file audio buffers to timed sample buffers |
AVAudioFile/Core Media |
RemoteCommandHandler |
Capability consumed by remote-command adapter |
RemoteCommand enum |
NowPlayingCenter |
Publish current metadata and playback state |
MediaPlayer |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Playlist struct, semaphore, playlist, serializer |
private |
Callers cannot bypass atomic façade operations. |
Protect thread-safe playlist invariants. |
| Serializer queue/renderers/items/cursors |
private |
Only queued serializer logic mutates playback internals. |
Enforce serial execution. |
SampleBufferItem.endOffset, isEnqueued, sampleBufferError |
private(set) |
Serializer can read state without arbitrary writes. |
Retain item mutation authority. |
Source nextSampleOffset |
private(set) |
Consumers inspect progress; only reads advance it. |
Keep file cursor and timestamp aligned. |
RenderingMode extension |
fileprivate |
Display conversion is local to ContentView.swift. |
Avoid a module-wide presentation helper. |
| Types |
implicit internal |
Despite a comment calling the player a public interface, no public API is exported. |
“Public” means app-facing here. |
Reference code
SampleBufferPlayer/Player/SampleBufferPlayer.swift:21 — mutable playlist truth is hidden behind the façade and semaphore.
private struct Playlist {
var items: [SampleBufferItem] = []
var currentIndex: Int?
}
private let atomicitySemaphore = DispatchSemaphore(value: 1)
private var playlist = Playlist()
private var playbackSerializer: SampleBufferSerializer!
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| SwiftUI playlist and controls |
ContentView |
Presentation and user intent. |
| Editable playlist semantics |
SampleBufferPlayer |
App-facing state authority. |
| Queue continuity and renderer timing |
SampleBufferSerializer |
Must execute serially. |
| File-to-sample conversion |
SampleBufferSource |
Focused media transformation. |
| Remote commands |
RemoteCommandCenter + player conformance |
Adapter decouples MediaPlayer callbacks from player methods. |
| Lock screen/control center state |
NowPlayingCenter |
External presentation integration. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Observable façade |
SampleBufferPlayer/Player/SampleBufferPlayer.swift:10 |
Gives SwiftUI one thread-safe playback surface. |
| Serial executor |
SampleBufferPlayer/Player/SampleBufferSerializer.swift:41 |
Orders renderer and queue mutations. |
| Pipeline stages |
SampleBufferPlayer/Player/SampleBufferSource.swift:40 |
Separates file reading, item state, serialization, and rendering. |
| Capability protocol |
SampleBufferPlayer/General/RemoteCommandCenter.swift:18 |
Decouples system command registration from concrete player type. |
| Notification bridge |
SampleBufferPlayer/Player/SampleBufferPlayer.swift:49 |
Moves serializer changes back to main/UI-observable state. |
Naming conventions
- The shared
SampleBuffer prefix connects façade, serializer, item, and source layers.
- Role suffixes state responsibility:
Player, Serializer, Item, Source, Center.
- Queue commands distinguish replacement from continuity:
restartQueue and continueQueue.
- Mutation methods use playlist verbs:
insertItem, removeItem, moveItems, replaceItems.
Architecture takeaways
- Separate editable playlist policy from serialized rendering mechanics.
- Make queue ownership and execution context explicit when framework renderers are stateful.
- Use wrapper items for per-play identity even when the underlying playlist model can repeat.
- Keep remote commands and Now Playing publication as adapters around the core player.
- Read-only setters expose useful status without surrendering mutation authority.
Source map
| Source file |
Relevant symbols |
SampleBufferPlayer/ContentView.swift |
Player ownership and UI commands |
SampleBufferPlayer/Player/SampleBufferPlayer.swift |
Observable façade and playlist |
SampleBufferPlayer/Player/SampleBufferSerializer.swift |
Serial playback engine |
SampleBufferPlayer/Player/SampleBufferItem.swift |
Per-play item state |
SampleBufferPlayer/Player/SampleBufferSource.swift |
File-to-sample conversion |
SampleBufferPlayer/General/RemoteCommandCenter.swift |
Command protocol/adapter |
SampleBufferPlayer/General/NowPlayingCenter.swift |
System metadata |