;; 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,<start>,<length>
							;; Play sample using chosen driver
							;; Sample data is 8-bit signed mono

defb "DRIVE","R"+&80		;; |DRIVER,<index>
							;; 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,<number>
							;; 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