apple2: delta modulation demo

This commit is contained in:
Steven Hugg 2020-07-15 19:01:28 -05:00
parent 73b766f1a3
commit ba2c288e8d
5 changed files with 228 additions and 10 deletions

View File

@ -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
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

Binary file not shown.

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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)'},
];