Storage Peripherals and Audio Realism
Adding hard drive, floppy, and cassette peripherals with Kansas City Standard encoding, plus a peripheral audio bus for realistic sounds.
Storage peripherals and audio realism
There’s a moment in any emulation project where you have to decide: am I building a terminal, or am I building a machine?
Today I chose machine. I added storage peripherals—floppy drives, hard drives, a cassette deck—with demo pages and E2E tests. And I built out the peripheral audio bus, because a floppy drive that doesn’t make noise isn’t really a floppy drive.
The peripheral audio bus
The old audio path worked, but it was tangled. Sound sources connected directly to the output, volume control was inconsistent, and adding a new peripheral meant threading through three layers of callbacks to figure out where to plug in.
The new architecture is cleaner:
Generators → Buses → MasterChain → Destination
There’s a peripheralBus now—a dedicated audio bus for mechanical sounds. It has an 8kHz lowpass filter (because stepper motors don’t produce high frequencies), connects to the master reverb, and gives all peripherals consistent volume control.
// All peripheral audio routes through one place
this.peripheralBus = new Tone.Gain(0.5).connect(this.masterReverb);
this.peripheralBusFilter = new Tone.Filter(8000, "lowpass")
.connect(this.peripheralBus);
The PeripheralAudioHelper class gives each peripheral the same API: playTone(), playSweep(), playClicks(), startMotor(). The floppy drive uses sweeps for head seeks. The cassette deck uses clicks for transport mechanism sounds. The hard drive uses motor noise and seek chirps. They all route through the same bus, so when you mute peripheral audio from the settings menu, everything goes quiet.
(Later I added a settings toggle for peripheral audio. Some people find mechanical sounds charming; others find them annoying. Now it’s a preference.)
Storage peripherals as first-class objects
The hard drive peripheral isn’t just a UI widget—it’s a proper CHS-addressed storage device with four capacity variants spanning a decade of technology:
- Seagate ST-225 (20MB MFM, 1984) — $300
- Seagate ST-4096 (80MB MFM, 1987) — $400
- Conner CP-3104 (120MB IDE, 1991) — $450
- WD Caviar 2540 (540MB EIDE, 1995) — $500
Each has its own geometry, interface type, and timing characteristics. Format operations take longer on bigger drives. Seek sounds vary based on capacity. The goal is that switching drives in the peripheral shop actually feels different.
The cassette peripheral is even more interesting. It implements the Kansas City Standard—FSK encoding at 2400Hz for ones and 1200Hz for zeros, the same scheme that Altair and TRS-80 users used to save programs to audio cassette in the late ’70s.
export interface TapeImage {
format: string;
label: string;
tapeLength: number;
currentPosition: number;
baudRate: BaudRate;
programs: TapeProgram[];
}
// Simulates a Kansas City Standard cassette data recorder
export class CassettePeripheral extends PeripheralInterface {
private transportState: TransportState = 'stopped';
private fskEncoder: FSKEncoder;
}
The transport state is real: PLAY, STOP, REW, FFW, REC. The tape position advances and rewinds. You can load a program from “tape” and it behaves like loading from tape should—with motor sounds and position tracking and the faint anxiety of hoping the data didn’t get corrupted.
(I wrote a deeper piece on the KCS cassette implementation if you want the technical details.)
Dial-up drama
I also expanded the wrong-number scenarios. Real dial-up wasn’t just “connect to BBS, do stuff.” It was a landscape of busy signals, disconnected numbers, confused humans, and fax machine screams.
The new announcement system has 120 voice variations across six categories:
- not-in-service: Professional operator recordings
- person-confused: Genuinely puzzled responses
- person-annoyed: Frustrated, impatient reactions
- person-polite: Understanding, gentle responses
- person-curious: Inquisitive, investigative tones
- person-casual: Relaxed, laid-back responses
Each category has 20 MP3 files generated with Bark TTS, then filtered through a phone-line bandpass (300-3400Hz). When you dial a wrong number, you might get a busy signal, or a “this number has been disconnected” recording, or a real person asking who you’re trying to reach.
Later in the week I added IVR systems too—automated phone trees for fake businesses. But that’s another story.
Z-Machine progress
The Z-Machine work continued in the background. I expanded the opcode support for V5 compatibility and added a WASM API that plays better with the web integration:
// WASM-friendly interface: run until we need input
z_result_t z_run_until_input(z_machine_t *zm);
z_result_t z_provide_input(z_machine_t *zm, const char *input);
The model is “run until you need something from the user, then pause.” This makes it much easier to integrate with the terminal interface—the Z-Machine runs, produces output, hits an input instruction, and yields control back to the browser event loop.
Also added stack inspection functions for debugging. When you’re tracking down why Zork crashes, being able to dump the call stack is invaluable.
What changed
- Added hard drive, floppy, and cassette peripherals with demo pages
- Built peripheral audio bus with centralized volume control
- Created PeripheralAudioHelper class for consistent mechanical sounds
- Expanded wrong-number scenarios with 120 voice variations
- Grew Z-Machine WASM API and opcode support
What I was going for
A machine you can hear. Floppy drives that whir and click. Cassette decks with transport mechanism sounds. A phone network that occasionally connects you to confused strangers or automated phone trees.
What went sideways
A Z-Machine jump offset bug showed up—off-by-one errors in branch instructions are particularly annoying because the symptoms are “the game acts weird sometimes.” Also discovered that async audio initialization in Lit components needed careful handling; the audio context has to be ready before the peripheral tries to use it.
What’s next
The system was starting to feel tangible. Next was proving it with tests and polish—CC-40 completion and the IVR phone system launch.
Previous: 2026-01-28