# Acapella v2 — Product & Technical Specification

*A from-scratch rewrite. Supersedes the v1 prototype in this directory.*

## 1. Product vision

An iPhone/iPad app for building multi-part a-cappella recordings of yourself.
You load an arrangement (SATB or any set of parts), hear the parts as guide
tones, pick a part, and record yourself singing it while listening to the
others through headphones. Part by part, you replace synthetic tones with your
own voice until the whole arrangement is you. Scrolling sheet music shows your
part highlighted while you sing. The final mix exports as an audio file.

**Primary persona:** an amateur choral singer (comfortable reading notation)
recording alone at home with wired or Bluetooth headphones and the built-in mic.

### What v1 got wrong that v2 must get right

1. **One timebase.** In v1, tones, recorded WAVs, the visual cursor, and the
   mic recording each ran on separate, drifting clocks (wall-clock timers,
   "play now" scheduling, frame-skip alignment hacks). In v2 there is exactly
   one master timeline, measured in samples on the audio hardware clock, and
   everything else is derived from it. This is the non-negotiable heart of the
   rewrite.
2. **Recordings carry metadata.** v1 stored bare WAVs with no tempo, offset, or
   sample rate. v2 stores every take with the exact sample-accurate offset at
   which it aligns to the song, the tempo it was recorded at, and its format.
3. **Musical time is exact.** v1 used floating-point beats with epsilon hacks
   that silently truncated notes crossing barlines. v2 uses integer ticks.
4. **Correct notation.** v1 misrendered bass clef, had no beaming, and didn't
   align simultaneous notes across parts. v2 engraves properly.

## 2. Scope

### In scope (v1.0 of the rewrite)

- iPhone + iPad, SwiftUI, one codebase. Portrait and landscape.
- Song library: bundled starter arrangements + user-imported songs.
- Import: extended text format (native) and MusicXML (.musicxml / .mxl).
- Playback: sample-accurate guide tones per part; mute/solo/volume per part;
  loop a measure range; adjustable tempo; count-in; metronome (on/off, volume).
- Recording: record any part while hearing any mix of the others
  (tones and/or previously recorded takes); count-in; automatic sample-accurate
  alignment; multiple takes per part with an "active take" selection;
  re-record freely; record a sub-range (punch-in at measure granularity).
- Notation: engraved sheet music (SMuFL font), all parts as aligned systems,
  your part highlighted, smooth playback cursor, auto-scroll/page-turn,
  pinch-to-zoom, tap/drag measure selection.
- Live pitch indicator while recording (am I sharp/flat vs. my part's current
  note) — display only, no correction.
- Mix & export: offline sample-accurate mixdown to .m4a/.wav, share sheet.
- Latency calibration: automatic (device-reported) + optional guided
  measurement (play-and-record a click, cross-correlate).
- Project persistence as a document package; iCloud Drive-friendly.

### Explicitly out of scope for v1.0 (planned later)

- Automatic pitch correction of takes (v2.x; the model reserves room for it).
- Lyrics rendering under notes (format supports lyrics; rendering is v1.x).
- In-app arrangement editing beyond tempo/key transposition.
- Social/sharing platform, cloud accounts, Android, macOS.
- Mid-song time-signature changes (format reserves them; engine/UI reject for
  now). Mid-song **tempo** changes ARE in scope (tempo map).

## 3. Core concepts & data model

### 3.1 Musical time

- **Tick**: integer, 960 per quarter note (PPQ = 960). All note onsets and
  durations are ticks. Divisible by 2,3,5 → exact dotted notes and tuplets.
- **TempoMap**: ordered list of `(tick, bpm)` changes; v1 UI exposes a single
  base tempo plus a global user tempo multiplier, but all math goes through
  the map. Conversion: ticks → seconds via tempo map; seconds → samples via
  sample rate. Never floats as stored truth.
- **Score positions** are `(measureIndex, tickWithinMeasure)` when displayed,
  but stored absolute ticks internally.

### 3.2 Score model

