diff --git a/Makefile b/Makefile index 5e57313..b69f7c5 100644 --- a/Makefile +++ b/Makefile @@ -34,12 +34,27 @@ never-gonna-give-you-up.wav : never-gonna-give-you-up.mp4 -ar 15704 \ -y $@ +rickroll-17s.wav.s : rickroll-17s.wav pack-vocoder.js + node pack-vocoder.js $< $@ + +rickroll-17s.wav : rickroll-17s.mp4 + ffmpeg -i "rickroll-17s.mp4" \ + -vn \ + -t 2.0 \ + -acodec pcm_u8 \ + -ac 1 \ + -ar 15000 \ + -y $@ + %.o : %.s ca65 -v -t atari -o $@ $< -%.xex : %.o dither4.o never-gonna-give-you-up.wav.o atari-asm-xex.cfg +rickroll.xex : rickroll.o dither4.o never-gonna-give-you-up.wav.o atari-asm-xex.cfg ld65 -v -C ./atari-asm-xex.cfg -o $@ dither4.o never-gonna-give-you-up.wav.o $< +vocoder.xex : rickroll.o vocoder.o rickroll-17s.wav.o atari-asm-xex.cfg + ld65 -v -C ./atari-asm-xex.cfg -o $@ vocoder.o rickroll-17s.wav.o $< + clean : rm -f *.o rm -f *.s.png diff --git a/dither-image.js b/dither-image.js index f14f1d8..f311ffd 100644 --- a/dither-image.js +++ b/dither-image.js @@ -79,8 +79,7 @@ class RGB { let lm = val & 15; let crlv = cr ? 50 : 0; - /* - let phase = ((cr - 1) * 25 - 58) * (2 * Math.PI / 360); + let phase = ((cr - 1) * 25 - 33) * (2 * Math.PI / 360); let y = 255 * (lm + 1) / 16; let i = crlv * Math.cos(phase); @@ -89,8 +88,8 @@ class RGB { let r = y + 0.956 * i + 0.621 * q; let g = y - 0.272 * i - 0.647 * q; let b = y - 1.107 * i + 1.704 * q; - */ + /* // PAL let phase = ((cr - 1) * 25.7 - 15) * (2 * Math.PI / 360); @@ -101,8 +100,9 @@ class RGB { let r = y + 0.956 * i + 0.621 * q; let g = y - 0.272 * i - 0.647 * q; let b = y - 1.107 * i + 1.704 * q; + */ - return new RGB(r, g, b).clamp().fromSRGB(); + return new RGB(r, g, b).clamp().fromNTSC(); } map(callback) { @@ -1004,11 +1004,9 @@ ${byte2byte(odd(frame.palette3))} .align 1024 displaylist: ; 24 lines overscan - .repeat 2 + .repeat 3 .byte $70 ; 8 blank lines .endrep - ; include a DLI to mark us as frame 0 - .byte $f0 ; 8 blank lines ; ${height} lines graphics ; ANTIC mode e (160px 2bpp, 1 scan line per line) diff --git a/dither4.s b/dither4.s index cdb2371..5c942f8 100644 --- a/dither4.s +++ b/dither4.s @@ -27,7 +27,6 @@ sample_ptrh = $85 sample_ptr = sample_ptrl scanline = $86 audiotemp = $87 -frame_counter = $89 ;height = 160 height = 192 @@ -66,6 +65,7 @@ audio_high_byte: byteseq $3 byteseq $4 byteseq $5 + byteseq $6 byteseq $7 byteseq $8 byteseq $9 @@ -102,12 +102,6 @@ audio_high_byte: lda #.hibyte(displaylist) sta DLISTH - ; Set up the DLI handler - lda #.lobyte(dli_handler) - sta VDSLSTL - lda #.hibyte(dli_handler) - sta VDSLSTH - ; Disable VBI but allow Reset and DLI lda #$a0 sta NMIEN @@ -158,25 +152,24 @@ wait_loop: sta AUDC1 ; 4 cyc .endmacro - .macro audio_prep + .macro audio_prep ; 8-9 cycles ; Y is VCOUNT at entry lda (sample_ptr),y ; 5/6 cyc sta audiotemp ; 3 cyc .endmacro - ; call with A pre-loaded to audiotemp - .macro audio_play_lo - ;lda audiotemp ; 3 cyc - and #$0f ; 2 cyc - ora #$10 ; 2 cyc - sta AUDC1 ; 4 cyc + .macro audio_play_lo ; 8 cycles + ; A is loaded with packed audio byte at entry + and #$0f ; 2 cyc + ora #$10 ; 2 cyc + sta AUDC1 ; 4 cyc .endmacro ; clobbers Y .macro audio_play_hi ; 12 cycles - ldy audiotemp ; 3 cyc + ldy audiotemp ; 3 cyc lda audio_high_byte,y ; 5 cyc - sta AUDC1 ; 4 cyc + sta AUDC1 ; 4 cyc .endmacro .macro audio_inc @@ -191,17 +184,12 @@ wait_loop: cmp #.hibyte(audio_samples_end) ; 2 cyc bmi audio_cont ; 2 cyc - sta WSYNC - ; 10 cycles, optional lda #.lobyte(audio_samples) ; 2 sta sample_ptrl ; 3 lda #.hibyte(audio_samples) ; 2 sta sample_ptrh ; 3 - sta WSYNC - ldy VCOUNT ; 4 cycles - audio_cont: .endmacro @@ -215,26 +203,22 @@ wait_loop: sty scanline ; 3 cycles inner_scanline frame_offset, 0 ; 23-26 cycles before break, 12 cycles after - ldy scanline ; 3 cycles - audio_prep - audio_play_lo + ldy scanline ; 3 cycles + audio_prep ; 8-9 cycles + audio_play_lo ; 8 cycles ldy scanline ; 3 cycles inner_scanline frame_offset, 128 ; 23-26 cycles before break, 12 cycles after - audio_play_hi + audio_play_hi ; 12 cycles - ; pair cleanup: 6 cycles ldy VCOUNT ; 4 cycles bne each_scanline_pair ; 2 cycles + ; Do bookkeeping during vblank! audio_inc ; 22-32 cycles + ;ldy VCOUNT ; 4 cycles - ; frame cleanup: 11 cycles - lda frame_counter ; 3 cycles - eor #1 ; 2 cycles - sta frame_counter ; 3 cycles - ;jmp wait_start ; 3 cycles jmp each_frame ; 3 cycles .endscope .endmacro @@ -244,10 +228,3 @@ run_frame1: run_frame 0 .endproc - - -.proc dli_handler - lda #0 - sta frame_counter - rti -.endproc diff --git a/pack-vocoder.js b/pack-vocoder.js new file mode 100644 index 0000000..22be90f --- /dev/null +++ b/pack-vocoder.js @@ -0,0 +1,102 @@ +import wavefile from 'wavefile'; +let WaveFile = wavefile.WaveFile; + +import {default as dct} from 'dct'; + +const sampleRate = 15000; +let frameRate = 60; +let samplesPerFrame = sampleRate / frameRate; + +import { + readFileSync, + writeFileSync +} from 'fs'; + +function audio2voices(samples) { + let voices = []; + + let floatSamples = samples.map((byte) => ((byte / 256) - 0.5) * 2); + let transformed = dct(floatSamples); + console.log('audio2voices'); + console.log(floatSamples); + console.log(transformed); + //throw new Error('xxxxx'); + + let freqs = transformed.map((_f, i) => i * frameRate); + + let bands = new Float64Array(256); + for (let i = 0; i < transformed.length; i++) { + let amplitude = transformed[i]; + let freq = freqs[i]; + if (freq == 0) { + continue; + } + let divisor = Math.floor(sampleRate / freq) - 1; + if (divisor > 255) { + continue; + } + bands[divisor] += amplitude; + } + console.log(bands); + + for (let i = 0; i < 4; i++) { + let divisor = 0; + let max = 0; + for (let j = 0; j < bands.length; j++) { + if (bands[j] > max) { + divisor = j; + max = bands[j]; + } + } + let amplitude16 = Math.floor(max * 7) + 8; + voices.push(divisor, amplitude16); + } + return voices; +} + +function byte2byte(arr) { + let lines = []; + for (let i=0; i < arr.length; i++) { + lines.push(`.byte ${arr[i]}`); + } + return lines.join('\n'); +} + +function output2assembly(output) { + return ` +.segment "AUDIO" + + .export audio_samples + .export audio_samples_end + +audio_samples: + ${byte2byte(output)} +audio_samples_end: + .byte 24 + +`; +} + +function wav2assembly(buffer) { + let wav = new WaveFile(buffer); + let samples = wav.getSamples(); + let seconds = samples.length / sampleRate; + let frames = Math.floor(seconds * frameRate); + + let output = []; + for (let i = 0; i < frames * samplesPerFrame; i += samplesPerFrame) { + let voices = audio2voices(samples.slice(i, i + samplesPerFrame)); + output.push(...voices); + } + + console.log(output); + return output2assembly(output); +} + +let infile = process.argv[2]; +let outfile = process.argv[3]; + +let buffer = readFileSync(infile); +let asm = wav2assembly(buffer); +writeFileSync(outfile, asm, 'utf-8'); + diff --git a/pack-wav.js b/pack-wav.js index 50379a0..9e87df9 100644 --- a/pack-wav.js +++ b/pack-wav.js @@ -20,7 +20,7 @@ class Dither { } to4bit(val8) { - let val = (val8 / 255) - this.err; + let val = (val8 / 255) + this.err; if (val < 0) { val = 0; } @@ -29,7 +29,7 @@ class Dither { } let val4 = Math.round(val * 15); let dithered = (val4 / 15); - this.err = (dithered - val); + this.err = (val - dithered); return val4; } } diff --git a/package-lock.json b/package-lock.json index 19cd297..1a1c1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "wavefile": "^11.0.0" }, "devDependencies": { + "dct": "^0.1.0", "eslint": "^8.36.0" } }, @@ -814,6 +815,16 @@ "node": ">= 8" } }, + "node_modules/dct": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dct/-/dct-0.1.0.tgz", + "integrity": "sha512-/uUtEniuMq1aUxvLAoDtAduyl12oM1zhA/le2f83UFN/9+4KDHXFB6znEfoj5SDDLiTpUTr26NpxC7t8IFOYhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2603,6 +2614,12 @@ "which": "^2.0.1" } }, + "dct": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dct/-/dct-0.1.0.tgz", + "integrity": "sha512-/uUtEniuMq1aUxvLAoDtAduyl12oM1zhA/le2f83UFN/9+4KDHXFB6znEfoj5SDDLiTpUTr26NpxC7t8IFOYhQ==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 9d78029..755fbef 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "wavefile": "^11.0.0" }, "devDependencies": { + "dct": "^0.1.0", "eslint": "^8.36.0" } } diff --git a/vocoder.s b/vocoder.s new file mode 100644 index 0000000..5c942f8 --- /dev/null +++ b/vocoder.s @@ -0,0 +1,230 @@ +SAVMSC = $58 +VDSLST = $200 +VDSLSTL = $200 +VDSLSTH = $201 +COLPF0 = $D016 +COLPF1 = $D017 +COLPF2 = $D018 +COLPF3 = $D019 +COLBK = $D01A + +AUDC1 = $D201 +DMACTL = $D400 +DLISTL = $D402 +DLISTH = $D403 +WSYNC = $D40A +VCOUNT = $D40B +NMIEN = $D40E + +temp1l = $80 +temp1h = $81 +temp1 = temp1l +temp2l = $82 +temp2h = $83 +temp2 = temp2l +sample_ptrl = $84 +sample_ptrh = $85 +sample_ptr = sample_ptrl +scanline = $86 +audiotemp = $87 + +;height = 160 +height = 192 +bytes_per_line = 40 +pages_per_frame = 32 +lines_per_frame = 262 +;scanline_offset = 31 + (40 - 24) / 2 +scanline_offset = 30 +scanline_max = (lines_per_frame - scanline_offset) / 2 + +.data + +.import audio_samples +.import audio_samples_end + +.import frame1_top +.import frame1_bottom +.import frame1_palette1_even +.import frame1_palette1_odd +.import frame1_palette2_even +.import frame1_palette2_odd +.import frame1_palette3_even +.import frame1_palette3_odd +.import displaylist + +audio_high_byte: + .scope + .macro byteseq val + .repeat 16 + .byte val | $10 + .endrep + .endmacro + byteseq $0 + byteseq $1 + byteseq $2 + byteseq $3 + byteseq $4 + byteseq $5 + byteseq $6 + byteseq $7 + byteseq $8 + byteseq $9 + byteseq $a + byteseq $b + byteseq $c + byteseq $d + byteseq $e + byteseq $f + .endscope + +.code + +.export start + +.proc start + ; Set up the audio sample buffer + lda #.lobyte(audio_samples) + sta sample_ptrl + lda #.hibyte(audio_samples) + sta sample_ptrh + + ; Disable display DMA + lda #$00 + sta DMACTL + + ; Disable VBI and DLI but allow Reset + lda #$20 + sta NMIEN + + ; Set up the display list + lda #.lobyte(displaylist) + sta DLISTL + lda #.hibyte(displaylist) + sta DLISTH + + ; Disable VBI but allow Reset and DLI + lda #$a0 + sta NMIEN + + ; Manually wait for first scan line +wait_vblank: + sta WSYNC + lda VCOUNT + bne wait_vblank + + ; Re-enable display DMA + lda #$22 + sta DMACTL + +wait_start: + ; Wait for the vblank + ; Resynchronize the scanline counter +wait_loop: + ldy VCOUNT ; 4 cycles + bne wait_loop ; 2 cycles + + .macro inner_scanline frame_offset, line_offset + ; Y should be VCOUNT at entry + ; it'll fire on unused lines, but harmlessly + + ; 23-26 cycles before break + ; Leisurely memory fetches + lda frame1_palette1_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5 @FIXME alternate + pha ; 3 + ldx frame1_palette2_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5 + lda frame1_palette3_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5 + tay ; 2 + pla ; 3 + + ; Wait for horizontal blank + sta WSYNC ; 4 + + ; 12 cycles after break + ; Update color registers as fast as possible + sta COLPF0 ; 4 + stx COLPF1 ; 4 + sty COLPF2 ; 4 + .endmacro + + .macro audio_play_raw + ;ldy VCOUNT ; set on entry + lda (sample_ptr),y ; 5/6 cyc + sta AUDC1 ; 4 cyc + .endmacro + + .macro audio_prep ; 8-9 cycles + ; Y is VCOUNT at entry + lda (sample_ptr),y ; 5/6 cyc + sta audiotemp ; 3 cyc + .endmacro + + .macro audio_play_lo ; 8 cycles + ; A is loaded with packed audio byte at entry + and #$0f ; 2 cyc + ora #$10 ; 2 cyc + sta AUDC1 ; 4 cyc + .endmacro + + ; clobbers Y + .macro audio_play_hi ; 12 cycles + ldy audiotemp ; 3 cyc + lda audio_high_byte,y ; 5 cyc + sta AUDC1 ; 4 cyc + .endmacro + + .macro audio_inc + ; 22 cycles + lda sample_ptrl ; 3 cyc + clc ; 2 cyc + adc #131 ; 2 cyc + sta sample_ptrl ; 3 cyc + lda sample_ptrh ; 3 cyc + adc #0 ; 2 cyc + sta sample_ptrh ; 3 cyc + cmp #.hibyte(audio_samples_end) ; 2 cyc + bmi audio_cont ; 2 cyc + + ; 10 cycles, optional + lda #.lobyte(audio_samples) ; 2 + sta sample_ptrl ; 3 + lda #.hibyte(audio_samples) ; 2 + sta sample_ptrh ; 3 + + audio_cont: + .endmacro + + .macro run_frame frame_offset + .scope + ; each scanline is 228 color clocks + ; that's 114 CPU cycles + ; minus 41-43 for DMA leaves 71-73 clock cycles per line + + each_scanline_pair: + sty scanline ; 3 cycles + inner_scanline frame_offset, 0 ; 23-26 cycles before break, 12 cycles after + + ldy scanline ; 3 cycles + audio_prep ; 8-9 cycles + audio_play_lo ; 8 cycles + + ldy scanline ; 3 cycles + inner_scanline frame_offset, 128 ; 23-26 cycles before break, 12 cycles after + + audio_play_hi ; 12 cycles + + ldy VCOUNT ; 4 cycles + bne each_scanline_pair ; 2 cycles + + ; Do bookkeeping during vblank! + audio_inc ; 22-32 cycles + ;ldy VCOUNT ; 4 cycles + + jmp each_frame ; 3 cycles + .endscope + .endmacro + +each_frame: +run_frame1: + run_frame 0 + +.endproc