First implementation of sound

Sounds are packed into a bank, loaded, copied to Ensoniq RAM, and can be played through oscillators. The most basic form of GS sound system.
This commit is contained in:
blondie7575 2023-07-04 20:09:17 -07:00
parent b289166038
commit 50b1e71521
15 changed files with 471 additions and 4 deletions

View File

@ -25,9 +25,12 @@
7088096D1F2ECE8D00D4C950 /* GenerateRenderSpans.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = GenerateRenderSpans.py; sourceTree = "<group>"; };
708D1B1E27B9A1A600909AFC /* crosshair.s */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.asm; path = crosshair.s; sourceTree = "<group>"; };
709175C01F60D263008FAFAB /* GenerateCircles.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = GenerateCircles.py; sourceTree = "<group>"; };
709816A32A53B21800108D9C /* GenerateSoundBank.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = GenerateSoundBank.sh; sourceTree = "<group>"; };
709816A42A54E19F00108D9C /* ReadMe.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ReadMe.md; sourceTree = "<group>"; };
7099E3841F41022100182A82 /* gameobject.s */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.asm; path = gameobject.s; sourceTree = "<group>"; };
7099E3851F4107B100182A82 /* GenerateVRAMYOffset.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = GenerateVRAMYOffset.py; sourceTree = "<group>"; };
70A80FB01F43D7F200BD34C9 /* gamemanager.s */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.asm; path = gamemanager.s; sourceTree = "<group>"; };
70BCB0CE2A4FC92D00B19803 /* sound.s */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.asm; path = sound.s; sourceTree = "<group>"; };
70BDCBC92006AD5F00CB51F1 /* linkerConfig */ = {isa = PBXFileReference; lastKnownFileType = text; path = linkerConfig; sourceTree = "<group>"; };
70BDCBCA200A99F200CB51F1 /* ParseMapFile.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = ParseMapFile.py; sourceTree = "<group>"; };
70C073091F5BAA3E009844A9 /* collision.s */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.asm; path = collision.s; sourceTree = "<group>"; };
@ -66,6 +69,8 @@
700C39C51F2E5CA800C24F9C /* tables.s */,
70E266E31F6F262D005AC7E4 /* circleTable.s */,
70F0869F1F413A89002446C3 /* player.s */,
70BCB0CE2A4FC92D00B19803 /* sound.s */,
709816A32A53B21800108D9C /* GenerateSoundBank.sh */,
705456882A4D336200A2B866 /* animation.s */,
700B5E6F2069831000B31C00 /* inventory.s */,
700F21DF1F4A364600D7007D /* projectile.s */,
@ -85,6 +90,7 @@
705456862A43E03B00A2B866 /* GeneratePixelCircle.py */,
709175C01F60D263008FAFAB /* GenerateCircles.py */,
70BDCBCA200A99F200CB51F1 /* ParseMapFile.py */,
709816A42A54E19F00108D9C /* ReadMe.md */,
);
sourceTree = "<group>";
};

61
GenerateSoundBank.sh Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash
############################################
# Takes a list of WAV files for the sound effects,
# downsamples them to GS-friendly rates,
# and packs them into a 64k block that can
# be loaded into Ensoniq RAM
#
# IMPORTANT: This depends on sox, which MacOS doesn't have natively.
# You can get it from Homebrew ("brew install sox")
#
# Big thanks to Jeremy Rand, from whom this approach
# borrows heavily
# https://github.com/jeremysrand/BuGS/blob/master/BuGS/sound/sources/createRaw
rm -f "SoundBank#060000"
rm -f "Sound/SoundMap.txt"
currAddr=0
for (( index=1; index<="$#"; index+=2 )); do
waveFilePath=${!index}
# Downsample and convert to 8-bit unsigned raw waveform
rateIndex=$((index+1))
rate=${!rateIndex}
sox "$waveFilePath" -c 1 -r "$rate" --bits 8 --encoding unsigned tempSound.raw
waveFile=$(basename -- "$waveFilePath")
rawFile="Sound/Generated/${waveFile%.wav}.raw"
# Replace any 00 bytes with 01 because 00 is the terminator for Ensoniq
LC_ALL=C tr < tempSound.raw '\000' '\001' > "$rawFile"
rm tempSound.raw
# Null-terminate the waveform
rawSize=`stat -f '%z' "$rawFile"`
dd if=/dev/zero bs=1 count=1 >> "$rawFile"
# Pad the file out to a wavetable page boundary
rawSize=`stat -f '%z' "$rawFile"`
pageSize=`echo "a=$rawSize; b=4096; if ( a%b ) (a/b+1)*4096 else a/b*4096" | bc`
let "padding = $pageSize - $rawSize"
dd if=/dev/zero bs=1 count="$padding" >> "$rawFile"
# Concatenate to our soundbank
cat "$rawFile" >> "SoundBank#060000"
# Update the human-readable sound map (so human can modify loading code as needed)
soundName="${waveFile%.wav}"
printf "$%04x\t%s\t%d bytes\t%d Hz\n" "$currAddr" "$soundName" "$pageSize" "$rate" >> "Sound/SoundMap.txt"
# Increment address for next sound in bank
currAddr=$((currAddr+pageSize))
done
echo
echo "Sound RAM mapping:"
cat "Sound/SoundMap.txt"

