u/Lanky_Scarcity6223

It's alive! PicoFaceD5 — Roland D-50 LA synthesis on an RP2350, built from the machine's own firmware

It's alive! PicoFaceD5 — Roland D-50 LA synthesis on an RP2350, built from the machine's own firmware

Hey everyone,

The tenth instrument in my RP2350 collection is running: PicoFaceD5, a Roland D-50. Bare metal, 32 kHz, no OS, same board and same core as the other nine.

What it actually is

Not a chip emulation. It is a native LA engine over the D-50's own PCM data: sampled attacks dovetailed with synthesized sustains, the seven structures with their ring modulator, three tone-global LFOs, the pitch envelope, and the common block's EQ, chorus and reverb. Sixteen voices on one tone, eight and eight when both play.

The part I did not expect to get

I disassembled the machine. The program EPROM and the internal ROM of its uPD78312 went through MAME's 78K/III debugger, and once that worked, measuring things by ear stopped being the method. Out of the ROM and into the engine, byte for byte:

  • the envelope arithmetic — a rate index per segment, and a time law that compensates the level distance so the inner segments come out time-constant
  • the release, which is rate-constant, not time-constant: the distance lookup is computed and then overwritten. Dead code in a 1987 ROM.
  • the pitch constant, the keyfollow and depth tables, the LFO rate law and its two-phase delay, portamento, aftertouch, the bender modes, the TVA level basis
  • the voice allocator: one pool of sixteen slots, all sixteen to the upper tone in whole mode, and a free list that makes the machine drop a new note when it is empty rather than steal a held one. I had it stealing, like everyone does. The firmware says otherwise.

Two things the firmware settled that I had wrong

The sawtooth is an octave up. The LA32 builds a saw as square ⊗ cosine, so the ring comb only carries even harmonics of the base — the MT-32 compensates for that in its control ROM, and I copied that. The D-50's pitch path never reads the waveform. Captures of a real machine confirm it: same key, square at 262 Hz, saw at 524. And the factory bank is written around it — the squares sit an octave above the saws in every unison layer.

Pulse width 0 is an honest square, not a needle pulse. I had the law inverted, which put the fundamental 22 dB down on every patch that used it. Pipe Solo was the giveaway.

The samples

100 PCM waves, 512 KB total. Every loop turns out to be a power-of-two number of words holding a power-of-two number of cycles, so every root frequency lands exactly on 32000/2^k — 15.625, 31.25, 62.5, 125, 250, 500, 1000 Hz, nothing else. Their geometry is in the program ROM too: a start page and a length class per wave, which also resolved the 24 combination waves that had been silent.

What it will not do

The reverb is not the D-50's. That chip holds 32 types of coefficients in silicon and the firmware says nothing about them, so the 32 panel types stand on the MT-32's Boss RRV-10 topology, which the munt project read out — same era, same Roland department. Right character, not the original impulse response. Chorus and EQ are the same deal.

Practical

  • MIDI: the D-50's own CC list, read out of its dispatch table (1, 5, 6, 7, 38, 64, 65, 98-101), plus the hold pedal and Roland exclusive — DT1 in, RQ1 answered, so an editor can program it and a librarian can pull a bank back out
  • 884 KB of flash, 264 KB of RAM. It is the one sample-based instrument in the collection that fits a 4 MB board.
  • Hardware is the same as the rest: I2S DAC, 128x64 SH1106 OLED over I2C, three encoders, USB and DIN MIDI. Board target is sparkfun_promicro_rp2350.

One catch, and it is the reason there is no download. It needs a D-50 ROM set, which is not mine to distribute, so this one is not in the release binaries — the eight that are unaffected still build and flash as before. Put your own dumps in roms/ and it builds; without them the configure step skips it and says so. GPL-3, like the rest.

https://github.com/Michi71/PicoVintageSynthCollection

Happy to go into any of it. The disassembly notes are in the repo.

youtu.be
u/Lanky_Scarcity6223 — 5 days ago

[MATH][DIAGRAM] Beyond Polyphone: A multi-point zero-crossing alignment algorithm for perfect, click-free sample loops (with source code)

Hey everyone,

After the deep dive into the RP2350 cache architecture yesterday, another math/DSP problem came to mind that I wanted to share. It's about a tool I wrote for my PicoVintageSynthCollection to solve a painful classic: finding loop points for long samples without spending an evening on each one.

