# Tasks: `reggaeton-professional-mix` ## type: architecture ## status: draft --- ## Phase 1 — Note Validity (Foundation) ### 1.1 Clamp pitch in `encode_notes_block()` **File**: `src/flp_builder/events.py` **Location**: `encode_notes_block()`, lines 201–223 **Change**: Replace `key=key & 0x7F` with explicit `max(0, min(127, key))`. **Why**: `key & 0x7F` silently wraps values ≥128 rather than clamping, causing the FL Studio "invalid notes" warning. **Validation**: Verify notes with key=128 produce no warning; key=0 and key=127 work. ### 1.2 Clamp velocity in `encode_notes_block()` **File**: `src/flp_builder/events.py` **Location**: `encode_notes_block()`, line 219 — `velocity=velocity & 0x7F` **Change**: Replace with `max(1, min(127, velocity))`. **Why**: Velocity 0 is invalid in MIDI; `& 0x7F` allows 0 from input. **Validation**: velocity=0 → clamped to 1; velocity=127 stays 127; velocity=200 → clamped to 127. ### 1.3 Clamp duration ≥ 1 in `encode_notes_block()` **File**: `src/flp_builder/events.py` **Location**: `encode_notes_block()`, line 212 — `length=max(length, 1)` **Change**: Already present. Confirm `max(length, 1)` is retained (was added in prior fix). **Why**: Duration of 0 would produce 0-byte notes, which FL Studio rejects. **Validation**: length=0 → stays 1 after max(); length=1 → stays 1. --- ## Phase 2 — Mix Settings ### 2.1 Add mix fields to `PatternDef` **File**: `src/flp_builder/schema.py` **Location**: `PatternDef` dataclass, lines 72–87 **Change**: Add four fields to `PatternDef`: ```python volume: float = 0.85 # 0.0–1.0, default -1.5 dB pan: float = 0.0 # -1.0 to 1.0, center reverb_send: float = 0.2 # 0.0–1.0 delay_send: float = 0.1 # 0.0–1.0 ``` Add `volume`, `pan`, `reverb_send`, `delay_send` to `_PATTERN_KEYS` frozenset for validation. **Why**: Needed so the composer can specify per-pattern mix settings; previously all channels used defaults. **Validation**: JSON round-trip: `PatternDef(..., volume=0.8, pan=-0.3, reverb_send=0.4, delay_send=0.2)` survives `to_json()` → `from_json()`. ### 2.2 Patch mix events in `ChannelSkeletonLoader` **File**: `src/flp_builder/skeleton.py` **Location**: `ChannelSkeletonLoader.load()` — after sample patching, before assembly **Change**: Accept `mix_map: dict[int, dict]` parameter (channel_index → {volume, pan, reverb_send, delay_send}). After patching samples, append/replace word events: - `ChVolWord` (72): volume as 0–255 word (`int(vol * 200)`, 200 = 0 dB) - `ChPanWord` (73): pan as signed 0–255 (`int((pan + 1.0) * 127.5)`) - `ChReverb` (139): reverb_send as 0–255 (`int(send * 255)`) - `ChStereoDelay` (85): delay_send as 0–255 (`int(send * 255)`) **Why**: FL Studio stores these as word events on the channel; skeleton loader must inject them. **Validation**: Load skeleton with mix_map, verify output bytes contain the four event IDs with expected values. ### 2.3 Pass mix_map from `FLPBuilder` to skeleton loader **File**: `src/flp_builder/builder.py` (assumed existing — if not found, locate) **Location**: `FLPBuilder.build()` call to `ChannelSkeletonLoader.load()` **Change**: Build mix_map from `PatternDef` fields and pass to loader. **Why**: Bridges schema (PatternDef) to FLP rendering (skeleton). **Note**: If builder.py does not yet accept/forward mix_map, add it. --- ## Phase 3 — Groove ### 3.1 Add `groove_strength` to rhythm generators **File**: `src/composer/rhythm.py` **Location**: All generator functions + `get_notes()` dispatcher **Change**: Add `groove_strength: float = 0.0` parameter (0.0 = no groove, 1.0 = max groove) to all generators: - `kick_main_notes`, `kick_sparse_notes`, `kick_outro_notes` - `snare_verse_notes`, `snare_fill_notes`, `snare_outro_notes` - `hihat_16th_notes`, `hihat_8th_notes` - `clap_24_notes`, `perc_combo_notes`, `rim_build_notes` Apply groove via `_apply_groove(note_dict, groove_strength)` helper: - Velocity jitter: `vel ±= random.randint(0, int(5 + groove_strength * 10))` - Positional nudge: `pos += random.uniform(-groove_strength * 0.02, groove_strength * 0.02)` - Swing (every other 16th): shift even-index 16ths forward by `swing_amount = groove_strength * 0.1` **Why**: Groove makes drum patterns feel human and avoids mechanical timing. **Validation**: `get_notes("kick_main_notes", bars=1, groove_strength=0.5)` returns notes with positional variation vs. `groove_strength=0.0`. ### 3.2 Update `get_notes()` dispatcher signature **File**: `src/composer/rhythm.py` **Location**: `get_notes()`, lines 303–311 **Change**: Add `groove_strength: float = 0.0` to dispatcher and pass to generator. **Why**: All generators now accept groove_strength; dispatcher must forward it. --- ## Phase 4 — Melodic Humanization ### 4.1 Add `humanize` to melodic generators **File**: `src/composer/melodic.py` **Location**: `bass_tresillo()`, `lead_hook()`, `chords_block()`, `pad_sustain()` **Change**: Add `humanize: float = 0.0` parameter (0.0 = no humanization, 1.0 = max humanization). Apply humanization via `_apply_humanize(note_dict, humanize)` helper: - Velocity jitter: `vel ±= random.randint(0, int(humanize * 5))` — gentler than drums - Micro-timing: `pos += random.uniform(-humanize * 0.03, humanize * 0.03)` — tighter than drums **Why**: Melodic instruments (bass, lead, pad) need subtler humanization than drums to avoid sounding out of tune. **Validation**: `bass_tresillo(key="Am", bars=1, humanize=0.5)` vs. `humanize=0.0` shows velocity/position variance. ### 4.2 Add humanization to melodic dispatcher (if exists) **File**: `src/composer/melodic.py` **Location**: Any `get_melodic_notes()` dispatcher function **Change**: Forward `humanize` parameter to all generators. **Why**: Single entry point for melodic generation. --- ## Phase 5 — Transitions ### 5.1 Create FX channel (ch21) with riser sample **File**: `scripts/compose_full_track.py` **Location**: After melodic track setup, before `SongDefinition` **Change**: Add FX channel (channel_index=21) with role `"fx"` and riser sample from `SampleSelector`. Use `rim_build_notes` pattern at channel 21 for riser fills. Place riser clips before chorus sections at bars 19 and 43 (1-bar riser items). **Why**: Transitions between verse→chorus need riser FX to build energy. **Note**: Verify ch21 is available (not used by drums/melodic); if conflicts, use next available. ### 5.2 Add rim_build pre-chorus fills **File**: `scripts/compose_full_track.py` **Location**: Arrangement items for bars 18–19 (pre-chorus 1) and 42–43 (pre-chorus 2) **Change**: Insert 2-bar rim_build pattern before each chorus: - Pattern id 9: `rim_build_notes` at channel 13 - Place at bars 18–19 for chorus 1 (bars 20–31) - Place at bars 42–43 for chorus 2 (bars 44–55) **Why**: Rim roll builds tension going into the chorus ("rim_build" generator is designed for this). ### 5.3 Place riser FX before choruses **File**: `scripts/compose_full_track.py` **Location**: Arrangement items, pre-chorus sections **Change**: Add 1-bar riser clip at bars 19 and 43 (before chorus 1 and 2). Riser should be on a dedicated FX track (arrangement track 7). **Why**: Riser sample creates sonic "lift" into the chorus section. --- ## Phase 6 — Compose Script Update ### 6.1 Add reggaeton-standard mix values to PatternDefs **File**: `scripts/compose_full_track.py` **Location**: `patterns` list, lines 128–137 **Change**: Set explicit mix values per pattern: - `kick_main`: volume=0.88, pan=0.0, reverb_send=0.1, delay_send=0.0 - `kick_sparse`: volume=0.82, pan=0.0, reverb_send=0.1, delay_send=0.0 - `snare_main`: volume=0.80, pan=0.05, reverb_send=0.35, delay_send=0.15 - `hihat_main`: volume=0.65, pan=0.0, reverb_send=0.25, delay_send=0.0 - `clap_main`: volume=0.82, pan=0.0, reverb_send=0.45, delay_send=0.1 - `perc_main`: volume=0.72, pan=-0.15, reverb_send=0.3, delay_send=0.1 - `perc2_main`: volume=0.72, pan=0.15, reverb_send=0.3, delay_send=0.1 **Why**: These values are the reggaeton-standard mix targets documented in the design. ### 6.2 Enable `groove_strength` on drum patterns **File**: `scripts/compose_full_track.py` **Location**: `patterns` list — update call sites that generate drum notes **Change**: Pass `groove_strength=0.3` to drum rhythm generators in compose script. **Why**: Drums benefit from subtle groove humanization to avoid mechanical feel. **Note**: This requires `get_notes()` to accept `groove_strength` (Phase 3 must be complete first). ### 6.3 Enable `humanize` on melodic tracks **File**: `scripts/compose_full_track.py` **Location**: `section_notes()` calls for bass, lead, pad, pluck **Change**: Pass `humanize=0.2` to melodic generators. **Why**: Melodic humanization makes bass/lead/pad feel organic and less quantized. ### 6.4 Add transition sections to arrangement **File**: `scripts/compose_full_track.py` **Location**: `items` list — insert pre-chorus transition blocks **Change**: Insert pre-chorus sections at bars 18–19 and 42–43: - Rim_build pattern at channel 13 (bars 18–19 and 42–43) - Riser FX at channel 21 (bars 19 and 43, 1 bar each) **Why**: Proper transitions create professional song flow rather than abrupt section changes. --- ## Dependencies - Phase 2 depends on Phase 1 (schema fields needed before builder integration) - Phase 3 can run in parallel with Phase 4 (separate modules) - Phase 5 depends on Phase 2 (mix infrastructure for FX sends) - Phase 6 depends on Phases 2, 3, 4, 5 (all capabilities wired up) ## Files Modified (summary) | File | Phases | |------|--------| | `src/flp_builder/events.py` | 1 | | `src/flp_builder/schema.py` | 2 | | `src/flp_builder/skeleton.py` | 2 | | `src/flp_builder/builder.py` | 2 | | `src/composer/rhythm.py` | 3 | | `src/composer/melodic.py` | 4 | | `scripts/compose_full_track.py` | 5, 6 |