This document explains how the current firmware is structured, how the main modules work, and how the code is organized for someone who is new both to the project and to embedded C++.
It focuses on the active firmware path:
It also points out where the repository still contains older prototype code.
At a high level, the firmware turns a Teensy-based hardware instrument into a touch-first mono synthesizer with:
- USB host MIDI input
- a five-knob control surface through a multiplexer
- a touchscreen interface built with LVGL
- a mono synth voice with two oscillators, noise, filter, envelope, glide, and LFO
- a transport shared by an arpeggiator and step sequencer
- three effect modes: echo, reverb, and drive
- selectable Persian/Iranian-inspired tuning tables
The main idea is simple:
- read physical inputs
- update application state
- render the UI from that state
- drive the synth engine from that state
- repeat forever
The active runtime looks like this:
Hardware inputs
|- touch controller
|- five pots wired directly to analog pins 14/15/16/17/22
|- USB MIDI device
v
Input and event layers
|- main.cpp touch callback
|- ControlInput
|- PlayMode
v
Shared state and control logic
|- AppState
|- Settings
|- MainMenu
|- PerformanceEngine
v
Audio layers
|- Synth
|- audio_setup.cpp audio graph + effects
v
Audio codec / I2S output
Two design choices define the whole project:
AppStateis the central shared data model.main.cppcoordinates subsystems in a single cooperative loop.
-
src/main.cppBoot sequence, LVGL setup, touchscreen input bridge, main runtime loop. -
src/app_state.cppandinclude/app_state.hCentral application state and label helpers. -
src/control_input.cppandinclude/control_input.hMultiplexer-based knob and confirm-button scanning. -
src/play_mode.cppandinclude/play_mode.hUSB host MIDI stack and MIDI event registration. -
src/performance_engine.cppandinclude/performance_engine.hTransport, arpeggiator, sequencer, held notes, and generated note timing. -
src/synth.cppandinclude/synth.hVoice logic and mapping from patch state to Teensy Audio objects. -
src/audio_setup.cppandinclude/audio_setup.hAudio graph definition, codec initialization, and effect routing. -
src/main_menu.cppandinclude/main_menu.hLVGL panel UI and page-specific control mapping. -
src/input_test_page.cppandinclude/input_test_page.hHardware diagnostics page for touch, knobs, spare analog pins, audio self-test, and MIDI status. -
include/settings.handsrc/settings.cppSmall global mode tracker. -
include/knob_pins.hShared multiplexer pin assignments. -
include/lv_conf.hLVGL compile-time configuration.
-
docs/Human documentation. -
etc/Board pictures and KiCad design files.
-
lib/synthesizer/Older synth prototype using a different architecture. -
lib/ui/Older menu prototype. -
lib/settings.hOlder settings singleton that matches the prototype code.
These lib/ files are useful as history, but they are not the current implementation path described by src/main.cpp.
The build configuration is defined in platformio.ini.
Important points:
- target board:
teensy41 - framework:
arduino - upload protocol:
teensy-cli - UI libraries:
lvgl,TFT_eSPI,XPT2046_Touchscreen - USB MIDI serial support enabled by
-D USB_MIDI_SERIAL
From a code-reading perspective, this tells you:
- the runtime model is Arduino-style
- graphics are handled by LVGL
- the screen is an SPI TFT
- the touch controller is separate from the display
- the audio path uses the Teensy Audio ecosystem
The current boot sequence lives in src/main.cpp.
Several long-lived objects are created at file scope:
- touchscreen driver
- TFT display driver
- LVGL draw buffer
play_modesynth- other singleton/global modules in their own
.cppfiles
This is common in embedded firmware. The project assumes there is one instrument and one set of hardware services.
setup() performs these steps:
- starts serial output
- initializes LVGL
- initializes the TFT display
- initializes the XPT2046 touch controller
- creates LVGL display and input devices
- renders the main UI with
main_menu.render() - starts control-input scanning with
control_input.begin() - initializes the audio graph with
setupAudio() - stores audio status in
AppState - initializes the synth voice with
synth.setup() - applies initial output volume
- starts USB host MIDI with
play_mode.setup() - gives the synth pointer to
performance_engine.begin(&synth)
This is the system assembly phase. After setup(), all major services are alive.
loop() runs forever and does the following:
- refreshes transient MIDI status in
AppState - checks the high-level
ModefromSettings - if input-test mode is active: runs the diagnostic page loop
- otherwise:
updates knob/button input
sends pot changes into
MainMenusends confirm-button presses intoMainMenupolls USB MIDI updates transport/arpeggiator/sequencer timing applies patch and effects to audio runs synth smoothing/modulation logic updates audio status inAppState - listens for serial
!to reboot into bootloader - lets
MainMenurefresh the UI if needed - advances LVGL ticks and timers
This is a cooperative system. No module owns the CPU for long. Each module does a small amount of work and returns quickly.
The central state type is declared in include/app_state.h.
Instead of letting each module keep its own disconnected state, the firmware keeps most instrument state in one place:
- patch parameters
- transport state
- arpeggiator state
- effect state
- sequencer data
- UI state
- MIDI status
- audio status
This makes the UI, performance logic, and audio logic coordinate through shared data instead of directly calling each other all the time.
Contains the sound-design parameters:
- oscillator mix
- oscillator waveforms
- noise mix
- oscillator octave offset
- detune
- filter cutoff and resonance
- ADSR envelope
- LFO rate and depth
- LFO target
- glide
- pitch-bend range
- tuning selection
This is the heart of the sound patch.
Contains tempo and step timing state:
- BPM
- whether the transport is running
- swing amount
- current step index
- clock source (
INTERNALorEXTERNAL) - whether an external clock is currently present
This transport is shared by both the sequencer and arpeggiator. When the clock
source is EXTERNAL, step timing is driven by incoming MIDI clock ticks instead
of the internal micros() scheduler (see section 10.2).
Contains:
- enabled flag
- latch flag
- arp mode
- octave range
- rhythmic division
- gate percentage
Contains:
- enabled flag
- current effect mode
- per-mode parameter blocks for echo, reverb, and drive
Contains:
- enabled flag
- record-arm flag
- pattern length
- current playhead
- selected step
- visible bank
SequenceStep steps[16]
Each SequenceStep stores:
activetienotegate
The tie field exists but is not yet actively used by the current step-playback logic.
Contains:
- current page
- dirty flag
- whether the input-test page is being shown
- whether the sequencer clear action is waiting for confirmation
The dirty flag is important. It tells the UI when it must redraw instead of constantly rebuilding labels every loop.
Contains:
- whether a USB MIDI device is connected
- vendor and product IDs
- last note and velocity
- recent-note timing info for transient status display
Contains:
- whether the codec started successfully
- whether audio self-test is currently active
- output volume
Important methods:
-
setPageChanges page and clears transient UI flags. -
setInputTestVisibleShows or hides diagnostic mode inside the UI. -
markDirtyRequests a UI refresh. -
currentModeConverts UI page/test state into a higher-levelMode. -
updateMidiDeviceUpdates MIDI connection info and marks the UI dirty when needed. -
registerMidiNoteStores the most recent incoming note for status display. -
refreshTransientStatusClears “recent note” status after a timeout. -
updateAudioStatusKeeps codec and self-test status in sync. -
setOutputVolumeClamps and stores volume in normalized form.
Settings is a much smaller singleton that only tracks the current high-level mode:
MENUSYNTHESIZERSEQUENCERARPEGGIATORINPUT_TEST
In the current code, MainMenu writes this mode by asking AppState::currentMode().
You can think of Settings as a lightweight “what subsystem loop branch should run right now?” flag.
The firmware has three main input sources:
- touch screen
- multiplexed analog controls
- USB MIDI
The touch handling in src/main.cpp is more than a thin hardware wrapper.
It adds filtering and confirmation rules:
- raw touches must be inside calibrated ranges
- pressure thresholds are used
- touches must be stable for more than one sample before being accepted
- movement is also filtered before a new touch point is committed
This is done to avoid noisy touch behavior on the resistive touch controller.
Key helper functions:
touchPointInRangetouchPointsClosestorePendingTouchcommitTouchSampleholdLastTouchreleaseTouch
The LVGL input callback is my_touchpad_read. It translates a raw TS_Point into:
LV_INDEV_STATE_PRESSEDorLV_INDEV_STATE_RELEASED- screen coordinates in the
480x320display space
The same touch information is also forwarded to input_test_page.updateTouch(...) so the diagnostic page can display raw and mapped touch data.
src/control_input.cpp reads the five knobs directly from Teensy analog pins 14/15/16/17/22.
Important implementation details:
- knob pins come from
include/knob_pins.h - ADC resolution is set to 12 bits
- each channel is oversampled and averaged
- a smoothing filter reduces jitter
- a threshold decides whether a pot change is worth reporting
- the button is debounced in software
The active runtime mapping is:
- knob 1 -> channel
C2 - knob 2 -> channel
C4 - knob 3 -> channel
C1 - knob 4 -> channel
C5 - knob 5 -> channel
C0 - OK button -> channel
C7
This mapping is defined by ControlInput::kPotChannels and kButtonChannel.
-
beginConfigures the knob analog pins and captures initial pot values. -
updateReads all controls, filters them, and records pending changes. -
consumePotChangeReturns one pot change exactly once. -
consumeOkPressReturns one debounced confirm-button press exactly once.
This “consume” pattern is useful because it decouples scanning from action handling.
src/input_test_page.cpp provides a service/debug page inside the UI.
It can show:
- touch coordinates and raw pressure
- audio self-test state
- MIDI connection state
- live multiplexer values
- live direct analog pin values
- min/max movement ranges since last reset
It uses a slower, diagnostic-oriented scanning model than the normal control path.
Important details:
- it can start a short audio self-test by calling
synth.startSelfTest() - it resets activity ranges when requested
- it samples the knob pins and probe pins periodically
- it samples a set of direct analog pins separately
The diagnostic page still contains some older “KiCad pot guess” labels and channel assumptions that do not fully match the active runtime control mapping in ControlInput.
For real instrument behavior, the source of truth is:
That means the input-test page is best understood as a hardware probing tool, not as the definitive live-control map.
The MIDI layer is implemented by src/play_mode.cpp.
Despite the name, this file is primarily a USB host MIDI service.
It creates global USB host stack objects:
USBHost myusb- two
USBHubobjects - two
MIDIDeviceinstances (midi1,midi2)
Two host-side MIDIDevice instances let a keyboard and a clock-sending device
(for example an Ableton Move) coexist through a USB hub on the single host port;
either can carry notes or clock.
MIDI also arrives from the device side: because the build enables
-D USB_MIDI_SERIAL, the Teensy enumerates as a USB-MIDI device on its native
port, so a computer / DAW such as Ableton Live can send notes and clock over the
same cable used to upload firmware. That stream is read through the core
usbMIDI object.
It:
- waits briefly before enabling USB host power
- starts the USB host stack
- registers callback handlers for many MIDI message types
A registerHandlers(...) template applies the common handlers (notes, pitch
bend, and clock / start / continue / stop) uniformly to midi1, midi2, and the
device-side usbMIDI. Callbacks with real behavior:
- note on
- note off
- pitch bend
- clock, start, continue, stop (transport sync, active only when clock source is
EXTERNAL)
Most others are stubbed out with unused-parameter handling.
When MIDI note-on arrives:
AppStatestores recent-note statusPerformanceEngine::onMidiNoteOn(...)is called
When MIDI note-off arrives:
PerformanceEngine::onMidiNoteOff(...)is called
When pitch bend arrives:
PerformanceEngine::onMidiPitchBend(...)is called
When a clock / start / continue / stop message arrives:
- the matching
PerformanceEngine::onMidiClockTick / onMidiStart / onMidiContinue / onMidiStopis called - these only drive the transport when the clock source is
EXTERNAL
PlayMode::loop() must be called continuously.
It performs:
myusb.Task()midi1.read()andmidi2.read()(host side)usbMIDI.read()(device side)AppState::updateMidiDevice(...)
This is an important embedded pattern: the callbacks are only triggered because the code actively polls the device in the main loop.
The UI is implemented in src/main_menu.cpp.
This is the largest file in the active firmware because it combines:
- LVGL object creation
- screen layout
- page labels
- value formatting
- control mapping
- touch-action logic
The screen has three bands:
- top status bar
- content area
- bottom tab bar
Inside the content area, the main page can show:
- five pot cards and four action buttons
- sequencer overview + eight step buttons
- or the input-test page
MainMenu::loop() only refreshes when state.ui.dirty is true.
That matters because:
- embedded display updates are relatively expensive
- most loop iterations do not need a full UI refresh
This pattern helps keep the UI responsive without redrawing everything every frame.
Creates all LVGL widgets once:
- root container
- top bar
- content panel
- input-test panel
- pot cards
- action buttons
- sequencer buttons
- tab bar
This function mostly describes structure and styling.
Maps the five physical knobs to different parameters depending on the active page.
This is one of the central project behaviors. The same five physical controls mean different things on different pages.
Maps touch buttons to page-specific actions.
Examples:
- waveform selection on
OSC / MIX - LFO target selection on
MOD - effect mode selection on
FX - transport and record actions on
SEQ
Currently only confirms sequence clear when the UI is waiting for OK.
These update different parts of the screen:
- status bar
- tabs
- pot cards
- action buttons
- sequencer buttons
- visibility of page-specific panels
Each page has:
- five pot labels
- five pot value formatters
- up to four touch actions
That logic is distributed through:
potNameformatPotValuerefreshActionButtonshandlePotChangehandleAction
This is a practical pattern for a fixed control surface. Instead of modeling every page as its own class, the project keeps one UI object and switches behavior by PageId.
The sequencer page has custom behavior:
- eight step buttons are shown at a time
visible_bankselects steps1..8or9..16- first tap selects a step
- second tap on the selected step toggles its active state
- a cooldown prevents accidental multiple touch actions
The sequencer also has a two-step clear flow:
- touch
CLEAR - press the physical OK button
This is implemented with state.ui.confirm_clear_sequence.
The button labeled PANIC on the PLAY page currently calls performance_engine.stopTransport().
That stops transport-driven playback, but it is not a full “all notes off everywhere” implementation in the current code.
The musical event scheduler lives in src/performance_engine.cpp.
This file answers questions such as:
- if a MIDI note comes in, should it play directly?
- should the arpeggiator consume it?
- should the sequencer record it?
- when should the next step happen?
- how long should a generated note stay on?
- remember held MIDI notes
- schedule transport steps
- run arpeggiator note generation
- run sequencer note generation
- manage note gate durations for generated notes
- forward note events to
Synth
stepIntervalUs(...) computes the duration of one transport step in microseconds.
Important details:
- BPM is clamped to a minimum of
40 - one step is treated as a sixteenth note
- swing alternates the duration of even and odd steps
This means the transport is grid-based and lightweight rather than sample-accurate or DAW-style complex.
The work of triggering a single step (playhead update, sequencer/arp handling,
step-index increment) lives in fireStep(). Internal timing calls it from
advanceTransportStep() after computing the next due time; external sync calls it
directly from clock ticks.
When the clock source is EXTERNAL, the internal micros() scheduler is skipped
and the transport is driven by incoming MIDI clock instead:
- MIDI clock is
24PPQN, so6ticks advance one sixteenth-note step (kClocksPerStep) onMidiClockTick()estimates BPM from the spacing between ticks (smoothed) and writes it back intoTransportState, so gate lengths and the BPM readout stay correctonMidiStart/onMidiContinue/onMidiStopmap the host transport ontotransport.running- if clock messages stop arriving (~0.5s timeout)
ext_clock_presentclears and the transport stops
PerformanceEngine::update():
- syncs transport state changes
- releases generated notes whose gate time expired
- if the clock source is
EXTERNAL: skips internal step scheduling (steps fire from clock ticks) and only checks for clock dropout - otherwise, if transport is running: advances one or more due steps
The code includes a small safety limit so a slow loop iteration does not spend too long catching up.
The code path depends on current state:
-
if self-test is active: ignore musical input
-
if sequencer is running and record-armed: record the note into the current step and also play it
-
otherwise: add the note to held notes
-
if arpeggiator is enabled: direct play is mostly suppressed while arp logic owns playback
-
if sequencer is running: direct play is suppressed
-
otherwise: play directly through
Synth
Similarly:
- remove from held notes
- possibly stop direct playback
- or ignore release if transport-owned playback is active
The arpeggiator uses:
- held-note list
- mode
- division
- octave range
- gate percentage
Supported modes:
UPDOWNUP_DOWNRANDOM
nextArpNote() chooses the next note and can add octave offsets based on the current transport step.
Each active sequencer step can generate:
- note number
- fixed velocity for generated playback
- gate duration derived from step gate percentage
If a step is inactive, generated playback is cleared.
If record-arm is enabled and MIDI notes arrive while running, recordStepFromMidi(...) writes the incoming note into the current playhead step.
The arpeggiator and sequencer produce notes that are separate from physically held input notes.
The engine tracks:
- whether a generated note is active
- which note it is
- when its gate should end
This is why there are dedicated helpers:
triggerGeneratedNotereleaseGeneratedNoteIfDueclearGeneratedNote
The synth voice logic lives in src/synth.cpp.
This file is the main bridge between PatchState and the Teensy Audio objects representing the lead voice.
The current playable instrument is essentially a mono lead voice, even though the audio graph declares additional mid/bass/drum objects.
The active Synth object is constructed with:
lead_waveform1lead_waveform2lead_pinklead_filterlead_envelope
So the current musical firmware uses the lead chain only.
It initializes:
- oscillator pulse widths
- oscillator waveforms
- lead mixer gains
- filter defaults
- envelope defaults
- current note and frequencies
- patch application
It also captures an initial last_update_us_ timestamp used for glide/update calculations.
applyPatch(const PatchState &patch) updates sound parameters on the audio objects.
It handles:
- waveform changes
- oscillator/noise mix
- filter frequency and resonance
- ADSR envelope values
- live voice refresh if a note is already sounding
Important design detail:
- UI and state store normalized values
Synthconverts them into real audio-domain values
Examples:
- cutoff is converted with an exponential mapping to hertz
- ADSR normalized values are mapped to milliseconds
- resonance is mapped into the filter’s expected range
The synth keeps a small note buffer of size 8.
When a new note-on arrives:
- duplicate entries are removed
- the note is appended if there is room
- the newest note becomes the current note
When note-off arrives:
- the released note is removed
- if other notes remain, the most recent remaining note becomes active
- if none remain, the envelope is released
This is a mono last-note-priority design.
If glide is low, frequency jumps directly to the target.
If glide is higher, the current frequency moves gradually toward the target frequency in loop().
This happens separately from note scheduling. The scheduler decides which note to play, and the synth decides how smoothly to move to it.
The synth supports two LFO targets:
- filter
- pitch
The modulation is generated as a triangle shape.
Important implementation detail:
updateModulation() advances phase using a fixed 0.001f seconds per loop iteration rather than the measured loop delta.
That means the LFO is simple and practical, but not mathematically tied to exact real elapsed time in the most precise way.
The file defines several cent tables:
- standard equal temperament
- Shur
- Abuata
- Afshari
- Segah
- Chahargah
- Homayun
- Bayat-e Esfahan
- Mahur
- Rast-Panjgah
Some tuning IDs intentionally reuse the same cent table in the current implementation.
noteToFrequency(...) works like this:
- choose the cent table for the selected tuning
- split MIDI note into note class and octave
- measure relative to
C4 = 261.63 Hz - compute the final frequency with
powf(2.0f, octave_ratio)
This is one of the project’s most distinctive musical features.
The synth can temporarily enter a self-test mode:
- both oscillators become sine waves
- fixed test frequencies are used
- the envelope is forced on
- the test auto-stops after about
1200 ms
This is triggered by the input-test page and is useful for verifying that the audio path works.
The audio graph is defined in src/audio_setup.cpp with declarations mirrored in include/audio_setup.h.
Most of the file is generated object wiring from the Teensy Audio System Design Tool.
That includes:
- waveform sources
- noise sources
- filters
- mixers
- envelopes
- delay
- reverb
- waveshaper
- bitcrusher
- final stereo output
Generated audio wiring files are usually large because every connection is explicit.
Although many voices are declared:
- lead
- mid
- bass
- drum
the current firmware actively drives the lead voice path only.
The rest of the audio graph is best understood as reserved or prototype structure that could support expansion later.
This function:
- allocates Teensy audio memory
- enables the SGTL5000 codec
- sets codec output levels
- builds the drive waveshaper curve
- initializes mixer balances
- initializes delay times
- initializes reverb defaults
- initializes bitcrusher defaults
It returns true or false depending on whether the audio codec could be enabled.
setOutputVolume(float volume) is a thin wrapper around sgtl5000_1.volume(...).
The normalized AppState value remains the UI/state representation, while this function applies the real codec setting.
applyFxState(const FxState &fx) is the effect-routing controller.
It:
- checks whether effects are initialized
- skips work if the effect state has not changed
- handles bypass behavior
- reconfigures routing and parameters for echo, reverb, or drive
Controls:
- wet/dry mix
- delay time
- feedback
- left/right ratio
- smear
Implementation techniques:
- two delay taps for stereo feel
- feedback mixer
- some bleed into the reverb input for smear
Controls:
- wet/dry mix
- size
- damping
- predelay
- tone
Implementation techniques:
- delay channel used for predelay
- reverb fed with a tone-dependent balance
- different left/right wet levels
Controls:
- wet/dry mix
- drive amount
- tone
- bit crush
- level
Implementation techniques:
- pre-gain into waveshaper
- bitcrusher depth
- sample-rate reduction
The function stores the last applied FxState and compares the new one with memcmp(...).
That is a performance optimization to avoid reapplying the same settings every loop.
This section explains what each page changes in code.
Knobs:
- cutoff
- resonance
- glide
- arp gate
- BPM
Actions:
- toggle transport
- toggle arpeggiator
- unused button slot
- stop transport
Knobs:
- oscillator 1 mix
- oscillator 2 mix
- noise mix
- octave index
- detune
Actions:
- previous/next waveform for oscillator 1
- previous/next waveform for oscillator 2
Knobs:
- cutoff
- resonance
- attack
- decay
- release
Actions:
- sustain down
- sustain up
- short envelope preset
- long envelope preset
Knobs:
- LFO rate
- LFO depth
- glide
- bend range
- LFO target
Actions:
- LFO off
- filter LFO
- pitch LFO
- depth zero
Knobs depend on current effect mode.
Actions:
- bypass
- select echo
- select reverb
- select drive
Knobs:
- BPM
- division
- gate
- octave range
- mode
Actions:
- enable
- latch
- toggle transport
- clear held notes
Knobs:
- BPM
- length
- swing
- selected step note
- selected step gate
Actions:
- run/stop
- record arm
- bank switch
- arm/confirm clear flow
Knobs:
- output volume
- tuning
Action:
- open input-test page
include/lv_conf.h is the LVGL configuration header.
You do not need to read it line by line to understand the firmware logic.
The important high-level facts are:
- color depth is
16 - the default refresh period is
33 ms - LVGL logging is enabled
- the default font is
Montserrat 16 - the memory pool is configured for embedded use
Treat this file as framework configuration rather than product logic.
The repository still contains older prototype code.
This older synth implementation:
- uses a much simpler architecture
- handles MIDI directly inside the synth module
- has its own LFO implementation
- predates the current
AppState + MainMenu + PerformanceEngine + Synthsplit
It is useful if you want to see the project’s earlier direction, but it is not the main firmware path now.
This contains an older LVGL menu prototype based on a simpler page/menu model.
The current UI no longer uses this structure.
For maintenance, it is important to know they exist so you do not confuse:
- active implementation
- historical prototype
When documenting or modifying current behavior, prefer src/ and include/.
src/main.cppBoot, loop, display flush, touch filtering, high-level orchestration.
-
include/app_state.hType definitions for almost all live application state. -
src/app_state.cppState initialization and label/helper implementations. -
include/settings.hSmall mode singleton. -
src/settings.cppDefines the static singleton pointer.
-
include/knob_pins.hShared knob pin numbers. -
include/control_input.hPublic API for normal control scanning. -
src/control_input.cppNormal runtime scanning, filtering, and button debounce. -
include/input_test_page.hPublic API for diagnostic page. -
src/input_test_page.cppDiagnostic scanning and UI text generation.
-
include/play_mode.hMIDI service declarations. -
src/play_mode.cppUSB host MIDI setup and event callbacks. -
include/performance_engine.hScheduler/arpeggiator/sequencer declarations. -
src/performance_engine.cppMusical event timing and generated note logic.
-
include/synth.hMono voice interface. -
src/synth.cppVoice implementation, tuning tables, modulation, glide, self-test. -
include/audio_setup.hAudio graph declarations and public audio helper functions. -
src/audio_setup.cppAudio graph instantiation, codec setup, and effect routing.
-
include/main_menu.hMain UI class declaration. -
src/main_menu.cppUI creation, refresh logic, formatting, touch actions, page control mapping.
include/lv_conf.hLVGL compile-time settings.
These are not bugs by themselves, but they are important to know when working on the project:
-
The system is single-threaded and cooperative. Long blocking work in any module will hurt touch response, MIDI timing, and UI updates.
-
AppStateis the main source of truth. If behavior looks wrong, verify whether the state is being updated correctly before blaming audio or UI code. -
The UI often displays interpreted values, not raw internal values. Some labels are approximations for readability.
-
The current playable synth is mono and lead-voice focused. The audio graph is larger than the feature set actively exposed today.
-
The input-test page is diagnostic, not the final authority on current control mapping.
-
The repository still contains older architectures in
lib/. Do not accidentally treat them as the active code path.
If you want to learn the codebase efficiently:
- read
src/main.cppfor runtime flow - read
include/app_state.hfor the data model - read
src/main_menu.cppfor control mapping - read
src/performance_engine.cppfor note scheduling - read
src/synth.cppfor sound generation logic - read
src/audio_setup.cppfor audio routing - read
src/input_test_page.cpponly after the main flow is clear
That order follows the most useful path from “what runs” to “what it means” to “how it sounds”.