# Acapella v2

iPhone/iPad app for recording multi-part a-cappella arrangements of yourself.
Spec: `../SPEC.md`. Plan: `../IMPLEMENTATION_PLAN.md`.

## Architecture (the one rule)

There is exactly ONE master timeline: `songSamplePosition`, an integer sample
count advanced on the audio render thread inside AudioCore's ConductorNode.
Never introduce Date(), Timer, or wall-clock math into any musical timing
path. UI reads time via transport snapshots and extrapolates for display only.

Musical time is integer ticks, 960 per quarter note (`Tick`). Never store
beats as floating point.

## Layout

- `Packages/MusicModel` — score model, tick math, .acasong parse/serialize. Pure Swift, no Apple framework deps beyond Foundation.
- `Packages/AudioCore` — transport, ConductorNode, recording, alignment, mixdown. AVFoundation. Must stay testable via offline manual rendering (no device/session needed in tests).
- `Packages/ScoreLayout` — pure geometry: Score → SystemLayout[] glyph positions. No UIKit; app target draws.
- `App/` — SwiftUI app target (thin).
- Project file is GENERATED: edit `project.yml`, run `xcodegen generate`. Never hand-edit Acapella.xcodeproj (it's gitignored).

## Commands

- Full check: `Scripts/test.sh` (package tests + sim build)
- Fast tests: `Scripts/test.sh --tests-only`, or `cd Packages/X && swift test`
- Run on simulator: build with xcodebuild, install/launch via `xcrun simctl`

## Conventions

- Swift 6, strict concurrency. Render-thread state uses explicitly audited
  @unchecked Sendable wrappers; document the invariant at each one.
- REALTIME CLOSURES (AVAudioSourceNode render blocks, input tap blocks,
  anything called on an audio/capture thread) must be created inside a
  `nonisolated static` factory, never inline in a @MainActor method — a
  closure literal formed in actor context inherits the isolation and Swift
  compiles a dynamic main-thread assertion into it, which traps the audio
  thread (EXC_BREAKPOINT in _dispatch_assert_queue_fail). See
  PlaybackEngine.makeSourceNode / CaptureController.makeTapBlock.
- Tests use swift-testing (`import Testing`, `@Test`, `#expect`).
- Audio correctness is proven by offline-render tests that assert exact
  sample positions — keep these green; they are the project's foundation.
