C64 READY.

Docs / Datasette (1530 / C2N) — Architecture

Datasette (1530 / C2N) — Architecture

Source: src/datasette.js. Wired into the machine in src/machine.js; see the master overview and the machine orchestrator.

The Commodore 1530 Datasette plays back .tap recordings by replaying their pulse train as edges on the CIA1 FLAG line — exactly the signal a real datasette feeds the C64. The C64 has no tape controller: the KERNAL bit-bangs tape I/O by timing FLAG interrupts, so the emulator only has to deliver correctly-timed edges.

1. Signal path

  .tap pulses ──► Datasette.clock() ──► FLAG falling edge ──► CIA1 FLAG IRQ ──► KERNAL tape reader
                         ▲                                              │
       MOTOR ── CPU $01 bit 5 (low = run)          SENSE ── CPU $01 bit 4 ◄── PLAY button

Three lines connect the deck to the machine:

2. TAP file format

A 20-byte header followed by a stream of pulse-length bytes:

  $00–$0B  "C64-TAPE-RAW" magic
  $0C      version (0, 1, or 2)
  $0D–$0F  reserved
  $10–$13  data size, little-endian
  $14…     pulse data

Each data byte encodes the cycles until the next edge:

loadTap() validates the magic and version (≤ 2), slices out the data by the header size, and pre-computes an estimated duration.

3. Playback engine

State is a cursor into the pulse stream plus a down-counter to the next edge:

clock(cycles) runs each master cycle (only while motorOn && playPressed): it subtracts the elapsed cycles and, whenever the counter reaches zero, emits an edge and loads the next pulse (a while loop, so several short pulses can fire within one tick). Edge shape depends on the TAP version:

4. Motor & the 300 ms startup window

A real 1530 needs roughly 300 ms for the capstan to reach a stable speed; pulses delivered before then would be mis-decoded. MOTOR_STARTUP_CYCLES (≈ 0.30 s × 985248 Hz) models this: when the motor starts — or when PLAY is pressed with the motor already running — a stabilization window is armed, and clock() withholds edges until it elapses (consuming the remainder of the tick that crosses the boundary so no cycles are lost).

5. Transport, SENSE, position & duration

6. Machine integration & clocking

The machine constructs one Datasette, wires flagCallback to CIA1, and clocks it once per master cycle after the CPU step (so a pulse edge sets the CIA1 FLAG data for the next cycle — consistent with the phi1/phi2 ordering in the master cycle). loadTap / setTapePlayPressed / rewindTape / ejectTape are exposed as machine methods driven by the UI transport buttons.

7. Save-state

serialize() / deserialize() capture the transport position and motor/PLAY state (pos, cyclesUntilEdge, motorOn, playPressed, atEnd, FLAG level, counters). The TAP bytes are captured separately as bundled media by the machine, and flagCallback is re-wired on restore — so deserialize() must run after the TAP data has been re-attached via loadTap().

8. Limitations

Playback only — there is no recording, tape write, or fast-forward path, and motor speed is idealised (only the startup window is modelled, not wow/flutter). See Known issues.