If you have ever used software like Polyphone to prepare samples (e.g. for FluidSynth or embedded players), you know the pain. Finding loop points manually is tedious, and auto-loop tools usually settle for the first zero-crossing that roughly matches in amplitude. That catches a zero-crossing, but not necessarily one at the same point in the wave cycle — and the result is a click, a phase cancellation, or a loop that sounds a semitone-ish off because it is short or long by half a cycle.

To fix this for my sampled engines (Reface CP port), I wrote a small pattern-matching tool. Instead of scoring a single point, it matches the phase pattern around the loop end against the phase pattern around candidate loop starts.

diagram

The algorithm

  1. The stencil. The tool collects every zero-crossing in the file and takes the last one as its reference. Around it, it builds a small template: the crossing's direction (rising or falling) plus the distances back to the previous, second-previous and third-previous crossing — d₁, d₂, d₃. Four points, three intervals. Where that reference sits is up to you: the tool uses the end of the file, so you trim the input to somewhere in the stable part of the tone (around 1.5–2 s works well) before running it.
  2. A period estimate. d₁ — the distance to the previous crossing — becomes the unit of length, . Note this is not the full cycle: any waveform has at least two zero-crossings per cycle, so for an asymmetric wave is typically the shorter half of one cycle. In the diagram, = 47 samples while the true period is 122.
  3. Where to look. For a candidate loop length k, the tool centres a search window at loopEnd − k·P̂ and opens it up by ±2·P̂ to both sides. Every zero-crossing inside that window is a candidate.
  4. Scoring. A candidate of the wrong direction is rejected outright — a rising crossing never matches a falling one, whatever the distances say. The rest get a weighted relative error:
  5. The nearest interval weighs three times as much as the farthest, because the crossing right next to the splice is where an error is most audible. Lowest score in the window wins.
  6. Choosing k. It starts at k = 10 and, if the score is worse than 0.01, walks upward to 30, stopping at the first window that clears the threshold. If nothing does, it walks back down to k = 3, and keeps the best it saw. So it isn't a global minimum over all k — it is the first window good enough to stop looking, which in practice is what you want, because every extra period is flash you pay for.

Why three intervals instead of one

A single zero-crossing carries almost no information: a 440 Hz note at 32 kHz has one every ~36 samples and they all look alike. Direction plus three intervals pins down where in the cycle you are. In the diagram's search window there are three candidates: one is a falling crossing (rejected on direction alone), one is a rising crossing that sits at the wrong point in the cycle and scores 0.064, and one reproduces d₁/d₂/d₃ exactly. Only the last one splices cleanly.

The other thing this buys you is that the loop length comes out as a whole number of cycles automatically. You never end up half a cycle short, which is the usual cause of that "the loop is slightly out of tune" feeling.

What it does not do

Worth being clear about, because it changes how you use it:

  • No crossfade. The splice is a hard cut. It works because the phase matches, not because anything is smoothed over.
  • No amplitude matching. Only distances and direction go into the score — never the sample values. On a sample that is still decaying noticeably, the loop start is louder than the loop end and you will hear the level step on each pass, no matter how good the score is. Trim into a region where the decay has flattened out.
  • It won't rescue a bad sample. Noisy or inharmonic material produces jittery crossings and no window scores well. The tool reports that (>0.15 it says so outright) rather than pretending.

Within those limits it has been reliable for me: for the CP and MKS-20 sample sets it found scores under 0.01 for the large majority of notes, unattended.

The implementation

One C++17 file, no dependencies, reading and writing 16-bit mono WAV:

tools/cp_sampleprep/build_loop_finder.sh
tools/cp_sampleprep/FindLoopPoints <sample.wav> [num_periods]

It overwrites the input WAV in place, trimmed to the loop end, and writes the loop start to a <sample>.loop file next to it. Work on copies. In the collection it isn't run by hand — prepare_samples.py shells out to it once per sample while building the voice headers.

Source: tools/cp_sampleprep/src/FindLoopPoints.cpp in https://github.com/Michi71/PicoVintageSynthCollection.git

Have you built similar pattern-matching loops for your samplers, or do you still rely on manual crossfading? And has anyone tried scoring the derivative across the splice as well — I suspect that would catch the cases where the phase matches but the slope doesn't quite.

u/Lanky_Scarcity6223 — 9 days ago

Embedded Audio Paradox: Why emulating a 1986 Roland MKS-20 requires 480 MHz (Dual-Core), while a 1992 JV-880 runs at 444 MHz on a single core

Hey everyone,

Over the weekend I profiled two engines in my PicoVintageSynthCollection for the RP2350 to settle something that had been bugging me:

