# Delta Spec: 808 Bass Sidechain Ducking ## ADDED Requirements ### Requirement: MIDI CC11 Event Data Model The schema MUST support an `CCEvent` dataclass with controller, time, and value fields, and `ClipDef` MUST accept an optional `midi_cc: list[CCEvent]` field defaulting to empty list. #### Scenario: CCEvent round-trips correctly - GIVEN `CCEvent(controller=11, time=0.5, value=50)` - WHEN serialized/deserialized via dataclass - THEN all fields preserved exactly #### Scenario: ClipDef with midi_cc - GIVEN a `ClipDef` with `midi_cc=[CCEvent(11, 0.0, 50), CCEvent(11, 0.18, 127)]` - WHEN clip is processed by builder - THEN builder sees `midi_cc` field and can iterate it ### Requirement: Kick Position Cache A kick-cache dict `{drumloop_wav_path: list[beat_positions]}` SHALL be computed once per session, keyed by WAV path. `DrumLoopAnalyzer.transient_positions("kick")` MUST be the source, filtered by `confidence >= KICK_CONFIDENCE_THRESHOLD` (default 0.6). #### Scenario: Cache hit - GIVEN drumloop WAV already analyzed in same session - WHEN `build_bass_track()` requests kick positions for that path - THEN cached positions returned without re-analyzing WAV #### Scenario: Cache miss - GIVEN drumloop WAV not yet cached - WHEN kick positions requested - THEN `DrumLoopAnalyzer.analyze()` runs, positions cached by path key ### Requirement: CC11 Ducking on Kick Hits For each kick transient position in the bass clip's time span, the system MUST emit CC11 events forming a ducking envelope: instantaneous drop to value 50 at kick time, hold at 50 for 0.02 beats, ramp to 127 by 0.18 beats after kick. #### Scenario: Single kick duck - GIVEN kick at beat 1.0 within a 4-beat bass clip - WHEN CC events generated - THEN emits `CCEvent(11, 1.0, 50)`, `CCEvent(11, 1.02, 50)`, `CCEvent(11, 1.18, 127)` #### Scenario: No kicks in clip - GIVEN drumloop with no kick transients in clip time range - THEN `midi_cc` is empty list — no CC events emitted ## MODIFIED Requirements ### Requirement: RPPBuilder MIDI Source Encoding `_build_midi_source()` MUST emit MIDI CC events as `E B0 0B xx` lines interleaved with note events, all sorted by absolute start time. Delta-encoding MUST continue for all E-lines. #### Scenario: CC events interleaved with notes - GIVEN clip with `midi_notes=[Note(60, 0.5, 1.0)]` and `midi_cc=[CCEvent(11, 0.0, 50)]` - WHEN `_build_midi_source()` called - THEN E-lines emitted in time order: CC at 0.0, Note at 0.5 - AND CC line reads `E 0 B0 0B 32` (delta=0, CC11, value=50=0x32) #### Scenario: Delta sequencing across note+CC - GIVEN CC at 0.0, note at 0.5 beats - WHEN building E-lines - THEN CC delta = 0×960 = 0; note delta = 0.5×960 - 0 = 480 - AND cursor reset correctly after CC event ticks ### Requirement: Bass Track Generation `build_bass_track()` SHALL accept an optional `kick_cache: dict[str, list[float]]` parameter. When kick data is present for the drumloop used in each section, `midi_cc` events SHALL be generated and added to the bass `ClipDef`. #### Scenario: Bass clip with ducking CC - GIVEN kick cache has `[1.0, 2.5]` for drumloop, section covers beats 0-16 - WHEN bass track built - THEN bass clip at that section has `midi_cc` with 2×3 CC events (one envelope per kick in range) - AND note generation unchanged from existing behavior #### Scenario: No kick cache provided - GIVEN `kick_cache` is `{}` or omitted - THEN `midi_cc` is empty — zero behavioral change from current output