cpcjs
cpcjs is the Amstrad CPC 464 emulator "Roland", written entirely in JavaScript by Antonio Villena. It runs in-browser, rendering the Gate Array picture to an HTML5 canvas, and bundles the CPC 464 firmware so it boots straight to the Locomotive BASIC "Ready" prompt with no downloads. Its Zilog Z80 CPU, paged memory and RAM are plain JavaScript globals, so the whole machine can be stepped and inspected live.
Visit the project on SourceForge ↗
Runs on: Web browser
cpcjs Online Emulator
Play cpcjs using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Amstrad CPC 464 BASIC | cpcjs | Amstrad CPC 464 | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
cpcjs is the pure-JavaScript Roland Amstrad CPC emulator (project EmuScriptoria, GPLv3). It is a set of plain-global scripts with no bundler: vendor them and load them in the project's own order — the Z80 core, then the AY, then the CPC machine, then the 464 specifics, then the video/init unit.
Boot. The core reads a single initialisation blob from the global string emul: a splash prefix, then the 32 KB firmware ROM (OS + Locomotive BASIC) at offset 0x30045, then a 64 KB RAM image. Fetch that blob, publish it as emul, then call init() — it installs the ROM into rom[0]/rom[1], fills RAM, points the Z80 at PC=0 and the machine boots the firmware to the BASIC Ready prompt:
var by= new Uint8Array(arrayBuffer), s= '';
for (var i= 0; i < by.length; i++) s+= String.fromCharCode(by[i]);
window.emul= s; // the core reads emul.charCodeAt(0x30045 + n)
init(); // loads ROM+RAM, resets the Z80, boots to BASIC
Because init() starts its own uncontrollable loop (setInterval(run,20) or a Web-Audio onaudioprocess callback), we clear it immediately and instead call run() ourselves from requestAnimationFrame, which is what lets the debugger pause, single-step and breakpoint the machine.
The machine is plain globals. Everything the debugger needs is a live global — there is no wasm heap to reach into:
| Member | Kind | What it does |
|---|---|---|
g[m[pc>>14&3][pc++&16383]]() | expr | Execute exactly one Z80 instruction: fetch the opcode through the paged bus and dispatch it. The single-step primitive. |
run() | method | Run one whole CPC video frame (scanline-timed, firing the two raster interrupts). |
pc, sp, a, b, c, d, e, h, l | fields | The live Z80 registers. IX = xl|xh<<8, IY = yl|yh<<8; i, r, iff, im too. |
f() / setf(v) | methods | Read / write the flags byte (the core keeps flags lazily in fa,fb,fr,ff). |
m[a>>14&3][a&16383] | field | The banked CPU bus: four 16 KB pages, each ROM or RAM per the Gate Array. |
mw[], rom[0], rom[1] | fields | The raw 64 KB RAM, and the OS (lower) and BASIC (upper) ROMs. |
z80interrupt() | method | Deliver a maskable interrupt (the CRTC fires it every 52 scanlines). |
Debugger integration
The cpcjs-debug.js plug-in reads the live emulator from window.EMU_BOOT and calls EmuKit.defineMachine. Single-stepping is just the opcode dispatch g[m[pc>>14&3][pc++&16383]](); the register set is read straight off the globals and written back through them (flags via f()/setf()); the disassembler is the shared z80 decoder.
Breakpoints and watchpoints. Our loop replicates run()'s scanline schedule but checks the program counter against a breakpoint Set before every instruction, and (for watchpoints) compares the watched RAM cells after each instruction — pausing the moment either fires. Memory reads for the hex/disasm views go through m[] directly, which is side-effect-free on the CPC because all I/O is port-mapped rather than memory-mapped.
function stepOne(){ r++; g[m[pc>>14&3][pc++&16383]](); } // one Z80 instruction
Architecture
Roland is a faithful, readable CPC written entirely in JavaScript. Each chip is plain code driven from one scanline-timed frame loop:
z80pc.js— the Zilog Z80 core: a flat dispatch arrayg[]covering the base page and the CB / ED / DD (IX) / FD (IY) / DDCB / FDCB sub-tables, with CPC instruction timing.jcpc.js— the Gate Array and CRTC 6845:run()clocks the Z80 per scanline and fires the raster interrupt; the Gate Array selects the screen mode, palette and ROM/RAM banking.out464s0.js— the video look-up tables andpaintScreen(), which rasters the CPC's mode 0/1/2 bitmap into the canvas, plusinit().j464.js— the CPC 464: the 8255 PPI ports, the keyboard matrix, and SNA/TAP snapshot handling.ay.js— the AY-3-8912 sound chip (silent until a user gesture resumes Web-Audio).rom[0]/rom[1]— the 464 firmware: the OS ROM and Locomotive BASIC 1.0, installed from the initialisation blob.
The CPU, bus and RAM are ordinary JavaScript objects, so the debugger single-steps by calling the opcode dispatch, reads and writes registers straight off the globals, and implements breakpoints and watchpoints as host-side checks around those calls — with no change to the emulator core.
Sound
Pattern: V-stub (re-enable an emulated chip that was disabled for headless). The CPC's sound is the AY-3-8912 three-voice PSG, already fully emulated in ay.js (aystep() mixes the three tone/noise/envelope channels; aymute() and cstep() advance the tone counters). Roland originally drove it from its own Web-Audio onaudioprocess callback — but that callback also calls run(), so it cannot coexist with our owned, pausable, instruction-steppable run-loop. We neutralise that callback (it is zero-filled in _afterInit) and instead pull the chip's samples from inside our own per-frame step, routing them to the shared EmuAudio sink. So this is V-stub, not V-native: we do not use the core's AudioContext, we feed /debugger/src/audio.js.
Where it hooks. Our frame loop _loop() runs one CPC video frame (run(), or the breakpoint-checked equivalent) and then calls _audioFrame(), which generates that frame's audio and calls EmuAudio.push(). It runs from the same loop the debugger owns, so pausing / stepping / breakpoints stay intact and audio simply stops with the machine.
Sample rate & pitch. In ay.js a channel's square output toggles when its counter reaches the 12-bit period P, so one full cycle is 2*P chip advances and the pitch is advanceRate / (2*P). Each aymute()/cstep() call advances the counters by one. The real CPC clocks the AY at 1 MHz with tone f = clock / (16*P), so to get the PITCH right the chip must be advanced at clock/8 = 125000 steps per second — independent of the audio output rate. We keep exactly that advance rate with a fractional accumulator while emitting exactly Math.round(EmuAudio.sampleRate/60) samples per video frame, so no resampling is needed:
var _per= 125000 / EmuAudio.sampleRate; // AY advances per output sample
_ayFrac+= _per;
var _k= _ayFrac|0; _ayFrac-= _k; // integer advances this sample
for (var _j= 1; _j < _k; _j++) aymute(); // advance without sampling
var _v= aystep(); // final advance -> amplitude 0..1
The chip's output is a unipolar 0..1 level, so a one-pole DC blocker (y = x - x1 + 0.995*y1) centres it before it is scaled to Int16 and written to both stereo channels (the AY output is mono, duplicated L/R).
Mute contract. EMU_BOOT.transport exposes isMuted() and setMute(m), delegated to EmuAudio, which the shell's Sound button drives. It starts muted (browsers block audio before a user gesture); the first real click unmutes and resumes the context. While muted, _audioFrame() skips generation entirely.
Plays when the software makes it. The machine boots to the BASIC Ready prompt and stays there. At the idle prompt the firmware holds every channel at volume 0, so a bare boot is silent until a program writes the PSG registers — correct CPC behaviour, not a missing chip. Sound plays whenever the software drives the AY: type a Locomotive BASIC SOUND and it comes straight out. For example, this holds a ~440 Hz tone (SOUND period 284, since f = 125000 / period) on channel 1 at full volume:
10 SOUND 1,284,100,15
20 GOTO 10
RUN
Caveat. Because the tuning is derived from the chip clock rather than the (variable) output rate, the pitch is correct even at 44.1 kHz or 48 kHz.