zdsp-core is a header-only C++ shared component — JUCE-based DSP headers, no separate library artifact and no build step of its own. It is consumed by add_subdirectory(zdsp-core) + linking the zdsp::core interface target; JUCE (juce_audio_basics + juce_core) must already be present in the consuming project. This page describes the unit surface, where each unit came from, and how apps consume it. Every unit is the generic kernel of something the Audio-Haxor audio-engine had already shipped inline. Paid product — proprietary (UNLICENSED), part of the paid audio stack.
DSP units
| header | type | what |
|---|---|---|
zdsp/ola_time_stretch.h | zdsp::OlaTimeStretch + SpeedMode | OLA time stretcher — change playback speed without altering pitch. Hann-windowed overlap-add, 75% overlap (synthesis hop = window/4); analysis hop = synthHop × speed. Stereo. prepare() → process(src, speed, buffer) → reset(), speed ∈ [0.25, 4.0]. SpeedMode = Resample (pitch follows rate) vs TimeStretch (preserve pitch). |
zdsp/channel_strip.h | zdsp::DspAtomics + applyDspFrame | Lock-free channel strip — 3-band IIR EQ (low-shelf 200 Hz / mid-peak 1 kHz / high-shelf 8 kHz) + gain + pan, optional mono fold. Params held in RT-safe DspAtomics (float-as-uint32 atomics); applyDspFrame processes one stereo frame against caller-owned IIR filter state. |
zdsp/tone_audio_source.h | zdsp::ToneAudioSource | Sine test-tone generator juce::AudioSource (configurable toneHz/toneGain), silence when off, with optional spectrum/scope taps. |
zdsp/input_peak_callback.h | zdsp::InputPeakCallback | Input peak meter juce::AudioIODeviceCallback — abs-max per block with exponential release (decay); read peak (0..1). |
zdsp/spectrogram_analysis.h | zdsp::computeSpectrogramGrid | Offline spectrogram analysis — mono samples → heightPx × widthPx dBFS grid (−100..0), Hann-windowed STFT, log→linear binning. The DSP half of ZGui.viz spectrogram/waterfall (this computes, those draw). Plus magnitudeToDb. |
zdsp/lock_free_stream_source.h | zdsp::LockFreeStreamSource | RT-safe streaming source + stream→RAM hot-swap — moves file I/O to a background TimeSliceThread, hands the audio thread a power-of-two ring buffer (spinlock counters, no CoreAudio-blocking CriticalSection). requestReaderSwap routes a RAM-backed reader glitch-free mid-playback; models a "virtual EOF" so loop-off plays out to the iteration boundary. |
zdsp/dsp_stereo_file_source.h | zdsp::DspStereoFileSource (+ InsertChain) | The playback orchestrator — a transport-ready stereo file source composing all of the above: tape-speed (ResamplingAudioSource) or OlaTimeStretch, optionally fed by LockFreeStreamSource; per-sample applyDspFrame channel strip, optional InsertChain plugin rack (the app implements it), mono fold, seek-fade, reverse-RAM path, peak/spectrum/scope taps. |
JUCE modules per header
zdsp-core includes JUCE headers directly, so the consuming target must link the modules each header pulls in — they are not uniform across the seven units. channel_strip.h also uses std::bit_cast, which needs C++20 on the consuming target even though zdsp::core itself only requires C++17 (the test harness sets cxx_std_20 for this reason).
| header | juce modules |
|---|---|
ola_time_stretch.h | juce_audio_basics, juce_core |
channel_strip.h | juce_dsp, juce_core |
tone_audio_source.h | juce_audio_basics, juce_core |
input_peak_callback.h | juce_audio_devices, juce_core |
spectrogram_analysis.h | juce_dsp, juce_core |
lock_free_stream_source.h | juce_audio_basics, juce_core |
dsp_stereo_file_source.h | the three composed headers + juce_audio_formats, juce_dsp |
How it composes
DspStereoFileSource is the single orchestrator; every other unit is a piece it wires together. As a juce::PositionableAudioSource driven from the app's AudioTransportSource, its forward path per block runs: AudioFormatReaderSource (optionally wrapped in LockFreeStreamSource to move disk I/O off the audio thread) → tape-speed ResamplingAudioSource or OlaTimeStretch.process per SpeedMode → mono-source upmix → seek-fade ramp (kSeekTotalSamples = 1536: a 1024-sample mute then a 512-sample ramp) → per-sample applyDspFrame channel strip (3-band EQ → gain → pan) → the app's InsertChain when active → mono fold (deferred past the inserts so stereo plugins can't undo it) → peak/spectrum/scope taps. Reverse playback is a separate branch reading a pre-decoded RAM buffer backwards with no inserts.
The one app coupling that was abstracted out is the InsertChain interface — four pure-virtual methods (prepare(sampleRate, maxBlock), release(), isActive(), process(buffer, startSample, numSamples)) that zdsp-core declares and the consuming app implements to host VST/AU plugins. The orchestrator calls prepare/release alongside its own lifecycle and process after the channel strip when isActive(); plugin scanning, hosting, and state stay entirely in the app.
Extraction provenance
All seven units are verbatim ports out of the Audio-Haxor audio-engine's Engine.cpp — engine constants became configurable members, appLogLine became an optional logLine, and the in-app plugin chain became the InsertChain interface. The full DSP stack and the playback pipeline now live here; what stays in each consuming app is only the file-format wiring, IPC/transport plumbing, plugin scanning, and the InsertChain implementation.
| unit | extracted from |
|---|---|
all 7 units (OLA stretch, channel strip, tone, peak meter, spectrogram, lock-free streaming, DspStereoFileSource orchestrator) | Audio-Haxor audio-engine (Engine.cpp) |
Tests
A headless JUCE console app (tests/zdsp_tests.cpp) runs known-answer unit tests over the pure-math surface of every header — dB↔linear conversion, power-of-two reduction, the spectrogram STFT frequency response (DC → bottom row, Nyquist → top row), the channel-strip pan law + biquad EQ DC gain, the OLA Hann window + constant-overlap-add property, the tone oscillator period/amplitude, and the peak meter's exponential release. It builds against the host JUCE and returns non-zero on failure: cmake -S tests -B tests/build && cmake --build tests/build && ctest --test-dir tests/build --output-on-failure (pass -DZDSP_JUCE_DIR=/path/to/JUCE to override the JUCE checkout).
Consumption model
A consuming app adds zdsp-core as a submodule (typically under libs/), calls add_subdirectory(libs/zdsp-core), and links zdsp::core. Because the library is header-only and links the host's own JUCE targets, there is no version skew between the app's JUCE and the DSP code. The units carry no host-specific assumptions beyond the JUCE modules each header links (see the table above) — they run identically inside Audio-Haxor's Tauri host and the JUCE plugin WebViews.