Skip to content

Latest commit

 

History

History
195 lines (154 loc) · 7.87 KB

File metadata and controls

195 lines (154 loc) · 7.87 KB

AGENTS.md

Project Overview

Embedded C++ firmware for custom audio effects/synthesizers on the GuitarML FunBox guitar pedal, which uses an Electro-Smith Daisy Seed board (STM32H750 Cortex-M7). Cross-compiled with arm-none-eabi-gcc, flashed via DFU. There are no unit tests -- verification is done by flashing to hardware.

Each experiment lives in its own subdirectory (e.g. basesynth/, template/) with its own Makefile. Shared headers live in include/. Two git submodules provide the hardware abstraction (libDaisy) and DSP library (DaisySP).

Build Commands

Dev Environment Setup (nix-shell)

nix-shell              # enters shell with gcc-arm-embedded, make, dfu-util, bear
nix-shell --run "make" # one-shot build of everything

The nix shellHook runs bear -- make automatically to generate compile_commands.json for clangd LSP support.

Building

# Build libraries first (inits submodules, applies funbox.patch to libDaisy)
make libs

# Build everything (libs + all experiments)
make

# Build a single experiment
make basesynth
make template

# Build from within an experiment directory (after libs are built)
cd basesynth && make

Build artifacts go to <experiment>/build/ (.bin, .elf, .hex, .map).

Cleaning

make clean             # removes all build/ directories (libs + experiments)
cd basesynth && make clean  # clean a single experiment

Flashing (DFU)

Requires the Daisy Seed to be in DFU mode and correct host udev rules. dfu-util must be available on the host (nix-shell alone may not work).

# From within an experiment directory:
make program-dfu       # flash via USB DFU (main firmware)
make program-boot      # flash the bootloader

Debugging (OpenOCD/ST-Link)

make openocd           # start OpenOCD server
make program           # flash via OpenOCD (BOOT_NONE app type only)
make debug             # attach GDB to running OpenOCD session

Testing

There is no test suite. This is bare-metal embedded firmware -- validation is done by flashing to the physical Daisy Seed hardware and testing manually.

Project Structure

Makefile                  # Top-level orchestrator (libs, experiments, clean)
shell.nix                 # Nix dev environment
funbox.patch              # Patch applied to libDaisy for FunBox pin mappings
include/
  funbox.h                # Hardware abstraction (knobs, switches, LEDs enums)
  expressionHandler.h     # Expression pedal handler (FunBox v3)
basesynth/                # FM synthesizer experiment
  Makefile                # Sets TARGET, CPP_SOURCES, includes libDaisy/core/Makefile
  main.cpp                # Entry point, hardware init, audio callback
  basesynth.h/cpp         # Tap tempo + program logic
  synth.h/cpp             # Polyphonic FM synth (Voice/Synth classes)
  note.h                  # Note frequencies, intervals, and chord utilities
template/                 # Skeleton for new experiments
  Makefile
  template.cpp
libDaisy/                 # Git submodule (Electro-Smith HAL)
  core/Makefile           # Core build system included by all experiments
DaisySP/                  # Git submodule (Electro-Smith DSP library)

Creating a New Experiment

  1. Copy template/ to a new directory (e.g. myeffect/)
  2. Edit the new Makefile: set TARGET = myeffect and list CPP_SOURCES
  3. Add C_INCLUDES += -I../include to access funbox.h
  4. Add a target to the top-level Makefile following the basesynth pattern
  5. Use template.cpp as the starting point -- it has the full boilerplate for controls, switches, audio callback, and main loop

Code Style

Formatting (.clang-format)

  • Based on LLVM style
  • 2-space indentation, 120-column limit
  • Allman brace style (braces on new lines for all block types)
  • Format with: clang-format -i <file>

Naming Conventions

  • Classes: PascalCase (BaseSynth, Voice, Synth, ExpressionHandler)
  • Methods: snake_case for project classes (init, process, note_on, tap_tempo, set_ratio); PascalCase for callbacks and functions that interface with the Daisy API (AudioCallback, UpdateButtons, UpdateSwitches)
  • Member variables: snake_case, accessed via this-> explicitly (this->freq, this->sample_rate, this->mod_ratio)
  • Local variables: snake_case (sample_rate, mod_freq, total_diff)
  • Constants/macros: UPPER_SNAKE_CASE (#define MAX_VOICES 8, TAP_TEMPO_SAMPLES, TEMPO_DISPLAY_DURATION)
  • Namespaces: lowercase (funbox, daisy, daisysp)
  • Enum values: UPPER_SNAKE_CASE within classes (FOOTSWITCH_1, KNOB_1, LED_1)
  • Global variables: short lowercase names for hardware state (hw, bypass, led1, led2, param1..param6)

Includes

  • Project headers first (#include "basesynth.h")
  • Then Daisy/DaisySP headers (#include "daisy_petal.h", #include "daisysp.h")
  • Then shared project headers (#include "funbox.h")
  • Use #pragma once for header guards (project headers use this pattern; expressionHandler.h additionally has traditional #ifndef guards)

Language and Compiler

  • C++ standard: gnu++14 (set in libDaisy/core/Makefile)
  • Exceptions disabled (-fno-exceptions), RTTI disabled (-fno-rtti)
  • No STL containers or dynamic allocation -- this is bare-metal embedded code
  • Float literals always use f suffix (0.5f, 1.0f, 440.0f)
  • Inline trivial methods in the header with inline keyword

Patterns

  • Audio callback: a static void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) function registered via hw.StartAudio(). Process controls first, then iterate samples.
  • Bypass pattern: global bool bypass toggled by footswitch; when true, pass input directly to output.
  • Hardware access: global DaisyPetal hw instance. Use Funbox:: enum values from funbox.h for knob/switch/LED indices.
  • Namespaces: always using namespace daisy;, using namespace daisysp;, using namespace funbox; at file scope in .cpp files.
  • Member access: use explicit this-> for member variable access in class method implementations.
  • DSP classes: follow init(float sample_rate) / process() / note_on() / note_off() pattern matching DaisySP conventions.
  • Note/chord utilities (note.h): use constexpr functions for note-to-frequency conversion (sharp, flat, octave, interval, note) and inline functions returning a Chord struct for chord construction (chord_major, chord_minor, chord_seventh, etc.). constexpr note functions use recursive expansion (not pow) for C++14 compatibility. Chord functions use chord_from_intervals(root, intervals, count) as a shared builder. The Chord struct holds up to MAX_CHORD_NOTES (8) frequencies in a fixed-size array with a count field -- no allocation.
  • Compile-time vs runtime: prefer inline constexpr for pure arithmetic on note frequencies -- the compiler evaluates these at compile time when arguments are constants. Use plain inline for functions that populate structs via loops (C++14 constexpr restrictions).

Error Handling

There is no exception handling or error reporting infrastructure. This is bare-metal firmware with no OS. Functions return default/safe values on edge cases (e.g. TAP_TEMPO_DEFAULT when no valid tempo data exists). The main loop runs indefinitely with System::Delay(10).

Things to Avoid

  • Do not use dynamic memory allocation (new, malloc) -- all objects are statically allocated
  • Do not use C++ exceptions or RTTI (disabled by compiler flags)
  • Do not use STL containers that allocate (e.g. std::vector, std::string)
  • Do not modify files inside libDaisy/ or DaisySP/ directly -- changes to libDaisy go through funbox.patch
  • Do not add files to .gitignore patterns */build/ or .cache -- these are already ignored