- Extracted preset data from all_plugins_v2.rpp for 14 previously broken plugins - Fixed PLUGIN_REGISTRY entries: Kontakt 7, Gullfoss, ValhallaDelay, VC 160/76, The Glue - Template parser falls back to PLUGIN_PRESETS when source RPP has fake data - Substitute Transient Master (not installed) with FabFilter Pro-C 2 - All 25 plugins now load correctly in REAPER - Added template generator scripts and ground truth references - Cleaned up temp/debug files from output/
213 lines
7.4 KiB
Markdown
213 lines
7.4 KiB
Markdown
# Design: audio-clip-playlist
|
||
|
||
## Context
|
||
|
||
The current `FLPBuilder` emits pattern-based playlist items (single 32-byte `ArrangementItem`) for all tracks. Melodic tracks with `source_type="audio"` need a different encoding: 3 consecutive 32-byte items per clip, with fixed indices `0x3FF0` / `0x8080` / `0x6440`.
|
||
|
||
---
|
||
|
||
## Architecture Decisions
|
||
|
||
| Decision | Choice | Rationale |
|
||
|----------|--------|-----------|
|
||
| Polymorphism | `to_items()` returns `list[bytes]` (3 items) vs `to_bytes()` returns `bytes` (1 item) | Avoids union-type juggling; builder collects all items uniformly |
|
||
| `source_type` default | `"pattern"` | Existing callers unchanged; audio is opt-in |
|
||
| Audio clip item indices | Treat `0x8080`/`0x6440`/`0x3FF0` as constants | Per-proposal risk mitigation; verified against `audio_clip_reference.flp` |
|
||
| ChSamplePath correlation | Audio clip item precedes its `ChSamplePath` event in channel data | Builder writes items first, then channel events with sample paths already handled by `ChannelSkeletonLoader` |
|
||
|
||
---
|
||
|
||
## File Changes
|
||
|
||
| File | Action |
|
||
|------|--------|
|
||
| `src/flp_builder/arrangement.py` | Add `AudioClipItem`, constants `AUDIO_CLIP_HEADER_INDEX`, `AUDIO_CLIP_DATA_INDEX` |
|
||
| `src/flp_builder/schema.py` | Add `source_type: str = "pattern"` field to `MelodicTrack` |
|
||
| `src/flp_builder/builder.py` | Route on `track.source_type`; append `AudioClipItem` objects to arrangement items list |
|
||
|
||
---
|
||
|
||
## Binary Format
|
||
|
||
Each audio clip placement = **96 bytes** (3 × 32):
|
||
|
||
### Item 0 — PATTERN positioning
|
||
```
|
||
struct.pack("<IHHIHH HH 4B ff",
|
||
position, # bar * ppq * 4
|
||
0x5000, # pattern_base (distinct from drum pattern_base 0x5000)
|
||
0x5000 + pattern_id,
|
||
length, # num_bars * ppq * 4
|
||
track_rvidx, # (max_tracks - 1) - track_index
|
||
0, # group
|
||
0x0078,
|
||
0x0040, # flags (no mute for audio)
|
||
64, 100, 128, 128,
|
||
-1.0, -1.0,
|
||
)
|
||
```
|
||
|
||
### Item 1 — AUDIO_CLIP_HEADER
|
||
```
|
||
00 00 00 00 00 00 F0 3F 00 00 00 00 00 00 00 00
|
||
00 03 00 00 00 50 05 00 03 00 00 F3 01 00 00
|
||
```
|
||
- `item_index = 0x3FF0`
|
||
- All other bytes constant (observed from reference FLP)
|
||
|
||
### Item 2 — AUDIO_CLIP_DATA
|
||
```
|
||
78 00 40 00 40 64 80 80 00 00 80 BF 00 00 80 BF
|
||
02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||
```
|
||
- `pattern_base = 0x6440`
|
||
- `item_index = 0x8080`
|
||
- Remaining bytes constant
|
||
|
||
---
|
||
|
||
## Data Flow
|
||
|
||
```
|
||
SongDefinition.melodic_tracks[i]
|
||
├── source_type == "pattern" → ArrangementItem (existing path)
|
||
└── source_type == "audio" → AudioClipItem.to_items() [3 × 32 bytes]
|
||
│
|
||
▼
|
||
build_arrangement_section()
|
||
(all items concatenated into Playlist ID233 data)
|
||
```
|
||
|
||
---
|
||
|
||
## AudioClipItem Interface
|
||
|
||
```python
|
||
# src/flp_builder/arrangement.py
|
||
|
||
AUDIO_CLIP_HEADER_INDEX = 0x3FF0 # item_index for header item
|
||
AUDIO_CLIP_DATA_INDEX = 0x8080 # item_index for data item
|
||
AUDIO_CLIP_DATA_BASE = 0x6440 # pattern_base for data item
|
||
|
||
@dataclass
|
||
class AudioClipItem:
|
||
"""Playlist item referencing an audio clip via 3×32-byte structure."""
|
||
|
||
pattern_id: int # references the pattern containing audio notes
|
||
bar: float # start bar (0-based)
|
||
num_bars: float # length in bars
|
||
track_index: int # 0-based track index
|
||
muted: bool = False
|
||
|
||
def to_items(self, ppq: int = PPQ_DEFAULT, max_tracks: int = MAX_TRACKS_DEFAULT) -> list[bytes]:
|
||
"""Return 3 consecutive 32-byte items: [PATTERN, HEADER, DATA]."""
|
||
...
|
||
|
||
# Convenience: to_bytes() returns concatenation for polymorphic use
|
||
def to_bytes(self, ppq: int = PPQ_DEFAULT, max_tracks: int = MAX_TRACKS_DEFAULT) -> bytes:
|
||
return b"".join(self.to_items(ppq, max_tracks))
|
||
```
|
||
|
||
---
|
||
|
||
## Schema Change
|
||
|
||
```python
|
||
# src/flp_builder/schema.py — MelodicTrack
|
||
|
||
@dataclass
|
||
class MelodicTrack:
|
||
role: str
|
||
sample_path: str
|
||
notes: list[MelodicNote]
|
||
channel_index: int
|
||
volume: float = 0.85
|
||
pan: float = 0.0
|
||
source_type: str = "pattern" # NEW: "pattern" | "audio"
|
||
```
|
||
|
||
`SongDefinition.validate()` also validates:
|
||
- `source_type in ("pattern", "audio")`
|
||
- If `source_type == "audio"`: `channel_index >= 17`
|
||
|
||
---
|
||
|
||
## Builder Routing
|
||
|
||
```python
|
||
# src/flp_builder/builder.py — _build_arrangement
|
||
|
||
def _build_arrangement(self, song, track_data_template):
|
||
items: list[ArrangementItem | AudioClipItem] = []
|
||
|
||
# Pattern tracks → ArrangementItem (existing)
|
||
for item in song.items:
|
||
items.append(ArrangementItem(
|
||
pattern_id=item.pattern,
|
||
bar=item.bar,
|
||
num_bars=item.bars,
|
||
track_index=item.track - 1,
|
||
muted=item.muted,
|
||
))
|
||
|
||
# Melodic tracks → route on source_type
|
||
drum_pattern_count = len(song.patterns)
|
||
max_drum_track = max((it.track for it in song.items), default=1)
|
||
|
||
for i, mt in enumerate(song.melodic_tracks):
|
||
pattern_id = drum_pattern_count + i + 1
|
||
track_index = max_drum_track + i # 0-based
|
||
|
||
if mt.source_type == "audio":
|
||
items.append(AudioClipItem(
|
||
pattern_id=pattern_id,
|
||
bar=mt.bar, # from MelodicTrack.bar (default 0.0)
|
||
num_bars=mt.num_bars, # from MelodicTrack.num_bars (default 4.0)
|
||
track_index=track_index,
|
||
muted=False,
|
||
))
|
||
else: # "pattern"
|
||
items.append(ArrangementItem(
|
||
pattern_id=pattern_id,
|
||
bar=mt.bar or 0.0,
|
||
num_bars=mt.num_bars or 4.0,
|
||
track_index=track_index,
|
||
muted=False,
|
||
))
|
||
|
||
return build_arrangement_section(items, track_data_template, ppq=song.meta.ppq)
|
||
```
|
||
|
||
**Note**: `MelodicTrack` currently has no `bar`/`num_bars` fields. The proposal scope does not include adding them; for now audio clip items use `bar=0.0, num_bars=4.0` as placeholders until a future change extends `MelodicTrack` with timeline placement fields.
|
||
|
||
---
|
||
|
||
## ChSamplePath Correlation
|
||
|
||
`AudioClipItem` places a reference on the playlist. The actual sample file path is carried by `ChSamplePath` events (ID 196) in the channel data, already handled by `ChannelSkeletonLoader.load(melodic_map=...)` which patches sampler channels with the correct `.wav` paths.
|
||
|
||
No additional correlation is needed: the channel index on the melodic track (e.g., ch17) maps to the same channel that carries the `ChSamplePath` event. The builder ensures channel events precede arrangement events in the FLP binary.
|
||
|
||
---
|
||
|
||
## Validation Strategy
|
||
|
||
Binary diff against `audio_clip_reference.flp`:
|
||
1. Generate a minimal FLP with one audio clip item
|
||
2. Extract playlist bytes (ID 233 data, after `encode_data_event` wrapper)
|
||
3. Compare first 96 bytes of playlist data against reference
|
||
4. Specifically verify:
|
||
- Bytes 0–31: pattern positioning (`PATTERN_BASE = 0x5000`)
|
||
- Bytes 32–63: header (`item_index = 0x3FF0`)
|
||
- Bytes 64–95: data (`pattern_base = 0x6440`, `item_index = 0x8080`)
|
||
|
||
If mismatch at 0x8080/0x6440: derive values from reference via `build_track_data_template`-style extraction, then update constants.
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
| Question | Status |
|
||
|----------|--------|
|
||
| `MelodicTrack.bar`/`num_bars` fields needed? | Not in scope; placeholders used. Future change to add placement fields. |
|
||
| Index pool (0x8080/0x6440) per-project vs constant? | Treated as constants per proposal risk mitigation. If FLP fails to open, switch to derivation. | |