-
Notifications
You must be signed in to change notification settings - Fork 13
02 Architecture & Development Guide
zicBox is a modular C++ framework for building music applications, grooveboxes, synths, and custom audio hardware across desktop Linux and embedded targets (e.g., Raspberry Pi, STM32H7, ESP32).
Applications in zicBox follow a 3-tier decoupled architecture designed to ensure real-time audio stability, portable UI rendering, and hardware abstraction.
graph TD
Main["Main Entry (zic.cpp)"] --> SFML["Desktop Runtime (runtimeDesktopSFML.h)"]
Main --> HW["Hardware Runtime (runtimeHardware.h)"]
SFML --> UI["Generic UI Layer (ui.h, draw.h)"]
HW --> UI
UI <-->|"Shared State & Mutexes"| Audio["Audio Thread (audioWorker.h)\n[SCHED_FIFO Priority 30]"]
Audio --> Pool["TrackRenderPool (Multi-core Render)"]
Audio --> Engines["Audio Engines (audio/engines/)"]
Audio --> Seq["Audio Sequencer (audio/sequencer/)"]
Audio --> ALSA["ALSA Audio Output Stream"]
-
Target Hardware Abstraction Layer (HAL) (
runtime*.h): Manages platform-specific inputs (GPIO keys, rotary encoders, keyboard/mouse) and output displays (SFML desktop window, ST7789 SPI, framebuffer). -
Generic Application & UI Layer (
ui.h,draw/): Handles application state, navigation, visual layout, and component interaction without depending on platform-specific UI frameworks. -
High-Priority Audio Engine & Sequencer (
audioWorker.h,TrackRenderPool.h,audio/): Performs real-time audio synthesis, pattern sequencing, and DSP processing on dedicated high-priority threads.
Applications isolate hardware-specific logic behind runtime headers, selected at compile time via preprocessor defines:
-
Desktop Runtime (
runtimeDesktopSFML.h):- Map PC keys to hardware button definitions (
KEY_F1..KEY_F5,KEY_1..KEY_8). - Includes a Headless Screenshot Pipeline (
ZIC_SCREENSHOT=<path_prefix>): when set, the application renders every view sequentially, exports PNG screenshots, and terminates cleanly for automated documentation.
- Map PC keys to hardware button definitions (
-
Embedded Hardware Runtime (
runtimeHardware.h):-
GPIO Key Matrix: Reads physical push buttons using
GpioKey. -
Rotary Encoders: Reads quadrature encoders via
GpioEncoderwith speed scaling (encGetScaledDirection()). -
SPI Display: Renders pixel buffers directly to physical displays (ST7789 via
DrawToST7789). -
Dynamic Mapping: Reads GPIO pin configurations from
config.json(orZIC_CONFIG_PATH), falling back to built-in hardware defaults. - Lock-Free Event Queueing: Interrupt-driven GPIO events are queued safely and flushed on the main loop iteration to eliminate race conditions.
-
GPIO Key Matrix: Reads physical push buttons using
UI and Audio processing must never share a single thread loop.
| Thread | Priority | Role |
|---|---|---|
Main Thread (zicBox_UI) |
SCHED_OTHER (Normal) |
Hardware/desktop event loop, UI component state updates, display rendering. |
Audio Thread (zicBox_Audio) |
SCHED_FIFO (Priority 30) |
Realtime clock timing, step clock processing, multi-track DSP synth rendering, master FX, ALSA buffer writes. |
To fully leverage multi-core CPUs without starving UI rendering or the OS scheduler:
-
Core Reservation: Allocates worker threads reserving cores for UI and OS tasks (
workers = hardware_concurrency() - 2). -
2-Phase Pipeline:
-
Phase 1 (Serial Tick): Locks
audioMutex, advances step clocks, and collects per-track frame events (noteOn,noteOff,loadClip). -
Phase 2 (Parallel Render): Renders
Ntracks concurrently across worker threads into thread-local mix buffers, sums partial mixes, applies master FX (Scatter, Filter, Compressor, Tape saturation, Soft clip), and outputs 16-bit PCM to ALSA.
-
Phase 1 (Serial Tick): Locks
All synth, drum, and sample engines reside in audio/engines/.
Engines inherit from EngineBase<Derived> using the Curiously Recurring Template Pattern (CRTP):
#pragma once
#include "audio/engines/EngineBase.h"
class MyEngine : public EngineBase<MyEngine> {
public:
// CRITICAL: Size N must match the exact number of addParam calls!
Param params[4];
Param& pitch = addParam({ .key = "pitch", .label = "Pitch", .value = 60.0f, .max = 127.0f });
Param& cutoff = addParam({ .key = "cutoff", .label = "Cutoff", .value = 0.5f, .max = 1.0f });
Param& reso = addParam({ .key = "reso", .label = "Reso", .value = 0.1f, .max = 0.95f });
Param& volume = addParam({ .key = "vol", .label = "Volume", .value = 0.8f, .max = 1.0f });
MyEngine(const float sampleRate = 44100.0f)
: EngineBase(Synth, "MyEngine", params)
, sampleRate(sampleRate) {}
// Required audio render callback
float sampleImpl() { return 0.0f; }
// Optional callbacks
void noteOnImpl(uint8_t note, float velocity) { pitch.value = note; }
void noteOffImpl(uint8_t note) {}
private:
float sampleRate;
};Why CRTP? On embedded microcontrollers (such as STM32H7), virtual function calls inside high-frequency sample loops incur vtable lookup overhead and prevent compiler inlining. CRTP resolves implementation calls at compile time, eliminating vtable overhead.
- Declare a fixed-size
Param params[N]array whereNmatches the exact number ofaddParamregistrations. -
Safety Rule: Calling
addParam()more times than the size ofparamscauses out-of-bounds memory access and results in a runtime segmentation fault.
All reusable sequence and persistence components reside in audio/sequencer/:
audio/sequencer/
├── Step.h # Step data model & SEQ_STEPS override guard
├── Clip.h # Clip container & ParamValue data structures
├── Generator.h # Algorithmic pattern generators (Kick, Bass, Drum, Perc, Snare, Hat, Clap)
├── SequenceUtils.h # Sequence manipulation (stretch, compress, clear)
├── ClipChain.h # Clip chain helper utilities (add, remove, clear, toggle)
└── ProjectIO.h # JSON project & clip persistence (saveClip, loadClip, saveProject, loadProject)
-
Configurable Step Resolution:
Step.hdefines step structure guarded by#ifndef SEQ_STEPS(default: 64 steps), allowing compile-time overrides (-DSEQ_STEPS=32). -
Clip Container:
Clip.hstores track step sequences, active parameter key-value pairs (ParamValue), engine IDs, and note repeat settings. -
Algorithmic Generators:
namespace GeneratorinGenerator.hprovides pattern generation algorithms (Kick, Bass, Drum, Perc, Snare, Hat, Clap) with 4-parameter UI knob overloads and 1-parameter default function wrappers. -
SFINAE JSON Persistence:
ProjectIO.huses C++17 type traits (nlohmann/json.hpp) to serialize and deserialize project and clip files across differentTrackorStudiodata structures without tight coupling.
zicBox includes Agent Skills located in .agents/skills/. These skills provide specialized domain context and architectural guidelines for AI coding agents (such as Gemini, Antigravity, Claude, ChatGPT, etc.).
When building, refactoring, or extending a zicBox application using AI agents, the agent reads these skill definitions to strictly adhere to established project standards.
| Skill | Path | Description |
|---|---|---|
audio-engine |
.agents/skills/audio-engine |
Rules for creating audio engines (EngineBase CRTP inheritance, parameter array bounds safety, file location, callback casting). |
audio-sequencer |
.agents/skills/audio-sequencer |
Standards for sequencer steps, clips, pattern generators (namespace Generator), clip chains, and SFINAE JSON persistence. |
firmware-architecture |
.agents/skills/firmware-architecture |
Application architecture rules (HAL separation, POSIX realtime threads, TrackRenderPool multi-core rendering, screenshot pipeline). |
AI agents equipped with these skills can autonomously perform complex development tasks:
-
Creating New Audio Engines: Prompt the agent to generate a new synth or drum engine (e.g., "Create an FM Percussion engine"). The agent uses the
audio-engineskill to automatically write a CRTP engine with exactParam params[N]sizing and register it inaudio/engines/. -
Adding Sequencer Generators & FX: Prompt the agent to add pattern generators or sequence manipulators. The agent uses the
audio-sequencerskill to follownamespace Generatordesign patterns. -
Target Porting & Hardware Architecture: Prompt the agent to structure a new firmware target or application view. The agent uses
firmware-architectureto enforce HAL decoupling, real-time priority allocation (SCHED_FIFO), and multi-coreTrackRenderPoolrendering.
When creating a new zicBox application, structure project files according to this standard format:
myApp/
├── zic.cpp # Main entry point (ALSA initialization, UI loop, realtime audio thread startup)
├── audioWorker.h # Audio thread loop, ALSA management, sequencer clock, multi-track rendering
├── runtimeDesktopSFML.h # Desktop SFML window driver, keyboard/mouse mapping, headless screenshot mode
├── runtimeHardware.h # Target hardware GPIO keys, rotary encoders, ST7789 display driver, config loader
├── studio.h (or state.h) # Application state container, track arrays, shared audio mutexes
├── ui.h # Main UI coordinator, view state switcher, unified event dispatcher
├── ui*.h # Individual modular UI view components (e.g. uiTrack.h, uiSeq.h, uiMenu.h)
└── makefile # Build configuration defining DRAW_SMFL for desktop or embedded targets
Previous: Home | Next: 05-Hardware
02-Architecture-&-Development-Guide
- Architecture Overview
- Runtime Layer
- Threading Model
- Multi-Core Audio Rendering
- Audio Engines
- Engine Parameters
- Sequencer
- Typical Application Flow
- Typical Project Layout
- Desktop-First Development
- AI Coding Assistant Support
- Design Principles