15 KiB
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) MusicBankmanager for ROM loading/savingSpcParserandSpcSerializerBrrCodecfor sample handling
Sprint 2: Tracker View (Read-Only)
Status: Complete
TrackerViewcomponent 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
PianoRollViewcomponent (Horizontal)- Mouse-based note entry/editing
- Zoom controls
- Integrated with
MusicEditorstate
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
MusicEditortabs.
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
MusicEditorstate to browser actions.
Sprint 8: Export & Polish
Status: In Progress
- ✅ Integrated
SpcParser::ParseSong()intoMusicBank::LoadSongTable()so the editor now reflects real ROM song data (vanilla + custom slots). - ROM patching finalization (ensure
SaveToRomworks robustly). - ASM export (
music_macrosformat 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)
- Replace hardcoded colors with theme system -
tracker_view.cc(4 instances),music_editor.cc(3 instances) - Integrate SpcParser into MusicBank -
MusicBank::LoadSongTable()now usesSpcParser::ParseSong() - Fix serializer relocation bug - Track address calculation in
SerializeSongis incorrect - Implement SaveToRom() - Currently returns
UnimplementedError
Medium Priority (Quality)
- Add undo stack size limit - Capped at 50 states with FIFO eviction (
music_editor.cc) - Fix oscilloscope ring buffer wrap-around - Proper mask
& (kBufferSize - 1)(music_editor.cc) - Add VU meter smoothing/peak-hold - Currently uses instantaneous sample values
- 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_AUXat$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
.asmsong files directly - Export songs to
music_macrossyntax - Syntax highlighting for N-SPC commands
- Live preview compilation (ASM → binary → playback)
- Subroutine deduplication detection
Export Format Example:
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 (0xE0–0xFF)
$E0 SetInstrument i— pick instrument indexi(0–24 vanilla ALTTP).$E1 SetPan p— pan left/right ($00hard L,$10center,$1Fhard 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 subroutinerepstimes.$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),$00marks track end.
Instruments (ALTTP table at $3D00)
- Bytes:
[sample][AD][SR][gain][pitch_hi][pitch_lo]sample= BRR slot (0–24 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
SetInstrumentcommand changes these fields for the channel until another$E0appears.
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
Tconstants (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:
%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) throughB6($C7) - Special:
Tie($C8),Rest($C9),End($00)
Expanded Bank Hooking
Oracle of Secrets expands music by hooking the LoadOverworldSongs routine:
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:
- Detect if ROM has expanded music patch applied
- Parse expanded song table at
SongBank_OverworldExpanded_Main - Allow editing both vanilla and expanded songs
- 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):
; 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:
-
Header Generation:
- Calculate
!ARAMAddrbased on target bank position - Generate intro/loop section pointers
- Create channel pointer table with
!ARAMCoffsets
- Calculate
-
Channel Data:
- Convert
TrackEventobjects to macro calls - Use instrument helper macros where applicable
- Apply duration optimization (only emit when changed)
- Convert
-
Subroutine Extraction:
- Detect repeated note patterns across channels
- Extract to
.subXXXlabels - Replace inline data with
%CallSubroutine()calls
-
Naming Convention:
- Prefer semantic names:
.MelodyVerseA,.BasslineIntro,.PercussionFill1 - Fall back to numbered:
.sub001,.sub002if semantic unclear
- Prefer semantic names:
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:
- Calculate total song size before save
- Warn user if approaching limit (>90% used)
- Suggest moving songs to auxiliary bank
- Auto-suggest subroutine deduplication
References
- spannerisms ASM Music Guide
- Oracle of Secrets GitHub
- SNES APU Documentation
- 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)