diff --git a/presets/apple2/deltamod.dasm b/presets/apple2/deltamod.dasm new file mode 100644 index 00000000..c6c06997 --- /dev/null +++ b/presets/apple2/deltamod.dasm @@ -0,0 +1,218 @@ + + 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 diff --git a/presets/apple2/springchicken.dat12.bin b/presets/apple2/springchicken.dat12.bin new file mode 100644 index 00000000..a5eec8df Binary files /dev/null and b/presets/apple2/springchicken.dat12.bin differ diff --git a/src/common/audio.ts b/src/common/audio.ts index 398b699e..554dac13 100644 --- a/src/common/audio.ts +++ b/src/common/audio.ts @@ -482,10 +482,12 @@ export var SampleAudio = function(clockfreq) { this.feedSample = function(value, count) { accum += value * count; sfrac += sinc * count; - if (sfrac > 1) { + if (sfrac >= 1) { accum /= sfrac; - this.addSingleSample(accum * sinc); - sfrac -= Math.floor(sfrac); + while (sfrac >= 1) { + this.addSingleSample(accum * sinc); + sfrac -= 1; + } accum *= sfrac; } } diff --git a/src/machine/apple2.ts b/src/machine/apple2.ts index 14941313..d03fd61d 100644 --- a/src/machine/apple2.ts +++ b/src/machine/apple2.ts @@ -4,10 +4,6 @@ import { Bus, BasicScanlineMachine, xorshift32, SavesState } from "../common/dev import { KeyFlags } from "../common/emu"; // TODO import { hex, lzgmini, stringToByteArray, RGBA, printFlags } from "../common/util"; -const cpuFrequency = 1022727; -const cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ -const cpuCyclesPerFrame = 65*262; - // TODO: read prodos/ca65 header? const VM_BASE = 0x803; // where to JMP after pr#6 const LOAD_BASE = VM_BASE; @@ -39,10 +35,11 @@ interface SlotDevice extends Bus { export class AppleII extends BasicScanlineMachine { - cpuFrequency = 1023000; + // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ + cpuFrequency = 1022727; sampleRate = this.cpuFrequency; - cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ - cpuCyclesPerFrame = 65*262; + cpuCyclesPerLine = 912/14; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ + cpuCyclesPerFrame = this.cpuCyclesPerLine * 262; canvasWidth = 280; numVisibleScanlines = 192; numTotalScanlines = 262; diff --git a/src/platform/apple2.ts b/src/platform/apple2.ts index 6c415278..c49dba89 100644 --- a/src/platform/apple2.ts +++ b/src/platform/apple2.ts @@ -16,6 +16,7 @@ const APPLE2_PRESETS = [ {id:'hgrtest.a', name:"HGR Test (ASM)"}, {id:'conway.a', name:"Conway's Game of Life (ASM)"}, {id:'lz4fh.a', name:"LZ4FH Decompressor (ASM)"}, + {id:'deltamod.dasm', name:"Delta Modulation (ASM)"}, // {id:'zap.dasm', name:"ZAP! (ASM)"}, // {id:'tb_6502.s', name:'Tom Bombem (assembler game)'}, ];