```
Score
├── title, composer, source
├── timeSignature (one for v1; stored as a map for future)
├── keySignature
├── tempoMap: [(tick, bpm)]
└── parts: [Part]
      ├── name ("Soprano"), shortName ("S"), color index
      ├── clef (explicit: treble, treble8vb, alto, bass — from range if absent)
      └── notes: [NoteEvent]
            ├── startTick, durationTicks (absolute, gap = rest)
            ├── pitch: Pitch? (nil = explicit rest) — spelled: step, alter, octave
            ├── tieToNext: Bool
            ├── tupletGroup: (id, actualNotes, normalNotes)?  // e.g. 3:2
            └── lyric: String?   // stored, not rendered in v1.0
```

**Validation is strict**: a note may not cross a barline unless tied (importer
splits + ties automatically); measures must sum exactly to the time signature
(except a declared pickup measure); violations are load errors with line/measure
numbers, never silent truncation.

### 3.3 Project (document) model

A project = one score + recordings + mix state. Stored as a document package:

```
MySong.acaproj/
├── manifest.json          // versioned; everything below is described here
├── score.acasong          // native text format (§4.1)
├── takes/
│   ├── take-<uuid>.caf    // mic audio, unmodified, full stream incl. count-in
│   └── ...
└── artwork/ (optional)
```

`manifest.json` (per take): `partName`, `fileName`, `sampleRate`,
`alignmentOffsetSamples` (signed: sample in the file that corresponds to song
tick 0 — THE critical field), `recordedTempoMultiplier`, `recordedRange`
(measures), `createdAt`, `gainDb`, `isActive`. Per part: volume, mute, solo.
Global: user tempo multiplier, transposition, loop range, calibration offset.

Rationale: takes are never trimmed or rewritten — alignment lives in metadata,
so it can be nudged (manually or by recalibration) without touching audio.
Tempo mismatch between a take and current playback tempo is *detectable*
(warn + offer re-record) instead of silently wrong.

## 4. File formats

### 4.1 Native text format (`.acasong`)

Human-writable, versioned superset of v1's format:

```
acasong 1
title: Simple Harmony Test
time: 4/4
key: G major
tempo: 80            // optional later lines: "tempo@17: 92" (measure 17)
pickup: 1/4          // optional pickup measure of one quarter

part Soprano [treble]
G4 4  A4 4  B4 4  A4 4 | G4 4  B4 4  D5 2 | B4 4~ B4 4 G4 2
part Bass [bass]
G3 4  A3 4  B3 4  A3 4 | G3 4  G3 4  G3 2 | 3(D3 8 E3 8 F#3 8) G3 4 G3 2
```

- Tokens as v1: `Pitch Duration` pairs, `R` for rests, `.` dot suffix.
- New: `~` tie suffix; `3( ... )` tuplets; `|` optional barline check tokens
  (validated if present); `part Name [clef]` headers allow spaces in names;
  32nd notes; `lyrics Soprano: ...` lines (stored); versioned magic first line.
- v1 files: a small converter script migrates the four existing samples.

### 4.2 MusicXML import

Supports the practical subset: partwise scores, notes/rests/dots/ties/tuplets,
key/time/tempo, part names, lyrics (first verse). `.mxl` (zip) unpacked.
Chords within one part: keep top note, warn. Unsupported elements are ignored
with a summarized import report ("dynamics ignored", etc.), not failures.
Import produces a native `.acasong` — MusicXML is a gateway, not a live format.

### 4.3 Export

Offline mixdown (§5.5) to 48 kHz stereo `.m4a` (AAC) or `.wav` (16-bit PCM),
via share sheet. Also export the project package itself for backup.

## 5. Audio architecture

### 5.1 The master clock (the whole point of the rewrite)

- One `AVAudioEngine`. One **TransportClock** owned by the audio side.
- All rendering flows through a single `AVAudioSourceNode` ("ConductorNode")
  whose render callback maintains `songSamplePosition: Int64` — the master
  timeline. Song time exists *only* as a sample count advanced inside the
  render thread. There is no Date(), no Timer, no wall clock anywhere in the
  musical timing path.
- The UI reads time via a lock-free snapshot `(hostTime, songSamplePosition,
  isPlaying)` published each render cycle (atomic/seqlock), and *extrapolates*
  between snapshots using `mach_absolute_time` only for cursor smoothness —
  display-only, never fed back into audio.

