Sample CodemacOSReviewed 2026-07-21View on Apple Developer

Building an audio sequencer to arrange and play clips

At a glance

Item Summary
Purpose Imports clips, detects tempo, and launches synchronized scenes through a multitrack audio graph.
App architecture SwiftUI MVVM over a controller, actor-isolated loader, and main-actor engine.
Main patterns MVVM, orchestration layer, AsyncStream events, actor boundary, extension-based responsibility split.
Project style Feature folders for Views, ViewModel, Controller, Models, and Audio; engine behavior is split across extensions.

Project structure

ClipLauncher/ClipLauncher/
├── ClipLauncherApp.swift
├── Views/
│   ├── ContentView.swift
│   ├── Grid/
│   └── Controls/
├── ViewModel/ClipLauncherViewModel.swift
├── Controller/ClipLauncherController.swift
├── Models/{AudioClip,ClipTrack,ClipScene}.swift
└── Audio/
    ├── ClipEngine.swift
    ├── ClipEngine+Playback.swift
    ├── ClipEngine+Transport.swift
    ├── ClipEngine+Synchronization.swift
    ├── ClipEngine+Metering.swift
    └── ClipLoader.swift

Structure observations

  • Folder names describe architectural layers; small UI controls are further grouped by role.
  • ClipEngine stores shared engine state while same-type extensions separate playback, transport, synchronization, and metering.

Overall architecture

Reference code

ClipLauncher/ClipLauncher/Controller/ClipLauncherController.swift:125 — the controller crosses the loader boundary, updates the engine, then emits a UI event.

func loadClip(from url: URL, into trackID: ClipEngine.TrackID, sceneID: UUID) async {
    do {
        let result = try await loader.load(from: url)
        engine.ensureTrack(trackID)
        engine.loadClip(result.clip, into: trackID)
        emit(.clipLoaded(trackID: trackID, sceneID: sceneID, clip: result.clip))
    } catch {
        emit(.clipLoadFailed(trackID: trackID, sceneID: sceneID, error: error.localizedDescription))
    }
}

Interpretation

The view model owns presentation state and exposes user actions. The controller coordinates cross-component operations. File I/O and analysis are isolated in an actor; graph mutation remains on the main actor.

Ownership and state

Ownership evidence

ClipLauncher/ClipLauncher/ViewModel/ClipLauncherViewModel.swift:52 — the view model owns UI state while hiding its controller and tasks.

private(set) var tracks: [ClipTrack] = []
private(set) var mainTempo: Double = Constants.BPM.defaultTempo
@ObservationIgnored private let controller = ClipLauncherController()
@ObservationIgnored private var eventTask: Task<Void, Never>?
Owner Object or state Relationship Mutation authority
ContentView ClipLauncherViewModel Creates with @State View model
ClipLauncherViewModel Tracks, scene flags, tempo, volume Authoritative UI state View model action/event handlers
ClipLauncherController Engine, loader, event continuation, track mix state Creates and coordinates Controller
ClipEngine AVAudioEngine graph and loaded-node maps Creates and retains Main-actor engine methods/extensions
ClipLoader Temporary file/analysis work Actor-owned operation state Actor

Class and protocol design

Type Responsibility Depends on
ClipLauncherViewModel Presentable state, action wrappers, event reduction ClipLauncherController
ClipLauncherController Orchestrate loading, engine commands, and domain events ClipLoader, ClipEngine
ClipLoader Read buffers, detect rhythm, extract waveform data AVFAudio, MusicUnderstanding
ClipEngine Build and mutate the multitrack graph AVAudioEngine nodes
AudioClip / ClipTrack / ClipScene Transfer audio and represent UI/domain state Value types and stable IDs

The main abstraction boundary is actor isolation and the controller API; the sample does not introduce an app-defined service protocol.

Access control

Symbol Access Verified effect Likely rationale
tracks, mainTempo private(set) Views observe values but cannot replace canonical state. Preserve event-reduction invariants.
View-model controller/tasks private plus @ObservationIgnored Views cannot issue low-level commands; observation skips infrastructure. Keep the observable surface meaningful.
Controller engine/loader/continuation private All cross-service coordination enters through controller methods. Enforce the orchestration boundary.
ClipEngine stored properties implicit internal Same-type extensions in separate files can share them. Swift private would block these cross-file extensions.
App types implicit internal No external module API. Application-only architecture.

