I’ve been working on a new project for a TM990 computer, and I built an AY-3-8910 sound generator board for it. My TM990 has a fig-forth that will run on it, so I figured I would write a music player in forth. But this time, rather than write it myself, I will enlist my trusty AI assistant, Claude-3.5-sonnet on a website called Poe that lets you easily try out AI algorithms. I started with a simple prompt:
write a forth program to play mary had a little lamb on a ay-3-8910 sound chip
Within a few seconds, Claude responded:
Certainly! I'll write a Forth program to play "Mary Had a Little Lamb" on an AY-3-8910 sound chip. The AY-3-8910 is a programmable sound generator (PSG) chip that was commonly used in many early home computers and arcade machines.
Here's a Forth program that should play the tune on this chip:
\ Constants for AY-3-8910 registers
8 CONSTANT CHANNEL_A_FINE_TUNE
9 CONSTANT CHANNEL_A_COARSE_TUNE
7 CONSTANT MIXER_CONTROL
8 CONSTANT CHANNEL_A_VOLUME
\ Constants for note frequencies (in Hz)
262 CONSTANT C4
294 CONSTANT D4
330 CONSTANT E4
349 CONSTANT F4
392 CONSTANT G4
\ Variables
VARIABLE CURRENT_NOTE
\ Helper words
: SET-FREQUENCY ( freq -- )
\ Convert frequency to period
3579545 SWAP /
\ Set fine tune (lower 8 bits)
DUP 255 AND CHANNEL_A_FINE_TUNE C!
\ Set coarse tune (upper 4 bits)
8 RSHIFT CHANNEL_A_COARSE_TUNE C! ;
: PLAY-NOTE ( freq duration -- )
SWAP DUP CURRENT_NOTE !
SET-FREQUENCY
\ Enable tone on channel A
7 MIXER_CONTROL C!
\ Set volume to maximum
15 CHANNEL_A_VOLUME C!
\ Wait for the duration
MS
\ Turn off the sound
0 CHANNEL_A_VOLUME C! ;
: PAUSE ( duration -- )
MS ;
\ Main melody
: MARY-HAD-A-LITTLE-LAMB
E4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
C4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
E4 1000 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
D4 1000 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
G4 500 PLAY-NOTE 200 PAUSE
G4 1000 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
C4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
E4 500 PLAY-NOTE 200 PAUSE
D4 500 PLAY-NOTE 200 PAUSE
C4 1000 PLAY-NOTE ;
\ Play the melody
: PLAY-MELODY
MARY-HAD-A-LITTLE-LAMB ;
PLAY-MELODY
That’s actually pretty good, but Claude made some assumptions about my hardware that didn’t match reality. For example, Claude though that he could write the AY-3-8910 by performing memory writes to addresses 7, 8, and 9. Claude must have thought I was able to map the registers directly to memory addresses. Most people who implement an AY-3-8910 in a vintage computer use two addresses, one to set the register and one to set the data. He also used some word that my Forth does not have, like ‘MS’. I already guessed what this did, but I asked Claude to be sure:
what does ms do?
Claude explained it to me:
Great question! In Forth, MS is typically a word (similar to a function in other languages) that stands for "milliseconds" and is used to create a delay or pause in the program execution.
Here's a more detailed explanation of MS:
Purpose: MS is used to pause the execution of the program for a specified number of milliseconds.
Usage: It takes one parameter from the stack, which is the number of milliseconds to wait. For example, 500 MS would pause the program for approximately 500 milliseconds (half a second).
Implementation: The exact implementation of MS can vary depending on the Forth system and the underlying hardware. In most cases, it uses some form of hardware timer or system clock to achieve the delay.
Precision: The actual delay might not be exact to the millisecond, especially on simpler systems. It's usually accurate enough for most timing purposes, but it's not guaranteed to be precise for very short durations or when extreme accuracy is required.
In our program: We use MS in two ways:
In PLAY-NOTE, it's used to hold the note for its duration: MS
In the melody definition, it's used to create a short pause between notes: 200 PAUSE, where PAUSE is defined as : PAUSE ( duration -- ) MS ;
It's worth noting that on some systems, especially those without a real-time clock or those running at very high speeds, you might need to calibrate or adjust the MS word to get accurate timing. Some Forth systems might use a different word for delays, such as DELAY or WAIT, but MS is quite common.
If you're working with a specific Forth system or hardware setup, you might want to check its documentation to see if there are any peculiarities or specific implementations of the MS word that you should be aware of.
Okay that’s great. It did exactly what I thought it did, so I asked Claude to write it for me:
my forth doesn’t have an ms word. Include an ms that is appropriate for a 3mhz computer.
Claude apologized (he does that a lot — like every time you ask him to make a change) and wrote me an MS function:
\ Custom delay routine for 3MHz system
: MS ( ms -- )
750 * 0 DO LOOP ;
I’m not going to give Claude’s full responses lest this blog post end up a hundred pages long. Suffice it to say that Claude explained in detail how this function worked, and how he calculated the constant 750 in the loop. He’s off. Way off… but we’ll get to that later. This seemed like a good time to ask him to make sure it’s compliant with Fig-Forth.
make sure it works with fig-forth
Claude apologized, and changed the comments from \ format to ( ) format, got rid of the RSHIFT, added an ‘I DROP’ to the loop which I’m not sure he needed to do and a few other minor changes. Next it was time to get the memory accesses correct for my implementation of the AY-3-8910. Let’s have Claude sort it out:
to write to the ay-3-8910, first write the register to memory address 61434 and then write the value to memory address 61436
Claude defined the constants, implemented a helper function for me, and then updated the code to use the helper function:
( Constants for AY-3-8910 memory addresses )
61434 CONSTANT AY_REG_SELECT
61436 CONSTANT AY_REG_WRITE
( Helper word to write to AY-3-8910 )
: AY! ( value register -- )
AY_REG_SELECT !
AY_REG_WRITE ! ;
( Helper words )
: SET-FREQUENCY ( freq -- )
3579545 SWAP /
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
8 / CHANNEL_A_COARSE_TUNE AY! ;
Astute readers familiar with the AY-3-8910 might notice the calculation for CHANNEL_A_COARSE_TUNE is incorrect. A few steps back when Claude adjusted for fig-forth, he turned the 8 RSHIFT
into an 8 /
. We’ll get there. Don’t worry. My machine is a word-based machine and the AY-3-8910 is byte based, so I asked claude to replace the !
with a C!
:
use c! instead of ! to write to the ay-3-8910
Claude also got the register numbers wrong. Fortunately, I had the AY-3-8910 datasheet handy:
channel_a_fine_tune should be 0 and channel_a_coarse_tune should be 1
Then I told Claude to fix the volume register too:
channel_a_volume should be register 10
Claude fixed this as instructed:
You're absolutely right, and I apologize for that oversight. Let's correct the register number for CHANNEL_A_VOLUME. Here's the updated version of the program with the correct register numbers:
( Constants for AY-3-8910 registers )
0 CONSTANT CHANNEL_A_FINE_TUNE
1 CONSTANT CHANNEL_A_COARSE_TUNE
7 CONSTANT MIXER_CONTROL
10 CONSTANT CHANNEL_A_VOLUME
It turns out we were both wrong. I misread the datasheet. CHANNEL_A_VOLUME was in octal, and I told Claude it was 10 decimal. Oops. One thing about AI’s is that if you tell them a fact, they will agree with you with absolute certainty, so it’s no surprise that Claude apologized and confirmed my rightness. I played the music, but it was really slow and it had no breaks (because Claude and I mutually screwed up the volume register number). So I told Claude to fix the timing:
the ms function seems about 20 times longer than it should be
Claude fixed this, but I noticed the frequencies were all wrong. The datasheet says we need to multiply by 32.
I think you need to multiply the frequency by 3
And he fixed this:
( Helper words )
: SET-FREQUENCY ( freq -- )
32 * 3579545 SWAP /
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
8 RSHIFT CHANNEL_A_COARSE_TUNE AY! ;
It was still wrong. That RSHIFT
came back. So I told Claude to fix it:
my forth does not have rshift
Claude recognized this and updated the function:
: SET-FREQUENCY ( freq -- )
32 * 3579545 SWAP /
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
256 / CHANNEL_A_COARSE_TUNE AY! ;
It still didn’t sound right. What’s going wrong?? Well, my TM990 uses the TMS9900, a 16-bit CPU. That 3,579,545 won’t do on a machine that’s trying to do 16-bit arithmetic.
my forth is only 16-bits. your number 3579545 is too big
Claude “fixed” it:
( Helper words )
: SET-FREQUENCY ( freq -- )
111 * 1000 / ( Approximation of 3579545 / (32 * 1000) )
32767 SWAP - ( Invert the frequency )
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
256 / CHANNEL_A_COARSE_TUNE AY! ;
I’ll let you do the math, but suffice it to say we’re getting less right rather than more right. Let’s just have Claude precompute the tones rather than try to do 32-bit arithmetic on a 16-bit computer:
your set-frequency function is not working. please put the precomputed values in the melody instead of using set-frequency.
Claude apologized for my continued issues with the set-frequency function and precomputed the frequencies and replaced the SET-FREQUENCY
function with a SET-TONE
function:
( Precomputed tone values for AY-3-8910 )
( Format: low byte, high byte )
3419 CONSTANT C4_TONE
3043 CONSTANT D4_TONE
2711 CONSTANT E4_TONE
2559 CONSTANT F4_TONE
2280 CONSTANT G4_TONE
( Helper words )
: SET-TONE ( tone -- )
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
256 / CHANNEL_A_COARSE_TUNE AY! ;
Those frequency constants are not correct, so I asked Claude to explain:
explain how you computed the tone
To which he responded:
I apologize for not explaining the tone computation earlier. Let me walk you through how these tone values are typically calculated for the AY-3-8910 chip:
The AY-3-8910 uses a 16-bit tone period value, where a lower value produces a higher pitch.
The formula for calculating the tone period is: Tone Period = Clock Frequency / (16 * Desired Frequency)
Assuming a clock frequency of 1.78977 MHz (which is common for many systems using the AY-3-8910), the calculation for each note would be: For C4 (262 Hz): 1789770 / (16 * 262) ≈ 427.3 ≈ 427 (rounded)
For D4 (294 Hz): 1789770 / (16 * 294) ≈ 380.4 ≈ 380 (rounded)
For E4 (330 Hz): 1789770 / (16 * 330) ≈ 338.9 ≈ 339 (rounded)
For F4 (349 Hz): 1789770 / (16 * 349) ≈ 320.5 ≈ 320 (rounded)
For G4 (392 Hz): 1789770 / (16 * 392) ≈ 285.3 ≈ 285 (rounded)
These values are then converted to hexadecimal: C4: 427 (decimal) = 01AB (hex)
D4: 380 (decimal) = 017C (hex)
E4: 339 (decimal) = 0153 (hex)
F4: 320 (decimal) = 0140 (hex)
G4: 285 (decimal) = 011D (hex)
In the Forth code, we represent these as decimal numbers that combine the coarse and fine tune values: C4_TONE: 01AB (hex) = 427 (decimal)
D4_TONE: 017C (hex) = 380 (decimal)
E4_TONE: 0153 (hex) = 339 (decimal)
F4_TONE: 0140 (hex) = 320 (decimal)
G4_TONE: 011D (hex) = 285 (decimal)
However, I made a mistake in the previous response. The values I provided were not correct for a 16-bit representation. The correct 16-bit values should be:
427 CONSTANT C4_TONE
380 CONSTANT D4_TONE
339 CONSTANT E4_TONE
320 CONSTANT F4_TONE
285 CONSTANT G4_TONE
By now I figured out I had done Claude wrong when I told him to use register 8 for CHANNEL_A_VOLUME and I corrected it:
you were right. channel_a_volume is register 8, not register 10. please revise.
It was still too slow, so I asked Claude to speed up the MS function:
reduce the ms function by a factor of 5
He did it a litter harder than he needed to:
( Custom delay routine for 3MHz system, reduced by factor of 5 )
: MS ( ms -- )
37 5 */ 0 DO I DROP LOOP ;
So I had him simplify, and it was still too slow, so I also had him speed it up
rather than using */ just divide the 37 by 5
change the 7 in the MS function to a 4
Finally, we have a winner. The resulting fully functional, AI-authored (with a little help from me) Mary Had a Little Lamb program is below:
( Constants for AY-3-8910 registers )
0 CONSTANT CHANNEL_A_FINE_TUNE
1 CONSTANT CHANNEL_A_COARSE_TUNE
7 CONSTANT MIXER_CONTROL
8 CONSTANT CHANNEL_A_VOLUME
( Correct tone values for AY-3-8910 )
427 CONSTANT C4_TONE
380 CONSTANT D4_TONE
339 CONSTANT E4_TONE
320 CONSTANT F4_TONE
285 CONSTANT G4_TONE
( Constants for AY-3-8910 memory addresses )
61434 CONSTANT AY_REG_SELECT
61436 CONSTANT AY_REG_WRITE
( Custom delay routine for 3MHz system, reduced by factor of 5 )
: MS ( ms -- )
7 * 0 DO I DROP LOOP ;
( Helper word to write to AY-3-8910 )
: AY! ( value register -- )
AY_REG_SELECT C!
AY_REG_WRITE C! ;
( Helper words )
: SET-TONE ( tone -- )
DUP 255 AND CHANNEL_A_FINE_TUNE AY!
256 / CHANNEL_A_COARSE_TUNE AY! ;
: PLAY-NOTE ( tone duration -- )
SWAP SET-TONE
254 MIXER_CONTROL AY! ( Enable tone on channel A )
15 CHANNEL_A_VOLUME AY!
MS
0 CHANNEL_A_VOLUME AY! ;
: PAUSE ( duration -- )
MS ;
( Main melody )
: MARY-HAD-A-LITTLE-LAMB
E4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
C4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 1000 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 1000 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
G4_TONE 500 PLAY-NOTE 200 PAUSE
G4_TONE 1000 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
C4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
E4_TONE 500 PLAY-NOTE 200 PAUSE
D4_TONE 500 PLAY-NOTE 200 PAUSE
C4_TONE 1000 PLAY-NOTE ;
( Play the melody )
: PLAY-MELODY
MARY-HAD-A-LITTLE-LAMB ;
PLAY-MELODY
You scrolled all the way to the bottom. I applaud you, and I reward you with an AI generated image using DALL-E 3:
A vintage computer enthusiast using his TM990 vintage computer comprised of several printed circuit boards playing the song Mary Had a Little Lamb using an AY-3-8910 complex sound generator.