Firmware

fm1-sid

Rising79/100

C64 music on the M-VAVE FM-1: a cycle-exact 6510, reSID, GoatTracker songs and editor; runs as an fm1-dock module

fm1-sid is custom firmware for the M-VAVE FM-1, by @joakimeriksson. Open source under GPL-3.0, latest release v0.3 (October 2026), 1 star on GitHub.

forM-VAVE FM-1·by@joakimeriksson

Unclaimed. Is this yours? Sign in with GitHub as @joakimeriksson to claim it.

Downloads
0
Star
1
Forks
0
Commits
49
Last commit
Download v0.3

fm1-sid-0.3.fwsc· 596 KB· +1 file

GPL-3.0· joakimeriksson/fm1-sid·downloads come from the creator’s GitHub releases

43
Commits
1
Releases
43 commits and 1 release in the last year
Activity · last 12 months

fm1-sid — a SID synth for the M-VAVE FM-1?

A feasibility study, with a working prototype: can the M-VAVE FM-1 pocket synth (JieLi AC791N, pi32v2 CPU at 240 MHz, see the emulator) be turned into a synth built on the MOS 6581 / 8580 SID, the Commodore 64's sound chip?

Just want to use it?

  • Put it on your FM-1 from the browser (Chrome, Edge), over USB: on fm1-dock's page, install fm1-dock, then Add FM-1 SID to my fm1-dock.
  • Try it first in the browser, the whole FM-1 emulated: the FM-1 SID page.
  • Write your own chiptune on the FM-1 itself, with its GoatTracker 2 editor: your first chiptune, step by step.
  • Everything else is in the user guide. The rest of this file is how it was built and measured.

Where it lives: this repository is also fm1-emu's fm1-sid/ (a submodule): its web page and some builds use the emulator next to it (../web, ../target, ../tools). Clone fm1-emu with --recurse-submodules for all of it; the host tests (make test) and the fm1-dock module (dock/build.sh) build from this repository alone. Licences: LICENSING.md.

On a real FM-1 it runs on fm1-dock, which owns USB, the installers and the rescue: as FM-1 SID's own firmware (web/sidplayer.fwsc, dock/build_solo.sh: fm1-dock with FM-1 SID and the sine, starting in FM-1 SID), or as a module deployed into an fm1-dock with other synths (dock/build.sh). PSID and RSID tunes, GoatTracker songs, the editor, songs saved in fm1-dock's flash banks, subtunes on ALGORITHM. Any .sid can go to a real FM-1 over USB without an install: "Play on my FM-1" on the page, or fm1-dock's tools/dock.py send tune.sid [--arg SONG], writes it into FM-1 SID's C64 memory as it arrives and plays it (nothing in flash; dock/send_test.py checks it in fm1-emu). It has played on a real FM-1 as a module (Arkanoid at full speed, 2026-10-24), and as its own firmware (installed, USB answering as it plays, the heaviest audio block at 34 %, no overruns, 2026-10-25). The standalone builds (Felucca's and Dinghy's) are not for a real FM-1: their USB replies never reach a computer.

Summary

Yes, it can be done. Two things were studied:

  1. An own SID model (src/sid.c, integer C) plus a synth layer that plays the chip the way C64 music does (src/sidsynth.c: 50 Hz frames, waveform tables, PWM and filter sweeps, sync / ring, hard restart). ~320 instructions per sample and chip on the FM-1, ~6 % of a core.
  2. A comparison with the ready-made engines reSID 0.16, reSID 1.0 and reSIDfp: the same register stream through all of them, the cost measured on the emulated FM-1, the sound level-matched for listening.

Conclusion: reSID 0.16 in "fast" mode is the right chip engine for the FM-1. It costs 440 instructions per sample (8 % of a core per chip), sounds bit for bit the same on the FM-1 as on the computer, and has waveform tables and filter curves measured on real chips. The own model is cheaper but worse. reSID's high-quality modes (resampling, alias-free) clock the chip cycle by cycle and cost 1-7 cores; reSID 1.0 and reSIDfp do not fit the FM-1 at all (8-21 MB of filter tables per chip model, against 1 MB of flash in total). The synth layer (sidsynth) is what is new here, and it stays on top of reSID. On the way, four bugs were found in the FM-1 emulator (64-bit carry, flags, multiply-accumulate and push / pop masks) that made all double arithmetic wrong; the fixes are in PR #31.

There is also a SID player firmware (firmware/): real C64 tunes (PSID and RSID) run their own 6502 code on the FM-1's CPU, GoatTracker 2 songs (.sng) play by GoatTracker's own playroutine straight from the file, the sound comes from reSID 0.16 (or the own model), and the LCD shows the three voices, sync / ring and the filter. It runs in fm1-emu and as a web page (web/, GitHub Pages) where the whole FM-1 is emulated and all of HVSC can be searched and played; the tunes are fetched straight from hvsc.c64.org. The sound matches reSID on the computer. As a synth (which its update package starts as) it has a step sequencer and stereo effects after Felucca's: chorus, delay and reverb.

