Frodhes 0.9.7: a physically modelled electric piano, vibraphone and marimba for the M-VAVE FM-1. Every note is computed live from the mechanics of the instrument; there are no samples.
Download: frodhes.fwsc below (the same file as frodhes-0.9.7.fwsc; the newest is always at https://github.com/leonardoMirabal/frodhes/releases/latest/download/frodhes.fwsc). Try it first in your browser: https://leonardomirabal.github.io/frodhes/
Install
pip install mido python-rtmidi
python3 tools/fm1_install.py frodhes.fwscIf anything goes wrong, switch the FM-1 off, hold OCT− and switch it on to get the USB rescue, then install again.
New in 0.9.7
- Install from the browser: the emulator page installs Frodhes on the FM-1 (Chrome or Edge, MIDI with SysEx): Install on your FM-1,
Find my FM-1, then Install. It runs the same update astools/fm1_install.py, checks the package and its
sha256 first, and checks the FM-1's identity after the restart. An install that was cut off gets Resume the
install. Safari and Firefox get the terminal installer's command instead.
Changes
- The headphone jack keeps the full resolution after MASTER: the volume was applied and then cut to 16 bits, which
left a tiny rounding error with a small offset (about -95 dBFS, under the FM-1's own hiss). USB audio is
unchanged. - The USB name carries the model (GitHub #21): the FM-1 is Frodhes FM-1 on the computer (it was "Frodhes"), an
SMK-37 Frodhes SMK-37 Pro (Elite, MKE-P37 likewise). It is the name of the MIDI port, the audio input and the
USB device. The community firmware flasher now finds the unit for its DOWNLOAD MODE. Each model has a new USB
product id, so the computer sets the unit up as a new device by itself. A Mac asks "Allow accessory to connect?" once
(allow it); the DAW then sees a new MIDI device, "Frodhes FM-1", and it has to be selected again (switch its port on
once where the old one was). The update mode keeps its name, "Felucca Update".
Fixed
- Recording over USB at 48 kHz: a loud, bright passage no longer reaches full scale. The 44.1 to 48 kHz resampler
could overshoot a passage the limiter kept at its ceiling and clip one sample; above three quarters of full scale
it now softens the overshoot instead. 44.1 kHz and the headphone jack are unchanged. - SMK-37 (GitHub #17): the CHORD button opens the CHORD page (press again: chord mode on / off) and PARA opens the
PHASER page (again: on / off). They were the other way round. Their lights and the screen's hints follow. - A held note dying away no longer leaves a faint, fixed distortion under it (GitHub #16): the engines' mix is
dithered where it is cut to 16 bits, so its last few bits fall into a steady noise floor (about -96 dBFS, under
the headphone jack's own hiss) instead of odd harmonics that stay put while the note fades. Silence is still
exact digital zero. Every engine (Rhodes, VIBES, MARIMBA). - F11 RESO SWEEP is as loud as the other Rhodes presets: it was about 1.7 LU under F01 (over the 1.5 LU the factory
presets keep). Its LEVEL goes up by 1.5 dB; nothing else in it changes. - Recording over USB: when the FM-1's USB controller did not answer one status read in time, the audio input could
drop one packet of sound (about 1 ms) with no counter showing it. The packet now waits for the next poll instead,
and the event is counted (CC 116 item 21). Found by reading the code, not heard in testing.
Tools
tools/fm1_install.py: "FM-1 not found" now suggests naming the port (--port NAME). An FM-1 running Frodhes
(its port is called "Frodhes" since 0.9.5, now "Frodhes FM-1") is found without--port; the installer of 0.9.4
and before does not know those names, so update with this one.- The device tools send on MIDI channel 8 by default (
FRODHES_MIDI_CH=1..16to change it), so they work with the
FM-1's MIDI CHANNEL on 8 or on ALL. tools/hil_crackle.py(the USB repeat check of the monkey and the crackle test): 4 or 5 equal frames where the
music itself turns slowly are no longer counted as a repeated USB packet; the 44.1 kHz monkey takes that failed on
them now pass, and injected copies are still found.tools/hil_monkey.py: at 48 kHz one sample at full scale fails the take (--old-48kfor firmware before this
one); a passing take with notes that did not sound is kept (silent-<seed>.npz) to look at.tools/release.pynever pushesmain(it is protected): the release goes in through a pull request, then
tools/release.py <version> --after-mergetags it and checks the published files.- CI: the emulator's job runs in Playwright's own image, the generators run once, and the host tests run in five
jobs at once (make pytest SHARD=K/N,tools/pytest_shard.py). tools/final/hil_cpu_fx.py(the CPU check): a take that fails on CoreAudio's error -10863 ("cannot do in current
context", the first take right after an install) before anything is measured runs once more, on a line of its own.
The retry is judged like any take; the failed attempt never counts.tools/hil_crackle.py(the USB repeat check of the monkey and the crackle test): a USB copy repeats the whole stereo
frame, so every channel must show it. One channel's evidence alone (a mono passage, or the other channel at its
turn) needs 18 dB, and 4 or more equal frames need the wave clearly smoother without them. The host build's own
music no longer fails the check, and neither does the FM-1 take it failed (a plateau of the music, every counter
clean).tools/hil_monkey.py: a take that fails an audio check is triaged in the same run. The host build plays the same
seed and is judged the same way, then one line in the report: HOST TOO (the test or the music), COUNTED (a device
counter explains it) or DEVICE ONLY (the event and the counters). The host render is kept next to the take.tools/hil_monkey.py,tools/hil_usb_bench.pyand each step oftools/final/run_final_rev.shrecord the
computer's load before and after: a busy computer starves the recorder.tools/final/run_final_rev.sh: VIBES and MARIMBA get the key-velocity gate's 5-run follow-up after a 3-run FAIL,
as the Rhodes does; the 5 runs decide.tools/hil_must.pyandtools/hil_cpu.pyread the new counter; the gate wants it at 0.
Tested
- Host: tests/py/test_uac_fir.py (the firmware's resampler bit for bit against its reference with the knee; a loud
9.5-11 kHz passage at the limiter's ceiling clips without it and not with it), Gate R (every render unchanged). - FM-1: tools/hil_monkey.py at 48 kHz, 25 takes: 0 samples at full scale (the gate now fails on one);
tools/hil_cpu.py --worst --usb 48000, 3 runs per engine: the whole audio interrupt at most 4016 us. - Host: tests/py/test_master_click.py (the MASTER code from platform.c at four knob positions, with USB VOL fixed
and following: within one 24-bit step of the exact product, no offset), tests/py/test_isr_tail.py. - FM-1: the jack's idle floor as 0.9.6 (-92.5 dBFS): unchanged where the hiss dominates.
- Host: tests/py/test_smk_app.py (the buttons' matrix codes, their LEDs and the hints), make test-smk; the FM-1
build is byte for byte the same with and without the change. Not on an SMK-37 (none here). - Host: tests/py/test_dither.py (a held Rhodes A2: its 3rd and 5th harmonics at the noise floor where they stood at
-117 dBFS before; exact zero before and after a note on every engine, SPREAD on); every render's baseline
re-saved on purpose (tools/gate_r.py). - FM-1: tools/cpu_check.sh, 4 cases of 4 (VIBES at most 4052 us for the whole audio interrupt); the reverb SIZE
swept under VIBES with every effect on, at most 4120 us (INFINITE) and 4140 us (PLATE). - Host: tests/usb_desc_test.c and tests/py/test_usb_desc.py (the FM-1, the FM-1 without USB audio, the three SMK-37
models and the update loader: product id and name); the installers' tests (terminal and web) find and update a
unit on either name. The sound is unchanged: every render byte for byte the same (tools/gate_r.py). - Host: tools/hil_scan.py --host --only presets (F11 -1.5 LU re F01 before, +0.0 now), tests/py/test_hil_limits.py;
tests/py/test_cpu_gate.py (the -10863 retry); Gate R (no render uses F11: every render unchanged). - Host: tests/uac_seq_test.c and tests/py/test_uac_seq.py run the firmware's own 44.1 kHz USB audio path frame by
frame for 36 simulated hours (130 million packets, the host's timing jitter, skipped and missing frames, the timer
interrupting the audio at every point that matters): no frame repeated or lost without a counter. A planted
one-frame copy is caught; a timed-out status read now loses nothing, where the previous release lost the packet. - Host: the repeat check finds 0 events on 800 host takes (7 before); of 1-5-frame copies injected at random it finds
1498 of 1880 (1492 before) and 1530 of 1878 on takes not used to tune it (1524 before); its self-test finds every
injected glitch. Every render byte for byte the same (tools/gate_r.py).
Full manual: https://github.com/leonardoMirabal/frodhes/blob/main/docs/MANUAL.md · Built on Dinghy. GPL-3.0.