Give an accepted action a sound. The same caller changes its picture and triggers this effect; a second version performs the action with the trigger removed. Listen to both.
Working effect
Trigger omitted
Take the effect
The reusable effect
; TIA channel0; sound duration is serviced in overscan, not a CPU delay.
trigger_sound:
lda #4
sta $15
lda #15
sta $17
sta $19
lda #8
sta $85
rts
service_sound:
lda $85
beq sound_done
dec $85
bne sound_done
lda #0
sta $19
sound_done:
rtsTIA channel 0 ($15/$17/$19), duration byte $85; trigger changes A/flags and service changes A/flags. The caller owns TIA/RIOT and the software scanline schedule.
Include sound.inc once with the caller’s constants and duration byte. Initialise the duration byte to zero before the first service call. Call trigger_sound for an accepted action, then call service_sound exactly once per accepted display update. The eighth service call mutes the effect. Because the supplied caller services in the triggering iteration too, this is a count of service calls, not eight complete elapsed frame intervals. Triggering again restarts the duration; there is no queue.
The complete initialiser also mutes other channels. To reuse just this effect beside music, agree channel ownership and remove unrelated global setup only after providing a replacement. Device latches, power, routing and master volume may affect more than one voice.
Change it and recognise a mistake
Change AUDC0/AUDF0/AUDV0 for another effect. Keep all work within the overscan budget; moving a blocking effect here can break the picture.
The omitted-trigger control still accepts the action but fails the same audio guard as the working caller. Keyboard clicks or other machine output are not enough: the BASIC checks compare against a separate rejected-command recording.
Deliberately omitted sound trigger
; Capability caller: original input/frame service, explicit drawing and sound.
; NTSC 4 KiB cartridge. Display servicing runs every field, but only a new
; fire press advances the turn. Owns all TIA state and RIOT RAM $80-$84.
* = $f000
start:
sei
cld
ldx #$ff
txs
lda #0
ldx #$7f
clear_ram:
sta $80,x
dex
bpl clear_ram
lda #2
sta $01 ; VBLANK: blank, button latching disabled
jsr init_picture
frame:
lda #2
sta $00 ; VSYNC
sta $02
sta $02
sta $02 ; three sync lines
lda #0
sta $00
ldx #37
blank_lines:
sta $02 ; WSYNC
dex
bne blank_lines
lda #0
sta $01
ldx #192
visible_lines:
sta $02
dex
bne visible_lines
lda #2
sta $01
sta $02 ; overscan line 1; next line contains short work
!source "../sample.inc"
lda $81
eor #$ff
and $84
beq no_action
inc $80 ; byte turn counter, wraps naturally
nop
nop
nop
no_action:
lda $84
sta $81
inc $82 ; field counter, low
bne show
inc $83
show:
jsr draw_picture
jsr service_sound
sta $02 ; finish overscan line 3 (bounded drawing/audio work)
ldx #27
overscan:
sta $02
dex
bne overscan ; 30 overscan lines including the two-line work window
jmp frame
!source "../picture.inc"
!source "../sound.inc"
!fill $fffa-*,0
!word start,start,startThe working implementation above repairs that omission. If an effect instead drones on, check the mute/service path and that its state is initialised. Servicing twice makes a counted effect shorter; servicing only after input can leave it running while the player waits. Those are lifecycle decisions to test when adapting it.
Run the caller
Build the complete caller with make -C atari-2600/patterns/assembly/input/read-action. Press fire once, keep it held, then release and press again. The piece moves on each new press and returns to its starting position after two; holding does not repeat the action. Load the cartridge produced by the Makefile in the named configuration; retain its reset/header setup.

Where setup, trigger and service belong
; Capability caller: original input/frame service, explicit drawing and sound.
; NTSC 4 KiB cartridge. Display servicing runs every field, but only a new
; fire press advances the turn. Owns all TIA state and RIOT RAM $80-$84.
* = $f000
start:
sei
cld
ldx #$ff
txs
lda #0
ldx #$7f
clear_ram:
sta $80,x
dex
bpl clear_ram
lda #2
sta $01 ; VBLANK: blank, button latching disabled
jsr init_picture
frame:
lda #2
sta $00 ; VSYNC
sta $02
sta $02
sta $02 ; three sync lines
lda #0
sta $00
ldx #37
blank_lines:
sta $02 ; WSYNC
dex
bne blank_lines
lda #0
sta $01
ldx #192
visible_lines:
sta $02
dex
bne visible_lines
lda #2
sta $01
sta $02 ; overscan line 1; next line contains short work
!source "sample.inc"
lda $81
eor #$ff
and $84
beq no_action
inc $80 ; byte turn counter, wraps naturally
jsr trigger_sound
no_action:
lda $84
sta $81
inc $82 ; field counter, low
bne show
inc $83
show:
jsr draw_picture
jsr service_sound
sta $02 ; finish overscan line 3 (bounded drawing/audio work)
ldx #27
overscan:
sta $02
dex
bne overscan ; 30 overscan lines including the two-line work window
jmp frame
!source "picture.inc"
!source "sound.inc"
!fill $fffa-*,0
!word start,start,startCompare blocking, serviced and queued sound when choosing the next step.
Verification and sources
Target: NTSC 4 KiB cartridge. These callers were executed with released Emu198x 0.44.1; assembly uses Asm198x 0.0.71. The sample folder contains reproduction instructions and retained observations. The independent checker binds source, native captures and actual output; its rejection tests exercise wrong state, missing files and false audio claims.
This qualifies the named emulator/firmware path. Pitch accuracy, original hardware and other peripherals require their own observations.
Atari, Stella Programmer’s Guide: INPT4, VBLANK/WSYNC and audio registers; MOS 6507/RIOT execution.