A lightweight C++ template for rapid Audio & DSP prototyping. dsplayground is a minimalist boilerplate designed to let you jump straight into DSP coding without worrying about backend plumbing. It combines the immediate-mode GUI power of Dear ImGui with the modern, low-latency audio capabilities of PipeWire.
  • C++ 82%
  • CMake 14.2%
  • Shell 3.8%
Find a file
Julien Mazari Garcia e5ad83b8dd Base de prototypage audio temps réel en C++20 :
- 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)
2026-03-20 11:05:00 +01:00
extern Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
scripts Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
src Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
tests Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
.gitignore Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
.gitmodules Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
.projectile Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
CMakeLists.txt Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
imgui.ini Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00
README.md Base de prototypage audio temps réel en C++20 : 2026-03-20 11:05:00 +01:00

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 / delete
  • mutex.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, pas 0.7071067
  • gain_filtre(Fc) ≈ -3 dB — définition même de Fc
  • pente ≈ -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 :

  1. Écris le test qui décrit la propriété → RED
  2. Implémente le minimum → GREEN
  3. 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)