Why does PicoFaceRD (Roland MKS-20 / MK-80 digital piano) need a 480 MHz overclock and both cores to hold 12 voices, while the newer, sample-heavy PicoFaceJV (JV-880) runs 24 voices at 444 MHz on a single core at 69 % peak load?

Intuitively the 1986 machine should be the easy one. It isn't, and the reason turned out to be more specific than "old hardware is weird."

It's not the arithmetic, it's where the samples live

Both engines are descriptor-driven — the original firmware's voice programming was captured offline and is replayed on-device. Neither is emulating a CPU. So the difference isn't emulation overhead. It's the shape of the memory access.

PicoFaceJV. A JV-880 patch has up to 4 tones, and each sounding tone is one voice — same unit the original machine counts, where 28-voice polyphony means a 4-tone patch gives you 7 notes. My cap is 24, so 24 concurrent sample streams, worst case, each decoded sequentially through its own region. Measured on hardware with B33 Brass Combo at full polyphony: 69 % peak, of which about 5 % is fixed cost (chorus, reverb, block overhead) and ~2.7 % per voice.

PicoFaceRD. Here's the wild part. To get its characteristic sound, the MKS-20 layers 10 separate parts per single note. At the 32 kHz base limit of twelve voices, the engine issues 119 wave-ROM loads per output sample — one per part, ten parts per note, exactly as the architecture predicts.

So it's 24 streams against 119, on the same chip, for one note each.

What the cache does with that

I built a probe that captures every wave-ROM address the RD engine issues and runs the stream through a model of the RP2350's XIP cache (16 KB, two-way, 8-byte lines). Measured miss rates at 12 voices, per patch:

patch  3   85.7 %        patch  8   43.0 %
patch 14   84.6 %        patch  7   16.2 %
patch  0   77.1 %        patch  5    0.2 %
patch 13   83.1 %        patch 15    0.1 %

That spread is the actual finding, and it surprised me more than the average did. It is not "the MKS-20 thrashes the cache." It's patch-dependent by a factor of several hundred. Patch 15's wave data fits the cache and every voice reuses it — and it stays that way as voices are added, still 0.1 % at 32 voices. Patch 3 goes the other way: 66 % of the cycle budget lost to stalls at 12 voices, 96 % at 24.

The base limit of twelve is set entirely by patches like 3. Patches like 15 are being punished for their neighbours — which suggests a per-patch limit derived offline is the obvious next lever, and my voice governor doesn't have it yet.

The 480 MHz is not what it looks like

I want to correct something I'd have written a week ago. The higher clock is not buying flash bandwidth. On this board:

  • RD at 480 MHz: QMI CLKDIV=4 → 120 MHz flash, within spec
  • Every other instrument at 444 MHz: CLKDIV=3 → 148 MHz flash, above the chip's nominal 133 MHz

The divider is an integer, so pushing the core to 480 actually leaves RD with the slowest flash in the collection. The 480 MHz buys arithmetic throughput and core-1 parallelism; it pays for that with flash speed. Anyone reaching for an overclock to fix a memory-bound problem should check which side of that trade they land on.

What I have not shown

The probe measures miss rates. Converting those to "percent of cycle budget" assumes 96 CPU cycles per miss (120 MHz QSPI, 4:1 ratio) — halve or double that and the absolute numbers move. The ordering and the several-hundred-fold spread don't.

And it does not show that the cache-friendly patches could run 24 voices. Arithmetic scales with voice count too, and this probe doesn't measure that at all. If patch 15 fails at 24 voices, it won't be flash. That's a hardware test I still owe: patch 15 against patch 3, both at 24 fixed voices, reading peak load off the footer.

Conclusion

Newer doesn't mean harder. A 1992 PCM synth streaming 24 sequential voices is gentler on a modern MCU than a 1986 digital piano layering 10 parts per note across scattered ROM regions — and even that isn't uniform, because within the same engine, one patch can be 500× more flash-bound than another. Access pattern beats both age and instruction count.

Repo, including the probe and the full write-up with the numbers above: https://github.com/Michi71/PicoVintageSynthCollection

Context — the JV-880 clone this came out of: https://www.reddit.com/r/synthdiy/comments/1vi0qz5/picofacejv_a_jv880_clone_for_the_rp2350/

u/Lanky_Scarcity6223 — 10 days ago

PicoFaceJV - a JV-880 clone for the RP2350

