Files
yaze/docs/internal/plans/music_editor.md

455 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Music Editor Development Guide
This is a living document tracking the development of yaze's SNES music editor.
## Overview
The music editor enables editing ALTTP music data stored in ROM, composing new songs, and integrating with the Oracle of Secrets music_macros ASM format for custom music.
## Sprint Plan
### Sprint 1: Data Model
**Status: Complete**
- Core data structures (`Note`, `MusicCommand`, `TrackEvent`, `MusicSong`)
- `MusicBank` manager for ROM loading/saving
- `SpcParser` and `SpcSerializer`
- `BrrCodec` for sample handling
### Sprint 2: Tracker View (Read-Only)
**Status: Complete**
- `TrackerView` component with 8-channel grid
- Song selector and playback integration
- Visualization of notes and commands
### Sprint 3: Tracker Editing
**Status: Complete**
- Keyboard navigation and note entry
- Selection system (cells and ranges)
- Undo/Redo system
- Clipboard placeholder
### Sprint 4: APU Playback
**Status: Complete**
- Mixer Panel with Mute/Solo
- Real-time Volume Meters
- Master Oscilloscope
- DSP state integration
### Sprint 5: Piano Roll
**Status: Complete**
- `PianoRollView` component (Horizontal)
- Mouse-based note entry/editing
- Zoom controls
- Integrated with `MusicEditor` state
### Sprint 6: Instruments & Samples
**Status: Complete**
- `InstrumentEditorView`: ADSR visualization (`ImPlot`), property editing.
- `SampleEditorView`: Waveform visualization (`ImPlot`), loop point editing.
- Basic WAV import placeholder (generates sine wave).
- Integrated into `MusicEditor` tabs.
### Sprint 7: Project Management & Song Browser
**Status: Complete**
- **Song Browser:**
- Tree view separating "Vanilla Songs" (IDs 1-34) and "Custom Songs" (IDs 35+).
- Dockable "Song Browser" card in Activity Bar.
- Search filtering and context menus (Duplicate/Delete).
- **Integration:**
- Replaced legacy dropdown selector with robust browser selection.
- Linked `MusicEditor` state to browser actions.
### Sprint 8: Export & Polish
**Status: In Progress**
- ✅ Integrated `SpcParser::ParseSong()` into `MusicBank::LoadSongTable()` so the editor now reflects real ROM song data (vanilla + custom slots).
- [ ] ROM patching finalization (ensure `SaveToRom` works robustly).
- [ ] ASM export (`music_macros` format generation).
- [ ] SPC file export (standalone).
- [ ] Full WAV import/resampling logic (replace dummy sine wave).
---
## Quality Improvements (Review Findings)
Tracked fixes identified during the agent swarm code review.
### High Priority (Blockers)
- [x] **Replace hardcoded colors with theme system** - `tracker_view.cc` (4 instances), `music_editor.cc` (3 instances)
- [x] **Integrate SpcParser into MusicBank** - `MusicBank::LoadSongTable()` now uses `SpcParser::ParseSong()`
- [ ] **Fix serializer relocation bug** - Track address calculation in `SerializeSong` is incorrect
- [ ] **Implement SaveToRom()** - Currently returns `UnimplementedError`
### Medium Priority (Quality)
- [x] **Add undo stack size limit** - Capped at 50 states with FIFO eviction (`music_editor.cc`)
- [x] **Fix oscilloscope ring buffer wrap-around** - Proper mask `& (kBufferSize - 1)` (`music_editor.cc`)
- [ ] **Add VU meter smoothing/peak-hold** - Currently uses instantaneous sample values
- [x] **Change Copy()/Paste() to return UnimplementedError** - Honest API reporting (`music_editor.cc`)
### Low Priority (Nice to Have)
- [ ] Add stereo oscilloscope toggle
- [ ] Implement range deletion in TrackerView
- [ ] Add visual octave indicator (F1/F2 feedback)
- [ ] Per-song undo stacks
### Test Gaps
- [ ] ROM-dependent integration tests (`test/e2e/rom_dependent/music_rom_test.cc`)
- [ ] Error handling tests for parse failures
- [ ] Parse → Serialize → Parse roundtrip validation
---
### Sprint 9: Expanded Music Banks (Oracle of Secrets Integration)
**Status: Planned**
The Oracle of Secrets ROM hack demonstrates expanded music bank support, allowing custom songs beyond vanilla ROM limits. This sprint brings those capabilities to yaze.
**Goals:**
- [ ] Support for expanded bank detection (`SongBank_OverworldExpanded_Main`)
- [ ] Dynamic bank allocation for custom songs (slots 35+)
- [ ] Auxiliary bank support (`SONG_POINTERS_AUX` at `$2B00`)
- [ ] Bank space visualization with overflow warnings
- [ ] Auto-relocate songs when bank space exceeded
**Bank Architecture (from Oracle of Secrets):**
| Symbol | Address | Purpose |
|--------|---------|---------|
| `SPC_ENGINE` | `$0800` | Sound driver code |
| `SFX_DATA` | `$17C0` | Sound effects |
| `SAMPLE_POINTERS` | `$3C00` | Sample directory |
| `INSTRUMENT_DATA` | `$3D00` | Instrument definitions |
| `SAMPLE_DATA` | `$4000` | BRR sample data |
| `SONG_POINTERS` | `$D000` | Main song pointer table |
| `SONG_POINTERS_AUX` | `$2B00` | Auxiliary song pointers |
### Sprint 10: ASM Editor Integration
**Status: Planned**
Full integration with Oracle of Secrets `music_macros.asm` format for bidirectional editing.
**Goals:**
- [ ] Import `.asm` song files directly
- [ ] Export songs to `music_macros` syntax
- [ ] Syntax highlighting for N-SPC commands
- [ ] Live preview compilation (ASM → binary → playback)
- [ ] Subroutine deduplication detection
**Export Format Example:**
```asm
MySong:
!ARAMAddr = $D86A
dw !ARAMAddr+$0A ; Intro section
dw !ARAMAddr+$1A ; Looping section
dw $00FF ; Fade-in
dw !ARAMAddr+$02 ; Loop start
dw $0000
.Channels
!ARAMC = !ARAMAddr-MySong
dw .Channel0+!ARAMC
dw .Channel1+!ARAMC
; ... 8 channels
.Channel0
%SetMasterVolume($DA)
%SetTempo(62)
%Piano()
%SetDurationN(!4th, $7F)
%CallSubroutine(.MelodyA+!ARAMC, 3)
db End
```
### Sprint 11: Common Patterns Library
**Status: Planned**
Inspired by Oracle of Secrets documentation proposals for reusable musical patterns.
**Goals:**
- [ ] Built-in pattern library (drum loops, arpeggios, basslines)
- [ ] User-defined pattern saving/loading
- [ ] Pattern browser with preview
- [ ] Auto-convert repeated sections to subroutines
**Pattern Categories:**
- Percussion: Standard 4/4 beats, fills, breaks
- Basslines: Walking bass, pedal tones, arpeggiated
- Arpeggios: Major, minor, diminished, suspended
- Effects: Risers, sweeps, transitions
---
## Oracle of Secrets Integration
### music_macros.asm Reference
The Oracle of Secrets project uses a comprehensive macro system for music composition. Key macros that yaze should support for import/export:
**Duration Constants:**
| Macro | Value | Description |
|-------|-------|-------------|
| `!4th` | `$48` | Quarter note (72 ticks) |
| `!4thD` | `$6C` | Dotted quarter (108 ticks) |
| `!4thT` | `$30` | Quarter triplet (48 ticks) |
---
## Plain-English Reference (Tracker/ASM)
### Tracker Cell Cheatsheet
- **Tick column**: absolute tick where the event starts (0-based). Quarter note = 72 ticks, eighth = 36, sixteenth = 18.
- **Pitch**: C1 = `$80`, B6 = `$C7`; `$C8` = tie, `$C9` = rest.
- **Duration**: stored per-note; tracker shows the event at the start tick, piano roll shows its full width.
- **Channel**: 8 channels run in parallel; channel-local commands (transpose/volume/pan) affect only that lane.
### Common N-SPC Commands (0xE00xFF)
- `$E0 SetInstrument i` — pick instrument index `i` (024 vanilla ALTTP).
- `$E1 SetPan p` — pan left/right (`$00` hard L, `$10` center, `$1F` hard R).
- `$E5 SetMasterVolume v` — global volume.
- `$E7 SetTempo t` — tempo; higher = faster.
- `$E9 GlobalTranspose s` — shift all channels by semitones (signed byte).
- `$EA ChannelTranspose s` — shift this channel only.
- `$ED SetChannelVolume v` — per-channel volume.
- `$EF CallSubroutine addr, reps` — jump to a subroutine `reps` times.
- `$F5 EchoVBits mask` / `$F6 EchoOff` — enable/disable echo.
- `$F7 EchoParams vL vR delay` — echo volume/delay.
- `$F9 PitchSlide` — slide toward target pitch.
- `$FA PercussionPatch` — switch to percussion set.
- `$FF` (unused), `$00` marks track end.
### Instruments (ALTTP table at $3D00)
- **Bytes**: `[sample][AD][SR][gain][pitch_hi][pitch_lo]`
- `sample` = BRR slot (024 in vanilla).
- `AD` = Attack (bits0-3, 0=slow, F=fast) + Decay (bits4-6).
- `SR` = Sustain rate (bits0-4, higher = faster decay) + Sustain level (bits5-7, 0=quiet, 7=loud).
- `gain` = raw gain byte (rarely used in ALTTP; ADSR preferred).
- `pitch_hi/lo` = 16-bit pitch multiplier (0x1000 = normal; >0x1000 raises pitch).
- In the tracker, a `SetInstrument` command changes these fields for the channel until another `$E0` appears.
### music_macros Quick Map
- `%SetInstrument(i)``$E0 i`
- `%SetPan(p)``$E1 p`
- `%SetTempo(t)``$E7 t`
- `%SetChannelVolume(v)``$ED v`
- `%CallSubroutine(label, reps)``$EF` + 16-bit pointer + `reps`
- `%SetDurationN(!4th, vel)` → duration prefix + note byte; duration constants map to the table above.
- Note names in macros (e.g., `%C4`, `%F#3`) map directly to tracker pitches (C1 = `$80`).
### Durations and Snapping
- Quarter = 72 ticks, Eighth = 36, Sixteenth = 18, Triplets use the `T` constants (e.g., `!4thT` = 48).
- Piano roll snap defaults to sixteenth (18 ticks); turn snap off to place micro-timing.
| `!8th` | `$24` | Eighth note (36 ticks) |
| `!8thD` | `$36` | Dotted eighth (54 ticks) |
| `!8thT` | `$18` | Eighth triplet (24 ticks) |
| `!16th` | `$12` | Sixteenth (18 ticks) |
| `!32nd` | `$09` | Thirty-second (9 ticks) |
**Instrument Helpers:**
```asm
%Piano() ; SetInstrument($18)
%Strings() ; SetInstrument($09)
%Trumpet() ; SetInstrument($11)
%Flute() ; SetInstrument($16)
%Choir() ; SetInstrument($15)
%Tympani() ; SetInstrument($02)
%Harp() ; SetInstrument($0F)
%Snare() ; SetInstrument($13)
```
**Note Constants (Octaves 1-6):**
- Format: `C4`, `C4s` (sharp), `D4`, etc.
- Range: `C1` ($80) through `B6` ($C7)
- Special: `Tie` ($C8), `Rest` ($C9), `End` ($00)
### Expanded Bank Hooking
Oracle of Secrets expands music by hooking the `LoadOverworldSongs` routine:
```asm
org $008919 ; LoadOverworldSongs
JSL LoadOverworldSongsExpanded
LoadOverworldSongsExpanded:
LDA.w $0FFF : BEQ .light_world
; Load expanded bank for Dark World
LDA.b #SongBank_OverworldExpanded_Main>>0
STA.b $00
; ... set bank pointer
RTL
.light_world
; Load vanilla bank
; ...
```
**yaze Implementation Strategy:**
1. Detect if ROM has expanded music patch applied
2. Parse expanded song table at `SongBank_OverworldExpanded_Main`
3. Allow editing both vanilla and expanded songs
4. Generate proper ASM patches for new songs
### Proposed Advanced Macros
The Oracle of Secrets documentation proposes these macros for cleaner composition (not yet implemented there, but yaze could pioneer):
```asm
; Define a reusable measure
%DefineMeasure(VerseMelody, !8th, C4, D4, E4, F4, G4, A4, B4, C5)
%DefineMeasure(VerseBass, !4th, C2, G2, A2, F2)
; Use in channel data
.Channel0
%PlayMeasure(VerseMelody, 4) ; Play 4 times
db End
.Channel1
%PlayMeasure(VerseBass, 4)
db End
```
**Benefits:**
- Channel data reads like a high-level arrangement
- Automatic subroutine address calculation
- Reduced code duplication
---
## Technical Reference
### N-SPC Command Bytes
| Byte | Macro | Params | Description |
|------|-------|--------|-------------|
| $E0 | SetInstrument | 1 | Select instrument (0-24) |
| $E1 | SetPan | 1 | Stereo pan (0-20, 10=center) |
| $E2 | PanFade | 2 | Fade pan over time |
| $E3 | VibratoOn | 3 | Enable pitch vibrato (delay, rate, depth) |
| $E4 | VibratoOff | 0 | Disable vibrato |
| $E5 | SetMasterVolume | 1 | Global volume |
| $E6 | MasterVolumeFade | 2 | Fade master volume |
| $E7 | SetTempo | 1 | Song tempo |
| $E8 | TempoFade | 2 | Gradual tempo change |
| $E9 | GlobalTranspose | 1 | Transpose all channels |
| $EA | ChannelTranspose | 1 | Transpose single channel |
| $EB | TremoloOn | 3 | Volume oscillation |
| $EC | TremoloOff | 0 | Disable tremolo |
| $ED | SetChannelVolume | 1 | Per-channel volume |
| $EE | ChannelVolumeFade | 2 | Fade channel volume |
| $EF | CallSubroutine | 3 | Call music subroutine (addr_lo, addr_hi, repeat) |
| $F0 | VibratoFade | 1 | Fade vibrato depth |
| $F1 | PitchEnvelopeTo | 3 | Pitch slide to note |
| $F2 | PitchEnvelopeFrom | 3 | Pitch slide from note |
| $F3 | PitchEnvelopeOff | 0 | Disable pitch envelope |
| $F4 | Tuning | 1 | Fine pitch adjustment |
| $F5 | EchoVBits | 3 | Echo enable + volumes |
| $F6 | EchoOff | 0 | Disable echo |
| $F7 | EchoParams | 3 | Echo delay/feedback/filter |
| $F8 | EchoVolumeFade | 3 | Fade echo volumes |
| $F9 | PitchSlide | 3 | Direct pitch slide |
| $FA | PercussionPatchBass | 1 | Set percussion base instrument |
### ARAM Layout (Extended)
| Address | Contents |
|---------|----------|
| $0000-$00EF | Zero Page |
| $00F0-$00FF | APU Registers |
| $0100-$01FF | Stack |
| $0200-$07FF | Sound driver code |
| $0800-$17BF | SPC Engine (expanded) |
| $17C0-$2AFF | SFX Data |
| $2B00-$3BFF | Auxiliary song pointers |
| $3C00-$3CFF | Sample pointers |
| $3D00-$3DFF | Instrument definitions |
| $3E00-$3FFF | SFX instrument data |
| $4000-$CFFF | Sample data (~36KB) |
| $D000-$FFBF | Song data (~12KB) |
| $FFC0-$FFFF | IPL ROM |
### Instrument IDs (Extended)
| ID | Name | Notes |
|----|------|-------|
| $00 | Noise | White noise generator |
| $01 | Rain | Rain/ambient |
| $02 | Tympani | Timpani drums |
| $03 | Square wave | 8-bit synth |
| $04 | Saw wave | Sawtooth synth |
| $05 | Sine wave | Pure tone |
| $06 | Wobbly lead | Modulated synth |
| $07 | Compound saw | Rich saw |
| $08 | Tweet | Bird/high chirp |
| $09 | Strings A | Orchestral strings |
| $0A | Strings B | Alternate strings |
| $0B | Trombone | Brass |
| $0C | Cymbal | Crash/ride |
| $0D | Ocarina | Wind instrument |
| $0E | Chime | Bell/chime |
| $0F | Harp | Plucked strings |
| $10 | Splash | Water/impact |
| $11 | Trumpet | Brass lead |
| $12 | Horn | French horn |
| $13 | Snare A | Snare drum |
| $14 | Snare B | Alternate snare |
| $15 | Choir | Vocal pad |
| $16 | Flute | Wind melody |
| $17 | Oof | Voice/grunt |
| $18 | Piano | Keyboard |
---
## Implementation Notes
### ASM Export Strategy
When exporting to `music_macros` format:
1. **Header Generation:**
- Calculate `!ARAMAddr` based on target bank position
- Generate intro/loop section pointers
- Create channel pointer table with `!ARAMC` offsets
2. **Channel Data:**
- Convert `TrackEvent` objects to macro calls
- Use instrument helper macros where applicable
- Apply duration optimization (only emit when changed)
3. **Subroutine Extraction:**
- Detect repeated note patterns across channels
- Extract to `.subXXX` labels
- Replace inline data with `%CallSubroutine()` calls
4. **Naming Convention:**
- Prefer semantic names: `.MelodyVerseA`, `.BasslineIntro`, `.PercussionFill1`
- Fall back to numbered: `.sub001`, `.sub002` if semantic unclear
### Bank Space Management
**Constraints:**
- Main bank: ~12KB at `$D000-$FFBF`
- Echo buffer can consume song space if delay > 2
- Subroutines shared across all songs in bank
**Overflow Handling:**
1. Calculate total song size before save
2. Warn user if approaching limit (>90% used)
3. Suggest moving songs to auxiliary bank
4. Auto-suggest subroutine deduplication
---
## References
- [spannerisms ASM Music Guide](https://spannerisms.github.io/asmmusic)
- [Oracle of Secrets GitHub](https://github.com/scawful/Oracle-of-Secrets)
- [SNES APU Documentation](https://wiki.superfamicom.org/spc700-reference)
- Oracle of Secrets `Core/music_macros.asm` (comprehensive macro library)
- Oracle of Secrets `Music/expanded.asm` (bank expansion technique)
- Hyrule Magic source code (tracker.cc)