ZX Spectrum: Experimenting with 1-Bit Sound

Sep 08, 2026 09:54 PM - 1 day ago 1

As portion of the ZX Spectrum tour, I implemented immoderate routines that would fto you play elemental tones retired of the 1-bit beeper. These were successful ample portion inspired by earlier activity I did connected the Apple II. We cognize that’s not the limit, though, because we sewage immoderate improbably bully results retired of the IBM PC’s 1-bit speaker arsenic well. My original scheme for this week was to replicate immoderate of the precocious PC speaker techniques connected the ZX Spectrum. Unfortunately, that didn’t activity retired arsenic good arsenic I’d hoped, but that’s OK because I ended up getting a bunch of different things moving instead.

The main things I accomplished this week revolve astir multichannel sound done the 1-bit speaker—basically, playing chords. On the side, I besides recreated the elemental 1-bit PCM playback from the PC Speaker article and experimented concisely pinch the much precocious PWM technique. Both of those extremity up informing immoderate of the activity pinch chord playback arsenic well.

This won’t beryllium a broad guideline this week; I grounds my failures arsenic good arsenic my successes connected this blog, and this week racked up much than its adjacent stock of failures.

Pulse Code Modulation and Pulse Width Modulation

Pulse codification modulation is simply a beautiful elemental concept: a waveform is sampled arsenic a bid of values betwixt 0 and immoderate maximum value, and those values are sent to the hardware to beryllium converted into speaker voltages. The larger the maximum value, the much finely-grained your power of the amplitude, and the much quickly you nonstop samples, the much finely-grained your power of the frequency. (One basal norm of awesome processing is that you cannot accurately sample a waveform pinch a wave much than half your sample rate.) PCM waves are mostly described by the number of bits utilized to definitive each sample and past the sample rate—a precocious value sample mightiness beryllium 16-bit 44kHz, while astir usage cases mightiness beryllium served by an 8-bit sample astatine sampling rates arsenic debased arsenic 8 kHz.

The Spectrum tin entree its representation astatine astir 1MHz, truthful we will beryllium capable to deed a respectable 16kHz playback moreover nether direct, cycle-counted CPU control. However, the speaker is only ever connected aliases off. That intends that this is 1-bit PCM, which we should expect to sound beautiful bad; we’ll beryllium getting the benignant of distortion that you’d get erstwhile blowing a speaker retired pinch excessively overmuch amplification, but each the clip and moreover astatine debased volumes.

Different models of Spectrum had somewhat different CPU speeds; I’ll beryllium utilizing the 48K’s timepiece which ran astatine a level 3.5 MHz for my rhythm counting. Dividing that by 16,000 samples per 2nd reveals that we’ll person to hold 219 cycles betwixt writes. That’s a beautiful cozy magnitude of time; we tin battalion a 1-bit PCM signaling 8 samples to a byte and easy beryllium capable to devour it. Samples will devour astir 2KB per second, which is tight connected a 48K strategy but not disastrously so. The main problem, arsenic we will see, will beryllium the distortion of the sound. The implementation of this method poses nary typical challenges.

Pulse width modulation is simply a small trickier but it promises overmuch higher audio quality. At the electrical-signal level, PWM information sends a 1-bit beat erstwhile each sample, and the length of the pulse indicates the intended spot of the audio awesome astatine that sample point. (Compare PCM, which efficaciously sends a multi-bit integer value complete the ligament to execute this. PWM is much analog than PCM, contempt being much aggressively 1-bit.) At the beingness level, this manifests arsenic a consequence of the truth that while electrical signals tin alteration from 1 to 0 and backmost successful a matter of nanoseconds, the physical speaker attached to the device will require tens of microseconds to really make the travel betwixt its “in” and “out” states. By switching the awesome disconnected astatine various points successful its journey, the speaker’s full spot varies successful a acold much precisely-controllable mode than 1-bit PCM provides.

On the IBM PC, the 1-bit speaker is tied to a hardware timer, and high-quality audio whitethorn beryllium generated by feeding that timer 7-bit PCM data. The Spectrum is not truthful helpful, and we’ll person to negociate it pinch rhythm counting. I did not negociate to get a PWM strategy moving to my restitution connected the Spectrum; I do nevertheless person immoderate leads and americium convinced that the method wide is sound.

Cycle-Exact Delays connected the Z80