### 5.2 Playback (tones + takes in one render path)

- The ConductorNode renders everything itself, mixing:
  - **Tone sequencer**: per part, a precompiled `[(startSample, endSample,
    frequency)]` schedule (from ticks via tempo map). Note boundaries land at
    exact sample offsets *within* the render buffer; envelopes applied
    per-sample. Timbre: band-limited soft synth (sine + gentle 2nd/3rd
    harmonics + 5 ms/25 ms AR envelope), one voice per part.
  - **Take playback**: active takes decoded to float PCM in memory at engine
    rate (SRC on load, not in the render path). The render callback copies
    `take[songSamplePosition − alignmentOffset]` — alignment is array
    indexing, correct by construction. (Mono take at 48 kHz ≈ 11 MB/min; fine.
    Streaming from disk is a documented fallback if memory ever matters.)
  - **Metronome & count-in**: click events precompiled into the same
    sample-scheduled event list. Count-in = transport starts at negative
    sample positions; song tick 0 is just sample 0.
- Tempo changes while stopped → schedules recompile. Tempo changes while
  playing: allowed for tones/metronome (recompile from current position);
  if takes are audible, tempo slider is locked (v1) — no time-stretching.
- Mute/solo/volume are per-part gains inside the ConductorNode, changeable
  live (smoothed over ~10 ms to avoid zipper noise).

### 5.3 Recording & alignment

- Mic input tap writes **everything** to disk (.caf) from the moment record
  is armed — no gating, no frame skipping, count-in included.
- Alignment via hardware timestamps: the tap's first buffer carries an
  `AVAudioTime`; the ConductorNode records the host time at which
  `songSamplePosition == 0` played. Then
  `alignmentOffsetSamples = (inputStartHostTime → samples) − songZero −
  (inputLatency + outputLatency + calibrationTrim)`.
  Stored in the manifest; audio file never modified.
- **Calibration**: defaults from `AVAudioSession` reported latencies; a guided
  routine (app plays clicks into headphones, user holds earcup to mic, or
  speaker-mode loopback; cross-correlate recorded vs. played) measures the
  true round trip and stores `calibrationTrim` per audio route. A manual
  ±nudge (ms) per take is available in the take list as an escape hatch.
- While recording: engine stays running; the tap is installed at setup time
  (armed/disarmed by flag) so the v1 "stop engine to install tap" bug class
  is structurally impossible.
- Monitor options: hear other parts (tones/takes per normal mix), optional
  quiet metronome, optional own-part guide tone. Recording forces
  "all other parts" mode.

### 5.4 Route & session handling

- `AVAudioSession` `.playAndRecord`, `.measurement`-informed but voice-friendly
  mode, preferred IO buffer 5 ms.
- Route changes: pause transport gracefully, re-read latencies, restore.
- Bluetooth: playback over A2DP is fine; warn that BT latency is higher and
  push calibration; always record from built-in mic.
- Interruptions (calls/Siri): pause, preserve partial take (it's already on
  disk), offer keep/discard on resume.
- Background audio capability: recording/playback continues if user switches
  apps briefly.

### 5.5 Mixdown

Offline (faster-than-real-time) render using the *same* ConductorNode code in
manual-render mode — the mix you hear is the mix you export, by construction.
Per-part gains applied; unrecorded parts optionally included as tones or
omitted (user choice); soft-knee limiter; loudness-normalized to ≈ −16 LUFS.

### 5.6 Live pitch indicator

Reuse the YIN detector from v1 (it was good): 2048-sample window, hop 512,
window-center time alignment (fixing v1's ~21 ms skew). While recording, show
cents deviation vs. the part's current scheduled note (needle/color trail on
the cursor). Display only; runs on a utility queue fed by the same input tap.

## 6. Notation rendering

- **Font**: Bravura (SMuFL, OFL-licensed), embedded; glyph metrics from SMuFL
  metadata JSON — no magic pixel offsets.
- **Pipeline**: `Score → LayoutEngine → SystemLayout[] → CanvasRenderer`.
  LayoutEngine is pure/testable: produces typed glyph positions.