Reference code

ClipLauncher/ClipLauncher/Audio/ClipEngine.swift:38 — engine internals deliberately remain module-visible for cross-file extensions.

// MARK: - Internal Implementation
let engine = AVAudioEngine()
let mixer: AVAudioMixerNode
var trackMixers: [TrackID: AVAudioMixerNode] = [:]
var clips: [TrackID: [UUID: LoadedClip]] = [:]
var launchQueue: [TrackID: AudioClip] = [:]

Logic ownership and placement

Logic Owning type or file Placement rationale
View rendering and bindings Views/ Presentation-only layer.
UI state reduction and action mapping ClipLauncherViewModel Single observable authority.
Cross-service workflows ClipLauncherController Keeps the view model thin and engine focused.
File I/O and tempo analysis ClipLoader Actor isolates expensive asynchronous work.
Graph construction ClipEngine.swift Core node ownership.
Playback/transport/metering ClipEngine+*.swift Responsibility-focused extensions over shared engine state.

Design patterns

Pattern Source evidence Purpose or tradeoff
MVVM ClipLauncher/ClipLauncher/Views/ContentView.swift:11 and ClipLauncher/ClipLauncher/ViewModel/ClipLauncherViewModel.swift:39 Separates SwiftUI rendering from state and actions.
Controller/service layer ClipLauncher/ClipLauncher/Controller/ClipLauncherController.swift:43 Coordinates loader and engine without pushing workflow into the view model.
Async event stream ClipLauncher/ClipLauncher/Controller/ClipLauncherController.swift:69 Sends engine/load results back through a typed, buffered channel.
Actor-isolated loader ClipLauncher/ClipLauncher/Audio/ClipLoader.swift:21 Makes file and analysis work safe across concurrency boundaries.
Extension-based decomposition ClipLauncher/ClipLauncher/Audio/ClipEngine.swift:38 Splits a cohesive engine by behavior while sharing state.

Main application flow

Reference code

ClipLauncher/ClipLauncher/ViewModel/ClipLauncherViewModel.swift:228 — scene launch batches commands before one synchronized flush.

for tIndex in tracks.indices {
    guard let clip = tracks[tIndex].scenes[sceneIndex].clip else { continue }
    try? controller.queueClip(track: tracks[tIndex].id, clipID: clip.id)
}
controller.flushLaunchQueue()

Naming conventions

  • Layer roles appear as suffixes: View, ViewModel, Controller, Engine, Loader.
  • Commands use direct verbs: importClip, queueClip, flushLaunchQueue, stopAll.
  • State names use domain language: ClipTrack, ClipScene, launchQueue, beatCounter.
  • Engine extension filenames name their behavior: +Playback, +Transport, +Synchronization, +Metering.

Architecture takeaways

  • Give observable UI state, orchestration, I/O, and graph mutation distinct owners.
  • Use a typed event stream when lower layers must report asynchronous domain events upward.
  • Keep a single actor in charge of AVAudioEngine graph state while moving file analysis to another actor.
  • Cross-file extensions can justify internal engine state; document that boundary rather than marking everything private.
  • Batch scene commands, then commit once, when synchronization is the business invariant.

Source map

Source file Relevant symbols
ClipLauncher/ClipLauncher/Views/ContentView.swift Root view and view-model ownership
ClipLauncher/ClipLauncher/ViewModel/ClipLauncherViewModel.swift UI state, actions, event consumption
ClipLauncher/ClipLauncher/Controller/ClipLauncherController.swift Orchestration and Event stream
ClipLauncher/ClipLauncher/Audio/ClipEngine.swift Engine state and graph construction
ClipLauncher/ClipLauncher/Audio/ClipLoader.swift Actor-isolated loading and analysis
ClipLauncher/ClipLauncher/Models/AudioClip.swift Sendable clip model