TriBand Upward Compressor (Rocket)
The TriBand Upward Compressor is a studio-grade transient designer and upward compressor guitar pedal running on the Electro-smith Daisy Seed. It leverages a custom block-based, phase-linear LeGall 5/3 Discrete Wavelet Transform (DWT) algorithm to shape transients without muddying the frequency spectrum.

Schematic Hardware Breakdown
The diagram above provides the complete hardware framework to securely run the TriBand Upward Compressor on the Daisy Seed:
-
Power Supply & Virtual Ground: A
+9VDC supply is utilized to power the input/output Op-Amps. A classic voltage divider consisting of two \(10\text{k}\Omega\) resistors (R1andR2) bisects the 9V supply down into a clean4.5Vvirtual ground (\(V_{\text{ref}}\)) for the op-amp biasing. Bulk electrolytic caps (\(100\mu\text{F}\) and \(47\mu\text{F}\)) aggressively filter power-line rippling. -
Audio Input Buffer (OPA2134 - U1A): The raw
Pedal INPUTis immediately AC-coupled via a \(0.1\mu\text{F}\) film capacitor and fed into a non-inverting unity-gain buffer. This presents a massive \(1\text{M}\Omega\) input impedance (R4) to the guitar pickups, completely preventing 'tone suck' and preserving pristine high-end detail before passing the AC signal to the Daisy Seed'sAUDIO_IN_1(Pin 16). -
Audio Output Buffer (OPA2134 - U1B): The processed digital signal exits from the Daisy Seed's
AUDIO_OUT_1(Pin 18), passing securely into the second half of the audiophile-grade Op-Amp to be adequately buffered to a strong, low-impedance stream. Before reaching thePedal OUTPUTjack, the signal is conditioned through a dedicated RC output network:- A \(100\Omega\) series resistor (
R5) protects the op-amp from short circuits and stabilizes the output against the capacitance of long instrument cables. - A \(1\mu\text{F}\) AC-coupling film capacitor (
C4) blocks any underlying DC bias voltage, ensuring only the pure audio waveform is passed. - A \(1\text{M}\Omega\) pull-down resistor (
R6) continuously bleeds accumulated DC charge from the capacitor to ground, guaranteeing completely silent, 'pop'-free switching and cable plugging.
- A \(100\Omega\) series resistor (
-
DSP Potentiometer Controls: Three \(100\text{k}\Omega\) potentiometers (
RV1,RV2,RV3) control the algorithm's Mix, Threshold, and Focus. These are fed using the Daisy's onboard pristine+3.3Vanalog reference voltage instead of+9V, and are directly mapped as safe control voltages returning into ADC pinsA0(Pin 22),A1(Pin 23), andA2(Pin 24).
DSP Engine Features
- 3-Level Cascading Wavelet Transform: The incoming mono signal is mathematically decomposed into three distinct bands (High/Pick, Mid/Snap, Low/Body) natively across a 64-sample boundary-perfect shifting block.
- Selective Upward Compression: Unlike standard compressors that downwardly squash loud signals, this engine emphasizes hidden transient details by applying upward expansion to quiet coefficients within each wavelet scale.
- Array-Mapped Overlaps: To eliminate audio distortion and phase alignment issues, the algorithm leverages multi-level array tracking (
prev_even[3],prev_odd[3]) to precisely carry block boundaries into the future block across all recursive zoom scales.
Hardware Controls
The physical control knobs are read continuously through the hardware ADC. They feature 1-pole low-pass filtering to prevent "zipper noise" when swept.
- Mix (A0): Controls an equal-power crossfade between the dry instrument and the selectively compressed DWT signal.
- Threshold (A1): Trims the target dB threshold of the Upward Compressor (sweeps between
-40dBand0dB). - Focus (A2): The 3-way continuous parameter knob directing the DWT's localized targeting:
- 0.0 to 0.5 (Zone 1): Sweeps the equal-power focus from the Highs (D1) smoothly into the Mids (D2).
- 0.5 to 1.0 (Zone 2): Completes the 3-way sweep, fading focus from the Mids (D2) fully into the Lows (D3).
Technical Deep Dive: Frequencies & Dynamics
Wavelet Frequency Decomposition
Assuming a standard Daisy Seed 48kHz sample rate (yielding a Nyquist limit of 24kHz), the 3-level LeGall 5/3 wavelet cascade naturally subdivides the spectrum into octaves. Because frequency and scale are inversely proportional within a Discrete Wavelet Transform:
- D1 (Highs/Pick): The highest frequency (~12kHz to 24kHz) but the lowest scale (finest micro-detail). Controls the sharp, fastest high-frequency clicking transients of a plastic guitar pick.
- D2 (Mids/Fingernail Snap): The mid-high frequency (~6kHz to 12kHz) at a medium scale. This effectively isolates the slightly slower, metallic percussive fretboard snap or aggressive fingernail harmonics.
- D3 (Lows/Thumb Body): The lowest frequency (~3kHz to 6kHz) at the highest scale (coarsest macro-detail). This highlights the slowest, widest acoustic resonance and tactile flesh "thump" of the thumb or palm against the body. (Frequencies below 3kHz are passed alongside cleanly within the core 'approximation' stream without localized dynamic coloring).
The Upward Compressor
Traditional compressors trigger downward attenuation when a signal safely exceeds a maximum value. The upward compressor instead identifies incredibly quiet transients taking place below a designated ceiling and lifts them up.
- Dynamic Threshold Mapping: The physical Threshold (A1) knob allows immediate sliding of the compressor's ceiling anywhere across a span from -40dBFS up to 0dBFS.
- Fixed DSP States:
- Transient Envelope Attack (1.0ms): Near-instant transient tracking configured solely to aggressively capture the fastest micro-transients.
- Transient Envelope Release (50.0ms): A moderately short release tail tailored to gracefully relax tracking to complement a string's natural acoustic decay.
- Compression Ratio (2:1): Emphasizes targeted coefficients gracefully via a musical 2:1 expansion multiplier.
- Noise Floor (-60.0dB): Explicitly isolates the noise floor to prevent dragging up ambient line hiss or 60-cycle hum. Any frequencies reading below -60dBFS automatically bypass the
gain_multiplierlogic. - Soft Knee (6.0dB): Eases the threshold transition calculations naturally matching analog tubes.
Background
The Frequencies of the Fretboard
To understand why the Wavelet targets are placed where they are, we first need to look at the mathematical limits of a standard guitar.
Assuming an equal-temperament tuning of A4 = 440 Hz, a standard 22-fret six-string guitar produces the following fundamental frequency range:
- Lowest Note (Open Low E string): E2 (~82.41 Hz)
- Highest Note (22nd Fret on High E string): D6 (~1174.66 Hz)
While the actual fundamental pitches of the guitar stop entirely before 1.2kHz, the instrument's percussive physical characteristics—such as the attack of the plastic pick, the snap of the string against the frets, and the resonant "thump" of the strumming hand—contain complex harmonic and inharmonic content extending far beyond 10kHz.
By pushing the Wavelet transformation bands into the upper kilohertz ranges (3kHz to 24kHz), the engine fundamentally ignores the sustained pitch of the notes (passing them safely through the low-frequency stream), to surgically target solely the high-frequency transient character of your playing.
Sampling Limits and Aliasing Mitigation
The standard Electro-smith Daisy Seed architecture processes audio at a 48kHz sampling rate. According to the Nyquist-Shannon sampling theorem, the absolute maximum frequency a digital system can accurately reproduce is exactly half its sampling rate—a mathematical ceiling known as the Nyquist frequency (which is exactly 24kHz here).
If the pedal attempted to process, generate, or upwardly compress frequency content exceeding this strict 24kHz ceiling, those ultra-high pitches wouldn't just disappear. Instead, they would "fold back" or alias down into the audible spectrum as mathematically unrelated frequencies.
By meticulously defining the absolute upper bound of the D1 (Highs) Wavelet band exactly at the 24kHz Nyquist limit, the DSP naturally restricts all upward compression algorithms to only physically reproducible octaves. This prevents dragging ugly, folded-back aliasing distortion into your signal.
Requirements & Dependencies
Software Stack
- Language: C++14 (compiled under
-std=gnu++14) - Core Library:
libDaisy(Hardware Abstraction Layer mapping directly to the STM32 architecture) - Build System: GNU Make
- Compilers: ARM GNU Toolchain (
arm-none-eabi-gcc/arm-none-eabi-g++)
Compilation Instructions
Using standard make functionality, initialize a clean output build from your terminal:
# Clean cached objects and rebuild the firmware
make clean; make
# Flash the binary using your configured method
make program
Hardware & Serial Debugging (DEBUG=1 and DEBUG=2)
The main.cpp codebase is equipped with optional hardware-level diagnostics managed entirely by the DEBUG_LEVEL preprocessor directive.
DEBUG=1(Hardware Oscilloscope Tracing): Automatically re-routes the internal DSP envelope and gain factors directly to DAC OUT1 and DAC OUT2.DEBUG=2(USB Serial Plotting): Instantiates the DAC hooks above, AND activates all real-time tracking volume strings to the USB CDC console at 10Hz.
In standard compilation (omitting the overarching debug flag), all diagnostic variable tracking, DAC memory, and USB logs are explicitly stripped out by the compiler, assuring the maximum possible DSP stability and audio fidelity in live production.
Source Code & Build Configuration
main.cpp
/**
* @file main.cpp
* @brief Bloodhoney Upward Wavelet Compressor - Main DSP & Control Logic
*
* This file contains the complete top-to-bottom implementation for the
* Bloodhoney guitar pedal, running on the Electro-smith Daisy Seed. It features
* a 3-Level Cascading, phase-linear LeGall 5/3 Wavelet transform dynamically
* mapping audio across High, Mid, and Low bands with perfectly smooth overlapping
* block boundaries and localized upward dynamics compression.
*/
#include "daisy_seed.h"
#include "daisysp.h"
#include <math.h>
#include <string.h>
using namespace daisy;
using namespace daisysp;
// Hardware object representing the Daisy Seed board.
DaisySeed hw;
#if defined(DEBUG_LEVEL) && DEBUG_LEVEL >= 2
// Global variable to keep track of the peak input level for serial USB logging.
// This is stripped out of the binary unless compiled with DEBUG=2.
float peak_in = 0.0f;
#endif
// ============================================================================
// --- Bloodhoney DSP Classes ---
// ============================================================================
/**
* @brief A simple fast-attack, slow-release Envelope Detector.
* Uses a leaky integrator approach to track the amplitude of the incoming
* signal.
*/
class EnvelopeDetector {
float env_, attackCoef_, releaseCoef_;
public:
void Init(float sampleRate, float attackMs, float releaseMs) {
// Convert time constants in milliseconds to discrete-time filter
// coefficients
attackCoef_ = expf(-1.0f / (attackMs * 0.001f * sampleRate));
releaseCoef_ = expf(-1.0f / (releaseMs * 0.001f * sampleRate));
env_ = 0.0f;
}
float Process(float in) {
float rect = fabs(in); // Full-wave rectify the audio signal
// Apply different filter coefficients based on whether the signal is rising
// or falling
env_ = (rect > env_) ? (attackCoef_ * (env_ - rect) + rect)
: (releaseCoef_ * (env_ - rect) + rect);
return env_;
}
};
/**
* @brief An Upward Compressor used to emphasize details in the wavelet domain.
* Instead of squashing loud signals (downward), it amplifies quiet signals
* that fall below a given threshold.
*
* To prevent a massive "cliff" (digital chatter) when a signal decays across
* the noise floor boundary, this implementation uses a smooth downward expansion
* curve. As the envelope falls below the noise floor, the applied gain multiplier
* is smoothly ramped back down to 1.0 (0dB), allowing the physical string's natural
* decay to pass through cleanly and keeping the noise floor quiet.
*
* Performance Note: This compressor is evaluated across 56 wavelet coefficients
* per audio block. To save strict CPU cycles inside this inner loop, the core
* dB calculation is mathematically transposed into Base-2. This allows us to
* bypass expensive generic std::log10f and std::powf routines and leverage the
* heavily optimized polynomial daisysp::fastlog2f() approximations and hardware
* exp2f() operations.
*/
class UpwardCompressor {
float thresh_, ratio_, noiseFloor_, expansionRatio_, knee_;
public:
UpwardCompressor() {}
UpwardCompressor(float thresh, float ratio, float noiseFloor, float expansionRatio, float knee)
: thresh_(thresh), ratio_(ratio), noiseFloor_(noiseFloor), expansionRatio_(expansionRatio), knee_(knee) {}
void SetThreshold(float t) { thresh_ = t; }
float CalculateGainMultiplier(float envLevel) {
// Avoid calculating log(0) for pure silence
if (envLevel < 1e-6f)
envLevel = 1e-6f;
// Fast Base-2 conversion to Decibels.
// Mathematically: 20 * log10(x) = 20 * (log2(x) / log2(10))
// -> 20 / 3.321928 * log2(x) -> 6.0205999 * log2(x)
float envDb = 6.0205999f * daisysp::fastlog2f(envLevel);
// If the signal is louder than our threshold, it is in the "pass-through"
// region. No compression is applied (gain is 0dB, giving a 1.0x multiplier).
if (envDb >= thresh_)
return 1.0f;
float gainDb = 0.0f;
if (envDb >= noiseFloor_) {
// ----------------------------------------------------------------------
// REGION 1: Upward Compression
// For signals sitting comfortably between the Threshold and Noise Floor,
// apply gain. The quieter the signal gets, the MORE gain is applied.
// ----------------------------------------------------------------------
gainDb = (thresh_ - envDb) * (1.0f - (1.0f / ratio_));
} else {
// ----------------------------------------------------------------------
// REGION 2: Downward Expansion (The Noise Floor "Soft Gate")
// To prevent digital chatter at the boundary, we calculate the absolute
// maximum compression gain that was achieved EXACTLY at the noise floor...
// ----------------------------------------------------------------------
float maxGainDb = (thresh_ - noiseFloor_) * (1.0f - (1.0f / ratio_));
// ...and then gracefully subtract from that max gain the further the
// signal drops below the noise floor. An expansion ratio of 2.0 means
// for every 1dB the signal decays, the compressor pulls back 2dB of gain.
gainDb = maxGainDb - (noiseFloor_ - envDb) * expansionRatio_;
// The clamp ensures the multiplier never drops below 1.0x (0dB).
// We are only removing our artificial upward gain, NOT actually gating
// the fundamental dry guitar signal.
if (gainDb < 0.0f)
gainDb = 0.0f;
}
// Convert the calculated dB gain back to a linear multiplier for the audio signal.
// Mathematically: 10^(gainDb/20) = 2^(gainDb / 6.0205999) = 2^(gainDb * 0.1660964)
// We use the FPU optimized exp2f instead of powf for extreme performance speedups.
return exp2f(gainDb * 0.1660964f);
}
};
/**
* @brief A 1-pole lowpass filter for smoothing physical potentiometer
* movements. Prevents "zipper noise" when turning hardware knobs by gliding
* parameters to their targets.
*/
class SmoothedParameter {
float val_, target_, coef_;
public:
// smoothingTime dictates how fast the value catches up to the target knob
// position
SmoothedParameter(float smoothingTime) : val_(0), target_(0), coef_(smoothingTime) {}
void SetTarget(float t) { target_ = t; }
float Process() {
val_ += coef_ * (target_ - val_);
return val_;
}
};
// ============================================================================
// --- Wavelet DSP State Variables ---
// ============================================================================
// The hardware will trigger audio callbacks in blocks of exactly 64 samples.
// This must be a power of 2 for the wavelet math to subdivide cleanly.
const size_t BLOCK_SIZE = 64;
float work_buf[BLOCK_SIZE];
// Pi/2 constant used for Equal-Power Trigonometric crossfading.
// Translating a linear knob (0.0 to 1.0) through a 90-degree curve (Pi/2 radians)
// mathematically guarantees the combined audio power never dips in the center.
constexpr float PI_OVER_2 = 1.57079632679f;
// Boundary memory for the LeGall 5/3 lifting scheme.
// Because the wavelet transform looks slightly into the future and past, we
// must carry over the very last calculations from the previous audio block.
float prev_even[3] = {0.0f, 0.0f, 0.0f};
float prev_odd[3] = {0.0f, 0.0f, 0.0f};
// Instantiated DSP Objects
EnvelopeDetector transientEnv[3];
UpwardCompressor punchComp;
// Hardware Controls (Smoothed to prevent zipper noise)
SmoothedParameter mixKnob(0.005f); // Mapped to seed::A0
SmoothedParameter thresholdKnob(0.005f); // Mapped to seed::A1
SmoothedParameter focusKnob(0.005f); // Mapped to seed::A2 (Continuous 3-Way Focus Sweep)
// ============================================================================
// --- The Audio Processing Pipeline ---
// ============================================================================
/**
* @brief The primary 3-Level Wavelet processing subroutine.
*
* Mathematically, the Discrete Wavelet Transform (DWT) maintains an inverse
* relationship between Frequency and Scale. This decomposes the mono input into:
* - D1 (Highs/Pick) ............. : Highest Frequency (~12-24kHz) | Lowest Scale (Finest Detail)
* - D2 (Mids/Fingernail Snap) ... : Mid-High Freq (~6-12kHz) | Medium Scale
* - D3 (Lows/Thumb or Finger Body): Lowest Frequency (~3-6kHz) | Highest Scale (Coarsest Detail)
* *(Everything below 3kHz passes cleanly through the uncompressed Approximation stream).*
*
* Evaluates the continuous 'Focus' knob to map targeted upward compression exclusively
* across the D1, D2, or D3 layers before perfectly reconstructing the cascading
* block boundaries back to the output buffer.
*/
void ProcessEffect(const float *in, float *out, size_t size, float &out_max_env,
float &out_max_gain) {
out_max_env = 0.0f;
out_max_gain = 1.0f;
// 1. Hardware Parameters & Crossfade Math
float current_mix = mixKnob.Process();
punchComp.SetThreshold(thresholdKnob.Process());
float focus = focusKnob.Process();
float dry_level = cosf(current_mix * PI_OVER_2);
float global_wet_level = sinf(current_mix * PI_OVER_2);
// Map the continuous physical 'Focus' knob (0.0 to 1.0) to the three target Wavelet bands.
// We use an Equal-Power trigonometric mapping (cos/sin matching Pi/2 or 1.570796f) rather than a
// linear fade. This ensures the perceived compression intensity never drops (-3dB) while sweeping
// between the distinct octaves.
float focus_gains[3] = {0.0f, 0.0f, 0.0f};
if (focus < 0.5f) {
// Zone 1 (0.0 to 0.5): Sweep focus from D3 (Lows/Thumb: ~3-6kHz) into D2 (Mids/Fingernail
// Snap: ~6-12kHz). We multiply `focus` by 2.0 to stretch the first half-turn of the physical
// knob into a full 0.0 to 1.0 math range.
float x = focus * 2.0f;
focus_gains[2] = cosf(x * PI_OVER_2); // Smoothly fades D3 Gain DOWN
focus_gains[1] = sinf(x * PI_OVER_2); // Smoothly fades D2 Gain UP
} else {
// Zone 2 (0.5 to 1.0): Sweep focus from D2 (Mids/Fingernail Snap: ~6-12kHz) into D1 (Highs/Pick
// : ~12-24kHz). We subtract 0.5 and multiply by 2.0 to stretch the second half-turn into a
// fresh 0.0 to 1.0 math range.
float x = (focus - 0.5f) * 2.0f;
focus_gains[1] = cosf(x * PI_OVER_2); // Smoothly fades D2 Gain DOWN
focus_gains[0] = sinf(x * PI_OVER_2); // Smoothly fades D1 Gain UP
}
for (size_t i = 0; i < size; i++) {
work_buf[i] = in[i];
}
// --- 2. FORWARD DWT (3-Level Cascade) ---
int current_size = size;
int step = 1;
for (int lvl = 0; lvl < 3; lvl++) {
// A. Resolve the boundary detail from the previous audio block
// work_buf[0] always holds the first even sample of the new block
float boundary_detail = prev_odd[lvl] - 0.5f * (prev_even[lvl] + work_buf[0]);
(void)boundary_detail; // Suppress unused warning
// B. Predict Step (Calculate Detail)
for (int i = 0; i < current_size / 2 - 1; i++) {
work_buf[(2 * i + 1) * step] -=
0.5f * (work_buf[(2 * i) * step] + work_buf[(2 * i + 2) * step]);
}
// C. Update Step (Calculate Approximation)
for (int i = 1; i < current_size / 2; i++) {
work_buf[(2 * i) * step] +=
0.25f * (work_buf[(2 * i - 1) * step] + work_buf[(2 * i + 1) * step]);
}
// --- 3. TARGETED COMPRESSION (Only run if Focus knob allows it) ---
if (focus_gains[lvl] > 0.001f) {
// Calculate the final wet mix amount for this specific level
float level_wet_mix = global_wet_level * focus_gains[lvl];
for (int i = 0; i < current_size / 2 - 1; i++) {
int detail_index = (2 * i + 1) * step;
float detail_dry = work_buf[detail_index];
// Use the specific envelope detector for this Wavelet scale
float env_level = transientEnv[lvl].Process(detail_dry);
float gain_multiplier = punchComp.CalculateGainMultiplier(env_level);
if (env_level > out_max_env)
out_max_env = env_level;
if (gain_multiplier > out_max_gain)
out_max_gain = gain_multiplier;
float detail_wet = detail_dry * gain_multiplier;
// Recombine dry and scaled wet signals in-place
work_buf[detail_index] = (detail_dry * dry_level) + (detail_wet * level_wet_mix);
}
}
// D. Save state for the NEXT audio block boundary
prev_even[lvl] = work_buf[(current_size - 2) * step];
prev_odd[lvl] = work_buf[(current_size - 1) * step];
// E. Prepare stride and size for the next Wavelet level down
step *= 2;
current_size /= 2;
}
// --- 4. INVERSE DWT (3-Level Reconstruction) ---
step = 4; // Reset to Level 3 stride
current_size = size / 4;
for (int lvl = 2; lvl >= 0; lvl--) {
// Reverse Update Step
for (int i = 1; i < current_size / 2; i++) {
work_buf[(2 * i) * step] -=
0.25f * (work_buf[(2 * i - 1) * step] + work_buf[(2 * i + 1) * step]);
}
// Reverse Predict Step
for (int i = 0; i < current_size / 2 - 1; i++) {
work_buf[(2 * i + 1) * step] +=
0.5f * (work_buf[(2 * i) * step] + work_buf[(2 * i + 2) * step]);
}
step /= 2;
current_size *= 2;
}
// 5. Output
for (size_t i = 0; i < size; i++) {
out[i] = work_buf[i];
}
}
/**
* @brief The hardware audio interrupt callback.
* Daisy automatically fires this function every time it needs a new buffer.
*/
static void Callback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) {
float max_env = 0.0f;
float max_gain = 1.0f;
// Hand off audio processing to our custom subroutine (Processing Left channel
// only)
ProcessEffect(in[0], out[0], size, max_env, max_gain);
#if defined(DEBUG_LEVEL) && DEBUG_LEVEL >= 1
// Output diagnostics to DAC OUT1 and DAC OUT2 for oscilloscope visualization
// DAC1 -> Envelope Level (0.0 to 1.0 maps to 0 to 4095)
uint16_t dac1_val = (uint16_t)(max_env * 15.0f * 4095.0f);
if (dac1_val > 4095)
dac1_val = 4095;
// DAC2 -> Gain Multiplier
// Subtract 1.0 (no gain = 0V out).
// Compressors use decibels, so a 3.0x linear multiplier is a massive +9.5dB push!
// Multiplying by 1500 forces a 3.0x multiplier to hit max screen height (3.3V)
float clamped_gain = max_gain - 1.0f;
if (clamped_gain < 0.0f)
clamped_gain = 0.0f;
uint16_t dac2_val = (uint16_t)(clamped_gain * 1500.0f);
if (dac2_val > 4095)
dac2_val = 4095;
hw.dac.WriteValue(DacHandle::Channel::ONE, dac1_val);
hw.dac.WriteValue(DacHandle::Channel::TWO, dac2_val);
#if DEBUG_LEVEL >= 2
// Optional block to measure the absolute peak input on the left channel for
// USB logging
for (size_t i = 0; i < size; i++) {
float val = fabs(in[0][i]);
if (val > peak_in) {
peak_in = val;
}
}
#endif
#endif
}
// ============================================================================
// --- Hardware Initialization & Control Loop ---
// ============================================================================
int main(void) {
// 1. Initialize the board and hardware peripherals (clocks, GPIOs, etc)
hw.Configure();
hw.Init();
#if defined(DEBUG_LEVEL) && DEBUG_LEVEL >= 1
// Initialize DACs for visual oscilloscope tracking
DacHandle::Config dac_config;
dac_config.bitdepth = DacHandle::BitDepth::BITS_12;
dac_config.buff_state = DacHandle::BufferState::ENABLED;
dac_config.mode = DacHandle::Mode::POLLING;
dac_config.chn = DacHandle::Channel::BOTH;
hw.dac.Init(dac_config);
#if DEBUG_LEVEL >= 2
// Start logging over USB CDC, without halting the boot process for a serial
// terminal
hw.StartLog(false);
#endif
#endif
// Lock the audio block size to a strict power of 2 for the wavelet math
hw.SetAudioBlockSize(BLOCK_SIZE);
float sample_rate = hw.AudioSampleRate();
// 2. Initialize DSP Classes
// 1ms Attack to catch the pick transient, 50ms Release for a natural guitar
// decay
for (int i = 0; i < 3; i++) {
transientEnv[i].Init(sample_rate, 1.0f, 50.0f);
}
// Upward Compressor: -20dB thresh, 2:1 ratio, downward expansion below -60dB,
// 2.0x expansion ratio, 6dB soft knee
punchComp = UpwardCompressor(-20.0f, 2.0f, -60.0f, 2.0f, 6.0f);
// 3. Configure the Analog-to-Digital Converters (ADC) for the potentiometers
AdcChannelConfig adcConfig[3];
adcConfig[0].InitSingle(seed::A0); // Mix Knob
adcConfig[1].InitSingle(seed::A1); // Threshold Knob
adcConfig[2].InitSingle(seed::A2); // Focus Knob
hw.adc.Init(adcConfig, 3);
hw.adc.Start();
// 4. Start the Audio Processing thread (Callback runs continuously in the
// background now)
hw.StartAudio(Callback);
#if defined(DEBUG_LEVEL) && DEBUG_LEVEL >= 2
// Timer for printing to the Serial console
uint32_t last_print = System::GetNow();
#endif
// 5. The Infinite Control Loop
// This runs continuously in the foreground, updating slow-moving components
// like knobs
while (1) {
// Read the ADC pins (values return as floats from 0.0 to 1.0)
float raw_mix = hw.adc.GetFloat(0);
float raw_thresh = hw.adc.GetFloat(1);
float raw_focus = hw.adc.GetFloat(2);
// Map the raw threshold (0.0 to 1.0) to a usable decibel range (e.g., -40dB
// to 0dB)
float mapped_thresh_dB = (raw_thresh * 40.0f) - 40.0f;
// Feed the raw hardware readings directly to the smoothers
mixKnob.SetTarget(raw_mix);
thresholdKnob.SetTarget(mapped_thresh_dB);
focusKnob.SetTarget(raw_focus);
// Wait exactly 1ms to prevent overloading the control loop/processor
System::Delay(1);
#if defined(DEBUG_LEVEL) && DEBUG_LEVEL >= 2
uint32_t now = System::GetNow();
// Print at a smooth 10Hz rate (every 100ms)
if (now - last_print > 100) {
last_print = now;
// Capture and reset peak level for the next frame
float current_peak = peak_in;
peak_in = 0.0f;
// Calculate the decibel level, clamping at tiny values to avoid log(0)
// errors
float peak_db = -100.0f;
if (current_peak > 0.00001f) {
peak_db = 6.0205999f * daisysp::fastlog2f(current_peak);
}
// Print the peak value to the USB Serial terminal
hw.PrintLine("Peak in: " FLT_FMT3 " (dBFS: " FLT_FMT3 ")", FLT_VAR3(current_peak),
FLT_VAR3(peak_db));
}
#endif
}
}
Makefile
# Project Name
TARGET = TriBandUpwardCompressor
# Sources
CPP_SOURCES = main.cpp
# Library Locations (pointing to your local submodules)
LIBDAISY_DIR = ./libDaisy
DAISYSP_DIR = ./DaisySP
ifdef DEBUG
CPP_DEFS += -DDEBUG_LEVEL=$(DEBUG)
C_DEFS += -DDEBUG_LEVEL=$(DEBUG)
endif
# Core location, and generic Makefile.
SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core
include $(SYSTEM_FILES_DIR)/Makefile