Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

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

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

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