Where things live and why they are arranged this way. For how to build and test, see CONTRIBUTING.md. For what the instrument does and why it does it that way, see DESIGN.md.
There are three layers, and the boundary between the first two is the important one.
A DSP core with no JUCE in it. Everything under Source/dsp/ compiles from a bare compiler with no framework and no third-party headers. A CI job checks this. It builds the core with c++ -std=c++17 -Wall -Wextra -Werror and runs its tests, so an accidental #include <juce_core/...> fails the build. This means the part that does the arithmetic can be tested without a plugin host, a display or a build system.
A JUCE layer that owns parameters, MIDI, state and the editor. PluginProcessor handles notes and rendering, PluginParameters declares the 850 parameters and flattens them into a plain snapshot the audio thread can read, and Presets holds the forty-one factory patches.
A UI layer under Source/UI/, which draws the mixer and knows nothing about how sound is made.
There are two threads, and neither waits for the other.
The audio thread reads a snapshot of the parameters rather than the AudioProcessorValueTreeState itself. It allocates nothing. A host may hand over a bigger block than the one it promised in prepareToPlay. Growing a buffer to fit would take the allocator's lock on the audio thread, where whatever the message thread is doing can make it wait. So the block is cut into pieces that fit the existing scratch buffers. A test renders the same passage through both paths and compares them sample by sample.
The message thread owns the editor and the state tree. Values the editor displays, such as meters, lamps and voice counts, are published as atomics by the render loop, and the editor polls them. Adaptive's result for the held chord is too big for an atomic, so the engine publishes it through a TripleBuffer in dsp/AdaptiveSnapshot.h: three copies swapped with one atomic exchange each way, so the audio thread never waits and the editor always reads a whole copy.
Work that arrives over MIDI but belongs to the message thread is handled the same way. A program change is a preset load, which moves hundreds of parameters and reports each one to the host. So the audio thread stores the number in an atomic and a 20 Hz timer on the processor picks it up. Nothing is posted from the audio thread, which would take the message queue's lock.
There is also a third lifetime, and it is the easiest one to get wrong. A plugin is a library the host loads and later unloads, so anything with static lifetime is destroyed at unload rather than when the last instance is removed. By then JUCE has shut its event system down, and on Windows the destructors run under the loader lock. A destructor that needs another thread to finish, or hands something back to a graphics driver whose threads are waiting on that same lock, deadlocks the host instead of only delaying the unload. So the rule is that a static holds plain memory and nothing else: tables, identifiers and lists of names. The test is whether its destructor touches anything outside the heap, not whether it owns a thread. A juce::Image can be a texture on a graphics device, which is why the panel's grain tile lives in a juce::SharedResourcePointer held by each look and feel and goes with the last window. juce::PropertiesFile privately inherits juce::Timer, and every Timer holds a reference to the single thread JUCE runs timers on, so a settings file kept in a static joins a thread at unload with nothing in the code to show it. The picture caches the controls draw from belong to the controls for the same reason.
packaging/
macos/ pkgbuild and productbuild into a .pkg with a chooser
windows/ Inno Setup script for the installer .exe
Resources/
logo.png the overtonium wordmark, compiled into the binary
dehli-musikk.svg the maker's logo, vector since it is drawn small
icon.png the standalone build's program icon, 1024 square
Source/
dsp/ JUCE-free DSP core, unit tested standalone
Harmonics.h tuning table and blend maths
Temperament.h how the keyboard is tuned, the fixed temperaments and
Adaptive's chain of fifths
SineTable.h interpolated wavetable lookup, and the plain sine
Character.h the oscillator characters, as band-limited tables
Measured.h what the six recorded characters were measured to be
Drift.h seeded PRNG and the smooth random contour
Lfo.h the LFO shapes
Envelope.h per-partial delay, ADSR and the two-stage key-off
Velocity.h what key velocity does to the fader, the attack and the
delay
Params.h plain-data parameter snapshot
Exact.h exact equality, for comparisons meant to be exact
Voice.* 32 partials, one note
Halfband.h the half-band filter the drive oversamples through
BusDrive.* the drive each character puts on the summed mix
TapeEcho.* the master echo as a tape loop
BucketEcho.* the same as a bucket-brigade delay
DigitalEcho.* the same as a ping-pong digital delay
Reverb.* the master reverb as a feedback delay network
PlateReverb.* the same as a modulated plate
SpringReverb.* the same as a tray of springs
KarplusString.* the string the noise channel strikes, and each of the
resonator's 88
SympatheticStrings.* the resonator: 88 sympathetic strings, before the bus drive
Wobble.* a warped-record wobble on the whole instrument
OutputStage.* the eight machines at the end of the signal chain
Cassette.* the cassette machine's Dolby, record side, tape and transport
Console.* the console machine's transformer, compressor and tape stage
SynthEngine.* voice pool, allocation, stealing, effects, master stage
AdaptiveSnapshot.h Adaptive's result for the held chord, and the
triple buffer that hands it to the editor
PluginParameters.* APVTS layout, 850 parameters, and the audio-thread snapshot
Presets.* factory presets
MidiLearn.* which controller moves which control
PluginProcessor.* MIDI handling, sample-accurate rendering, state
PluginEditor.* window, zoom, LINK, gutter
UpdateCheck.* the opt-in check for a new version, in the background,
and the settings file its answer is kept in
UI/ theme, look and feel, channel and noise strips, top bar,
the Playing and Tuning bars, the Settings lists both
share, macro panel
Notice.* the in-window dialog the update question and a new
release are shown in
PaintCache.h the last two looks of a control, kept as pictures
Tests/
dsp_test.cpp standalone DSP tests and CPU benchmark
plugin_runtime_test.cpp headless plugin integration tests
music_utils_chord_types.h the 125 chord types Adaptive is tested on
Tools/
render_docs_images.cpp renders the screenshots on the site
render_docs_audio.cpp renders the clips on the site
preset_to_code.cpp turns a saved preset into a factory preset's code
build_site.py writes the parts every page shares, from site_data.py
compose_cards.py composes the social cards
.github/
workflows/ build and test on every push, release on a tag, and
tell IndexNow which pages changed once they are live
scripts/ the checks those run that are not inline
release-notes/ one file per version, read by the release workflow
docs/ the project page, one folder per URL
Every column in the mixer is the same list of rows, and all of them come from layoutRows in Source/UI/Theme.cpp. The caption gutter, the 32 channel strips and the noise strip each call it with their own rectangle and get back a RowBounds indexed by the Row enum.
That is why the captions on the left always line up with the knobs on the right, and it is why folding a section away is a small change: the rows in a folded section are given zero height in one place and the gutter, the strips, the lamps, the hover highlight and the repaint bands all follow.
It also means rows belong to the whole mixer. Folding a group on one channel and not the next would put every row below it out of line with the gutter captions, which are the only labels the knobs have.
The meters and lamps are the only things that move on their own, so they set what drawing costs while a note plays. Three decisions keep that cost down:
- Nothing repaints itself. Each meter and lamp hands the band that changed up to the editor, which collects all of them and invalidates a few rectangles once.
- Merged by row, not by neighbour. Lamps sit at the same heights on every strip, so merging across a row gives a thin wide band. Merging by proximity would pair a lamp at the top of one strip with a meter at the bottom of it.
mergeIntoRowsandcoalesceRegionsinTheme.cppare the two passes, and the comments there carry the measurements. - Segmented rather than continuous, so the display only changes when a level crosses a boundary rather than on every frame in which it moves at all.
Soloing a channel, scrolling the mixer and resizing the window redraw every strip at once, which is a different cost: hundreds of controls that mostly look exactly as they did. Three things keep that down:
- Controls are copied, not drawn. Knobs, faders, the shape buttons, the segment readouts and the lamp buttons each keep their last two looks as pictures in a
PaintCache, at the display's own resolution, keyed by a hash of everything their drawing reads. Two looks, because soloing swaps each control between its lit and its unlit look. - The strips are opaque, so JUCE does not repaint the panel behind each one first.
- One background for every strip. The look and feel keeps the strip's gradient and grain as a picture for each size, so 32 strips share it.
JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS is set for Apple builds in CMakeLists.txt. Without it CoreGraphics answers a list of scattered dirty rectangles by redrawing the one rectangle enclosing them, which for this layout is the whole window, and none of the savings above take effect.
Parameters are declared once in PluginParameters.cpp and reached by string id, built from a suffix and a channel index by oscParamId. Adding a channel control means adding it there, giving it a Row, and placing that row in a section. A control on the top bar has no Row and goes into a group on the bar instead. Either way, two things then fail until they are updated: the expected parameter count in Tests/plugin_runtime_test.cpp, and docs/parameters.json, which overtonium_runtime_test --write-parameters writes.
The saved state is the AudioProcessorValueTreeState tree plus a few properties written alongside it for things that belong to the session rather than the patch: window size, zoom, LINK settings, which sections are folded, which of the Playing and Tuning bars are open, where the parameter section is scrolled to, the MIDI Learn bindings, the name of the loaded preset and the current program. Presets deliberately carry less than the state does, so loading a sound never moves your window or your temperament.
The factory presets are generated C++ rather than data files, converted from patches saved on the panel. factoryCode in Presets.cpp writes that code, diffing against neutralBase so a preset only spells out what it changes.
juce_add_plugin builds VST3 and standalone everywhere, an Audio Unit on macOS and LV2 on Linux. Most of the plugin does not know which format it is running in, with one deliberate exception. getNumPrograms reports the forty-one factory presets to the Audio Unit alone, because Logic reads its preset menu from there. A program count above one would make the VST3 wrapper publish an automatable parameter that rewrites every other parameter when it moves.