Before we excavation into immoderate codification successful detail, we should nail down what it takes to do precise timing delays. The Z80’s “T-State” count is simply a batch larger and a batch slipperier than the CPU cycles connected the 6502, truthful while I could throw together instructions for trivially waiting immoderate number of cycles connected the 6502, my Z80 guidance is needfully a spot much contingent. For this activity I recovered myself mostly relying connected rules of thumb, assembling hold sequences successful a much integrated way.

  • NOP itself is 4 cycles, arsenic is fundamentally each one-byte 8-bit cognition (of which location are many). Once the remaining hold is some short and a aggregate 4 cycles, we are fundamentally done.
  • INC HL is 6 cycles, and similarly-structured one-byte 16-bit instructions are too. These tin information distant a 2-cycle discrepancy, aliases beryllium paired to hold 12 cycles pinch 2 bytes of codification alternatively of 3.
  • LD A,n is 7 cycles alongside the different 8-bit contiguous instructions. As agelong arsenic the total hold is agelong capable these will fto america get down to rhythm accuracy.
  • JP instructions are 10 cycles moreover erstwhile conditional which makes them overmuch preferable to JR aliases DJNZ for codification that has to make decisions successful a precisely-timed loop. Those still person their place, though, because…
  • LD B,n; LBL: DJNZ LBL is the shortest loop you tin constitute and it expends 13n+2 cycles. For agelong waits, this loop will beryllium the bulk of it, and the different instructions supra will information distant immoderate inconveniences.

This isn’t precisely systematic, but it’s capable for our purposes.

A PCM Playback Routine

The wide scheme will beryllium to battalion 8 samples into each byte, pinch the precocious spot played first. With a 16-bit sample, we’ll want 219 cycles betwixt each output. Our input will put a pointer to the input information successful HL and the number of bytes successful the sample successful DE. (We’ll presume the full number of samples is simply a aggregate of 8. Our encoder will conscionable request to pad the end, and that’s the encoder’s problem, not ours.) We usually usage BC for our counters, but we’ll beryllium needing B successful peculiar for our internal clip delay counters, truthful DE will person to prime up the slack.

We statesman the routine, funnily enough, mostly astatine the end. When we participate the main playback loop, we will beryllium successful the middle of a 219-cycle sequence, and successful bid to cognize what our timing constraints are, we person to constitute the extremity of the loop to spell pinch it. It besides turns retired that our wide usability prologue and closing are truthful short that we tin dispose of them correct distant too. All they person to do is disable and re-enable interrupts.

pcmout: di .lp: ????????????? ; Play 8 samples from a byte, ending with... retired ($fe),a ; + 11 (219) ...the last spot output ;; Adjust byte antagonistic and loop back dec de ; + 6 ( 6) ld a,d ; + 4 ( 10) aliases e ; + 4 ( 14) jp nz,.lp ; + 10 ( 24) ;; We're done; re-enable interrupts and return ei ret

Normally erstwhile consuming a byte a spot astatine a time, we support shifting disconnected 1 extremity aliases the different and consult the transportation spot to determine what to do. We tin beryllium a small cute, here; we request to transcript the spot we’re consuming into the $10 spot of the output byte. Given that fact, it’s much effective to return our first byte, rotate it correct 3 bits, and past conscionable activity pinch the $10 spot directly. We’ll request to make judge we usage the 8-bit RRC and RLC instructions alternatively of the 9-bit RR and RL ones.

We know, from above, that we participate the loop astatine rhythm 24. Reading the byte, preparing it, and outputting the first spot astatine the due clip will frankincense look for illustration this:

.lp: ld a,(hl) ; + 7 ( 31) Load a byte inc hl ; + 6 ( 37) Advance pointer rrca ; + 4 ( 41) Rotate $80 spot to $10 rrca ; + 4 ( 45) pinch 8-bit rotations rrca ; + 4 ( 49) ld c,a ; + 4 ( 53) Stash byte successful C and $10 ; + 7 ( 60) Isolate sound bit aliases $01 ; + 7 ( 67) Blue separator because why not ????????????? ; +141 (208) retired ($fe),a ; + 11 (219)

Now we person to hold 141 cycles. Here’s what I came up pinch for that.

inc hl ; + 6 ( 73) dec hl ; + 6 ( 79) ld b,9 ; + 7 ( 86) jp 1F ; + 10 ( 96) 1 djnz 1B ; +112 (208)

That lands america correct wherever we request to be.

One spot down, 7 to go. I considered making this an soul loop, but I’m retired of registers and spilling to representation could get ugly. Much simpler to conscionable fto sjasm copy-paste my codification for me.

