No posts yet. Check back soon!
Unfinished Business with 1-Bit Audio Chords
07:30 PM · Sep 19, 2026
My article from two weeks ago on fancy 1-bit sound on the ZX Spectrum was a bit incomplete. I used a shortcut to test out the “interleaved oscillator” technique, and I couldn’t get pulse-width modulation to work at all.
The good news is that I’ve gotten both of those working properly and I’ve collected everything into a single program that can be loaded and run on a 48K Spectrum or anything better, and that puts everything through its paces.

The bad news is that this didn’t leave time to actually write everything up. Instead of just skipping a week, I’ll be covering the chord-playing systems this week and look at sample playback next time. There’s only one actual new player here, and in the end it turns out to be short and sweet, so before we move on to that I am also going to blog like it’s 2006 and do a link roundup too.
New Discoveries
I got a better look at some of the things I mentioned last time, and a few blog-relevant releases also crossed my desk in the interim.
- The VBCC compiler suite, with a
vbcccompiler, thevasmassembler, and thevlinklinker, now apparently supports the 65816 CPU and can even produce GS/OS executables for the Apple IIgs. Previously, Merlin 32 was the only no-cost tool I knew of that could do this because the IIgs is such a unique platform. - The BlastEm emulator has reached its 1.0 milestone, supporting not only the Sega Genesis/MegaDrive, but also a big chunk of other Sega platforms, including the SG-1000. Even the ColecoVision makes an appearance! BlastEm aims for high accuracy and efficient execution, and it’s been my emulator of choice for playing Genesis games for awhile. Most exciting to me here, though, is that the debugger has been entirely rewritten. The Genesis debugging situation is pretty dire and any progress is a big deal.
- Apple2TS is a TypeScript-based Apple IIe emulator that has a lot of convenient options and runs in the browser. It also packages itself as an AppImage as required. This is probably my new first choice for emulating the Apple 8-bits off of Windows. (AppleWin still reigns supreme there.)
- The incredible We Are Vocoders demo has its source code available on GitHub, and while I’ll look at it briefly next week, I will note here that his approach to sound control was markedly different from my own.
- It also isn’t alone. The author Dmitry Milk has a collection of published demos and I am rather fond of Beepdrive as well.
- Similarly, I invoked the Follin brothers in the earlier article and the magic they worked with Spectrum sound; I took a look at the Agent X II music driver as well. This is actually weaving in the later Spectrums’ AY-3-8910-class sound chips alongside the beeper, and so ultimately it solves a different problem than the one I was focusing on.
- The square wave may be the simplest sound wave we can make a digital circuit emit, but properly simulating the output of such a generator is trickier than it looks. The Fuse emulator relies on Shay “Blargg” Green’s
Blip_Bufferlibrary to provide that simulation and adds some filtering over it to represent the electrical and physical limitations of the circuit and speaker. Adjacent to that is this article on the principles of actually sampling square waves properly that is a good primer on how hard this “simplest” problem really is.
With that, let’s move on to the new designs and code.
Interleaved Polyphony
Last time I presented two approaches to playing chords out of a monaural 1-bit speaker:
- Use the standard single-note beeper routine but cycle between each note in your chord, letting each “note” last only 20 milliseconds or so. This produces a sound very like the “chords” in SID and MOD tunes on the Commodore machines.
- Generate each tone simultaneously and independently, and sum up the signal before truncating it back to a 1-bit value. This can only really be done cleanly if there are an odd number of voices.
The last technique sort of mixes the two, and also sort of brings in the approach that the Apple IIgs used for its own multi-channel output: compute the signal for each channel simultaneously and independently, and then cycle through which channel you actually output, swapping channels every sample. This is, possibly coincidentaly, a slight generalization of a two-channel technique described to me as “add up the channels and then have the three possible values of 0, 1, and alternating really fast between 0 and 1.”
Before we get to coding, there is one additional improvement we can make to the design over the IIgs hardware that inspired me. One deeply obnoxious aspect of the IIgs sound system is that the frequency codes you gave channels were only processed when that channel “got a turn,” which in turn means that, say, the frequency code for middle C when a single channel is active becomes the frequency code for low C if two channels are active. Four channels active drops it another octave again, and so on. I will be hard-coding three channels into this test program, and I will avoid this annoyance by adding the frequency code to a channel three times for each time it’s that channel’s turn to be played. That way the frequency codes will be broadly in line with all my other sound generators.
The implementation itself will look very similar to the channel-sum playback system: I will define a macro that fully handles one channel, and then invoke it three times with each active tone’s counter and frequency code. This turns out to be even simpler and more uniform than the channel-sum system, because one full iteration through the loop will produce three samples instead of one, meaning that the duration counter also gets updated thrice per iteration, meaning that we don’t have to balance logic against equal-time no-op sequences.
The macro body updates the counter, determines the appropriate value to send to the speaker, sends it, decrements the duration counter in BC, and sets the zero flag appropriately for the decrement. That code is straight line:
macro count_channel count,freq
ld hl,(count) ; +16
ld de,(freq) ; +20
add hl,de ; +11
add hl,de ; +11
add hl,de ; +11
ld a,h ; + 4
rrca ; + 4
rrca ; + 4
rrca ; + 4
and $10 ; + 7
or $01 ; + 7
out ($fe),a ; +11
ld (count),hl ; +16
dec bc ; + 6
ld a,b ; + 4
or c ; + 4
endmacro
Conditional jumps are the only glue necessary from here, so the main body implementation of chord is trivial:
chord: ld hl,0
ld (counter1),hl
ld (counter2),hl
ld (counter3),hl
di
.lp: count_channel counter1, freq1
jp z,.end
count_channel counter2, freq2
jp z,.end
count_channel counter3, freq3
jp nz,.lp
.end: ei
ret
Counting cycles yields 150 cycles between each call to OUT. From this we can get our frequency codes:
- Processing one channel costs 150 cycles / 3.5 cycles per microsecond, which is about 43 microseconds.
- The top bit of the 16-bit counter is what we send to the speaker, which means that one full wave happens once we count the full 65,536 (0x10000) values in range.
- The number of iterations per wavelength is thus 0x10000 / n, where n is our frequency code.
- It is also the case that the number of iterations per wavelength is the wavelength divided by the 43 microseconds of our channel-process time.
- Set those two equal to each other and solve for the frequency code n and we get
n = 0x10000 * iterationTime / wavelength, or equivalentlyn = 0x10000 * iterationTime * frequency.
Some quick Python gets me all the notes from the A below middle C to the A above it:
step = 2 ** (1/12)iteration = 150 / 3.5e6scale = [220 * (step ** n) for n in range(13)]codes = [f"int(0x10000 * iteration * f):04x" for f in scale]
This gives us some handy values to plug into the chord function.
;;; A B C D E F G A ;;; $0269 $02b5 $02de $0338 $039d $03d4 $044c $04d3
Listening to the Results
I made an OGG file of all the sound test programs in sequence and put it in the GitHub repository. We get some pretty striking results.
- It opens with a C-major scale with the frequency beeper from the original ZX Spectrum tour. It’s not literally the
BEEPcommand but it’s close enough. - This is the 50Hz arpeggio, which produces the distinctive chiptune trill.
- This is the channel-sum technique, which produces my favorite of all the tones, coming in somewhere between an organ and an electric guitar.
- This is the method we implemented today. The chord itself is basically fine, but there’s an ugly rattling squeal over the entire playback.
- This is the 1-bit sound sample playback, and it’s basically just static. I’m pretty sure this is how the old Apple II game Castle Wolfenstein did its voice clips, though; it very much has the same feel.
- This is the pulse-width modulation system I finally got working, and which we’ll get into next week. As you can hear, this is much clearer sound quality than 1-bit systems should be able to do, and also there is, at most, a tiny hiss in the background. There’s nothing like the rattling squeal in our chords.
So what’s up with that squeal? Let’s look at the actual waveforms in Audacity. Here’s the 50Hz arpeggios.

