I am currently in the process of porting (or rather, reimplementing from scratch) an old Macintosh game to the Apple II, and I wanted it to have acceptable sound. Like this:
It works as usual with Apple II sampled audio: with duty cycles looping, alternating accesses to the speaker at $C030 and doing other things / wasting cycles.
The solution I’ve come up is to write a generator, which takes care of cycle counting for me. It generates ca65 assembler files; one for the player, and one per sampled sound you want to integrate.
The solution allows for a “5.5bit” DAC at 16kHz and can use samples at 4 or 8kHz, between which you can choose to attain a good balance between size and quality. The table here gives the details:
Sample rate | STEP | DAC resolution | Player size | 1 second sample size |
4000 | 1 | 47 levels | < 4kB | 4kB |
4000 | 2 | 24 levels | < 2kB | 4kB |
8000 | 1 | 47 levels | < 2kB | 8kB |
8000 | 2 | 24 levels | < 1kB | 8kB |
You can get the code using:
git clone https://github.com/colinleroy/a2tools.git
cd a2tools/src/sound_player_generator
Generating the sound player is done in two steps; first build the binary using gcc, then run the program with the desired sample rate parameter and pipe its output to a .s file.
gcc -DCPU_65c02 -g -O0 gen_sound_player.c -o gen_sound_player
./gen_sound_player 8000 > play_sound.gen.s
You can then generate sound clips by first transforming your input file to a raw 8-bit unsigned sample stream, for example using sox
, bubble.wav
being the original file:
sox bubble.wav -b 8 -e unsigned-integer -t raw bubble.raw channels 1 rate -v 8000
Finally, you can generate the assembler file for your sound sample using:
gcc -g -O0 gen_sound_clip.c -o gen_sound_clip
./gen_sound_clip 8000 bubble.raw > bubble.gen.s
The last thing to do is use them in your code:
.import _play_sample, _bubble_snd
; [...]
lda #<_bubble_snd
ldx #>_bubble_snd
jsr _play_sample
All of the build and generation can be automated in a Makefile, for example:
TARGET=apple2enh
CPU=65c02
SAMPLING_HZ=8000
sounds = \
bubble.snd.s
SOURCES := \
$(YOUR_SOURCES) \
play_sound.gen.s \
$(sounds)
all: \
sounddemo.bin
CFLAGS = -t $(TARGET) -Cl -C ../../config/apple2enh-aligndata.cfg
#Build the sound player with the correct definitions
gen_sound_player: gen_sound_player.c sound.h
gcc -DCPU_$(CPU) -g -O0 $< -o $@
#Build the sound clip generator with the correct definitions
gen_sound_clip: gen_sound_clip.c sound.h
gcc -g -O0 $< -o $@
play_sound.gen.s: gen_sound_player
./gen_sound_player $(SAMPLING_HZ) > $@
$(sounds): %.snd.s: %.wav
%.snd.s: %.wav gen_sound_clip
rm -f $<.raw
sox $< -b 8 -e unsigned-integer -t raw $<.raw channels 1 rate -v $(SAMPLING_HZ)
./gen_sound_clip $(SAMPLING_HZ) $<.raw > $@
rm $<.raw
clean:
rm -f *.bin *.o \
gen_sound_clip \
gen_sound_player \
*.gen.s
sounddemo.bin: $(SOURCES)
cl65 $(CFLAGS) -o $@ $^
As some instructions used to read the stream have an extra cycle when a page is crossed, the best results are achieved if the DATA segment is aligned. In the above example, this is done in the ../../config/apple2enh-aligndata.cfg
using the following line for the DATA
segment in the SEGMENTS
section of the file:
DATA: load = MAIN, type = rw, align = $100;
Finally, the sound.h
file allows to set the stepping from 1 to 2, which makes the player smaller but the DAC less precise.
The video from the start of the article is the result of the demo from a2tools/src/sounddemo.