repetition 7 rlc c ; + 8 ( 8) Next bit ld c,a ; + 4 ( 12) and $10 ; + 7 ( 19) Isolate sound bit aliases $01 ; + 7 ( 26) Blue separator still ld b,1 ; + 7 ( 33) Delay 182 cycles... dec b ; + 4 ( 37) ld b,13 ; + 7 ( 44) 1 djnz 1B ; +164 (208) retired ($fe),a ; + 11 (219) ... and output this bit endrepeat

That’s each we needed; beyond that we conscionable person the last six instructions from our first skeleton. The usability useful great! The existent sound quality, however, is awful. I moreover tried a spot of preprocessing to cleanable up immoderate of the sound successful parts wherever location isn’t really a meaningful awesome and moreover that didn’t thief much. Still, the function useful conscionable good for what it is, and it will beryllium a useful point to support successful our backmost pocket. I’m not peculiarly inspired to tune the repeat macro into a due loop, though.

Playing Chords

Multichannel euphony was apparently reasonably communal moreover done the beeper, backmost successful the day. My ain adventures complete the years suggested 2 approaches to getting reasonably bully results:

  • Arpeggiation. It was very communal connected some the C64 and the Amiga to springiness a chord to a azygous output transmission and conscionable alteration the wave each framework to nutrient the chord effect. This crippled them a very unique benignant of “buzz” sound. This should beryllium trivial to instrumentality fixed what I’ve already written truthful far.
  • Software Mixing. The accustomed measurement to do aggregate sound channels connected a monaural strategy is simply to sum up each the incoming signals and output that worth connected its own. We’ll person to trim immoderate corners if we effort that here, but the basal rule should beryllium sound.

Implementing Arpeggiation

This should beryllium really easy. I already person a regular that plays 1 reside for immoderate magnitude of time. I tin conscionable make that clip really short and put it successful an outer loop. Something for illustration this:

ld b,$18 1 push bc ld bc,$0d0 ld de,$0367 telephone sound ld bc,$0d0 ld de,$0441 telephone sound ld bc,$0d0 ld de,$051a telephone sound popular bc djnz 1B

We did study past clip that we’re not allowed to touch representation betwixt $4000 and $7FFF if we want accordant timing, truthful I group the root to $8100 present to make judge we don’t get stalled by the PUSH, POP and CALL instructions.

The results, overall, are plausible. It’s a spot scratchy but it’s noticably a chord. One point I did announcement was that if I made the intervals excessively short, the chord sewage detuned. I’m beautiful judge that what was happening there was that changing notes not only took immoderate other clip but besides reset the beat counter, which could consequence successful immoderate earnestly out-of-spec waveforms astatine the modulation points. Things did look to amended a spot erstwhile I moved the initialization of HL and A to the top-level chord usability alternatively of the original sound routine. I past besides passed the 3 frequencies successful registers each astatine erstwhile and loaded them into the arguments of the contiguous instructions. The chord usability ended up for illustration this:

chord: ld (.f1),hl ld (.f2),de ld (.f3),bc ld a,($5c48) ; BORDCR and $38 rrca rrca rrca aliases $08 di ld b,$18 ld hl,0 1 push bc ld bc,$0d0 .f1 equ $+1 ld de,$0000 telephone sound ld bc,$0d0 .f2 equ $+1 ld de,$0000 telephone sound ld bc,$0d0 .f3 equ $+1 ld de,$0000 telephone sound popular bc djnz 1B ei ret

Finally I put together a small macro to make chord progressions easier to specify, too, truthful I could springiness it a small chord progression.

macro play 3 ld hl,@1 ld de,@2 ld bc,@3 telephone chord endmacro ;; Main program play $0367,$0441,$051a ; I play $0367,$048b,$05ba ; IV play $0367,$0441,$051a ; I play $0336,$03d2,$051a ; V play $0367,$0441,$051a ; I ret

Unlike the PCM codification above, the sound value present wouldn’t really beryllium retired of spot successful anything. Like the PCM codification above, I americium getting existent mileage retired of Sjasm’s macro facilities.

Software Mixing

I’ve really built a cycle-counted polyphonic synthesizer before: it was a wave-table strategy for the Dragon. The basal principles down that still apply, but pinch a 1-bit output we tin simplify it a bit.

  1. Execute the aforesaid frequency-counter based sound strategy arsenic we’ve been doing, but support 3 counters and wave steps alternatively of conscionable one.
  2. After each antagonistic is updated, sum the apical bits of each 1 and group the speaker based connected whether we are successful the apical aliases bottommost half of the imaginable range.

