This is a tiny (currently under ~300 bytes) (non-firmware) SFX engine for the Amstrad CPC that I code to use in my game Reginald and the She Vampires (https://typhonsoft.itch.io/reginald). Current version is 0.1, released 18th December 2025. Code is released under the GPL2.
Its not a replacement for Arkos Tracker support or anything like that. Its just an example of how to prod the PSG chip directly in Z80 machine code. I'm sure there's plenty of enhancements to be made, and there's probably bugs in it. I used ChatGPT to generate the data values for the notes, but I built the code up through ridiculous amounts of trial and error from first principles (I knew some Z80 but not how to produce sound) of producing a single tone using Z80 as there's not really anywhere on the internet that describes how to prod the CPC soundchip from the ground up. Everywhere uses Firmware or some sort of prebuilt engine.
You shouldn't really use this unless you have to, and the sounds aren't much cop, but they are indeed vaguely recogisable SFX!
Where possible you should use the lovely Arkos Tracker at https://www.julien-nevo.com/arkostracker/ and the CPCTeleraNext engine at https://github.com/Arnaud6128/cpctelera/commits/SDCC_4.5.0_next/ if you can instead.
I've included some sample sfx and a CPCTelera project binding the necessary routines to use them. Load and run the DSK, and press Keys A to H to hear them (the border of the screen will change colour when playing a sfx).
- A - footstep
- B - get an item
- C - enemy spawn
- D - show hint alert
- E - open a door
- F - use a bomb
- G - kill a monster
- H - shoot a bullet
Currently the Engine takes 299 bytes, and the data for the eight sample sounds is 289 bytes.
Data for a Sound Effect is made up of a number of different notes (optionally including rest/silent notes), and these are all sequentially played after ope another on Channel A. There is an initial byte for the note count (including any rest notes), followed by the notes themselves (9 bytes each).
If you look at the data file, you'll see the Note format is:
; Note format for each note (9 bytes):
; .dw period
; .db duration
; .db volume
; .db flags (noise, envelope, vibrato)
; .db noise period
; .db envelope
; .db vibrato depth
; .db vibrato speed
A simple example with one note followed by a silence (rest) note:
.globl _sfx_footstep
_sfx_footstep:
.db 2 ; note count (including rest notes)
.dw 0x0200
.db 4
.db 10
.db FLAG_ENV | FLAG_NOISE
.db 14
.db 0x0D
.db 0
.db 0
.dw 0x0000
.db 4
.db 0
.db 0
.db 0
.db 0
.db 0
.db 0
To use this in CPCTelera, like I have done, make the data visible to your code:
extern const unsigned char sfx_footstep[];
And then queue it by calling one of the exposed functions:
sfx_start((void *)sfx_footstep);
To actually play sounds though, you need to call another method, sfx_update() regularly (in the example code I do this in the main loop):
cpct_waitVSYNC();
sfx_update();
Note sfx_update() is perfectly interrupt-safe, I call it in my own custom interrupt handler in Reginald (https://typhonsoft.itch.io/reginald) once every 1/50 of a second (6 frames), that's probably the preferred way of doing things.
There is also another method exposed called sfx_stop() which stops all output immediately.
Please note this uses the new calling convention used by SDCC4.5, and thus needs the CPCTeleraNext branch developed by Arnauld to compile. Feel free to fork and improve, I am sure I have made some mistakes somewhere, and my understanding of sound/sfx concepts is not great, but there's plenty of room to add new features. Let's try and keep the player under 512 bytes if possible, and if possible, keep it to sFX only.
- Multi-channel
- Pitchslide