draft: false
Name: ZX
short-description: A cycle-accurate ZX Spectrum 128K that turns the modular jacks/knobs/switch into a patchable control surface — plus OneBit (1-bit beeper synth) as an alternate boot mode.
Language: C++ (Pico SDK + ComputerCard)
Creator: Andy Jenkinson (uglifruit)
Version: "1.2.1"
Status: Released
License: MIT (card source); vendored components keep their own licences
Editor: https://uglifruit.github.io/WorkshopZX/interface.html
web-entry: interface.html
repository: https://github.com/uglifruit/WorkshopZX
discussion: https://discord.com/channels/1210238368898879569/1530739124703723691
date-created: 2026-07-26
date-updated: 2026-08-04
contact:
email: andyuglifruit@hotmail.com
website: https://github.com/uglifruit/WorkshopZX
tags:
- emulator
- zx-spectrum
- z80
- ay-3-8912
- pt3
- 1bit
- chiptune
# The summary renders through an inline Markdown pass, which flattens blank-line
# paragraph breaks — hence the explicit
tags. `br` is on the sanitiser's
# allowed inline-tag list, so they survive to the page.
summary: |
A ZX Spectrum in Eurorack. The instrument you never needed.
ZX SPECTRUM (normal boot): a ZX Spectrum 128K in your Workshop Computer. Load .z80/.sna snapshots and .ay/.pt3 chip-music
over a browser Web UI (interface.html, WebMIDI/SysEx).
Each jack/switch
is mappable to a Spectrum key, joystick direction, or a Z80-readable port,
so a Spectrum program can read patched CV/gates as data.
Type into the Spectrum from a laptop via
the Web UI. The baked-in demo's source is a worked example of a
Spectrum program interfacing with the CV world.
Alternatively CV mangle .ay/.pt3 file playback.
ONEBIT (hold the momentary switch DOWN at power-on): a 1-bit "beeper synth" with
seven selectable engines (ports of classic ZX-Spectrum beeper routines) and drums.
Pitch CV, gate, duophony, and a cycleable 'drum kit'.
# Two boot modes, two panels (see panels/manifest.yaml). ZX Spectrum is the normal
# boot and the default view, so its labels are the UNCONDITIONED base rows below.
# OneBit is the alternate boot: it only needs `when: { panel: onebit }` rows for
# the sockets and knobs whose meaning actually changes, which then override the
# base in that view. Same shape as 45_bends, which conditions only its knobs.
panel:
inputs:
- id: CVIn1
name: Mapped input
type: cv
description: Mappable to a Spectrum key, a joystick direction, or a Z80-readable port (comparator; read at port 0x2B / 43).
- id: CVIn2
name: Mapped input
type: cv
description: Mappable input; also the memory-probe source. Read at port 0x2F / 47.
- id: AudioIn1
name: Mapped input
type: audio
description: Mappable input, audio-rate (comparator). Read at port 0x33 / 51.
- id: AudioIn2
name: Mapped input
type: audio
description: Mappable input, audio-rate (comparator). Read at port 0x63 / 99.
- id: PulseIn1
name: Mapped gate
type: pulse
description: Mappable gate — a key, a joystick direction, or a port. Read at port 0x23 / 35.
- id: PulseIn2
name: Mapped gate
type: pulse
description: Mappable gate. Read at port 0x27 / 39.
# OneBit overrides — pitch/gate/drum instead of keyboard and CV-as-data.
- id: CVIn1
when: { panel: onebit }
name: Voice 1 pitch
type: cv
description: Voice 1 pitch, 1V/oct (offset by Knob X).
- id: CVIn2
when: { panel: onebit }
name: Voice 2 pitch
type: cv
description: Voice 2 pitch, 1V/oct, when the switch is in the Middle position.
- id: AudioIn1
when: { panel: onebit }
name: Duty & drum pick
type: audio
description: Duty modulation; also latches the drum selection at each Pulse In 2 edge.
- id: AudioIn2
when: { panel: onebit }
name: Duty mod
type: audio
description: Duty / timbre modulation.
- id: PulseIn1
when: { panel: onebit }
name: Note gate
type: pulse
description: Note gate — held gates sustain the note.
- id: PulseIn2
when: { panel: onebit }
name: Drum trigger
type: pulse
description: Drum trigger; the drum played is chosen by Audio In 1.
outputs:
- id: PulseOut1
name: Beeper
type: pulse
description: The ULA beeper (port 0xFE bit 4) — the Spectrum's 1-bit speaker line.
- id: PulseOut2
name: MIC / tape
type: pulse
description: The ULA MIC/tape output line.
- id: CVOut1
name: Border voltage
type: cv
description: The border colour (0-7) as a stepped CV — the classic loading-stripe signal as modulation.
- id: CVOut2
name: Memory probe
type: cv
description: Any RAM address, picked in the Web UI, as 0-5V (byte value 0-255).
- id: AudioOut1
name: Reverb
type: audio
description: Reverb of the beeper + AY mix; wet/dry on Knob X.
- id: AudioOut2
name: AY sound
type: audio
description: The AY-3-8912 output — 128K game audio and .ay/.pt3 music.
# OneBit overrides — the two 1-bit lanes and their density monitors.
- id: PulseOut1
when: { panel: onebit }
name: 1-bit tone
type: pulse
description: The bitbanged 1-bit tone, straight from the beeper engine.
- id: PulseOut2
when: { panel: onebit }
name: 1-bit drum
type: pulse
description: The 1-bit drum lane.
- id: CVOut1
when: { panel: onebit }
name: Tone density
type: cv
description: PCM-density monitor of the tone lane.
- id: CVOut2
when: { panel: onebit }
name: Drum density
type: cv
description: PCM-density monitor of the drum lane.
- id: AudioOut1
when: { panel: onebit }
name: Tone audio
type: audio
description: Tone-density audio output.
- id: AudioOut2
when: { panel: onebit }
name: Drum audio
type: audio
description: Drum-density audio output.
controls:
knobs:
- main:
name: Speed
description: Emulation speed, with a centre deadzone at exactly 100%.
x:
name: Reverb
description: Reverb wet/dry on Audio Out 1.
y:
name: Port 0x5F
description: Readable by the running Z80 program at port 0x5F (IN 95) — a knob the Spectrum itself can see.
- when: { panel: onebit }
main:
name: Engine
description: Engine select x note decay — seven beeper engines.
x:
name: Pitch
description: Root pitch, summed with CV In 1.
y:
name: Interval
description: Voice-2 interval, or per-engine timbre depending on the engine.
# The switch can't be split per panel — the schema allows only name/description
# here, no `when`. So these carry the ZX (default boot) meaning, with a trailing
# asterisk marking each position that does something else in OneBit; the last
# line of `down` explains what the asterisk means. The switch is also what
# selects the mode at boot, so it belongs to both views regardless.
switch:
up:
name: Pause *
description: Pause the emulator — the position for uploading snapshots and editing the mapping from the Web UI. (* OneBit — duophonic voicing, interval on Knob Y.)
middle:
name: Run *
description: Run the machine. (* OneBit — voice 2 follows CV In 2, with Knob Y as timbre.)
down:
name: Keypress *
description: A mappable momentary keypress. HOLD at power-on to boot OneBit instead of the Spectrum. (* OneBit — cycles the drum kit. Positions marked * do something different in OneBit, the alternate boot mode.)
tap:
name: Momentary *
description: A brief tap — a mappable momentary keypress. (* OneBit — cycles the drum kit.)
leds:
- display: list
items:
- id: LED0
name: Mode / status
description: Left column shows the machine mode (128K / 48K / AY); right column shows heartbeat, correct-speed lock, and beeper activity.
- display: list
when: { panel: onebit }
items:
- id: LED0
name: Engine / activity
description: Left column shows the engine number in binary; right column shows note decay, kit selection and drum activity.
host:
notes: |
v1.2.1 — fixes the mapping read-back in the Web UI: longer SysEx replies from
the card could arrive split across several WebMIDI events, and the UI dropped
every fragment after the first, so reading the mapping back showed only a
couple of inputs assigned and the rest blank.
v1.2 — live AY mangling while .ay/.pt3 music plays: per-channel square duty
(PWM the real chip can't do), envelope and noise period bend, and gate-mute,
each with its own depth control. Also moves the Z80-readable input ports to a
decode-safe set (0x23/0x27/0x2B/0x2F/0x33/0x63/0x67) that the ULA, Kempston
and 128K paging latch can never claim. Snapshots and music decode in the
browser and stream to the card, so any size fits with no large buffer on the
RP2040. Hold the momentary switch DOWN at power-on to boot OneBit instead of
the Spectrum.
CREDITS — ComputerCard by Chris Johnson (Music Thing Modular). Z80 core:
superzazu/z80 (Nicolas Allemand, MIT). Sinclair ROMs (c) Amstrad, used by
permission. AY volume curve from measured real-chip data. OneBit's beeper engines
are ports of real ZX-Spectrum routines (Joffa Smith, Shiru, Jason Brooke, Mark
Alexander, utz, via Beepola / Chris Cowley). PT3 player: Pro Tracker 3 / Vortex
Tracker. Built with Claude Code. See README.md for the full credits.