Skip to navigation

Elite on the BBC Micro and NES

Music in NES Elite

How David Whittaker's music module plays The Blue Danube

NES Elite has four tunes in it. Two of them are converted from the Commodore 64 version ("Elite Theme" and "The Blue Danube"), while two are brand new compositions by David Whittaker ("Game Theme" and "Assassin's Touch"). There are five tune numbers, with the last one combining the two new tunes into one (as they always play one after the other in the combat demo).

The tune numbers are as follows:

  • 0 for the title music ("Elite Theme"), which is set in the TITLE routine and as the default tune in the ResetMusic routine
  • 1 for docking ("The Blue Danube"), which is set in the TT102 routine
  • 2 for the combat demo music ("Game Theme"), though this is never set directly, only via tune 4
  • 3 for the scroll text music ("Assassin's Touch"), though this is never set directly, only via tune 4
  • 4 for the full combat demo suite ("Assassin's Touch" followed by "Game Theme"), which is set in the DEATH2 routine

You can listen to all these tunes in the Video Game Music Preservation Foundation's entry on NES Elite.

As with the sound routines, which you can read about in the deep dive on sound effects in NES Elite, the music routines were written by David Whittaker, one of the most prolific sound artists of the 8-bit era. His CV is really quite astonishing - here's a biography that includes a list of games on which he worked. It takes a lot of scrolling to get to the end...

Let's see how the music routines work.

The music routines

