CP-1610 machine code, 125 DECLEs1=156.25 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 1D2 CLRR R2 ; use R3:R2 as a 32-bit value
4808 1DB CLRR R3 ; (initialized to 0)
4809 2B9 1E3 MVII #483, R1 ; process multiplication by 483
480B 0C2 mult ADDR R0, R2 ; by adding R0 that many times
480C 02B ADCR R3 ; to R3:R2
480D 011 DECR R1
480E 22C 004 BNEQ mult
4810 2FA 15E ADDI #350, R2 ; R3:R2 += 350
; (carry never set -> no ADCR R3)
4812 042 SWAP R2 ; R2 = R3:R2 >> 9
4813 043 SWAP R3
4814 3BA 0FF ANDI #$FF, R2
4816 1DA XORR R3, R2
4817 07A SARC R2
4818 090 MOVR R2, R0 ; copy R2 to R0
4819 378 00F CMPI #$F, R0 ; loop until R0 = $F
481B 22E 016 BGT init
;; -------------------------------------------------------- ;;
;; play the song ;;
;; -------------------------------------------------------- ;;
481D 240 1FB MVO R0, $1FB ; set the volume on channel A to $F
481F 001 loop SDBD ; R4 = pointer into chords table
4820 2BC 057 048 MVII #chords, R4
4823 2A2 chord MVI@ R4, R2 ; R2 = initial note address
4824 092 TSTR R2 ; time to loop?
4825 224 007 BEQ loop
4827 274 PSHR R4 ; save R4 on the stack
4828 2E4 ADD@ R4, R4 ; R4 = pointer into delta values
4829 2BD 008 MVII #8, R5 ; R5 = octave counter
482B 093 octave MOVR R2, R3 ; copy R2 to R3
482C 2B9 004 MVII #4, R1 ; R1 = note counter
482E 298 note MVI@ R3, R0 ; R0 = note period
482F 240 1F0 MVO R0, $1F0 ; save the low bits
4831 040 SWAP R0 ; and the high bits
4832 240 1F4 MVO R0, $1F4 ; into the PSG registers
4834 001 SDBD ; wait for ~184500 cycles
4835 2B8 00C 030 MVII #12300, R0 ; (approximately 185ms)
4838 010 spin DECR R0 ; (6 cycles)
4839 22C 002 BNEQ spin ; (9 cycles)
483B 2E3 ADD@ R4, R3 ; update R3
483C 33B 005 SUBI #5, R3
483E 011 DECR R1 ; decrement the note counter
483F 22C 012 BNEQ note ; loop if not zero
4841 37D 005 CMPI #5, R5 ; compare octave counter with 5
4843 20C 003 BNEQ phase ; time to switch the phase? ...
4845 09A MOVR R3, R2 ; ... yes: copy R3 to R2
4846 200 008 B next
4848 20E 002 phase BGT asc ; ascending phase?
484A 33A 018 SUBI #24, R2 ; descending phase: -12 semitones
484C 2FA 00C asc ADDI #12, R2 ; ascending phase: +12 semitones
484E 33C 004 SUBI #4, R4 ; rewind R4
4850 015 next DECR R5 ; decrement the octave counter
4851 22C 027 BNEQ octave ; loop if not zero
4853 2B4 PULR R4 ; advance to the next chord
4854 00C INCR R4
4855 220 033 B chord
;; -------------------------------------------------------- ;;
;; chords tables ;;
;; -------------------------------------------------------- ;;
4857 2F7 00F chords DECLE $2F7, add2 - $ - 1 ; C/add2
4859 2F4 014 DECLE $2F4, add2_m - $ - 1 ; Am/add2
485B 2F7 00B DECLE $2F7, add2 - $ - 1 ; C/add2
485D 2F4 010 DECLE $2F4, add2_m - $ - 1 ; Am/add2
485F 2F0 007 DECLE $2F0, add2 - $ - 1 ; F/add2
4861 2F2 005 DECLE $2F2, add2 - $ - 1 ; G/add2
4863 2F3 011 DECLE $2F3, maj7 - $ - 1 ; G#/maj7
4865 2F5 00F DECLE $2F5, maj7 - $ - 1 ; A#/maj7
4867 000 DECLE 0
; delta values, with +5 offset
4868 007 007 ... add2 DECLE 7, 7, 8, 10, 0, 2, 3
486F 007 006 ... add2_m DECLE 7, 6, 9, 10, 0, 1, 4
4876 009 008 ... maj7 DECLE 9, 8, 9, 6, 4, 1, 2
0x487D 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 either additions and shifts (see the previous revision) or just repeated additions (the current revision). Besides, we need 32-bit values to have enough precision. So we use either one or two pairs of 16-bit registers depending on the method (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.