Music Engine Description

From BindingForce Wiki
Revision as of 05:08, 19 August 2023 by Bentglasstube (talk | contribs) (→‎Duration LUT)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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.

Title Music Engine

The title music engine is different from the engine that plays music during the rest of the game. In particular, it has a wider range of notes that can be represented. If you want to look into the actual code, the title screen music engine main loop is at $8000 (vesus $9000 for the game music loop).

The metadata for the songs is all the same as in the other areas, although the title music is actually broken up into several songs itself. These song boundaries control the timing of the title scroll.

The following songs exist in the title song table:

  1. Intro
  2. Start
  3. Build up
  4. Main
  5. Breakdown

The start of song 2 triggers the title to scroll into view. The start of song 4 triggers a countdown until the story scroll begins. After song 5 finishes, the engine loops back to song 2.

Within the note data, a special format is used. Rather than encoding the duration and pitch together in a single byte, the title music has "pitch" bytes and "duration" bytes. Any byte with the highest bit set (i.e. anything >= $80) is interpreted as a duration change, which sets the duration of all future notes. Anything else is a pitch value which indicates a note of the current duration should be played. The low nybble of duration values keys into a lookup table at bank 6 $8084 and store the duration byte at $07FF. Values are as follows:

  • $80 - 8 ticks (sixteenth note)
  • $81 - 24 ticks (dotted eighth note)
  • $82 - 16 ticks (eighth note)
  • $83 - 32 ticks (quarter note)
  • $84 - 48 ticks (dotted quarter note)
  • $85 - 64 ticks (half note)
  • $86 - 96 ticks (dotted half note)
  • $87 - 128 ticks (whole note)
  • $88 - 11 ticks (eighth note triplet, first two)
  • $89 - 10 ticks (eighth note triplet, third)
  • $8A - 80 ticks (half note + eighth note)

The pitch values are stored in a lookup table at bank 6 from $808F to $810E. Any value from $00 to $7F represents an entry in this table. $46 is A4 and every semitone away from that adds or subtracts 2 from the value. Thus, C5 (three semitones above A4) is $4C. As in the game music engine, the value $02 represents a rest. Unlike the game music engine, $00 also represents a rest but the code has special handling for $02 so using that is recommended.

      -2-  -3-  -4-  -5-  -6-  -7-
 C  : $04  $1C  $34  $4C  $64  $7C
 C# : $06  $1E  $36  $4E  $66  $7F
 D  : $08  $20  $38  $50  $68
 Eb : $0A  $22  $3A  $52  $6A
 E  : $0C  $24  $3C  $54  $6C
 F  : $0E  $26  $3E  $56  $6E
 F# : $10  $28  $40  $58  $70
 G  : $12  $2A  $42  $5A  $72
 G# : $14  $2C  $44  $5C  $74
 A  : $16  $2E  $46  $5E  $76
 Bb : $18  $30  $48  $60  $78
 B  : $1A  $32  $4A  $62  $7A

By way of an example, let's look at the main section of the vanilla title music. The title metadata starts at bank 6 $84DA:

   08 11 14 16 19 1E 1E 1E
            ^ Main section

The main section is the fourth song, starting at $84DA + $16 = $84F0:

   3F 3F 00

This means there is a single phrase for the main section, which is repeated twice. The phrase data is at $84DA + $3F = $8519.

   00 3D 86 3A 1A 5F

From this, we see the melody note data is located at $863D:

   83 Quarter notes
   02 Rest
   48 C5
   82 Eighth notes
   46 B4
   83 Quarter notes
   3E G4
   34 D4
   84 Dotted quarter notes
   2E B3
   83 Quarter notes
   30 C4
   34 D4
   3A F4
   38 E4
   34 D4
   30 C4
   82 Eighth notes
   34 D4
   83 Quarter notes
   30 C4
   85 Half notes
   2E B3
   82 Eighth notes
   02 Rest
   00 End of Data

Main Music Engine

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.

Pulse Envelope $9135
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:

Duration LUT $914D
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 09 0A

The base value of each row is the third one, which represents an eighth note. The other values are usually related numbers to give these normal note lengths:

  1. Sixteenth note†
  2. Dotted eighth note†
  3. Eighth note
  4. Quarter note
  5. Dotted quarter note
  6. Half note

The last two columns are used to do various kind of triplets. Since the values in the table are not always divisible by three, triplets need to be made by rounding. However, that would introduce error in the length of the notes, so a second value is used for the final triplet to correct that error. For example, in row $10, one would use $07, $07, $06 for eighth note triplets. These lengths add up to 20 which is exactly twice the eighth note length of 10 in that row.

† Row $08 has weird values for columns 0 and 1 that are best avoided.

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.

Pitch LUT $918F
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

Value $02 represents a rest.

Note that although the table goes through $7A values above $3E aren't usable due to the 5 bit limit for pitch data in the song data. However, the sound effects routines make use of these extra notes. For example, the sword beam effect alternates between playing C7 and F6.

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


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
$07DA - Ganon Laugh sample
$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
$07EC - Ganon Laugh counter
$07ED - Sound FX counter (E9)
$07EE - Sound FX counter (E9)
$07F5 - Sound FX counter (ED)
$07FA - Current SFX (E9)
$07FB - Current song
$07FD - Current SFX (ED)
$07FE - ??? Likely sound effect flag for pulse 2 channel override
$07FF - Current SFX (EF)


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.

Crumble Bridge
$04 and $10
Crumble Block / Enemy Hurt
$20 and $80
Sword Strike
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:


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.