A song or sound can be composed of multiple instruments or channels. The NES has 5 channels: 2 pulse (square wave) channels, 1 triangle channel, 1 noise channel, and 1 delta modulation channel. Contra maintains 6 slots of data in memory that are used in priority order to play sounds. Higher slots are played before the lower slots. Each slot is linked to an NES sound channel.
- #$00 = pulse 1 channel
- #$01 = pulse 2 channel
- #$02 = triangle channel
- #$03 = noise and dmc channel
- #$04 = pulse 1
- #$05 = noise channel
For example, if a sound is loaded in slot #$00 and slot #$04 (both pulse 1
channel), then the sound in slot #$04 will be played since it is higher. The
code that converts from sound slot to channel is @load_sound_channel_offset
.
When the game loads a sound to play, it first determines which slots are needed
by loading data from sound_table_00
. This table specifies the number of slots
needed to play the sound and where the instructions to play the sound exists,
i.e. the 2 byte cpu address where the sound channel instructions are.
Below is a table of all the sounds that exist in Contra, including unused sounds. The Japanese names were obtained from the "sound mode" feature in the Famicom version of the game. Each sound is related to one or more sound codes. For example, the level 3 waterfall music uses 4 sound codes, which means that sound uses 4 sound channels.
At the bottom of the table are the DPCM samples that are used throughout the game by various other sounds.
Sound | Japanese Name | Description | sound_code(s) | Slot | Command Type | Channel |
---|---|---|---|---|---|---|
#$01 | empty/silence, used to initialize channel | sound_01 |
||||
#$02 | percussive tick (bass drum/tom drum) | sound_02 |
5 | low | noise | |
#$03 | FOOT | player landing on ground or water | sound_03 |
4 | low | pulse 1 |
sound_04 |
5 | low | noise | |||
#$05 | ROCK | waterfall rock landing on ground | sound_05 |
4 | low | pulse 1 |
#$06 | TYPE 1 | unused, keyboard typing in Japanese version of game | sound_06 |
4 | low | pulse 1 |
sound_07 |
5 | low | noise | |||
#$08 | unused, rumbling | sound_08 |
5 | low | noise | |
#$09 | FIRE | energy zone fire beam | sound_09 |
5 | low | noise |
#$0a | SHOTGUN1 | default weapon | sound_0a |
4 | low | pulse 1 |
sound_0b |
5 | low | noise | |||
#$0c | SHOTGUN2 | M weapon, turret man | sound_0c |
4 | low | pulse 1 |
sound_0d |
5 | low | noise | |||
#$0e | LASER | L weapon | sound_0e |
4 | low | pulse 1 |
sound_0f |
5 | low | noise | |||
#$10 | PL FIRE | F weapon | sound_10 |
4 | low | pulse 1 |
sound_11 |
5 | low | noise | |||
#$12 | SPREAD | S weapon | sound_12 |
4 | low | pulse 1 |
sound_13 |
5 | low | noise | |||
#$14 | HIBIWARE | bullet shielded wall plating ting | sound_14 |
4 | low | pulse 1 |
#$15 | CHAKUCHI | energy zone boss landing | sound_15 |
4 | low | pulse 1 |
#$16 | DAMEGE 1 | bullet to metal collision ting | sound_16 |
4 | low | pulse 1 |
sound_17 |
5 | low | noise | |||
#$18 | DAMEGE 2 | alien heart boss hit | sound_18 |
4 | low | pulse 1 |
#$19 | TEKI OUT | enemy destroyed | sound_19 |
4 | low | pulse 1 |
#$1a | HIRAI 1 | ice grenade whistling noise | sound_1a |
4 | low | pulse 1 |
#$1b | SENSOR | level 1 jungle boss siren | sound_1b |
4 | low | pulse 1 |
#$1c | KANDEN | electrocution sound | sound_1c |
4 | low | pulse 1 |
sound_1d |
5 | low | noise | |||
#$1e | CAR | tank advancing | sound_1e |
4 | low | noise |
#$1f | POWER UP | pick up weapon item | sound_1f |
4 | low | pulse 1 |
#$20 | 1UP | extra life | sound_20 |
4 | low | pulse 1 |
#$21 | HERI | helicopter rotors | sound_21 |
4 | low | pulse 1 |
sound_21 |
1 | low | pulse 2 | |||
sound_23 |
5 | low | noise | |||
#$24 | BAKUHA 1 | explosion | sound_24 |
5 | low | noise |
#$25 | BAKUHA 2 | game intro, indoor wall, and island explosion | sound_25 |
5 | low | noise |
#$26 | TITLE | game intro tune | sound_26 |
0 | high | pulse 1 |
sound_27 |
1 | high | pulse 2 | |||
sound_28 |
2 | high | triangle | |||
sound_29 |
3 | high | noise | |||
#$2a | BGM 1 | level 1 jungle and level 7 hangar music | sound_2a |
0 | high | pulse 1 |
sound_2b |
1 | high | pulse 2 | |||
sound_2c |
2 | high | triangle | |||
sound_2d |
3 | high | noise | |||
#$2e | BGM 2 | level 3 waterfall music | sound_2e |
0 | high | pulse 1 |
sound_2f |
1 | high | pulse 2 | |||
sound_30 |
2 | high | triangle | |||
sound_31 |
3 | high | noise | |||
#$32 | BGM 3 | level 5 snow field music | sound_32 |
0 | high | pulse 1 |
sound_33 |
1 | high | pulse 2 | |||
sound_34 |
2 | high | triangle | |||
sound_35 |
3 | high | noise | |||
#$36 | BGM 4 | level 6 energy zone | sound_36 |
0 | high | pulse 1 |
sound_37 |
1 | high | pulse 2 | |||
sound_38 |
2 | high | triangle | |||
sound_39 |
3 | high | noise | |||
#$3a | BGM 5 | level 8 alien's lair music | sound_3a |
0 | high | pulse 1 |
sound_3b |
1 | high | pulse 2 | |||
sound_3c |
2 | high | triangle | |||
sound_3d |
3 | high | noise | |||
#$3e | 3D BGM | indoor/base level music | sound_3e |
0 | high | pulse 1 |
sound_3f |
1 | high | pulse 2 | |||
sound_40 |
2 | high | triangle | |||
sound_41 |
3 | high | noise | |||
#$42 | BOSS | indoor/base boss screen music | sound_42 |
0 | high | pulse 1 |
sound_43 |
1 | high | pulse 2 | |||
sound_44 |
2 | high | triangle | |||
sound_45 |
3 | high | noise | |||
#$46 | PCLR | end of level tune | sound_46 |
0 | high | pulse 1 |
sound_47 |
1 | high | pulse 2 | |||
sound_48 |
2 | high | triangle | |||
sound_49 |
3 | high | noise | |||
#$4a | ENDING | end credits | sound_4a |
0 | high | pulse 1 |
sound_4b |
1 | high | pulse 2 | |||
sound_4c |
2 | high | triangle | |||
sound_4d |
3 | high | noise | |||
#$4e | OVER | game over/after end credits, presented by Konami | sound_4e |
0 | high | pulse 1 |
sound_4f |
1 | high | pulse 2 | |||
sound_50 |
2 | high | triangle | |||
sound_51 |
3 | high | noise | |||
#$52 | PL OUT | player death | sound_52 |
4 | low | pulse 1 |
sound_53 |
5 | low | noise | |||
#$54 | game pausing | sound_54 |
4 | low | pulse 1 | |
#$55 | BOSS BK | tank, boss ufo, boss giant, alien guardian destroyed | sound_55 |
4 | low | pulse 1 |
sound_56 |
5 | low | noise | |||
#$57 | BOSS OUT | boss destroyed | sound_57 |
4 | low | pulse 1 |
sound_58 |
1 | low | pulse 2 | |||
sound_59 |
5 | low | noise | |||
#$5a | n/a | high hat | n/a | n/a | low | delta modulation |
#$5b | n/a | snare | n/a | n/a | low | delta modulation |
#$5c | n/a | high hat | n/a | n/a | low | delta modulation |
#$ff | n/a | snowfield boss defeated door open (bug) | n/a | n/a | low | delta modulation |
The sound for pausing the game is not in the sound mode menu, presumably to not confuse players into thinking the game is paused. As for names, I can guess at some of the abbreviations and name meanings.
- BAKUHA - ばくはつ (爆発) - Japanese for explosion
- BGM - background music
- BK - BAKUHA, i.e. explosion
- CHAKUCHI - ちゃくち (着地) - Japanese for landing/touching the ground
- HIBIWARE - ひびわれ (罅割れ, ひび割れ) - Japanese for crack; crevice; fissure
- PCLR - player clear, or pattern clear
- PL - player
- TYPE - keyboard typing
Every video frame, the game loops through each sound slot to see if a sound is
currently playing, see @sound_slot_loop
. If a sound slot is populated, i.e.
a sound is playing, then handle_sound_code
will be called on that slot.
handle_sound_code
will first check if the game is paused, as the music and
sound effects are paused when the game is paused. If the game isn't paused, the
handle_sound_code
will decrement the current sound slot's sound length
(SOUND_CMD_LENGTH
) and if the sound is finished, move to read the next command
(read_sound_command_00
). If the sound isn't finished, then
@pulse_vol_and_vibrato
is called to possibly adjust the volume and frequency
of the current playing sound. Note frequency adjustments, i.e. vibrato isn't
used by Contra.
A sound_code
is composed of 1 or more sound commands. Each sound command will
configure variables or set APU registers. Not every sound command will make a
sound. Some just configure variables for the subsequent sound code, for example
setting SOUND_LENGTH_MULTIPLIER
for use by a subsequent sound command. The
sound commands are parsed according to the following logic.
The first byte of the entire sound code determines how the rest of the commands for the sound code will be parsed. When the byte is less than #$30, the the sound code is considered a 'low sound' code. Otherwise, the code will be parsed as a 'high sound' code.
All sound command types can reference addresses to other sound commands. When a sound command moves to another command, once that command is finished executing, then the sound read pointer goes back to the next bytes in the original command.
- #$fd - move to child sound command for playing shared sound data across
different
sound_xx
commands, or shared parts within the same sound code. Move to execute sound command at address specified by next 2 bytes. - #$fe - repeat the next sound command at address
a
n
times, wherea
is the next byte andn
is the 2nd and 3rd byte. - #$ff - finished reading sound command, exit to previous command if child sound command, otherwise, finished entire sound code
In Contra, low sound commands are used by sound slots #$01 (pulse 1), #$04
(pulse 2) and #$05 (noise). Low sound commands are used for sound effects.
The method in code for parsing low sound commands is read_low_sound_cmd
. In
general, low sound commands set the length, decrescendo start, pitch, and duty.
The first byte of the sound command dictates how the subsequent bytes are interpreted. The command is read recursively until the note period is set, then the parsing exits.
- Case 1 - #$2x - sets the number of video frames to wait before reading
the next sound command (
SOUND_LENGTH_MULTIPLIER
) as well as the high nibble of the APU configuration register for the sound channel (SOUND_CFG_HIGH
).- when low nibble is not #$f, then
SOUND_LENGTH_MULTIPLIER
is set to low nibble. - when low nibble is #$f, then the next full byte is used to set
SOUND_LENGTH_MULTIPLIER
- the following byte is then used to set high nibble of the APU
configuration register for the sound channel (
SOUND_CFG_HIGH
)
- when low nibble is not #$f, then
- Case 2 - #$10 - enable/disable sweep and set volume decrescendo. What
is set is based on the next byte. Additionally, if the sound slot is #$04,
then the pulse 1 channel
PULSE_VOL_DURATION
will also be set to the sweep value.- non-zero - sweep will be enabled and set to the value of the byte.
- #$00 - if the byte after #$10 is #$00, then sweep will be disabled by setting the APU register $4001 to #$7f. be set to #$7f.
- Case 3 - #$1x - slightly flatten the note that will be played by less
than 1Hz by setting bit 4 of
SOUND_FLAGS
. The low nibble is not used. This case is not used in Contra. However, notes are flattened in another flow, see@flip_flatten_note_adv
. - Case 4 - #$xx - if not #2x, or #$1x, then byte high nibble is used as
the low nibble (volume) for APU channel config. The high nibble and low
nibble from memory are merged (
SOUND_CFG_HIGH
andSOUND_CFG_LOW
respectively), unless volume is constant, then only the high nibble is used when setting the sound channel configuration ($4000). Also, the sound length (SOUND_CMD_LENGTH
), value is set based onSOUND_LENGTH_MULTIPLIER
. Finally, the note frequency/counter/pitch is set based off the next two bytes and then the parsing is complete.
After case 4 is parsed, the read low sound command method exits. Then the game
logic will continue. The next frame, a standard @sound_slot_loop
will pick up
the sound slot is populated and play possibly modify the sound's volume and
frequency for vibrato (not used in Contra). Every video frame, the game logic
repeats this until the sound is completed, the game logic will then continue
by reading the next byte of the next low sound code. This entire process
repeats until a #$ff is read.
In Contra, high sound commands are used by sound slots #$00 (pulse 1), #$01
(pulse 2), #$02 (triangle) and #$03 (noise & dmc channel). High sound commands
are used for the 'music' of the game: the level background music, the intro
tune, end credits song, and after credits/game over music. The method in code
for parsing low sound commands is read_high_sound_cmd
.
The first byte of the sound command dictates how the subsequent bytes are interpreted.
- Case 1 - if the sound slot is #$03 (noise and dmc channel), then
parse_percussion_cmd
is called to handle the percussion. See notes below in section titledPercussion Command
. - Case 2 - if byte 0 high nibble is less than #$c, then
simple_sound_cmd
is used. This plays a single note with a specified length change from previous note with a volume envelope specified bylvl_config_pulse
. - Case 3 - if byte 0 high nibble is greater than or equal to #$0c, then
@regular_sound_cmd
is used.@regular_sound_cmd
looks at bits 4 and 5 to know which entry insound_cmd_ptr_tbl
to use to handle the sound command. For details of what each method does, see sectionsound_cmd_routine_xx
.
sound_cmd_routine_00
- sets sound channel config to mute, marks channel as muted by setting bit 6 ofSOUND_FLAGS
.sound_cmd_routine_01
- sets sound length multiplier (SOUND_LENGTH_MULTIPLIER
) to low nibble. Sets in memory low nibble of sound channel. Can initialize channel by callingexe_channel_init_ptr_tbl_routine
. Otherwise, recursively calls back toread_high_sound_cmd
to handle next byte.sound_cmd_routine_02
- If the low nibble is less than #$5, then sets note adjustment flag
(
SOUND_PERIOD_ROTATE
) and recursively calls back toread_high_sound_cmd
. - If the low nibble is #$8, set bit 4 of
SOUND_FLAGS
to mark note as slightly flattened from original value. - If the low nibble is #$b, set vibrato variables and recursively call
read_high_sound_cmd
. - If the low nibble is #$c, set the pitch based on next sound byte, which
(when doubled) is an offset into
note_period_tbl
. - If the low nibble isn't any known value, just ignore it and recursively
call
read_high_sound_cmd
.
- If the low nibble is less than #$5, then sets note adjustment flag
(
sound_cmd_routine_03
- this function handles the end of a sound command and determines where to go next based on the byte value. See section above titledsound_code Sharing
.
Percussion commands are recursively read until Case 1 or Case 3 is reached.
- Case 1 - The byte's high nibble is #$f. This is an end of sound command,
the sound command will either end, repeat, or move back to parent sound
command. See the section titled
sound_code Sharing
. - Case 2 - The byte's high nibble is #$d. The low nibble is used to set
the sound length multiplier (
SOUND_LENGTH_MULTIPLIER
). - Case 3 - The byte high nibble isn't #$f, nor #$d. In this case, call
calc_cmd_len_play_percussion
to determine sound command length (SOUND_CMD_LENGTH
) based on the low nibble and the value determined from Case 2. Then callplay_percussive_sound
. This method will use the high nibble (shifted into low nibble) of the byte value to get offset intopercussion_tbl
, which specifies which DMC sound sample code to play. This value is passed toplay_sound
to play the sound code. Then, if the value was greater than or equal to #$3,sound_02
is also played with other sound code. The offsets are defined and which sound(s) is/are played are below. Note that in offset 5,sound_02
is not played because there is a check inload_sound_code_entry
andsound_25
is already playing in slot #$05.- 0 -
sound_02
- 1 -
sound_5a
- 2 -
sound_5b
- 3 -
sound_5a
andsound_02
- 4 -
sound_5b
andsound_02
- 5 -
sound_25
- 6 -
sound_5c
andsound_02
- 7 -
sound_5d
andsound_02
- 0 -