diff --git a/compiler/res/prog8lib/cx16/psg.p8 b/compiler/res/prog8lib/cx16/psg.p8 new file mode 100644 index 000000000..99d6890b2 --- /dev/null +++ b/compiler/res/prog8lib/cx16/psg.p8 @@ -0,0 +1,103 @@ +%import syslib + +psg { + ; $1F9C0 - $1F9FF 16 blocks of 4 PSG registers (16 voices) + ; 00 frequency word LSB + ; 01 frequency word MSB. freqword = HERZ / 0.3725290298461914 + ; 02 bit 7 =right, bit 6 = left, bits 5-0 = volume 0-63 levels + ; 03 bit 7,6 = waveform, bits 5-0 = Pulse width 0-63 + ; waveform: 0=pulse, 1=sawtooth, 2=triangle, 3=noise + const ubyte PULSE = %00000000 + const ubyte SAWTOOTH = %01000000 + const ubyte TRIANGLE = %10000000 + const ubyte NOISE = %11000000 + const ubyte LEFT = %01000000 + const ubyte RIGHT = %10000000 + + sub voice(ubyte voice_num, ubyte channel, ubyte volume, ubyte waveform, ubyte pulsewidth) { + cx16.vpoke(1, $f9c2 + voice_num * 4, channel | volume) + cx16.vpoke(1, $f9c3 + voice_num * 4, waveform | pulsewidth) + } + +; sub freq_hz(ubyte voice_num, float hertz) { +; ; this would rely on floating point math to convert hertz to vera frequency +; ; TODO should be replaced by integer math maybe with a lookup table? +; uword vera_freq = (hertz / 0.3725290298461914) as uword +; freq_vera(voice_num, vera_freq) +; } + + sub freq_vera(ubyte voice_num, uword vera_freq) { + cx16.vpoke(1, $f9c1 + voice_num*4, msb(vera_freq)) + cx16.vpoke(1, $f9c0 + voice_num*4, lsb(vera_freq)) + } + + sub volume(ubyte voice_num, ubyte vol) { + uword reg = $f9c2 + voice_num * 4 + cx16.vpoke(1, reg, cx16.vpeek(1, reg) & %11000000 | vol) + } + + sub pulse_width(ubyte voice_num, ubyte pw) { + uword reg = $f9c3 + voice_num * 4 + cx16.vpoke(1, reg, cx16.vpeek(1, reg) & %11000000 | pw) + } + + sub envelope(ubyte voice_num, ubyte attack, ubyte release) { + envelope_attacks[voice_num] = attack * $0040 + envelope_releases[voice_num] = release * $0040 + if attack + attack = 0 + else + attack = 63 ; max volume when no attack is set + envelope_volumes[voice_num] = mkword(attack, 0) + envelope_states[voice_num] = 0 + } + + sub silent() { + ubyte voice + for voice in 0 to 15 { + volume(voice, 0) + envelope_volumes[voice] = 0 + envelope_states[voice] = 1 + } + } + + sub envelopes_irq() { + ubyte vera_ctrl = cx16.VERA_CTRL + ubyte vera_addr_h = cx16.VERA_ADDR_H + ubyte vera_addr_m = cx16.VERA_ADDR_M + ubyte vera_addr_l = cx16.VERA_ADDR_L + uword vol_word + ubyte voice + for voice in 0 to 15 { + if envelope_states[voice] { + ; release + vol_word = envelope_volumes[voice] - envelope_releases[voice] + if msb(vol_word) & %11000000 { + vol_word = 0 + envelope_releases[voice] = 0 + } + envelope_volumes[voice] = vol_word + volume(voice, msb(vol_word)) + } else { + ; attack + vol_word = envelope_volumes[voice] + envelope_attacks[voice] + if msb(vol_word) & %11000000 or envelope_attacks[voice]==0 { + vol_word = mkword(63, 0) + envelope_attacks[voice] = 0 + envelope_states[voice] = 1 ; start release + } + envelope_volumes[voice] = vol_word + volume(voice, msb(vol_word)) + } + } + cx16.VERA_CTRL = vera_ctrl + cx16.VERA_ADDR_L = vera_addr_l + cx16.VERA_ADDR_M = vera_addr_m + cx16.VERA_ADDR_H = vera_addr_h + } + + ubyte[16] envelope_states + uword[16] envelope_volumes + uword[16] envelope_attacks + uword[16] envelope_releases +} diff --git a/docs/source/libraries.rst b/docs/source/libraries.rst index d2ec69b56..79f6120e6 100644 --- a/docs/source/libraries.rst +++ b/docs/source/libraries.rst @@ -364,3 +364,9 @@ Also contains a helper function to calculate the file size of a loaded file (alt to 16 bits, 64Kb) +psg (cx16 only) +---------------- +Available for the Cx16 target. +Contains a simple abstraction for the Vera's PSG (programmable sound generator) to play simple waveforms. +It includes an interrupt routine to handle simple Attack/Release envelopes as well. +See the examples/cx16/bdmusic.p8 program for ideas how to use it. diff --git a/examples/cx16/bdmusic.p8 b/examples/cx16/bdmusic.p8 index 64135cc8c..6d0731df8 100644 --- a/examples/cx16/bdmusic.p8 +++ b/examples/cx16/bdmusic.p8 @@ -1,53 +1,48 @@ %import textio %import syslib %import floats +%import psg main { -sub start() { + sub start() { - txt.print("will play the music from boulderdash,\nmade in 1984 by peter liepa.\npress enter to start: ") - void c64.CHRIN() - txt.clear_screen() + txt.print("will play the music from boulderdash,\nmade in 1984 by peter liepa.\npress enter to start: ") + void c64.CHRIN() + txt.clear_screen() - repeat { - uword note - for note in notes { - ubyte note1 = lsb(note) - ubyte note2 = msb(note) - uword freqR = freq(note1) - uword freqL = freq(note2) - cx16.vpoke(1, $F9C0, lsb(freqR)) - cx16.vpoke(1, $F9C1, msb(freqR)) - cx16.vpoke(1, $F9C2, %10111111) ; left, max volume - cx16.vpoke(1, $F9C3, %10000000) ; triangle - cx16.vpoke(1, $F9C4, lsb(freqL)) - cx16.vpoke(1, $F9C5, msb(freqL)) - cx16.vpoke(1, $F9C6, %01111111) ; right, max volume - cx16.vpoke(1, $F9C7, %10000000) ; triangle + psg.voice(0, psg.LEFT, 63, psg.TRIANGLE, 0) + psg.voice(1, psg.RIGHT, 63, psg.TRIANGLE, 0) + cx16.set_irq(&psg.envelopes_irq, false) - ; TODO ADSR of some kind? - - print_notes(note1, note2) - sys.wait(10) + repeat { + uword note + for note in notes { + ubyte note0 = lsb(note) + ubyte note1 = msb(note) + psg.freq_vera(0, vera_freq(note0)) + psg.freq_vera(1, vera_freq(note1)) + psg.envelope(0, 255, 6) + psg.envelope(1, 255, 6) + print_notes(note0, note1) + sys.wait(10) + } } } -} -sub freq(ubyte note) -> uword { - float fword = freqs_hz[note-10] / (48828.125 / 131072.0) ; formula from the Vera PSG docs - return fword as uword -} + sub vera_freq(ubyte note) -> uword { + return (freqs_hz[note-10] / 0.3725290298461914) as uword + } -sub print_notes(ubyte n1, ubyte n2) { - txt.nl() - txt.plot(n1, txt.DEFAULT_HEIGHT-1) - txt.color(7) - txt.chrout('Q') - txt.plot(n2, txt.DEFAULT_HEIGHT-1) - txt.color(4) - txt.chrout('Q') -} + sub print_notes(ubyte n1, ubyte n2) { + txt.nl() + txt.plot(n1, txt.DEFAULT_HEIGHT-1) + txt.color(7) + txt.chrout('Q') + txt.plot(n2, txt.DEFAULT_HEIGHT-1) + txt.color(4) + txt.chrout('Q') + } ; details about the boulderdash music can be found here: