aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-04-13 16:34:11 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-04-13 16:34:11 +0200
commitaef156ac6216ed157b06a2872ba051af43317e93 (patch)
treea6daca2bc0353e6b59823bc6bf019362ecb02507
parentc3c6f3b4469bf9eadff5763fa16dbbbf15637e57 (diff)
downloadjetpac.nes-aef156ac6216ed157b06a2872ba051af43317e93.tar.gz
jetpac.nes-aef156ac6216ed157b06a2872ba051af43317e93.zip
Add sound effects
Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
-rw-r--r--.nasm/memory.txt15
-rw-r--r--.nasm/segments.txt2
-rw-r--r--README.md36
-rw-r--r--include/apu.s16
-rw-r--r--src/bullets.s3
-rw-r--r--src/driver.s9
-rw-r--r--src/enemies.s3
-rw-r--r--src/interrupts.s5
-rw-r--r--src/items.s9
-rw-r--r--src/jetpac.s12
-rw-r--r--src/sound.s171
11 files changed, 277 insertions, 4 deletions
diff --git a/.nasm/memory.txt b/.nasm/memory.txt
index e2ab039..f0eff2b 100644
--- a/.nasm/memory.txt
+++ b/.nasm/memory.txt
@@ -63,6 +63,7 @@ $D2-$D3: zp_movement_fn
$D4: zp_pool_index
$D5: zp_movement_arg
$D6: zp_palette
+$DA: zp_frame_count
$E0: zp_pool_size
$E1: zp_last_allocated_index
$E2: zp_current_bullet_y
@@ -80,10 +81,22 @@ $2003: m_address
$2005: m_scroll
$2006: m_address
$2007: m_data
+$4000: m_square_1_envelope
+$4001: m_square_1_sweep
+$4002: m_square_1_low
+$4003: m_square_1_high
+$4004: m_square_2_envelope
+$4005: m_square_2_sweep
+$4006: m_square_2_low
+$4007: m_square_2_high
+$400C: m_noise_envelope
+$400E: m_noise_mode
+$400F: m_noise_counter
$4010: m_dmc
$4014: m_dma
+$4015: m_status
$4016: m_joypad
$4017: m_frame_counter
--- Summary (in bytes) ---
-- Internal RAM: 567/2048 (27.69%)
+- Internal RAM: 568/2048 (27.73%)
diff --git a/.nasm/segments.txt b/.nasm/segments.txt
index 5d8e4c8..d26da0f 100644
--- a/.nasm/segments.txt
+++ b/.nasm/segments.txt
@@ -1,4 +1,4 @@
- HEADER: 16/16 (100%)
-- ROM0: 9216/32762 (28.13%)
+- ROM0: 9395/32762 (28.68%)
- ROMV: 6/6 (100%)
- ROM2: 8192/8192 (100%)
diff --git a/README.md b/README.md
index 1a86fbe..6bfd9c6 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,42 @@ Lastly, note that if you walk close the a platform's edge the game won't force
you down as with the original. I found that unnecessary and it made things more
complex on the technical side, so I skipped implementing this behavior.
+## Sound
+
+The sound is entirely different from the original, and it's the thing that will
+stand out the most to players used to the original "soundtrack". The ZX Spectrum
+was very limited in this department, with only a single-channel beeper, and so
+the only sound in the original are beeps making up the sound effects. These
+beeps are charming and all, but they are next to impossible to reproduce on the
+NES/Famicom.
+
+All in all, this port stays in the same beeper department, but via the more
+advanced channels from the NES/Famicom. Long story short: all sounds are just
+different on this port. Having said that, there are a couple of considerations
+to be made.
+
+First of all, the take off animation is done via noise channel from the
+NES/Famicom, which is close enough to the original sound. But this noise channel
+is also used for enemy/player explosions, which will sound entirely different to
+the beeping from the original. All in all, I thought it was funny to have it
+this way, which sounds more aggressive and it's charming in its own distinct
+way.
+
+Moreover, in the original the CPU had to slot some time to produce the beeping,
+which made the game to lag in some situations. This doesn't happen on the
+NES/Famicom, simply because we don't have to waste CPU cycles to produce
+sound. When it comes to bullets, this lag made the beeping on the original
+unreliable. But if we delivered a sound effect for each bullet on the
+NES/Famicom, it would simply be overwhelming to the player, as they would get a
+fast stream of beeps. Because of this, I'm only delivering sound at a maximum
+capped frame rate. This will make the randomness of beeping from the original
+less random on this port.
+
+Last but not least, and realizing that this shooting game isn't that far off
+from games like Gradius when it comes to being a bullet smasher, the sound
+effect for each bullet is closer to those kinds of games in contrast to the
+original. In the end: different machine, different sound effects.
+
## Shooting
Shooting is something that is completely different to the original, as the
diff --git a/include/apu.s b/include/apu.s
index bb0319f..dc2ce87 100644
--- a/include/apu.s
+++ b/include/apu.s
@@ -1,4 +1,16 @@
.scope APU
- m_dmc = $4010
- m_frame_counter = $4017
+ m_square_1_envelope = $4000
+ m_square_1_sweep = $4001
+ m_square_1_low = $4002
+ m_square_1_high = $4003
+ m_square_2_envelope = $4004
+ m_square_2_sweep = $4005
+ m_square_2_low = $4006
+ m_square_2_high = $4007
+ m_noise_envelope = $400C
+ m_noise_mode = $400E
+ m_noise_counter = $400F
+ m_dmc = $4010
+ m_status = $4015
+ m_frame_counter = $4017
.endscope
diff --git a/src/bullets.s b/src/bullets.s
index 1072a8f..9e40546 100644
--- a/src/bullets.s
+++ b/src/bullets.s
@@ -133,6 +133,9 @@
beq @find_free_bullet_bucket
@initialize_bucket:
+ ;; Play the bullet sound if appropiate.
+ jsr Sound::play_bullet_maybe
+
;; We found a free bucket. Initialize the first byte to 0 since it has
;; not moved yet. The heading is taken from the player's state.
lda Player::zp_state
diff --git a/src/driver.s b/src/driver.s
index 2bc78ca..4e54e8c 100644
--- a/src/driver.s
+++ b/src/driver.s
@@ -240,6 +240,9 @@
sta zp_next_bullet_cycle
sta zp_next_enemy_cycle
+ ;; And make the sound for entering into a level.
+ SOUND_ENTER_LEVEL
+
@game:
;; Has the player died?
lda Globals::zp_flags
@@ -818,6 +821,9 @@
ora #%01100000
sta Globals::zp_flags
+ ;; Make some noise!
+ START_TAKE_OFF_SOUND
+
rts
.endproc
@@ -928,6 +934,9 @@
ora #%01100000
sta Globals::zp_flags
+ ;; Stop the sound effect for take off.
+ STOP_TAKE_OFF_SOUND
+
rts
@set:
diff --git a/src/enemies.s b/src/enemies.s
index 2208758..76f3ddf 100644
--- a/src/enemies.s
+++ b/src/enemies.s
@@ -695,6 +695,9 @@
ldx Enemies::zp_pool_index
sta Enemies::zp_pool_base + 3, x
+ ;; Play an explosion effect.
+ SOUND_EXPLOSION
+
;; Restore back the value for the 'y' register.
pla
tay
diff --git a/src/interrupts.s b/src/interrupts.s
index 7026223..658f6a3 100644
--- a/src/interrupts.s
+++ b/src/interrupts.s
@@ -161,6 +161,11 @@
sta PPU::m_scroll
sta PPU::m_scroll
+ ;; The sound effect from bullets follow a frame rule. Tick the frame count
+ ;; from sound as the very last thing to be done before unblocking the main
+ ;; code.
+ jsr Sound::tick
+
;; Unblock the main code.
lda #%01111111
and Globals::zp_flags
diff --git a/src/items.s b/src/items.s
index d8d1391..1e8a111 100644
--- a/src/items.s
+++ b/src/items.s
@@ -591,6 +591,9 @@
;; Increase the number of collected items.
inc Items::zp_collected
+ ;; Make the "item drop" sound
+ SOUND_ITEM_DROP
+
;; Now we unset the 'S' bit, which is unconditionally true regardless of
;; the collection state. That being said, if we still need to collect
;; more fuel tanks (the rocket has all its parts and we have not filled
@@ -679,6 +682,9 @@
@set_modes:
sta Items::zp_pool_base, x
+ ;; Make the sound for grabbing an item.
+ SOUND_ITEM_PICKUP
+
;; Mark the player's to be already grabbing an item.
lda Items::zp_state
ora #$80
@@ -988,6 +994,9 @@
ADD_ITEM_SCORE
ldx Globals::zp_tmp2
+ ;; Make the sound for item pickup.
+ SOUND_ITEM_PICKUP
+
rts
.endproc
diff --git a/src/jetpac.s b/src/jetpac.s
index f2e28b2..a011a45 100644
--- a/src/jetpac.s
+++ b/src/jetpac.s
@@ -42,6 +42,7 @@
.include "assets.s"
.include "background.s"
.include "prng.s"
+.include "sound.s"
.include "explosions.s"
.include "score.s"
.include "items.s"
@@ -53,6 +54,14 @@
.include "driver.s"
.include "interrupts.s"
+;; Sanity check for some constant values. 'cl65' fails at this, so I'm only
+;; doing the check on 'nasm'.
+.ifdef __NASM__
+ .if Bullets::BULLET_TIMER_VALUE > Sound::BULLET_SFX_FRAME_COUNT
+ .error "Sound::BULLET_SFX_FRAME_COUNT must be smaller than Bullets::BULLET_TIMER_VALUE"
+ .endif
+.endif
+
;; Pretty standard reset function, nothing crazy.
.proc reset
;; Disable interrupts and decimal mode.
@@ -73,6 +82,9 @@
stx PPU::m_mask
stx APU::m_dmc
+ ;; Initialize the APU.
+ jsr Sound::init
+
;; First PPU wait.
bit PPU::m_status
@vblankwait1:
diff --git a/src/sound.s b/src/sound.s
new file mode 100644
index 0000000..a824898
--- /dev/null
+++ b/src/sound.s
@@ -0,0 +1,171 @@
+.segment "CODE"
+
+;; The sound for this game is extremely simple, coming from a system that only
+;; allowed for 1-bit beeps. Here the sound is a bit different due to a vastly
+;; different audio hardware, and we make use of three channels:
+;;
+;; 1. Square 1: used on level entry and bullets.
+;; 2. Square 2: used for item collection or dropping.
+;; 3. Noise: enemy explosion and rocket launch.
+.scope Sound
+ ;; Bullets can go super fast, and if we delivered the sound effect for each
+ ;; bullet it could potentially be annoying. Moreover, because of the
+ ;; limitations from the ZX Spectrum, the original game didn't spit a sound
+ ;; effect for each bullet either. Hence, wait for some frames before
+ ;; producing a sound. This is why we call Sound::tick() on NMI code, and why
+ ;; the sound effect for bullet is called via Sound::play_bullet_maybe().
+ ;;
+ ;; NOTE: the maximum count value is supposed to be bigger than the timer for
+ ;; bullets creation. This is guaranteed in 'jetpac.s'.
+ zp_frame_count = $DA
+ BULLET_SFX_FRAME_COUNT = HZ / 10
+
+ ;; Period values for square channels.
+ .ifdef PAL
+ BULLET_SFX_LOW = $29
+ BULLET_SFX_HIGH = $00
+ ENTER_SFX_LOW = $4D
+ ENTER_SFX_HIGH = $01
+ PICKUP_SFX_LOW = $61
+ PICKUP_SFX_HIGH = $01
+ DROP_SFX_LOW = $3A
+ DROP_SFX_HIGH = $01
+ .else
+ BULLET_SFX_LOW = $2C
+ BULLET_SFX_HIGH = $00
+ ENTER_SFX_LOW = $67
+ ENTER_SFX_HIGH = $01
+ PICKUP_SFX_LOW = $7C
+ PICKUP_SFX_HIGH = $01
+ DROP_SFX_LOW = $52
+ DROP_SFX_HIGH = $01
+ .endif
+
+ ;; Initialize all the sound channels which are needed and reset some
+ ;; register values.
+ .proc init
+ ;; Enable square 1, 2; and noise.
+ lda #%00001011
+ sta APU::m_status
+
+ ;; Reset sweep registers and frame count.
+ lda #0
+ sta APU::m_square_1_sweep
+ sta APU::m_square_2_sweep
+ sta Sound::zp_frame_count
+
+ ;; Silence channels.
+ lda #0
+ sta APU::m_noise_envelope
+ lda #$30
+ sta APU::m_square_1_envelope
+ sta APU::m_square_2_envelope
+
+ rts
+ .endproc
+
+ ;; Tick the internal frame count for sound effects.
+ ;;
+ ;; NOTE: expected to only be called at the end of NMI code.
+ .proc tick
+ ;; If there is no bullet sound effect to be delivered, don't even sweat
+ ;; it.
+ lda Sound::zp_frame_count
+ beq @end
+
+ ;; Increase the frame counter and check the limit. If we reached that
+ ;; limit, reset it so we don't tick until the next bullet sfx request
+ ;; comes in.
+ clc
+ adc #1
+ cmp #Sound::BULLET_SFX_FRAME_COUNT
+ beq @reset
+ sta Sound::zp_frame_count
+ rts
+
+ @reset:
+ lda #0
+ sta Sound::zp_frame_count
+
+ @end:
+ rts
+ .endproc
+
+ ;; Play the bullet sound effect if we can (i.e. the frame count allows us to
+ ;; do it).
+ .proc play_bullet_maybe
+ ;; If we cannot play the sound yet, skip this altogether.
+ lda Sound::zp_frame_count
+ bne @end
+ inc Sound::zp_frame_count
+
+ lda #$01
+ sta APU::m_square_1_envelope
+ lda #%10000001
+ sta APU::m_square_1_sweep
+ lda #BULLET_SFX_LOW
+ sta APU::m_square_1_low
+ lda #BULLET_SFX_HIGH
+ sta APU::m_square_1_high
+
+ @end:
+ rts
+ .endproc
+.endscope
+
+;; Make an explosion sound via the noise channel.
+.macro SOUND_EXPLOSION
+ lda #$03
+ sta APU::m_noise_envelope
+ lda #$8F
+ sta APU::m_noise_mode
+ lda #$F8
+ sta APU::m_noise_counter
+.endmacro
+
+;; Make a small beep, suitable for level entry.
+.macro SOUND_ENTER_LEVEL
+ lda #%10000100
+ sta APU::m_square_1_envelope
+ lda #Sound::ENTER_SFX_LOW
+ sta APU::m_square_1_low
+ lda #Sound::ENTER_SFX_HIGH
+ sta APU::m_square_1_high
+.endmacro
+
+;; Make a small beep for item pickup.
+.macro SOUND_ITEM_PICKUP
+ lda #%10000100
+ sta APU::m_square_2_envelope
+ lda #Sound::PICKUP_SFX_LOW
+ sta APU::m_square_2_low
+ lda #Sound::PICKUP_SFX_HIGH
+ sta APU::m_square_2_high
+.endmacro
+
+;; Make a small beep for item collection in the droppping zone (i.e. fuel tanks
+;; and shuttle parts making into the shuttle).
+.macro SOUND_ITEM_DROP
+ lda #%10000100
+ sta APU::m_square_2_envelope
+ lda #Sound::DROP_SFX_LOW
+ sta APU::m_square_2_low
+ lda #Sound::DROP_SFX_HIGH
+ sta APU::m_square_2_high
+.endmacro
+
+;; Start the sound effect for the rocket take off animation.
+.macro START_TAKE_OFF_SOUND
+ lda #$38
+ sta APU::m_noise_envelope
+ lda #$0F
+ sta APU::m_noise_mode
+ lda #0
+ sta APU::m_noise_counter
+.endmacro
+
+;; Stop the sound effect for the rocket take off animation.
+.macro STOP_TAKE_OFF_SOUND
+ lda #0
+ sta APU::m_noise_envelope
+.endmacro