One thing jumps out at us immediately, which is that these are not remotely square waves at all. The filters applied to the input and the general impulse response in the first place produce much more angular approaches. Still, these shapes are what we see even from the humble BEEP command, so this is our baseline.
The channel sum is surprisingly similar:

There’s more going on here, but this still is pretty recognizably something very similar to the arpeggio. I liken it to taking three sine waves and adding them together to get a pure chord tone, but then overdriving the signal so hard that it ends up clipping 100% of the time. The result is very clear and very distinct; I think that we’ve basically encoded the minimum possible number of 1-bit transitions here to have all the primary frequencies appear in the result.
Now we look at our new interleave system. This looks quite a bit different:

The overall shape is very close to the channel-sum, but the wave is a lot messier at the points—and there are many—where the three voice are not uniformly high or low. Let’s zoom in on part of that:

With 150 cycles per iteration and a 3.5MHz CPU, our sample output rate is a vry respectable 23kHz. However, we’re sending out a repeating pattern of 0,0,1 here, meaning those 1s, on their own, are producing a wave on their own of about 7.8kHz, which is within the range of a piano’s top octave. That’s the oscillation we see here, and that oscillation is what produces the squeal. The fact that it’s only part of the signal sometimes—that it vanishes whenever the three voices are aligned—is what makes it rattle.
This in turn suggests that we should be able to make the squeal higher if there are only two voices. That kicks it up to more like 12kHz, which isn’t quite ultrasonic, but which does seem to basically remove the squeal. The wave looks like this:

The high-frequency jaggies are still there, but they’re both rarer, more regularly balanced, and closer to the origin. now. I suspect that this ends up also making them softer, because it’s much harder to pick it out here.
A Problem We Didn’t Solve
All of this code is simply striking chords. Normally we would want to play music, reading note data off of some kind of score and then having that play. It will also generally need to play in the background; we’d like at least a little animation and we almost certainly want to process input.
These aren’t conceptually difficult, even here, but they’re a whole lot more annoying. We know from our Spectrum Tour how to scan the keyboard directly; we can just collect keyscans in between samples. We’ve got the time for it, though we will also clearly be stuttering for any serious processing after a keypress is detected.
The part that will be tremendously obnoxious is that every case but the 50Hz trill is specifically designed for a fixed number of voices and furthermore silence within a voice is not allowed. Silent, always-zero channels will either damp the output into garbage or reintroduce even nastier versions of the background squeal. Any music driver using one of these chord players will need versions for each possible number of channels and swap between them as the score demands.
If that work were to be done, I think the best results would be to use the channel-sum technique almost everywhere, but alternate between 1 and 0 each iteration if there are an even number of channels and a “tie vote” on what the result should be. From a sound standpoint, that’s the best of both worlds. From a dev standpoint, I think it would end up being pretty tedious and macro-heavy—and you’d definitely want to preprocess the music data in some way to make sure that everything keeps behaving as the number of active channels changes—but this would the work of production, not invention.
Next time: functioning digital sample playback, finally!