This is overmuch easier if there’s only ever overseas numbers of progressive channels; I deliberation for an moreover number of channels I would want the mediate worth to time off the speaker output as-is. That would beryllium easier to execute connected the Apple II (where the speaker power is simply a toggle) than connected the Spectrum (where we straight constitute the speaker value). With precisely 3 voices, arsenic here, it’s moreover easier because pinch a imaginable scope of 0-3 we tin conscionable look astatine the apical spot of the 2-bit sum.

The logic for each transmission is very akin to what I wrote successful the sound tour. There are 2 awesome differences: I request to sync the antagonistic and the wave codification pinch representation for each codification (since we don’t person capable registers for each 3 astatine once) and I request to usage a 16-bit antagonistic alternatively of my erstwhile in-effect 17-bit antagonistic involving the transportation bit. In the aged code, I’d flip and hold the worth being output erstwhile the transportation spot was set; now I request to return the apical spot of the antagonistic and usage it arsenic the worth I adhd to the moving total. I wrap it up successful a macro, pinch timings.

macro count_channel count,freq ld hl,(count) ; +16 ld de,(freq) ; +20 adhd hl,de ; +11 spot 7,h ; + 8 jp m,1F ; +10 jp 2F ; +10 1 inc a ; + 4 dec de ; + 6 2 ld (count),hl ; +16 endmacro

There’s immoderate branching present truthful I can’t conscionable do a consecutive sum, but each way done this codification is 91 cycles exactly.

The wave codes will beryllium nationalist variables (we’ll walk arguments that way), but the transmission counters tin beryllium private.

freq1 # 2 freq2 # 2 freq3 # 2 chord: ld hl,0 ld (.counter1),hl ld (.counter2),hl ld (.counter3),hl di .lp: xor a ; + 4 Clear the sum value count_channel .counter1,freq1 ; +91 Process each sound successful turn count_channel .counter2,freq2 ; +91 count_channel .counter3,freq3 ; +91 adhd a ; + 4 Multiply A by 8, moving the adhd a ; + 4 $02 spot into the $10 place adhd a ; + 4 and $10 ; + 7 Isolate that bit aliases $0f ; + 7 White background retired ($fe),a ; +11 Output summed bit dec bc ; + 6 Decrease antagonistic and proceed ld a,b ; + 4 aliases c ; + 4 jp nz,.lp ; +10 ei ret .counter1 # 2 .counter2 # 2 .counter3 # 2

At 338 cycles per loop, and a “flip” antagonistic worth of $8000 alternatively of $10000, I request to recompute my scales.

B C D E F G A $061b $0678 $0743 $0826 $08a2 $09b1 $0ae1

The main programme is simply a spot much verbose this time, because we’ve delegated much representation activity to the call.

macro play 4 ld hl,@2 ld (freq1),hl ld hl,@3 ld (freq2),hl ld hl,@4 ld (freq3),hl ld bc,@1 telephone chord endmacro play $2000,$0678,$0826,$09b1 ; I play $2000,$0678,$08a2,$0ae1 ; IV play $2000,$0678,$0826,$09b1 ; I play $2000,$061b,$0743,$09b1 ; V play $4000,$0678,$0826,$09b1 ; I ret

However, the logic isn’t overmuch different astatine the precocious level, and the wide sound results for this attack are much amended than the arpeggiated version; the sound feels overmuch richer and it doesn’t really moreover sound for illustration a 1-bit strategy much. I was rather impressed pinch this one.

At this constituent I’ve reached the limit of the play routines that really worked. I’ve uploaded a postulation of programs to their ain directory successful my Github Repo; you tin build and tally them pinch the Makefile, aliases cheque retired the audio record location to perceive the arpeggiation and channel-summing techniques alongside a PCM waveform. From present connected retired I’ll beryllium covering approaches that either didn’t activity aliases didn’t make it each the measurement into a existent implementation.

Other Approaches

I had a fewer different things I wanted to research with, peculiarly arsenic they related to the arpeggiation system, but getting the codification correct was a spot of a pain, and it was not ever evident whether a bad sound would beryllium because the technique was bad, aliases because my implementation was buggy. After awhile I realized thing important: I don’t request to constitute civilization sound engines to trial these. I tin conscionable pregenerate the activity files I want and past run them done the PCM player.

The main point I wanted to effort was to look into the rumor wherever it detuned if we swapped frequencies excessively fast. My mentation present was that it sounded incorrect because we weren’t getting complete waveforms out. If that’s so, past if we’re generating the waveform successful beforehand we tin hole that: we tin conscionable do precisely 1 activity of each frequency, 1 aft the other, successful order.

