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.