Music Engine Description
There are two separate music engines in Zelda 2. Both are in bank 6 (along
with the song data). The music engine for the title screen resides at
$8000
and the engine for the rest of the game is at
$9000
. Each of these engines is called once per frame during the
appropriate part of the game.
Changes to the music engine should be very easy to make, as there is an abundance of free space in bank 6.
Contents
- 1 Title Music Engine
- 2 Main Music Engine
- 2.1 Data Tables
- 2.2 Variables
- 2.3 Routines
- 2.3.1 Main Music Loop $9000
- 2.3.2 Configure Pulse1 Channel $9031
- 2.3.3 Configure Pulse2 Channel $9038
- 2.3.4 Play Note Pulse1 $9042
- 2.3.5 Play Note $9044
- 2.3.6 Play Note Pulse2 $9067
- 2.3.7 Play Note Triangle $906B
- 2.3.8 Get Note Duration $906F
- 2.3.9 Vibrato Pulse1 $9089
- 2.3.10 Vibrato Pulse2 $9097
- 2.3.11 Vibrato $909E
- 2.3.12 Play E9 SFX $920B
- 2.3.13 Play EF SFX $92F4
- 2.3.14 Play EE SFX $9408
- 2.3.15 Play Noise $959D
- 2.3.16 Crumble Bridge SFX $956C
- 2.3.17 SFX Noise Decay $9587
- 2.3.18 Play ED SFX $95A7
- 2.3.19 Crumble Block / Enemy Hurt SFX $95E6
- 2.3.20 Crumble Block / Enemy Hurt SFX Decay $95EE
- 2.3.21 Play Noise Samples $9612
- 2.3.22 Sword Strike SFX $9604
- 2.3.23 Play EC SFX $990B
- 2.3.24 Play Music $9B18
- 2.3.25 Next Song $9B24
- 2.3.26 Mute All $9B3B
- 2.3.27 Load Song $9B61
- 2.3.28 Load Song Data $9B80
- 2.3.29 Play Notes
Title Music Engine
TODO document further.
Main Music Engine
The main loop for starts at bank 6 $9000
Data Tables
There are several data tables used by the music engine.
Pulse Envelope
This table is used to set the volume of the pulse channels over time. The index used starts at a certain value and decrements, so the table is in reverse order. The index is halved before use, so each value lasts two frames.
90 | 91 | 91 | 91 | 92 | 92 | 92 | 92 |
93 | 93 | 94 | 94 | 94 | 95 | 95 | 95 |
96 | 96 | 96 | 97 | 97 | 97 | 98 | 98 |
Duration LUT
This table is used to determine the duration of a note. The note data is stored with 5 bits of pitch information and 3 bits of duration, as such:
D1 D0 P4 P3 P2 P1 P0 D2
The Get Note Duration routine shifts things around to get the D bits into the
three least significant bits and masks it before use. This value is then added
to the tempo value in $E5
which is always a multiple of 8 to index
into the following table:
00 | 04 | 0C | 08 | 10 | 18 | 20 | 05 | 06 |
08 | 04 | 0F | 09 | 12 | 1B | 24 | 06 | 06 |
10 | 05 | 0F | 0A | 14 | 1E | 28 | 07 | 06 |
18 | 06 | 12 | 0C | 18 | 24 | 30 | 08 | 10 |
20 | 07 | 15 | 0E | 1C | 2A | 38 | 13 | 12 |
28 | 07 | 15 | 0E | 1C | 2A | 38 | XX | XX |
The last two values in each row seem a bit strange, as they are not nice multiples of the first entry which is usually thought of as the length of an eighth note. The last row has garbage values in those positions as that is the start of another data table, so they are not even listed here.
TODO Check what values are used in vanilla.
Pitch LUT
This table is used to detemine the pitch of a note. The note data is stored as described above, and the fact that the least significant bit is masked out is advantageous since the pulse channels can use 11 bits of timer data, so each entry in this table is actually two bytes wide.
Rather than show the raw bytes here, it's more useful to show the rough note that those values represent. If the raw values are interesting to you, check the ROM for them. It's worth noting that some of the higher notes are several cents off from the listed note.
00 | 02 | 04 | 06 | 08 | 0a | 0c | 0e | |
00 | C3 | --- | E3 | G3 | G#3 | A3 | A#3 | B3 |
10 | C4 | C#4 | D4 | D#4 | E4 | F4 | F#4 | G4 |
20 | G#4 | A4 | A#4 | B4 | C5 | C#5 | D5 | D#5 |
30 | E5 | F5 | F#5 | G5 | A5 | A#5 | B5 | C#3 |
40 | D3 | D#3 | F3 | F#3 | G#5 | C6 | C#6 | D6 |
50 | D#6 | E6 | F6 | F#6 | G6 | G#6 | A6 | A#6 |
60 | B6 | C7 | C#7 | D7 | D#7 | E7 | F7 | F#7 |
70 | G7 | G#7 | A7 | A#7 | B7 | C8 |
Note that although the table goes through $7A
values above
$3E
aren't usable due to the 5 bit limit for pitch data.
Value $02
represents a rest.
Noise Samples
Several sound effects use the same routine to load "samples" for the noise channel to play. These values have the noise volume in the high nybble and the noise period in the low nybble.
$90E8 - Sword slash $9123 - Enemy hurt $912C - Crumble block
Variables
This is just a dump of RAM addresses used by the music engine and a short description of what they are.
$00E0 - Current phrase note data address (low byte) $00E1 - Current phrase note data address (high byte) $00E2 - Song offset, converted from song id to be an index instead of a bit field $00E3 - Current phrase index $00E4 - Drums loop start $00E5 - Tempo $00E6 - Pulse 1 low bits (used for vibrato effect) $00E7 - Pulse 2 low bits (used for vibrato effect) $00E8 - Pitch storage, temporary $00E9 - Play sound effect $00EA - Disable music $00EB - Request new song $00EC - Play sound effect $00ED - Play sound effect $00EE - Play sound effect $00EF - Play sound effect
$0707 - Current world $075F - Queued song, loaded after screen transitions $07DB - Song to resume after fanfare $07DF - ??? Likely sound effect flag for pulse 1 channel override
$07E0 - ??? Likely sound effect flag for noise channel $07E2 - Pulse 2 envelope index $07E3 - Pulse 1 envelope index $07E4 - Drums current note duration $07E5 - Bass current note duration $07E6 - Harmony current note duration $07E7 - Melody current note duration $07E8 - Drums next note index $07E9 - Bass next note index $07EA - Harmony next note index $07EB - Melody next note index
$07F5 - Sound FX counter
$07FB - Current song $07FD - Current SFX (ED) $07FE - ??? Likely sound effect flag for pulse 2 channel override $07FF - ??? Likely sound effect flag for pulse 1 channel override
Routines
This is a non-exhaustive list of the routines in bank 6 involving the playing of music and sound effects. Each routine also lists its address so you can search in a disassembly for the actual code for that routine if these descriptions aren't clear.
Main Music Loop $9000
Checks that the music disable flag is off, then runs several SFX routines. After the SFX routines, the main music routine runs. After all this, various music related memory locations are cleared.
Configure Pulse1 Channel $9031
Takes registers x and y and configures the APU registers $4000
and $4001
respectively.
Configure Pulse2 Channel $9038
Takes registers x and y and configures the APU registers $4004
and $4005
respectively.
Play Note Pulse1 $9042
Takes a pitch index in register a and plays the note on the pulse 1 channel.
Play Note $9044
Called by the various play note routines. Takes a pitch index in register a and
uses register x as a channel selector. For the pulse channels, this routine
also saves the lower 8 bits of the APU timer register to $E6
or $E7
which
are used for a vibrato effect.
Play Note Pulse2 $9067
Takes a pitch index in register a and plays the note on the pulse 2 channel.
Play Note Triangle $906B
Takes a pitch index in register a and plays the note on the triangle channel.
Get Note Duration $906F
Takes note data in register a and looks up the duration. The original note
data is saved in register x and the result of the duration lookup is put in
register a. The duration lookup is offset by the tempo stored in $E5
.
Vibrato Pulse1 $9089
Applies a vibrato effect on the pulse 1 channel. Takes the duration of the note left in register a and the APU timer low bits in register y.
Vibrato Pulse2 $9097
Applies a vibrato effect on the pulse 2 channel. Takes the duration of the note left in register a and the APU timer low bits in register y.
Vibrato $909E
Takes the duration of the note remaining in register a, the low bits of the APU timer in register y, and the channel to use in register x. This is called by the other vibrato routines after setting x appropriately.
Uses the $04
bit of the duration to either add or subtract 2 from the low
timer bits. Only the low bits are used because writing to the high bits resets
the phase of the channel which can cause clicks. None of the notes from the
pitch LUT are close enough to the boundary for the change to require the high
bits to change anyway, so this works out fine.
Play E9 SFX $920B
Plays the sound effect requested in $E9
.
TODO document further.
Play EF SFX $92F4
Plays the sound effect requested in $EF
.
TODO document further.
Play EE SFX $9408
Plays the sound effect requested in $EE
.
TODO document further.
Play Noise $959D
Configures the APU noise registers to make a noise burst. Takes configuration
for registers $400F
, $400E
, and $400C
in registers y, x, and a.
Crumble Bridge SFX $956C
Starts playing the sound effect for a crumble bridge crumbling. This uses both the noise and the triangle channels so it sets flags to disable the music on those channels. Continues with SFX Noise Decay.
SFX Noise Decay $9587
Counts down the timer in $07F5
and turns off the noise when it reaches 0.
Also clears the flag for the ED sound effect in $07FD
.
Play ED SFX $95A7
Plays the sound effect request in $ED
or continues the sound effect saved in
$07FD
. Existing sound effects which are higher than the current sound effect
will be ignored. If the same sound effect is requested that is already playing,
it will restart the sound effect except for $80
which gives priority to
continuing. When the sound is continued instead of newly played, the decay
routine will be called instead.
$02
- Crumble Bridge
$04
and$10
- Crumble Block / Enemy Hurt
$20
and$80
- Sword Strike
$40
- Very short noise burst, probably unused. Doesn't store the effect, and doesn't have an associated decay.
Crumble Block / Enemy Hurt SFX $95E6
Starts playing the sound of a block breaking or an enemy getting hurt.
Crumble Block / Enemy Hurt SFX Decay $95EE
Looks up noise register values based on the timer and the sound effect value.
Crumble Block samples are at $912C
while Enemy Hurt is at $9123
. Continues
on to Play Noise Samples.
Play Noise Samples $9612
Takes a sample value in register a. The low four bits of this are used to set
the noise period via APU register $400E
. The high four bits are used to set
the noise volume via APU register $400C
.
Continues on the SFX Noise Decay process.
Sword Strike SFX $9604
Starts playing the sound of swinging your sword. This looks up samples based on
the duration left at $90E8
and plays them via Play Noise Samples.
Play EC SFX $990B
Plays the sound effect requested in $EC
.
TODO document further.
Play Music $9B18
Plays the music for the game.
Runs Load Song if a new song is requested in $EB
. Otherwise, runs Play Notes
if the current song is set in $07FB
. If neither of these is true, do nothing.
Next Song $9B24
Checks if the previous song should loop. If not, calls Mute All and returns.
If the previous song was $01
(the "intro" to the main theme), sets the next
song to $02
(the main theme loop).
If the previous song was $10
(the item get / level up fanfare), sets the
current song from $07DB
. Otherwise, leaves the current song the same as the
previous song.
Proceeds to Load Song.
Mute All $9B3B
Clears the current song at $07FB
and sets the APU registers to mute the volume
on both pulse channels, the triangle channel, and the noise channel.
Load Song $9B61
Takes a song id in register a and preps some variables for that song.
If the song is $10
(item get / level up fanfare) then it first saves the
previous song to $07FB
if it is less than $10
so that the previous music
will resume after the fanfare.
Sets the phrase counter at $E3
to 0 and the song offset at $E2
based on the
song id. Song ids are a bit field, and the song offset is the position of the
lowest bit in the song id for indexing into the song table.
Proceeds to Load Song Data.
Load Song Data $9B80
Loads the music variables based on the current world and current song offset.
There are four blocks of code that are basically the same, for each of the four different music tables. Each one just uses a different position for the tables to load.
Based on the current world in $0707
it loads data from these offsets:
$00
- Overworld music -
$9B87
$01
or $02
- Town music -
$9BC6
$03
or $04
- Palace music -
$9C05
$05
or higher
- Great Palace music -
$9C42
First, uses the song offset in $E2
to index into the song table at the base
offset given above.
The phrase counter at $E3
is added to that and used as in index again to
retrieve the current phrase offset. If the current phrase offset is $00
that
means the song is over and the process goes back to the Next Song routine.
Otherwise, the following structure is read at the base offset above + phrase offset just calculated.
$E5
Tempo$E0
Note Data Low$E1
Note Data High$07E9
Bass Note Pointer$07EA
Harmony Note Pointer$07E8
Drum Note Pointer
After this, the drum loop start point at $E4
is also set to the same value as
the drum note pointer. The melody note pointer at $07EB
is set to $00
, and
all the durations (melody at $07E7
, harmony at $07E6
, bass at $07E5
, and
drums at $07E4
) are set to $01
.
Proceeds to Play Notes.
Play Notes
This routine is broken into four major and similar sections: one each for melody, harmony, bass, and drums. The general process is as follows:
decrement duration if duration == 0 { load next note increment note pointer set duration for note if no overriding sound effect { play note on appropriate channel } }
Some channels have unique processing at different points in this. For example,
the melody note data is null terminated, so if the loaded note in $00
then the
routine will jump to Load Song Data instead to go to the next phrase. If the
drums channel gets a $00
note it will reset the drums note pointer to the
drums loop start and repeat the phrase. The bass and harmony channels don't
have this looping behavior. This means that if the bass or harmony channels
aren't long enough, the engine will happily just play whatever data comes next
in the ROM.
The melody is played on pulse 2 channel, the harmony on pulse 1 (which gets subverted by sound effects that need just one pulse channel), the bass on the triangle channel, and the drums on the noise channel.
For the pulse channels, after the note is played, the envelope counter for that
channel ($07E2
for pulse 1, $07E3
for pulse 2) is set. Normally the
envelope is set to $2F
but song $40
(crystal fanfare and final boss music)
sets it to $18
instead. The envelope is not set if no note was played because
the pitch index was $02
which indicates a rest.
Also for the pulse channels, after the above pseudocode, the vibrato and
enveloping happens as long as there is no overriding sound effect. The envelope
works by checking the envelope counter set previously and decrementing it. The
counter value is halved and used to look up the envelope value in the LUT at
$9135
. These values are written directly to APU register $4000
or $4004
depending on the channel.
The bass channel has one special handling as well. The APU register $4008
is
set differently based on the current song id. For song $10
(item get / level
up fanfare) the register is set to $60
but all other songs use $1f
.
The noise channel is not affect at all by the pitch. Any pitch value that is non zero will play a burst of noise.