processor 6502 ;;; ;;; Apple ][ Delta Modulation Sample Playback ;;; ;;; This is the same technique used in the NES ;;; for DMC samples, but adapted for the Apple ][. ;;; Basically: Every 1 bit increases the signal ;;; by 1 step, every 0 bit decreases by 1 step. ;;; The signal is clipped on the top and bottom. ;;; ;;; The NES has a 6-bit DAC, but we only have a ;;; PWM-encoded signal. We have 13 different ;;; routines to generate pulses ranging from 4 to ;;; 14 microseconds long, with a 29-30 microsecond ;;; interval (when we reach 50% duty cycle we just ;;; invert the signal) ;;; ;;; Every 8 bits we have to fetch a new byte ;;; and increment the pointer. To avoid a high- ;;; frequency tone, we generate another pulse ;;; (the same width as the last) in this macro. ;;; ;;; also see http://michaeljmahon.com/RTSynth.html ;;; and https://github.com/oliverschmidt/Play-BTc ;;; ; uninitialized zero-page variables seg.u ZEROPAGE org $0 PWMBUF .ds 2 PWMLEN .ds 1 TRASH .ds 1 ; speaker toggle SPKR equ $c030 ; max pulse width = PULWID*2+4 cycles PULWID = 6 ; sleep {1} cycles ; must be multiple of 2 MAC SLEEP2 .CYCLES SET {1} IF .CYCLES == 1 ECHO "MACRO ERROR: 'SLEEP': Duration must be > 1" ERR ENDIF IF .CYCLES >= 12 jsr HandyRTS .CYCLES SET .CYCLES - 12 ENDIF REPEAT .CYCLES / 2 nop REPEND ENDM ENDM ; pulse macro ; flip speaker, wait N*2 cycles ; flip again, wait (MAX-N)*2 cycles ; total = 8 + PULWID*2 cycles MAC PULSE sta SPKR SLEEP2 {1}*2 sta SPKR SLEEP2 (PULWID-{1})*2 ENDM ; shift next bit into carry flag ; load next byte and increment ; pointer if needed ; {1} : index of previous routine ; {2} : index of current routine ; we also interleave a speaker pulse in this routine ; using macros (these are the IF/ENDIF blocks) MAC NEXTPULSE lsr ; next bit -> carry flag dex ; bit count == 0? bne .noinc ; skip inc/reload cycle ; total pulse = 15 + PULWID*2 cycles IF {2} > 0 sta SPKR ELSE SLEEP2 8 ; no SPKR toggle, take up 8 cycles ENDIF IF {2} == 1 sta SPKR ENDIF ; increment pointer lo byte iny ; 2-1 = 1 IF {2} == 2 sta SPKR ENDIF bne .noinchi ; 4 ; increment pointer hi byte inc PWMBUF+1 dec PWMLEN bne .noinchi rts ; end of sound data .noinchi IF {2} == 3 sta SPKR ENDIF ; reset X counter ldx #8 ; 6 IF {2} == 4 || {2} == 5 sta SPKR ENDIF ; fetch new byte lda (PWMBUF),y ; 11 ; a few NOPs to match the normal pulse width ; (inx, lsr, branch) IF {2} == 6 sta SPKR nop ENDIF IF {2} >= 7 nop sta SPKR ENDIF ; make up cycles we missed SLEEP2 PULWID*2-6 .noinc ; carry flag still valid from lsr bcc LVLS{1} ENDM ;;; start of code seg CODE org $803 ; starting address Start lda #>(SAMPLE_END-SAMPLES)+1 sta PWMLEN lda #SAMPLES sta PWMBUF+1 jsr Play jmp Start ; handy RTS for wasting 6+6 cycles HandyRTS rts ; each pulse takes PULWID*2+18(-1) cycles ; every 8 bits, the last pulse is repeated ; 1022727 / 29.5 / (9/8) = 30817 Hz ; Y = PWMBUF lo address ; PWMBUF+0 = 0 Play ldy PWMBUF lda #0 sta PWMBUF ldx #8 lda (PWMBUF),y ; each label emits a pulse at a duty cycle ; from 0 (silent) to 7 ; if bit is 1, falls through to next highest level ; otherwise, jumps back to previous level LVLS0 SLEEP2 PULWID*2+8 ; sleep, don't click NEXTPULSE 0,0 LVLS1 PULSE 0 NEXTPULSE 0,1 LVLS2 PULSE 1 NEXTPULSE 1,2 LVLS3 PULSE 2 NEXTPULSE 2,3 LVLS4 PULSE 3 NEXTPULSE 3,4 LVLS5 PULSE 4 NEXTPULSE 4,5 LVLS6 LVLS6INV equ *+3 ; skip STA SPKR to invert signal PULSE 5 NEXTPULSE 5,6 bcs LVLS7INV ; 1 = invert signal LVLS7 LVLS7INV equ *+3 ; skip STA SPKR to invert signal PULSE 4 NEXTPULSE 6INV,5 ; 0 = invert signal LVLS8 PULSE 3 NEXTPULSE 7,4 LVLS9 PULSE 2 NEXTPULSE 8,3 LVLS10 PULSE 1 NEXTPULSE 9,2 LVLS11 PULSE 0 NEXTPULSE 10,1 LVLS12 SLEEP2 PULWID*2+8 ; sleep, don't click NEXTPULSE 11,0 bcs LVLS12 ; if 1, stay at highest level ; delta-encoded samples SAMPLES .incbin "springchicken.dat12.bin" ; "Spring Chicken" by BryanTeoh SAMPLE_END