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