- Correct clef-relative pitch placement (treble, treble8vb, alto, bass);
  automatic ledger lines; key signature on every system; accidentals follow
  the key + measure accidental-carry rules.
- **Beaming**: automatic within beat groups (time-signature driven). Ties and
  tuplet brackets rendered. No slurs/dynamics/articulations in v1.
- **Vertical time alignment**: one shared horizontal spacing per system,
  computed from the union of all parts' onsets (spring/rod-lite: width ∝
  duration^0.6 with minimums) — simultaneous notes align across staves.
- **Cursor**: vertical line positioned by *interpolating between actual onset
  x-positions* (not measure-fraction like v1); driven by CADisplayLink reading
  the transport snapshot (§5.1). Auto-scroll keeps the cursor's system + the
  next one visible; page-flip mode (less motion) as a setting.
- Your recording part is tinted; others dimmed. Tap a measure = set cursor;
  drag / two taps = select range (loop/punch region). Pinch zoom scales the
  whole layout (re-layout, not bitmap scale).
- Rendering into a `UIView`/`CALayer`-backed canvas with tiling for long
  scores; only cursor layer updates per frame (no full redraw per tick —
  fixes v1's 60 Hz full-view invalidation).

## 7. UI structure

```
LibraryView (grid of songs: bundled + imported + projects)
└── ProjectView (one song)
    ├── ScoreView (notation, cursor, selection)          ← main area
    ├── MixerStrip (one chip per part: name/color, mute, solo,
    │        volume, take status ●, "record this part")  ← above transport
    ├── TransportBar (play/stop, record, loop, tempo, count-in, position)
    ├── TakeSheet (per part: list of takes, active toggle, nudge, delete)
    └── SettingsSheet (metronome, monitoring, calibration, export)
```

- iPhone portrait: mixer strip collapses to horizontal chips; landscape favors
  notation. iPad: persistent side mixer panel, bigger systems.
- Recording flow: tap part's ● → arm → count-in (visual + audible) → sing →
  auto-stop at range end (or tap stop) → take saved, instant playback review →
  keep/retake. Two taps total from "I want to record the alto" to singing.
- State: `@Observable` app models; the audio engine is an actor-ish service
  with a narrow async API + AsyncStream of transport events. No SwiftUI state
  writes from the render thread.

## 8. Starter library

Ship ≥ 8 public-domain arrangements converted to `.acasong` (via MusicXML
import), covering: a round (Row Row Row / Dona Nobis Pacem), simple hymn-style
SATB (Amazing Grace, Ode to Joy), a carol or two, one barbershop-style piece.
Each ≤ 3 minutes, tagged with difficulty and voice count. (v1's four samples
get migrated too.)

## 9. Quality bars & acceptance criteria

- **Sync**: in an offline test, a take with known alignment mixed against
  tones shows < 1 sample of relative offset. On device, guide tones vs.
  recorded playback of those tones align within ±10 ms after calibration
  (wired), ±20 ms (Bluetooth playback + built-in mic).
- **Timing stability**: note-onset jitter of generated tones = 0 samples by
  construction (verified by rendering to file and measuring onsets).
- **Cursor**: no visible stutter at 60/120 Hz; cursor-to-audio agreement
  within one animation frame.
- **Model exactness**: property tests — parse→serialize→parse round-trips
  byte-identically; every sample song validates; barline violations rejected
  with correct locations; tick math has no rounding drift over 10k measures.
- **Performance**: 4-part, 100-measure score lays out < 50 ms on an A15;
  scroll at 60 fps; render-callback CPU < 15% worst case.
- **Resilience**: route change, interruption, backgrounding during recording
  never lose already-captured audio.

## 10. Tech stack

Swift 6 (strict concurrency), SwiftUI + minimal UIKit (score canvas,
document pickers), AVAudioEngine/AVAudioSession directly (no AudioKit),
Accelerate/vDSP for mixing & cross-correlation, XCTest (+ swift-testing)
with offline-render audio tests. Min target: iOS 17. New Xcode project
`AcapellaV2`, new git repo from day one.