View File

@ -4,8 +4,7 @@
#
# Created by Quinn Dunki on 7/14/15.
# One Girl, One Laptop Productions
# http://www.quinndunki.com
# http://www.quinndunki.com/blondihacks
# http://www.blondihacks.com
#
@ -18,10 +17,12 @@ ADDR=0800
CODEBANK=CODEBANK\#060000
CODEBANKE1=CODEBANKE1\#060800
EXEC=$(PGM)\#06$(ADDR)
SOUNDBANK=SOUNDBANK\#060000
PGM=gscats
MRSPRITE=../MrSprite/mrsprite
GENART=Art/Generated
GENSOUND=Sound/Generated
CHROMA=a4dffb
PALETTE=a4dffb a4dffb 008800 886611 cc9933 eebb44 dd6666 ff99aa 777777 ff0000 b7b7b7 dddddd 0077bb ffff00 000000 ffffff
SPRITES=SpriteBank
@ -41,12 +42,14 @@ diskimage:
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(IMG)/QUIT.SYSTEM/QUIT.SYSTEM#FF2000
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(IMG)/PRODOS/PRODOS#FF0000
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(IMG)/BASIC.SYSTEM/BASIC.SYSTEM#FF2000
$(PGM):
@echo $(REMOTESYMBOLS)
@PATH=$(PATH):/usr/local/bin; $(CL65) -t apple2enh -C linkerConfig --cpu 65816 --start-addr 0000 -l$(PGM).lst $(REMOTESYMBOLS) $(PGM).s -o $(CODEBANK)
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(CODEBANK)
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(SPRITEBANK)
$(CAD) ADDFILE $(PGM).2mg /$(VOLNAME) $(SOUNDBANK)
rm -f $(CODEBANK)
rm -f $(PGM).o
rm -f terrain_e1.map
@ -87,3 +90,9 @@ art:
./MerlinToCA65.sh $(GENART)/$(SPRITES)Src.txt > spritebank.s
rm $(GENART)/*.txt
rm -f Art/*m.gif
.PHONY: sound
sound:
rm -f $(GENSOUND)/*
./GenerateSoundBank.sh Sound/CatHowl.wav 11264 Sound/Meow1.wav 5513
rm -f $(GENSOUND)/*

24
ReadMe.md Normal file
View File

@ -0,0 +1,24 @@
# GSCats
## Introduction
This is a simple game in the style of Apple II Artilley, Scorched Earth, or Worms but with cats and nobody gets hurt.
This was written as an exercise for me to learn the fundamentals of Apple IIgs game programming, and I certainly won't claim there's anything special here technically. The neatest part is probably the terrain, which is deformable and scrolls at 60fps on stock hardware. Well, sometimes it does, anyway, depending on how you hold your nose. The GS is hard.
## Acknowledgements
I'm writing this game now because I'm learning all the things I wished I could know when I was a teenager and playing all the amazing GS games. Games like Rastan, Task Force, Alien Mind, The Immortal, and Sword of Sodan that did things that people said the GS shouldn't be capable of. I was never able to acquire the knowledge of how these things were done back then, but thanks to an amazing modern retro community, I can! In that spirit, thanks to:
- John Brooks, who has forgotten more about the GS than I will ever know.
- Rebecca Heineman, who wrote every good game on the platform that you probably played except the ones John wrote
- Jeremy Rand, who is inspiring to me with his prolific retro programming endeavours and humble spirit. Not to mention I learned a lot from reading his code for BuGS.
- Brutal Deluxe, who have made so many great GS tools, including [Mr. Sprite](http://www.brutaldeluxe.fr/products/crossdevtools/mrsprite/index.html) and Cadius, upon which this game depends. I doubt you could find anyone more consistently devoted to the platform than these guys. Mr. Sprite's Tech Page is probably the greatest single treatise ever written on how to do fast Apple IIgs graphics.
- Dagen Brock, for tirelessly maintaining the last remaining (as of this writing) good GS emulator, GSPlus. Without good emulators, retro-development dies, so thank you for fighting the good fight, Dagen. Emulating the GS is a thankless job for a platform almost nobody cares about.
- All of the KansasFest and Apple II community on Slack and elsewhere
### Sound Credits:
- Cat noise compilation, by Counter-gamer. Used with permission under CC3.0 license. [https://freesound.org/s/213889/](url)
- Cat Howl, by InspectorJ. Used with permission under CC4.0 license. [https://freesound.org/s/415209/](url)

BIN
Sound/CatCompilation.wav Normal file

Binary file not shown.

BIN
Sound/CatHowl.wav Normal file

Binary file not shown.

BIN
Sound/Meow1.wav Normal file

Binary file not shown.

2
Sound/SoundMap.txt Normal file
View File

@ -0,0 +1,2 @@
$0000 CatHowl 8192 bytes 11264 Hz
$2000 Meow1 8192 bytes 5513 Hz

BIN
SoundBank#060000 Normal file

Binary file not shown.

View File

@ -14,6 +14,12 @@ SHADOWVRAMBANK = $010000
PRODOS = $bf00 ; MLI entry point
PRODOSRETURN = $300 ; Indirect jump to get back to ProDOS from any bank
ENSONIQ_CONTROL = $e1c03c ; Ensoniq-related registers/locations
ENSONIQ_DATA = $e1c03d
ENSONIQ_ADDRL = $e1c03e
ENSONIQ_ADDRH = $e1c03f
SYSTEM_VOLUME = $e100ca
; Zero page locations we use (unused by Monitor, Applesoft, or ProDOS)
PARAM0 = $06
PARAM1 = $07

View File

@ -36,6 +36,9 @@ beginGameplay:
lda #3
sta SpriteBankBank00+3 ; Tell compiled sprites what bank they are in
BITS16
; Set up audio
jsr initSoundSystem
; Erase the screen
ldx #$0000

View File

@ -37,6 +37,7 @@ quitGame:
.include "random.s"
.include "graphics.s"
.include "sound.s"
.include "font.s"
.include "smallNumbers.s"
.include "animation.s"

View File

@ -138,6 +138,33 @@ loadData:
ldy #0
jsr copyBytes
EMULATION
; Open the sound file
jsr PRODOS
.byte $c8
.addr fileOpenSound
bne ioError
; Load the sound data into bank 0
jsr PRODOS
.byte $ca
.addr fileRead
bne ioError
; Close the file
jsr PRODOS
.byte $cc
.addr fileClose
NATIVE
; Copy sound data into bank 4
ldx fileReadLen
lda #4
ldy #0
jsr copyBytes
; Set up a long jump into bank 2, and
; a way for game code to get back here to exit
; properly to ProDOS 8
@ -259,9 +286,18 @@ fileOpenSprites:
.byte 0 ; Result (file handle)
.byte 0 ; Padding
fileOpenSound:
.byte 3
.addr soundPath
.addr $9200 ; 1k below BASIC.SYSTEM
.byte 0 ; Result (file handle)
.byte 0 ; Padding
codePath:
pstring "/GSAPP/CODEBANK"
codePathE1:
pstring "/GSAPP/CODEBANKE1"
spritePath:
pstring "/GSAPP/SPRITEBANK"
soundPath:
pstring "/GSAPP/SOUNDBANK"

View File

@ -335,6 +335,13 @@ unrenderPlayers:
;
renderHitAnimation:
SAVE_AXY
; Play hit sound
phy
ldy #SOUND_HOWL
jsr playSound
ply
PLAYERPTR_Y
jsr unrenderPlayers

312
sound.s Normal file
View File

@ -0,0 +1,312 @@
;
; sound
; Code and data structures for playing sound effects
;
; Created by Quinn Dunki on 6/30/23
;
NUM_SOUNDS = 2
SOUND_HOWL=0
SOUND_MEOW1=2
soundTable:
; Sound Ram Address, Wave Size, Low Frequency
.byte $00,WAVE_SIZE_8192,200 ; SOUND_HOWL
.byte $20,WAVE_SIZE_8192,100 ; SOUND_MEOW1
; Ensoniq Control Register bit patterns
DOC_RAMACCESS = $40 ; or
DOC_DOCACCESS = $bf ; and
DOC_AUTOINC = $20 ; or
DOC_NOINC = $df ; and
; Oscillator Control Register bit patterns
OSC_MODE_FREE = $00
OSC_MODE_ONESHOT = $02
OSC_MODE_SYNC = $04
OSC_MODE_SWAP = $06
OSC_CHANNEL_ALL = $70
OSC_CHANNEL_RIGHT = $00
OSC_CHANNEL_LEFT = $10
; Internal oscillator-related registers (additional per-oscillator registers per the big table on page 110 of Apple IIGS Hardware Reference Manual)
OSC_REG_INTERRUPT = $e0
OSC_REG_ENABLE = $e1
OSC_REG_DIGITIZE = $e2
; Wave table page size bit patterns
WAVE_SIZE_256 = $07
WAVE_SIZE_512 = $0f
WAVE_SIZE_1024 = $17
WAVE_SIZE_2048 = $1f
WAVE_SIZE_4096 = $27
WAVE_SIZE_8192 = $2f
WAVE_SIZE_16384 = $37
WAVE_SIZE_32768 = $3f
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ENSONIQ_WAIT
;
; Polls the high bit of the Sound Control Register, which tells us when we're allowed
; to talk to the DOC
;
.macro ENSONIQ_WAIT
.local busyLoop
BITS8A
busyLoop:
lda ENSONIQ_CONTROL
bmi busyLoop
BITS16
.endmacro
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; writeInternalRegister - Call in 8 BIT MODE ONLY (also why it's a macro)
;
; Writes to the internal registers of the Ensoniq
;
; A = Data to write (8 bits)
; X = Register number to write to (8 bits)
;
; Trashes A
;
.macro writeInternalRegister
pha
txa
sta ENSONIQ_ADDRL
pla
sta ENSONIQ_DATA
.endmacro
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; writeInternalRegisterPair - Call in 8 BIT MODE ONLY (also why it's a macro)
;
; Writes to the internal registers of the Ensoniq for a pair of oscillators
;
; A = Data to write (8 bits)
; X = Register number to write to (8 bits). X+1 will also be written
;
; Trashes A
;
.macro writeInternalRegisterPair
pha
txa
sta ENSONIQ_ADDRL
pla
sta ENSONIQ_DATA
pha
inx
txa
sta ENSONIQ_ADDRL
pla
sta ENSONIQ_DATA
.endmacro
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; initSoundSystem
;
; Trashes SCRATCHL
;
initSoundSystem:
SAVE_AXY
ENSONIQ_WAIT
; Load sound file bank into Ensoniq RAM
lda #$0000 ; Sound location in bank 4
sta PARAML0
ldx #$04
ldy #11749
jsr copyToSoundRAM
; Configure all our oscillators
lda #0
sta SCRATCHL
ldy #0
initSoundSystemLoop:
BITS8A
lda soundTable,y
sta PARAM0
iny
lda soundTable,y
sta PARAM1
iny
lda soundTable,y
sta PARAM2
iny
BITS16
phy
ldy SCRATCHL
jsr configureOscillatorPair
iny
iny
cpy #NUM_SOUNDS*2
beq initSoundSystemReady
sty SCRATCHL
ply
bra initSoundSystemLoop
initSoundSystemReady:
ply
; Enable oscillators (see page 108 of Apple IIGS Hardware Reference Manual)
BITS8
lda #(NUM_SOUNDS*2)<<1 ; Two oscillators per sound and leaving low bit untouched
ldx #OSC_REG_ENABLE
writeInternalRegister
BITS16
RESTORE_AXY
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; playSound
;
; Y = Oscillator to start (+1 also starts)
;
playSound:
SAVE_AX
ENSONIQ_WAIT
BITS8
; Configure run mode for our oscillators
lda #OSC_CHANNEL_RIGHT | OSC_MODE_ONESHOT
ldx docRegisterTableControl,y
writeInternalRegister
lda #OSC_CHANNEL_LEFT | OSC_MODE_ONESHOT
ldx docRegisterTableControl+1,y
writeInternalRegister
BITS16
RESTORE_AX
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; configureOscillatorPair
;
; Y = Oscillator number (+1 will also be configured)
; PARAM0 = Wave table address (one byte)
; PARAM1 = Wave table page size (must use WAVE_SIZE constants)
; PARAM2 = Low frequency (high frequency is always 0)
;
configureOscillatorPair:
SAVE_AXY
ENSONIQ_WAIT
BITS8
; Configure low frequency
lda PARAM2
ldx docRegisterTableLowFreq,y
writeInternalRegisterPair
; Configure high frequency
lda #0
ldx docRegisterTableHighFreq,y
writeInternalRegisterPair
; Configure volume
lda #$ff ; This is relative to system volume, so set it to max
ldx docRegisterTableVolume,y
writeInternalRegisterPair
; Configure sound RAM pointer
lda PARAM0
ldx docRegisterTableWavePointer,y
writeInternalRegisterPair
; Configure sound RAM size
; Format of this register is tricky. See page 114 of Apple IIGS Hardware Reference Manual
lda PARAM1
ldx docRegisterTableWaveSize,y
writeInternalRegisterPair
BITS16
RESTORE_AXY
rts
; DOC register locations for each oscillator
docRegisterTableLowFreq:
.byte $00,$01,$02,$03,$04,$05,$06,$07,$08,$09,$0a,$0b,$0c,$0d,$0e,$0f,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$1a,$1b,$1c,$1d,$1e,$1f
docRegisterTableHighFreq:
.byte $20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$2a,$2b,$2c,$2d,$2e,$2f,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3a,$3b,$3c,$3d,$3e,$3f
docRegisterTableVolume:
.byte $40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4a,$4b,$4c,$4d,$4e,$4f,$50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$5a,$5b,$5c,$5d,$5e,$5f
docRegisterTableData:
.byte $60,$61,$62,$63,$64,$65,$66,$67,$68,$69,$6a,$6b,$6c,$6d,$6e,$6f,$70,$71,$72,$73,$74,$75,$76,$77,$78,$79,$7a,$7b,$7c,$7d,$7e,$7f
docRegisterTableWavePointer:
.byte $80,$81,$82,$83,$84,$85,$86,$87,$88,$89,$8a,$8b,$8c,$8d,$8e,$8f,$90,$91,$92,$93,$94,$95,$96,$97,$98,$99,$9a,$9b,$9c,$9d,$9e,$9f
docRegisterTableControl:
.byte $a0,$a1,$a2,$a3,$a4,$a5,$a6,$a7,$a8,$a9,$aa,$ab,$ac,$ad,$ae,$af,$b0,$b1,$b2,$b3,$b4,$b5,$b6,$b7,$b8,$b9,$ba,$bb,$bc,$bd,$be,$bf
docRegisterTableWaveSize:
.byte $c0,$c1,$c2,$c3,$c4,$c5,$c6,$c7,$c8,$c9,$ca,$cb,$cc,$cd,$ce,$cf,$d0,$d1,$d2,$d3,$d4,$d5,$d6,$d7,$d8,$d9,$da,$db,$dc,$dd,$de,$df
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; copyToSoundRAM
;
; Copies data from main RAM to sound RAM
;
; PARAML0 = Pointer to sound data in main RAM
; X = Bank number of sound data in main RAM
; Y = Size of sound bank in bytes
;
; Trashes SCRATCHL
;
copyToSoundRAM:
SAVE_AXY
sty SCRATCHL
ENSONIQ_WAIT
lda #0
BITS8
stx copyToSoundRAMLoop+3 ; Self modifying code. Don't panic
; Enable sound RAM access and auto-increment mode
lda ENSONIQ_CONTROL
ora #DOC_RAMACCESS | DOC_AUTOINC
sta ENSONIQ_CONTROL
BITS16
; Prepare start address in sound RAM
lda #$0000
sta ENSONIQ_ADDRL
; Copy all the data
lda #0
BITS8A
ldx #0
copyToSoundRAMLoop:
lda $ff0000,x ; Note: 8 bit accumulator! DOC takes one byte at a time
sta ENSONIQ_DATA ; Do not use indexed addressing here- see page 107 of Apple IIGS Hardware Reference Manual
inx
cpx SCRATCHL
bne copyToSoundRAMLoop
; Enable DOC access and disable auto-increment mode
; This is a nicer mode to leave the Ensoniq in for normal usage once loading is complete
lda ENSONIQ_CONTROL
and #DOC_DOCACCESS & DOC_NOINC
sta ENSONIQ_CONTROL
BITS16
RESTORE_AXY
rts