- C++ 82%
- CMake 14.2%
- Shell 3.8%
- PipeWire RT thread → SPSC lock-free ring buffer → main thread - Dear ImGui + SDL2/OpenGL 3.3 : waveform, spectrum, VU meter, controls - DspContext header-only portable vers Daisy Seed (SineOscillator, OnePoleLP) - CMake multi-config (Release/Debug/ASan/TSan) avec résolution ImGui flexible - Tests GTest : ring buffer (SPSC thread-safety), DSP (propriétés mathématiques), visual buffer (FFT) |
||
|---|---|---|
| extern | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| .gitmodules | ||
| .projectile | ||
| CMakeLists.txt | ||
| imgui.ini | ||
| README.md | ||
proto-audio
Base de prototypage audio temps réel — PipeWire + Dear ImGui + SDL2/OpenGL + C++20.
Stack
| Couche | Tech | Rôle |
|---|---|---|
| Audio RT | libpipewire-0.3 | Callback temps réel |
| Ring buffer | SPSC lock-free | Pont RT thread ↔ main thread |
| UI | Dear ImGui + SDL2/OpenGL 3.3 | Panels, sliders, canvas |
| DSP | C++20, header-only | Ton code DSP ici |
Architecture des threads
Main thread (SDL/ImGui ~60fps)
│ lit depuis ring buffer (lock-free pop)
│ calcule FFT pour spectrum
│ render ImGui + OpenGL
│
└─── [lock-free ring buffer] ←── PipeWire RT thread
appelle DspContext::process()
push samples dans ring buffer
Règle d'or du RT thread : dans DspContext::process(), jamais de :
malloc/new/deletemutex.lock()/std::lock_guard- I/O (printf, fichier, réseau)
- Appels système bloquants
Les paramètres partagés (gain, freq, mix) passent par std::atomic<float>.
Structure du projet
proto-audio/
├── src/
│ ├── main.cpp # Boucle SDL2/ImGui/OpenGL
│ ├── audio/
│ │ ├── ring_buffer.hpp # SPSC lock-free (RT↔main)
│ │ ├── pw_engine.hpp
│ │ └── pw_engine.cpp # Wrapper PipeWire
│ ├── dsp/
│ │ └── dsp_core.hpp # Ton DSP — portable vers Daisy
│ └── ui/
│ └── panels.hpp # ImGui panels + canvas ImDrawList
├── tests/
│ ├── test_ring_buffer.cpp
│ ├── test_dsp.cpp
│ ├── test_visual_buffer.cpp
│ └── CMakeLists.txt
├── scripts/
│ └── build.sh # Point d'entrée unique
├── docs/
│ └── ARCHITECTURE.md # Threads, portabilité Daisy, workflow TDD
├── build/ # Généré par cmake — non versionné
├── compile_commands.json # Symlink → build/ pour clangd
├── .gitignore
├── CMakeLists.txt
└── README.md
Installation des dépendances (Arch Linux)
# Dépendances système
sudo pacman -S pipewire pipewire-alsa pipewire-jack \
sdl2 mesa libgl \
cmake clang
# ImGui — soit depuis AUR
yay -S imgui
# Soit en clonant dans extern/ (si AUR non disponible)
mkdir extern && cd extern
git clone https://github.com/ocornut/imgui.git --branch v1.91.0
Build et utilisation
Tout passe par le script scripts/build.sh :
chmod +x scripts/build.sh
./scripts/build.sh # Release build
./scripts/build.sh debug # Debug + ASan/UBSan
./scripts/build.sh test # Debug + tous les tests GTest
./scripts/build.sh tsan # ThreadSanitizer (tests SPSC)
./scripts/build.sh clean # Supprime tous les build/
Lancer le binaire :
./build/proto-audio # duplex (capture + playback)
./build/proto-audio capture # analyse micro seulement
./build/proto-audio playback # génération seulement
Le script crée automatiquement compile_commands.json → build/
pour clangd et Doom Emacs.
Dans ~/.doom.d/config.el :
(after! lsp-clangd
(setq lsp-clients-clangd-args
'("--background-index" "--clang-tidy" "--header-insertion=never")))
Ajouter ton DSP
Tout se passe dans src/dsp/dsp_core.hpp, dans DspContext::process() :
void process(const float* in, float* out, uint32_t frames) {
for (uint32_t i = 0; i < frames; ++i) {
float in_l = in[i * 2 + 0];
float in_r = in[i * 2 + 1];
// ← Ton DSP ici
float out_l = my_filter_l.process(in_l);
float out_r = my_filter_r.process(in_r);
out[i * 2 + 0] = out_l;
out[i * 2 + 1] = out_r;
}
}
Pour exposer un paramètre à l'UI, ajoute un std::atomic<float> dans DspParams
et un slider dans draw_control_panel().
Portabilité vers Daisy Seed
Le callback DSP est conçu pour être identique à libDaisy :
| proto-audio | Daisy Seed |
|---|---|
DspContext::process(in, out, frames) |
AudioCallback(in, out, size) |
std::atomic<float> params |
Variables globales volatiles |
SineOscillator, OnePoleLP |
Identiques (no stdlib needed) |
La migration se résume à : copier dsp_core.hpp, remplacer les atomics par
des volatiles, plugger dans le AudioCallback Daisy.
Tests (TDD — Google Test)
# Dépendance système (ou FetchContent automatique si absent)
sudo pacman -S gtest
# Build Debug + run tous les tests
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc) --target run_tests
# Via CTest directement
cd build && ctest --output-on-failure --parallel
# Un seul binaire de test (listing + filtre)
./build/tests/test_dsp --gtest_list_tests
./build/tests/test_dsp --gtest_filter="DspContextTest.*"
# Watch avec entr
ls tests/*.cpp src/**/*.hpp | entr -c \
sh -c "cmake --build build -j$(nproc) && ./build/tests/test_dsp"
Structure des tests
| Fichier | Fixture | Ce qui est testé |
|---|---|---|
test_ring_buffer.cpp |
RingBufferTest + RingBufferSpsc |
FIFO, wrap, limites, SPSC thread-safety |
test_dsp.cpp |
SineOscillatorTest, OnePoleLPTest, DspContextTest |
Propriétés mathématiques, bypass, NaN, paramètres atomiques |
test_visual_buffer.cpp |
VisualBufferTest, VisualBufferFftTest |
Waveform circulaire, peak detection+release, FFT bins |
Philosophie TDD appliquée ici
Les tests DSP testent des propriétés mathématiques, pas des valeurs arbitraires :
rms(sinus) ≈ 1/√2— propriété universelle, pas0.7071067gain_filtre(Fc) ≈ -3 dB— définition même de Fcpente ≈ -6 dB/oct— propriété du filtre 1er ordre IIR
La distinction ASSERT_* vs EXPECT_* est intentionnelle :
ASSERT_EQ: si ça échoue, continuer le test n'a pas de sens (ex: taille buffer)EXPECT_FLOAT_EQ/EXPECT_NEAR: on veut voir tous les échecs même si un seul bin est faux
Pour ajouter un composant DSP en TDD :
- Écris le test qui décrit la propriété → RED
- Implémente le minimum → GREEN
- Refactorise → REFACTOR
Mode ThreadSanitizer (data races)
cmake -B build-tsan -DCMAKE_BUILD_TYPE=Tsan
cmake --build build-tsan -j$(nproc)
./build-tsan/tests/test_ring_buffer --gtest_filter="*Spsc*"
Roadmap
- ImPlot pour des graphes plus riches (waterfall FFT)
- Sauvegarde des paramètres (JSON)
- Plugin LADSPA/LV2 loader
- Export WAV depuis la visualisation
- Trigger d'oscilloscope (comme un vrai scope)