尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Mutable Instruments Elements, the modal synthesis voice of Emilie Gillet, written in Faust for faust-rs, and checked sample by sample against the module's own C++.
Version française : README.fr.md. The article: paper/faust-elements.pdf.
Elements is a Eurorack module from 2014 whose firmware is open source (MIT, pichenettes/eurorack, elements/dsp). It is a complete physical-modelling voice in about 3,700 lines of C++ running at 32 kHz on a Cortex-M4:
This repository rewrites all of it as one Faust library, dsp/elements.lib (prefix el, 24 documented functions), and asks whether a Faust program can give the same samples as the firmware, not merely the same kind of sound. The answer, measured on 11 scenarios that exercise every exciter model, the three resonators, the parameters moving under held notes, the external input and the reverb (results/compare.txt):
elements::Part (the whole module) to -72 dB of the peak or better on every scenario, -100 dB on most, and elements::Voice to -82 dB or better, sample by sample over 2 to 6 seconds;faustprobe --check all);The C++ processes blocks of 16 samples. Reading the controls, the gate flags, the envelope, the strike exciter, the 52 modes' coefficients and the strings' parameters happen once per block; the per-sample loops then ramp a few values across the block. Getting the same samples out of a sample-by-sample language took five things.
The block clock. el.blk is 1 on the first sample of each block, counted from the first sample the program computes, as the C++'s first block. Everything the C++ computes once per block is a faust-rs ondemand domain clocked by blk: its inputs are read at that sample, its outputs hold for the block, and its own state (~, delays) advances once per block. The envelope (el.envelope_block), the gate flags, the modes (el.modes_block, including the C++'s refresh of the upper 27 modes on alternate blocks), the chord choice with its hysteresis, the strings' parameters: all are such domains. The controls themselves are held per block (_hb), so a control moved in the middle of a block takes effect at the next one, as on the module.
Ramps that accumulate as the C++ accumulates. The C++ ramps the envelope, the strength, the pickup position and four string parameters by adding an increment computed at the block start, (target - value) / 16, to a running value. _ramp does exactly that, from the value reached at the previous block's end, so the rounding of the sum is the C++'s too.
One random generator, shared, in the C++'s order. stmlib::Random is a single linear congruential generator that the bow draws from 16 times per block, then the blow 16 times, then the particles of the strike 0 to 4 times depending on their bounces (one word on the rising edge, two or three per bounce, at most one bounce per block), then each string once per tick. The model keeps the generator's state at the block start and hands every consumer the very word it would draw, with the generator's k-step jump (lcg_a(k) s + lcg_c(k), tabulated from the C++'s own step): the bow gets words 1 to 16, the blow 17 to 32, the strike runs its 16 steps at block rate from word 33 (el.random_block), and each string starts after the words of the strings before it. A string below 15.7 Hz ticks less than once per sample; its ticks are counted at the block start by running its phase through the block, so the next string's words stay right. Integers are 32-bit and wrap, as in C: unsigned comparisons flip the sign bit, unsigned shifts mask.
What the C++ does not process stands still. In string mode the modal resonator is not called; a bowed mode above the number of active modes, the four extra strings in single-string mode, the dispersion all-pass when it is out of the loop, and a slow string between its ticks are skipped. Each is an ondemand domain whose clock is that condition, so its filters and delay lines freeze, and resume where they were, as the C++'s do. The modes above the number of active ones keep their state with a select.
Delay lines read as ring buffers. A C++ DelayLine read at offset k before its write returns what was written k writes ago, and 2048 writes ago when k is a multiple of the size; after the write, offset k returns what was written k - 1 writes ago, the current value at 1. _ring and _ring_post give those reads on a recursion's fed-back value. The reverb engine (FxEngine) shares one 16-bit buffer between ten lines; its first all-pass is "smeared" by reading its line at an LFO-modulated offset and writing the result back 100 cells further, where the tail reads it 49 samples later. The model writes this as _q16(interpolation) @ 49, and quantizes every write to 16 bits as Compress does.
The tables (envelope curves, accent, stiffness, SVF coefficients, MIDI to frequency, the 128,013 samples of the strike and the 40,963 of the blow's noise) are read back from the compiled C++ by build/dump tables, not recomputed, and written to dsp/elements_tables.lib. So are four float32 constants the C++ computes in float: the particles' probabilities, the strings' initial delay, and the reverb LFOs' coefficients. The last ones matter: the LFO is a resonant recursion with a coefficient of 1.99999702, so close to 2 that one unit in the last place changes its frequency by 2 %; with the coefficient computed in double, the reverb's read offsets drifted and the space scenario agreed to -26 dB in single precision; with the float32 value, to -74 dB.
make results renders every scenario of scenarios/ through build/dump (the firmware's elements::Voice and elements::Part, compiled with -ffp-contract=off) and through faustprobe (dsp/elements_voice.dsp, dsp/elements.dsp, --sr 32000 --block 16, the scenario's control changes as --set and --at), in single and double precision: 44 renders, compared over their whole length. The worst channel of each, in dB of its peak:
| scenario | what it exercises | module, single | voice, single | module, double | voice, double |
|---|---|---|---|---|---|
| strike | the default patch, a mallet | -103 | -90 | -123 | -111 |
| samples | the strike's sample player, three samples and timbres | -98 | -82 | -52 | -35 |
| plectrum | plectrum and palm mute | -116 | -97 | -67 | -52 |
| particles | random bounces, three gates | -105 | -97 | -38 | -28 |
| bow | bow, ADSR, moving position and timbre | -83 | -84 | -58 | -35 |
| blow | granular noise, then the reed tube | -105 | -95 | -56 | -46 |
| string | one string, dispersion, infinite damping | -101 | -101 | -58 | -60 |
| chords | five strings, chords, a string below 15.7 Hz, bowed chord | -102 | -103 | -88 | -92 |
| moves | parameters moving, the model switched under a held note | -84 | -85 | -54 | -42 |
| space | raw, spread, reverb, frozen reverb | -72 | -97 | -50 | -39 |
| input | an external signal into the resonator | -105 | -86 | -95 | -76 |
On the machine of results/compare.txt: Apple M5, Apple clang 21.0.0, Elements at eurorack f4cfa76 (stmlib e3bd7c9), faust-rs 22999fe5 (faustprobe 0.9.2).
Cost (results/cost.txt, make cost, 100 s of a bowed note, compute only):
| resonator | C++ (build/dump, -O2) | Faust native (faust-rs, clang -O3) | faustprobe (Cranelift JIT) |
|---|---|---|---|
| modal | 3.5 ms/s | 11.2 ms/s | 18.2 ms/s |
| one string | 1.5 ms/s | 2.9 ms/s | 7.4 ms/s |
| five strings | 4.1 ms/s | 5.1 ms/s | 14.3 ms/s |
The difference lies in the modes: with 26 modes instead of 52 the native module costs 6.4 ms/s instead of 10.4, so the 52 modes are about 8 ms of its 11.2, while the whole C++ module costs 3.5. The generated code keeps each mode's state in the DSP structure and shifts it every sample, where the C++ walks an array of filters; whether that is the whole cause was not measured. At 11.2 ms per second, the module uses about 1 % of a core.
| path | what it is |
|---|---|
dsp/elements.lib | the model (prefix el), documented in the faustlibraries format: each public function has its #### Usage and #### Test (make doctests) |
dsp/elements_tables.lib | generated: the C++'s tables, samples and float32 constants (make tables) |
dsp/elements.dsp | the module: two audio inputs (blow, strike), two outputs (main, aux), the panel's controls |
dsp/elements_voice.dsp | the voice alone (raw, center, side), compared with elements::Voice |
dsp/elements_poly.dsp | a polyphonic MIDI instrument: the voice per note (freq, gain as strength, gate), effect = el.body (mix, soft limit, reverb) |
ref/dump.cpp | links Elements' sources unchanged; dumps the tables, renders scenarios through Part, Voice or the reverb alone |
scenarios/ | the test scenarios (FRAME NAME VALUE, at block boundaries) and demo.txt, a tour for listening |
scripts/ | compare.py (one scenario, both models), results.py, cost.sh, figures.py, gen_tables.py, check_doc_tests.py |
tools/render.cpp | the native renderer of a scenario through the faust-rs C++ (make render) |
tests/ | the table reads of elements.lib given arguments out of range (make check): signature_out_of_range.dsp against signature_bounds.dsp (the blow exciter's noise sample), table_reads_out_of_range.dsp against table_reads_bounds.dsp (s2r, the timbres' filters, the strike's sample player, the accent) |
web/ | the web version: page, keyboard, MIDI, AudioWorklet host of the faust-rs wasm, orbit explorer, atlas of sounds, timbre orbit (make web, make web-serve, make web-check, make web-presets, ); is faust-orbit-ui 0.4.1 |
make ref # build/dump, against ../../mi-eurorack (ELEMENTS=...)
make tables # dsp/elements_tables.lib from the C++
make check # every program compiles; --check all on the module
make doctests # the 24 #### Test blocks of elements.lib
make test # every scenario, voice and module, single and double precision
make results # results/compare.txt and results/cost.txt
make paper # paper/faust-elements.pdf
make render # build/demo.wav, the native render of scenarios/demo.txt (32 kHz)
make web # web/dsp: the voice and the body in wasm; make web-serve, then http://localhost:8000/web/
ELEMENTS points to a checkout of pichenettes/eurorack with its stmlib submodule (git submodule update --init stmlib), FAUST_RS to faust-rs with release faust-rs and faustprobe built. Python 3 with numpy (and matplotlib for scripts/figures.py), node for make web-check.
The sample rate is 32 kHz. Every table, delay length and coefficient of the C++ is computed for the module's codec rate, and the model keeps them: run it at 32 kHz (faustprobe --sr 32000; a Web Audio context can be created at 32 kHz and the browser resamples). At another rate it plays, transposed and with every time constant scaled.
Controls (the names are the C++'s Patch and PerformanceState fields; the ranges are those CvScaler gives them):
| group | controls |
|---|---|
| exciter | envelope (contour), bow, blow, strike (levels), bow_timbre, flow (blow meta), blow_timbre, mallet (strike meta), strike_timbre |
| resonator | geometry, brightness, damping, position, model (0 modal, 1 string, 2 strings) |
| space | space (raw below 0.1, stereo, reverb from 0.6, frozen above 1.75) |
| unit | signature, mod_frequency, mod_offset (0 to 0.5), diffusion, reverb_lp: what Part::Seed derives from each module's serial number, at the values of Part::Init |
| performance | note (MIDI), strength (the pressure input, 0.5 is unity), modulation (semitones, the FM input), gate |
web/ plays the instrument in the browser. make web compiles the voice and the body of dsp/elements_poly.dsp to WebAssembly with faust-rs (single precision), make web-serve serves the repository; then open http://localhost:8000/web/ (Web MIDI and AudioWorklet need localhost or HTTPS).
Engine. An AudioWorklet (web/elements-processor.js) at 32 kHz, the model's rate, which the browser resamples to the device. It runs 1 to 16 voices, 8 by default (1 is the module itself, which is monophonic), sums their raw, center and side into the body as Part sums its voices, sends every control to every voice and to the body, starts each note with one 16-frame block of low gate so that the voice always sees a rising edge (a key struck again restrikes its own voice on the strings still sounding), holds back a release that arrives before the strike has been computed, stops computing a voice once it has been silent for a second (three if its key is still held), and steals the oldest released voice, else the oldest held one. A voice whose output is not finite or passes 1000 has its state cleared (instanceClear): Elements does diverge, a lossless string (damping above 0.95) under a strong bow at a high pitch growing without bound in the C++ as in the model, and the module resets its voice then (the panic of Part::Process, not ported to the library).
Playing. The keyboard on the page, C1 to C8 in a box that scrolls (lower on a key is louder; dragging slides from key to key, a glissando, one note per finger on a touch screen, as in faust-coopiano), the computer keyboard (two rows from Z and Q, marked on the page, ← → for the octave), Web MIDI (notes and velocity, pitch bend on the modulation input over ±2 semitones, all notes off). A hold works as a piano's sustain pedal, for chords: while it is down, released keys keep sounding (shown in a lighter colour) and lifting it releases them. It is down while the Hold notes switch is on, the space bar is pressed or a MIDI sustain pedal (CC64) is down.
Controls. Every control of the voice and the body, one tab per group of the panel (exciter, resonator, space, unit, performance; the browser remembers the last one), each with its explanation in English and French, and 24 presets: the module's defaults; struck (wooden bar, bell, gong, steel drum, wood block, kalimba), bowed and blown (bowed glass, singing bowl, low bow, reed pipe, breath, flute, clarinet), strings (plucked, harp, dispersive string, muted string, string chord, bowed chord) and textures (particles, rain stick, wind chimes, drone). make web-presets renders each one with the wasm on three keys and fails when one is nearly silent or exceeds 0 dBFS; the sounds themselves are to be judged by ear.
Orbit. A tab explores the parameter space with faust-orbit-ui (Yann Orlarey, MIT; 0.4.1 vendored in web/vendor/faust-orbit-ui): every control of the voice and the body (but the note, its velocity and the pitch offset) is a dot on an orbit, dragged one by one or all together from the centre, or scattered at random. Its library starts with the 24 presets, projected on a plane (PCA): a click on a disc recalls a preset, dragging the cross between discs morphs from one sound to another (Shepard interpolation), a double-click keeps the current sound. The orbit and the other tabs follow each other in both directions, and the preset menu shows the preset the orbit has recalled, or marks the last one modified; the model, a whole number, is rounded while the orbit interpolates. What is added, renamed or deleted in the library is kept by the browser.
Part: when the resonator level exceeds 200 the C++ resets the voice for one block; no scenario reaches it.Part: the CV scaler's pot smoothing and calibration, the input noise gates of elements.cc, the 16-bit SoftConvert of the codec.CvScaler caps the knob at 0.9995, and so does the slider.elements_poly.dsp each voice has its own generator, where Part with several voices would share one.MIT, as the Elements firmware. The model: GRAME, 2026. Elements' DSP code and the tables and samples in dsp/elements_tables.lib: Emilie Gillet, 2014, MIT (LICENSE).
make web-atlasweb/vendor/faust-orbit-uiweb/phone/, web/pwa.js | the phone app (PWA) the page builds in its Phone tab: the app's page, keyboard and motion (phone/), the tab (pwa.js), the mappings of the accelerometer (motion.js), the QR codes (qrcode.js), the static app served from its address (play/, its configuration by phone-config.js); served by scripts/web_serve.py, certificates by scripts/pwa_cert.sh (make pwa-cert) |
paper/ | the article, faust-elements.pdf (make paper) |
Atlas. The last tab lays out sounds by what they sound like rather than by their controls. make web-atlas (scripts/gen_atlas.mjs, about a minute) renders 2,000 random patches, drawn as the orbit's Random draws them, and the 24 presets with the page's wasm, each as A3 at velocity 0.7 held one second, and measures them with web/describe.js, where the definitions are: brightness (spectral centroid), noisiness (spectral flatness, bins more than 100 dB below the strongest counted at that level), harmonicity (share of the energy near the harmonics of 220 Hz), attack, sustain, decay to −30 dB (extrapolated from the slope of the last 500 ms when the render ends first, as for 65 % of the sounds, at most 30 s), loudness and stereo width. The plane is the first two principal components of the standardized descriptors, loudness excepted: the envelope (sustain, decay, attack; 28 % of the variance) and the spectrum (brightness and noisiness; 25 %); any descriptor can be chosen as an axis instead. The colour of a dot is the resonator's model, the presets are labelled, hovering shows the measures, and a click loads the patch (the orbit and the preset menu follow) and plays the note it was measured with. The wheel or a pinch zooms about the pointer (over an axis's ticks, that axis alone), a drag moves the view, and a double click or the ⤢ button shows the whole atlas again; the small map beside the orbit zooms the same way. On a wide screen, a separator between the two sides of the Orbit and Atlas tabs shares the width: drag it (or use the arrow keys), double-click it for the default; the browser remembers the share. The output, web/atlas.json (about 400 KB, with the standardization, so that the page can place new sounds), is generated by make web-atlas and committed, for GitHub Pages. Levels measured on eight sounds rendered again by faustprobe from dsp/elements_poly.dsp agree within 0.02 dB; patches are rendered with their values rounded to three digits, as stored, since some move by several dB with the fourth.
The current sound. After every change (a slider, a preset, the orbit, the atlas), a Web Worker (web/measure-worker.js) renders and measures the current patch as the atlas does, in about 28 ms, and a diamond places it on the atlas and on a small map beside the orbit: one sees where a move of the orbit takes the timbre.
Timbre orbit. Beside the atlas, a second faust-orbit-ui whose dots are the eight descriptors rather than the controls, each a percentile among the atlas's sounds (0.5: the median sound). Moving them asks for a sound: the page loads the atlas's nearest sound (of the current model when one is almost as near, so that the model does not flicker), then, once the dots rest, the worker refines it by a local search over 40 measured renders (about a second). The atlas shows the sound asked for (a dashed ring) and the sound obtained (the diamond), and the gap between them in percentiles. Its library holds the presets at their measured percentiles, so that its calque lays them out by timbre. Blending several neighbours' controls was tried and dropped: over 30 targets, the nearest sound alone is at a median gap of 11 percentiles, a blend of two to six at 21 to 24, as the controls of Elements do not mix linearly into its timbre; the search then gains about two points. Not every combination of descriptors exists: a random request gets the nearest reachable sound, typically 10 to 20 percentiles away.
Phone app. The Phone tab makes an app for the phone (a PWA) from the current sound. A mapping ties a control to an axis of the accelerometer (X, Y or Z, m/s² with gravity: tilting the phone) with the semantics of Faust's [acc: axis curve amin amid amax] metadata (faustwasm's FaustSensors.ts, the architectures' ValueConverter.h), written again in web/motion.js (faustwasm does not export its converters, and they belong to its own runtime, which the app does not use): the axis clipped to low .. high and mapped by two segments onto the control's min, centre and max along a curve (Up, Down, Up-down, Down-up), smoothed by a 80 ms low-pass, Android's axes turned to iOS's. Three sliders simulate the axes on the computer. Options choose what the phone shows: the keyboard (two octaves, glissando, hold) and an ON/OFF button holding a chosen note (as the SmartFaust apps' ON/OFF switch: without the keyboard, the phone is an instrument played by its motion); the app's interface is its own (web/phone/). Three ways to serve it, each shown as a QR code (web/qrcode.js, versions 1 to 20, written for the page, decoded by the system's reader in make check-qrcode):
web/play/ reads the patch, the mappings and the options from its address (?c=, a compact JSON in base64url, web/phone-config.js); its service worker keeps the page's files for offline use and answers manifest.webmanifest?c= with a manifest of the configuration's own name, identity (a hash of it) and start address, so that several configurations install side by side on Android (iOS keeps the address it adds to the home screen); the first visit reloads once, when the worker takes control. The page offers only this mode when no server answers /pwa/ (GitHub Pages);make web-serve): the page builds the app's files (the DSPs, the player, web/phone/, a config.json, a manifest, icons drawn on a canvas, a service worker caching everything under the build's name) and the page's server stores them (POST /pwa/apps, build/pwa/ID/) and serves them read only, never the rest of the repository, over HTTPS (make pwa-cert makes a local authority, limited by name constraints to private addresses and .local names, that the phone trusts once from a second QR code, and a certificate for this machine's address; https://LAN-IP:8443/ID/);Checked in headless Chrome (a temporary profile): a built app installable with no error, its 16 files cached, playing offline, its meters following the axes, the ON/OFF button and the keyboard hidden; two static configurations in one profile with two manifests (their own names and ids), installable, offline, started offline with ON/OFF and motion; the HTTPS server with the local certificate (curl --cacert), listings, .. and other origins refused. Not checked: a real phone (installation, the iOS permission prompt, the sensors), the ngrok mode (it publishes the apps), Android's trust of a user-installed authority. The page's own browser pane refuses service workers.
GitHub Pages. .github/workflows/pages.yml publishes web/ as it is on main, as faust-coopiano does: nothing is compiled there, the wasm modules (make -B web) and the atlas (make web-atlas) are built locally and committed with the page. The phone apps are then served from the published page's address.
Controls out of range. Faust does not bound a control by its declared range, and a host may write past it (the page's wasm host writes what it is given). make web-bounds (scripts/check_bounds.mjs) writes each of the 22 controls of the voice and the body, and the velocity, well below and above its range: before the fix, 27 of the 44 writes differed from their bound, seven gave NaN (bow_timbre, strike_timbre, brightness, position, mod_frequency, mod_offset, reverb_lp) and, by their index formulas and table sizes, five families of table reads could go past their tables (the blow exciter's noise sample through the signature, s2r, the timbres' filter tables, the strike's sample boundaries through a negative mallet, the accent through a velocity above 1). Every control is now clamped to its range where it is defined, the velocity at the voice's entry, and the table indices are clamped again as integers inside the functions, for direct calls; all 44 writes sound exactly as their bound. Within the ranges nothing changes: make test prints the same lines, and the atlas (2,024 patches through the single-precision wasm) is the same file, byte for byte. A first version clamped the float argument of s2r too, and the compiler then grouped st + 128 differently: single-precision chords moved by an ulp, which the strings amplify (the atlas changed 391 of its 16,192 values); s2r clamps its integer indices only.
Random patches. make web-fuzz plays 2,000 random patches through the wasm, as the orbit's Random makes them, with the page's guard, and fails if a non-finite or huge value reaches the output. It found that mod_offset above 0.85 makes the side pickup's cosine oscillator diverge (its amplitudes reach 1e35): the module's serial number gives it 0.05 to 0.15, and the slider now stops at 0.5. The few voices the guard clears diverge in the C++ too.
Check. make web-check renders the wasm under node as the worklet does and compares it with faustprobe (--nvoices 1, single precision) on three notes: a struck note -132 dB of the peak, a bowed note identical, a chord of strings -78 dB; the wasm costs 15.6 to 19.3 ms per second of audio for one voice and the body (node 22, Apple M5).