# 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 |
