Wouldn't you know it, but it came to mind the other day when I was looking at a recent haul of Convergent WorkSlate stuff I've got to catalogue. Officially the WorkSlate's only means of telecommunications is its 300 baud internal modem. While we have a 9600bps way of wiring up a Workslate to a modern computer, it's always nice to have a simpler alternative, and I figured this would be a great challenge to see if John's old program could let my Commodore SX-64 talk to my WorkSlate. Spoiler alert: it works!
I don't know precisely what happened to John; regrettably I know little of his personal history. For a period of time he was a very prolific poster on comp.sys.cbm, but his last post there was July 19, 2000, in which he replied to someone's question about the relationship between sound frequencies and SID register values. (We'll actually talk about this in a bit.) His last post I can find in any Commodore newsgroup of the era is dated the next day, July 20, though he posted through CompuServe and it's possible may have made later posts there. Among his many contributions, including this one, are the Spyne self-extracting file archive utility, a .d64 downloader patch for Common Sense, an in-place PETSCII to ASCII text file converter, a user port-based audio A/D converter and player, and a custom track-by-track floppy disk formatting tool. He issued them all freely for anyone to use for any purpose. While he and I briefly corresponded over snail mail, I can't find the letter he sent me and I don't remember his exact location, and I never heard from him again. Sadly, although I hope I'm wrong, from his handwriting I knew he wasn't a young man and I'm all but certain he has since passed away. Rootsweb lists a John J. Iannetta who died in April 2001 at the age of 82. (If you know for sure, post in the comments, or E-mail me privately at ckaiser at floodgap dawt com.)
The "35-cent modem" was first posted on October 27, 1998. John estimated the 35 cent cost based on the then-purchase price of an RCA jack ("listed in my Jameco catalog at 35 cents each"), though this didn't include the phone cable, or about 68 cents in 2025 dollars. Looking in their online catalogue now, you could even go cheaper, since Jameco (not affiliated, not sponsored, just using for comparison) now sells a through-hole right-angle RCA jack for $0.29 — in 1998 dollars, that would have been a mere 15 cents. If you don't have a landline phone cable anymore, the lowest Jameco price for a compatible connector I could find was a 6P6C modular cable for $1.49. Such a cable is technically a RJ-25, but it or an RJ-14 (6P4C) will do just fine. This project is a very easy build job, so let's do it.
The key notion is that we're replacing the telephone tip and ring connection (carried on the innermost centre conductors of the RJ-11 cable) with an RCA phono jack that we can directly connect to the 64's audio line output. We last talked about tip-and-ring when we discussed how T1 lines work. On old exchanges like this one (used in rural New South Wales, Australia), phone lines were carried by literal tip-and-ring connectors plugged into the switchboard to connect calls, which is where the name comes from. Telco guys call a combination of tip and ring a "pair." Each phone line uses one pair.Although it likely makes little difference for this application, there is a polarity to the connection which should be observed, i.e., tip is positive and ring is negative. The cable I used is a real USOC RJ-11C with a 6P2C connector and thus has only two wires for a single pair. The tip wire for this first pair on typical North American RJ-11 installations can be green, or white with a blue stripe (in other countries it may be any number of other colours); the ring wire can be red, blue, or blue with a white stripe. Thus, after stripping back the wires — sometimes easier said than done on a sticky old cable — connect ring to the phono jack's ground/sheath and tip to the phono jack's centre. Make it pretty and you're done.
An important warning before we continue: from the telephone company side the line pair carries voltage used to power the phone and ringer, so never plug this cable into a wall jack — doing so could potentially send up to 48 volts to the computer, with likely undesirable and even fiery results. A cable like this should only ever be directly connected to another modem.
The software part has to do with how data from the Commodore 64 is modulated to send to the other system's modem. For that, we turn to John's program, as he posted it (in separate versions for NTSC and PAL Commodores for reasons I'll explain as we analyse the disassembly). It was presented as a type-in program in BASIC, short enough to type in by hand, with an embedded machine language section loaded from DATA statements. Here's a couple videos showing what it looked like in practice. The modulated audio is played through the speaker, so don't have it up too high.
After the machine code is transferred to memory, a continuous tone starts playing and the program enters a basic terminal mode. As we type on the keyboard, we hear the tone "warble" as each bit of the character's ASCII value is modulated and played to the other end.
If you press F1, it will then give you the opportunity to send a file. This file is sent using Xmodem-CRC so the remote side can reliably detect errors, but since the transmission is one-direction the 64 side simply pretends everything succeeds, pauses briefly to let the other end send its acknowledge byte, and then goes onto the next block. You can hear the file itself being modulated in the audio also; around 1'10" the last block is sent, which is padded with ^Z and thus has a characteristic repeated sound (plus a single "blip" after it for the end-of-text character ^D). When the transmission is complete, the program ends.
The modulation scheme itself is very simple, and the phone dweebs reading this have already guessed what it is just from the audio.
Recall from earlier discussions, such as when we wrote a bitbanged serial transmission routine so the KIM-1 could speak through a DECtalk, that the transmission of a byte or character of data is divided into more or less distinct phases. The default state is mark (one). At the beginning of transmission comes a start bit (always space, or zero), followed by the data (seven or eight bits). After the data comes an optional parity bit, then back to the stop bit for at least one and sometimes two or more bit times. The most common transmission type is 8N1, which is eight data bits, no parity bit, and one stop bit (i.e., characters must be separated by no less than one stop bit, though it can be more). If we load the audio up into an acoustic analysis program like Praat, we see a sine wave of varying wavelength. In the spectrogram at the bottom we can pick out two distinct frequencies being used to encode a character, as shown on the dark black band. This is the hallmark of audio frequency-shift keying (AFSK), or often just called FSK.For this spectrogram I've typed the letter "U" which in binary is 01010101. That's the "wiggle" in the middle. John's program sends 8-N-1, so since we know the byte is framed by stop bits, which are marks/ones, we can deduce the initial frequency is used to transmit a one. Serial communications send the bits in little endian order, i.e., from least significant to most significant, meaning the wiggle is actually the start bit (space/zero), followed by 10, 10, 10, 10, then a stop bit (mark/one) and finally the normal mark state between bytes, which in this plot is indistinguishable from the stop bit.
By zooming in on the spectrogram we can quantify the frequencies more precisely. Eyeballing the centre of the black band during the stop bit, we get ~2226Hz. Similarly, the centre of the black band during the start bit looks around 2054Hz. In fact, the frequencies are officially 2225Hz and 2025Hz, exactly 200 cycles apart, allowing easy distinguishability on an acoustic coupler or analogue phone lines of even poor quality. (Note that the band's thickness in these images can vary based on the analysis time slice used to compute the spectrogram.) These two frequencies represent the answer side of a Bell 101 or 103-compatible modulation scheme.The Bell 101 was the first commercial modem (then called a "data set"), developed by AT&T in 1958 for the U.S. military's Semi-Automatic Ground Environment (SAGE) environment. SAGE integrated data from multiple radar sites into a common wide-area view used as part of NORAD's nuclear response capability and was at least in part the inspiration for Dr. Strangelove. It remained in operation well into the 1980s; even as just a giant coincidence there are many suspicious similarities between the concept and WarGames' WOPR. The 101 ran at 110 baud over regular telephone lines and became available for commercial sale in 1959. On the wire it uses separate sets of frequencies for each side of the conversation: 1070Hz and 1270Hz (space, mark) for the modem originating the call, and 2025Hz and 2225Hz (space, mark) for the modem answering the call.
In 1962 AT&T introduced the Bell 103, which used the same frequencies but ran over twice as fast at 300 baud. It quickly became very popular and almost completely replaced the 101 in commercial use. Even after the 1976 Bell 202 introduced 1200 baud operation (with different frequencies and duplex modes), it remained compatible with the 103 in 300 baud mode, and virtually every third-party 300 baud modem was compatible as well (many were also compatible with ITU-T V.21, which uses the same basic communication scheme but different frequency sets). The Originate-Answer switch on 300 baud modems like the Commodore 1600 VICMODEM and Commodore 1660 "Modem/300" selects which two frequencies the modem will send bits with, using the other two frequencies for receiving. Since any 300bps modem can speak Bell 103, that's why John chose it, and since we're "responding" to the other side that "initiated" the "call" this code uses answerer frequencies.
The software came in both NTSC and PAL versions because obviously something like this is highly timing-dependent, and PAL Commodore 64s run slightly slower (0.985250MHz) than NTSC systems (1.022730MHz). The variance is because each video standard uses a different master crystal from which all other clocks are obtained by dividing down, including the colourburst frequency needed for correct display, and also the clock speed of the CPU. This speed additionally affects the 6581 SID sound chip, since each of its three oscillators are incremented by the given audio value (0-65535) every clock cycle, so we need different values for the mark and space frequencies on PAL and NTSC systems. John's last known post in comp.sys.cbm, using slightly different processor speeds, explains the math (shown as written):
It might be a good idea if you ask specific questions, since we don't know what info you already have. Regarding the low/high values for frequency (for registers 0/1, 7/8, and 14/15), together they defines a 16-bit word: Word = low_value + 256 * high_value The frequency of the tone is proportional to that value: Frequency = Word * F / 2^24 where F = system clock frequwency (1.022727 MHz for NTSC and 0.985249 MHz for PAL). So we get: Frequency = Word / A where A = 16.4044 for NTSC and 17.0284 for PAL.
Using this relationship, we can then solve for "word" to get the proper value for the SID frequency register based on the detected video standard. SID's three voices are thus able to generate tones up to ~3848Hz on PAL machines and ~3995Hz on NTSC machines, well in excess of the necessary range. Because SID generates audio asynchronously, we can just tell it to use infinite sustain (to infinitely prolong the note until we gate it off), play the mark frequency, and then leave the note playing while we go do something else, keeping the line open. Since the specification requires a sinusoidal wave, the code uses the SID's triangle waveform which is the closest approximation. The result is, in fact, the very tone you hear at the beginning of the videos.
However, there's one other reason we need separate NTSC and PAL versions, and that's because of how John set up the baudrate. Here's how the BASIC loader starts (from the NTSC version):
10 poke55,.:poke56,160:clr:c=49152:b=1 20 ready$:print"reading line";b;"of 20" 30 fork=1to2:a$=mid$(y$,j*2+k) 40 a=asc(a$)-48:ifa>9thena=a-7 50 e=a+16*e:next:ife=256then90 60 pokej+i+c,e:n=n+e:j=j+1:e=0 70 ifj=15thenj=0:i=i+15:b=b+1:goto20 80 goto30 90 s=54272:ifn=38107then110 100 print"data statement error":end 110 d=56320:fori=stos+24:pokei,.:next 120 e=56576:pokee+4,80:z=d+13:pokez,127 130 pokee+5,13:pokee+13,127:pokee+15,8 140 pokes+6,240:pokes,148:pokes+1,142 150 pokes+24,6:pokee+11,.:pokee+14,1 160 poked+15,8:poked+11,.:pokes+4,17 170 sys49152:pokez,129:ifpeek(2)then190 180 end
After reading the hex-encoded DATA statements into memory, at line 110 John's code starts initializing both the SID and the two CIA chips, though he primarily uses CIA #2. To determine bit times, rather than having the CPU manually count off a specific number of clock cycles, this code has the CIA do it. A critical point is that while the CIA chips can be set to issue IRQs or not, their interrupt control registers will still indicate when they would have fired one, even if that individual interrupt condition is technically disabled. John turns off all interrupts on both CIAs so they won't fire and upset system timing, including the usual Timer A IRQ on CIA #1 used for keyscan, then sets Timer A on CIA #2 to repeatedly count down $0d50 (3408) clock cycles. If we divide 1022730 by 3408, we get ... 300.09, almost exactly our baud rate. (It's okay to be a bit faster as long as you're never slower.) A smaller value is used for PAL systems.
With the CIAs (and audio) set up, we then go into the mini-terminal, which is loaded into memory and started from the usual location for such routines at 49152 ($c000). We disassemble that next.
lc000 jsr $e518 ; Kernal cint without PAL/NTSC detect lc003 lda $dc0d ; wait until Timer A interrupt time beq lc003 lc008 jsr $ea87 ; Kernal scnkey lda $c6 beq lc003 ; no key, go back to waiting lc00f jsr $f142 ; Kernal getin but always keyboard cmp #$85 ; F1 beq lc024 lc016 cmp #$88 ; F7 beq lc022 lc01a jsr $e716 ; Kernal chrout to screen jsr lc027 ; modulate the character bcc lc003 ; go again lc022 lda #$00 lc024 sta $02 rts
John chose to call direct into the Kernal for these routines to short-circuit code he didn't need. With thousands of cycles available to send each single bit, the full-fat routines would have been fine, but why bother with work you don't need to do? After initializing the screen editor, the code waits for the next Timer A interval to fire and scans the keyboard manually (since the IRQ isn't running anymore), then fetches the next key, if there is one. Assuming it's not F1 (send a file) or F7 (quit the terminal), the code then goes on to send a character using this subroutine at $c027:
lc027 sta $b0 ; stash registers stx $b1 sty $b2 ldx $dd0d ; clear ICR on CIA #2 lc030 ldx $dd0d ; wait for Timer A CIA #2 beq lc030
Each access on the interrupt control register clears any conditions that were set. This apparent "double-wait" on entering the routine isn't an error: it ensures not only that everything's in a known state, but that also at least one stop bit's interval has elapsed between the prior character and this one. Once that has occurred, we clock out the start bit, then eight data bits least significant first, and finally leave back at the stop bit frequency. Each time, except for the very end, we wait for another trigger on CIA #2's ICR before we proceed.
lc035 ldx #$81 stx $d401 ldx #$c3 stx $d400 ; start bit = 0 = 2025 Hz ldy #$08 lc041 ldx $dd0d ; wait for next interval beq lc041 ; shift bits out lc046 lsr bcs lc058 lc049 ldx #$81 stx $d401 ldx #$c3 stx $d400 ; 0 bit = 2025 Hz dey bne lc041 lc056 beq lc065 lc058 ldx #$8e stx $d401 ldx #$94 stx $d400 ; 1 bit = 2225 Hz dey bne lc041 lc065 ldx $dd0d ; wait one bit time on last bit beq lc065 lc06a ldx #$8e stx $d401 ldx #$94 stx $d400 ; stop bit = 1 = 2225 Hz lda $b0 ldx $b1 ldy $b2 ; restore registers clc ; jorb well done rts
When F1 or F7 is pressed, the mini-terminal sets location $2 to non-zero or zero respectively (above, after the call at $c00f) and returns to BASIC. BASIC then turns back on the Timer A IRQ on CIA #1, and if $2 is non-zero, it proceeds to ask for a device number and filename. This is a fun routine on its own, but you've seen enough of the code to understand the basics of how it works, so let's get out the WorkSlate now and try it with a real device.
For the Commodore side, we're going to use one of my portable SX-64 systems. A warning about the SX-64 specifically: never plug in a video cable — more specifically, never connect the audio output — with the computer's power on. Doing so runs you a decent chance of frying the SID, something I actually did many years ago and is a well-known problem. This goes likewise for connecting our mutant phone cable to the SX-64, since we necessarily have to use the computer's video port for the audio signal.
The WorkSlate has a built-in terminal desk accessory which can be activated from the Phone menu and selecting Terminal. However, simply selecting the Terminal is not enough. The trick with the WorkSlate is to have the speakerphone line open (Phone, SpkPhone) first, and then try to answer with the Terminal. This is supported by the device; it assumes in this case that you've manually dialed a number somehow (say, from an attached phone handset) and the computer on the other end has answered. We already have the answer stop bit tone playing, so the WorkSlate's modem immediately hears it and tries to go on-line.
And here we are! John's code indeed works as written and what we type on the SX-64 appears on the WorkSlate. The initial call to Kernal CINT resets the Commodore to uppercase/graphics mode, so we don't need a PETSCII-to-ASCII conversion step because unshifted letters we type on the Commodore come out as uppercase on both sides. If you were doing this with a Hayes-compatible modem connected to the C64, a command like ATX1D sets up a "blind dial" so that the 64's answer signal is also immediately recognized.The interesting part is comparing how the speakerphone operated between the three Workslates I now have. On the most recent one I acquired and on my "tester" unit that I soldered jumper probes to (both with serial numbers starting with CCA8415), I could hear the "call" and what the SX-64 was sending when the Workslate was in speakerphone mode, as expected. However, on my regular unit (a later machine with a CCA8417 serial number), I could hear both ends of the conversation through the SX-64's speaker, including the Workslate's dial tones and originate frequencies — and nothing on the Workslate's speaker. I'm not sure if this is due to different internal wiring, changes in the tape gate array or both. Again, this is a good reminder that the SID in the SX-64 is unusually vulnerable to stray voltages: if there were proper isolation I shouldn't have been able to hear incoming audio through the speaker output. In fairness, Commodore probably didn't think people would be wiring phone lines to SID audio either.
But let's embroider the situation a little more. Some modems may listen for a dial tone first before they attempt to do anything, especially if you need to actually dial a phony (narf narf narf) telephone number, since they reasonably expect there's a real POTS "plain old telephone service" line on the other side.
Indeed, if you try to manually dial a number on the WorkSlate ... ... there's no dial tone, so it fails, and the connection is dropped.Fortunately, the SID is perfectly capable of synthesizing other in-band telephony signals. For example, the Modem/300 came with cables that hooked up to the SID's audio output, and instead of the modem sending out DTMF touch tones to dial numbers, the computer did. Programs like Common Sense could be provided a phone number and dial it by playing tones like music. (Interestingly, the VIC-20 does not seem to be capable of precise enough frequency control to generate DTMF; Commodore even warns against it in the Modem/300 manual.)
Dialtones and other call-progress tones are often multi-frequency tones similar to DTMF, but they're specified separately by each region's telephone system. In the North American Bell System's Precise Tone Plan, dialtone is a combination of 350Hz and 440Hz at -13dBm, also played using a sine wave. If we use the formulas above and solve for the SID register values using those frequencies, these statements in BASIC will make a sufficient approximation of a dialtone on SID voices 1 and 2 (NTSC):
fori=54272to54296:pokei,0:next poke54272,49:poke54273,28 poke54279,109:poke54280,22 poke54278,240:poke54285,240 poke54276,17:poke54283,17 poke54296,6
The last thing to do is tie this all together. While John's original program was written as a quick, type-in-able means to get files transferred one way from a Commodore 64 to a larger system, here we're using it more as an experimental way to allow the Commmodore to talk to a smaller system, so let's make it a little friendlier. I removed the BASIC portion and wrote up a new menu system in pure assembly, incorporating and converting John's original code, and merging the PAL and NTSC versions together. It LOADs and RUNs like a BASIC program but is fully machine language.
This version can synthesize both a dial tone and the answer frequency, and I also added an ASCII translation table so that you send true ASCII in the mini-terminal. I also kept John's Xmodem-CRC code and added a simple transmit routine that sends a file straight up with no protocol, since the WorkSlate doesn't speak any file transfer protocols internally. Vanilla Xmodem, written by Ward Christensen, uses a fixed 128 data bytes per block and a simple checksum with known deficiencies, so John opted for the more complex version with a cyclic redundancy check to ensure errors could be promptly detected. Most terminal programs support this mode. We previously encountered a variant of Xmodem-CRC when we were figuring out how The Newsroom's Wire Service operated. From that article, the CRC-16-CCITT used in Xmodem-CRC is transmitted using this algorithm, rendered in K&R C:
/* * This function calculates the CRC used by the XMODEM/CRC Protocol * The first argument is a pointer to the message block. * The second argument is the number of bytes in the message block. * The function returns an integer which contains the CRC. * The low order 16 bits are the coefficients of the CRC. */ int calcrc(ptr, count) char *ptr; int count; { int crc, i; crc = 0; while (--count >= 0) { crc = crc ^ (int)*ptr++ << 8; for (i = 0; i < 8; ++i) if (crc & 0x8000) crc = crc << 1 ^ 0x1021; else crc = crc << 1; } return (crc & 0xFFFF); }
Many implementations of this algorithm use a lookup table for computation, but not John, who instead was optimizing for code size. Here's how John's routine sends an Xmodem-CRC packet (we'll assume that a file has already been opened by the Kernal and set up for reading).
lda #$01 sta $96 ; number of current packet ; start sending the current Xmodem packet lc08a lda #$00 sta $02 lda #$01 jsr lc027 ; send Xmodem SOH $01 lda $96 jsr lc027 ; send packet number eor #$ff jsr lc027 ; send inverse of packet number ldy #$03 jsr $ffa5 ; Kernal acptr, read next byte from file sta $8b ; store in high byte of CRC jsr lc027 ; transmit it inc $02 ldx $90 ; EOF? beq lc0b3 lc0ad lda #$1a ; yes, handle final packet, store a ^Z sta $8c bne lc115 lc0b3 jsr $ffa5 ; read again sta $8c ; store in low byte of CRC jsr lc027 ; transmit again ldx $90 bne lc115 ; check EOF again lc0bf inc $02
The routine keeps a running tally of the number of data bytes in the current Xmodem block in location $2, the current packet number (0-255) in $96, and the running CRC-16 in $8b/$8c. Notably, John maintains this value big-endian, unlike the usual 6502 little-endian convention, and exploits the fact that most transmitted blocks will have at least two data bytes. The routine starts each block by clearing the count, then with the modulation routine at $c027 above it sends the standard Xmodem start-of-header (^A) character, the packet number and inverse of packet number, then (checking for EOF each time) reads two characters into the high byte and low byte of the running CRC-16 and transmits them. If the status word at $90 shows an EOF, this condition remains until the file is closed.
For each byte after that to complete the block, another one is read and stored into $8d, then shifted into $8b and $8c:
lc0c1 jsr $ffa5 ; read again lc0c4 jsr lc027 ; transmit again lc0c7 sta $8d ; hold this byte ; for eight bits, compute crc, rolling newest byte in ldx #$08 lc0cb asl $8d rol $8c rol $8b ; << 1 bcc lc0df ; if (crc & 0x8000) lc0d3 lda $8b ; crc ^= 0x1021 eor #$10 sta $8b lda $8c eor #$21 sta $8c lc0df dex bne lc0cb
When a high bit is rolled out of the rolling CRC-16, this is detected as carry being set (no need for a bitmask) and the rolling CRC-16 bytes are exclusive-ORed with the required polynomial value ($1021, 4129). This is a very efficient translation of the algorithm.
The code then continues to run to complete the block of 128 bytes.
lc0e2 lda $90 bne lc115 ; check EOF again lc0e6 inc $02 bpl lc0c1 ; if not 128 bytes, get some more ; packet's done lc0ea txa ; x is zero, so zero accumulator dey ; flush the header as zeroes into CRC bne lc0c7 lc0ee inc $96 ; increment packet number lda $8b ; send CRC high byte first jsr lc027 lda $8c jsr lc027 ; delay briefly to wait for the ACK $06 to go by lc0fa inx txa pha pla tax bne lc0fa lc101 iny bne lc0fa ; pretend it worked lc104 lda $90 beq lc08a ; if not EOF, go back and do another packet
After the last byte is read from the file and shifted in, we need to incorporate three zero bytes into the CRC-16 to represent the header we sent. We then send that value and "wait" to pretend it worked, then go back for the next block. The weird delay routine allowed John to fit the entire loop into the maximum 7-bit displacement of a relative branch instruction. In fact, when I added code to flash the border on each block, I had to insert an absolute jump instead since those three extra bytes upset the apple cart.
At the very end of the file, any block in progress is padded with EOT (^Z), and then Xmodem EOF (^D) is sent to terminate the transmission:
; EOF, send EOT lc108 lda #$04 jsr lc027 ; send Xmodem EOT $04 lda #$81 sta $dc0d ; turn CIA #1 interrupts back on jmp $ffcc ; Kernal clrchn and exit back to BASIC ; handle end of file ; basically fill the last packet up with ^Z lc115 inc $02 bmi lc0ea ; it's full lc119 lda #$1a bne lc0c4 ; it's not, send and stuff ^Z
Note that the branch at $c11b will always be taken since we just loaded a non-zero immediate into the accumulator. Again, another nice way of increasing code density and reducing type-in size. Should the file have ended in the first two bytes used to prime the CRC-16, John's code just stuffs ^Zs into it manually.
This video shows the unified, enhanced version in action. When it starts up, it detects the correct video standard (or you can force it), and allows you to send a dialtone or go directly to an answer tone. Once the "softmodem" has answered, you can then enter the terminal or upload files. You can enter and exit the terminal or file transfer as desired, or simply "hang up" when you're done, which turns off the SID's output and causes any connected modem to perceive loss of carrier and terminate the link.
I've put John's original post containing the type-in versions (you can cut and paste these into VICE, if you like), plus the assembly source for this unified version and a pre-built binary, on Github. As John never asserted copyright to his programs and explicitly intended them to be freely distributable so that others could use and learn from them, I've placed this version into the public domain (to the extent available in your jurisdiction). You can assemble it using xa65.
John was a good guy with a clever programming style and it was nice to see his code running again (and working, though that was a given). Plus, this is a great use for a Commodore to support your other systems, and a roadmap for doing something similar on other machines with sufficiently capable sound hardware. In future articles I think we'll explore a few other things he wrote, including that audio digitizer. I think he would have enjoyed it.
No comments:
Post a Comment
Comments are subject to moderation. Be nice.