It's been way too long since I've done any retro computing. So, let's remedy that by making a very old computer do something it was never designed to. So, what you're looking at here is an emulated Commodore PET 4032. It looks like this. The first version of the PET
came out in 1977, making it one of the very first of the ubiquitous home microcomputers. It was extremely expensive, extremely underpowered, and made out of filing cabinets. Yes, really. It's powered by a 1 MHz 6502. This one here has 32K of RAM. It's got a disc system, which is twice as powerful as the computer itself, and it was intended for programming in basic.
So I can do like so it's got no graphics and no color. It can display text only in one color which is green. It does have a wide variety of graphics characters as well as the ability to display text in lowerase. But you can't display the graphics characters and the lowerase text at the same time. you have to pick
one or the other. The machine is currently set up in capital letters and graphics characters mode. However, I'm not interested in any of that. I'm interested in what happens if I reset the machine. It plays sound. This machine actually has a speaker. Here's a schematic of how the speaker works. It's
really, really simple. There's a GPIO attached to one of the IO chips. This is simply rooted through a speaker and that's it. That GPIO can be set to on or off. The way it plays tones is by
oscillating the state of the speaker rapidly producing a square wave thus producing you know sound. I can demonstrate that if I write a little program and trying to remember what the numbers are I have to use for this. Oh yeah. 236 and 468 comma 204 go to 10 and I run it. You get an unpleasant buzzy sound that's producing a fairly coarse square wave and is also rather irregular. I'm going to press some keys on the keyboard. If you listen to what's
happens. So what's happening is when I press keys, the computer is taking time off to process the interrupts which are generated as I press the keys. This takes a varying amount of time depending how many keys are pressed which causes the timing of the square wave loop to go all weird. If I insert some deliberate delays like there, the pitch goes down. Unfortunately, basic is so slow that this approach here can't really produce sounds above maybe 50 to 100 hertz. To do better than that, we have to go to machine code. Here is a simple stub boilerplate
program I wrote. This allows me to actually write machine code and deploy it onto the machine. So I assemble it using the be assembler. I go to my emulator and I do dload.prug. This loads it off an
emulator disc. The program contains a little basic stub at the top that then runs the machine code that forms the rest of the program. So here's the basic stub. Here's the machine code. A simple RTS. If I run it, nothing happens. So if we translate this into machine code, it looks like 236 SDA 594684 SDA 59468. Jump
entry. Save. Assemble. and run. It actually makes a fairly nasty buzzing sound. This is because while basic is too slow, machine code is way too fast. This is actually generating a supersonic frequency square wave that the pet is unable to reproduce. Also, we still have
interrupts on, so pressing keys does weird things. So, if I reset this, and now I put some delays in to slow it down to a reasonable speed. Let's say, so we're using the X register. Put
128 into it. Decrement. If it's not zero, loop and put another one there. Uh, let's change this assemble. Well, that is a really horrible noise. And once again press keys. That is not what we really want to play sound. The problem is that the
interrupts are messing up everything. So in fact if we just put an SEI at the top of the program that will disable interrupts entirely. So assemble and we now get a nice pure sinewave [Music] tone which we can only stop by resetting the machine. We can vary the tone by changing the length of these delay loops. So let's actually factor these out a bit.
define a routine called weight. Let's change this to 200 loop loop.ts. So now we can just do JSR weight JSR weight like so. And what this does
is produces a lower pitch. And this is how that startup sound is produced. It simply toggles the speaker bit rapidly in various different patterns and produces those tones. But I
want to do something else. And for that, first I'm going to have to explain. So a word of warning, the sketching you're about to see is going to be really bad. I am drawing a soundwave. In the real world, this is
air molecules slloshing backwards and forwards. There we go. So beautiful. Computers deal with this by using a microphone to turn the motion of the air molecules into a voltage and then they sample the voltage at a regular interval and record the voltage. They can then reproduce the analog wave by sending these samples to a DAC which reproduces the voltage which goes to a speaker which reproduces the motion of the sound waves. Now the PET hardware while it can
produce sound it cannot reproduce particular voltages. The speaker can either be on or off. So, one thing we could do is to very crudely map our analog wave to a square wave and reproduce that signal instead. Unfortunately, while this would sort of work, the resulting sound quality would be terrible because the square wave being either on or off cannot replicate the variable amplitude of the real soundwave. So here, for example, the
amplitude is considerably lower than the square wave itself. So while it will reproduce the frequency, the shape of the resulting audio wave will be all wrong. So, what I'm going to do instead is something called pulse density modulation. And here is a much better diagram drawn by somebody else. Here is our audio wave. Here is a dizzard square wave that produces a approximation to the audio wave. When the amplitude is
low, the square wave is mostly off. When the amplitude is high, the square wave is mostly on. When it is in between, the square wave turns on and off rapidly, thus averaging out a value that is halfway in between on and off. This is actually a well-known way of reproducing sound, and a lot of common hardware does it, but usually a much higher frequencies than our sad little Commodore pet. So, going back to the
code here, I have a raw sound sample. This is an array of 32-bit floats. And here I have a converter that someone else has written which so I don't have to understand the maths which uses a thing called a delta sigma encoder to turn that 32bit soundwave into a bit stream which is this. So hopefully in order to play our
sound, all we have to do is to go through this one bit at a time, setting the speaker state every 18,000th of a second cuz that's the sample rate used by this. So let's give that a try. Now, because this does need to be fairly time exact, I'm actually going to use one of the PET system timers, which is implemented by the same 6522 that is actually handling our GPIO. So, the the PET clock rate is 1 MHz. Our sample rate is 8 kHz. Therefore, The number of ticks per sample is very simply one divided by the other. So
we're going to use the system timer to count this many ticks and we're going to use that as the master clock controlling the playback. So I need some boiler plate. I'll just go and do that. So I've defined a bunch of constants for easy access to the 6522's registers. So now we need to initialize the timer. the
timer counts down for a from a particular tick value to zero and every time it hits zero it will generate an interrupt. So we need to program in the number of ticks which is not that it's that. And we're storing it this into t1 l pi like so. The L here means latch. This is the storage value for the tick count. The C up here means counter. This is the value that actually counts down. And we
also want to configure the timer via the ACR register which is documented here. And it contains a bunch of settings for you know how the 6522 operates. So the top two bits here 7 and six are timer control. We want to set it for continuous interrupts. Timer two is we're not using
it. The shift register, we're not using it. PB and PA, we're not using those either. So, all we need to do is to set the value to 01 O, which is zero. Okay. And it's also polite to
write to the counter register as that will set the timer running. It should be running anyway, but that's, you know, the slightly better way to do it. So, now we have a program that initializes the timer. Does it does it assemble? It does not. Okay.
And this should make it do exactly the same thing it did last time. There we go. So the next thing we have our wait routine here. This just counts down until a certain number of cycles have passed. Instead we want to change this
so that it waits until the timer fires. So what we do here is we read the interrupt flag register of the 6522. This register which is defined somewhere somewhere here tells the computer which subsystem of the 6522 is currently signaling that something's happened. In our case, we care about timer one timing out. This flag will be set every time the timer value counts down below zero. So we need to check to see whether the appropriate bit 6 is set. If it's not set, just loop
round and keep waiting. Once it's set, we need to tell the 6522 that we've noticed and it can reset the flag, which we do by doing one of these things. So we want to read T1 C low. T1C low. There we go. And that is all we need. So up here in
our sound program, we want to put a label there and change that. And so now every time we go around the loop, it will spend 1 8,000th of a second with the speaker in one state and then 18,000 of a second in the other state. So let's see if that works. It assembles. What does it do? Nothing. So, this could actually be a good thing. This is the emulator's debugger,
which we're going to use to find out what's actually going on. I use the IO command. This dumps the state of the various hardware peripherals. So, here we can see the via the 6522. Here is the counter value. Here is the latch value. One doesn't seem right. I mean 1 million divided by 8,000 is 125 probably. So what's it actually
doing? It is spinning waiting for [Music] the timer to fire. This is not actually counting. Okay. Have I done something wrong? I had a quick look at the documentation. It looks all right. So let's stick a break point at the beginning of our program. Reload it. Run it. And let's actually step
through and see what actually happens. So interrupts off. 7D is the value I was expecting. 125. Ah, I was actually looking at the wrong register here. These do something
else entirely. These are the timer registers. You can see it's written 70 there. That should set the other registers. And now we can see that this is counting down. Wait a minute. Why is it overwritten that value? We program the latch value to 70 0 0. We clear the
counter and that has in fact reset these two. I don't think I'm allowed to set the counter. Let me look at the documentation. Yes, writing to the counter writes to the latches. So what I
was actually trying to do was to set the latch value and then reset the timer. However, if I just write to the latch, then writing to the latch happens. If instead I write to the counter registers, then it will update the latch. And if I write to the low register, it will reset the timer. In fact, if I write to either, it
will reset the timer. So I want to write to the high one last cuz that causes both latches to be transferred to the counter and initiates countdown. So I just need to change this to this and that should be all we need. Okay. Step step step right we should have initialized there we go latch value of 7D counter value of 7D step once this is counting down so let's just go and what we should have there is a 4 kohz tone because this loop here is taking exactly two ticks per oscillation. Excellent. That is working. All right. Now, we want to actually write
our player. Now, our bitstream is in the file called sound. A2 stream. So, that goes there. Good. That works. Our program now has got quite a lot bigger to very nearly 7k most of which is the sound sample. So we want some variables in zero page. We need a
pointer to the sample currently being played. And because that sample bite contains eight bits and we have to latch these out one at a time, we also want to be able to store that value. Okay. Initialize by doing [Applause] sample. Actually, we don't need to
initialize current sample. So, our loop here is going to Load the current sample. Mask off all but the bottom bit. Convert that to either EC or AC. These are the on and off commands we need to write to PCR to change the speaker state and send that to PCR. Next, we want to rotate the current sample right by one. Uh, we also need a
counter. This is the number of bits we've processed. So, we want to process eight bits. And then we want to advance the pointer and load the next sample.
So we load that with eight. And in fact we are going to initialize current sample. That's wrong. Sorry that should be pointing at our data. That should be binary data. Going to load the first value and put that in current sample. So
when the loop enters we actually have valid data. So rotate write the current sample decrement the bit counter if it is not zero then jump back to the top of the loop. And at the top of the loop we actually want to do a JSR weight. Okay, so this will clock out eight bits and then we fall off the end of the loop here. This point we need to advance the pointer and reload current sample and reset bit counter. In fact, the pointer is post increment. So the pointer is
always pointing at the next value. So we just want to load the next value which we do uh current sample sample pointer stash that in current sample reset the bit counter and then we want to increment our 16 bit pointer. So if if we increment the low bite and the result is not zero then we're done.
If the result is zero then we clearly want to increment the high bite and I think that's all we need. Let's see what it does. So load program. It's much bigger now. And run. Oh yeah, there's a break point. Go. [Music] [Music] [Music] Heat. Heat.
[Applause] Well, that was interesting. It wasn't right, but it was interesting. Okay, let's step through it and see what happens.
So we initialize our timer here. We're initializing the binary data. So the address is four. Wait a
minute. I see what I've [Music] done. That should be + one. That should be zero. So in fact the pointer was pointing at just random garbage in memory. Okay. [Applause] Go. So I think it just tried to play
something and failed miserably. So here I'm clocking the bits out right to left. So the lowest bit goes first. And now I'm wondering if my converter doesn't actually work like that. So let's try clocking them out
left to right. The way we do that is we roll the sample left. In fact, we can just do that here. Roll left. The top bit goes into
the carry flag. Then we roll a. That top bit then ends up in A. And now we can stash it in X and go again. The rest of it doesn't need modifying. So what will this [Applause] do? That has not worked either. In fact, I think that sounded worse than it did before. So let's put that back the way
it was. And we can actually simplify this code. Like so. So now I'm wondering if the converter is actually doing the right thing. Let me just go and investigate that off camera a bit, shall I? I wrote a quick program to take the bitstream output, turn it back into a sound sample, and here it is in Audacity. And if I play it, you
[Applause] get which sounds familiar. And if I play back the pet version again and [Applause] go. So, in fact, it's working fine. It's just playing back garbage because I told it to play back garbage. Okay. It's possible that this converter doesn't really like operating at 8 kHz. I think it may want a much higher sample rate.
So, let me try reconverting into 16 kHz and see if that makes a difference. I did a bit of hunting around for delta sigma converters and I found a thing known as a second order delta sigma converter that does a better job than the first order one that this converter was using. And the results are well and hear for yourself.
Yes, it's supposed to be the Windows 95 startup sound, but it has clearly lost something in the conversion. Now, that's at 16 kz. So the sample rate of 16 kHz, the maximum frequency that it can actually generate is 8 kHz. That is one bit on, one bit off. And the way these converters work is they will produce a lot of noise at that frequency. So clearly we want to get the sample rate as high as possible. However, we're
running out of two things. The first is memory. We only have 32k to deal with. The other is time. This takes quite a while to do. And at 16 kHz, we only have
what, 60 cycles between samples. Luckily, the 6522 has us covered here. It's got this thing called a shift register. You can
give it a bite of data and it will clock it out to a particular pin all by itself. And all you have to do is to keep reloading the register when it runs out of data. Here is the shift register documentation. Now, this is not configurable. So, the output is only
ever a single pin which you can't change. Luckily, that pin is CB2, which is exactly the one we want. And in fact, this is how the PET normally does tone generation. It sets a value in the shift register. It sets a clock speed in timer
2, and it just keeps shifting out the same 8 bit pattern over and over again. So, we should be able to use this to make a much faster and easier sample player. So, I did a bit of reading up on how the shift register works, and it's a bit weird. The shift register does use timer 2, but in a peculiar mode. Timer
two doesn't work the same way as timer one. So it doesn't have the free running mode, but you can make it just run continuously ignoring the latch value except the latch value is used by the shift register code to determine the speed at which it shifts bits. So anyway, we do this. We want to configure the shift register itself. We want to run in shift register output free running mode. So the control value is 10 which goes into these three bits. Uh so we do this. We don't care
about timer one anymore. And I think that's all we need. Our render code is now much simpler than it was. We just need the sample pointer. We don't need the bit counter. We don't need current sample, etc. So, all we really need to do is uh actually let's make a few changes here. Our wait code wants to wait for
the shift register empty interrupt which is this one bit two. So that is a 04. So at this point the shift register is now empty. We need to load it rapidly because the timer is still running. If we don't reload the shift register by the time the next tick comes along, then we lose a bit. So, we
load our sample and we write it to the shift register. So, now that's done, we actually have a few cycles to spare. So, what we're going to do is simply increment the sample pointer. And we
don't need this anymore. And I didn't put in a via SR. So, SR is E84A. Via SR. E84A. Okay. Let's see what this does. I
didn't put T2 in either. That's E848. That's interesting. Didn't this code tell me that the the contents of the low order T2 latch? So, where is the low order T2 latch? There isn't a register for that. Oh, of course, it will be accessed by writing to the counter register exactly the same way that timer one worked. So, so T2 control low is E848 high, which we're never going to use, is E849. So, change that to
that. It [Applause] assembles and does nothing. Well, I think this is T2. Yep, it is counting down. And we are currently stalled at 420. So yes, we are waiting for a interrupt. So, if I
put a break point at 427 and go, okay, we're not getting shift register interrupts. I wonder if we need to turn those on somehow. I do know what the problem is. The problem is that the shift register only actually starts operating when you write to the shift register. But the first thing we're doing is waiting for a shift register interrupt. But we won't get a shift register interrupt because we haven't written to the shift register. So we just move the uh no let's actually let's not do that.
Let's just write a arbitrary value to the shift register to kick off the process. So run and nothing happens. Okay, we are yeah we are waiting for the shift register. There is a setting in the PCR
under CB2 control. This is the thing we were using to actually set the speaker setting low output or high output which is pulse output which I think we may have to set. So let's do that. And the values we were setting down here were CC or EC. So we want 101 and CB1 control was zero. 101 which is A and the low nibble wants to be a C. Okay, let's try that.
And we've hit our break point, but that doesn't mean anything because the break point has actually moved. So, delete breakpoint one. This is put the new breakpoint at 431. Go. Nope. Hasn't
helped. Oh, for heaven's sake. So, in free running mode, the interrupt is never set. That's why I'm not getting an interrupt. So, this just isn't going to work. Going to have to do something
else. Well, I could use shift out under T2 control. With this, you do get the interrupt. So, you see ifr2 is set after
eight shift pulses. The only problem is that as far as I can tell, shifting is initialized by writing to the SR register. So that after my eight shift pulses, there will then be a gap while I think about things and then when I write to SR again, it will shift another eight bits. So that you won't get seamless
output without you know a lot of careful cycle counting which is annoying. But I can do something else. I can use both timers together. So we use timer two to control the shift register and we use timer one to decide when to reload the shift register. So we set up both timers and we want to set ACR So that timer one is producing continuous interrupts. So that's a four bit as well as the one bit. That's a
five. And then down here we actually want to wait for timer one to roll over which will happen every 8 ticks at which point we reload SR. And because we're using timer one here, I want to clear the interrupt. Read T1C low. There we
go. So, what does this do? Oops. Something wrong with the keyboard there. Heat. Heat. [Music] Well, that was exciting but wrong. But I
think I know what's happened here. If we go down and look at the SR documentation, wherever it's got to, here we go. When shifting out, bit seven is the first bit out. So, in fact, this is sending data in the other direction than my converter app has produced it. So, I'm going to have to change that. So, here is the bit where it
actually sets the bit. So, what I actually want is to change this to into a shift right by n mod bits. Socc converter converter sound assemble and That has also not worked. Why is this
not working? I mean, it is very clearly playing sound. It is a sample of some description. It's just not the right one. Here is the program. It's a tiny
amount of machine code followed by all the sample data. Now, one possibility is that my timers here are not matched up properly. This is hopefully outputting one tick every ticks. I wonder if it's like two times. Let's change this to I don't think this is right. I'm just going to see what happens. I heard window sounds there. Okay, my suspicion here is that
my two timers are out of sync. So, we're actually resetting this here. But then it's a fair number of cycles until we go here. So I think that we need to rekick the timer with the appropriate value. So the timer is in freeun mode. Where am I? Here we go. So that I can reset the timer by
writing to the high counter register. So, if I were to simply move this code down here, that should reset the timer. And I don't need this anymore. However, it's a little bit more complicated than that.
So, if we make slow kit tick equals tick time 8. So, this now becomes low slow tick. Like so. We can then change this to slow six as well. The thing is we write to SR. SR starts shifting. We then spend a certain
number of cycles doing this and a certain number of cycles doing this. So slow ticks here that should be an eight is actually tick time 8 minus a certain number of cycles. So it's a LDA constant which is this immediate is two cycles followed by a STA to absolute address which is four cycles. So I think that's going to be tick * 8 - 6. Okay. So, I think I can actually hear some window startup sound in there, but it's way noisier than it was doing it the other way. So, something's not
right. Let's do some single stepping and see how things work out. So here is our program and 439 is where [Applause] we is where we're actually setting the timer. So go
run the state of the timers are this is of course free running this is counting down from 01 dB step okay write to SR right to the high bite of timer one timer one has been reset to 01 E this is the shift register value and we go around again. Unfortunately, it looks like the shift register value rolls around and I'm not even sure it's actually shown here. I think it may be a special internal value that you're seeing. I found a different data sheet for the 6522 which actually goes into more detail about how the shift register modes work. So I think I can actually
use the singleshot shift register mode. Shift out under T2 control. There's a bit here that says that the shifting operation is triggered by the read or write of the shift register if the SR flag is set in the IFR. Otherwise, the
first shift will occur at the next timeout of T2 after a read or write of the SR. So we do have control of what's going on there. Okay. So, let's get rid of this timer one stuff and go back to our old code. Actually, let's just hit U for a while. Like, so we want to change this because we want to run the shift register in mode five rather than mode four. So, that's a four there. So here we are waiting for the shift register to finish shifting. Then and this is the important
bit. We want to clear the SR flag in the shift register which is interrupt flag register cleared by read or write shift reg. But there's the other way of clearing the flags. The interrupt flag methods may be read directly. Individual flag bits may be cleared by writing a one. So we clear that bit. Now we can write
to SR here and T2 will continue to run and after this happens the next bit will be clocked out on schedule according to T2 rather than being synchronized to this right here. So let's try that. Uh de log approve. Okay, that actually worked. But there's something [Music] wrong. I think it's running at half
speed. So, let's change this to tick / two. [Applause] Okay, that worked. So now we're using
the shift register. We can actually crank up the clock rate. So let's change this to 2205. And I'll write out my tracks resample 2205. and we'll see what it sounds like at 22.05
[Music] kHz. It's better. Can we go higher? Well, the critical factor is the number of cycles between here and here. So this is and with immediate is two cycles. BQ not taken is two cycles. LDA immediate is two cycles. Memory write is
four. This is two. This is uh indirect comma y is five and this will also be four. So 9 11 15 17 19 [Music] 21. So 1 E6 divided by 21 gives us about that many hertz. So, in fact, we should be able to crank this way up if we had enough RAM, which we don't. So, we are currently at
18K. I think we can go to so export at 32 kz convert and our program is now of 26k and [Music] Oh, although it would help if I'd remembered to change this 52,000 and [Music] [Music] Oh, it is painfully quiet, but that is because this sound here is quite quiet. So, let's just normalize. And it only got a little louder. Let's
just let's just crank the volume up a fair bit. Allow clipping. We go a bit louder than that. I mean, it's going to sound like suck, but it's going to anyway. And go. [Music] Yeah, that didn't work very well. Let's just put that back the way it was. Yeah. Uh I suspect that the delta
sigma encoding really doesn't like clipping because it'll treat that as huge error values and everything goes horribly wrong. So aggressive normalization I think is as good as we can get. But it does actually seem to be working. Okay, let's call this the final
[Music] version. Yeah, that is pretty terrible. And that funny noise is it's playing all the rest of memory cuz I never put in any code to stop playing. It'll wrap around that has just played the sample. Again, the reason why it's so quiet is because a square wave is of course the maximum possible volume.
So simulating a lower volume with that square wave is going to produce a lower volume. It may be possible to get higher quality, but we probably have to use a shorter sample because we don't have very much RAM. I bet that this loop can actually be made faster. There needs to be enough time here to reload SR before the next bit needs to be clocked out. We figured out earlier that that could be 47 kHz. So I reckon that we could
probably play air quotes CD quality on air quotes sound. However, the conversion to one bit format is going to lose massive amounts of quality. If you're going to use delta sigma encoding for CD quality sound, you actually need a clock rate of I think 3 GHz. I saw somewhere. So I don't think that's going
to work. Okay, so I had to try it. I optimized the hell out of the replay loop. The critical section is now 14 cycles. That allowed me to push the clock rate all the way up to 60 kHz. And as a result, it now sounds like this. And in fact, the sample is so big
that it doesn't fit in memory and overwrites the screen. There you go. But the program still runs. So listen and enjoy. So it's looping through memory in only a few seconds because the clock rate is so high. And it is super quiet because the the onoff bits of the bitstream are so close together that the simulated speaker cone doesn't actually get a chance to move very far. But it does work and it doesn't sound bad. So I
think there you have it. Real CD quality sound coming out of an unmodified simulated Commodore pet. Sort of. The other thing you can do, of course, is to synthesize sounds on the fly, and I do have a few ideas about doing that, but I'm not going to do it in this video. I think that's enough.
I'll stick this program up somewhere. See the video description for details. If you ever want to play your own Windows 95 startup sound on a Commodore PET, and I would be very interested to know if this actually worked on a real pet.
Anyway, I hope you've enjoyed this rather rambly and unstructured tour through the innards of the pet sound system and trying to make it do things it was never intended to do. It's something I've always been interested in and I'm glad I finally got round to giving it a go. As always, I hope you enjoyed this video. Please let
me know what you think in the comments and I'll see you all next time.
2025-05-01 23:13