;; This code demonstrates how to do sample playback ;; on the Amstrad CPC. ;; ;; It has "drivers" for Digiblaster/Soundplayer, Amdrum and the internal ;; AY-3-8912 soundchip of the CPC. ;; ;; The samples are stored as 8-bit mono signed, suitable for playing direct ;; to digiblaster. (Note the printer hardware inverts bit 7, so the digiblaster actually ends ;; up playing unsigned mono samples). ;; ;; The CPC doesn't have an interrupt that can be reprogrammed to any rate, ;; so to playback samples we have to use software loops to control the playback ;; rate. This also means the CPU is effectively 100% used. ;; The timing of each instruction is therefore important. You will see the timings ;; for each instruction written between these brackets []. e.g. [3] ;; ;; It is possible to use the delay time between writing data to each "device" to ;; perform other operations, but this is not shown in this example. ;; ;; This code initialises some RSX commands that can be used direct from ;; BASIC. ;; ;; You are advised to use the tools from Devilmarkus for converting samples ;; from the PC or other system to a form that this player can use. ;; Use Markus's tools for conversion and ensure "invert sample" is checked. ;; Process: ;; Take an 8-bit unsigned mono WAV. ;; Convert to 8-bit signed mono raw data. ;; When played to digiblaster, the printer port hardware will invert bit 7 ;; automatically making the sample unsigned again and then the digiblaster will convert this to analogue. ;; firmware function KL LOG EXT to register RSX commands kl_log_ext: equ &bcd1 ;; this can be any address in the range &0040-&a7ff. org &8000 ;; This table must be aligned to a 256 byte boundary. i.e. the lower 8-bits ;; of the address of this table must be 0. ;; ;; This table is used to convert from 8-bit mono unsigned sample value ;; to AY-3-8912 volume register value. table equ &8600 ;;------------------------------------------------------------------------------- ;; start start: ;; init conversion table for sample playback using AY-3-8912 call init_table ;; set initial playback speed (8Khz) ld a,8 call set_speed2 ;;------------------------------------------------------------------------------- ;; install RSX ld hl,work_space ;;address of a 4 byte workspace useable by Kernel ld bc,jump_table ;;address of command name table and routine handlers jp kl_log_ext ;;Install RSX's ;;------------------------------------------------------------------------------- work_space: ;Space for firmware kernel to use defs 4 ;;------------------------------------------------------------------------------- ;; RSX function call definition jump_table: defw name_table jp playsample jp setdriver jp setspeed jp setbank jp setleft jp setright jp setcenter ;;------------------------------------------------------------------------------- ;; RSX name definition name_table: defb "PLA","Y"+&80 ;; |PLAY,, ;; Play sample using chosen driver ;; Sample data is 8-bit signed mono defb "DRIVE","R"+&80 ;; |DRIVER, ;; 0 = AY ;; 1 = digiblaster ;; 2 = amdrum defb "SPEE","D"+&80 ;; Set playback rate. ;; ;; 0 = 1KHz (1000hz) ;; 1 = 1KHz (1000hz) ;; 2 = 2Khz (2000hz) ;; 3 = 3Khz (3000hz) ;; 4 = 4Khz (4000hz) ;; 5 = 5Khz (5000hz) ;; 6 = 6Khz (6000hz) ;; 7 = 7Khz (7000hz) ;; 8 = 8Khz (8000hz) ;; 9 = 9Khz (9000hz) ;; 10 = 10Khz (10000hz) ;; 11 = 11Khz (11025hz) ;; 12 = 12Khz (12000hz) ;; 13 = 13Khz (13000hz) ;; 14 = 14Khz (14000hz) ;; 15 = 15Khz (15000hz) ;; 16 = 16Khz (16000hz) ;; 17+ -> 1Khz defb "BAN","K"+&80 ;; |BANK, ;; Define RAM configuration for dk'tronics compatible ram ;; expansion (extra 64k ram inside CPC6128) defb "LEF","T"+&80 ;; When AY driver, playback to channel A defb "RIGH","T"+&80 ;; When AY driver, playback to channel B defb "CENTE","R"+&80 ;; When AY driver, playback to channel C defb 0 ;;------------------------------------------------------------------------------- setbank: ld a,(ix+0) ;; bank number and &3f ;; ensure it's a valid configuration or &c0 ;; %11000000 -> define ram configuration ld b,&7f ;; write to hardware out (c),a ret ;;------------------------------------------------------------------------------- ;; set playback driver setdriver: ld a,(ix+0) or a ld hl,play_sample_ay jr z,setdriver2 dec a ld hl,play_sample_digi jr z,setdriver2 ld hl,play_sample_amdrum setdriver2: ld (driver+1),hl ret ;;------------------------------------------------------------------------------- ;; this is the time for CALL: RET combination time_call equ 5+3 ;; these are the timings for the bare code to write to the hardware ;; we use these to modify the timings ;; for the following see the timings in [] brackets next to the instructions ;; in each code ;; timing to write to digiblaster+time for call to delay sample_digi_time equ 5+2+1+1+1+3+time_call ;; timing to write to amdrum+time for call to delay sample_amdrum_time equ 2+2+4+2+1+1+3+2+time_call ;; timing to write to ay+time for call to delay sample_ay_time equ 2+2+4+2+1+2+1+1+1+3+time_call setspeed: ld a,(ix+0) ;; A = speed id set_speed2: ;; ensure it is within range cp 17 jr c,set_speed3 xor a set_speed3: ;; lookup into table to get the number of cycles between each write to the hardware ld l,a ld h,0 add hl,hl ld de,speed_table add hl,de ld e,(hl) inc hl ld d,(hl) ;; DE = number of cycles between each write of the hardware ;; now adjust for digiblaster playback ld bc,sample_digi_time call calc_speed ld (speed_digi+1),hl ;; now adjust for amdrum playback ld bc,sample_amdrum_time call calc_speed ld (speed_amdrum+1),hl ;; now adjust for AY-3-8912 playback ld bc,sample_ay_time call calc_speed ld (speed_ay+1),hl ret ;;------------------------------------------------------------------------------- ;; de = timing value from speed_table ;; bc = timing value for instructions to update hardware for playback calc_speed: ld l,e ld h,d or a sbc hl,bc ;; HL = delay we require (including CALL/RET) ;; calculate address from end of delay so we have the correct number of NOPs ;; until RET for this delay ld c,l ld b,h ld hl,end_speed_delay or a sbc hl,bc ret ;;------------------------------------------------------------------------------- ;; Calculating the timings: ;; ;; Hz = number of samples played per second ;; 1,000,000 "NOP cycles" processed by Z80 per second. ;; A "NOP cycle" is a timing unit within the Amstrad enforced by the video hardware. ;; So to playback at the rate we want ;; we need to update the sound hardware at a rate defined in the table below. ;; These values don't take into account the actual time needed to read the sample, ;; convert it it required, write to hardware, and then update playback values ;; ;; Cycles = 1,000,000/Hz speed_table: defw 1000 ;; 1KHz (1000hz) defw 1000 ;; 1KHz (1000hz) defw 500 ;; 2Khz (2000hz) defw 333 ;; 3Khz (3000hz) defw 250 ;; 4Khz (4000hz) defw 200 ;; 5Khz (5000hz) defw 167 ;; 6Khz (6000hz) defw 143 ;; 7Khz (7000hz) defw 125 ;; 8Khz (8000hz) defw 111 ;; 9Khz (9000hz) defw 100 ;; 10Khz (10000hz) defw 91 ;; 11Khz (11025hz) defw 83 ;; 12Khz (12000hz) defw 77 ;; 13Khz (13000hz) defw 71 ;; 14Khz (14000hz) defw 67 ;; 15Khz (15000hz) defw 63 ;; 16Khz (16000hz) ;;------------------------------------------------------------------------------- ;; The speed is controlled by a software delay. ;; ;; This part of the code has 1000 NOP instructions, enough for a playback rate of 1khz. ;; But we have the accuracy of finer control, so we could in theory have other rates than we have defined. ;; The fastest rate that the CPC can playback is around 18Khz, any faster and the CPC can't write to the hardware ;; fast enough. ;; ;; Using the speed_table and the number of cycles that is needed to update the ;; hardware we calculate an address within this code here. The code CALLs to the calculated address ;; then the nops are executed until RET is reached, and then this returns back to the ;; playback code. speed_delay: defs 1000 end_speed_delay: ret ;;------------------------------------------------------------------------------- ;; this is the main RSX entry point for driver playback playsample: cp 2 ret nz driver: jp play_sample_ay ;;------------------------------------------------------------------------------- play_sample_digi: ;; This is the RSX function for the Digiblaster "driver" ld e,(ix+0) ld d,(ix+1) ld l,(ix+2) ld h,(ix+3) ;; DE = length in samples ;; HL = start address of sample data ;; Play a sample using the Digiblaster/SoundPlayer ;; ;; This device plays 8-bit mono signed samples. ;; ;; This device is connected to the printer port on the CPC. ;; The recommended port number for this is &efxx. ;; xx = any value ;; ;; disable interrupts. This stops interrupts from breaking our timing ;; and breaking the sound. di ;; OUTI decrements B before sending data to I/O port. ;; So B = &ef+1 ld b,&f0 digiloop: ;; This performs the same as: ;; DEC B ;; LD A,(HL) ;; OUT (C),A ;; INC HL ;; ;; but without effecting A register and being much quicker outi ;; [5] ;; restore B back for next iteration of loop inc b ;; [1] ;; this CALL is modified based on the playback rate chosen speed_digi: call 0 ;; update number of samples remaining to play dec de ;; [2] ld a,d ;; [1] or e ;; [1] jp nz,digiloop ;; [3] ;; re-enable interrupts again ei ;; return back to basic ret ;;------------------------------------------------------------------------------- play_sample_amdrum: ;; This is the RSX function for the Amdrum "driver" ld e,(ix+0) ld d,(ix+1) ld l,(ix+2) ld h,(ix+3) ;; DE = length in samples ;; HL = start address of sample data ;; Play a sample using the Amdrum ;; ;; This device plays 8-bit mono unsigned samples. ;; ;; The recommended port number for this is &ffxx. ;; xx = any value ;; disable interrupts. This stops interrupts from breaking our timing ;; and breaking the sound. di ld b,&ff amdrumloop: ;; read sample byte ld a,(hl) ;; [2] ;; convert from signed to unsigned xor &80 ;; [2] ;; write to amdrum out (c),a ;; [4] ;; update sample pointer inc hl ;; [2] ;; this CALL is modified based on the playback rate chosen speed_amdrum: call 0 ;; update number of bytes remaining to play dec de ;; [2] ld a,d ;; [1] or e ;; [1] jp nz,amdrumloop ;; [3] ei ;; return back to BASIC ret ;;------------------------------------------------------------------------------- ;; channel definition for ay-playback channel: db 9 ;;------------------------------------------------------------------------------- setleft: ;; channel A ld a,8 ld (channel),a ret ;;------------------------------------------------------------------------------- setcenter: ;; channel B ld a,9 ld (channel),a ret ;;------------------------------------------------------------------------------- setright: ;; channel C ld a,10 ld (channel),a ret ;;------------------------------------------------------------------------------- play_sample_ay: ld l,(ix+2) ld h,(ix+3) ;; HL = start address of sample data ;; disable interrupts to stop BASIC and firmware from causing corruption ;; to sample playback di exx ;; store de' for basic push de ;; now setup length of sample data in DE' ld e,(ix+0) ld d,(ix+1) ;; DE' = length in samples exx call init_ay_sample ;; select ay volume register that we will write sample data to ld b,&f4 ld a,(channel) out (c),a ;; "write register index" ld bc,&f6c0 out (c),c ;; "inactive" ld bc,&f600 out (c),c ;; "write data" ld bc,&f680 out (c),c ;; I/O port to write to AY ld b,&f4 ;; upper byte of table aligned to 256 byte boundary (i.e. lower byte is 0) ld d, table /256 ayloop: ;; convert 8-bit sample to 4-bit sample ;; read 8-bit sample ld e,(hl) ;; [2] ;; lookup into table to get 4-bit sample ld a,(de) ;; [2] ;; write to AY out (c),a ;; [4] ;; increment sample pointer inc hl ;; [2] ;; this CALL is modified based on the playback rate chosen speed_ay: call 0 ;; update number of bytes remaining to play exx ;; [1] dec de ;; [2] ld a,d ;; [1] or e ;; [1] exx ;; [1] jp nz,ayloop ;; [3] ;; restore de' for basic exx pop de exx ;; "inactive" ld bc,&f600 out (c),c ;; enable interrupts for basic ei ;; return to basic ret ;;------------------------------------------------------------------------------- ;; write data to AY registers to initialise for sample playback init_ay_sample: ld hl,samp_init_ay_registers ld b,14 ld d,0 ias1: push bc ld e,(hl) inc hl call write_ay_reg pop bc inc d djnz ias1 ret ;;------------------------------------------------------------------------------- ;; data to init AY for sample playback samp_init_ay_registers: defb 0 ;; channel A tone period defb 0 defb 0 ;; channel B tone period defb 0 defb 0 ;; channel C tone period defb 0 defb 0 ;; noise period defb &3f ;; mixer (disable tone and noise) defb 0 ;; channel A volume defb 0 ;; channel B volume defb 0 ;; channel C volume defb 0 ;; envelope period defb 0 defb 0 ;; envelope shape ;;------------------------------------------------------------------------------- ;; Write data to an AY register ;; E = register ;; D = data write_ay_reg: ;; write register index ld b,&f4 out (c),e ;; "write register index" ld bc,&f6c0 out (c),c ;; "inactive" ld c,&00 out (c),c ;; write register data ld b,&f4 out (c),d ;; "write data" ld bc,&f680 out (c),c ;; "inactive" ld c,0 out (c),c ret ;;------------------------------------------------------------------------------- ;; initialise lookup table to convert from 8-bit signed mono samples ;; to 4-bit AY volume register values init_table: ;; We look at 8-bit unsigned mono samples, check for specific values ;; and map these to AY volume register values. ;; NOTE: The AY volume output is non-linear. ;; We then want to put it into the table so that 8-bit signed mono values ;; can be used for lookup. ;; start address of table ld hl,table ;; start 8-bit unsigned mono sample value ld d,0 ;; count (8-bit = 256 possible values) ld b,0 ;; The values used here are those used by Gasman in his Spectrum demos. init_tab2: ld a,d cp 2 ld c,0 jr c,init_tab3 cp 5 ld c,1 jr c,init_tab3 cp 7 ld c,2 jr c,init_tab3 cp 10 ld c,3 jr c,init_tab3 cp 14 ld c,4 jr c,init_tab3 cp 19 ld c,5 jr c,init_tab3 cp 29 ld c,6 jr c,init_tab3 cp 40 ld c,7 jr c,init_tab3 cp 56 ld c,8 jr c,init_tab3 cp 80 ld c,9 jr c,init_tab3 cp 103 ld c,10 jr c,init_tab3 cp 131 ld c,11 jr c,init_tab3 cp 161 ld c,12 jr c,init_tab3 cp 197 ld c,13 jr c,init_tab3 cp 236 ld c,14 jr c,init_tab3 ld c,15 init_tab3: ;; A = D ;; C = AY register value ;; 8-bit unsigned to 8-bit signed xor &80 ld l,a ;; L = low byte of address in table ;; but also effectively the sample value ;; write into table ;; C = value for AY ld (hl),c inc d djnz init_tab2 ret