Music is played by the ChooseMusic routine, which takes a tune number as an argument. The routine initialises four 19-byte blocks of memory, one for each of the APU channels; the block for the SQ1 channel, for example, runs from sectionDataSQ1 to applyVolumeSQ1. Not every channel uses every variable (the NOISE channel doesn't have an applyVolumeNOISE variable, for example), but the same block structure is used anyway, betraying the fact that David Whittaker's sound routines were almost certainly generated by macros.

Once the music data blocks are set up, the actual playing of the music is taken on by the NMI handler. As with the sound routines, the NMI handler calls the MakeSounds routine in every VBlank, which then calls the MakeMusic routine to play the music. This in turn calls MakeMusicOnSQ1, MakeMusicOnSQ2, MakeMusicOnTRI and MakeMusicOnNOISE to send the music data to the APU.

The data that is sent to the APU for each tune is defined in the tuneData table. This table contains the bulk of the data for the game music (though there are two other tables, volumeEnvelope for the volume envelope data and pitchEnvelope for the pitch envelope data). All these tables are implemented using linked trees of varying depths, so let's take a look at that now.

Linked tree format

The bulk of the music data is in the tuneData table. There are five tunes, numbered 0 to 4. The data for each tune is stored as a linked tree, with the data at level 3, so this would be the path in tuneData to reach the first block of music data for the SQ1 channel in the first part of the first tune:

  tune0Data -> tune0Data_SQ1 -> tune0Data_SQ1_0

Let's look at each level in turn. Each tune has a lookup table at the start of tuneData that consists of a single byte (containing the tune speed) followed by the addresses of four blocks, one for each channel. So tune 0 has the following lookup table at tune0Data, for example:

  EQUB 47
  EQUW tune0Data_SQ1
  EQUW tune0Data_SQ2
  EQUW tune0Data_TRI
  EQUW tune0Data_NOISE

So tune 0 is played with speed 47, and then there are four addresses, one for each sound channel. These addresses each point to another list of links, which contain the addresses of each section in the tune. So, for example, we have the following section links in tune0Data_SQ1 for tune 0's SQ1 channel:

  EQUW tune0Data_SQ1_0
  EQUW tune0Data_SQ1_0
  EQUW tune0Data_SQ1_1
  EQUW tune0Data_SQ1_2
  EQUW tune0Data_SQ1_1
  EQUW tune0Data_SQ1_2
  EQUW tune0Data_SQ1_1
  EQUW tune0Data_SQ1_3
  EQUW tune0Data_SQ1_1
  EQUW tune0Data_SQ1_4
  EQUW 0

These blocks of section links are typically terminated by a null address, at which point the tune wraps around to the start again. If there is no null terminator, then the tune will keep playing by moving on to the sections in the next list in memory, which it will play until a null terminator is found.

Each of these section addresses points to the actual note data for each section, so that's tune0Data_SQ1_0 in our example, which contains the following music data for the SQ1 channel for the first section of tune 0:

  EQUB $FA, $70, $F7, $05, $F6, $09, $65, $0C
  EQUB $0C, $0C, $63, $07, $61, $07, $63, $07
  EQUB $07, $FF

These blocks contain note data in the command format described below, and define what gets sent to the APU for this section of the tune.

Note that tunes 3 and 4 share data, so in these cases there are two labels applied to the shared data (so tune3Data_SQ1_0 and tune4Data_SQ1_0 point to the same data, for example).

Music data format

The note data consists of notes, rests and commands. If A is a byte from the note data (such as the example shown above), then it is interpreted as follows:

  • If $00 <= A <= $5F then this is a note, so send it to the APU after converting the note value (which is a frequency) into an APU period (so higher values of A are higher notes with shorter periods). This conversion is done via the noteFrequency table.
  • If $60 <= A <= $7F then this is a rest, with the length of the rest given by A - $5F (so if A = $60 the rest length is one iteration, and if A = $7F the rest length is 32 iterations).
  • If A >= $80 (i.e. bit 7 is set) then this is a command byte. The commands are between one and three bytes in size, and are listed in the table below.

The commands are as follows:

<$F4 $xx>Set the playback speed to $xx
<$F5 $xx $yy>Change tune to the tune data at address $yyxx (so if $yyxx is tune2Data_SQ1, then this will jump to the start of tune 2, assuming this is the SQ1 data)
<$F6 $xx>Set the volume envelope number to $xx
<$F7 $xx>Set the pitch envelope number to $xx
<$F8>Set the volume of the current channel to zero
<$F9>Enable the volume envelope for the current channel
<$FA %ddlc0000>Configure the volume of the current channel in the APU: duty %dd, loop %l, const %c
<$FB $xx>Set the tuning for all channels to $xx (so this adds $xx to the pitch of all notes)
<$FC $xx>Set the tuning for the current channel to $xx (so this adds $xx to the pitch of all notes on the specified channel)
<$FD $xx>Set the sweep for the current channel to $xx
<$FE>Stop the music and disable sound
<$FF>Move to the next section in the current tune

If a command byte is fetched that doesn't appear in the above list, then it is ignored.

The volume envelope data can be found in the volumeEnvelope table, and the pitch envelope in the pitchEnvelope table. The changes in volume and pitch defined in these envelopes are applied to the music by four routines, one for each channel: ApplyEnvelopeSQ1, ApplyEnvelopeSQ2, ApplyEnvelopeTRI and ApplyEnvelopeTRI.

The values in these tables get applied to the channel volume and pitch using the envelopes set by the $F6 and $F7 commands.

For the volume envelope, the first byte indicates how many VBlanks each value in the envelope should be applied for, so this effectively sets the speed of the envelope. A value with bit 7 set indicates that this is the last entry in the envelope.

The envelope is applied one step at a time, by setting the channel's volume in the APU as follows:

  • The high nibble is taken from that value that's set via command byte $FA and which contains the duty, loop and NES envelope settings to send to the APU
  • The low nibble (i.e. the volume) is set to the low nibble of the byte from the envelope

So the volume envelope describes how the volume of the music on each channel changes with each new VBlank.

The pitch envelope is similar, but it affects the pitch. The pitch envelope is always applied at a rate of one byte per VBlank, with a value of $80 indicating the end of the envelope (in which case we wrap around to the start of the envelope). The current byte in the envelope gets added to the low byte of the pitch that's sent to the APU, so the pitch varies by the amount in each byte of the pitch envelope, and because the addition is signed, envelope values greater than $80 reduce the pitch.

Overall, the music player is pretty flexible. Hats off to David Whittaker for creating a system that not only brought these iconic tunes to Elite on the NES, but for composing two of them as well. That's some talent!