From aef156ac6216ed157b06a2872ba051af43317e93 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 13 Apr 2026 16:34:11 +0200 Subject: Add sound effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- .nasm/memory.txt | 15 ++++- .nasm/segments.txt | 2 +- README.md | 36 +++++++++++ include/apu.s | 16 ++++- src/bullets.s | 3 + src/driver.s | 9 +++ src/enemies.s | 3 + src/interrupts.s | 5 ++ src/items.s | 9 +++ src/jetpac.s | 12 ++++ src/sound.s | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 src/sound.s 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 -- cgit v1.2.3