Ten vintage synthesizer emulations for the RP2350, built from one shared codebase. Same board, same core, one firmware image per instrument.
Rendered on a host from the firmware's own drawing code
(tools/host_tests/ui) — what the OLED shows, pixel for pixel.
| Folder | Instrument | Binary |
|---|---|---|
| instruments/PicoFaceYC | Yamaha reface YC (drawbar organ) | PicoFaceYC.uf2 |
| instruments/PicoFaceCP | Yamaha reface CP (electric piano, mdaEPiano) | PicoFaceCP.uf2 |
| instruments/PicoFaceDX | Yamaha reface DX (4-operator FM) | PicoFaceDX.uf2 |
| instruments/PicoFaceJ6 | Roland Juno-6 | PicoFaceJ6.uf2 |
| instruments/PicoFaceMD | Minimoog Model D | PicoFaceMD.uf2 |
| instruments/PicoFaceSM | ARP/Eminent Solina String Ensemble | PicoFaceSM.uf2 |
| instruments/PicoFaceOB | Oberheim OB-X (engine ported from OB-Xf) | PicoFaceOB.uf2 |
| instruments/PicoFaceRD | Roland RD / MKS-20 (sample piano) | local build only |
| instruments/PicoFaceJV | Roland JV-880 (native engine over the original PCM data) | local build only |
| instruments/PicoFaceD5 | Roland D-50 (native LA engine over the original PCM data) | local build only |
The RD, the JV-880 and the D-50 are the exceptions to "download and flash": they only build where the respective ROM set is present, and those ROMs are not distributable, so none of the three is in the release binaries. Without them the configure step skips them and the seven above are unaffected. All three run on the hardware; see the flash note below for the one build option the JV-880 needs on a small board.
- RP2350; the reference board carries 16 MB of flash, 4 MB is enough for every instrument but the JV-880 (see How much flash an instrument needs). The board must feed the core from the RP2350's own regulator -- see The board has to take the overclock below before buying anything cheap.
- I2S audio (PCM5102 DAC)
- 128x64 OLED over I2C (SH1106)
- Three rotary encoders with push buttons
- USB MIDI and DIN MIDI
The pin map is the same for every instrument and lives in core/include/project_config.h.
Which RP2350 board. The default build assumes a Pico-2-format board with an
RP2350A -- Pico 2, Waveshare RP2350-Plus and the like -- where I2S sits on
GP26/27/28 at header pins 31/32/34. The Waveshare RP2350B-Plus-W has the
same header but carries GP40/41/42 at those three positions (the RP2350B's
ADC0-2 take the Pico's ADC spots; GP26-28 are only on the pads underneath), so
a default image on it drives the I2S lines into thin air: display, MIDI and
encoders work, only the sound is missing, and every counter reads healthy. It
is a build-time choice, not a wiring one -- see Building for
-DPICOFACE_BOARD=rp2350b_plus_w.
The board has to take the overclock. These engines are budgeted for a
core clock of 444 MHz (480 for the RD) at a core voltage of 1.60 V:
three times the RP2350's rated 150 MHz, and a voltage above what the SDK
allows without an explicit override. That is not a tuning option, it is the
design point -- below it the instruments do not finish their audio blocks.
The firmware sets that voltage through the chip's on-board regulator, so the
board has to route the core supply (DVDD) through that regulator, the way
the Pico 2 reference design does. Known good: Raspberry Pi Pico 2,
Waveshare RP2350-Plus, Waveshare RP2350B-Plus-W (with its build variant).
Known bad: unbranded 16 MB "RP2350" boards from marketplace sellers. One
was bought to settle issue #107: its chip and flash are fine, but the
regulator register has no effect on its core voltage, so the core stays at the
board's fixed supply and stops dead at 312 MHz. No build option, flash timing
or stepping has anything to do with it, and no firmware can get 444 MHz from
such a board. The symptom is either nothing on the screen with the release
image, or "boots at 300 MHz, no sound" with a downclocked build. If a board
does neither of the things above, the working image is the diagnosis: build
PicoFaceMD with -DPICOFACE_BOOT_DIAG=ON and read the last line on the
screen; the whole investigation, with the images that separate flash, core
and voltage, is in the MD README under
The board that stops at 300 MHz.
Please check that section before opening an issue about a board that will
not start: a chip marked A4 is not the reason either (the splash shows the
bootrom revision as b4 now, and it changes nothing).
If the screen stays dark. The panel is on I2C at 1 MHz with only the chip's
internal pull-ups. That is above what an SH1106 datasheet promises (400 kHz) and
it is what the reference board runs, because the display push is paced in half
tile rows of roughly 1.5 ms of I2C each -- the bus rate is directly the UI's
frame time, and 400 kHz would make a full screen 60 ms instead of 24. On longer
jumper leads it is the first thing to suspect. Build with
-DPICOFACE_OLED_I2C_HZ=400000, and consider real 2.2k-4.7k pull-ups before
blaming the display.
A display that is simply absent costs nothing: the I2C write returns an error on a NACK and the firmware carries on. A display that holds a line low used to take the whole instrument down -- the wait for the bus is unbounded, and the first transfer happens before the splash loop, which is the only place USB gets serviced during startup, so the board would not even enumerate. It reads exactly like a firmware that does not boot. That write is now bounded at 5 ms and the panel is written off after twenty consecutive failures, so a broken display costs you the display and not the MIDI, the audio and the encoders.
On the board setting. The build defaults to PICO_BOARD=sparkfun_promicro_rp2350,
and that is a statement about flash size rather than about hardware: the
prototype runs a Waveshare RP2350-Plus, and the SparkFun definition was picked
because it declares 16 MB where the plain pico2 definition declares 4. Any
RP2350 board with 16 MB works if the pins above are reachable on it. For a
Pico-format board the honest pair is
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICO_BOARD=pico2 -DPICO_FLASH_SIZE_BYTES=16777216Without that second flag, pico2 caps the image at 4 MB. The CP's sample
sets were cut for exactly that size, so every instrument but the JV fits
within it (the CP at 3.99 MB, with 190 KB to spare); the flag is for the JV,
and for a board that really has 16 MB. Nothing else in the SparkFun definition
reaches the firmware: the flash timing and the system clock are set by the project itself in
core/src/pico_hw.cpp, and PICO_FLASH_SPI_CLKDIV is
identical in both. The one thing it declares that the prototype does not have is
an 8 MB PSRAM region on GPIO 19 — nothing is placed there, so it costs nothing,
but PSRAM is not a thing to reach for on this hardware without checking the
board first.
One board, ten firmware images, so choosing a different instrument is something
you do rather than something a factory does once. Hold all three encoder
buttons together. After half a second the display takes over and counts down
from two; let go and nothing happens. Hold it out and the board silences itself,
drains the audio it had already queued, and reappears as the RPI-RP2 drive —
drop the next .uf2 on it.
No button combination in any instrument uses all three at once, so it cannot happen by accident, and the BOOTSEL button on the module is never needed for this. Keep that button reachable anyway: if a firmware is too broken to reach its own user interface, BOOTSEL plus a power cycle is the way back.
The reference board carries 16 MB. That is not a requirement of the design: three instruments ship sample data, and only one of them needs a build option to fit the 4 MB that a base Raspberry Pi Pico 2 and many other RP2350 boards provide:
| Instrument | Image | On a 4 MB board |
|---|---|---|
| YC, DX, J6, MD, SM, OB | 90-190 KB | fits anywhere |
| PicoFaceCP | 3.99 MB | fits, 190 KB to spare |
| PicoFaceJV | 4.34 MB | only with -DPICOFACEJV_4MB=ON (3.76 MB, banks A+B) |
| PicoFaceD5 | 0.86 MB | fits |
| PicoFaceRD | 2.56 MB | fits |
The JV's reduced variant drops something real: the user bank, and the 22 samples only those patches used. Banks A and B are otherwise untouched -- nothing is resampled or requantised.
The RD used to need one too, and no longer does. Its packs held four
sampled velocity layers of every note; they hold the parameter ROM's own
corners now and the engine interpolates, which is both exact at all 128
velocities and a fifth the size. The whole instrument went from 5.15 MB to
2.56 MB. -DPICOFACERD_MODEL=MKS20 or =MK80 still builds one machine on its
own -- eight patches, and half a ROM set is enough for it -- but nobody needs
it to fit a board any more.
The CP has no reduced variant either, and needs none. It used to be 4.22 MB and carried its five built sample sets as one indivisible whole, with no subset to drop; the sets were then cut for a 4 MB flash without dropping anything: every sample keeps its attack byte for byte and loops 40-100 ms earlier, at a loop cycle chosen to stay within 1 dB and 10 % spectral centroid of the cycle it looped before. The rule and the numbers are in tools/cp_sampleprep. Anything from 8 MB up holds any of the ten instruments. The D-50 is the one sample-based instrument that never needed cutting -- its whole sample ROM is 512 KB.
An oversized .uf2 fails to copy in a way that names no reason: the file
transfer stops or the drive rejects it, with nothing on screen about flash size.
That is the symptom, and it is not a corrupt download.
git clone --recurse-submodules https://github.com/Michi71/PicoVintageSynthCollection.git
cd PicoVintageSynthCollection
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build buildThe artifacts are then in build/<instrument>.uf2.
Board variant. -DPICOFACE_BOARD=pico (the default) builds for any
Pico-2-format RP2350A board. -DPICOFACE_BOARD=rp2350b_plus_w builds for the
Waveshare RP2350B-Plus-W: it selects an RP2350B board profile (48 GPIOs, 16 MB),
puts I2S on GP40/41/42, and re-bases PIO0 to GPIO 16-47 before the I2S program
is loaded, because an RP2350 PIO addresses either GPIO 0-31 or 16-47. The firmware
also puts such a board's flash into quad by itself when the bootrom left it in
dual -- the Puya P25Q128H on the reference board ships with its quad-enable bit
clear, and the bootrom never sets one -- and then tops the flash timing ladder
one rung lower (111 MHz instead of 148; RD 96 instead of 120): that part
verifies 111 MHz in quad and hangs at 148, and a rung that hangs cannot be
stepped down from. See the PicoFaceMD README, The bit the bootrom never sets. A board
with a faster flash can ask for the full rung with
-DPICOFACE_QMI_M0_TIMING_TARGET=PICOFACE_QMI_M0_TIMING_RX4. The released UF2s
are the default variant; the RP2350B build is one cmake flag away and produces
the same ten images.
cmake -S . -B build-b -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICOFACE_BOARD=rp2350b_plus_w
cmake --build build-bThe splash screen names the build. Every instrument shows a version taken
from git describe, not a number typed into its instrument.cmake: on a
release tag it is the release (1.7.0), anywhere else it says how far past that
tag and from which commit (1.7.0-4-ga39504b), and a trailing + means the
tree had uncommitted changes when it was built. The About page shows the same
string. Quote it in a bug report and the image is identified exactly; the
version used to read 0.1 in every build ever made, which identified nothing.
A tarball without .git builds fine and shows unknown.
PicoFaceRD, PicoFaceJV and PicoFaceD5 play the original machines' own
data, which is not distributable. Each looks in its own roms/ directory —
instruments/<name>/roms/, gitignored — and removes itself from the build with
a note if what it needs is not there. Nothing else is affected.
what goes in roms/ |
||
|---|---|---|
| PicoFaceD5 | two PCM ROMs and a program EPROM | identified by CRC32, so names do not matter |
plus optional *.syx bulk dumps for the patch banks |
||
| PicoFaceJV | jv880_rom2.bin, jv880_waverom1.bin, jv880_waverom2.bin |
exact names, 256 KB + 2 MB + 2 MB |
| PicoFaceRD | mks20_15179736.BIN … 41.BIN, MK80_IC5.bin, MK80_IC6.bin, MK80_IC7.bin |
exact names, 128 KB each |
pack_p0.rdp … pack_p15.rdp |
built, not found — see below |
The RD needs one thing nobody else does. Its sixteen .rdp packs are note
descriptors derived from those ROMs, and they have to be made once:
R=instruments/PicoFaceRD/roms
python3 tools/rd_extract/rd_descramble.py $R \
RD200_B.bin mks20_15179757.BIN /tmp/prog.bin /tmp/prm.bin
python3 tools/rd_extract/rd_make_packs.py /tmp/prog.bin /tmp/prm.bin $R \
0 0x000000 1 0x008000 …Python and the ROMs, nothing else -- no emulator, no toolchain beyond the one already building the firmware.
To check that all of this actually works from scratch, tools/check_clean_build.sh
clones into a temporary directory, copies the ROMs in from outside git, and
builds all ten. That last part is the point: the ROM sets are not in this
repository and must never be, so a build here is the one thing CI cannot cover.
Name a directory to build there instead and keep the result; add --variants
for the reduced images too.
RD also builds as a single machine — -DPICOFACERD_MODEL=MKS20 (1.73 MB) or
=MK80 (1.23 MB), eight patches each, and only that machine's ROMs are needed.
The default is both machines and 2.56 MB, which fits a 4 MB board. See
instruments/PicoFaceRD/README.md for the
whole recipe and all sixteen patch offsets.
Building a single instrument:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPICOFACE_INSTRUMENTS_FILTER=PicoFaceMD
cmake --build buildOn macOS a mixed package manager prefix produces this, and it reads like a problem with the project:
dyld: Library not loaded: /opt/local/lib/libarchive.13.dylib
Referenced from: /opt/local/bin/cmake
Reason: (mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64'))
Abort trap: 6
It is not. The complaint comes from dyld about loading cmake itself, before
any of this repository is read, and "need 'x86_64'" means the cmake binary is
an Intel build sitting next to Apple Silicon libraries — a MacPorts or Homebrew
prefix that was installed under Rosetta, or carried over from an Intel Mac
without the migration. Check with:
uname -m # arm64 on Apple Silicon
file $(which cmake) # must say the sameIf they disagree, reinstall the toolchain for the machine's own architecture (MacPorts documents a migration procedure for exactly this). Apple Silicon itself is fine: the collection is developed on it.
core/ shared runtime: audio pipeline, hardware, USB/DIN MIDI, GUI, persistence
cmake/ picoface_add_instrument() and the SDK import helpers
lib/ third-party static libraries (audio, encoder, u8g2) and the SDK submodules
instruments/ one folder per instrument: instrument.cmake, src/, include/, doc/, README.md
tools/ host-side tools; not part of any firmware image
docs/ documentation shared by all instruments
hardware/ the module as a board: schematic, two layouts, panel, parts, and
what the first built pair taught
img/ the instruments' screens and the panel sheets above, rendered
from the firmware by tools/host_tests/ui
Shared:
- Architecture - how core and instruments fit together, and why
- Adding an instrument - the three steps needed
- The module bus - the hardware interface several modules in one case would share: power, MIDI and audio. Planned, not built
- Hardware - the module as a 10 HP Eurorack board: schematic, the two layouts and the panel, parts, and the first built pair
- Host tools
Per instrument, under instruments/<name>/doc/: MIDI implementation charts,
persistence formats, preset tables and the engineering logs of the individual
ports. The per-instrument README links them.
Manufacturer manuals and service documentation are not part of this repository. Where an emulation follows a specific manual or schematic, the instrument's README names the document so it can be obtained separately.
All ten instruments build from a single configure run, each with its own USB PID, and all ten run on the hardware. Seven are in the release binaries; the RD, the JV-880 and the D-50 need a local ROM set and are therefore built locally only. Open points are listed in docs/ARCHITECTURE.md, section 8.
GNU General Public License, version 3 or later - see LICENSE. Copyright (C) 2026 Michi71.
The collection was assembled in August 2026 from seven single-instrument repositories -- YC, CP, DX, RD, J6, MD and SM; OB, JV and D5 were written here afterwards, which is why the number is seven and not ten. Each of those seven already shipped the identical GPL-3 licence text, and every instrument README states GPL-3. It is also the lowest common denominator of what the engines are built on. Where an instrument derives from someone else's work:
| Part | Upstream it derives from | That upstream's license |
|---|---|---|
| core, cmake, tools | own work | - |
| PicoFaceYC | OpenB3 / BeatrixCPP - tone generation concepts only, no code | AGPL-3.0 upstream, does not reach here; see below |
| PicoFaceCP | mda-EPiano (engine, in tree) | GPL-3.0-or-later |
| PicoFaceDX | an ESP32 reface DX emulation (engine) | see that project |
| PicoFaceRD | giulioz/rdpiano + MAME (reference emulator; host-side only, not in this repository) | GPL |
| PicoFaceJ6 | junox (patch table, parameter scaling) | GPL-3 |
| PicoFaceMD | BelaMiniMoogEmulation (ladder filter) | stated by its author to be under no copyright |
| PicoFaceSM | string-machine (DSP models) | Boost Software License 1.0 |
| PicoFaceOB | OB-Xf (engine) | GPL-3.0-or-later |
| PicoFaceJV | own engine over the machine's own PCM data, measured against giulioz/jv880_juce (host-side reference; tone field layout) | non-commercial; not vendored, see below |
| PicoFaceD5 | munt - the LA32's wave generation and the Boss reverb topology as that project documents them, no code | LGPL-2.1-or-later upstream, does not reach here; see below |
Two rows in that table need a sentence more than a table cell holds.
PicoFaceRD used to carry the reference emulator's sources in the tree, and
three decoded ROM sets with them. Neither is here any more. The emulator is a
separate checkout the regression harness is pointed at, and the sample data is
built at configure time from a local ROM set into a blob -- the same arrangement
as the JV and the D5, and for the same reason. What the device plays is
RdNewEngine over descriptors that emulator captured offline.
PicoFaceJV contains none of that emulator's code, and cannot: its licence
forbids commercial use and fits neither MIT nor GPL-3. It was used as a host-side
measuring instrument — the harness in tools/jv_extract/ drives it to take the
readings the engine was calibrated against, and the patch and tone field layout
comes from its dataStructures.h. The engine itself is written from the ROM
formats. tools/jv_extract/README.md records the reasoning; the work is Giulio
Zausa's, and his emulator in turn derives from NukeYKT's
Nuked-SC55.
instruments/PicoFaceOB/ additionally carries its own LICENSE, identical in
text, because that instrument's engine is a direct port of OB-Xf and its files
keep the upstream copyright headers. Every instrument builds into its own
binary, so a stricter licence on one of them stays confined to that binary.
Source files carry a two-line SPDX header rather than the full notice, to keep it out of the way of the code. Three kinds exist:
- own work:
GPL-3.0-or-laterplus the copyright line; - ported trees (
instruments/PicoFaceDX/include/dx_engine/): the licence line plus a note that copyright is shared with the upstream authors - no sole claim is made there; - upstream files (
instruments/PicoFaceOB/include/obxf/, CP'smdaEPiano.*, the SDK-derivedusb_descriptors.c,get_serial.*andtusb_config.h): untouched, they keep the header they came with. The last four are MIT and BSD-3-Clause, not GPL.
On PicoFaceD5 and munt. The D-50 and the MT-32 share the LA32 sound chip,
and the MT-32 emulator munt has read that chip
out and written down what it does: how a cutoff becomes the width of a cosine
edge, how the resonance decays, and - from the same era's Boss reverb chip,
whose data lines were traced by Lord_Nightmare, balrog and Mok - the topology
the reverb here follows. munt is LGPL-2.1-or-later. Nothing was copied: the
engine under instruments/PicoFaceD5/include/d5_engine/ was written for this
project from that written description and from the D-50's own firmware, which
is why no file there carries an upstream copyright header. The description is
what mattered, and a description is not the program.
On PicoFaceYC and the AGPL. OpenB3 / BeatrixCPP, whose tone generation
concepts the YC drawbar engine follows, is AGPL-3.0 - stricter than GPL-3, and
not something that can be dropped by relicensing. It does not apply here: the
engine under instruments/PicoFaceYC/include/yc_engine/ was written for this
project and contains no upstream code, which is why no file there carries an
upstream copyright header. Copyright covers the expression, not the concepts,
so GPL-3 is the right licence for it.