A pure-JS SPC700 + S-DSP emulator and Web Audio player. No WebAssembly. No build step. No dependencies.
- SPC700 CPU + S-DSP emulation. The same sound hardware that powered the Super Famicom / SNES, running entirely in JavaScript.
- Zero-config link binding. Any
<a href="song.spc">on your page becomes a play button automatically. - High-quality resampling. The DSP's native 32 kHz output is converted to your device's sample rate with 4-point Catmull-Rom cubic interpolation, keeping playback smooth and free of aliasing artifacts.
- Click-free start and stop. A raised-cosine fade envelope (about 50 ms) eliminates pops when playback begins or ends.
- Tiny API surface.
load(),loadUrl(),play(),stop(). That is all you need. - Flexible audio routing. Pass your own
AudioContextand destination node to plug the player into an existing Web Audio graph (analysers, gain nodes, effects). - Works everywhere. Exposes itself as
window.SPCPlayerin the browser and throughmodule.exportsin CommonJS environments.
Include the script, then write links:
<script src="libspc.js"></script>
<a href="music/super-mario-world-overworld.spc">Play: Overworld Theme</a>
<a href="music/zelda-lost-woods.spc">Play: Lost Woods</a>Clicking a link downloads, parses, and plays the SPC file immediately. The default click handler matches:
| Selector | Matches |
|---|---|
a[href$=".spc"] |
Links ending in .spc |
a[href*=".spc?"] |
Links with query strings (.spc?v=2) |
a[data-spc] |
Any link explicitly opted in |
Use data-spc for URLs that do not end in .spc, such as an API endpoint or CDN route that serves SPC data:
<a href="/api/track/42/download" data-spc>Play track 42</a>const player = new SPCPlayer();
// Download, load, and play in one call
const meta = await player.loadUrl('music/chrono-trigger-corridors-of-time.spc');
console.log(meta);
// Stop with a smooth fade-out
player.stop();Useful for drag-and-drop, file inputs, or bundled assets:
input.addEventListener('change', async (e) => {
const buffer = await e.target.files[0].arrayBuffer();
const player = SPCPlayer.getInstance();
const meta = player.load(buffer); // parse and reset emulator state
player.play();
});Creates a new player instance.
| Parameter | Type | Default | Description |
|---|---|---|---|
audioCtx |
AudioContext |
A new AudioContext |
Reuse an existing context. |
destination |
AudioNode |
audioCtx.destination |
Where the player's output is connected. |
// Route SPC audio through your own gain and analyser chain
const ctx = new AudioContext();
const gain = ctx.createGain();
const analyser = ctx.createAnalyser();
gain.connect(analyser).connect(ctx.destination);
const player = new SPCPlayer(ctx, gain);Parses an SPC file and resets the emulator. Does not start playback.
buffer:ArrayBufferorUint8Arraycontaining the.spcfile.- Returns: the parsed metadata object, also stored in
player.currentMeta.
Fetches an SPC file over HTTP(S), loads it, and starts playback automatically.
- Throws if the request fails (
!response.ok). - Subject to standard browser CORS rules. Cross-origin hosts must send
Access-Control-Allow-Origin.
Starts or resumes playback with a smooth fade-in. Automatically resumes a suspended AudioContext, so it is safe to call from a click handler to satisfy browser autoplay policies.
Fades out over roughly 50 ms, then disconnects the audio node to free up CPU.
Returns a lazily created shared singleton. Use it when you only need one player on the page. Link binding uses it internally, so a new click always replaces whatever is currently playing.
Attaches a single delegated click listener to document that turns matching links into SPC play buttons.
// Custom selector: only links inside a playlist container
SPCPlayer.bindLinks('#playlist a');bindLinks() is called automatically with the default selector once the DOM is ready. Call it manually only if you want to customize the selector.
| Property | Type | Description |
|---|---|---|
player.playing |
boolean |
true while audio is being generated. |
player.currentMeta |
object |
Metadata of the last loaded SPC (title, game, etc.). |
player.engine |
SPCEngine |
Direct access to the underlying emulator core. |
+-----------+ +--------------+ +--------------+ +--------------+
| .spc | -> | parseSPC() | -> | SPCEngine | -> | 32 kHz PCM |
| file | | RAM / regs | | SPC700 CPU | | stereo |
| | | DSP state | | + S-DSP | | samples |
+-----------+ +--------------+ +--------------+ +------+-------+
|
+------------------------v-------+
| Cubic (Catmull-Rom) resampler |
| 32 kHz -> device sample rate |
+------------------------+-------+
|
+------------------------v-------+
| Raised-cosine fade envelope |
| -> ScriptProcessorNode |
| -> speakers |
+--------------------------------+
parseSPCreads the.spcsnapshot: 64 KB of APU RAM, CPU registers, DSP registers, and ID666 metadata.SPCEnginerestores that state and runs the SPC700 CPU alongside the S-DSP, producing one stereo sample at a time at the native 32,000 Hz.- A 4-tap cubic interpolator resamples the stream on the fly to match
AudioContext.sampleRate(44.1 kHz, 48 kHz, and so on). - A cosine-shaped gain ramp smooths every start and stop.
- Audio is delivered through a
ScriptProcessorNodein 8192-sample blocks.
Any modern browser with the Web Audio API.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| Yes | Yes | Yes | Yes |
Safari's prefixed webkitAudioContext is handled automatically.
Autoplay policy: browsers require a user gesture before audio can start. Link-click binding satisfies this by design. If you call play() on page load, it stays silent until the user interacts with the page.
libspc.js SPCPlayer class and link-binding glue
(requires SPCEngine, SPC700, DSP and parseSPC to be
loaded or bundled in the same scope)
SPCPlayer is the playback front-end. It depends on the emulator core (SPC700, DSP, SPCEngine) and the parseSPC loader, so make sure these are included before SPCPlayer is instantiated.
- Retro game music jukeboxes and playlists
- Game preservation and VGM archive sites
- Web games that want authentic SNES audio without re-encoding to MP3 or OGG
- Visualizers, by routing output through an
AnalyserNodevia thedestinationparameter - Chiptune and emulation research and education
- Uses
ScriptProcessorNodefor maximum compatibility. It runs on the main thread, so very heavy pages may glitch. Migrating toAudioWorkletis a natural next step. - Only one track plays per player instance. Loading a new file replaces the current one.
- SPC files contain copyrighted game music. Make sure you have the right to host and distribute any tracks you serve.
-
AudioWorkletbackend - Volume, per-voice mute and solo controls
- Pause and seek support
- Track length, loop, and fade-out handling from ID666 tags
- Bundled TypeScript typings
Issues and pull requests are welcome. If you are fixing an accuracy bug in the CPU or DSP, including the game and track that reproduces it makes review much faster.
Released under the MIT License. See LICENSE for details.
Built for the golden age of 16-bit audio.