CP-1610 machine code, 136 DECLEs1=170 bytes
1. CP-1610 instructions are encoded with 10-bit values (0x000 to 0x3FF), known as DECLEs. Although the Intellivision is also able to work on 16-bit data, programs were really stored in 10-bit ROM back then.
A fantasy console?!? Let's do it on a real one!
This code is meant to be run on a PAL Intellivision (50Hz). It would also work on an NTSC system (60Hz), but at a different pitch and speed.
Demo
Here is what we get on the real hardware (YouTube video), using the LTO Flash!.
Source code
ROMW 10 ; use 10-bit ROM width
0x4800 ORG $4800 ; map our code at $4800
;; -------------------------------------------------------- ;;
;; build the note period table in RAM: ;;
;; p(0) = 1432 ;;
;; p(n) = p(n-1) * 483 + 350 >> 9 ;;
;; -------------------------------------------------------- ;;
4800 001 SDBD ; R0 = note period, starting at ...
4801 2B8 098 005 MVII #1432, R0 ; ... the highest value 1432 for F-2
4804 2BC 2F0 MVII #$2F0, R4 ; R4 = write pointer in RAM
4806 260 init MVO@ R0, R4 ; write the period
4807 1C9 CLRR R1 ; R1 and R3 are the most significant
4808 1DB CLRR R3 ; words in R1:R0 and R3:R2 (32-bit)
; multiplication by 483, split into 2 + 1 - 32 + 512
4809 082 MOVR R0, R2 ; R2 = R0 \
480A 048 SLL R0 ; R0 *= 2 |__ 16-bit operations
480B 0C2 ADDR R0, R2 ; R2 += R0 |
480C 04C SLL R0, 2 ; R0 *= 4 /
480D 05C SLLC R0, 2 ; 32-bit values required from here
480E 055 RLC R1, 2 ; R1:R0 *= 4
480F 102 SUBR R0, R2 ; R3:R2 -= R1:R0
; (carry never set -> no ADCR R3)
4810 013 DECR R3
4811 10B SUBR R1, R3
4812 05C SLLC R0, 2 ; R1:R0 *= 16
4813 055 RLC R1, 2
4814 05C SLLC R0, 2
4815 055 RLC R1, 2
4816 0C2 ADDR R0, R2 ; R3:R2 += R1:R0
4817 02B ADCR R3
4818 0CB ADDR R1, R3
4819 2FA 15E ADDI #350, R2 ; R3:R2 += 350
; (carry never set -> no ADCR R3)
481B 042 SWAP R2 ; R2 = R3:R2 >> 9
481C 043 SWAP R3
481D 3BA 0FF ANDI #$FF, R2
481F 1DA XORR R3, R2
4820 07A SARC R2
4821 090 MOVR R2, R0 ; copy R2 to R0
4822 378 00F CMPI #$F, R0 ; loop until R0 = $F
4824 22E 01F BGT init
;; -------------------------------------------------------- ;;
;; play the song ;;
;; -------------------------------------------------------- ;;
4826 240 1FB MVO R0, $1FB ; set the volume on channel A to $F
4828 001 loop SDBD ; R4 = pointer into chords table
4829 2BC 063 048 MVII #chords, R4
482C 2A2 chord MVI@ R4, R2 ; R2 = initial note address
482D 274 PSHR R4 ; save R4 on the stack
482E 2E4 ADD@ R4, R4 ; R4 = pointer into delta values
482F 2BD 008 MVII #8, R5 ; R5 = octave counter
4831 093 octave MOVR R2, R3 ; copy R2 to R3
4832 2B9 004 MVII #4, R1 ; R1 = note counter
4834 298 note MVI@ R3, R0 ; R0 = note period
4835 240 1F0 MVO R0, $1F0 ; save the low bits
4837 040 SWAP R0
4838 240 1F4 MVO R0, $1F4 ; save the high bits
483A 001 SDBD ; wait for ~184500 cycles
483B 2B8 00C 030 MVII #12300, R0 ; (approximately 185ms)
483E 010 spin DECR R0 ; (6 cycles)
483F 22C 002 BNEQ spin ; (9 cycles)
4841 2E3 ADD@ R4, R3 ; update R3
4842 33B 005 SUBI #5, R3
4844 011 DECR R1 ; decrement the note counter
4845 22C 012 BNEQ note ; loop if not zero
4847 37D 005 CMPI #5, R5 ; compare octave counter with 5
4849 20C 003 BNEQ phase ; time to switch the phase? ...
484B 09A MOVR R3, R2 ; ... yes: copy R3 to R2
484C 200 008 B next
484E 20E 002 phase BGT asc ; ascending phase?
4850 33A 018 SUBI #24, R2 ; descending phase: -12 semitones
4852 2FA 00C asc ADDI #12, R2 ; ascending phase: +12 semitones
4854 33C 004 SUBI #4, R4 ; rewind R4
4856 015 next DECR R5 ; decrement the octave counter
4857 22C 027 BNEQ octave ; loop if not zero
4859 2B4 PULR R4 ; advance to the next chord
485A 00C INCR R4
485B 001 SDBD ; was it the last chord?
485C 37C 073 048 CMPI #ch_end, R4
485F 225 034 BLT chord
4861 220 03A B loop ; if yes, repeat from the beginning
;; -------------------------------------------------------- ;;
;; chords tables ;;
;; -------------------------------------------------------- ;;
4863 2F7 00E chords DECLE $2F7, add2 - $ - 1 ; C/add2
4865 2F4 013 DECLE $2F4, add2_m - $ - 1 ; Am/add2
4867 2F7 00A DECLE $2F7, add2 - $ - 1 ; C/add2
4869 2F4 00F DECLE $2F4, add2_m - $ - 1 ; Am/add2
486B 2F0 006 DECLE $2F0, add2 - $ - 1 ; F/add2
486D 2F2 004 DECLE $2F2, add2 - $ - 1 ; G/add2
486F 2F3 010 DECLE $2F3, maj7 - $ - 1 ; G#/maj7
4871 2F5 00E DECLE $2F5, maj7 - $ - 1 ; A#/maj7
0x4873 ch_end
; delta values, with +5 offset
4873 007 007 ... add2 DECLE 7, 7, 8, 10, 0, 2, 3
487A 007 006 ... add2_m DECLE 7, 6, 9, 10, 0, 1, 4
4881 009 008 ... maj7 DECLE 9, 8, 9, 6, 4, 1, 2
0x4888 end
How it works
About the Programmable Sound Generator (PSG)
The PAL-based Intellivision uses a 4.00MHz clock. The PSG is driven from a clock signal at half this rate. Internally, the PSG divides down its clock by 16 to determine the final square-wave frequency. The frequency of a tone produced by a PAL Intellivision is therefore given by:
$$F\_tone=\frac{4000000}{32\times P\_channel}$$
where \$P\_channel\$ is the period register setting for the given channel.
(adapted from the psg.txt
file included in jzIntv)
Building the note period table
The frequency ratio between two consecutive semitones is given by:
$$\sqrt[12]{2}\approx 1,0594631$$
And what we really need is the period ratio between two consecutive semitones:
$$1/\sqrt[12]{2}\approx 0,9438743$$
To apply this ratio to a period \$P_n\$, we use the following approximation:
$$P_{n+1}=\left\lfloor\frac{P_n \times 483 + 350}{512}\right\rfloor$$
We start with \$P_0=1432\$, which is the period for F2 (87.31Hz) on a PAL Intellivision, computed with the formula described in the previous section. We stop when a period of 15 is reached -- an arbitrary choice to have a register ready to initialize the volume on the PSG.
The CP-1610 doesn't have any multiplication instruction, so we have to compute it with additions and shifts. Besides, we need 32-bit values to have enough precision. So two pairs of 16-bit registers are used (R0/R1 and R2/R3).
The integer division by 512 is of course much simpler since it boils down to a right-shift by 9.
Chords encoding
Each chord is described by the address of the first note in our period table, followed by a pointer to 7 delta values:
a0, a1, a2, s, d0, d1, d2
where:
a0
, a1
, a2
are the delta values for the ascending phase, repeated 4 times and starting at the next octave each time
s
is the delta value applied when switching from the ascending to the descending phase
d0
, d1
, d2
are the delta values for the descending phase, repeated 4 times and starting at the previous octave each time
The actual delta values are obtained by subtracting 5 to the stored values.