-
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
🚧 Help Wanted: Whether you're a coder, designer, maker, or musician, your contributions are more than welcome!
In addition to making your own zicBox build, you can take part in developing and improving the existing ecosystem. Contributions can range from hardware optimizations, firmware enhancements, and UI/UX improvements to documentation and testing. The project thrives on collaboration and shared experimentation.
The best way to get started is to reach out on the Discord thread, let's have a quick chat and figure out how you can jump in. Don't be shy, there are plenty of ways to get involved, no matter your background or experience.
The zicBox community has already produced multiple great enclosure designs — mostly 3D-printable and easy to assemble. But innovation doesn’t stop there! We’d love to see experiments with CNC carving, laser-cut acrylics, or even metal housings. If you’re a designer or maker interested in refining the look and feel of Zic Pixel and other builds, your ideas are more than welcome.
The current PCB was designed in a self-taught, DIY style. It works well, but the project is ready to evolve into something more modular and flexible.
The next phase focuses on creating a small, reusable core board that includes everything needed to power a custom zicBox build:
- Integrated audio codec
- MIDI interface (with optocoupler isolation)
- Optional battery controller
- I/O expander for additional inputs and outputs
The goal is to make this board compatible with a range of platforms — from Raspberry Pi models to microcontrollers like the ESP32 — making it easy to prototype and build new instruments or devices.
Although there’s currently no active CM4-based project, exploring a custom CM4 carrier board remains a future goal once the modular base design is solidified.
If you’re experienced in hardware design or want to help refine and expand this next-generation board, your input is highly appreciated.
Skills welcome:
- PCB design (currently working on EasyEDA)
- SMD component selection and layout
- Designing for manufacturing or open-source hardware projects
zicBox is primarily written in C++. The code is structured around reusable libraries, with plenty of room to expand:
- Create new audio modules
- Develop UI components
- Improve performance or interconnectivity between modules
Beginner to expert, every contribution is welcome, as long you know your way around C++.
Tip
To simplify development, install a cross-compiler toolchain for ARM on your dev machine. In order to do this, let's use zicOs a custom OS create for zicBox, based on buildroot: https://github.com/apiel/zicOs
Install alongside to your zicBox project:
- dev folder
-- zicBox
-- zicOs
Good design makes a huge difference, not only in how zicBox looks, but in how easy and enjoyable it is to use.
We’re looking for help with:
- UX Design: Simplifying and improving the user interface while keeping CPU usage minimal. Clean, low-overhead UI/UX patterns are key, thoughtful layout and flow.
- Visual Design: Logos, icons, UI assets, and visual elements for both the app and the GitHub/docs.
- Promotional Material: Graphics or layouts for sharing zicBox on social media, in videos, or presentations.
Skills welcome:
- UX thinking for embedded or constrained environments
- Design tools like Figma, Sketch, or even pen & paper
- Visual identity and branding for open source projects
Documentation often falls behind as development moves fast. We need help keeping things up-to-date and beginner-friendly.
Great contributions include:
- Updating documentation with recent changes
- Writing clear guides, diagrams, and module descriptions
- Improving the README and setup instructions
- Helping with onboarding and dev setup notes
Using zicBox and exploring its capabilities is a valuable form of contribution! You can help by testing features, reporting bugs, and creating preset examples or full projects.
Things to do:
- Create presets or musical setups
- Try new builds and report bugs or suggestions
- Share use cases and feedback
zicBox is best understood when seen in action. If you're into video content, your help is gold.
Ideas for video contributions:
- Walkthroughs and feature overviews
- Performance demos or live jams
- Setup tutorials and build guides
- Showcasing what zicBox can do!
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