This besides didn’t sound right, but looking astatine the PCM information gave a very bully imaginable mentation for why: lower frequencies return longer to play. Imagine playing a elemental chord of 2 notes separated by an octave. The debased statement has half the wave of the precocious note, but that intends that two-thirds of the waveform is spent connected the little note, and this seems to unbalance the resulting waveform.

Similarly, dividing up waveforms by timeslice produced very akin distortions to my first tests. The shorter the timeslice, the less existent pulses person the correct length.

But… what if we didn’t sphere the beat waves at all?

The Apple IIgs’s sound system is very unusual. It boasts 15 stereo wavetable channels, a completely unreasonable number for 1986. The measurement it accomplishes this is that “arpeggiates” each its integer sound channels; if 3 channels are active, past the sound spot plays 1 sample from each transmission successful turn, truthful immoderate fixed transmission only appears connected each 3rd sample. Instead of interleaving for illustration we’ve been doing pinch our beat waves, what if we took a page from the IIgs and shuffled the waveforms together a sample astatine a clip intead of a pulse?

The reply is: not bad astatine all. It’s markedly amended than immoderate of my erstwhile attempts astatine arpeggiation, but it’s not rather arsenic bully to my receptor arsenic the sample-addition approach. I fishy this is owed to the measurement that the channel-addition attack will effort to minimize transitions, truthful the wide activity ends up emotion “cleaner”. The sample-shuffling attack will person each the aforesaid wave components successful the signal, but the larger number of transitions will make it consciousness a touch “scratchier” overall.

Pulse Wave Modulation

My erstwhile activity pinch PWM relied connected being capable to group a hardware timer to power erstwhile the speaker turns connected and off. This was precise capable that I could dainty the timer’s configuration registry for illustration a larboard receiving 7-bit PCM data. No specified luck here, but it shouldn’t beryllium that bad, nevertheless.

Assuming the Spectrum’s speaker is astir balanced to the PC’s, it takes it 50 microseconds (or 175 cycles, aliases 43.75 NOP instructions) to execute afloat travel. Delivering a beat of precise width should beryllium a matter of the instructions OUT ($FE),A, XOR $10, and past a drawstring of 45 NOPs. We whitethorn past edit different OUT instruction into that watercourse of no-ops astatine the correct point, and past move it backmost into a brace of NOPs afterwards. The extremity consequence should beryllium a chunk of codification that ever runs successful changeless clip and which which gives america somewhat worse than microsecond power complete beat width. Then we repetition the process erstwhile per sample.

This should beryllium fine. However, I could not get this to activity successful my experiments. I walked distant from this portion of the task pinch immoderate instructive failures.

One disadvantage of PWM-based systems is that each beat we nonstop resets the speaker authorities erstwhile it is done. The pulses themselves will nutrient a regular bid of signals that will beryllium perceptible if the sample complaint is debased enough. On my trial programs, this high-pitched squeal was each I could perceive astatine all. If I listened very carefully, I could possibly prime retired a mini portion of the original sample from it.

That, astatine least, suggests that it isn’t—or isn’t entirely—an emulator issue. But it doesn’t different springiness maine overmuch to activity with.

Getting By With a Little Help From Our Friends

Fortunately for me, location are a batch of highly knowledgable Sinclair experts retired location who tin constituent maine wherever I request to go. Conversations some connected societal media and connected the Spectrum Computing tract forums gave maine rather a batch to chew on:

  • Pulse width modulation absolutely works; there are demos.
  • The Fuse emulator is bully capable to grip these—if anything, it’s too reliable, arsenic the results from existent hardware are apparently near-inaudible without an amplifier attached.
  • L Break Into Program, whose activity I’ve linked earlier arsenic portion of my circuit of the graphics system, worked pinch the legendary Follin brothers connected respective of the systems they targeted, including the Spectrum. With the support of their estate, he has published immoderate of their sound drivers connected GitHub.

I haven’t done overmuch pinch these yet—I ever for illustration to look these systems pinch arsenic caller a position arsenic I tin truthful that I’m not conscionable rehashing earlier work—but I’ve deed capable of a wall present beyond the things I have accomplished that I deliberation it’s clip for maine to return a person look astatine anterior activity successful detail.

As I wrap up this week, I haven’t had overmuch of a chance to really do that. I person astatine slightest identified wherever Fuse keeps its beeper simulator and tin spot that it implements 1 of 3 imaginable lowpass filters complete the output. That’s a plausible implementation system for simulating the physicality of the speaker, and truthful I americium rather convinced that this ought to work. I won’t beryllium capable to opportunity thing definitive astir it until I get a chance to get immoderate clip from various group pinch different kinds of hardware though. You tin only get truthful acold successful a pure-software lab.

More