This is a Roland JV-880 running as a native engine on an RP2350 — no CPU emulation. The video is just the thing playing: the same four-bar progression through all 128 preset patches, banks A and B, nothing else going on.

Why bother. Look at what a JV-880 costs now. Same story for every rack module from that era — the ones that were the boring workhorse choice in 1993 are now an eBay auction. I don't think the price of a 33-year-old ROM set should be what decides whether you get to play these sounds. The RP2350 costs about as much as a coffee.

And it is a beast for this. 24 voices, up to 4 tones each, 32 kHz native, peaks at 69% CPU on a single core, no PSRAM — the second core sits idle the whole time.

To be straight about the clock: that's at 444 MHz, which is an overclock — the RP2350's stock speed is 150 MHz. The whole collection runs 444 with the QMI flash timing moved to match, so it's the baseline here rather than something I reached for to make this one work. The point is that the JV needs nothing above that baseline: one instrument in the collection (the MKS-20) goes to 480 MHz and uses core1 as a voice worker, and this one needs neither.

I capped it at 24 voices rather than the machine's 28 because the bottom of the keyboard costs more per voice and I wanted the margin, not because it ran out of room.

How it was built, honestly. I did not port an emulator. Emulating the H8/532, the sub-MCU and the PCM chip is ~400M cycles/s, and worse, ~5.1M random flash reads per second against an 8 KB XIP cache — the RP2350 would choke on the flash, not the arithmetic. So the engine reads the machine's own ROM tables and runs its own voice chain, decoding each voice sequentially, which keeps flash access at ~0.9M reads/s and nearly all cache hits.

Everything else was measured. I ran a reference emulator host-side, purely as a measurement instrument, and fitted each law differentially against it: envelopes, filter, all seven velocity curves, LFOs, the mod matrix, chorus, both delay types, portamento, tone delays. That took the night, and most of the night went on things that turned out to be my own wrong assumptions rather than mysteries of the hardware. Two favourites:

  • A throb on four patches was alternating (ping-pong) loops — and the return pass is negated. The chip keeps integrating the differential stream in the same direction while the address walks backwards. Subtracting instead, which is the arithmetically "correct" way to retrace, matched at r = −0.995: perfectly shaped and exactly the wrong sign.
  • Patches with no delay had an obvious delay on them. Reverb type is three bits, not four. Bit 3 is set on 40 of 192 patches and was turning 67 of them into DELAY or PAN-DLY.

What it does: all 128 preset patches, 4 tones each, TVF/TVA envelopes, both LFOs with key sync and fade, the modulation matrix, FXM, chorus (both types), the two delay types, reverb, poly/solo with portamento and legato, tone delays, velocity curves and windows. Full MIDI CC set, pitch bend with each patch's own up/down range, program change and bank select.

What it does not: no rhythm/drum sets — the ROM has them, the engine doesn't. The reverb is matched, not reproduced: type, time and level laws are measured, but the topology is a Schroeder network of my own, and tails on decaying material run about 13 dB low. Single part, no Performance mode, no patch editing — it plays the factory banks. No SR-JV80 expansion support. And on a base 4 MB Pico 2 the user bank drops out to make it fit (banks A and B stay bit-identical to the full build, nothing is resampled).

ROMs: not included, and not something I can distribute. The build skips the instrument entirely unless you supply your own set, so the other eight instruments in the collection still build from a clean checkout.

It's one of nine instruments in an open-source collection that all share a core and a board — reface YC/CP/DX, MKS-20, Juno-6, Minimoog, Solina, OB-X, and now this. Repo and the full measurement write-up (including every measurement I got wrong first, which is most of what makes the rest trustworthy):

https://github.com/Michi71/PicoVintageSynthCollection

Happy to go into any of it.

youtube.com
u/Lanky_Scarcity6223 — 13 days ago

PicoVintageSynthCollection HD 720p

Eight vintage synthesizer emulations for the RP2350, built from one shared codebase. Same board, same core, one firmware image per instrument.

Here PicoFaceRD, a Roland MKS-20 / MK-80 ("S/A synthesis") digital piano clone. nstead of emulating the original hardware cycle-exactly at runtime, it plays a descriptor-driven re-implementation of the S/A engine: the original firmware's voice programming was captured note-by-note on a host-side reference emulator, distilled into compact per-note descriptors, and is replayed on-device with chip-exact envelope arithmetic.

See more at: https://github.com/Michi71/PicoVintageSynthCollection.git

youtube.com
u/Lanky_Scarcity6223 — 15 days ago