New: RSID tunes (Arkanoid, Great Giana Sisters, Game Over and others) run on a whole emulated C64, GoatTracker 2 songs play exactly as in GoatTracker (the same SID registers, frame for frame), and there is a GoatTracker editor on the FM-1 itself (SEQ in the player): patterns, order lists, instruments, tables and the song's name, edited with the FM-1's knobs and keys (the keys C4-D#5 type hex 0-F) while the song plays. Songs are saved in 16 flash slots and show up in the player's list. How to use it is in the user guide.

On real FM-1s: FM-1 SID plays as an fm1-dock module (release 0.3): .sid tunes, RSID tunes, GoatTracker songs, the editor and its saves, and tunes sent from the browser. The first build, a firmware of its own on Felucca's platform, did not play on the first real FM-1 it was tried on (2026-10-24); that is why it runs on fm1-dock, which owns USB, the installers and the rescue.

User guide: FM-1 SID

FM-1 SID turns the M-VAVE FM-1 into a Commodore 64. It plays C64 music (.sid files) and GoatTracker 2 songs (.sng) through an emulated SID chip, and it lets you write your own GoatTracker songs on the FM-1 itself and save them. It is also a SID synth with a step sequencer and effects.

Installing firmware is at your own risk. The way back to any other firmware, or to M-VAVE's own, stays open: fm1-dock answers the FM-1 Firmware Hub's updater, and the FM-1 has its own recovery (below).

Try it without an FM-1

  • In the browser: joakimeriksson.github.io/fm1-emu/fm1-sid runs the whole FM-1 in the page, with FM-1 SID's own firmware (fm1-dock with FM-1 SID). Pick a tune, search all of HVSC, or drop your own .sid files and GoatTracker .sng songs on the page.

  • On a Mac with this repository: build the emulator (cargo build --release in the repository's root) and open a package in its window:

    target/release/fm1-live --fwsc fm1-sid/web/sidplayer.fwsc --flash my_fm1_flash.bin
    

    --flash keeps the FM-1's flash in a file, so the songs you save are still there the next time. Esc quits. The computer keyboard plays the FM-1 (the keys).

Install it on an FM-1: as an fm1-dock module

In Chrome or Edge, with the FM-1 plugged in by USB, on fm1-dock's page:

  1. Install fm1-dock on my FM-1 (skip it if fm1-dock 0.6 or later is on it already).
  2. Add FM-1 SID to my fm1-dock: the module goes into fm1-dock's flash next to its other synths, with GoatTracker 2's example songs. Nothing else on the FM-1 changes.
  3. On the FM-1, hold HOME for 1.5 s and pick FM-1 SID. fm1-dock starts it at power-on from then on.

To hear the classics right away, the same page has ▶ Arkanoid, ▶ Wizball and ▶ Commando, and on the FM-1 SID page Play on my FM-1 sends any of HVSC's tunes to it (into its memory: nothing is written to flash).

By hand, in fm1-dock's tree: uv run --with python-rtmidi tools/dock.py deploy fm1sid.dock, or drop the file on the console. fm1sid.dock is on the page, or built with your own tunes (below).

FM-1 SID as its own firmware

FM-1 SID can also be the whole firmware: fm1-dock with FM-1 SID as its only synth (and the sine), starting in it.

  1. Install FM-1 SID on my FM-1 on the FM-1 SID page or the install page; or fm1-sid-X.Y.fwsc from the latest release with fm1-dock's tools/fm1_install.py; or build one with your own tunes (below).
  2. To install something else later, run its installer, or the Firmware Hub: fm1-dock answers them. If one does not reach it, switch the FM-1 on with OCT− held: fm1-dock's USB rescue, only the updater running (fm1-dock's keys).
  3. If nothing else works, the FM-1 has its own recovery, outside any firmware: hold OCT− and OCT+ while switching it on, with the USB cable plugged in, and use M-flasher, the Firmware Hub or MvaveFM1Unbricker with the official firmware (issues #22 and #23). FM-1 SID never writes the flash this needs.

The package from the page starts as the SID synth: it is silent until you play a key. Press HOME for the tune player.

The tune player

The screen shows the tune's name and author, the tune number, the time, the chip (6581 or 8580) and the CPU load; under that the three SID voices (waveform, gate, sync / ring, filter, note, envelope, pulse width), the filter and an oscilloscope.

ControlWhat it does
PRESETS or SELECT (turn), OCT− / OCT+previous / next tune
ALGORITHM (turn)the tune's subtune (a SID or GoatTracker song with several: "Song 2/11")
PLAYpause / play
EDITthe SID engine: reSID 0.16 (default) or fm1-sid's own model
FXthe effects page (below); FX again goes back
HOMEthe SID synth (and back); held 5 s: USB mode (for installers)
MASTERthe volume
SEQthe GoatTracker editor on the song that plays (below)

The effects page (FX): KNOB1 chorus, KNOB2 delay, KNOB3 the delay time (1/16, 1/8, dotted 1/8, 1/4), KNOB4 reverb, SELECT the delay's feedback, PRESETS the chip (6581 / 8580). The player starts dry (the tunes were mixed for the bare chip); turn the knobs up for space.

What plays:

  • .sid tunes, PSID and RSID: almost all of the High Voltage SID Collection. RSID tunes (Arkanoid, Great Giana Sisters, Game Over, ...) run on a whole emulated C64. Not: the few tunes written in C64 BASIC, and tunes for two or three SID chips.
  • GoatTracker 2 songs (.sng), played by GoatTracker's own playroutine, exactly as GoatTracker plays them. GoatTracker's release has example songs in its examples folder; make your own in GoatTracker, sid-tracker or on the FM-1. Songs made for 2x speed play at normal speed (the speed setting is not in a .sng).
  • Your saved songs: the songs you save in the editor come after the package's tunes.

USB-MIDI

Plugged into a computer, the FM-1 shows up as a USB-MIDI device called FM-1 SID (class compliant: no driver on macOS, Windows, Linux, iOS). A USB in green on the player's top line says a computer has it.

  • Notes play the SID synth (in the synth mode: the polyphonic synth, or with the sequencer's page up the selected track; with REC on they are recorded as the keys are) and the editor (test notes on the cursor's channel; on PATTERNS with REC on they are written, as the keys write). A MIDI note plays at its own pitch, whatever the octave.
  • Program change N picks tune N+1 in the player.
  • Installers find it and install through it (Felucca's update code: their handshake and upgrade command F0 22 24 35 7F F7), the web installer and Felucca's tools/fm1_install.py.
  • The chip's update mode: Felucca's "soft key" SysEx F0 22 24 35 7D F7 restarts the FM-1 into it, as holding OCT− and OCT+ does. From Python with mido: python3 -c "import mido; mido.open_output('FM-1 SID').send(mido.Message('sysex', data=[0x22, 0x24, 0x35, 0x7D]))" (the port's name can differ a little, e.g. FM-1 SID MIDI 1: mido.get_output_names()).

Not yet: songs in and out over SysEx, USB audio, MIDI out, and the TRS MIDI jack. The USB code is Felucca's own driver and update code, which run on real FM-1s; in FM-1 SID they work in the emulator (fm1-live --usb), but on the one real FM-1 tried so far the installers got no answer (see above): if that happens, hold HOME 5 s for USB mode.

Your own tunes on the FM-1

To carry your own .sid tunes and .sng songs in flash (so PRESETS steps through them), build FM-1 SID with them. With this repository and fm1-dock next to it (FM1_DOCK), after tools/fetch_ext.sh (see Ways to build it and dock/):

dock/build.sh Commando.sid Arkanoid.sid:1 Dojo.sng          # -> build/dock/fm1sid.dock: the module
dock/build_solo.sh Commando.sid Wizball.sid:7 Dojo.sng      # -> build/solo/fm1-sid.fwsc: its own firmware
  • :N picks the song in a .sid (from 1), or the subtune in a .sng (from 1). Without it: the tune's own start song, the song's first subtune. List a file twice for two of its songs.
  • About 64 KiB of tunes fit in the module beside its code (a .sid is 2-20 KB, a GoatTracker song 1-20 KB); its own firmware takes more. Each file is checked first; one the FM-1 would not play is named, with the reason.

Deploy the module or install the package as above. A tune you only want to hear needs none of this: Play on my FM-1 on the FM-1 SID page sends it. Songs you write in the editor are saved on the FM-1 itself.

The SID synth

HOME switches between the player and the synth. The 27 keys play 12 SID instruments. PRESETS / SELECT: the instrument; OCT− / OCT+: the octave; KNOB1-4: cutoff, resonance, attack, release. SEQ opens the step sequencer: three tracks of 16 steps, PLAY runs it, REC records (running: onto the nearest step; stopped: at the cursor, which SELECT moves), SCL / ENV / LFO pick track 1 / 2 / 3, PRESETS its instrument (track 3 starts as a drum kit), ALGORITHM the tempo, GLO clears a step, ARP a track.

The GoatTracker editor

A GoatTracker song is built from four things:

  • Patterns: rows of notes, one pattern per channel at a time. A row has a note, an instrument number and a command with its data. Patterns are 1-128 rows long.
  • Order lists: one per channel (the SID has three), the patterns in the order they play, with repeats and transposes. The list ends by jumping back to a restart position. A song can have up to 32 subtunes, each with its own three order lists.
  • Instruments: an envelope (attack / decay, sustain / release) and where in the tables the sound starts.
  • Tables: the wave table makes the sound (the waveform per frame, arpeggios), the pulse table sweeps the pulse width, the filter table the filter, and the speed table holds the vibrato and portamento speeds.

GoatTracker's own manual (readme.txt in its release) explains all of it in depth; the editor here edits the same song, and it plays exactly as in GoatTracker.

New to trackers? Start with your first chiptune, step by step: a bass, chords and drums from an empty song in about 15 minutes. The pages below are the reference.

Open it: press SEQ in the player. If the tune playing is a GoatTracker song, it opens in the editor; if it is a .sid tune, you get a new, empty song. HOME leaves the editor (press it twice if there are changes you have not saved: they are lost when you leave).

Controls on every page

ControlWhat it does
SEQnext page: PATTERNS → ORDERS → INSTRUMENT → TABLES → SONG
RECedit on / off. Off, nothing changes and the keys just play notes. On, the red REC shows
PRESETS (turn)move up / down
SELECT (turn)move left / right
ALGORITHM (turn)change the value under the cursor (with REC on)
note keysplay notes; with REC on, write them (patterns) or type hex digits (below)
PLAYplay / stop
OCT− / OCT+the octave of the keys
KNOB1 (turn)the instrument the keys play and write
LFOmute / unmute the cursor's channel
SAVEsave the song (into its slot, or the first free one)
HOMEleave the editor
FX, EDITthe effects page, the SID engine, as in the player

Typing hex digits: with REC on and the cursor on a hex number, the 16 keys from C4 (middle C) to D#5 type 0 to F, in order, black keys included:

KeyC4C#4D4D#4E4F4F#4G4G#4A4A#4B4C5C#5D5D#5
Digit0123456789ABCDEF
Emulator keyAWSEDFTGYHUJKOLP

The top line shows the page, the instrument (I01), the octave (O4), the step (S1, on the patterns page), REC, PLY while it plays, and * when there are unsaved changes. The bottom line says which keys do what on the page, and shows messages.

PATTERNS

The three channels side by side, with each channel's pattern number and length above it (P05:40: pattern 05, 40 hex = 64 rows). A row reads C-4 01 305: note C-4, instrument 01, command 3 with data 05. ... is an empty note, --- a key off.

ControlWhat it does
note key (REC on, cursor on a note)write the note with the current instrument, move on by the step
hex keys (REC on, cursor on a number)type the instrument (2 digits), the command (1) and its data (2)
GLOclear the note (or the instrument, or the command and data)
ARPkey off (the note is released)
SCL / ENVinsert / delete a row
KNOB2which pattern the cursor's channel shows
KNOB3the pattern's length
KNOB4the step: how many rows a note moves the cursor (0-16)
PLAYplay the three patterns over and over, from their first row

While it plays, the cursor follows the music (up / down does nothing then; stop with PLAY). You can still write: with REC on, a key writes at the row that plays.

The commands (data in hex):

CommandCommand
1portamento up (speed table entry)9pulse table position
2portamento down (speed table entry)Afilter table position
3slide to the note (speed table entry; 00 at once)Bfilter control (resonance and channels; 00 filter off)
4vibrato (speed table entry)Cfilter cutoff
5attack / decayDmaster volume (00-0F)
6sustain / releaseEfunk tempo (speed table entry: two alternating tempos)
7waveformFtempo (03-7F, about the frames per row; 80 added: this channel only)
8wave table position0nothing

ORDERS

The subtune's three order lists, one column per channel, the position numbers on the left. An entry is a pattern number (00-CF), a repeat (D0-DF: the next pattern 1-16 more times) or a transpose (E0-EF down 16-1 semitones, F0-FE up 0-14). After the last entry, Rxx is the restart: where the list jumps back to.

ControlWhat it does
hex keys (REC on)type an entry, two digits; on the R line, the restart (or a new entry at the end)
SCL / ENVinsert (a copy of the entry) / delete an entry
GLOthe restart here
KNOB2the subtune (00-1F; a new one starts empty: give each of the three channels an entry with SCL, then it plays)
PLAYplay from the cursor's position

Moving the cursor here also changes the patterns the PATTERNS page shows: those at this position.

INSTRUMENT

KNOB1 picks the instrument (01-3F). Its fields, in hex:

FieldWhat it is
Attack/Decay, Sustain/Rel.the SID envelope: one hex digit each (attack, decay, sustain level, release)
Wavetablewhere the sound starts in the wave table (its row; 00: none, then the note gets no pitch)
Pulsetable, Filtertablewhere its pulse sweep / filter program starts (00: none)
Vibrato par.a speed table entry for the vibrato (00: none)
Vibrato del.frames before the vibrato starts
HR/Gatetimerframes before the next note that the gate goes off (hard restart); 80 added: no hard restart, 40 added: no gate off
1st wavethe waveform of a note's first frame (09: the usual hard restart; 00: none)
Name16 letters: SELECT moves along it, ALGORITHM changes the letter

With REC on, two hex digits set a field and move to the next. With REC off the keys play the instrument, so you hear each change.

TABLES

The four tables side by side, rows 01-FF, each row two hex numbers (left, right). SELECT moves along the digits, typing moves on by itself, and after the last digit of a row to the next row. Rows a playing channel is on show green.

TableLeftRight
WAVEthe waveform: 11 triangle, 21 sawtooth, 41 pulse, 81 noise (these with the gate on); 01-0F wait that many frames; FF jumpthe note: 00-7F up from the played note, 80 the same, 81-DF a fixed note; after FF: the row to jump to (00: stop)
PULSE8x-Fx: set the width to xyy; 01-7F: sweep for that many frames; FF jumpthe low byte (set), or the speed (sweep)
FILT90 / A0 / C0: filter type low / band / high pass; 00: set the cutoff; 01-7F: sweep; FF jumpresonance and channels (type), the cutoff, or the speed
SPEEDvibrato: depth; portamento: high bytevibrato: speed; portamento: low byte
ControlWhat it does
SCL / ENVinsert / delete a row (instruments, commands and jumps that point past it follow, as in GoatTracker)
GLOclear the row

SONG

The song's name, author and year (SELECT moves along, ALGORITHM changes the letter, with REC on), its size, the editor's free memory, and the 16 flash slots with what is in each (> marks the song's own slot).

ControlWhat it does (on a slot)
SAVEsave the song into this slot (press twice to save over another song)
GLOload the song in this slot (twice if there are unsaved changes)
ARPdelete the slot (twice)
SCLa new, empty song (twice)

Saving: SAVE on any other page saves into the song's own slot, or the first free one. The sound stops for a moment while it is written. Saved songs stay when the FM-1 is off and appear in the player after the package's tunes, where SEQ opens them again. The 16 slots share about 290 KB of flash. Installing a package again can wipe the saved songs, and there is no way yet to copy them to a computer.

Your first chiptune, step by step

A loop with the classic C64 sound: a pulsing bass, arpeggio chords (the chord played one note per frame, so fast it sounds like one) and drums made of noise. About 15 minutes, all on the FM-1; every step below was played key by key on the emulated FM-1 and the bar recorded. It is one bar, and step 6 says where to take it from there.

What you type are hex digits, with REC on: the 16 keys from C4 to D#5 type 0-F (the table above). "Turn PRESETS" moves the cursor up or down, "turn SELECT" left or right, one row or one digit a click.

1. A new song. In the player press SEQ: the editor opens the song playing. Press SEQ four more times, to the SONG page, and SCL twice: a new, empty song.

2. Six instruments. Press SEQ three times, to INSTRUMENT, and REC (it lights red). Instrument 01 is shown, the cursor on Attack/Decay. Type its digits: each pair fills a field and moves to the next.

InstrumentTypeWhat it sets
01 Bass08 48 01 01Attack/Decay 08, Sustain/Rel. 48, Wavetable 01, Pulsetable 01
02 Lead09 A8 01 01 00 01 08the same sound, held longer, with vibrato: Filtertable 00, Vibrato par. 01, Vibrato del. 08
03 Chords09 A8 04 01Wavetable 04: the chord
04 Kick00 88 09 00Wavetable 09
05 Snare00 9A 0E 00Wavetable 0E
06 Hat00 56 13 00Wavetable 13

Before each next one: turn KNOB1 one click to the next instrument, then turn PRESETS back up until the cursor is on Attack/Decay again (it stays on the field it was on).

3. The sounds: the tables. Press SEQ: TABLES, the cursor on WAVE row 01. Type these rows, four digits each; after the fourth digit the cursor goes on to the next row by itself.

WAVE rowsTypeWhat it is
01-0341 00 0F 80 FF 02a pulse wave at the played note, held (bass and lead)
04-0841 00 41 03 41 07 41 0C FF 04a minor chord: the note, +3, +7, +12 semitones, a frame each, round and round
09-0D81 A8 11 9F 11 9A 11 96 FF 00kick: a click of noise, then a triangle falling in pitch
0E-1281 BE 81 BA 81 B6 81 B2 FF 00snare: four frames of noise
13-1581 D4 81 D0 FF 00hi-hat: two frames of high noise

Then turn PRESETS back up to row 01 and SELECT four clicks right, into PULSE, and type 87 40 30 08 30 F8 FF 02: the pulse width set, then swept up and down for ever (the classic C64 "PWM" sound of the bass). Turn PRESETS back up to row 01 again and SELECT eight more clicks right, into SPEED, and type 08 18: the lead's vibrato.

4. The notes. Press SEQ twice: PATTERNS, the cursor on channel 1, row 00. The three columns are the SID's three channels: bass, chords and drums.

  • Turn KNOB3 down until the top says P00:10 (16 rows: one bar), KNOB4 to S2 (a note every other row), KNOB1 to I01, and press OCT− once (O3).
  • The bass: play A3, A4, A3, A4, D4, D5, D4, D5. Each key writes its note an octave down (A-2, A-3 ... D-4) with instrument 01 and moves two rows; after the eighth the cursor is back on row 00.
  • The tempo: turn SELECT three clicks right, onto the command of row 00, and type F 0 6 (command F, tempo 06: six frames a row, about 125 BPM). A new song has no tempo of its own.
  • The chords: the cursor has moved down to row 02: turn PRESETS back up to row 00 and SELECT three clicks right, onto channel 2. KNOB3 down to P01:10, KNOB4 to S8, KNOB1 to I03, and play A4 and D4: an A minor chord on row 00, D minor on row 08.
  • The drums: turn SELECT six clicks right, onto channel 3. KNOB3 down to P02:10, KNOB4 to S4. Then KNOB1 to the drum and any key (C4) for each: I04 (kick, row 00), I06 (hat, row 04), I05 (snare, row 08), I06 (hat, row 0C).

5. Play it, save it. PLAY: the bar loops, the cursor running with it (about two seconds a bar). PLAY again stops. SAVE: the bottom line says saved in slot 0 (the first free one of 16), and the SONG page lists it. To name it: on the SONG page, with REC on, ALGORITHM changes the letter under the cursor and SELECT moves along the name; then SEQ to another page and SAVE again (press it on PATTERNS, say: on the SONG page it works on the slot list). HOME leaves the editor: your song is now in the player's list (PRESETS steps to it), and SEQ there opens it again.

6. Make it yours.

  • A melody: channel 2 with instrument 02 (Lead) instead of the chords, or a new pattern for it: KNOB2 on PATTERNS shows another pattern number on the cursor's channel, and on ORDERS you say which plays when.
  • More bars: on ORDERS, SCL inserts entries. A transpose before a pattern plays it higher: F2 then 00 plays the bass two semitones up, the same pattern.
  • A major chord: wave rows 41 00 41 04 41 07 41 0C and an FF back to the first of them, in rows of their own (for example from 16), and an instrument whose Wavetable points there.
  • The filter: the commands B (filter on, which channels), C (cutoff) and A (a filter table program) in the patterns: a bass with a sweeping filter is the next classic sound.
  • Learn from the masters: the songs in the player (Dojo, Unleash, Sanction and the others, from GoatTracker 2's examples) open in the editor with SEQ like your own: see how their tables make their sounds.
  • On a computer: GoatTracker 2 itself, or sid-tracker in the browser, writes the same .sng files; build them into FM-1 SID (your own tunes) and they play as they do there.

The emulator's keyboard

Emulator keyFM-1 control
A W S E D F T G Y H U J K O L P ; 'the note keys C4 to F5 (and the hex digits 0-F, A to P)
Z / XOCT− / OCT+
← / →SELECT (in the editor: left / right)
↑ / ↓PRESETS (in the editor: up / down)
, / .ALGORITHM
SpacePLAY
RREC
1 2 3 4 5 6 7 8 9 0FX, SCL, ENV, LFO, EDIT, GLO, HOME, SAVE, ARP, SEQ
mouse (drag or scroll)the knobs, KNOB1-4 among them; click a button or key
Escquit (with --flash, the flash is saved to its file)

When something is not right

  • A note makes no sound: its instrument needs a wave table position, and the wave table a waveform there (see the first song). An instrument's 1st wave of 00 and no wave table is silent.
  • "the song's memory is full": the editor has about 60 KB for the patterns and order lists. Shorten patterns you do not use (patterns never written take no room).
  • "too big for the editor": the song does not fit into those 60 KB; it still plays in the player.
  • "the flash store is full": delete a slot on the SONG page.
  • Up / down does nothing on PATTERNS: the song plays and the cursor follows it; stop it with PLAY.
  • Back to another firmware: run its installer. If it does not find FM-1 SID, hold HOME for 5 seconds (USB mode) and run it again.

Short answer

Yes. A SID chip costs ~8 % of one 240 MHz core with reSID 0.16 (fast mode) and ~6 % with the model written here, measured on the emulated FM-1 and bit-identical to the host builds. Use reSID 0.16 for the chip (real combined-waveform tables, measured filter curves, years of use in VICE / sidplay); keep sidsynth (the instrument driver) on top. The high-quality, alias-free modes of reSID and the newer engines do not fit the FM-1 (see the comparison). The player firmware built on it is also a synth (a step sequencer, stereo chorus, delay and reverb after Felucca's), which its update package starts in.

Engine comparison: own model, reSID 0.16, reSID 1.0, reSIDfp

The demo pieces' register streams are recorded (host/demo.c record: every write with its sample position) and replayed on every engine, each clocked to the exact cycle of each write (host/replay.cc). On the FM-1 the bench workload's stream replays through reSID 0.16 built for pi32v2 (fm1/bench_resid.cc, fm1/run_resid.py).

engineFM-1 instr / sampleof a 240 MHz coreRAMhost ms per s of audioverdict
own (src/sid.c)3165.8 %~1 KB RAM + 32 KB tables2.4fits, lowest fidelity
reSID 0.16 fast4408.1 %17 KB RAM + 128 KB tables3.1fits: use this
reSID 0.16 interpolate5,562102 %17 KB RAM + 128 KB tables27.9too heavy
reSID 0.16 resample39,226721 %167 KB RAM + 128 KB tables133far too heavy
reSID 1.0 resample––8.3 MB of filter tables per chip model117does not fit
reSIDfp resample––~21 MB of filter tables per chip model, std::thread72does not fit

Per SID chip (3 voices + filter), 44.1 kHz. Host: one core of an Intel Xeon at 2.1 GHz. FM-1 outputs equal the host's bit for bit for every row with a number. Tables are constant after init, so they can live in flash (XIP) instead of RAM: reSID 0.16's 128 KB of wave tables and the own model's 32 KB fit easily. The FM-1 has 1 MB of flash in all (~568 KB for the app), so the 8-21 MB filter tables of reSID 1.0 and reSIDfp do not fit there either.

What the listening shows (tools/compare.sh, tools/listen.py, level-matched to -20 dBFS):

  • Aliasing: the own model and reSID fast both sample the waveforms at 44.1 kHz; high notes get folded partials (spectrograms of the high chord: a dense floor between the harmonics). The resample modes are clean, at 15–90x the cost.
  • 6581 filter: reSID 1.0 / reSIDfp model its non-linearity; reSID 0.16 has the measured cutoff curve without distortion; the own model has an approximate curve.
  • Combined waveforms: reSID plays tables sampled from real chips; the own model guesses.

Next quality step that fits: reSID 0.16 fast at 2x the output rate with a half-band decimator (about 2x the cost, ~16 % of a core per chip) to push most aliasing out of the audible band; to be measured.

Real C64 tunes

host/sidlogger.cpp runs a tune on libsidplayfp's C64 emulation (PSID and RSID, multi-SID) and logs every SID write with its C64 cycle (sidlog v2); host/replay.cc then plays that log on every engine, each clocked to the cycle of each write. libsidplayfp 2.6.0 is built in ext/ with a 24-line patch (tools/libsidplayfp-2.6.0-sidlog.patch) that calls a hook on each SID write. tools/scan_tunes.py ranks HVSC tunes by how much they use hard sync, ring modulation and the filter (from their register streams); tools/tunes.sh fetches the chosen ones (they and their renders stay in build/, not in the repo) and renders each with sidplayfp as a reference. tools/refcheck.py aligns ours with the reference: the tempo holds and the log-spectra correlate ~0.8 with our reSIDfp replay; the rest is engine settings (filter curve) and where in the frame the player runs.

Emulator fixes found on the way (ai-smarthome PR #31)

reSID's double setup came out wrong on the emulator; tracing it found four gaps in fm1-core: no arithmetic flags (64-bit add lost its carry, 64-bit subtract always borrowed, 64-bit compares read psr = 0), the add / subtract-with-carry classes fitted to the corpus registers only, the 64-bit multiply-accumulate fc e1, and rets + register-mask push / pop for masks the corpora never had. The fixes, their probes and notes are in PR #31 (branch fm-1-flags-carry). Until it is merged, fm1/run_resid.py needs an fm1-emu built from that branch; fm1/run.py (the own model, integer only) runs on either.

The SID player firmware (firmware/, player/)

Path C below, built: C64 tunes played by their own 6502 code on the FM-1's CPU, with the chip drawn on the LCD.

  • player/psid.c is the C64 side of a PSID tune around its own NMOS 6510, player/cpu6510.h: an instruction per dispatch, every access at its cycle in the instruction (all 256 opcodes, the undocumented ones and NMOS decimal mode included): 64 KB RAM, $01 banking, the CIA timer latch ($DC04/05) for the play rate, $D012 raster reads, OSC3 / ENV3 reads. It runs init and play as subroutines and hands each SID write to a callback with its C64 cycle.

  • RSID tunes (Arkanoid, Game Over, Great Giana Sisters: about 7 % of HVSC) need the C64 around the 6502, and for them the same file keeps a whole one running: both CIAs' timers (CIA 1 -> IRQ, CIA 2 -> NMI, powering up at $FFFF as the chips do), the VIC's raster counter, raster interrupt and bad lines (the CPU stopped at its first read from cycle 11 to cycle 54, as libsidplayfp's VIC drops BA; its writes go on), the $01 banking and, in the KERNAL's place, a stub with its interrupt entries and exits ($FF48, $FE43, $EA31 / $EA81 / $FEBC; RTS elsewhere). Init is entered as libsidplayfp's driver enters it (CIA 1 timer A at 50 Hz, interrupts on, an idle loop to return to) and may never return; each psid_play runs the machine on for a PAL frame. A JMP to itself is skipped a whole JMP at a time up to the next change (a bad line's stop runs it), and the chips run behind the CPU: they catch up when it touches the VIC or a CIA and at the cycles a line changes or a timer underflows (cycle for cycle the same as stepping them every cycle), the interrupts polled between instructions as the 6502 does (a line up two cycles before). Not covered: RSID tunes in C64 BASIC (refused: no BASIC ROM), tunes that need cycle-exact CIA quirks (Jammer's Foregone Destruction runs code out of CIA 1's timer registers), keys. The core replaced floooh's cycle-stepped m6502.h (2026-10-24: a dispatch per clock was ~80 % of an RSID tune's time on the FM-1, and Arkanoid fell behind): 2.6x faster on the host, RSID tunes keep up on the FM-1 (fm1-emu strict: Arkanoid, Giana, Game Over, Vicious SID 2's 8 kHz NMI samples). Every PSID tune's register stream is bit for bit the old one's; against libsidplayfp the RSID tunes score as before or better (Vicious SID 2 0.29 -> 0.55, Foregone Destruction 0.26 -> 0.52). Checked: every opcode's cycles (the NMOS table), and each opcode from random states against m6502.h (the same but for SHA / SHX / SHY / TAS, whose behaviour differs from chip to chip, and BRK / PHP pushing B set, as the 6510 does).

  • Checked against libsidplayfp given the same KERNAL stub (build/sidlogger with FM1SID_KERNAL=, the image from build/psidlog --kernal; without a KERNAL its $EA31 is a BRK): over 20 s, log-spectrum correlation of the reSID renders 0.91 for Arkanoid's title tune (its sampled drums: the write intervals come out identical), 0.94 Giana's title tune (digi drums from CIA 2 NMIs), 0.91 Game Over, 0.95 / 0.99 Arkanoid's alternative drums / song 3; 0.84-0.99 for 15 of 22 RSID tunes drawn at random from HVSC (4 in BASIC, 2 silent in both, 1 the CIA trick above). PSID tunes play as before (60 random tunes: the same register stream, byte for byte).

  • Cost: a busy RSID tune (Arkanoid's title, whose main loop polls a flag) keeps the 6502 running every cycle: in fm1-live the FM-1's core is 62 % busy with it (38 % idle) against 20 % for a PSID tune; tunes whose init returns, or that loop in a JMP to themselves, cost less (Arkanoid song 3: 58 % idle). The web page's emulator runs it at 1.8x real time in node on an M1 (Wizball: 4.4x).

  • GoatTracker 2 songs (.sng, GTS3-GTS5): player/gt2.c is GoatTracker v2.76's playroutine (gplay.c, Lasse Öörni and contributors, GPL-2.0-or-later) kept statement for statement, without the editor's parts. GoatTracker holds a song in fixed arrays (208 patterns of 128 rows, 32 order lists: about 135 KB, where the firmware has 7 KB free); here the song stays in flash as the file it is. Loading checks it and makes an index (where each order list and pattern starts, how long it is), copies the instruments and the four tables (with the older versions' conversions), and the playroutine reads patterns and order lists through the index: 7.4 KB for all of it (with the editor's play state), in the PSID player's memory (one or the other plays). What the file leaves out reads as GoatTracker's cleared song does (an empty pattern is 64 rests). It plays as GoatTracker's editor does at its default settings (1x speed, hard restart $0F00, the pulse and realtime optimizations), the 25 registers every frame from $18 down to $00, 14 cycles apart, as its sound code writes them; a song's own .sid export settings are not in the .sng (2x-speed songs play at 1x). The subtunes are its songs.

  • Checked against GoatTracker itself: tools/gt2check.sh (make gt2check) runs sid-tracker's gt2dump (GoatTracker's gplay.c and gsong.c compiled unmodified) and build/gt2log on every subtune and compares the registers frame by frame: the 14 example songs of GoatTracker 2.76's release and sid-tracker's 3, 30 subtunes, the same for 10 minutes each (or to the frame where the song stops itself). The loader takes damaged files without reading outside them (4500 cut or scrambled files under AddressSanitizer / UBSan, 2000 frames each); tools/sidpack.py and web/pack.js check a file the way it loads it (the same verdict on 1000 damaged files). Cost on the FM-1: 83 % idle in fm1-live with Unleash the Fury (the reSID rendering is most of the rest); the firmware grew 6 KB.

  • player/psidlog.c (make build/psidlog) runs the same code on the host and writes a sidlog v2, so host/replay.cc and tools/refcheck.py can hold it against libsidplayfp's stream: Sanxion 0.979, Roland's Rat Race 0.888, Contaxia 0.915 log-spectrum correlation against the sidplayfp render.

  • firmware/main.c (GPL-3.0-only: it is built on Felucca's HAL, startup and ISR code in ext/Felucca) plays them. The main loop runs the play routine up to three frames ahead and queues the writes with their cycle; the audio interrupt (ALNK0, 128-frame half buffers at 44117.6 Hz) clocks reSID 0.16 fast (firmware/resid_glue.cc) to each write's cycle, or renders the own model to the sample. The LCD shows each voice (waveform, gate, note, envelope, pulse width; SYNC / RING badges), the filter (mode, routing, cutoff, resonance), an oscilloscope and the CPU load.

  • Keys: PRESETS / SELECT encoder or OCT− / OCT+ = tune, ALGORITHM = its subtune, PLAY = pause, EDIT = engine (reSID / own model), FX = the effects page (below), SEQ = the GoatTracker editor (below), MASTER = the volume (the pot read from the main loop, its square the level, as Felucca's). Keys and buttons light while held; PLAY, REC, FX and SEQ also while playing, recording, on the FX page, in the editor; SCL / ENV / LFO the sequencer's track, LFO a muted channel in the editor. Every other button and key that does something on the page glows dim (Felucca's glow, fm1_led_dim, ~1/30 of lit), so the black FM-1's panel can be found in the dark. HOME held 5 s (or at power-on): USB mode (below). OCT− and OCT+ held 3 s: the chip's update mode.

  • Effects (firmware/fx.c, after Felucca's src/fx.c, GPL-3.0-only): a stereo chorus (two taps swinging against each other), a tempo delay whose right side answers 1.5 times later (1/16, 1/8, dotted 1/8 or 1/4 of the tempo, at most 0.49 s; low-passed repeats) and Felucca's ROOM reverb (4 damped combs, 2 allpasses). The chip is mono, the FM-1's output stereo: the chorus and the delay give it width. FX opens their page: KNOB1 chorus, KNOB2 delay, KNOB3 its time, KNOB4 reverb, SELECT the delay's feedback, PRESETS 6581 / 8580; FX again leaves it. One setting per mode: the synth starts with a little of each, the player dry (its output is then the chip's samples, bit for bit as without the effects). They always run, like sends, and take the audio interrupt from ~10 % to ~14 % of the core; their buffers are 78 KB of RAM.

  • Synth mode (HOME switches): the 27 note keys play src/sidsynth.c's 12 instruments on the same chip and engine. The driver's register writes go into the same cycle-stamped queue as a tune's: sidsynth has an optional write / envelope hook for another engine (sidsyn_t.write, .env, sidsyn_tick; without it nothing changes, same bench hash). Its 50 Hz frames run in the audio interrupt, each at its own cycle, and key presses reach it through a small command queue at the start of the next half buffer (2.9 ms). PRESETS / SELECT pick the instrument, OCT- / OCT+ the octave, KNOB1-4 cutoff, resonance, attack and release; PLAY sends all notes off. Pack flag bit 3 boots straight into it.

  • Step sequencer in the synth mode: three tracks of 16 steps, each a sidsynth part on its own SID voice with its own patch (sidsyn_set_voice; track 3 starts as a drum kit where a note's pitch class picks kick, snare or hat). The tracks share the chip's one filter the way C64 music does: routing bits merge, cutoff, resonance and mode come from the first track whose patch has a filter; sync and ring take the voice before as their source. Steps fire in the audio interrupt at their own cycle, between the driver's 50 Hz frames. SEQ shows the pattern on the LCD, PLAY runs it, REC records (running: to the nearest step; stopped: at the cursor, which SELECT moves), SCL / ENV / LFO pick track 1-3, PRESETS its patch, ALGORITHM the tempo, GLO clears a step, ARP a track. It starts with a demo pattern; nothing is saved yet.

  • GoatTracker editor (firmware/tracker.c; SEQ in the player): the GoatTracker song that plays opens in it (a .sid: a new song), moved into RAM by player/gt2edit.c (60 KB of the PSID player's memory: the order lists and patterns each as long as they are, one after the other; a song that has not got a pattern takes no room for it), and GoatTracker's own playroutine plays it while it is edited, with GoatTracker's editor ways to play: a pattern over and over, from an order list position, test notes from the keys, channels muted. Five pages as GoatTracker's, SEQ goes round them: PATTERNS (the three channels, the row the song plays followed), ORDERS (a subtune's lists, repeats and transposes, the restart), INSTRUMENT (its nine bytes and name), TABLES (wave, pulse, filter, speed side by side) and SONG (the texts, the flash slots). The FM-1's controls: PRESETS up / down, SELECT left / right (the emulator's arrow keys), ALGORITHM the value at the cursor, KNOB1 the instrument, KNOB2 the pattern (on ORDERS the subtune), KNOB3 its length, KNOB4 the step; REC edit on / off (off, the keys only play); with REC on, on a hex digit, the 16 keys from C4 to D#5 type 0-F; GLO clear, ARP key off, SCL / ENV insert / delete (rows, entries, table rows), LFO mute, PLAY play / stop, SAVE save, HOME leave. The changes follow GoatTracker's editor: rows in and out of a pattern as gpattern.c, order list entries and the restart as gorder.c, table rows as gtable.c (the pointers into the table from instruments, wave table commands, patterns and its own jumps follow), and the .sng it writes as gsong.c's.

  • Checked on the host: build/gt2edtest (make gt2edtest) saves each song as loaded (all 17: the file again, byte for byte), moves it into RAM (it saves and plays the same), then makes 2000 random edits while it plays (rows, lengths, order lists, tables, instruments, new subtunes) and saves it: the .sng loads back to the same song, saved again the same bytes, playing the same registers as the edited one (5 seeds x 3000 edits, AddressSanitizer / UBSan). The editor's ways to play against GoatTracker's own initsongpos (a copy of sid-tracker's gt2dump started that way): 42 of 42 runs the same, frame by frame. In fm1-emu (fm1-live --shot with scripted keys): notes, hex digits, order list entries typed, every page drawn, a song saved in a slot, found again after a power cycle and played from the player's list; fm1-dock's tests/trio_test.py with the boot slot build.

  • Songs in flash (firmware/store.c): 16 slots in the free end of the firmware's own app slot (from the first 4 KB sector after the image and its pack to 0x92000; about 290 KB with few built-in tunes), flash no other firmware keeps anything in: Felucca's store and fm1-dock's table and modules are above 0x93000. A song is a record of whole sectors, a head (name, length, CRC-32, slot, save number) and the .sng; the head is programmed last, and a slot's older record is erased only once the new one reads back right, so a save cut off keeps the song before it. The store is read with the flash's own read command, a page at a time, not through the XIP window: FM-1 SID leaves the SFC's settings (its plain window) as the boot set them, so no firmware installed after it can inherit a changed one. A saved song is copied into RAM (the end of the PSID player's memory) to play or to edit, and joins the player's list after the pack's tunes. The audio stops while a song is written (erase and program run from RAM with the flash unmapped and interrupts off, Felucca's hal/fm1_flash.h). Installing a package rewrites the slot: the songs go with it. The boot slot build (DOCK_BOOT) has no store: the slot's flash is fm1-dock's to give.

  • Not bricking an FM-1 (the published FM-1 SID had no USB: installed, it played, but no installer could reach the FM-1 again until people found the FM-1's own recovery, #22, #23):

    • USB-MIDI and Felucca's update code (ext/Felucca/firmware/src/ota.c, copied unchanged by firmware/build.sh, with src/ota_hw.c's hooks in firmware/main.c): the installers' handshake gets the identity FM-1_922, their upgrade command a session that stages the package's update loader in Felucca's OTA area (0xE0000..0xE5000) and restarts into it once the installer says "success"; anything else leaves FM-1 SID as it was. The soft key (7D): the chip's update mode. A staged loader left over is erased at boot, as Felucca does.
    • USB mode (usb_mode): HOME held 5 s, HOME held at power-on, or the third of three starts in a row that die in their first 30 s: the audio interrupt stopped, no player, only USB (the TIMER5 interrupt) and its requests in the main loop; the screen counts USB packets and handshakes. HOME leaves (where it was). If USB mode dies early too, the next start is the chip's update mode. OCT− and OCT+ held 3 s: the chip's update mode (only the recovery tools write there, from Windows or Linux).
    • Flash writes only inside the store and the OTA area, checked in the RAM code that writes (FL_RANGE_OK; none at all in the boot slot build): the bootloader, the image and every other firmware's flash are out of reach, so the FM-1's own recovery (OCT− and OCT+ held while it is switched on, USB plugged in: M-flasher, the Firmware Hub, MvaveFM1Unbricker) stays.
    • No global setting changed that outlives a reset: the SFC's plain window is left alone.
    • On a real FM-1 (2026-10-24): installed (through Scintilla's installer), it enumerated as "FM-1 SID" and took MIDI (a program change switched the tune), but played no tunes and answered no handshake; holding OCT− and OCT+ took it to the chip's update mode, and MvaveFM1Unbricker (Linux) put M-VAVE's V15 back. In fm1-emu with USB both work, also with the SIE 50x slower; at half the clock Arkanoid stops as it did there, Dojo plays on. Not found yet. Try both ways back on one FM-1 before giving it out.
  • The tunes ride after the firmware in flash as a tune pack (firmware/main.c reads it at the image's end, _pack in firmware/app.ld): "SIDPACK1", a count, the first entry and two flags (own model; chip 6581 / 8580 for every tune), then offset, length and song per entry. tools/sidpack.py makes one from .sid and .sng files, the web page builds the same in JavaScript (web/pack.js). Without a pack the firmware plays its own tune.

  • tools/housetune.py writes that tune, Pocket Sync (31 s, PSID, BSD-2): the music is computed in Python frame by frame as SID register writes (bass and drums through a resonant low-pass, arpeggios, a lead that goes plain, hard-synced, then ring-modulated) and a 60-byte 6502 routine plays one frame of writes per call.

  • USB-MIDI: Felucca's USB driver (ext/Felucca/firmware/src/usb.c, the one that runs on real FM-1s in Felucca, the update loader and SLOOP / Dinghy's family; firmware/build.sh copies it with one change, the product name "FM-1 SID", and checks the change took), included by firmware/main.c without its CDC console and USB audio, with the update code (above). Its SIE is polled from the TIMER5 ISR at 2 kHz (Felucca defers the poll while its audio ISR runs; FM-1 SID does not); MIDI in waits in its ring for the main loop, where notes go to the synth or the editor and a program change picks the tune. In the emulator: fm1-live --usb (the FM-1's USB controller and a host, fm1-emu's crates/fm1-soc/src/usb.rs): it enumerates as "FM-1 SID (emu)" and Felucca's fm1_install.py --info finds it; headless, FM1_USB_SEND='3000:F0 00 32 45 00 00 00 40 7F F7' with --shot sends the handshake and prints the answer.

  • firmware/build.sh makes build/fw/sidplayer.bin (242 KB of the 568 KB app slot: players, the editor, reSID with its wave tables in flash, the own model, a font and the own tune; 426 KB RAM of 432 KB with the effects' 78 KB), sidplayer_xip.bin for fm1-emu --bin, and copies the image without a pack to web/sidplayer.bin (committed, since CI has no JieLi toolchain).

  • The main loop sleeps (idle) until the next interrupt. On the FM-1 that saves power; in the emulator it lets idle skipping jump ahead, which made the web build 6x faster.

  • Measured in fm1-emu: the audio interrupt takes ~9.5 % of the 240 MHz core with reSID, and its output matches host reSID fed the same stream (correlation 0.984). It needs PR #31's emulator fixes; on master the CPU halts.

  • tools/fwsc.py packs it as an FM-1 update package, web/sidplayer.fwsc: Felucca's update loader (built here) and tools/fm1pkg_make.py, with three Apache-2.0 files of JieLi's AC79 SDK (tools/fetch_ext.sh fetches them into ext/ac79sdk and checks Felucca's hashes; the page ships web/LICENSES/Apache-2.0.txt). The app is the image with a pack of its own tune that starts it in the synth mode (silent until a key is played; HOME switches to the player). ../web/fwsc.js unpacks exactly that app from it, and the emulator boots it like the community packages.

  • Your own tunes on an FM-1: tools/fwsc.py OUT.fwsc A.sid B.sid:3 C.sng ... makes the package with those tunes and GoatTracker songs instead (same options as sidpack.py, plus --synth to start in the synth mode). They live in the firmware's flash, after the image: up to about 360 KB of them (a GoatTracker song is 1-20 KB). The firmware has no way to take tunes while it runs, so a new set is a new package to install. Each file is checked the way the firmware loads it (psid_load: a PSID header, the data inside the file; gt2_load: every part inside the file and inside GoatTracker's limits).

  • In fm1-dock's boot slot: DOCK_BOOT=1 firmware/build.sh builds the same firmware to run from fm1-dock's boot slot (flash 0x15000..0x93000, XIP 0x02011000: 504 KiB, about 298 KB of it for tunes), tools/sidpack.py --boot adds the tunes, and fm1-dock's tools/hbboot.py make OUT.bin --name FM1SID --no-data makes the boot file. It writes no flash, so fm1-dock's deployed modules (CS-1, Scintilla, ...) stay beside it; power-on starts whichever was picked last, and HOME held at power-on is always fm1-dock, with its USB. Tested in fm1-emu with fm1-dock's tests/trio_test.py.

  • On real FM-1s: people installed the first version (no USB) and it started and played (issues #22, #24), but no installer could reach it again; they got back with M-flasher and OCT− + OCT+ held at power-on (#22). This version answers the installers itself, and has USB mode (HOME held 5 s); on the one FM-1 it has been on so far it did not (above). In fm1-dock's boot slot, HOME at power-on is fm1-dock's: USB mode is HOME held 5 s there.

tools/fetch_ext.sh                            # engines, m6502.h, Felucca
firmware/build.sh                             # build/fw/sidplayer*.bin, web/sidplayer.bin
tools/fwsc.py                                 # web/sidplayer.fwsc, the update package
tools/fwsc.py my.fwsc A.sid B.sid:3 C.sng     # the update package with your tunes and songs
DOCK_BOOT=1 firmware/build.sh && tools/sidpack.py my_boot.bin --boot A.sid B.sid:3   # for fm1-dock
tools/sidpack.py build/fw/my_xip.bin --xip --first 1 A.sid B.sid:3   # a firmware with tunes
../target/release/fm1-emu --bin build/fw/my_xip.bin --entry 0x02000120 \
    --ms 3000 --wav build/fw/out.wav --keys "t:PRESET:+1,t:EDIT" --snap 2500  # LCD -> work/
tools/webpage.sh [..]                    # the web page with the emulator -> build/pages

The web page (web/, GitHub Pages)

web/ is the player as a page: the FM-1's front panel (the emulator's own drawing code, as on ../web), the emulated machine in an AudioWorklet booting sidplayer.bin plus a tune pack the page builds from its playlist, and around it a player:

  • Playlist: picks chosen for sync, ring modulation and filter work (web/tunes.json, from tools/picks.py: the HVSC path, header fields, the song's length from HVSC's Songlengths.md5, and tags measured from each tune's own register stream with build/psidlog), then Pocket Sync.
  • All of HVSC: the page fetches Songlengths.md5 (5 MB, all 61,157 paths) from hvsc.c64.org when asked and searches it; a tune is fetched when picked, added to the playlist, and the machine reboots on the new pack. "Surprise me" draws a random tune (not one in C64 BASIC). hvsc.c64.org sends Access-Control-Allow-Origin: *; no tune is hosted with the page.
  • Your files: .sid files dropped on the page or picked join the playlist, locally.
  • SID synth: the page's mode switch (HOME on the panel) turns the FM-1 into the synth; the Instruments tab lists the 12 patches, the computer keyboard (A W S E D ... ; ') and a MIDI keyboard over Web MIDI play the note keys (MIDI notes fold into the keys' range), and the octave and the four knobs have buttons. ?mode=synth starts there.
  • The page mirrors the firmware's state from the keys it sends (tune, engine, chip, pause), so its buttons and the panel's knobs stay in step; it moves to the next tune at the end of the HVSC song length. The address follows the tune: ?t=MUSICIANS/H/Hubbard_Rob/Sanxion.sid&s=2.
  • ../tools/build_site.sh puts the page and the emulator build's fm1_web.wasm together; .github/workflows/pages.yml runs web/smoke.mjs on the result: the wasm must boot the firmware with a pack of the own tune (no network) to sound and a screen, and in the synth mode stay silent until a key goes down, then sound. With an fm1-core that lacks PR #31 this check fails (the CPU halts), so merge PR #31 first.
  • Headless Chromium on this cloud machine runs it at 0.9-1.0x real time; a desktop browser has room to spare. The emulator skips idle time, so the work per second does not depend on the emulated clock.

Measured (own model)

fm1/run.py builds the bench (fm1/bench.c + fm1/workload.h: a held chord on every voice, SAW LEAD patch with the filter and vibrato, a chord change halfway, rendered in 128-sample blocks like Felucca's I2S half buffers) with the JieLi clang for pi32v2, runs it in the emulator, counts the instructions between two markers and compares the output with the host build:

SID chipsvoicesinstr / sample (-O2)of a 240 MHz core-Os (Felucca's flag)output = host
133165.8 %334 (6.1 %)yes
2664211.8 %yes
3997918.0 %yes
4121,34824.8 %1,418 (26.1 %)yes
  • Instructions are not cycles: the percentages assume one instruction per cycle. pi32v2 issues one most of the time, but loads from XIP flash and divides take longer, so read them as a lower bound. Even at 2x the FM-1 has room for four SIDs plus effects.
  • Init (building the combined-waveform tables) costs ~9.9 M instructions per chip model (14.7 M at -Os), once at boot (~40–60 ms). In a firmware these should become const tables in flash.
  • The code is small: the whole bench, SID model + synth driver + 12 patches, is 6 KB of pi32v2 code.
  • make test: 14 checks against the datasheet / known chip behaviour pass (pitch, attack times, decay / release with the exponential divider, sustain, the ADSR delay bug, noise, hard sync, a retrigger at full level, filter stability at full resonance, 6581 volume-register digis).

What is in here

src/sid.h, sid.c          the chip: register-level 6581 / 8580 model (integer only)
src/sidsynth.h, .c        the instrument driver + 12 example patches
host/demo.c               self tests, demo WAVs, the host side of the bench, host timing
fm1/workload.h            the bench workload (shared by host and FM-1 builds)
fm1/bench.c, crt0.S,      bare-metal pi32v2 program for the emulator
  bench.ld, build.sh
fm1/run.py                build + run on the emulated FM-1 + compare + report
fm1/bench_resid.cc,       reSID 0.16 replaying the workload's register stream on the FM-1
  bench_resid.ld, build_resid.sh, run_resid.py, sidlog2h.py
host/replay.cc,           one register stream on every engine (own, reSID 0.16 / 1.0, reSIDfp)
  engines.h, eng_*.cc
tools/fetch_ext.sh        fetch reSID 0.16, reSID 1.0, reSIDfp into ext/ (GPL, not in the repo)
tools/compare.sh          record every demo piece, replay on every engine -> build/cmp/
tools/listen.py,          level-matched MP3s, spectrograms and the A/B page -> build/listen/
  listen.html
host/sidlogger.cpp        a tune's SID writes, cycle-exact, from libsidplayfp's C64 (sidlog v2)
tools/scan_tunes.py       rank HVSC tunes by sync / ring / filter use
tools/tunes.sh,           HVSC tunes on every engine + sidplayfp reference; alignment check
  refcheck.py
player/psid.h, psid.c     PSID and RSID tunes on cpu6510.h (the C64 around it: RAM, banking,
                          both CIAs, the VIC's raster, a KERNAL stub)
player/cpu6510.h          an NMOS 6510 an instruction at a time, each access at its cycle
player/psidlog.c          the same on the host -> sidlog v2, to check against libsidplayfp
player/gt2.h, gt2.c       GoatTracker 2 songs: GoatTracker's playroutine on the .sng in place
                          (GPL-2.0-or-later)
player/gt2log.c           the same on the host: registers per frame (as gt2dump), or sidlog v2
tools/gt2check.sh         gt2log against GoatTracker's own gplay.c (sid-tracker's gt2dump)
player/gt2edit.c          a GoatTracker song to edit: order lists and patterns in a pool of RAM,
                          GoatTracker's editor changes, the .sng writer (GPL-2.0-or-later)
player/gt2edtest.c        on the host: save as loaded, into RAM, random edits, saved and back
firmware/tracker.c        the GoatTracker editor on the FM-1's LCD and controls
firmware/store.c          songs saved in flash, in the app slot's free end
firmware/main.c, fx.c,    the FM-1 SID player: 6502 + reSID 0.16 / own model, LCD visualiser,
  resid_glue.cc, app.ld,  effects (GPL-3.0-only, on Felucca's HAL); tunes from a pack after the image
  gen_assets.py, build.sh
tools/sidpack.py          a firmware image with a tune pack
tools/fwsc.py             the firmware as an FM-1 update package (web/sidplayer.fwsc)
tools/housetune.py        the firmware's own tune, Pocket Sync (PSID)
web/                      the player page (GitHub Pages): panel, playlist, HVSC search, files
tools/picks.py            web/tunes.json: the picks with HVSC lengths and measured tags
tools/webpage.sh          the page with the emulator's wasm in build/pages, for a local look
make test                 # self tests
make wav                  # build/patch_*.wav (every patch, 8580) and build/song_{6581,8580}.wav
make bench                # host timing
fm1/run.py [chips...]     # the FM-1 measurement (needs the toolchain and the emulator)
tools/fetch_ext.sh && make build/sid_record build/sid_replay && tools/compare.sh && tools/listen.py
fm1/run_resid.py [0 1 2]  # reSID 0.16 fast / interpolate / resample on the FM-1 (after compare.sh)

fm1/run.py needs ../target/release/fm1-emu (cargo build --release -p fm1-emu in the repository root) and the JieLi Linux toolchain, which runs natively on Linux x86-64 (../docs/recon.md has the URL; JIELI_TOOLCHAIN points at it, the default is ../reference/toolchain/jieli-linux-toolchains-*). On macOS run the toolchain through Docker as ../tools/probe_build.sh does.

The chip model (src/sid.c)

Written from public descriptions of the chip — no code from reSID / reSIDfp — so it can be BSD like the rest of this repo. Integer-only, no 64-bit arithmetic and no division per sample, so the FM-1 and the host produce identical samples. It runs at the output rate and advances the chip by the ~22.3 SID cycles that fit in each sample, solving the parts that need cycle precision analytically:

  • Oscillators: 24-bit phase accumulators, triangle / saw / pulse / noise; hard sync restarts the synced voice where it would be after the source's MSB rose inside the batch; ring modulation flips the triangle with the source's MSB; the 23-bit noise LFSR is clocked once per rising edge of accumulator bit 19 (several times per sample at high pitches); the test bit; "no waveform" holds the last DAC value.
  • Envelope: the real 15-bit rate counter, the 16-entry period table, the exponential decay / release divider, the sustain comparison, the freeze at zero — and the ADSR delay bug (lower the rate below the counter and it wraps through 0x7FFF first), which matters for how real C64 music sounds and why drivers use a hard restart.
  • Combined waveforms: AND of the components plus a neighbour-bit pull-down, harder on the 6581 than on the 8580. A model of my own, not sampled from real chips (see below).
  • Filter: 12 dB/oct state-variable filter in the TPT form (stable up to the top of the range), LP / BP / HP and their sums (notch), resonance Q 0.7–1.7 (8580) / 0.7–2.0 (6581); the 8580's linear cutoff curve and an S-shaped 6581 curve (approximate — real 6581s differ from chip to chip).
  • Mixer: 3OFF, the master volume, the 6581's DC offsets (so writing the volume register plays samples, the classic "digi" trick) and the output's AC coupling.

The synth driver (src/sidsynth.c)

The SID sound people know is as much the music drivers as the chip: registers written once per video frame from per-instrument tables. sidsynth does that, for 1–4 chips:

  • wavetable per instrument: waveform + note offset per frame, with a loop (drums = a noise frame then a falling pulse/triangle; the C64 "chord" = a 0-4-7 arpeggio looped at 50 Hz)
  • pulse width sweep (ping-pong), filter: mode, resonance, cutoff start and sweep
  • vibrato with delay, sync / ring patches (the note then uses voice 1 as the sounding voice and voice 3 as the silent modulator at its own interval, with a pitch sweep), hard restart
  • polyphony: 3 notes per chip (1 per chip with sync / ring patches); the notes on a chip share its filter, as on the real thing; 50 Hz (PAL) frames, settable for "multispeed"

Patches: SAW LEAD, PWM BASS, MAJ / MIN CHORD, SYNC LEAD, RING BELL, KICK, SNARE, HAT, RESO PAD, P&S ORGAN, BP SWEEP. make wav renders each, plus a short four-chip song (bass, chords, sync lead, drums) on both chip models.

The FM-1 as a SID synth

What the hardware gives (from ../docs/recon.md and Felucca's HAL):

CPUpi32v2, 240 MHz, two cores, FPU (single)
Memory~512 KB SRAM, 1 MB flash (XIP), ~568 KB of it for the app
AudioI2S codec, 44.1 kHz, 24-bit, 128-frame half buffers (2.9 ms)
Panel27 keys, 14 buttons with LEDs, 7 encoders, MASTER pot, 240×240 LCD
I/OUSB-C (MIDI, audio, the SysEx firmware update), TRS MIDI in, BLE

Everything a SID instrument needs is there. A possible panel layout:

  • keys: notes (or, in a "tracker" mode, pattern steps)
  • encoders: K1 cutoff, K2 resonance, K3 pulse width, K4 waveform / wavetable; SELECT / ALGO / PRESET for patch browsing and the ADSR page
  • buttons: 6581 / 8580, filter mode LP / BP / HP, SYNC, RING, ARP, 1–4 SIDs
  • screen: the 29 registers live, the envelope, the oscilloscope, the wavetable editor

Polyphony: 12 voices with four chips is ~25 % of a core. The second core is free in Felucca-style firmwares (Melodee and X0X hand audio to it), which could run more chips or a .sid player.

Ways to build it

A. A SID engine in Felucca (recommended first step). Felucca (hugelton/Felucca, GPL-3.0) already has the UI, presets, the sequencer, USB / TRS MIDI, flash storage, the update package and a browser emulator, and it runs in the emulator's emulator. Its engines are plug-ins (engine_t in firmware/src/core.h): note_on, render, block (once per block — the place for the 50 Hz driver tick), ownenv + done (the engine's own envelope is the amplitude: the SID ADSR), 8 EDIT parameters and a part memory (12.9 KB per part; a 1-chip sidsynth_t is well under 1 KB). One wrinkle: Felucca renders per voice, a SID shares one filter between three voices — run the part as one chip (poly cap 3, the filter in block) rather than one chip per voice. The combined-waveform tables become const (flash). The result is GPL-3.0 (this code is BSD-2, which is compatible).

B. A dedicated firmware on Felucca's HAL: full freedom for a SID-specific UI (register view, wavetable / pulse / filter table editors, a small tracker), multi-SID, and room for C. More work: USB, MIDI, flash and the update path have to come along.

C. A .sid player (built: see "The SID player firmware" above). A 6502 core plus the PSID format plays the C64 music library with the same chip model. Cost is small: a typical play routine runs a few thousand 6502 cycles per frame, i.e. a few million pi32v2 instructions per second. Digi-heavy tunes that write registers at kHz rates need the chip rendered in smaller chunks (supported: sid_render takes any length).

Limits and open questions

  • Aliasing (own model and reSID fast alike): the waveforms are sampled at 44.1 kHz, while the real chip runs at ~1 MHz into an analogue filter. The alias-free reSID modes cost 1-7 cores. In between: reSID fast at 2x rate + a half-band decimator, or polyBLEP edges in the own model; to be measured.
  • Licence: reSID is GPL-2.0-or-later, so a firmware using it is GPL (as Felucca is). The own model stays BSD.
  • 6581 filter distortion is only in reSID 1.0 / reSIDfp, which do not fit.
  • Cycles, not instructions: confirm on hardware once the FM-1 arrives (the emulator does not model cycle timing; reSID's integer code and the own model run mostly single-cycle ops).
  • The FM-1 itself has not run this code, only its emulator (which needed four fixes for it).

Next steps

  1. reSID 0.16 (fast) as the chip under sidsynth; its wave tables as const in flash.
  2. Measure reSID fast at 88.2 kHz + half-band decimation against the alias-free modes.
  3. Felucca engine (path A): eng_sid.c, build Felucca with the toolchain, play it in fm1-live and the web emulator.
  4. On the real FM-1 when it arrives: cycle cost, sound check against a real C64.
  5. The player: multi-SID tunes, tunes loaded over USB instead of built in; for RSID tunes an instruction-stepped 6502 (the cycle-stepped one is most of their cost).
  6. USB: installing through the running firmware (Felucca's M-UPGRADE handshake, ota.c), USB audio (Felucca's UAC1 input), MIDI out and the TRS MIDI jack.
  7. The GoatTracker editor, next: songs out and in over MIDI (SysEx: the .sng to the page, to sid-tracker); copy / paste of rows and patterns; a flash store in fm1-dock's boot slot (its flash is fm1-dock's to give); the page following the editor (it mirrors the player and the synth only); faster entry (a computer's keys through the page).