From 4453c71d0b2aa0b3c3c4618a8baf6a24cdf97a9e Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Tue, 6 Jan 2026 22:10:33 -0600 Subject: [PATCH 01/20] added 606/808ish bd, snare, hats - bd needs work, snare good, hats are good but a but low --- src/dsp/mini_drumvoices.cpp | 398 +++++++++++++++++------------------- src/dsp/mini_drumvoices.h | 52 +++-- 2 files changed, 214 insertions(+), 236 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 6b46e6e..48974f5 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,22 +1,25 @@ #include "mini_drumvoices.h" - #include #include -DrumSynthVoice::DrumSynthVoice(float sampleRate) - : sampleRate(sampleRate), +// Utility: fast clamp +static inline float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); } + +DrumSynthVoice::DrumSynthVoice(float sr) + : sampleRate(sr), invSampleRate(0.0f) { - setSampleRate(sampleRate); + setSampleRate(sr); reset(); } void DrumSynthVoice::reset() { + // Kick (808-ish) kickPhase = 0.0f; - kickFreq = 60.0f; kickEnvAmp = 0.0f; kickEnvPitch = 0.0f; kickActive = false; + // Snare snareEnvAmp = 0.0f; snareToneEnv = 0.0f; snareActive = false; @@ -25,40 +28,51 @@ void DrumSynthVoice::reset() { snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; - hatEnvAmp = 0.0f; - hatToneEnv = 0.0f; - hatActive = false; - hatHp = 0.0f; - hatPrev = 0.0f; - hatPhaseA = 0.0f; - hatPhaseB = 0.0f; - - openHatEnvAmp = 0.0f; - openHatToneEnv = 0.0f; - openHatActive = false; - openHatHp = 0.0f; - openHatPrev = 0.0f; - openHatPhaseA = 0.0f; - openHatPhaseB = 0.0f; - - midTomPhase = 0.0f; - midTomEnv = 0.0f; - midTomActive = false; - - highTomPhase = 0.0f; - highTomEnv = 0.0f; - highTomActive = false; - - rimPhase = 0.0f; - rimEnv = 0.0f; - rimActive = false; - - clapEnv = 0.0f; - clapTrans = 0.0f; - clapNoise = 0.0f; - clapActive = false; - clapDelay = 0.0f; - + // 606 Schmitt-trigger oscillator set (analysis-derived) + hatOscFreqs[0] = 245.10f; + hatOscFreqs[1] = 308.60f; + hatOscFreqs[2] = 367.60f; + hatOscFreqs[3] = 416.60f; + hatOscFreqs[4] = 438.50f; + hatOscFreqs[5] = 625.00f; + + // Hats state + hatEnvAmp = 0.0f; hatToneEnv = 0.0f; hatActive = false; + openHatEnvAmp = 0.0f; openHatToneEnv = 0.0f; openHatActive = false; + for (int i = 0; i < 6; ++i) { hatPhases[i] = 0.0f; openHatPhases[i] = 0.0f; } + + // Biquad BP (~7.1k, Q ~0.9) coefficients + float f0 = 7100.0f; + float w0 = 2.0f * 3.14159265f * f0 / sampleRate; + float alpha = sinf(w0) / (2.0f * 0.9f); + float cosw0 = cosf(w0); + bp_b0 = alpha; + bp_b1 = 0.0f; + bp_b2 = -alpha; + float a0 = 1.0f + alpha; + bp_a1 = -2.0f * cosw0; + bp_a2 = 1.0f - alpha; + // normalize + bp_b0 /= a0; bp_b1 /= a0; bp_b2 /= a0; bp_a1 /= a0; bp_a2 /= a0; + + hatBP_x1 = hatBP_x2 = hatBP_y1 = hatBP_y2 = 0.0f; + openHatBP_x1 = openHatBP_x2 = openHatBP_y1 = openHatBP_y2 = 0.0f; + + // Post-VCA HP (one-pole): y[n] = a*(y[n-1] + x[n] - x[n-1]) + hatHP_y1 = hatHP_x1 = 0.0f; + openHatHP_y1 = openHatHP_x1 = 0.0f; + + // Toms + midTomPhase = 0.0f; midTomEnv = 0.0f; midTomActive = false; + highTomPhase = 0.0f; highTomEnv = 0.0f; highTomActive = false; + + // Rim + rimPhase = 0.0f; rimEnv = 0.0f; rimActive = false; + + // Clap + clapEnv = 0.0f; clapTrans = 0.0f; clapNoise = 0.0f; clapActive = false; clapDelay = 0.0f; + + // Parameters params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "", 0.0f, 1.0f, 0.8f, 1.0f / 128); } @@ -68,17 +82,17 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { invSampleRate = 1.0f / sampleRate; } +// --- Triggers --- void DrumSynthVoice::triggerKick() { kickActive = true; kickPhase = 0.0f; - kickEnvAmp = 1.2f; - kickEnvPitch = 1.0f; - kickFreq = 55.0f; + kickEnvAmp = 1.0f; // main amplitude envelope + kickEnvPitch = 1.0f; // fast attack pitch envelope } void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.1f; + snareEnvAmp = 1.0f; snareToneEnv = 1.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; @@ -86,259 +100,216 @@ void DrumSynthVoice::triggerSnare() { void DrumSynthVoice::triggerHat() { hatActive = true; - hatEnvAmp = 0.7f; + hatEnvAmp = 1.0f; hatToneEnv = 1.0f; - hatPhaseA = 0.0f; - hatPhaseB = 0.25f; - // closing the hat chokes any ringing open-hat tail + for (int i = 0; i < 6; ++i) hatPhases[i] = 0.0f; + // choke open-hat tail (606 behavior) openHatEnvAmp *= 0.3f; } void DrumSynthVoice::triggerOpenHat() { openHatActive = true; - openHatEnvAmp = 0.9f; + openHatEnvAmp = 1.0f; openHatToneEnv = 1.0f; - openHatPhaseA = 0.0f; - openHatPhaseB = 0.37f; + for (int i = 0; i < 6; ++i) openHatPhases[i] = 0.0f; } void DrumSynthVoice::triggerMidTom() { - midTomActive = true; - midTomEnv = 1.0f; - midTomPhase = 0.0f; + midTomActive = true; midTomEnv = 1.0f; midTomPhase = 0.0f; } void DrumSynthVoice::triggerHighTom() { - highTomActive = true; - highTomEnv = 1.0f; - highTomPhase = 0.0f; + highTomActive = true; highTomEnv = 1.0f; highTomPhase = 0.0f; } void DrumSynthVoice::triggerRim() { - rimActive = true; - rimEnv = 1.0f; - rimPhase = 0.0f; + rimActive = true; rimEnv = 1.0f; rimPhase = 0.0f; } void DrumSynthVoice::triggerClap() { - clapActive = true; - clapEnv = 1.0f; - clapTrans = 1.0f; - clapNoise = frand(); - clapDelay = 0.0f; + clapActive = true; clapEnv = 1.0f; clapTrans = 1.0f; clapNoise = frand(); clapDelay = 0.0f; } float DrumSynthVoice::frand() { return (float)rand() / (float)RAND_MAX * 2.0f - 1.0f; } +// --- Kick (TR-808-flavored): decaying sine with short click and subtle pitch drop --- float DrumSynthVoice::processKick() { - if (!kickActive) - return 0.0f; - - // Longer amp tail with faster pitch drop for a punchy thump - kickEnvAmp *= 0.9995f; - kickEnvPitch *= 0.997f; - if (kickEnvAmp < 0.0008f) { - kickActive = false; - return 0.0f; - } - - float pitchFactor = kickEnvPitch * kickEnvPitch; - float f = 42.0f + 170.0f * pitchFactor; - kickFreq = f; - kickPhase += kickFreq * invSampleRate; - if (kickPhase >= 1.0f) - kickPhase -= 1.0f; + if (!kickActive) return 0.0f; - float body = sinf(2.0f * 3.14159265f * kickPhase); - float transient = sinf(2.0f * 3.14159265f * kickPhase * 3.0f) * pitchFactor * 0.25f; - float driven = tanhf(body * (2.8f + 0.6f * kickEnvAmp)); - - return (driven * 0.85f + transient) * kickEnvAmp; -} - - -float DrumSynthVoice::processSnare() { - if (!snareActive) - return 0.0f; + // amplitude & pitch envelopes + kickEnvAmp *= 0.9996f; // long tail (808-style) + kickEnvPitch *= 0.9925f; // very quick drop for attack punch + if (kickEnvAmp < 0.0003f) { kickActive = false; return 0.0f; } - // --- ENVELOPES --- - // 808: Long noise decay, short tone decay - snareEnvAmp *= 0.9985f; // slow decay, long tail - snareToneEnv *= 0.99999f; // short tone "tick" + // Base frequency near 50–56 Hz; add small transient pitch rise then drop + float baseF = 55.0f; + float pitchAmt = 20.0f; // transient amount + float f = baseF + pitchAmt * (kickEnvPitch * kickEnvPitch); - if (snareEnvAmp < 0.0002f) { - snareActive = false; - return 0.0f; - } + // integrate phase + kickPhase += f * invSampleRate; + if (kickPhase >= 1.0f) kickPhase -= 1.0f; + float sine = sinf(2.0f * 3.14159265f * kickPhase); - // --- NOISE PROCESSING --- - float n = frand(); // assume 0.0–1.0 random + // short click (filtered noise burst) + float click = clampf((kickEnvPitch > 0.6f) ? frand() * 0.2f : 0.0f, -0.2f, 0.2f); - // 808: Noise is brighter with a bit of highpass emphasis - // simple bandpass around ~1–2 kHz - float f = 0.28f; - snareBp += f * (n - snareLp - 0.20f * snareBp); - snareLp += f * snareBp; + // tone: sine through gentle drive + float tone = tanhf((sine * 2.4f) + click); + return tone * kickEnvAmp * 0.95f; +} - // high fizz (808 has a lot of it) - float noiseHP = n - snareLp; // crude highpass - float noiseOut = snareBp * 0.35f + noiseHP * 0.65f; +// --- Snare (606-leaning: brighter noise + short tonal tick) --- +float DrumSynthVoice::processSnare() { + if (!snareActive) return 0.0f; - // --- TONE (two sines, tuned to classic 808) --- - // ~330 Hz + ~180 Hz slight mix, short decay - snareTonePhase += 330.0f * invSampleRate; - if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; - snareTonePhase2 += 180.0f * invSampleRate; - if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; + // envelopes + snareEnvAmp *= 0.9985f; // slow-ish noise decay + snareToneEnv *= 0.97f; // very short tone + if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } + // brighter noise + float n = frand(); + float hp = n - snareLp; // crude HP + float bpCoeff = 0.22f; // narrower band-pass (~2–3 kHz region) + snareBp += bpCoeff * (hp - 0.27f * snareBp); + snareLp += bpCoeff * snareBp; + float noiseOut = (hp * 0.55f + snareBp * 0.45f); + + // tonal tick (two short sines) + snareTonePhase += 260.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; + snareTonePhase2 += 420.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); - float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; + float tone = (toneA * 0.6f + toneB * 0.4f) * (snareToneEnv * 0.25f); + + float out = tanhf(noiseOut * 1.6f + tone * 0.75f); + return out * snareEnvAmp * 0.9f; +} - // --- MIX --- - // 808: tone only supports transient, noise dominates sustain - float out = noiseOut * 0.75f + tone * 0.65f; - return out * snareEnvAmp; +// --- Biquad BP apply helper --- +static inline float biquad_bp(float x, float b0, float b1, float b2, float a1, float a2, + float& x1, float& x2, float& y1, float& y2) { + float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + x2 = x1; x1 = x; y2 = y1; y1 = y; + return y; } +// --- Closed Hat (TR-606-style): six squares -> BP ~7.1 kHz -> VCA -> HPF --- float DrumSynthVoice::processHat() { - if (!hatActive) - return 0.0f; - - // hatEnvAmp *= 0.994f; // slower decay for a longer hat tail - hatEnvAmp *= 0.998f; // slower decay for a longer hat tail - hatToneEnv *= 0.92f; - if (hatEnvAmp < 0.0005f) { - hatActive = false; - return 0.0f; + if (!hatActive) return 0.0f; + + hatEnvAmp *= 0.9965f; // fast but audible CH decay + hatToneEnv *= 0.98f; + if (hatEnvAmp < 0.0006f) { hatActive = false; return 0.0f; } + + // six square oscillators + float mix = 0.0f; + for (int i = 0; i < 6; ++i) { + hatPhases[i] += hatOscFreqs[i] * invSampleRate; + if (hatPhases[i] >= 1.0f) hatPhases[i] -= 1.0f; + float sq = (hatPhases[i] < 0.5f) ? 1.0f : -1.0f; + mix += sq; } + mix *= (1.0f / 6.0f); - float n = frand(); - // crude highpass - float alpha = 0.92f; - hatHp = alpha * (hatHp + n - hatPrev); - hatPrev = n; - - // Simple metallic partials on top of noise - hatPhaseA += 6200.0f * invSampleRate; - if (hatPhaseA >= 1.0f) - hatPhaseA -= 1.0f; - hatPhaseB += 7400.0f * invSampleRate; - if (hatPhaseB >= 1.0f) - hatPhaseB -= 1.0f; - float tone = (sinf(2.0f * 3.14159265f * hatPhaseA) + sinf(2.0f * 3.14159265f * hatPhaseB)) * 0.5f * hatToneEnv; - - float out = hatHp * 0.65f + tone * 0.7f; - return out * hatEnvAmp * 0.6f; + // band-pass around ~7.1k (robust biquad) + float bp = biquad_bp(mix, bp_b0, bp_b1, bp_b2, bp_a1, bp_a2, + hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2); + + // VCA + gentle saturation + float vca = bp * hatEnvAmp; + float driven = tanhf(vca * 1.8f); + + // post-VCA high-pass (a ~ 0.98) + const float a = 0.98f; + float yhp = a * (hatHP_y1 + driven - hatHP_x1); + hatHP_y1 = yhp; hatHP_x1 = driven; + + return yhp * 0.9f; } +// --- Open Hat (same source; longer decay; choke by CH trigger) --- float DrumSynthVoice::processOpenHat() { - if (!openHatActive) - return 0.0f; - - openHatEnvAmp *= 0.9993f; - openHatToneEnv *= 0.94f; - if (openHatEnvAmp < 0.0004f) { - openHatActive = false; - return 0.0f; + if (!openHatActive) return 0.0f; + + openHatEnvAmp *= 0.9990f; // longer decay than CH + openHatToneEnv *= 0.992f; + if (openHatEnvAmp < 0.0005f) { openHatActive = false; return 0.0f; } + + float mix = 0.0f; + for (int i = 0; i < 6; ++i) { + openHatPhases[i] += hatOscFreqs[i] * invSampleRate; + if (openHatPhases[i] >= 1.0f) openHatPhases[i] -= 1.0f; + float sq = (openHatPhases[i] < 0.5f) ? 1.0f : -1.0f; + mix += sq; } + mix *= (1.0f / 6.0f); - float n = frand(); - float alpha = 0.93f; - openHatHp = alpha * (openHatHp + n - openHatPrev); - openHatPrev = n; - - openHatPhaseA += 5100.0f * invSampleRate; - if (openHatPhaseA >= 1.0f) - openHatPhaseA -= 1.0f; - openHatPhaseB += 6600.0f * invSampleRate; - if (openHatPhaseB >= 1.0f) - openHatPhaseB -= 1.0f; - float tone = (sinf(2.0f * 3.14159265f * openHatPhaseA) + sinf(2.0f * 3.14159265f * openHatPhaseB)) * 0.5f * openHatToneEnv; - - float out = openHatHp * 0.55f + tone * 0.95f; - return out * openHatEnvAmp * 0.7f; + float bp = biquad_bp(mix, bp_b0, bp_b1, bp_b2, bp_a1, bp_a2, + openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2); + + float vca = bp * openHatEnvAmp; + float driven = tanhf(vca * 1.6f); + + const float a = 0.98f; + float yhp = a * (openHatHP_y1 + driven - openHatHP_x1); + openHatHP_y1 = yhp; openHatHP_x1 = driven; + + return yhp * 0.95f; } +// --- Toms --- float DrumSynthVoice::processMidTom() { - if (!midTomActive) - return 0.0f; - + if (!midTomActive) return 0.0f; midTomEnv *= 0.99925f; - if (midTomEnv < 0.0003f) { - midTomActive = false; - return 0.0f; - } - + if (midTomEnv < 0.0003f) { midTomActive = false; return 0.0f; } float freq = 180.0f; midTomPhase += freq * invSampleRate; - if (midTomPhase >= 1.0f) - midTomPhase -= 1.0f; - + if (midTomPhase >= 1.0f) midTomPhase -= 1.0f; float tone = sinf(2.0f * 3.14159265f * midTomPhase); float slightNoise = frand() * 0.05f; return (tone * 0.9f + slightNoise) * midTomEnv * 0.8f; } float DrumSynthVoice::processHighTom() { - if (!highTomActive) - return 0.0f; - + if (!highTomActive) return 0.0f; highTomEnv *= 0.99915f; - if (highTomEnv < 0.0003f) { - highTomActive = false; - return 0.0f; - } - + if (highTomEnv < 0.0003f) { highTomActive = false; return 0.0f; } float freq = 240.0f; - highTomPhase += freq * invSampleRate; - if (highTomPhase >= 1.0f) - highTomPhase -= 1.0f; - + if (highTomPhase >= 1.0f) highTomPhase -= 1.0f; float tone = sinf(2.0f * 3.14159265f * highTomPhase); float slightNoise = frand() * 0.04f; return (tone * 0.88f + slightNoise) * highTomEnv * 0.75f; } +// --- Rim --- float DrumSynthVoice::processRim() { - if (!rimActive) - return 0.0f; - + if (!rimActive) return 0.0f; rimEnv *= 0.9985f; - if (rimEnv < 0.0004f) { - rimActive = false; - return 0.0f; - } - + if (rimEnv < 0.0004f) { rimActive = false; return 0.0f; } rimPhase += 900.0f * invSampleRate; - if (rimPhase >= 1.0f) - rimPhase -= 1.0f; + if (rimPhase >= 1.0f) rimPhase -= 1.0f; float tone = sinf(2.0f * 3.14159265f * rimPhase); float click = (frand() * 0.6f + 0.4f) * rimEnv; return (tone * 0.5f + click) * rimEnv * 0.8f; } +// --- Clap --- float DrumSynthVoice::processClap() { - if (!clapActive) - return 0.0f; - - clapEnv *= 0.99992f; + if (!clapActive) return 0.0f; + clapEnv *= 0.99992f; clapTrans *= 0.9985f; clapDelay += invSampleRate; - if (clapEnv < 0.0002f) { - clapActive = false; - return 0.0f; - } + if (clapEnv < 0.0002f) { clapActive = false; return 0.0f; } - // simple three-burst clap feel float burst = 0.0f; if (clapDelay < 0.024f) burst = 1.0f; else if (clapDelay < 0.048f) burst = 0.8f; else if (clapDelay < 0.072f) burst = 0.6f; - float noise = frand() * 0.7f + clapNoise * 0.3f; float tone = sinf(2.0f * 3.14159265f * 1100.0f * clapDelay); float out = (noise * 0.7f + tone * 0.3f) * clapTrans * burst; @@ -352,4 +323,3 @@ const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { void DrumSynthVoice::setParameter(DrumParamId id, float value) { params[static_cast(id)].setValue(value); } - diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 531ee96..805ed85 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -1,7 +1,5 @@ #pragma once - #include - #include "mini_dsp_params.h" enum class DrumParamId : uint8_t { @@ -12,9 +10,10 @@ enum class DrumParamId : uint8_t { class DrumSynthVoice { public: explicit DrumSynthVoice(float sampleRate); - void reset(); void setSampleRate(float sampleRate); + + // Triggers void triggerKick(); void triggerSnare(); void triggerHat(); @@ -24,6 +23,7 @@ class DrumSynthVoice { void triggerRim(); void triggerClap(); + // Processors (one sample per call) float processKick(); float processSnare(); float processHat(); @@ -39,57 +39,65 @@ class DrumSynthVoice { private: float frand(); + // --- Kick (808-style) --- float kickPhase; - float kickFreq; float kickEnvAmp; float kickEnvPitch; - bool kickActive; + bool kickActive; + // --- Snare --- float snareEnvAmp; float snareToneEnv; - bool snareActive; + bool snareActive; float snareBp; float snareLp; float snareTonePhase; float snareTonePhase2; + // --- Hats (606-style source + robust biquad BPF) --- float hatEnvAmp; float hatToneEnv; - bool hatActive; - float hatHp; - float hatPrev; - float hatPhaseA; - float hatPhaseB; + bool hatActive; + float hatPhases[6]; // six square oscillators + float hatOscFreqs[6]; // base oscillators' frequencies + // Biquad band-pass states (~7.1 kHz) + float hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2; + float openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2; + // Biquad coefficients + float bp_b0, bp_b1, bp_b2, bp_a1, bp_a2; + // Post-VCA highpass + float hatHP_y1, hatHP_x1; + float openHatHP_y1, openHatHP_x1; float openHatEnvAmp; float openHatToneEnv; - bool openHatActive; - float openHatHp; - float openHatPrev; - float openHatPhaseA; - float openHatPhaseB; + bool openHatActive; + float openHatPhases[6]; + // --- Toms --- float midTomPhase; float midTomEnv; - bool midTomActive; - + bool midTomActive; float highTomPhase; float highTomEnv; - bool highTomActive; + bool highTomActive; + // --- Rim --- float rimPhase; float rimEnv; - bool rimActive; + bool rimActive; + // --- Clap --- float clapEnv; float clapTrans; float clapNoise; - bool clapActive; + bool clapActive; float clapDelay; + // --- System --- float sampleRate; float invSampleRate; + // Parameters Parameter params[static_cast(DrumParamId::Count)]; }; - From d85355809a91cb69635c57771c389c92cc411a73 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Tue, 6 Jan 2026 22:20:21 -0600 Subject: [PATCH 02/20] made bd a bit more like the 808; hats a little different, pretty decent --- src/dsp/mini_drumvoices.cpp | 51 +++++++++++++++++++++++-------------- src/dsp/mini_drumvoices.h | 9 ++++--- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 48974f5..ae9ebbc 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -41,19 +41,31 @@ void DrumSynthVoice::reset() { openHatEnvAmp = 0.0f; openHatToneEnv = 0.0f; openHatActive = false; for (int i = 0; i < 6; ++i) { hatPhases[i] = 0.0f; openHatPhases[i] = 0.0f; } - // Biquad BP (~7.1k, Q ~0.9) coefficients - float f0 = 7100.0f; - float w0 = 2.0f * 3.14159265f * f0 / sampleRate; - float alpha = sinf(w0) / (2.0f * 0.9f); - float cosw0 = cosf(w0); - bp_b0 = alpha; - bp_b1 = 0.0f; - bp_b2 = -alpha; - float a0 = 1.0f + alpha; - bp_a1 = -2.0f * cosw0; - bp_a2 = 1.0f - alpha; - // normalize - bp_b0 /= a0; bp_b1 /= a0; bp_b2 /= a0; bp_a1 /= a0; bp_a2 /= a0; + // Biquad BP coefficients + // CH center ~7100 Hz, Q ~0.9 (classic 606 hats band-pass) + { + float f0 = 7100.0f; + float w0 = 2.0f * 3.14159265f * f0 / sampleRate; + float alpha = sinf(w0) / (2.0f * 0.9f); + float cosw0 = cosf(w0); + float b0 = alpha, b1 = 0.0f, b2 = -alpha; + float a0 = 1.0f + alpha; + float a1 = -2.0f * cosw0; + float a2 = 1.0f - alpha; + bp_b0_ch = b0 / a0; bp_b1_ch = b1 / a0; bp_b2_ch = b2 / a0; bp_a1_ch = a1 / a0; bp_a2_ch = a2 / a0; + } + // OH center slightly higher (~7800 Hz), Q ~0.85 for brighter, higher perceived pitch + { + float f0 = 7800.0f; + float w0 = 2.0f * 3.14159265f * f0 / sampleRate; + float alpha = sinf(w0) / (2.0f * 0.85f); + float cosw0 = cosf(w0); + float b0 = alpha, b1 = 0.0f, b2 = -alpha; + float a0 = 1.0f + alpha; + float a1 = -2.0f * cosw0; + float a2 = 1.0f - alpha; + bp_b0_oh = b0 / a0; bp_b1_oh = b1 / a0; bp_b2_oh = b2 / a0; bp_a1_oh = a1 / a0; bp_a2_oh = a2 / a0; + } hatBP_x1 = hatBP_x2 = hatBP_y1 = hatBP_y2 = 0.0f; openHatBP_x1 = openHatBP_x2 = openHatBP_y1 = openHatBP_y2 = 0.0f; @@ -215,8 +227,8 @@ float DrumSynthVoice::processHat() { } mix *= (1.0f / 6.0f); - // band-pass around ~7.1k (robust biquad) - float bp = biquad_bp(mix, bp_b0, bp_b1, bp_b2, bp_a1, bp_a2, + // band-pass around ~7.1k (robust biquad, CH coefficients) + float bp = biquad_bp(mix, bp_b0_ch, bp_b1_ch, bp_b2_ch, bp_a1_ch, bp_a2_ch, hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2); // VCA + gentle saturation @@ -231,7 +243,7 @@ float DrumSynthVoice::processHat() { return yhp * 0.9f; } -// --- Open Hat (same source; longer decay; choke by CH trigger) --- +// --- Open Hat (brighter): same source; higher BP center; longer decay; choke by CH trigger) --- float DrumSynthVoice::processOpenHat() { if (!openHatActive) return 0.0f; @@ -248,13 +260,14 @@ float DrumSynthVoice::processOpenHat() { } mix *= (1.0f / 6.0f); - float bp = biquad_bp(mix, bp_b0, bp_b1, bp_b2, bp_a1, bp_a2, + // band-pass with OH coefficients (higher center for brighter pitch) + float bp = biquad_bp(mix, bp_b0_oh, bp_b1_oh, bp_b2_oh, bp_a1_oh, bp_a2_oh, openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2); float vca = bp * openHatEnvAmp; - float driven = tanhf(vca * 1.6f); + float driven = tanhf(vca * 1.65f); - const float a = 0.98f; + const float a = 0.985f; // slightly less HP than CH to keep airy top float yhp = a * (openHatHP_y1 + driven - openHatHP_x1); openHatHP_y1 = yhp; openHatHP_x1 = driven; diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 805ed85..7d29757 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -60,11 +60,14 @@ class DrumSynthVoice { bool hatActive; float hatPhases[6]; // six square oscillators float hatOscFreqs[6]; // base oscillators' frequencies - // Biquad band-pass states (~7.1 kHz) + // Biquad band-pass states (~7.1 kHz for CH) float hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2; + // Biquad band-pass states for OH (slightly higher center) float openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2; - // Biquad coefficients - float bp_b0, bp_b1, bp_b2, bp_a1, bp_a2; + // Biquad coefficients for CH + float bp_b0_ch, bp_b1_ch, bp_b2_ch, bp_a1_ch, bp_a2_ch; + // Biquad coefficients for OH (brighter) + float bp_b0_oh, bp_b1_oh, bp_b2_oh, bp_a1_oh, bp_a2_oh; // Post-VCA highpass float hatHP_y1, hatHP_x1; float openHatHP_y1, openHatHP_x1; From c1f0a495c02d5cafbfdd2528af0f1ebc91a713d4 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Wed, 7 Jan 2026 17:17:06 -0600 Subject: [PATCH 03/20] improved drum sounds - clap broken --- src/dsp/mini_drumvoices.cpp | 441 +++++++++++++++++++----------------- src/dsp/mini_drumvoices.h | 72 +++--- 2 files changed, 279 insertions(+), 234 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index ae9ebbc..4538c0b 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,90 +1,54 @@ #include "mini_drumvoices.h" #include -#include -// Utility: fast clamp -static inline float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); } +// ---------------- Utility ---------------- +static inline float fast_tanhf(float x) { + // 3rd-order polynomial approximation is plenty for drum drive + const float x2 = x * x; + return x * (27.0f + x2) / (27.0f + 9.0f * x2); +} -DrumSynthVoice::DrumSynthVoice(float sr) - : sampleRate(sr), - invSampleRate(0.0f) { - setSampleRate(sr); +DrumSynthVoice::DrumSynthVoice(float sampleRate) + : sampleRate(sampleRate), invSampleRate(0.0f), rngState(0x12345678u) { + setSampleRate(sampleRate); reset(); } void DrumSynthVoice::reset() { - // Kick (808-ish) - kickPhase = 0.0f; - kickEnvAmp = 0.0f; - kickEnvPitch = 0.0f; + // Kick + kickPhase = 0.0f; kickFreq = 55.0f; + kickEnvAmp = 0.0f; kickEnvPitch = 0.0f; kickClickEnv = 0.0f; kickActive = false; // Snare - snareEnvAmp = 0.0f; - snareToneEnv = 0.0f; - snareActive = false; - snareBp = 0.0f; - snareLp = 0.0f; - snareTonePhase = 0.0f; - snareTonePhase2 = 0.0f; - - // 606 Schmitt-trigger oscillator set (analysis-derived) - hatOscFreqs[0] = 245.10f; - hatOscFreqs[1] = 308.60f; - hatOscFreqs[2] = 367.60f; - hatOscFreqs[3] = 416.60f; - hatOscFreqs[4] = 438.50f; - hatOscFreqs[5] = 625.00f; + snareEnvAmp = 0.0f; snareToneEnv = 0.0f; snareActive = false; + snareBp = 0.0f; snareLp = 0.0f; + snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; - // Hats state + // Hats hatEnvAmp = 0.0f; hatToneEnv = 0.0f; hatActive = false; - openHatEnvAmp = 0.0f; openHatToneEnv = 0.0f; openHatActive = false; - for (int i = 0; i < 6; ++i) { hatPhases[i] = 0.0f; openHatPhases[i] = 0.0f; } - - // Biquad BP coefficients - // CH center ~7100 Hz, Q ~0.9 (classic 606 hats band-pass) - { - float f0 = 7100.0f; - float w0 = 2.0f * 3.14159265f * f0 / sampleRate; - float alpha = sinf(w0) / (2.0f * 0.9f); - float cosw0 = cosf(w0); - float b0 = alpha, b1 = 0.0f, b2 = -alpha; - float a0 = 1.0f + alpha; - float a1 = -2.0f * cosw0; - float a2 = 1.0f - alpha; - bp_b0_ch = b0 / a0; bp_b1_ch = b1 / a0; bp_b2_ch = b2 / a0; bp_a1_ch = a1 / a0; bp_a2_ch = a2 / a0; - } - // OH center slightly higher (~7800 Hz), Q ~0.85 for brighter, higher perceived pitch - { - float f0 = 7800.0f; - float w0 = 2.0f * 3.14159265f * f0 / sampleRate; - float alpha = sinf(w0) / (2.0f * 0.85f); - float cosw0 = cosf(w0); - float b0 = alpha, b1 = 0.0f, b2 = -alpha; - float a0 = 1.0f + alpha; - float a1 = -2.0f * cosw0; - float a2 = 1.0f - alpha; - bp_b0_oh = b0 / a0; bp_b1_oh = b1 / a0; bp_b2_oh = b2 / a0; bp_a1_oh = a1 / a0; bp_a2_oh = a2 / a0; - } - - hatBP_x1 = hatBP_x2 = hatBP_y1 = hatBP_y2 = 0.0f; - openHatBP_x1 = openHatBP_x2 = openHatBP_y1 = openHatBP_y2 = 0.0f; + hatHp = 0.0f; hatPrev = 0.0f; + for (int i = 0; i < 6; ++i) hatPh[i] = 0.0f; - // Post-VCA HP (one-pole): y[n] = a*(y[n-1] + x[n] - x[n-1]) - hatHP_y1 = hatHP_x1 = 0.0f; - openHatHP_y1 = openHatHP_x1 = 0.0f; + openHatEnvAmp = 0.0f; openHatToneEnv = 0.0f; openHatActive = false; + openHatHp = 0.0f; openHatPrev = 0.0f; + for (int i = 0; i < 6; ++i) openHatPh[i] = 0.0f; // Toms - midTomPhase = 0.0f; midTomEnv = 0.0f; midTomActive = false; - highTomPhase = 0.0f; highTomEnv = 0.0f; highTomActive = false; + midTomPhase = 0.0f; midTomEnv = 0.0f; midTomPitchEnv = 0.0f; midTomActive = false; + highTomPhase = 0.0f; highTomEnv = 0.0f; highTomPitchEnv = 0.0f; highTomActive = false; // Rim - rimPhase = 0.0f; rimEnv = 0.0f; rimActive = false; + rimPhase = 0.0f; rimEnv = 0.0f; rimBp = 0.0f; rimLp = 0.0f; rimActive = false; // Clap - clapEnv = 0.0f; clapTrans = 0.0f; clapNoise = 0.0f; clapActive = false; clapDelay = 0.0f; + clapEnv = 0.0f; clapTrans = 0.0f; clapNoiseSeed = 0.0f; clapActive = false; + clapTime = 0.0f; clapIdx = 0; clapCombLen = (int)(0.008f * sampleRate); // ~8 ms + if (clapCombLen < 64) clapCombLen = 64; + if (clapCombLen > kClapBufMax) clapCombLen = kClapBufMax; + for (int i = 0; i < kClapBufMax; ++i) clapBuf[i] = 0.0f; - // Parameters + // Params params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "", 0.0f, 1.0f, 0.8f, 1.0f / 128); } @@ -92,243 +56,308 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { if (sampleRateHz <= 0.0f) sampleRateHz = 44100.0f; sampleRate = sampleRateHz; invSampleRate = 1.0f / sampleRate; + + // Hat oscillator target freqs (approx 808 metal partials, non-harmonic) + const float f[6] = { 2150.0f, 2700.0f, 3200.0f, 4100.0f, 5300.0f, 6600.0f }; + const float fo[6] = { 1900.0f, 2500.0f, 3000.0f, 3900.0f, 5100.0f, 6300.0f }; + for (int i = 0; i < 6; ++i) { + hatInc[i] = f[i] * invSampleRate; + openHatInc[i] = fo[i] * invSampleRate; + } + + // Clamp comb length to buffer + clapCombLen = (int)(0.008f * sampleRate); // ~8 ms + if (clapCombLen < 64) clapCombLen = 64; + if (clapCombLen > kClapBufMax) clapCombLen = kClapBufMax; +} + +// ---------------- RNG ---------------- +float DrumSynthVoice::frand() { + // xorshift32, returns [-1, 1] + uint32_t x = rngState; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + rngState = x; + const float u = (float)(x) * (1.0f / 4294967296.0f); // [0,1) + return u * 2.0f - 1.0f; } -// --- Triggers --- +// ---------------- Triggers ---------------- void DrumSynthVoice::triggerKick() { kickActive = true; kickPhase = 0.0f; - kickEnvAmp = 1.0f; // main amplitude envelope - kickEnvPitch = 1.0f; // fast attack pitch envelope + // Tight 606-like envelopes + kickEnvAmp = 1.15f; // fast decay, but a bit hot initially + kickEnvPitch = 1.0f; // quick pitch drop + kickClickEnv = 1.0f; // short click at onset + kickFreq = 60.0f; // start slightly above final for punch } void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.0f; - snareToneEnv = 1.0f; + snareEnvAmp = 1.1f; // noise sustain + snareToneEnv = 1.0f; // tone tick snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; } void DrumSynthVoice::triggerHat() { hatActive = true; - hatEnvAmp = 1.0f; + hatEnvAmp = 0.85f; // closed hat, shorter hatToneEnv = 1.0f; - for (int i = 0; i < 6; ++i) hatPhases[i] = 0.0f; - // choke open-hat tail (606 behavior) - openHatEnvAmp *= 0.3f; + // choke open-hat tail + openHatEnvAmp *= 0.25f; + for (int i = 0; i < 6; ++i) hatPh[i] = 0.0f; } void DrumSynthVoice::triggerOpenHat() { openHatActive = true; - openHatEnvAmp = 1.0f; + openHatEnvAmp = 0.95f; // open hat, longer openHatToneEnv = 1.0f; - for (int i = 0; i < 6; ++i) openHatPhases[i] = 0.0f; + for (int i = 0; i < 6; ++i) openHatPh[i] = 0.0f; } void DrumSynthVoice::triggerMidTom() { - midTomActive = true; midTomEnv = 1.0f; midTomPhase = 0.0f; + midTomActive = true; + midTomEnv = 1.0f; + midTomPitchEnv = 1.0f; // small sweep for character + midTomPhase = 0.0f; } void DrumSynthVoice::triggerHighTom() { - highTomActive = true; highTomEnv = 1.0f; highTomPhase = 0.0f; + highTomActive = true; + highTomEnv = 1.0f; + highTomPitchEnv = 1.0f; + highTomPhase = 0.0f; } void DrumSynthVoice::triggerRim() { - rimActive = true; rimEnv = 1.0f; rimPhase = 0.0f; + rimActive = true; + rimEnv = 1.0f; + rimPhase = 0.0f; + rimBp = 0.0f; + rimLp = 0.0f; } void DrumSynthVoice::triggerClap() { - clapActive = true; clapEnv = 1.0f; clapTrans = 1.0f; clapNoise = frand(); clapDelay = 0.0f; + clapActive = true; + clapEnv = 1.0f; + clapTrans = 1.0f; + clapNoiseSeed = frand(); // per-hit color + clapTime = 0.0f; + clapIdx = 0; } -float DrumSynthVoice::frand() { - return (float)rand() / (float)RAND_MAX * 2.0f - 1.0f; -} - -// --- Kick (TR-808-flavored): decaying sine with short click and subtle pitch drop --- +// ---------------- Processors ---------------- float DrumSynthVoice::processKick() { if (!kickActive) return 0.0f; - // amplitude & pitch envelopes - kickEnvAmp *= 0.9996f; // long tail (808-style) - kickEnvPitch *= 0.9925f; // very quick drop for attack punch - if (kickEnvAmp < 0.0003f) { kickActive = false; return 0.0f; } + // Envelope: fast amp & pitch decay for tight 606-like thump + kickEnvAmp *= 0.9965f; // ~150–200 ms + kickEnvPitch *= 0.985f; // fast pitch drop + kickClickEnv *= 0.92f; // very short transient + + if (kickEnvAmp < 0.0006f) { kickActive = false; return 0.0f; } - // Base frequency near 50–56 Hz; add small transient pitch rise then drop - float baseF = 55.0f; - float pitchAmt = 20.0f; // transient amount - float f = baseF + pitchAmt * (kickEnvPitch * kickEnvPitch); + // pitch factor + float p = kickEnvPitch * kickEnvPitch; + float f = 48.0f + 120.0f * p; // start higher, drop quickly + kickFreq = f; - // integrate phase - kickPhase += f * invSampleRate; + // oscillator + kickPhase += kickFreq * invSampleRate; if (kickPhase >= 1.0f) kickPhase -= 1.0f; - float sine = sinf(2.0f * 3.14159265f * kickPhase); - // short click (filtered noise burst) - float click = clampf((kickEnvPitch > 0.6f) ? frand() * 0.2f : 0.0f, -0.2f, 0.2f); + float body = sinf(2.0f * 3.14159265f * kickPhase); - // tone: sine through gentle drive - float tone = tanhf((sine * 2.4f) + click); - return tone * kickEnvAmp * 0.95f; + // slight drive gives 808/606 style compression feel + float driven = fast_tanhf(body * (2.6f + 0.7f * kickEnvAmp)); + + // click: short high-frequency burst mixed in at onset + float click = (frand() * 0.5f + 0.5f) * kickClickEnv * 0.3f; + + return (driven * 0.9f + click) * kickEnvAmp; } -// --- Snare (606-leaning: brighter noise + short tonal tick) --- float DrumSynthVoice::processSnare() { if (!snareActive) return 0.0f; - // envelopes - snareEnvAmp *= 0.9985f; // slow-ish noise decay - snareToneEnv *= 0.97f; // very short tone + // Envelopes: long noise tail, short tone tick + snareEnvAmp *= 0.9983f; // slow decay + snareToneEnv *= 0.94f; // short tick if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } - // brighter noise - float n = frand(); - float hp = n - snareLp; // crude HP - float bpCoeff = 0.22f; // narrower band-pass (~2–3 kHz region) - snareBp += bpCoeff * (hp - 0.27f * snareBp); - snareLp += bpCoeff * snareBp; - float noiseOut = (hp * 0.55f + snareBp * 0.45f); - - // tonal tick (two short sines) - snareTonePhase += 260.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; - snareTonePhase2 += 420.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; + // Noise: bright with slight HP emphasis + float n = frand(); // white + // crude bandpass around ~1–2 kHz using two one-poles + const float f = 0.30f; + snareBp += f * (n - snareLp - 0.22f * snareBp); + snareLp += f * snareBp; + float noiseHP = n - snareLp; + float noiseOut = snareBp * 0.38f + noiseHP * 0.62f; + + // Tones: ~330 Hz and ~180 Hz, short + snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; + snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); - float tone = (toneA * 0.6f + toneB * 0.4f) * (snareToneEnv * 0.25f); + float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; - float out = tanhf(noiseOut * 1.6f + tone * 0.75f); - return out * snareEnvAmp * 0.9f; + float out = noiseOut * 0.78f + tone * 0.55f; + return out * snareEnvAmp; } -// --- Biquad BP apply helper --- -static inline float biquad_bp(float x, float b0, float b1, float b2, float a1, float a2, - float& x1, float& x2, float& y1, float& y2) { - float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; - x2 = x1; x1 = x; y2 = y1; y1 = y; - return y; -} - -// --- Closed Hat (TR-606-style): six squares -> BP ~7.1 kHz -> VCA -> HPF --- float DrumSynthVoice::processHat() { if (!hatActive) return 0.0f; - hatEnvAmp *= 0.9965f; // fast but audible CH decay - hatToneEnv *= 0.98f; - if (hatEnvAmp < 0.0006f) { hatActive = false; return 0.0f; } + // Closed hat envelopes: short + hatEnvAmp *= 0.996f; // short tail + hatToneEnv *= 0.90f; + if (hatEnvAmp < 0.0005f) { hatActive = false; return 0.0f; } - // six square oscillators - float mix = 0.0f; + // Metal partials: six squares at non-harmonic freqs + float metal = 0.0f; for (int i = 0; i < 6; ++i) { - hatPhases[i] += hatOscFreqs[i] * invSampleRate; - if (hatPhases[i] >= 1.0f) hatPhases[i] -= 1.0f; - float sq = (hatPhases[i] < 0.5f) ? 1.0f : -1.0f; - mix += sq; + hatPh[i] += hatInc[i]; + if (hatPh[i] >= 1.0f) hatPh[i] -= 1.0f; + metal += (hatPh[i] < 0.5f ? 1.0f : -1.0f); // square } - mix *= (1.0f / 6.0f); - - // band-pass around ~7.1k (robust biquad, CH coefficients) - float bp = biquad_bp(mix, bp_b0_ch, bp_b1_ch, bp_b2_ch, bp_a1_ch, bp_a2_ch, - hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2); + metal = (metal / 6.0f) * hatToneEnv; - // VCA + gentle saturation - float vca = bp * hatEnvAmp; - float driven = tanhf(vca * 1.8f); + // Add a little noise for sizzle + float n = frand() * 0.6f; - // post-VCA high-pass (a ~ 0.98) - const float a = 0.98f; - float yhp = a * (hatHP_y1 + driven - hatHP_x1); - hatHP_y1 = yhp; hatHP_x1 = driven; + // HP filter (emphasize crispness) + const float alpha = 0.93f; + hatHp = alpha * (hatHp + n + metal - hatPrev); + hatPrev = n + metal; - return yhp * 0.9f; + // Simple BP tilt + float out = hatHp * 0.8f + metal * 0.35f; + return out * hatEnvAmp * 0.75f; } -// --- Open Hat (brighter): same source; higher BP center; longer decay; choke by CH trigger) --- float DrumSynthVoice::processOpenHat() { if (!openHatActive) return 0.0f; - openHatEnvAmp *= 0.9990f; // longer decay than CH - openHatToneEnv *= 0.992f; - if (openHatEnvAmp < 0.0005f) { openHatActive = false; return 0.0f; } + openHatEnvAmp *= 0.9988f; // longer decay than closed + openHatToneEnv *= 0.94f; + if (openHatEnvAmp < 0.0004f) { openHatActive = false; return 0.0f; } - float mix = 0.0f; + float metal = 0.0f; for (int i = 0; i < 6; ++i) { - openHatPhases[i] += hatOscFreqs[i] * invSampleRate; - if (openHatPhases[i] >= 1.0f) openHatPhases[i] -= 1.0f; - float sq = (openHatPhases[i] < 0.5f) ? 1.0f : -1.0f; - mix += sq; + openHatPh[i] += openHatInc[i]; + if (openHatPh[i] >= 1.0f) openHatPh[i] -= 1.0f; + metal += (openHatPh[i] < 0.5f ? 1.0f : -1.0f); } - mix *= (1.0f / 6.0f); + metal = (metal / 6.0f) * openHatToneEnv; - // band-pass with OH coefficients (higher center for brighter pitch) - float bp = biquad_bp(mix, bp_b0_oh, bp_b1_oh, bp_b2_oh, bp_a1_oh, bp_a2_oh, - openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2); + float n = frand() * 0.5f; - float vca = bp * openHatEnvAmp; - float driven = tanhf(vca * 1.65f); + const float alpha = 0.94f; + openHatHp = alpha * (openHatHp + n + metal - openHatPrev); + openHatPrev = n + metal; - const float a = 0.985f; // slightly less HP than CH to keep airy top - float yhp = a * (openHatHP_y1 + driven - openHatHP_x1); - openHatHP_y1 = yhp; openHatHP_x1 = driven; - - return yhp * 0.95f; + float out = openHatHp * 0.65f + metal * 0.55f; + return out * openHatEnvAmp * 0.8f; } -// --- Toms --- float DrumSynthVoice::processMidTom() { if (!midTomActive) return 0.0f; - midTomEnv *= 0.99925f; + + midTomEnv *= 0.9991f; // medium tail + midTomPitchEnv *= 0.9975f; // small pitch drop if (midTomEnv < 0.0003f) { midTomActive = false; return 0.0f; } - float freq = 180.0f; - midTomPhase += freq * invSampleRate; - if (midTomPhase >= 1.0f) midTomPhase -= 1.0f; + + float base = 170.0f; // ~808 mid + float freq = base + 15.0f * (midTomPitchEnv * midTomPitchEnv); + midTomPhase += freq * invSampleRate; if (midTomPhase >= 1.0f) midTomPhase -= 1.0f; + float tone = sinf(2.0f * 3.14159265f * midTomPhase); - float slightNoise = frand() * 0.05f; - return (tone * 0.9f + slightNoise) * midTomEnv * 0.8f; + // slight noise + subtle drive + float slightNoise = frand() * 0.03f; + float driven = fast_tanhf(tone * 2.0f); + + return (driven * 0.9f + slightNoise) * midTomEnv * 0.85f; } float DrumSynthVoice::processHighTom() { if (!highTomActive) return 0.0f; - highTomEnv *= 0.99915f; + + highTomEnv *= 0.9990f; + highTomPitchEnv *= 0.997f; if (highTomEnv < 0.0003f) { highTomActive = false; return 0.0f; } - float freq = 240.0f; - highTomPhase += freq * invSampleRate; - if (highTomPhase >= 1.0f) highTomPhase -= 1.0f; + + float base = 230.0f; // ~808 high + float freq = base + 18.0f * (highTomPitchEnv * highTomPitchEnv); + highTomPhase += freq * invSampleRate; if (highTomPhase >= 1.0f) highTomPhase -= 1.0f; + float tone = sinf(2.0f * 3.14159265f * highTomPhase); - float slightNoise = frand() * 0.04f; - return (tone * 0.88f + slightNoise) * highTomEnv * 0.75f; + float slightNoise = frand() * 0.028f; + float driven = fast_tanhf(tone * 2.0f); + + return (driven * 0.88f + slightNoise) * highTomEnv * 0.8f; } -// --- Rim --- float DrumSynthVoice::processRim() { if (!rimActive) return 0.0f; - rimEnv *= 0.9985f; - if (rimEnv < 0.0004f) { rimActive = false; return 0.0f; } - rimPhase += 900.0f * invSampleRate; - if (rimPhase >= 1.0f) rimPhase -= 1.0f; - float tone = sinf(2.0f * 3.14159265f * rimPhase); - float click = (frand() * 0.6f + 0.4f) * rimEnv; - return (tone * 0.5f + click) * rimEnv * 0.8f; + + rimEnv *= 0.9978f; // very short + if (rimEnv < 0.0006f) { rimActive = false; return 0.0f; } + + // Short tick + bandpass ~1.4 kHz for woody rim shot + rimPhase += 1400.0f * invSampleRate; if (rimPhase >= 1.0f) rimPhase -= 1.0f; + float tick = sinf(2.0f * 3.14159265f * rimPhase) * 0.6f; + + float n = frand(); + const float f = 0.35f; + rimBp += f * (n - rimLp - 0.30f * rimBp); + rimLp += f * rimBp; + float bp = rimBp; + + return (tick * 0.6f + bp * 0.7f) * rimEnv * 0.9f; } -// --- Clap --- float DrumSynthVoice::processClap() { if (!clapActive) return 0.0f; - clapEnv *= 0.99992f; - clapTrans *= 0.9985f; - clapDelay += invSampleRate; - if (clapEnv < 0.0002f) { clapActive = false; return 0.0f; } - - float burst = 0.0f; - if (clapDelay < 0.024f) burst = 1.0f; - else if (clapDelay < 0.048f) burst = 0.8f; - else if (clapDelay < 0.072f) burst = 0.6f; - float noise = frand() * 0.7f + clapNoise * 0.3f; - float tone = sinf(2.0f * 3.14159265f * 1100.0f * clapDelay); - float out = (noise * 0.7f + tone * 0.3f) * clapTrans * burst; - return out * clapEnv; + + clapEnv *= 0.99985f; // long-ish noise body + clapTrans *= 0.995f; // transient decays quicker + clapTime += invSampleRate; + if (clapEnv < 0.00025f) { clapActive = false; return 0.0f; } + + // 808 clap: series of short bursts (3–4) followed by a short reverb tail. + // Burst gates at ~12 ms intervals, decreasing strength. + float burstGate = 0.0f; + if (clapTime < 0.012f) burstGate = 1.0f; + else if (clapTime < 0.024f) burstGate = 0.85f; + else if (clapTime < 0.036f) burstGate = 0.7f; + else if (clapTime < 0.048f) burstGate = 0.55f; + + // Noise color per hit + float noise = (frand() * 0.7f + clapNoiseSeed * 0.3f) * burstGate; + + // Resonant bandpass around 1 kHz to 1.6 kHz (simple two one-poles) + static float bp = 0.0f, lp = 0.0f; + const float f = 0.32f; + bp += f * (noise - lp - 0.25f * bp); + lp += f * bp; + float colored = bp * 0.7f + (noise - lp) * 0.3f; + + // Short comb “reverb” (~8 ms). Use a simple feedback comb. + // y[n] = x[n] + 0.5 * y[n - D] + int readIdx = clapIdx - clapCombLen; + if (readIdx < 0) readIdx += kClapBufMax; + float tail = clapBuf[readIdx] * 0.5f; + float y = colored * clapTrans + tail; + clapBuf[clapIdx] = y; + clapIdx++; if (clapIdx >= kClapBufMax) clapIdx = 0; + + return y * clapEnv; } +// ---------------- Params ---------------- const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { return params[static_cast(id)]; } diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 7d29757..5a61ade 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -2,6 +2,7 @@ #include #include "mini_dsp_params.h" +// Public parameter ids (kept for compatibility) enum class DrumParamId : uint8_t { MainVolume = 0, Count @@ -23,7 +24,7 @@ class DrumSynthVoice { void triggerRim(); void triggerClap(); - // Processors (one sample per call) + // Audio processors (one sample per call) float processKick(); float processSnare(); float processHat(); @@ -33,74 +34,89 @@ class DrumSynthVoice { float processRim(); float processClap(); + // Parameters (kept) const Parameter& parameter(DrumParamId id) const; void setParameter(DrumParamId id, float value); private: + // Fast RNG [-1, 1] float frand(); + uint32_t rngState; - // --- Kick (808-style) --- + // ----- Kick (606-tight) ----- float kickPhase; + float kickFreq; float kickEnvAmp; float kickEnvPitch; + float kickClickEnv; bool kickActive; - // --- Snare --- - float snareEnvAmp; - float snareToneEnv; + // ----- Snare ----- + float snareEnvAmp; // noise amp + float snareToneEnv; // tone tick bool snareActive; + // simple state for noise filters float snareBp; float snareLp; + // two tone oscillators float snareTonePhase; float snareTonePhase2; - // --- Hats (606-style source + robust biquad BPF) --- + // ----- Closed Hat (metallic) ----- float hatEnvAmp; float hatToneEnv; bool hatActive; - float hatPhases[6]; // six square oscillators - float hatOscFreqs[6]; // base oscillators' frequencies - // Biquad band-pass states (~7.1 kHz for CH) - float hatBP_x1, hatBP_x2, hatBP_y1, hatBP_y2; - // Biquad band-pass states for OH (slightly higher center) - float openHatBP_x1, openHatBP_x2, openHatBP_y1, openHatBP_y2; - // Biquad coefficients for CH - float bp_b0_ch, bp_b1_ch, bp_b2_ch, bp_a1_ch, bp_a2_ch; - // Biquad coefficients for OH (brighter) - float bp_b0_oh, bp_b1_oh, bp_b2_oh, bp_a1_oh, bp_a2_oh; - // Post-VCA highpass - float hatHP_y1, hatHP_x1; - float openHatHP_y1, openHatHP_x1; + float hatHp; // HP filter state + float hatPrev; + // six square oscillators + float hatPh[6]; + // ----- Open Hat ----- float openHatEnvAmp; float openHatToneEnv; bool openHatActive; - float openHatPhases[6]; + float openHatHp; + float openHatPrev; + float openHatPh[6]; - // --- Toms --- + // ----- Toms ----- float midTomPhase; float midTomEnv; + float midTomPitchEnv; bool midTomActive; + float highTomPhase; float highTomEnv; + float highTomPitchEnv; bool highTomActive; - // --- Rim --- + // ----- Rimshot ----- float rimPhase; float rimEnv; + float rimBp; // bandpass state + float rimLp; bool rimActive; - // --- Clap --- + // ----- Clap ----- float clapEnv; - float clapTrans; - float clapNoise; + float clapTrans; // transient env + float clapNoiseSeed; // seed per hit bool clapActive; - float clapDelay; + float clapTime; // seconds since trigger + // mini comb/reverb + static const int kClapBufMax = 1024; + float clapBuf[kClapBufMax]; + int clapIdx; + int clapCombLen; - // --- System --- + // Sample rate float sampleRate; float invSampleRate; - // Parameters + // Hat oscillator freqs (phase increments computed per sampleRate) + float hatInc[6]; + float openHatInc[6]; + + // Global params Parameter params[static_cast(DrumParamId::Count)]; }; From e60791aadf75498d49e5970b19e549a9313206ea Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Wed, 7 Jan 2026 17:51:43 -0600 Subject: [PATCH 04/20] 808.3 - another round. clap is real bad. --- src/dsp/mini_drumvoices.cpp | 252 +++++++++++++++++++++--------------- src/dsp/mini_drumvoices.h | 47 ++++--- src/ui/ui_colors.h | 2 +- 3 files changed, 180 insertions(+), 121 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 4538c0b..f3762f0 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,15 +1,23 @@ + #include "mini_drumvoices.h" #include // ---------------- Utility ---------------- static inline float fast_tanhf(float x) { - // 3rd-order polynomial approximation is plenty for drum drive const float x2 = x * x; return x * (27.0f + x2) / (27.0f + 9.0f * x2); } +static inline float db_to_amp(float db) { return powf(10.0f, db * 0.05f); } +static inline float amp_to_db(float a) { + const float eps = 1e-12f; + return 20.0f * log10f(fabsf(a) + eps); +} DrumSynthVoice::DrumSynthVoice(float sampleRate) - : sampleRate(sampleRate), invSampleRate(0.0f), rngState(0x12345678u) { + : sampleRate(sampleRate), invSampleRate(0.0f), rngState(0x12345678u), + compEnv(0.0f), compAttackCoeff(0.0f), compReleaseCoeff(0.0f), + compGainDb(0.0f), compMakeupDb(0.0f), compThreshDb(-12.0f), + compRatio(3.0f), compKneeDb(6.0f), compAmount(0.35f) { setSampleRate(sampleRate); reset(); } @@ -41,15 +49,23 @@ void DrumSynthVoice::reset() { // Rim rimPhase = 0.0f; rimEnv = 0.0f; rimBp = 0.0f; rimLp = 0.0f; rimActive = false; - // Clap - clapEnv = 0.0f; clapTrans = 0.0f; clapNoiseSeed = 0.0f; clapActive = false; - clapTime = 0.0f; clapIdx = 0; clapCombLen = (int)(0.008f * sampleRate); // ~8 ms - if (clapCombLen < 64) clapCombLen = 64; - if (clapCombLen > kClapBufMax) clapCombLen = kClapBufMax; - for (int i = 0; i < kClapBufMax; ++i) clapBuf[i] = 0.0f; + // Clap (simplified) + clapEnv = 0.0f; clapTrans = 0.0f; clapTailEnv = 0.0f; + clapNoiseSeed = 0.0f; clapActive = false; clapTime = 0.0f; + clapHp = 0.0f; clapPrev = 0.0f; clapBp = 0.0f; clapLp = 0.0f; + + // Bus compressor defaults + compAmount = 0.35f; + compThreshDb = -18.0f + 12.0f * compAmount; // -18 .. -6 dB + compRatio = 2.0f + 4.0f * compAmount; // 2:1 .. 6:1 + compKneeDb = 6.0f; + compMakeupDb = 6.0f * compAmount; + compEnv = 0.0f; + compGainDb = 0.0f; // Params - params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "", 0.0f, 1.0f, 0.8f, 1.0f / 128); + params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "Main volume", 0.0f, 1.0f, 0.8f, 1.0f / 128); + params[static_cast(DrumParamId::BusCompAmount)] = Parameter("comp", "Bus comp amount", 0.0f, 1.0f, compAmount, 1.0f / 128); } void DrumSynthVoice::setSampleRate(float sampleRateHz) { @@ -58,17 +74,18 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { invSampleRate = 1.0f / sampleRate; // Hat oscillator target freqs (approx 808 metal partials, non-harmonic) - const float f[6] = { 2150.0f, 2700.0f, 3200.0f, 4100.0f, 5300.0f, 6600.0f }; + const float f[6] = { 2150.0f, 2700.0f, 3200.0f, 4100.0f, 5300.0f, 6600.0f }; const float fo[6] = { 1900.0f, 2500.0f, 3000.0f, 3900.0f, 5100.0f, 6300.0f }; for (int i = 0; i < 6; ++i) { - hatInc[i] = f[i] * invSampleRate; + hatInc[i] = f[i] * invSampleRate; openHatInc[i] = fo[i] * invSampleRate; } - // Clamp comb length to buffer - clapCombLen = (int)(0.008f * sampleRate); // ~8 ms - if (clapCombLen < 64) clapCombLen = 64; - if (clapCombLen > kClapBufMax) clapCombLen = kClapBufMax; + // Compressor coefficients (fixed times tuned for drums) + float attackTime = 0.005f; // ~5 ms + float releaseTime = 0.060f; // ~60 ms + compAttackCoeff = 1.0f - expf(-1.0f / (attackTime * sampleRate)); + compReleaseCoeff = 1.0f - expf(-1.0f / (releaseTime * sampleRate)); } // ---------------- RNG ---------------- @@ -87,33 +104,31 @@ float DrumSynthVoice::frand() { void DrumSynthVoice::triggerKick() { kickActive = true; kickPhase = 0.0f; - // Tight 606-like envelopes - kickEnvAmp = 1.15f; // fast decay, but a bit hot initially - kickEnvPitch = 1.0f; // quick pitch drop - kickClickEnv = 1.0f; // short click at onset - kickFreq = 60.0f; // start slightly above final for punch + kickEnvAmp = 1.15f; + kickEnvPitch = 1.0f; + kickClickEnv = 1.0f; + kickFreq = 60.0f; } void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.1f; // noise sustain - snareToneEnv = 1.0f; // tone tick + snareEnvAmp = 1.1f; + snareToneEnv = 1.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; } void DrumSynthVoice::triggerHat() { hatActive = true; - hatEnvAmp = 0.85f; // closed hat, shorter + hatEnvAmp = 0.85f; hatToneEnv = 1.0f; - // choke open-hat tail - openHatEnvAmp *= 0.25f; + openHatEnvAmp *= 0.25f; // choke for (int i = 0; i < 6; ++i) hatPh[i] = 0.0f; } void DrumSynthVoice::triggerOpenHat() { openHatActive = true; - openHatEnvAmp = 0.95f; // open hat, longer + openHatEnvAmp = 0.95f; openHatToneEnv = 1.0f; for (int i = 0; i < 6; ++i) openHatPh[i] = 0.0f; } @@ -121,7 +136,7 @@ void DrumSynthVoice::triggerOpenHat() { void DrumSynthVoice::triggerMidTom() { midTomActive = true; midTomEnv = 1.0f; - midTomPitchEnv = 1.0f; // small sweep for character + midTomPitchEnv = 1.0f; midTomPhase = 0.0f; } @@ -141,40 +156,36 @@ void DrumSynthVoice::triggerRim() { } void DrumSynthVoice::triggerClap() { - clapActive = true; - clapEnv = 1.0f; - clapTrans = 1.0f; - clapNoiseSeed = frand(); // per-hit color - clapTime = 0.0f; - clapIdx = 0; + clapActive = true; + clapEnv = 1.0f; // overall body + clapTrans = 1.0f; // transient decay + clapTailEnv = 0.85f; // tail starts lower than body + clapNoiseSeed = frand(); + clapTime = 0.0f; + + // reset shaper states + clapHp = 0.0f; clapPrev = 0.0f; clapBp = 0.0f; clapLp = 0.0f; } -// ---------------- Processors ---------------- +// ---------------- Processors: Voices ---------------- float DrumSynthVoice::processKick() { if (!kickActive) return 0.0f; - // Envelope: fast amp & pitch decay for tight 606-like thump - kickEnvAmp *= 0.9965f; // ~150–200 ms - kickEnvPitch *= 0.985f; // fast pitch drop - kickClickEnv *= 0.92f; // very short transient + kickEnvAmp *= 0.9965f; + kickEnvPitch *= 0.985f; + kickClickEnv *= 0.92f; if (kickEnvAmp < 0.0006f) { kickActive = false; return 0.0f; } - // pitch factor float p = kickEnvPitch * kickEnvPitch; - float f = 48.0f + 120.0f * p; // start higher, drop quickly + float f = 48.0f + 120.0f * p; kickFreq = f; - // oscillator kickPhase += kickFreq * invSampleRate; if (kickPhase >= 1.0f) kickPhase -= 1.0f; float body = sinf(2.0f * 3.14159265f * kickPhase); - - // slight drive gives 808/606 style compression feel float driven = fast_tanhf(body * (2.6f + 0.7f * kickEnvAmp)); - - // click: short high-frequency burst mixed in at onset float click = (frand() * 0.5f + 0.5f) * kickClickEnv * 0.3f; return (driven * 0.9f + click) * kickEnvAmp; @@ -183,21 +194,17 @@ float DrumSynthVoice::processKick() { float DrumSynthVoice::processSnare() { if (!snareActive) return 0.0f; - // Envelopes: long noise tail, short tone tick - snareEnvAmp *= 0.9983f; // slow decay - snareToneEnv *= 0.94f; // short tick + snareEnvAmp *= 0.9983f; + snareToneEnv *= 0.94f; if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } - // Noise: bright with slight HP emphasis - float n = frand(); // white - // crude bandpass around ~1–2 kHz using two one-poles + float n = frand(); const float f = 0.30f; snareBp += f * (n - snareLp - 0.22f * snareBp); snareLp += f * snareBp; - float noiseHP = n - snareLp; + float noiseHP = n - snareLp; float noiseOut = snareBp * 0.38f + noiseHP * 0.62f; - // Tones: ~330 Hz and ~180 Hz, short snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); @@ -211,29 +218,24 @@ float DrumSynthVoice::processSnare() { float DrumSynthVoice::processHat() { if (!hatActive) return 0.0f; - // Closed hat envelopes: short - hatEnvAmp *= 0.996f; // short tail + hatEnvAmp *= 0.996f; hatToneEnv *= 0.90f; if (hatEnvAmp < 0.0005f) { hatActive = false; return 0.0f; } - // Metal partials: six squares at non-harmonic freqs float metal = 0.0f; for (int i = 0; i < 6; ++i) { hatPh[i] += hatInc[i]; if (hatPh[i] >= 1.0f) hatPh[i] -= 1.0f; - metal += (hatPh[i] < 0.5f ? 1.0f : -1.0f); // square + metal += (hatPh[i] < 0.5f ? 1.0f : -1.0f); } metal = (metal / 6.0f) * hatToneEnv; - // Add a little noise for sizzle float n = frand() * 0.6f; - // HP filter (emphasize crispness) const float alpha = 0.93f; hatHp = alpha * (hatHp + n + metal - hatPrev); hatPrev = n + metal; - // Simple BP tilt float out = hatHp * 0.8f + metal * 0.35f; return out * hatEnvAmp * 0.75f; } @@ -241,7 +243,7 @@ float DrumSynthVoice::processHat() { float DrumSynthVoice::processOpenHat() { if (!openHatActive) return 0.0f; - openHatEnvAmp *= 0.9988f; // longer decay than closed + openHatEnvAmp *= 0.9988f; openHatToneEnv *= 0.94f; if (openHatEnvAmp < 0.0004f) { openHatActive = false; return 0.0f; } @@ -266,16 +268,15 @@ float DrumSynthVoice::processOpenHat() { float DrumSynthVoice::processMidTom() { if (!midTomActive) return 0.0f; - midTomEnv *= 0.9991f; // medium tail - midTomPitchEnv *= 0.9975f; // small pitch drop + midTomEnv *= 0.9991f; + midTomPitchEnv *= 0.9975f; if (midTomEnv < 0.0003f) { midTomActive = false; return 0.0f; } - float base = 170.0f; // ~808 mid + float base = 170.0f; float freq = base + 15.0f * (midTomPitchEnv * midTomPitchEnv); midTomPhase += freq * invSampleRate; if (midTomPhase >= 1.0f) midTomPhase -= 1.0f; float tone = sinf(2.0f * 3.14159265f * midTomPhase); - // slight noise + subtle drive float slightNoise = frand() * 0.03f; float driven = fast_tanhf(tone * 2.0f); @@ -289,7 +290,7 @@ float DrumSynthVoice::processHighTom() { highTomPitchEnv *= 0.997f; if (highTomEnv < 0.0003f) { highTomActive = false; return 0.0f; } - float base = 230.0f; // ~808 high + float base = 230.0f; float freq = base + 18.0f * (highTomPitchEnv * highTomPitchEnv); highTomPhase += freq * invSampleRate; if (highTomPhase >= 1.0f) highTomPhase -= 1.0f; @@ -303,10 +304,9 @@ float DrumSynthVoice::processHighTom() { float DrumSynthVoice::processRim() { if (!rimActive) return 0.0f; - rimEnv *= 0.9978f; // very short + rimEnv *= 0.9978f; if (rimEnv < 0.0006f) { rimActive = false; return 0.0f; } - // Short tick + bandpass ~1.4 kHz for woody rim shot rimPhase += 1400.0f * invSampleRate; if (rimPhase >= 1.0f) rimPhase -= 1.0f; float tick = sinf(2.0f * 3.14159265f * rimPhase) * 0.6f; @@ -319,42 +319,90 @@ float DrumSynthVoice::processRim() { return (tick * 0.6f + bp * 0.7f) * rimEnv * 0.9f; } +// ---------------- Clap (simplified, longer, bright) ---------------- float DrumSynthVoice::processClap() { if (!clapActive) return 0.0f; - clapEnv *= 0.99985f; // long-ish noise body - clapTrans *= 0.995f; // transient decays quicker - clapTime += invSampleRate; - if (clapEnv < 0.00025f) { clapActive = false; return 0.0f; } - - // 808 clap: series of short bursts (3–4) followed by a short reverb tail. - // Burst gates at ~12 ms intervals, decreasing strength. - float burstGate = 0.0f; - if (clapTime < 0.012f) burstGate = 1.0f; - else if (clapTime < 0.024f) burstGate = 0.85f; - else if (clapTime < 0.036f) burstGate = 0.7f; - else if (clapTime < 0.048f) burstGate = 0.55f; - - // Noise color per hit - float noise = (frand() * 0.7f + clapNoiseSeed * 0.3f) * burstGate; - - // Resonant bandpass around 1 kHz to 1.6 kHz (simple two one-poles) - static float bp = 0.0f, lp = 0.0f; - const float f = 0.32f; - bp += f * (noise - lp - 0.25f * bp); - lp += f * bp; - float colored = bp * 0.7f + (noise - lp) * 0.3f; - - // Short comb “reverb” (~8 ms). Use a simple feedback comb. - // y[n] = x[n] + 0.5 * y[n - D] - int readIdx = clapIdx - clapCombLen; - if (readIdx < 0) readIdx += kClapBufMax; - float tail = clapBuf[readIdx] * 0.5f; - float y = colored * clapTrans + tail; - clapBuf[clapIdx] = y; - clapIdx++; if (clapIdx >= kClapBufMax) clapIdx = 0; - - return y * clapEnv; + // Envelopes + clapEnv *= 0.99992f; // global tail (slow) + clapTrans *= 0.995f; // transient (fast) + clapTailEnv *= 0.99988f; // separate tail/body mix + clapTime += invSampleRate; + + // Stop after a reasonable duration + if (clapTime > 0.25f || clapEnv < 0.00025f) { clapActive = false; return 0.0f; } + + // Four burst gates (Gaussian) at ~13 ms spacing + const float tau = 0.0060f; // wider = less “spiky”, more clap-like + const float t0 = 0.000f, t1 = 0.013f, t2 = 0.026f, t3 = 0.039f; + const float a0 = 1.00f, a1 = 0.80f, a2 = 0.65f, a3 = 0.55f; + + float burst = 0.0f; + float dt = clapTime - t0; burst += a0 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t1; burst += a1 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t2; burst += a2 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t3; burst += a3 * expf(-(dt * dt) / (tau * tau)); + + // White noise with per-hit color + float w = (frand() * 0.75f + clapNoiseSeed * 0.25f); + + // Simple bright shaper: high-pass + mild band-pass tilt + const float hpAlpha = 0.96f; // higher => brighter + clapHp = hpAlpha * (clapHp + w - clapPrev); + clapPrev = w; + float hpOut = clapHp; + + const float bpF = 0.31f; // center ~1.2–1.5 kHz, gentle Q + clapBp += bpF * (hpOut - clapLp - 0.25f * clapBp); + clapLp += bpF * clapBp; + float colored = clapBp * 0.68f + (hpOut - clapLp) * 0.32f; + + // Body (bursts) + tail (longer, quieter) + float body = colored * burst * clapTrans; + float tail = colored * 0.55f * clapTailEnv; + + float y = (body + tail) * clapEnv; + + return y; +} + +// ---------------- Bus Compressor ---------------- +float DrumSynthVoice::processBus(float mixSample) { + // Update parameter cache (in case UI changed it) + compAmount = params[static_cast(DrumParamId::BusCompAmount)].value(); + compThreshDb = -18.0f + 12.0f * compAmount; // -18 .. -6 dB + compRatio = 2.0f + 4.0f * compAmount; // 2:1 .. 6:1 + compMakeupDb = 6.0f * compAmount; // up to ~+6 dB + compKneeDb = 6.0f; // fixed knee + + // Detector: peak-ish envelope follower + float inAbs = fabsf(mixSample); + float target = inAbs; + float coeff = (target > compEnv) ? compAttackCoeff : compReleaseCoeff; + compEnv += coeff * (target - compEnv); + + // Envelope to dB and soft-knee gain reduction + float levelDb = amp_to_db(compEnv); + float overDb = levelDb - compThreshDb; + float grDb = 0.0f; + if (overDb <= -compKneeDb * 0.5f) { + grDb = 0.0f; + } else if (overDb < compKneeDb * 0.5f) { + float x = (overDb + compKneeDb * 0.5f) / compKneeDb; // 0..1 + float kneeGain = (1.0f / compRatio - 1.0f) * (x * x); + grDb = kneeGain * compKneeDb; // negative + } else { + float levelOutDb = compThreshDb + overDb / compRatio; + grDb = levelOutDb - levelDb; // negative + } + + // Smooth gain reduction + const float grSmooth = 0.8f; + compGainDb = grSmooth * compGainDb + (1.0f - grSmooth) * grDb; + + // Apply makeup and reduction + float out = mixSample * db_to_amp(compGainDb + compMakeupDb); + return out; } // ---------------- Params ---------------- @@ -364,4 +412,4 @@ const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { void DrumSynthVoice::setParameter(DrumParamId id, float value) { params[static_cast(id)].setValue(value); -} +} \ No newline at end of file diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 5a61ade..a5aae7c 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -1,10 +1,12 @@ + #pragma once #include #include "mini_dsp_params.h" -// Public parameter ids (kept for compatibility) +// Public parameter ids (kept for compatibility + bus comp) enum class DrumParamId : uint8_t { MainVolume = 0, + BusCompAmount, // 0..1 one-knob compressor Count }; @@ -34,7 +36,10 @@ class DrumSynthVoice { float processRim(); float processClap(); - // Parameters (kept) + // Bus processing (apply one-knob compressor to the mixed sum) + float processBus(float mixSample); + + // Parameters const Parameter& parameter(DrumParamId id) const; void setParameter(DrumParamId id, float value); @@ -55,10 +60,8 @@ class DrumSynthVoice { float snareEnvAmp; // noise amp float snareToneEnv; // tone tick bool snareActive; - // simple state for noise filters float snareBp; float snareLp; - // two tone oscillators float snareTonePhase; float snareTonePhase2; @@ -68,8 +71,8 @@ class DrumSynthVoice { bool hatActive; float hatHp; // HP filter state float hatPrev; - // six square oscillators float hatPh[6]; + float hatInc[6]; // ----- Open Hat ----- float openHatEnvAmp; @@ -78,6 +81,7 @@ class DrumSynthVoice { float openHatHp; float openHatPrev; float openHatPh[6]; + float openHatInc[6]; // ----- Toms ----- float midTomPhase; @@ -93,29 +97,36 @@ class DrumSynthVoice { // ----- Rimshot ----- float rimPhase; float rimEnv; - float rimBp; // bandpass state + float rimBp; float rimLp; bool rimActive; - // ----- Clap ----- - float clapEnv; - float clapTrans; // transient env - float clapNoiseSeed; // seed per hit + // ----- Clap (simplified, no comb/diffusion) ----- + float clapEnv; // overall body envelope (longer tail) + float clapTrans; // transient envelope + float clapTailEnv; // separate tail envelope + float clapNoiseSeed; // per-hit color bool clapActive; float clapTime; // seconds since trigger - // mini comb/reverb - static const int kClapBufMax = 1024; - float clapBuf[kClapBufMax]; - int clapIdx; - int clapCombLen; + + // noise shaper states + float clapHp, clapPrev; + float clapBp, clapLp; // Sample rate float sampleRate; float invSampleRate; - // Hat oscillator freqs (phase increments computed per sampleRate) - float hatInc[6]; - float openHatInc[6]; + // ----- One-knob Bus Compressor ----- + float compEnv; // detector envelope + float compAttackCoeff; + float compReleaseCoeff; + float compGainDb; // smoothed gain reduction + float compMakeupDb; // auto makeup (dB) + float compThreshDb; // mapped from knob (-18 .. -6 dB) + float compRatio; // mapped from knob (2:1 .. 6:1) + float compKneeDb; // soft knee width (fixed ~6 dB) + float compAmount; // 0..1 parameter cache // Global params Parameter params[static_cast(DrumParamId::Count)]; diff --git a/src/ui/ui_colors.h b/src/ui/ui_colors.h index fb5f867..4fe8095 100644 --- a/src/ui/ui_colors.h +++ b/src/ui/ui_colors.h @@ -8,7 +8,7 @@ inline constexpr IGfxColor COLOR_GRAY = IGfxColor(0x202020); inline constexpr IGfxColor COLOR_DARKER = IGfxColor(0x101010); inline constexpr IGfxColor COLOR_WAVE = IGfxColor(0x00FF90); inline constexpr IGfxColor COLOR_PANEL = IGfxColor(0x181818); -inline constexpr IGfxColor COLOR_ACCENT = IGfxColor(0xFFB000); +inline constexpr IGfxColor COLOR_ACCENT = IGfxColor(0x10B0FF); inline constexpr IGfxColor COLOR_SLIDE = IGfxColor(0x0090FF); inline constexpr IGfxColor COLOR_303_NOTE = IGfxColor(0x00606F); inline constexpr IGfxColor COLOR_STEP_HILIGHT = IGfxColor(0xFFFFFF); From bf85dde3d8f48cf424fb2d9637a393ccd3e3a327 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Wed, 7 Jan 2026 17:59:52 -0600 Subject: [PATCH 05/20] brought back earlier version of AJR snare --- src/dsp/mini_drumvoices.cpp | 56 ++++++++++++++++++++++++++----------- src/dsp/mini_drumvoices.h | 3 ++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index f3762f0..2abc669 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -29,9 +29,13 @@ void DrumSynthVoice::reset() { kickActive = false; // Snare - snareEnvAmp = 0.0f; snareToneEnv = 0.0f; snareActive = false; - snareBp = 0.0f; snareLp = 0.0f; - snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; + snareEnvAmp = 0.0f; + snareToneEnv = 0.0f; + snareActive = false; + snareBp = 0.0f; + snareLp = 0.0f; + snareTonePhase = 0.0f; + snareTonePhase2 = 0.0f; // Hats hatEnvAmp = 0.0f; hatToneEnv = 0.0f; hatActive = false; @@ -112,7 +116,7 @@ void DrumSynthVoice::triggerKick() { void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.1f; + snareEnvAmp = 1.1f; snareToneEnv = 1.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; @@ -192,26 +196,46 @@ float DrumSynthVoice::processKick() { } float DrumSynthVoice::processSnare() { - if (!snareActive) return 0.0f; + if (!snareActive) + return 0.0f; - snareEnvAmp *= 0.9983f; - snareToneEnv *= 0.94f; - if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } + // --- ENVELOPES --- + // 808: Long noise decay, short tone decay + snareEnvAmp *= 0.9985f; // slow decay, long tail + snareToneEnv *= 0.99999f; // short tone "tick" - float n = frand(); - const float f = 0.30f; - snareBp += f * (n - snareLp - 0.22f * snareBp); + if (snareEnvAmp < 0.0002f) { + snareActive = false; + return 0.0f; + } + + // --- NOISE PROCESSING --- + float n = frand(); // assume 0.0–1.0 random + + // 808: Noise is brighter with a bit of highpass emphasis + // simple bandpass around ~1–2 kHz + float f = 0.28f; + snareBp += f * (n - snareLp - 0.20f * snareBp); snareLp += f * snareBp; - float noiseHP = n - snareLp; - float noiseOut = snareBp * 0.38f + noiseHP * 0.62f; - snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; - snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; + // high fizz (808 has a lot of it) + float noiseHP = n - snareLp; // crude highpass + float noiseOut = snareBp * 0.35f + noiseHP * 0.65f; + + // --- TONE (two sines, tuned to classic 808) --- + // ~330 Hz + ~180 Hz slight mix, short decay + snareTonePhase += 330.0f * invSampleRate; + if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; + snareTonePhase2 += 180.0f * invSampleRate; + if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; + float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; - float out = noiseOut * 0.78f + tone * 0.55f; + // --- MIX --- + // 808: tone only supports transient, noise dominates sustain + float out = noiseOut * 0.75f + tone * 0.65f; return out * snareEnvAmp; } diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index a5aae7c..976fa9f 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -39,6 +39,9 @@ class DrumSynthVoice { // Bus processing (apply one-knob compressor to the mixed sum) float processBus(float mixSample); + // Snare + float snareHpPrev; // extra high-pass memory + // Parameters const Parameter& parameter(DrumParamId id) const; void setParameter(DrumParamId id, float value); From 20b9067f2fae6348f08915088289a4ee4cd468c1 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Wed, 7 Jan 2026 18:16:56 -0600 Subject: [PATCH 06/20] change bottom end of resonance from 0.05 to 0.01 to get less resonance! --- src/dsp/mini_tb303.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsp/mini_tb303.cpp b/src/dsp/mini_tb303.cpp index ce48bb0..f693db4 100644 --- a/src/dsp/mini_tb303.cpp +++ b/src/dsp/mini_tb303.cpp @@ -209,7 +209,7 @@ int TB303Voice::oscillatorIndex() const { void TB303Voice::initParameters() { params[static_cast(TB303ParamId::Cutoff)] = Parameter("cut", "Hz", 60.0f, 2500.0f, 800.0f, (2500.f - 60.0f) / 128); - params[static_cast(TB303ParamId::Resonance)] = Parameter("res", "", 0.05f, 0.85f, 0.6f, (0.85f - 0.05f) / 128); + params[static_cast(TB303ParamId::Resonance)] = Parameter("res", "", 0.01f, 0.85f, 0.6f, (0.85f - 0.05f) / 128); params[static_cast(TB303ParamId::EnvAmount)] = Parameter("env", "Hz", 0.0f, 2000.0f, 400.0f, (2000.0f - 0.0f) / 128); params[static_cast(TB303ParamId::EnvDecay)] = Parameter("dec", "ms", 20.0f, 2200.0f, 420.0f, (2200.0f - 20.0f) / 128); params[static_cast(TB303ParamId::Oscillator)] = Parameter("osc", "", kOscillatorOptions, 3, 0); From ec96e1cf011d76b544dbb4cba2693df573891f54 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Wed, 7 Jan 2026 18:17:13 -0600 Subject: [PATCH 07/20] everything decent but the dang clap! --- src/dsp/mini_drumvoices.cpp | 150 +++++++++++++++++++++++------------- src/dsp/mini_drumvoices.h | 21 +++-- 2 files changed, 113 insertions(+), 58 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 2abc669..1b37f15 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -29,13 +29,9 @@ void DrumSynthVoice::reset() { kickActive = false; // Snare - snareEnvAmp = 0.0f; - snareToneEnv = 0.0f; - snareActive = false; - snareBp = 0.0f; - snareLp = 0.0f; - snareTonePhase = 0.0f; - snareTonePhase2 = 0.0f; + snareEnvAmp = 0.0f; snareToneEnv = 0.0f; snareActive = false; + snareBp = 0.0f; snareLp = 0.0f; + snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; // Hats hatEnvAmp = 0.0f; hatToneEnv = 0.0f; hatActive = false; @@ -53,10 +49,16 @@ void DrumSynthVoice::reset() { // Rim rimPhase = 0.0f; rimEnv = 0.0f; rimBp = 0.0f; rimLp = 0.0f; rimActive = false; - // Clap (simplified) + // Clap (revamped) clapEnv = 0.0f; clapTrans = 0.0f; clapTailEnv = 0.0f; clapNoiseSeed = 0.0f; clapActive = false; clapTime = 0.0f; clapHp = 0.0f; clapPrev = 0.0f; clapBp = 0.0f; clapLp = 0.0f; + clapBp2 = 0.0f; clapLp2 = 0.0f; + clapSnapPhase = 0.0f; clapSnapEnv = 0.0f; + clapTapIdx = 0; clapTapLen = (int)(0.020f * sampleRate) + 64; // > 19ms + margin + if (clapTapLen < 256) clapTapLen = 256; + if (clapTapLen > kClapTapBufMax) clapTapLen = kClapTapBufMax; + for (int i = 0; i < kClapTapBufMax; ++i) clapTapBuf[i] = 0.0f; // Bus compressor defaults compAmount = 0.35f; @@ -85,6 +87,15 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { openHatInc[i] = fo[i] * invSampleRate; } + // Multi-tap feed-forward delays for clap cluster (in samples) + clapD1 = (int)(0.0045f * sampleRate); // ~4.5 ms + clapD2 = (int)(0.0090f * sampleRate); // ~9 ms + clapD3 = (int)(0.0140f * sampleRate); // ~14 ms + clapD4 = (int)(0.0190f * sampleRate); // ~19 ms + clapTapLen = (int)(0.020f * sampleRate) + 64; + if (clapTapLen < 256) clapTapLen = 256; + if (clapTapLen > kClapTapBufMax) clapTapLen = kClapTapBufMax; + // Compressor coefficients (fixed times tuned for drums) float attackTime = 0.005f; // ~5 ms float releaseTime = 0.060f; // ~60 ms @@ -116,7 +127,7 @@ void DrumSynthVoice::triggerKick() { void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.1f; + snareEnvAmp = 1.1f; snareToneEnv = 1.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; @@ -161,14 +172,24 @@ void DrumSynthVoice::triggerRim() { void DrumSynthVoice::triggerClap() { clapActive = true; - clapEnv = 1.0f; // overall body - clapTrans = 1.0f; // transient decay - clapTailEnv = 0.85f; // tail starts lower than body + clapEnv = 1.0f; // global body + clapTrans = 1.0f; // transient + clapTailEnv = 0.90f; // independent tail body clapNoiseSeed = frand(); clapTime = 0.0f; - // reset shaper states - clapHp = 0.0f; clapPrev = 0.0f; clapBp = 0.0f; clapLp = 0.0f; + // shaper states + clapHp = 0.0f; clapPrev = 0.0f; + clapBp = 0.0f; clapLp = 0.0f; + clapBp2 = 0.0f; clapLp2 = 0.0f; + + // snap tone + clapSnapPhase = 0.0f; + clapSnapEnv = 1.0f; + + // cluster buffer + clapTapIdx = 0; + for (int i = 0; i < clapTapLen; ++i) clapTapBuf[i] = 0.0f; } // ---------------- Processors: Voices ---------------- @@ -198,41 +219,33 @@ float DrumSynthVoice::processKick() { float DrumSynthVoice::processSnare() { if (!snareActive) return 0.0f; - // --- ENVELOPES --- // 808: Long noise decay, short tone decay - snareEnvAmp *= 0.9985f; // slow decay, long tail - snareToneEnv *= 0.99999f; // short tone "tick" - + snareEnvAmp *= 0.9985f; // slow decay, long tail + snareToneEnv *= 0.99999f; // short tone "tick" if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } - // --- NOISE PROCESSING --- float n = frand(); // assume 0.0–1.0 random - // 808: Noise is brighter with a bit of highpass emphasis // simple bandpass around ~1–2 kHz float f = 0.28f; snareBp += f * (n - snareLp - 0.20f * snareBp); snareLp += f * snareBp; - // high fizz (808 has a lot of it) - float noiseHP = n - snareLp; // crude highpass + float noiseHP = n - snareLp; // crude highpass float noiseOut = snareBp * 0.35f + noiseHP * 0.65f; - // --- TONE (two sines, tuned to classic 808) --- // ~330 Hz + ~180 Hz slight mix, short decay snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; - float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; - // --- MIX --- // 808: tone only supports transient, noise dominates sustain float out = noiseOut * 0.75f + tone * 0.65f; @@ -343,51 +356,82 @@ float DrumSynthVoice::processRim() { return (tick * 0.6f + bp * 0.7f) * rimEnv * 0.9f; } -// ---------------- Clap (simplified, longer, bright) ---------------- +// ---------------- Clap (hand-snap + multi-tap cluster, no feedback) ---------------- float DrumSynthVoice::processClap() { if (!clapActive) return 0.0f; // Envelopes clapEnv *= 0.99992f; // global tail (slow) - clapTrans *= 0.995f; // transient (fast) - clapTailEnv *= 0.99988f; // separate tail/body mix + clapTrans *= 0.9950f; // transient (fast) + clapTailEnv *= 0.99988f; // tail/body + clapSnapEnv *= 0.93f; // very fast snap decay clapTime += invSampleRate; - // Stop after a reasonable duration - if (clapTime > 0.25f || clapEnv < 0.00025f) { clapActive = false; return 0.0f; } + if (clapTime > 0.28f || clapEnv < 0.0002f) { clapActive = false; return 0.0f; } - // Four burst gates (Gaussian) at ~13 ms spacing - const float tau = 0.0060f; // wider = less “spiky”, more clap-like + // Four Gaussian bursts at ~13 ms spacing + const float tau = 0.0055f; // width controls "hand softness" const float t0 = 0.000f, t1 = 0.013f, t2 = 0.026f, t3 = 0.039f; - const float a0 = 1.00f, a1 = 0.80f, a2 = 0.65f, a3 = 0.55f; + const float a0 = 1.00f, a1 = 0.82f, a2 = 0.68f, a3 = 0.60f; - float burst = 0.0f; - float dt = clapTime - t0; burst += a0 * expf(-(dt * dt) / (tau * tau)); - dt = clapTime - t1; burst += a1 * expf(-(dt * dt) / (tau * tau)); - dt = clapTime - t2; burst += a2 * expf(-(dt * dt) / (tau * tau)); - dt = clapTime - t3; burst += a3 * expf(-(dt * dt) / (tau * tau)); + float burst = 0.0f, dt = 0.0f; + dt = clapTime - t0; burst += a0 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t1; burst += a1 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t2; burst += a2 * expf(-(dt * dt) / (tau * tau)); + dt = clapTime - t3; burst += a3 * expf(-(dt * dt) / (tau * tau)); // White noise with per-hit color float w = (frand() * 0.75f + clapNoiseSeed * 0.25f); - // Simple bright shaper: high-pass + mild band-pass tilt - const float hpAlpha = 0.96f; // higher => brighter + // Bright shaper: HP + two gentle BP bands (~1.3 kHz & ~2.2 kHz) + const float hpAlpha = 0.970f; // brighter than before clapHp = hpAlpha * (clapHp + w - clapPrev); clapPrev = w; float hpOut = clapHp; - const float bpF = 0.31f; // center ~1.2–1.5 kHz, gentle Q - clapBp += bpF * (hpOut - clapLp - 0.25f * clapBp); - clapLp += bpF * clapBp; - float colored = clapBp * 0.68f + (hpOut - clapLp) * 0.32f; - - // Body (bursts) + tail (longer, quieter) - float body = colored * burst * clapTrans; - float tail = colored * 0.55f * clapTailEnv; - - float y = (body + tail) * clapEnv; - - return y; + const float bpF1 = 0.32f; // ~1.3 kHz band + clapBp += bpF1 * (hpOut - clapLp - 0.25f * clapBp); + clapLp += bpF1 * clapBp; + float band1 = clapBp * 0.65f + (hpOut - clapLp) * 0.35f; + + const float bpF2 = 0.38f; // ~2.2 kHz band (slightly brighter) + clapBp2 += bpF2 * (hpOut - clapLp2 - 0.22f * clapBp2); + clapLp2 += bpF2 * clapBp2; + float band2 = clapBp2 * 0.60f + (hpOut - clapLp2) * 0.40f; + + // Short tonal "snap" around 1.5 kHz (very brief) + clapSnapPhase += 1500.0f * invSampleRate; if (clapSnapPhase >= 1.0f) clapSnapPhase -= 1.0f; + float snapTone = sinf(2.0f * 3.14159265f * clapSnapPhase) * clapSnapEnv; + + // Body signal: bursts + snap + float colored = band1 * 0.58f + band2 * 0.42f; + float body = (colored * burst * clapTrans) + (snapTone * 0.45f * burst); + + // Tail (longer, quieter) to make it read as a clap rather than a noise blip + float tail = (band1 * 0.50f + band2 * 0.35f) * clapTailEnv; + + // Feed-forward multi-tap cluster: emulate multiple hands (no feedback) + // y = body + a1*x[n-d1] + a2*x[n-d2] + a3*x[n-d3] + a4*x[n-d4] + tail + // Write body into ring buffer, then read taps. + clapTapBuf[clapTapIdx] = body; + int idx = clapTapIdx; + int i1 = idx - clapD1; if (i1 < 0) i1 += clapTapLen; + int i2 = idx - clapD2; if (i2 < 0) i2 += clapTapLen; + int i3 = idx - clapD3; if (i3 < 0) i3 += clapTapLen; + int i4 = idx - clapD4; if (i4 < 0) i4 += clapTapLen; + + float y = body + + clapTapBuf[i1] * 0.60f + + clapTapBuf[i2] * 0.45f + + clapTapBuf[i3] * 0.30f + + clapTapBuf[i4] * 0.20f + + tail; + + // advance ring index + clapTapIdx++; if (clapTapIdx >= clapTapLen) clapTapIdx = 0; + + // global body envelope + return y * clapEnv; } // ---------------- Bus Compressor ---------------- @@ -436,4 +480,4 @@ const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { void DrumSynthVoice::setParameter(DrumParamId id, float value) { params[static_cast(id)].setValue(value); -} \ No newline at end of file +} diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 976fa9f..4392527 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -1,4 +1,3 @@ - #pragma once #include #include "mini_dsp_params.h" @@ -40,7 +39,7 @@ class DrumSynthVoice { float processBus(float mixSample); // Snare - float snareHpPrev; // extra high-pass memory + float snareHpPrev; // extra high-pass memory // Parameters const Parameter& parameter(DrumParamId id) const; @@ -104,9 +103,9 @@ class DrumSynthVoice { float rimLp; bool rimActive; - // ----- Clap (simplified, no comb/diffusion) ----- + // ----- Clap (revamped) ----- float clapEnv; // overall body envelope (longer tail) - float clapTrans; // transient envelope + float clapTrans; // transient envelope (fast) float clapTailEnv; // separate tail envelope float clapNoiseSeed; // per-hit color bool clapActive; @@ -114,7 +113,19 @@ class DrumSynthVoice { // noise shaper states float clapHp, clapPrev; - float clapBp, clapLp; + float clapBp, clapLp; // ~1.3 kHz region + float clapBp2, clapLp2; // ~2.2 kHz region + + // tonal snap (very short) + float clapSnapPhase; + float clapSnapEnv; + + // feed-forward multi-tap cluster (no feedback; safe on ESP) + static const int kClapTapBufMax = 1024; + float clapTapBuf[kClapTapBufMax]; + int clapTapIdx; + int clapD1, clapD2, clapD3, clapD4; // sample delays + int clapTapLen; // ring size (<= kClapTapBufMax) // Sample rate float sampleRate; From 2d290ee30dace15fff443d52370f3d7453a56aa2 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 09:59:47 -0600 Subject: [PATCH 08/20] 0.00 floor for resonance --- src/dsp/mini_tb303.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsp/mini_tb303.cpp b/src/dsp/mini_tb303.cpp index f693db4..83b4b4f 100644 --- a/src/dsp/mini_tb303.cpp +++ b/src/dsp/mini_tb303.cpp @@ -209,7 +209,7 @@ int TB303Voice::oscillatorIndex() const { void TB303Voice::initParameters() { params[static_cast(TB303ParamId::Cutoff)] = Parameter("cut", "Hz", 60.0f, 2500.0f, 800.0f, (2500.f - 60.0f) / 128); - params[static_cast(TB303ParamId::Resonance)] = Parameter("res", "", 0.01f, 0.85f, 0.6f, (0.85f - 0.05f) / 128); + params[static_cast(TB303ParamId::Resonance)] = Parameter("res", "", 0.00f, 0.85f, 0.6f, (0.85f - 0.05f) / 128); params[static_cast(TB303ParamId::EnvAmount)] = Parameter("env", "Hz", 0.0f, 2000.0f, 400.0f, (2000.0f - 0.0f) / 128); params[static_cast(TB303ParamId::EnvDecay)] = Parameter("dec", "ms", 20.0f, 2200.0f, 420.0f, (2200.0f - 20.0f) / 128); params[static_cast(TB303ParamId::Oscillator)] = Parameter("osc", "", kOscillatorOptions, 3, 0); From 50f706aea48cb5e81cf4494c59e857e700600443 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 10:04:50 -0600 Subject: [PATCH 09/20] changed banner to ajrAcid in blue so I can tell which one I'm running --- src/ui/miniacid_display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/miniacid_display.cpp b/src/ui/miniacid_display.cpp index aedcfc8..ea05907 100644 --- a/src/ui/miniacid_display.cpp +++ b/src/ui/miniacid_display.cpp @@ -140,7 +140,7 @@ void MiniAcidDisplay::drawSplashScreen() { if (start_y < 6) start_y = 6; gfx_.setFont(GfxFont::kFreeMono24pt); - centerText(start_y, "MiniAcid", COLOR_ACCENT); + centerText(start_y, "ajrAcid", COLOR_ACCENT); gfx_.setFont(GfxFont::kFont5x7); int info_y = start_y + title_h + gap; From 66dfea89d3c78e76384c066e122c01329e325a75 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 10:12:59 -0600 Subject: [PATCH 10/20] Update README.md --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 7a2c07c..5c26ee2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ +# fork: ajrAcid + +This is a personal fork of MiniAcid, a tiny acid groovebox for the M5Stack Cardputer. I've forked it to make some changes and make this awesome little app more to my liking. A huge THANK YOU to [urtubia](https://github.com/urtubia/) for developing this and sharing it with the world! The UI is excellent, it's well thought out. + +DONE: +- Simple bus compressor for the drums to increase punch +- Updated all drum voices - BD, SN, CH, OH, RS, and CP all have been changed. Current clap is a burst of static, but I'm working on that. +- Changed banner to "ajrAcid" in blue so I can tell whether I'm running my fork or the original +- Lowered the resonance floor - there was always some squelch with MiniAcid 0.0.5. Especially with two voices, it's important to me to have a squelchless 303. + +TODO - Short Term: +- Better clap - currently it's a burst of noise, working on that. Aiming for something like the 808 clap. +- Improving the hi-hats - I wanted a crispier and more metalic hi-hats, 606/808ish + +TODO - Medium term: +- Drum parameter screen: Modify drum BusCompAmount, drum voice decay time(s) +- Mixer screen: to change levels of each synth and drum voice + # MiniAcid MiniAcid is a tiny acid groovebox for the M5Stack Cardputer. It runs two squelchy TB-303 style voices plus a punchy TR-808 inspired drum section on the Cardputer's built-in keyboard and screen, so you can noodle basslines and beats anywhere. From 8c1b2244a305fdd621af84884477496f5e1b34f9 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 16:49:18 -0600 Subject: [PATCH 11/20] implemented bus compressor, improved clap --- src/dsp/mini_drumvoices.cpp | 204 ++++++++++++++++++++++-------------- src/dsp/mini_drumvoices.h | 99 +++++++---------- src/dsp/miniacid_engine.cpp | 56 +++++----- 3 files changed, 195 insertions(+), 164 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 1b37f15..01241cd 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,4 +1,3 @@ - #include "mini_drumvoices.h" #include @@ -28,10 +27,11 @@ void DrumSynthVoice::reset() { kickEnvAmp = 0.0f; kickEnvPitch = 0.0f; kickClickEnv = 0.0f; kickActive = false; - // Snare + // Snare (UNCHANGED) snareEnvAmp = 0.0f; snareToneEnv = 0.0f; snareActive = false; snareBp = 0.0f; snareLp = 0.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; + snareHpPrev = 0.0f; // Hats hatEnvAmp = 0.0f; hatToneEnv = 0.0f; hatActive = false; @@ -52,10 +52,20 @@ void DrumSynthVoice::reset() { // Clap (revamped) clapEnv = 0.0f; clapTrans = 0.0f; clapTailEnv = 0.0f; clapNoiseSeed = 0.0f; clapActive = false; clapTime = 0.0f; - clapHp = 0.0f; clapPrev = 0.0f; clapBp = 0.0f; clapLp = 0.0f; - clapBp2 = 0.0f; clapLp2 = 0.0f; - clapSnapPhase = 0.0f; clapSnapEnv = 0.0f; - clapTapIdx = 0; clapTapLen = (int)(0.020f * sampleRate) + 64; // > 19ms + margin + + clapHp = 0.0f; clapPrev = 0.0f; clapAirLp = 0.0f; + + // cavity bands + clapBpA = clapLpA = clapBpA2 = clapLpA2 = 0.0f; + clapBpB = clapLpB = clapBpB2 = clapLpB2 = 0.0f; + + // snaps + clapSnapPhase1 = clapSnapPhase2 = clapSnapPhase3 = 0.0f; + clapSnapEnv1 = clapSnapEnv2 = clapSnapEnv3 = 0.0f; + clapCrackEnv = 0.0f; + + // multi-tap cluster + clapTapIdx = 0; clapTapLen = (int)(0.032f * sampleRate) + 64; // up to ~32 ms if (clapTapLen < 256) clapTapLen = 256; if (clapTapLen > kClapTapBufMax) clapTapLen = kClapTapBufMax; for (int i = 0; i < kClapTapBufMax; ++i) clapTapBuf[i] = 0.0f; @@ -92,7 +102,10 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { clapD2 = (int)(0.0090f * sampleRate); // ~9 ms clapD3 = (int)(0.0140f * sampleRate); // ~14 ms clapD4 = (int)(0.0190f * sampleRate); // ~19 ms - clapTapLen = (int)(0.020f * sampleRate) + 64; + clapD5 = (int)(0.0230f * sampleRate); // ~23 ms + clapD6 = (int)(0.0270f * sampleRate); // ~27 ms + + clapTapLen = (int)(0.032f * sampleRate) + 64; if (clapTapLen < 256) clapTapLen = 256; if (clapTapLen > kClapTapBufMax) clapTapLen = kClapTapBufMax; @@ -172,20 +185,25 @@ void DrumSynthVoice::triggerRim() { void DrumSynthVoice::triggerClap() { clapActive = true; - clapEnv = 1.0f; // global body + clapEnv = 1.0f; // global body envelope clapTrans = 1.0f; // transient - clapTailEnv = 0.90f; // independent tail body + clapTailEnv = 0.95f; // tail/body clapNoiseSeed = frand(); clapTime = 0.0f; // shaper states - clapHp = 0.0f; clapPrev = 0.0f; - clapBp = 0.0f; clapLp = 0.0f; - clapBp2 = 0.0f; clapLp2 = 0.0f; + clapHp = 0.0f; clapPrev = 0.0f; clapAirLp = 0.0f; - // snap tone - clapSnapPhase = 0.0f; - clapSnapEnv = 1.0f; + // cavity bands + clapBpA = clapLpA = clapBpA2 = clapLpA2 = 0.0f; + clapBpB = clapLpB = clapBpB2 = clapLpB2 = 0.0f; + + // snaps & crack + clapSnapPhase1 = clapSnapPhase2 = clapSnapPhase3 = 0.0f; + clapSnapEnv1 = 1.0f; // ~1.3 kHz + clapSnapEnv2 = 1.0f; // ~1.6 kHz + clapSnapEnv3 = 0.9f; // ~2.0 kHz (softer) + clapCrackEnv = 1.0f; // very fast transient // cluster buffer clapTapIdx = 0; @@ -217,37 +235,28 @@ float DrumSynthVoice::processKick() { } float DrumSynthVoice::processSnare() { - if (!snareActive) - return 0.0f; + if (!snareActive) return 0.0f; // --- ENVELOPES --- - // 808: Long noise decay, short tone decay - snareEnvAmp *= 0.9985f; // slow decay, long tail - snareToneEnv *= 0.99999f; // short tone "tick" - if (snareEnvAmp < 0.0002f) { - snareActive = false; - return 0.0f; - } + snareEnvAmp *= 0.9985f; + snareToneEnv *= 0.99999f; + if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } + // --- NOISE PROCESSING --- - float n = frand(); // assume 0.0–1.0 random - // 808: Noise is brighter with a bit of highpass emphasis - // simple bandpass around ~1–2 kHz + float n = frand(); float f = 0.28f; snareBp += f * (n - snareLp - 0.20f * snareBp); snareLp += f * snareBp; - // high fizz (808 has a lot of it) - float noiseHP = n - snareLp; // crude highpass + float noiseHP = n - snareLp; float noiseOut = snareBp * 0.35f + noiseHP * 0.65f; - // --- TONE (two sines, tuned to classic 808) --- - // ~330 Hz + ~180 Hz slight mix, short decay - snareTonePhase += 330.0f * invSampleRate; - if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; - snareTonePhase2 += 180.0f * invSampleRate; - if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; + + // --- TONE --- + snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; + snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; + // --- MIX --- - // 808: tone only supports transient, noise dominates sustain float out = noiseOut * 0.75f + tone * 0.65f; return out * snareEnvAmp; } @@ -356,84 +365,125 @@ float DrumSynthVoice::processRim() { return (tick * 0.6f + bp * 0.7f) * rimEnv * 0.9f; } -// ---------------- Clap (hand-snap + multi-tap cluster, no feedback) ---------------- +// ---------------- Clap (hollow, multi-hand, boosted crack, no feedback) ---------------- float DrumSynthVoice::processClap() { if (!clapActive) return 0.0f; // Envelopes - clapEnv *= 0.99992f; // global tail (slow) - clapTrans *= 0.9950f; // transient (fast) - clapTailEnv *= 0.99988f; // tail/body - clapSnapEnv *= 0.93f; // very fast snap decay - clapTime += invSampleRate; + clapEnv *= 0.99993f; // overall tail (slow) + clapTrans *= 0.995f; // transient (fast) + clapTailEnv *= 0.99990f; // tail/body (medium) - if (clapTime > 0.28f || clapEnv < 0.0002f) { clapActive = false; return 0.0f; } + // snaps & crack decay quickly (give “hand” crack, but die fast) + clapSnapEnv1 *= 0.92f; + clapSnapEnv2 *= 0.90f; + clapSnapEnv3 *= 0.88f; + clapCrackEnv *= 0.90f; - // Four Gaussian bursts at ~13 ms spacing - const float tau = 0.0055f; // width controls "hand softness" - const float t0 = 0.000f, t1 = 0.013f, t2 = 0.026f, t3 = 0.039f; - const float a0 = 1.00f, a1 = 0.82f, a2 = 0.68f, a3 = 0.60f; + clapTime += invSampleRate; + if (clapTime > 0.30f || clapEnv < 0.0002f) { clapActive = false; return 0.0f; } + // Four Gaussian bursts ~13 ms apart (hands) + const float tau = 0.0042f; // narrower => less “busy” spectrum + const float t0 = 0.000f, t1 = 0.013f, t2 = 0.026f, t3 = 0.039f; + const float a0 = 1.00f, a1 = 0.80f, a2 = 0.65f, a3 = 0.55f; float burst = 0.0f, dt = 0.0f; dt = clapTime - t0; burst += a0 * expf(-(dt * dt) / (tau * tau)); dt = clapTime - t1; burst += a1 * expf(-(dt * dt) / (tau * tau)); dt = clapTime - t2; burst += a2 * expf(-(dt * dt) / (tau * tau)); dt = clapTime - t3; burst += a3 * expf(-(dt * dt) / (tau * tau)); - // White noise with per-hit color - float w = (frand() * 0.75f + clapNoiseSeed * 0.25f); + // Base noise (lower brightness to avoid “white noise”) + float w = (frand() * 0.55f + clapNoiseSeed * 0.45f); - // Bright shaper: HP + two gentle BP bands (~1.3 kHz & ~2.2 kHz) - const float hpAlpha = 0.970f; // brighter than before + // High-pass to remove lows + low-pass “air” to tame hiss + const float hpAlpha = 0.955f; // slightly lower = less hiss clapHp = hpAlpha * (clapHp + w - clapPrev); clapPrev = w; - float hpOut = clapHp; - const float bpF1 = 0.32f; // ~1.3 kHz band - clapBp += bpF1 * (hpOut - clapLp - 0.25f * clapBp); - clapLp += bpF1 * clapBp; - float band1 = clapBp * 0.65f + (hpOut - clapLp) * 0.35f; + const float lpAlpha = 0.15f; // stronger air LP + clapAirLp += lpAlpha * (clapHp - clapAirLp); + float bandInput = clapAirLp; + + // Two independent cavity bands (each cascaded to narrow the band) + // Formant A (lower mid cavity) + const float bpFA = 0.29f, dampA = 0.26f; + clapBpA += bpFA * (bandInput - clapLpA - dampA * clapBpA); + clapLpA += bpFA * clapBpA; + clapBpA2 += bpFA * (clapBpA - clapLpA2 - dampA * clapBpA2); + clapLpA2 += bpFA * clapBpA2; + + // Formant B (upper mid cavity) + const float bpFB = 0.33f, dampB = 0.24f; + clapBpB += bpFB * (bandInput - clapLpB - dampB * clapBpB); + clapLpB += bpFB * clapBpB; + clapBpB2 += bpFB * (clapBpB - clapLpB2 - dampB * clapBpB2); + clapLpB2 += bpFB * clapBpB2; - const float bpF2 = 0.38f; // ~2.2 kHz band (slightly brighter) - clapBp2 += bpF2 * (hpOut - clapLp2 - 0.22f * clapBp2); - clapLp2 += bpF2 * clapBp2; - float band2 = clapBp2 * 0.60f + (hpOut - clapLp2) * 0.40f; + // Narrow bands summed for hollow feel + float bandNarrow = clapBpA2 * 0.55f + clapBpB2 * 0.45f; - // Short tonal "snap" around 1.5 kHz (very brief) - clapSnapPhase += 1500.0f * invSampleRate; if (clapSnapPhase >= 1.0f) clapSnapPhase -= 1.0f; - float snapTone = sinf(2.0f * 3.14159265f * clapSnapPhase) * clapSnapEnv; + // Short tonal snaps near 1.3/1.6/2.0 kHz (very brief; under burst) + clapSnapPhase1 += 1300.0f * invSampleRate; if (clapSnapPhase1 >= 1.0f) clapSnapPhase1 -= 1.0f; + clapSnapPhase2 += 1600.0f * invSampleRate; if (clapSnapPhase2 >= 1.0f) clapSnapPhase2 -= 1.0f; + clapSnapPhase3 += 2000.0f * invSampleRate; if (clapSnapPhase3 >= 1.0f) clapSnapPhase3 -= 1.0f; - // Body signal: bursts + snap - float colored = band1 * 0.58f + band2 * 0.42f; - float body = (colored * burst * clapTrans) + (snapTone * 0.45f * burst); + float snap = + sinf(2.0f * 3.14159265f * clapSnapPhase1) * clapSnapEnv1 * 0.50f + + sinf(2.0f * 3.14159265f * clapSnapPhase2) * clapSnapEnv2 * 0.55f + + sinf(2.0f * 3.14159265f * clapSnapPhase3) * clapSnapEnv3 * 0.45f; - // Tail (longer, quieter) to make it read as a clap rather than a noise blip - float tail = (band1 * 0.50f + band2 * 0.35f) * clapTailEnv; + // Extra transient crack (fast, only at burst peaks) + float crack = (bandInput - bandNarrow) * 0.40f * clapCrackEnv; - // Feed-forward multi-tap cluster: emulate multiple hands (no feedback) - // y = body + a1*x[n-d1] + a2*x[n-d2] + a3*x[n-d3] + a4*x[n-d4] + tail - // Write body into ring buffer, then read taps. + // Body: narrow-band noise + snaps + crack, gated by burst + float body = (bandNarrow * 0.90f + snap * 0.55f + crack * 0.50f) * burst * clapTrans; + + // Tail: quieter narrow-band noise (keeps it “clappy” vs. a noise blip) + float tail = bandNarrow * 0.48f * clapTailEnv; + + // Feed-forward multi-hand cluster (no feedback; avoids glitches) clapTapBuf[clapTapIdx] = body; int idx = clapTapIdx; int i1 = idx - clapD1; if (i1 < 0) i1 += clapTapLen; int i2 = idx - clapD2; if (i2 < 0) i2 += clapTapLen; int i3 = idx - clapD3; if (i3 < 0) i3 += clapTapLen; int i4 = idx - clapD4; if (i4 < 0) i4 += clapTapLen; + int i5 = idx - clapD5; if (i5 < 0) i5 += clapTapLen; + int i6 = idx - clapD6; if (i6 < 0) i6 += clapTapLen; float y = body - + clapTapBuf[i1] * 0.60f - + clapTapBuf[i2] * 0.45f - + clapTapBuf[i3] * 0.30f + + clapTapBuf[i1] * 0.55f + + clapTapBuf[i2] * 0.40f + + clapTapBuf[i3] * 0.28f + clapTapBuf[i4] * 0.20f + + clapTapBuf[i5] * 0.13f + + clapTapBuf[i6] * 0.09f + tail; - // advance ring index clapTapIdx++; if (clapTapIdx >= clapTapLen) clapTapIdx = 0; - // global body envelope return y * clapEnv; } +// ---------------- NEW: per-sample drum bus ---------------- +// Sums all voices, runs bus compressor, applies MainVolume +float DrumSynthVoice::processFrame() { + float mix = 0.0f; + mix += processKick(); + mix += processSnare(); + mix += processClap(); + mix += processHat(); + mix += processOpenHat(); + mix += processMidTom(); + mix += processHighTom(); + mix += processRim(); + + mix = processBus(mix); // bus comp on the whole drum mix + mix *= params[static_cast(DrumParamId::MainVolume)].value(); + return mix; +} + // ---------------- Bus Compressor ---------------- float DrumSynthVoice::processBus(float mixSample) { // Update parameter cache (in case UI changed it) @@ -480,4 +530,4 @@ const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { void DrumSynthVoice::setParameter(DrumParamId id, float value) { params[static_cast(id)].setValue(value); -} +} \ No newline at end of file diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 4392527..1b436c1 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -1,3 +1,4 @@ + #pragma once #include #include "mini_dsp_params.h" @@ -27,15 +28,18 @@ class DrumSynthVoice { // Audio processors (one sample per call) float processKick(); - float processSnare(); + float processSnare(); // unchanged from your version float processHat(); float processOpenHat(); float processMidTom(); float processHighTom(); float processRim(); - float processClap(); + float processClap(); // updated + + // NEW: per-sample drum bus (sums all voices, runs bus comp, applies MainVolume) + float processFrame(); - // Bus processing (apply one-knob compressor to the mixed sum) + // Bus processing (apply one-knob compressor to any mix sample) float processBus(float mixSample); // Snare @@ -51,96 +55,71 @@ class DrumSynthVoice { uint32_t rngState; // ----- Kick (606-tight) ----- - float kickPhase; - float kickFreq; - float kickEnvAmp; - float kickEnvPitch; - float kickClickEnv; + float kickPhase, kickFreq, kickEnvAmp, kickEnvPitch, kickClickEnv; bool kickActive; // ----- Snare ----- - float snareEnvAmp; // noise amp - float snareToneEnv; // tone tick + float snareEnvAmp, snareToneEnv; bool snareActive; - float snareBp; - float snareLp; - float snareTonePhase; - float snareTonePhase2; + float snareBp, snareLp, snareTonePhase, snareTonePhase2; // ----- Closed Hat (metallic) ----- - float hatEnvAmp; - float hatToneEnv; + float hatEnvAmp, hatToneEnv; bool hatActive; - float hatHp; // HP filter state - float hatPrev; - float hatPh[6]; - float hatInc[6]; + float hatHp, hatPrev; + float hatPh[6], hatInc[6]; // ----- Open Hat ----- - float openHatEnvAmp; - float openHatToneEnv; + float openHatEnvAmp, openHatToneEnv; bool openHatActive; - float openHatHp; - float openHatPrev; - float openHatPh[6]; - float openHatInc[6]; + float openHatHp, openHatPrev; + float openHatPh[6], openHatInc[6]; // ----- Toms ----- - float midTomPhase; - float midTomEnv; - float midTomPitchEnv; + float midTomPhase, midTomEnv, midTomPitchEnv; bool midTomActive; - float highTomPhase; - float highTomEnv; - float highTomPitchEnv; + float highTomPhase, highTomEnv, highTomPitchEnv; bool highTomActive; // ----- Rimshot ----- - float rimPhase; - float rimEnv; - float rimBp; - float rimLp; + float rimPhase, rimEnv, rimBp, rimLp; bool rimActive; - // ----- Clap (revamped) ----- - float clapEnv; // overall body envelope (longer tail) + // ----- Clap (hollow, multi-hand) ----- + float clapEnv; // overall body envelope (slow) float clapTrans; // transient envelope (fast) - float clapTailEnv; // separate tail envelope + float clapTailEnv; // tail/body envelope (medium) float clapNoiseSeed; // per-hit color bool clapActive; float clapTime; // seconds since trigger - // noise shaper states - float clapHp, clapPrev; - float clapBp, clapLp; // ~1.3 kHz region - float clapBp2, clapLp2; // ~2.2 kHz region + // Noise shaper & cavity states + float clapHp, clapPrev; // high-pass + float clapAirLp; // air low-pass (tames hiss) + + // Two independent cavity bands (each cascaded to narrow the band) + float clapBpA, clapLpA, clapBpA2, clapLpA2; // formant A (~mid cavity) + float clapBpB, clapLpB, clapBpB2, clapLpB2; // formant B (~upper-mid cavity) - // tonal snap (very short) - float clapSnapPhase; - float clapSnapEnv; + // Tonal snaps (very short) + extra crack envelope + float clapSnapPhase1, clapSnapPhase2, clapSnapPhase3; + float clapSnapEnv1, clapSnapEnv2, clapSnapEnv3; + float clapCrackEnv; - // feed-forward multi-tap cluster (no feedback; safe on ESP) - static const int kClapTapBufMax = 1024; + // Feed-forward multi-tap cluster (no feedback) + static const int kClapTapBufMax = 2048; // big enough for ~30 ms @ 44.1 kHz float clapTapBuf[kClapTapBufMax]; int clapTapIdx; - int clapD1, clapD2, clapD3, clapD4; // sample delays + int clapD1, clapD2, clapD3, clapD4, clapD5, clapD6; // sample delays int clapTapLen; // ring size (<= kClapTapBufMax) // Sample rate - float sampleRate; - float invSampleRate; + float sampleRate, invSampleRate; // ----- One-knob Bus Compressor ----- - float compEnv; // detector envelope - float compAttackCoeff; - float compReleaseCoeff; - float compGainDb; // smoothed gain reduction - float compMakeupDb; // auto makeup (dB) - float compThreshDb; // mapped from knob (-18 .. -6 dB) - float compRatio; // mapped from knob (2:1 .. 6:1) - float compKneeDb; // soft knee width (fixed ~6 dB) - float compAmount; // 0..1 parameter cache + float compEnv, compAttackCoeff, compReleaseCoeff; + float compGainDb, compMakeupDb, compThreshDb, compRatio, compKneeDb, compAmount; // Global params Parameter params[static_cast(DrumParamId::Count)]; diff --git a/src/dsp/miniacid_engine.cpp b/src/dsp/miniacid_engine.cpp index f862a3b..e99696e 100644 --- a/src/dsp/miniacid_engine.cpp +++ b/src/dsp/miniacid_engine.cpp @@ -772,14 +772,16 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { samplesIntoStep++; } - float sample = 0.0f; + float sampleOut = 0.0f; + if (playing) { + // ---- 303 voices (with tempo delay) ---- float sample303 = 0.0f; if (!mute303) { float v = voice303.process() * 0.5f; sample303 += delay303.process(v); } else { - // keep delay line ticking even while muted to let tails decay + // keep delay.line ticking so tails decay naturally delay303.process(0.0f); } if (!mute303_2) { @@ -788,36 +790,36 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { } else { delay3032.process(0.0f); } - if (!muteKick) - sample += drums.processKick(); - if (!muteSnare) - sample += drums.processSnare(); - if (!muteHat) - sample += drums.processHat(); - if (!muteOpenHat) - sample += drums.processOpenHat(); - if (!muteMidTom) - sample += drums.processMidTom(); - if (!muteHighTom) - sample += drums.processHighTom(); - if (!muteRim) - sample += drums.processRim(); - if (!muteClap) - sample += drums.processClap(); - sample += sample303; + + // ---- DRUM BUS (sum all drums, then bus-comp) ---- + float drumSum = 0.0f; + if (!muteKick) drumSum += drums.processKick(); + if (!muteSnare) drumSum += drums.processSnare(); + if (!muteHat) drumSum += drums.processHat(); + if (!muteOpenHat) drumSum += drums.processOpenHat(); + if (!muteMidTom) drumSum += drums.processMidTom(); + if (!muteHighTom) drumSum += drums.processHighTom(); + if (!muteRim) drumSum += drums.processRim(); + if (!muteClap) drumSum += drums.processClap(); + + // Bus compressor applies to the whole DRUM MIX + // if you want to compress the entire mix (drums+303), move the processBus() call after sampleOut = drumSum + sample303. + // drumSum = drums.processBus(drumSum); + + // ---- Mix drums + 303 ---- + sampleOut = drumSum + sample303; + + // uncomment to use bus comp on the whole mix + sampleOut = drums.processBus(sampleOut); } // Soft clipping/limiting - sample *= 0.65f; - if (sample > 1.0f) - sample = 1.0f; - if (sample < -1.0f) - sample = -1.0f; - + sampleOut *= 0.65f; + if (sampleOut > 1.0f) sampleOut = 1.0f; + if (sampleOut < -1.0f) sampleOut = -1.0f; float currentVolume = params[static_cast(MiniAcidParamId::MainVolume)].value(); - buffer[i] = static_cast(sample * 32767.0f * currentVolume); - + buffer[i] = static_cast(sampleOut * 32767.0f * currentVolume); } size_t copyCount = numSamples; From 856103e00fc074839d7b1f86108f063db3f8ef35 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 17:06:00 -0600 Subject: [PATCH 12/20] bus comp minor edits - remove unused method --- src/dsp/mini_drumvoices.cpp | 20 +------------------- src/dsp/mini_drumvoices.h | 3 --- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 01241cd..404d383 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -466,25 +466,7 @@ float DrumSynthVoice::processClap() { return y * clapEnv; } -// ---------------- NEW: per-sample drum bus ---------------- -// Sums all voices, runs bus compressor, applies MainVolume -float DrumSynthVoice::processFrame() { - float mix = 0.0f; - mix += processKick(); - mix += processSnare(); - mix += processClap(); - mix += processHat(); - mix += processOpenHat(); - mix += processMidTom(); - mix += processHighTom(); - mix += processRim(); - - mix = processBus(mix); // bus comp on the whole drum mix - mix *= params[static_cast(DrumParamId::MainVolume)].value(); - return mix; -} - -// ---------------- Bus Compressor ---------------- +// Bus Compressor float DrumSynthVoice::processBus(float mixSample) { // Update parameter cache (in case UI changed it) compAmount = params[static_cast(DrumParamId::BusCompAmount)].value(); diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index 1b436c1..ac205f8 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -36,9 +36,6 @@ class DrumSynthVoice { float processRim(); float processClap(); // updated - // NEW: per-sample drum bus (sums all voices, runs bus comp, applies MainVolume) - float processFrame(); - // Bus processing (apply one-knob compressor to any mix sample) float processBus(float mixSample); From a2730a1783ff40c74c16ab041adec2d61549f37d Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Thu, 8 Jan 2026 17:17:27 -0600 Subject: [PATCH 13/20] bus comp fixes - implemented decimated updates to reduce cpu use - was getting crackling w/ the new clap and bus comp --- src/dsp/mini_drumvoices.cpp | 106 ++++++++++++++++++------------------ src/dsp/mini_drumvoices.h | 31 ++++++----- src/dsp/miniacid_engine.cpp | 10 ++-- 3 files changed, 72 insertions(+), 75 deletions(-) diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 404d383..69fb8f5 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,7 +1,6 @@ #include "mini_drumvoices.h" #include -// ---------------- Utility ---------------- static inline float fast_tanhf(float x) { const float x2 = x * x; return x * (27.0f + x2) / (27.0f + 9.0f * x2); @@ -78,6 +77,8 @@ void DrumSynthVoice::reset() { compMakeupDb = 6.0f * compAmount; compEnv = 0.0f; compGainDb = 0.0f; + compDecimCounter = 0; + compLastGainAmp = 1.0f; // Params params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "Main volume", 0.0f, 1.0f, 0.8f, 1.0f / 128); @@ -109,14 +110,13 @@ void DrumSynthVoice::setSampleRate(float sampleRateHz) { if (clapTapLen < 256) clapTapLen = 256; if (clapTapLen > kClapTapBufMax) clapTapLen = kClapTapBufMax; - // Compressor coefficients (fixed times tuned for drums) + // compressor coefficients (fixed times tuned for drums) float attackTime = 0.005f; // ~5 ms float releaseTime = 0.060f; // ~60 ms compAttackCoeff = 1.0f - expf(-1.0f / (attackTime * sampleRate)); compReleaseCoeff = 1.0f - expf(-1.0f / (releaseTime * sampleRate)); } -// ---------------- RNG ---------------- float DrumSynthVoice::frand() { // xorshift32, returns [-1, 1] uint32_t x = rngState; @@ -128,7 +128,6 @@ float DrumSynthVoice::frand() { return u * 2.0f - 1.0f; } -// ---------------- Triggers ---------------- void DrumSynthVoice::triggerKick() { kickActive = true; kickPhase = 0.0f; @@ -210,7 +209,6 @@ void DrumSynthVoice::triggerClap() { for (int i = 0; i < clapTapLen; ++i) clapTapBuf[i] = 0.0f; } -// ---------------- Processors: Voices ---------------- float DrumSynthVoice::processKick() { if (!kickActive) return 0.0f; @@ -241,7 +239,6 @@ float DrumSynthVoice::processSnare() { snareToneEnv *= 0.99999f; if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } - // --- NOISE PROCESSING --- float n = frand(); float f = 0.28f; snareBp += f * (n - snareLp - 0.20f * snareBp); @@ -249,14 +246,12 @@ float DrumSynthVoice::processSnare() { float noiseHP = n - snareLp; float noiseOut = snareBp * 0.35f + noiseHP * 0.65f; - // --- TONE --- snareTonePhase += 330.0f * invSampleRate; if (snareTonePhase >= 1.0f) snareTonePhase -= 1.0f; snareTonePhase2 += 180.0f * invSampleRate; if (snareTonePhase2 >= 1.0f) snareTonePhase2 -= 1.0f; float toneA = sinf(2.0f * 3.14159265f * snareTonePhase); float toneB = sinf(2.0f * 3.14159265f * snareTonePhase2); float tone = (toneA * 0.55f + toneB * 0.45f) * snareToneEnv; - // --- MIX --- float out = noiseOut * 0.75f + tone * 0.65f; return out * snareEnvAmp; } @@ -365,11 +360,10 @@ float DrumSynthVoice::processRim() { return (tick * 0.6f + bp * 0.7f) * rimEnv * 0.9f; } -// ---------------- Clap (hollow, multi-hand, boosted crack, no feedback) ---------------- float DrumSynthVoice::processClap() { if (!clapActive) return 0.0f; - // Envelopes + // envelopes clapEnv *= 0.99993f; // overall tail (slow) clapTrans *= 0.995f; // transient (fast) clapTailEnv *= 0.99990f; // tail/body (medium) @@ -393,10 +387,10 @@ float DrumSynthVoice::processClap() { dt = clapTime - t2; burst += a2 * expf(-(dt * dt) / (tau * tau)); dt = clapTime - t3; burst += a3 * expf(-(dt * dt) / (tau * tau)); - // Base noise (lower brightness to avoid “white noise”) + // base noise float w = (frand() * 0.55f + clapNoiseSeed * 0.45f); - // High-pass to remove lows + low-pass “air” to tame hiss + // high-pass to remove lows + low-pass “air” to tame hiss const float hpAlpha = 0.955f; // slightly lower = less hiss clapHp = hpAlpha * (clapHp + w - clapPrev); clapPrev = w; @@ -405,7 +399,7 @@ float DrumSynthVoice::processClap() { clapAirLp += lpAlpha * (clapHp - clapAirLp); float bandInput = clapAirLp; - // Two independent cavity bands (each cascaded to narrow the band) + // two independent cavity bands (each cascaded to narrow the band) // Formant A (lower mid cavity) const float bpFA = 0.29f, dampA = 0.26f; clapBpA += bpFA * (bandInput - clapLpA - dampA * clapBpA); @@ -413,17 +407,17 @@ float DrumSynthVoice::processClap() { clapBpA2 += bpFA * (clapBpA - clapLpA2 - dampA * clapBpA2); clapLpA2 += bpFA * clapBpA2; - // Formant B (upper mid cavity) + // formant B - upper mid cavity const float bpFB = 0.33f, dampB = 0.24f; clapBpB += bpFB * (bandInput - clapLpB - dampB * clapBpB); clapLpB += bpFB * clapBpB; clapBpB2 += bpFB * (clapBpB - clapLpB2 - dampB * clapBpB2); clapLpB2 += bpFB * clapBpB2; - // Narrow bands summed for hollow feel + // narrow bands summed for hollow feel float bandNarrow = clapBpA2 * 0.55f + clapBpB2 * 0.45f; - // Short tonal snaps near 1.3/1.6/2.0 kHz (very brief; under burst) + // short tonal snaps near 1.3/1.6/2.0 kHz clapSnapPhase1 += 1300.0f * invSampleRate; if (clapSnapPhase1 >= 1.0f) clapSnapPhase1 -= 1.0f; clapSnapPhase2 += 1600.0f * invSampleRate; if (clapSnapPhase2 >= 1.0f) clapSnapPhase2 -= 1.0f; clapSnapPhase3 += 2000.0f * invSampleRate; if (clapSnapPhase3 >= 1.0f) clapSnapPhase3 -= 1.0f; @@ -433,16 +427,16 @@ float DrumSynthVoice::processClap() { sinf(2.0f * 3.14159265f * clapSnapPhase2) * clapSnapEnv2 * 0.55f + sinf(2.0f * 3.14159265f * clapSnapPhase3) * clapSnapEnv3 * 0.45f; - // Extra transient crack (fast, only at burst peaks) + // extra transient crack (fast, only at burst peaks) float crack = (bandInput - bandNarrow) * 0.40f * clapCrackEnv; - // Body: narrow-band noise + snaps + crack, gated by burst + // body: narrow-band noise + snaps + crack, gated by burst float body = (bandNarrow * 0.90f + snap * 0.55f + crack * 0.50f) * burst * clapTrans; - // Tail: quieter narrow-band noise (keeps it “clappy” vs. a noise blip) + // tail: quieter narrow-band noise (keeps it “clappy” vs. a noise blip) float tail = bandNarrow * 0.48f * clapTailEnv; - // Feed-forward multi-hand cluster (no feedback; avoids glitches) + // feed-forward multi-hand cluster clapTapBuf[clapTapIdx] = body; int idx = clapTapIdx; int i1 = idx - clapD1; if (i1 < 0) i1 += clapTapLen; @@ -468,44 +462,48 @@ float DrumSynthVoice::processClap() { // Bus Compressor float DrumSynthVoice::processBus(float mixSample) { - // Update parameter cache (in case UI changed it) - compAmount = params[static_cast(DrumParamId::BusCompAmount)].value(); - compThreshDb = -18.0f + 12.0f * compAmount; // -18 .. -6 dB - compRatio = 2.0f + 4.0f * compAmount; // 2:1 .. 6:1 - compMakeupDb = 6.0f * compAmount; // up to ~+6 dB - compKneeDb = 6.0f; // fixed knee - - // Detector: peak-ish envelope follower - float inAbs = fabsf(mixSample); - float target = inAbs; - float coeff = (target > compEnv) ? compAttackCoeff : compReleaseCoeff; - compEnv += coeff * (target - compEnv); - - // Envelope to dB and soft-knee gain reduction - float levelDb = amp_to_db(compEnv); - float overDb = levelDb - compThreshDb; - float grDb = 0.0f; - if (overDb <= -compKneeDb * 0.5f) { - grDb = 0.0f; - } else if (overDb < compKneeDb * 0.5f) { - float x = (overDb + compKneeDb * 0.5f) / compKneeDb; // 0..1 - float kneeGain = (1.0f / compRatio - 1.0f) * (x * x); - grDb = kneeGain * compKneeDb; // negative - } else { - float levelOutDb = compThreshDb + overDb / compRatio; - grDb = levelOutDb - levelDb; // negative + const int kCompDecim = 4; // for tighter response, use 2 + + if (compDecimCounter == 0) { + compAmount = params[static_cast(DrumParamId::BusCompAmount)].value(); + compThreshDb = -18.0f + 12.0f * compAmount; // -18 .. -6 dB + compRatio = 2.0f + 4.0f * compAmount; // 2:1 .. 6:1 + compMakeupDb = 6.0f * compAmount; // up to ~+6 dB + compKneeDb = 6.0f; + + // bus comp detector + float inAbs = fabsf(mixSample); + float target = inAbs; + float coeff = (target > compEnv) ? compAttackCoeff : compReleaseCoeff; + compEnv += coeff * (target - compEnv); + + // dB-domain soft knee + float levelDb = amp_to_db(compEnv); + float overDb = levelDb - compThreshDb; + float grDb = 0.0f; + if (overDb <= -compKneeDb * 0.5f) { + grDb = 0.0f; + } else if (overDb < compKneeDb * 0.5f) { + float x = (overDb + compKneeDb * 0.5f) / compKneeDb; // 0..1 + float kneeGain = (1.0f / compRatio - 1.0f) * (x * x); + grDb = kneeGain * compKneeDb; // negative + } else { + float levelOutDb = compThreshDb + overDb / compRatio; + grDb = levelOutDb - levelDb; // negative + } + + // smooth gain reduction + const float grSmooth = 0.8f; + compGainDb = grSmooth * compGainDb + (1.0f - grSmooth) * grDb; + + // convert to amplitude + makeup once per update + compLastGainAmp = db_to_amp(compGainDb + compMakeupDb); } - // Smooth gain reduction - const float grSmooth = 0.8f; - compGainDb = grSmooth * compGainDb + (1.0f - grSmooth) * grDb; - - // Apply makeup and reduction - float out = mixSample * db_to_amp(compGainDb + compMakeupDb); - return out; + compDecimCounter = (compDecimCounter + 1) % kCompDecim; + return mixSample * compLastGainAmp; } -// ---------------- Params ---------------- const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { return params[static_cast(id)]; } diff --git a/src/dsp/mini_drumvoices.h b/src/dsp/mini_drumvoices.h index ac205f8..ca25f07 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -3,10 +3,9 @@ #include #include "mini_dsp_params.h" -// Public parameter ids (kept for compatibility + bus comp) enum class DrumParamId : uint8_t { MainVolume = 0, - BusCompAmount, // 0..1 one-knob compressor + BusCompAmount, // 0..1 Count }; @@ -36,13 +35,13 @@ class DrumSynthVoice { float processRim(); float processClap(); // updated - // Bus processing (apply one-knob compressor to any mix sample) + // Bus processing float processBus(float mixSample); // Snare float snareHpPrev; // extra high-pass memory - // Parameters + // Parameters - for later const Parameter& parameter(DrumParamId id) const; void setParameter(DrumParamId id, float value); @@ -51,39 +50,39 @@ class DrumSynthVoice { float frand(); uint32_t rngState; - // ----- Kick (606-tight) ----- + // Kick (606-tight) float kickPhase, kickFreq, kickEnvAmp, kickEnvPitch, kickClickEnv; bool kickActive; - // ----- Snare ----- + // Snare float snareEnvAmp, snareToneEnv; bool snareActive; float snareBp, snareLp, snareTonePhase, snareTonePhase2; - // ----- Closed Hat (metallic) ----- + // Closed Hat (metallic) float hatEnvAmp, hatToneEnv; bool hatActive; float hatHp, hatPrev; float hatPh[6], hatInc[6]; - // ----- Open Hat ----- + // Open Hat float openHatEnvAmp, openHatToneEnv; bool openHatActive; float openHatHp, openHatPrev; float openHatPh[6], openHatInc[6]; - // ----- Toms ----- + // Toms float midTomPhase, midTomEnv, midTomPitchEnv; bool midTomActive; float highTomPhase, highTomEnv, highTomPitchEnv; bool highTomActive; - // ----- Rimshot ----- + // Rimshot float rimPhase, rimEnv, rimBp, rimLp; bool rimActive; - // ----- Clap (hollow, multi-hand) ----- + // Clap (hollow, multi-hand) float clapEnv; // overall body envelope (slow) float clapTrans; // transient envelope (fast) float clapTailEnv; // tail/body envelope (medium) @@ -104,7 +103,7 @@ class DrumSynthVoice { float clapSnapEnv1, clapSnapEnv2, clapSnapEnv3; float clapCrackEnv; - // Feed-forward multi-tap cluster (no feedback) + // feed-forward multi-tap cluster (no feedback) static const int kClapTapBufMax = 2048; // big enough for ~30 ms @ 44.1 kHz float clapTapBuf[kClapTapBufMax]; int clapTapIdx; @@ -114,10 +113,12 @@ class DrumSynthVoice { // Sample rate float sampleRate, invSampleRate; - // ----- One-knob Bus Compressor ----- + // Bus Compressor float compEnv, compAttackCoeff, compReleaseCoeff; float compGainDb, compMakeupDb, compThreshDb, compRatio, compKneeDb, compAmount; - - // Global params + int compDecimCounter; // sub-rate update counter + float compLastGainAmp; // last applied amplitude gain + + // Global params - for later Parameter params[static_cast(DrumParamId::Count)]; }; diff --git a/src/dsp/miniacid_engine.cpp b/src/dsp/miniacid_engine.cpp index e99696e..5c1582d 100644 --- a/src/dsp/miniacid_engine.cpp +++ b/src/dsp/miniacid_engine.cpp @@ -775,7 +775,7 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { float sampleOut = 0.0f; if (playing) { - // ---- 303 voices (with tempo delay) ---- + // 303 voices (with tempo delay) float sample303 = 0.0f; if (!mute303) { float v = voice303.process() * 0.5f; @@ -791,7 +791,6 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { delay3032.process(0.0f); } - // ---- DRUM BUS (sum all drums, then bus-comp) ---- float drumSum = 0.0f; if (!muteKick) drumSum += drums.processKick(); if (!muteSnare) drumSum += drums.processSnare(); @@ -802,18 +801,17 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { if (!muteRim) drumSum += drums.processRim(); if (!muteClap) drumSum += drums.processClap(); - // Bus compressor applies to the whole DRUM MIX - // if you want to compress the entire mix (drums+303), move the processBus() call after sampleOut = drumSum + sample303. + // Bus compressor can be applied to the whole mix, or just the drums + // uncoment the line below to process the drums w/ the bus comp // drumSum = drums.processBus(drumSum); - // ---- Mix drums + 303 ---- sampleOut = drumSum + sample303; // uncomment to use bus comp on the whole mix sampleOut = drums.processBus(sampleOut); } - // Soft clipping/limiting + // soft clipping/limiting sampleOut *= 0.65f; if (sampleOut > 1.0f) sampleOut = 1.0f; if (sampleOut < -1.0f) sampleOut = -1.0f; From 6b336244c259091e5a5e7afae45910665876827b Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 11:17:58 -0600 Subject: [PATCH 14/20] bus comp ONLY on the drums - I think I prefer this --- src/dsp/miniacid_engine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dsp/miniacid_engine.cpp b/src/dsp/miniacid_engine.cpp index 5c1582d..0f232d1 100644 --- a/src/dsp/miniacid_engine.cpp +++ b/src/dsp/miniacid_engine.cpp @@ -803,12 +803,12 @@ void MiniAcid::generateAudioBuffer(int16_t *buffer, size_t numSamples) { // Bus compressor can be applied to the whole mix, or just the drums // uncoment the line below to process the drums w/ the bus comp - // drumSum = drums.processBus(drumSum); + drumSum = drums.processBus(drumSum); sampleOut = drumSum + sample303; // uncomment to use bus comp on the whole mix - sampleOut = drums.processBus(sampleOut); + // sampleOut = drums.processBus(sampleOut); } // soft clipping/limiting From c9ecbd0c7b0cc929d49b4326ef6d1d96f8bae51b Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 11:59:27 -0600 Subject: [PATCH 15/20] first round of pattern updates - don't forget to add to help! --- src/ui/pages/pattern_edit_page.cpp | 221 ++++++++++++++++++++++++++--- src/ui/pages/pattern_edit_page.h | 28 +++- 2 files changed, 230 insertions(+), 19 deletions(-) diff --git a/src/ui/pages/pattern_edit_page.cpp b/src/ui/pages/pattern_edit_page.cpp index 9e2f30f..20e4912 100644 --- a/src/ui/pages/pattern_edit_page.cpp +++ b/src/ui/pages/pattern_edit_page.cpp @@ -1,8 +1,7 @@ #include "pattern_edit_page.h" - #include #include - +#include // memcpy #include "../help_dialog.h" PatternEditPage::PatternEditPage(IGfx& gfx, MiniAcid& mini_acid, AudioGuard& audio_guard, int voice_index) @@ -179,8 +178,123 @@ bool PatternEditPage::hasHelpDialog() { return true; } +// --- helpers for last note + buffer + transpose --- + +int PatternEditPage::currentStepNote(int step) const { + const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); + if (!notes) return -1; + if (step < 0 || step >= SEQ_STEPS) return -1; + return notes[step]; +} + +void PatternEditPage::rememberLastNoteFromStep(int step) { + int n = currentStepNote(step); + if (n >= 0) last_entered_note_ = n; +} + +// Sets an empty step to an absolute note using existing adjust APIs. +// Strategy: nudge from known engine default (C#1 or C2) and then adjust by delta-to-target. +void PatternEditPage::setEmptyStepToAbsoluteNote(int step, int target_note) { + if (target_note < 0) { + withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, step); }); + return; + } + // Prime the step from empty: a +1 semitone turns empty into base C#1. + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, 1); }); + int cur = currentStepNote(step); + int delta = target_note - cur; + if (delta != 0) { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, delta); }); + } + rememberLastNoteFromStep(step); +} + +void PatternEditPage::copyCurrentPatternToBuffer() { + const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); + const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + if (!notes || !accent || !slide) return; + std::memcpy(buffer_.notes, notes, sizeof(buffer_.notes)); + std::memcpy(buffer_.accent, accent, sizeof(buffer_.accent)); + std::memcpy(buffer_.slide, slide, sizeof(buffer_.slide)); + buffer_.has_data = true; +} + +void PatternEditPage::cutCurrentPatternToBuffer() { + copyCurrentPatternToBuffer(); + // Clear notes, accent, slide + for (int i = 0; i < SEQ_STEPS; ++i) { + int n = currentStepNote(i); + if (n >= 0) { + withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, i); }); + } + const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + if (accent && accent[i]) { + withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, i); }); + } + if (slide && slide[i]) { + withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, i); }); + } + } +} + +void PatternEditPage::pasteBufferToCurrentPattern() { + if (!buffer_.has_data) return; + const bool* accentCur = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slideCur = mini_acid_.pattern303SlideSteps(voice_index_); + for (int i = 0; i < SEQ_STEPS; ++i) { + int cur = currentStepNote(i); + int tgt = buffer_.notes[i]; + if (tgt < 0) { + if (cur >= 0) { + withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, i); }); + } + } else { + if (cur < 0) { + // from empty → absolute + setEmptyStepToAbsoluteNote(i, tgt); + } else { + int delta = tgt - cur; + if (delta != 0) { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, i, delta); }); + } + rememberLastNoteFromStep(i); + } + } + // Accent reconcile + if (accentCur) { + bool want = buffer_.accent[i]; + bool have = accentCur[i]; + if (want != have) { + withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, i); }); + } + } + // Slide reconcile + if (slideCur) { + bool want = buffer_.slide[i]; + bool have = slideCur[i]; + if (want != have) { + withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, i); }); + } + } + } +} + +void PatternEditPage::transposePatternSemitone(int delta) { + if (delta == 0) return; + for (int i = 0; i < SEQ_STEPS; ++i) { + int n = currentStepNote(i); + if (n >= 0) { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, i, delta); }); + } + } +} + +// --- event handling --- bool PatternEditPage::handleEvent(UIEvent& ui_event) { if (ui_event.event_type != MINIACID_KEY_DOWN) return false; + bool handled = false; switch (ui_event.scancode) { case MINIACID_LEFT: @@ -207,6 +321,7 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { char key = ui_event.key; if (!key) return false; + // Enter on pattern row commits pattern selection if ((key == '\n' || key == '\r') && patternRowFocused()) { if (mini_acid_.songModeEnabled()) return true; int cursor = activePatternCursor(); @@ -218,8 +333,8 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { int patternIdx = patternIndexFromKey(key); bool patternKeyReserved = false; if (patternIdx >= 0) { - char lowerKey = static_cast(std::tolower(static_cast(key))); - patternKeyReserved = (lowerKey == 'q' || lowerKey == 'w'); + char lowerKeyForPattern = static_cast(std::tolower(static_cast(key))); + patternKeyReserved = (lowerKeyForPattern == 'q' || lowerKeyForPattern == 'w'); if (!patternKeyReserved || patternRowFocused()) { if (mini_acid_.songModeEnabled()) return true; focusPatternRow(); @@ -237,52 +352,123 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } }; + // Cut/Copy/Paste: ASCII control codes first, fallback to V/B/M + unsigned char ucharKey = static_cast(key); + if (ucharKey == 0x18 /*^X*/ || std::tolower(ucharKey) == 'v') { // Cut + cutCurrentPatternToBuffer(); + return true; + } + if (ucharKey == 0x03 /*^C*/ || std::tolower(ucharKey) == 'b') { // Copy + copyCurrentPatternToBuffer(); + return true; + } + if (ucharKey == 0x16 /*^V*/ || std::tolower(ucharKey) == 'n') { // Paste + pasteBufferToCurrentPattern(); + return true; + } + + // Transpose entire pattern char lowerKey = static_cast(std::tolower(static_cast(key))); + if (lowerKey == 'h') { // transpose up + transposePatternSemitone(+1); + return true; + } + if (lowerKey == 'j') { // transpose down + transposePatternSemitone(-1); + return true; + } + + // per-step edits switch (lowerKey) { - case 'q': { + case 'q': { // slide toggle ensureStepFocusAndCursor(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, step); }); return true; } - case 'w': { + case 'w': { // accent toggle ensureStepFocusAndCursor(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, step); }); return true; } - case 'a': { + case 'a': { // note + (with last-note on empty) ensureStepFocusAndCursor(); int step = activePatternStep(); - withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, 1); }); + int cur = currentStepNote(step); + if (cur < 0) { + if (last_entered_note_ >= 0) { + setEmptyStepToAbsoluteNote(step, last_entered_note_); + } else { + // original behavior + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, 1); }); + rememberLastNoteFromStep(step); + } + } else { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, 1); }); + rememberLastNoteFromStep(step); + } return true; } - case 'z': { + case 'z': { // note - (with last-note on empty) ensureStepFocusAndCursor(); int step = activePatternStep(); - withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, -1); }); + int cur = currentStepNote(step); + if (cur < 0) { + if (last_entered_note_ >= 0) { + setEmptyStepToAbsoluteNote(step, last_entered_note_); + } else { + // keep empty (no-op) + } + } else { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, step, -1); }); + rememberLastNoteFromStep(step); + } return true; } - case 's': { + case 's': { // octave + (with last-note+octave on empty) ensureStepFocusAndCursor(); int step = activePatternStep(); - withAudioGuard([&]() { mini_acid_.adjust303StepOctave(voice_index_, step, 1); }); + int cur = currentStepNote(step); + if (cur < 0) { + if (last_entered_note_ >= 0) { + setEmptyStepToAbsoluteNote(step, last_entered_note_ + 12); + } else { + // original behavior + withAudioGuard([&]() { mini_acid_.adjust303StepOctave(voice_index_, step, 1); }); + rememberLastNoteFromStep(step); + } + } else { + withAudioGuard([&]() { mini_acid_.adjust303StepOctave(voice_index_, step, 1); }); + rememberLastNoteFromStep(step); + } return true; } - case 'x': { + case 'x': { // octave - (with last-note-octave on empty) ensureStepFocusAndCursor(); int step = activePatternStep(); - withAudioGuard([&]() { mini_acid_.adjust303StepOctave(voice_index_, step, -1); }); + int cur = currentStepNote(step); + if (cur < 0) { + if (last_entered_note_ >= 0) { + setEmptyStepToAbsoluteNote(step, last_entered_note_ - 12); + } else { + // keep empty (no-op) + } + } else { + withAudioGuard([&]() { mini_acid_.adjust303StepOctave(voice_index_, step, -1); }); + rememberLastNoteFromStep(step); + } return true; } default: break; } - if (key == '\b') { + if (key == '\b') { // backspace = clear step ensureStepFocusAndCursor(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, step); }); + // keep last_entered_note_ unchanged return true; } @@ -290,7 +476,6 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } void PatternEditPage::draw(IGfx& gfx, int x, int y, int w, int h) { - int body_y = y + 2; int body_h = h - 2; if (body_h <= 0) return; @@ -298,6 +483,7 @@ void PatternEditPage::draw(IGfx& gfx, int x, int y, int w, int h) { const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + int stepCursor = pattern_edit_cursor_; int playing = mini_acid_.currentStep(); int selectedPattern = mini_acid_.display303PatternIndex(voice_index_); @@ -334,7 +520,7 @@ void PatternEditPage::draw(IGfx& gfx, int x, int y, int w, int h) { gfx.drawRect(cell_x - 2, pattern_row_y - 2, pattern_size + 4, pattern_size_height + 4, COLOR_STEP_SELECTED); } char label[4]; - snprintf(label, sizeof(label), "%d", i + 1); + std::snprintf(label, sizeof(label), "%d", i + 1); int tw = textWidth(gfx, label); int tx = cell_x + (pattern_size - tw) / 2; int ty = pattern_row_y + pattern_size_height / 2 - gfx.fontHeight() / 2; @@ -349,7 +535,6 @@ void PatternEditPage::draw(IGfx& gfx, int x, int y, int w, int h) { int indicator_h = 5; int indicator_gap = 1; int row_height = indicator_h + indicator_gap + cell_size + 4; - for (int i = 0; i < SEQ_STEPS; ++i) { int row = i / 8; int col = i % 8; diff --git a/src/ui/pages/pattern_edit_page.h b/src/ui/pages/pattern_edit_page.h index 08017b8..574a291 100644 --- a/src/ui/pages/pattern_edit_page.h +++ b/src/ui/pages/pattern_edit_page.h @@ -1,5 +1,5 @@ #pragma once - +#include // for std::function #include "../ui_core.h" #include "../ui_colors.h" #include "../ui_utils.h" @@ -27,11 +27,27 @@ class PatternEditPage : public IPage { private: enum class Focus { Steps = 0, PatternRow }; + // helpers / state int clampCursor(int cursorIndex) const; int patternIndexFromKey(char key) const; void ensureStepFocus(); void withAudioGuard(const std::function& fn); + // last-note memory + void rememberLastNoteFromStep(int step); + int currentStepNote(int step) const; + + // absolute set for empty steps (uses existing adjust APIs safely) + void setEmptyStepToAbsoluteNote(int step, int target_note); + + // cut/copy/paste helpers + void copyCurrentPatternToBuffer(); + void cutCurrentPatternToBuffer(); + void pasteBufferToCurrentPattern(); + + // transpose helpers + void transposePatternSemitone(int delta); + IGfx& gfx_; MiniAcid& mini_acid_; AudioGuard& audio_guard_; @@ -42,4 +58,14 @@ class PatternEditPage : public IPage { int help_page_index_ = 0; int total_help_pages_ = 1; std::string title_; + + // new state: last note memory & pattern buffer + int last_entered_note_ = -1; // -1 = none yet + + struct PatternBuffer { + bool has_data = false; + int8_t notes[SEQ_STEPS] = {0}; + bool accent[SEQ_STEPS] = {false}; + bool slide[SEQ_STEPS] = {false}; + } buffer_; }; From 1bce451e10c4145ba604869f76d0e0ec98515fff Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 14:32:53 -0600 Subject: [PATCH 16/20] changed cut, copy, paste keys; added D for duplicate top row to bottom; added pattern rotation (F,G); transposition in last commit --- src/ui/pages/pattern_edit_page.cpp | 110 +++++++++++++++++++++++------ src/ui/pages/pattern_edit_page.h | 4 ++ 2 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/ui/pages/pattern_edit_page.cpp b/src/ui/pages/pattern_edit_page.cpp index 20e4912..364c40a 100644 --- a/src/ui/pages/pattern_edit_page.cpp +++ b/src/ui/pages/pattern_edit_page.cpp @@ -291,6 +291,79 @@ void PatternEditPage::transposePatternSemitone(int delta) { } } +// --- rotation + duplication --- +void PatternEditPage::rotatePattern(int dir) { + // dir: +1 = forward (right), -1 = backward (left) + const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); + const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + if (!notes || !accent || !slide) return; + + int8_t nbuf[SEQ_STEPS]; + bool abuf[SEQ_STEPS]; + bool sbuf[SEQ_STEPS]; + + int len = SEQ_STEPS; + for (int j = 0; j < len; ++j) { + int src = (j - dir) % len; + if (src < 0) src += len; + nbuf[j] = notes[src]; + abuf[j] = accent[src]; + sbuf[j] = slide[src]; + } + + const bool* accentCur = accent; + const bool* slideCur = slide; + for (int j = 0; j < len; ++j) { + int cur = currentStepNote(j); + int tgt = nbuf[j]; + if (tgt < 0) { + if (cur >= 0) withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, j); }); + } else { + if (cur < 0) setEmptyStepToAbsoluteNote(j, tgt); + else { + int delta = tgt - cur; + if (delta != 0) withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, j, delta); }); + rememberLastNoteFromStep(j); + } + } + // accent/slide reconcile + if (accentCur) { bool have = accentCur[j]; bool want = abuf[j]; if (have != want) withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, j); }); } + if (slideCur) { bool have = slideCur[j]; bool want = sbuf[j]; if (have != want) withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, j); }); } + } +} + +void PatternEditPage::duplicateTopRowToBottomRow() { + const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); + const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + if (!notes || !accent || !slide) return; + + for (int i = 0; i < 8; ++i) { + int dst = i + 8; + int cur = currentStepNote(dst); + int tgt = notes[i]; + if (tgt < 0) { + if (cur >= 0) withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, dst); }); + } else { + if (cur < 0) setEmptyStepToAbsoluteNote(dst, tgt); + else { + int delta = tgt - cur; + if (delta != 0) withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, dst, delta); }); + rememberLastNoteFromStep(dst); + } + } + // accent/slide + const bool wantA = accent[i]; + const bool haveA = accent[dst]; + if (wantA != haveA) withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, dst); }); + + const bool wantS = slide[i]; + const bool haveS = slide[dst]; + if (wantS != haveS) withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, dst); }); + } +} + // --- event handling --- bool PatternEditPage::handleEvent(UIEvent& ui_event) { if (ui_event.event_type != MINIACID_KEY_DOWN) return false; @@ -352,31 +425,22 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } }; - // Cut/Copy/Paste: ASCII control codes first, fallback to V/B/M - unsigned char ucharKey = static_cast(key); - if (ucharKey == 0x18 /*^X*/ || std::tolower(ucharKey) == 'v') { // Cut - cutCurrentPatternToBuffer(); - return true; - } - if (ucharKey == 0x03 /*^C*/ || std::tolower(ucharKey) == 'b') { // Copy - copyCurrentPatternToBuffer(); - return true; - } - if (ucharKey == 0x16 /*^V*/ || std::tolower(ucharKey) == 'n') { // Paste - pasteBufferToCurrentPattern(); - return true; - } + // Cut/Copy/Paste mapped to V (cut), B (copy), N (paste) + char lowerKey = static_cast(std::tolower(static_cast(key))); + if (lowerKey == 'v') { cutCurrentPatternToBuffer(); return true; } + if (lowerKey == 'b') { copyCurrentPatternToBuffer(); return true; } + if (lowerKey == 'n') { pasteBufferToCurrentPattern(); return true; } // Transpose entire pattern - char lowerKey = static_cast(std::tolower(static_cast(key))); - if (lowerKey == 'h') { // transpose up - transposePatternSemitone(+1); - return true; - } - if (lowerKey == 'j') { // transpose down - transposePatternSemitone(-1); - return true; - } + if (lowerKey == 'h') { transposePatternSemitone(+1); return true; } + if (lowerKey == 'j') { transposePatternSemitone(-1); return true; } + + // Rotation: 'f' = backward (left), 'g' = forward (right) + if (lowerKey == 'f') { rotatePattern(-1); return true; } + if (lowerKey == 'g') { rotatePattern(+1); return true; } + + // Duplicate: 'd' = copy top row (0..7) to bottom row (8..15) + if (lowerKey == 'd') { duplicateTopRowToBottomRow(); return true; } // per-step edits switch (lowerKey) { diff --git a/src/ui/pages/pattern_edit_page.h b/src/ui/pages/pattern_edit_page.h index 574a291..642a68d 100644 --- a/src/ui/pages/pattern_edit_page.h +++ b/src/ui/pages/pattern_edit_page.h @@ -48,6 +48,10 @@ class PatternEditPage : public IPage { // transpose helpers void transposePatternSemitone(int delta); + // rotation + duplication + void rotatePattern(int dir); // dir = +1 forward, -1 backward + void duplicateTopRowToBottomRow(); + IGfx& gfx_; MiniAcid& mini_acid_; AudioGuard& audio_guard_; From 02613a2274e66e005b15f3f44a12cc5516d5a47f Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 15:47:27 -0600 Subject: [PATCH 17/20] d - transpose up half-step; c - transpose down half-step; f - rotate forward/right; v - rotate backward/left; h - copy pattern; n - paste pattern; j - undo; m - redo; backslash - cut/clear; single quote - copy steps 0-7 into 8-15 --- src/ui/pages/pattern_edit_page.cpp | 109 ++++++++++++++++++++++++----- src/ui/pages/pattern_edit_page.h | 21 +++++- 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/ui/pages/pattern_edit_page.cpp b/src/ui/pages/pattern_edit_page.cpp index 364c40a..b16fcfe 100644 --- a/src/ui/pages/pattern_edit_page.cpp +++ b/src/ui/pages/pattern_edit_page.cpp @@ -192,8 +192,6 @@ void PatternEditPage::rememberLastNoteFromStep(int step) { if (n >= 0) last_entered_note_ = n; } -// Sets an empty step to an absolute note using existing adjust APIs. -// Strategy: nudge from known engine default (C#1 or C2) and then adjust by delta-to-target. void PatternEditPage::setEmptyStepToAbsoluteNote(int step, int target_note) { if (target_note < 0) { withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, step); }); @@ -221,8 +219,8 @@ void PatternEditPage::copyCurrentPatternToBuffer() { } void PatternEditPage::cutCurrentPatternToBuffer() { + pushUndo(); copyCurrentPatternToBuffer(); - // Clear notes, accent, slide for (int i = 0; i < SEQ_STEPS; ++i) { int n = currentStepNote(i); if (n >= 0) { @@ -241,6 +239,7 @@ void PatternEditPage::cutCurrentPatternToBuffer() { void PatternEditPage::pasteBufferToCurrentPattern() { if (!buffer_.has_data) return; + pushUndo(); const bool* accentCur = mini_acid_.pattern303AccentSteps(voice_index_); const bool* slideCur = mini_acid_.pattern303SlideSteps(voice_index_); for (int i = 0; i < SEQ_STEPS; ++i) { @@ -283,6 +282,7 @@ void PatternEditPage::pasteBufferToCurrentPattern() { void PatternEditPage::transposePatternSemitone(int delta) { if (delta == 0) return; + pushUndo(); for (int i = 0; i < SEQ_STEPS; ++i) { int n = currentStepNote(i); if (n >= 0) { @@ -299,6 +299,8 @@ void PatternEditPage::rotatePattern(int dir) { const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); if (!notes || !accent || !slide) return; + pushUndo(); + int8_t nbuf[SEQ_STEPS]; bool abuf[SEQ_STEPS]; bool sbuf[SEQ_STEPS]; @@ -339,6 +341,8 @@ void PatternEditPage::duplicateTopRowToBottomRow() { const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); if (!notes || !accent || !slide) return; + pushUndo(); + for (int i = 0; i < 8; ++i) { int dst = i + 8; int cur = currentStepNote(dst); @@ -364,7 +368,72 @@ void PatternEditPage::duplicateTopRowToBottomRow() { } } -// --- event handling --- +// --- undo/redo helpers --- +void PatternEditPage::captureCurrentPattern(PatternState& out) const { + const int8_t* notes = mini_acid_.pattern303Steps(voice_index_); + const bool* accent = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slide = mini_acid_.pattern303SlideSteps(voice_index_); + if (!notes || !accent || !slide) return; // should not happen + std::memcpy(out.notes, notes, sizeof(out.notes)); + std::memcpy(out.accent, accent, sizeof(out.accent)); + std::memcpy(out.slide, slide, sizeof(out.slide)); +} + +void PatternEditPage::applyPatternState(const PatternState& st) { + const bool* accentCur = mini_acid_.pattern303AccentSteps(voice_index_); + const bool* slideCur = mini_acid_.pattern303SlideSteps(voice_index_); + for (int i = 0; i < SEQ_STEPS; ++i) { + int cur = currentStepNote(i); + int tgt = st.notes[i]; + if (tgt < 0) { + if (cur >= 0) withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, i); }); + } else { + if (cur < 0) setEmptyStepToAbsoluteNote(i, tgt); + else { + int delta = tgt - cur; + if (delta != 0) withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, i, delta); }); + rememberLastNoteFromStep(i); + } + } + if (accentCur) { bool want = st.accent[i]; bool have = accentCur[i]; if (want != have) withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, i); }); } + if (slideCur) { bool want = st.slide[i]; bool have = slideCur[i]; if (want != have) withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, i); }); } + } +} + +void PatternEditPage::pushUndo() { + PatternState s{}; + captureCurrentPattern(s); + undo_stack_.push_back(s); + if (undo_stack_.size() > static_cast(kMaxHistory)) { + undo_stack_.erase(undo_stack_.begin()); + } + clearRedo(); +} + +void PatternEditPage::clearRedo() { + redo_stack_.clear(); +} + +bool PatternEditPage::undo() { + if (undo_stack_.empty()) return false; + PatternState cur{}; captureCurrentPattern(cur); + redo_stack_.push_back(cur); + PatternState prev = undo_stack_.back(); + undo_stack_.pop_back(); + applyPatternState(prev); + return true; +} + +bool PatternEditPage::redo() { + if (redo_stack_.empty()) return false; + PatternState cur{}; captureCurrentPattern(cur); + undo_stack_.push_back(cur); + PatternState next = redo_stack_.back(); + redo_stack_.pop_back(); + applyPatternState(next); + return true; +} + bool PatternEditPage::handleEvent(UIEvent& ui_event) { if (ui_event.event_type != MINIACID_KEY_DOWN) return false; @@ -425,39 +494,37 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } }; - // Cut/Copy/Paste mapped to V (cut), B (copy), N (paste) char lowerKey = static_cast(std::tolower(static_cast(key))); - if (lowerKey == 'v') { cutCurrentPatternToBuffer(); return true; } - if (lowerKey == 'b') { copyCurrentPatternToBuffer(); return true; } + if (lowerKey == 'd') { transposePatternSemitone(+1); return true; } + if (lowerKey == 'c') { transposePatternSemitone(-1); return true; } + if (lowerKey == 'f') { rotatePattern(+1); return true; } + if (lowerKey == 'v') { rotatePattern(-1); return true; } + if (lowerKey == 'h') { copyCurrentPatternToBuffer(); return true; } if (lowerKey == 'n') { pasteBufferToCurrentPattern(); return true; } + if (lowerKey == 'j') { if (undo()) return true; } + if (lowerKey == 'm') { if (redo()) return true; } + if (key == '\'') { duplicateTopRowToBottomRow(); return true; } + if (key == '\\') { cutCurrentPatternToBuffer(); return true; } - // Transpose entire pattern - if (lowerKey == 'h') { transposePatternSemitone(+1); return true; } - if (lowerKey == 'j') { transposePatternSemitone(-1); return true; } - - // Rotation: 'f' = backward (left), 'g' = forward (right) - if (lowerKey == 'f') { rotatePattern(-1); return true; } - if (lowerKey == 'g') { rotatePattern(+1); return true; } - - // Duplicate: 'd' = copy top row (0..7) to bottom row (8..15) - if (lowerKey == 'd') { duplicateTopRowToBottomRow(); return true; } - - // per-step edits + // per-step edits (unchanged) switch (lowerKey) { case 'q': { // slide toggle ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, step); }); return true; } case 'w': { // accent toggle ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, step); }); return true; } case 'a': { // note + (with last-note on empty) ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); int cur = currentStepNote(step); if (cur < 0) { @@ -476,6 +543,7 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } case 'z': { // note - (with last-note on empty) ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); int cur = currentStepNote(step); if (cur < 0) { @@ -492,6 +560,7 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } case 's': { // octave + (with last-note+octave on empty) ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); int cur = currentStepNote(step); if (cur < 0) { @@ -510,6 +579,7 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { } case 'x': { // octave - (with last-note-octave on empty) ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); int cur = currentStepNote(step); if (cur < 0) { @@ -530,6 +600,7 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { if (key == '\b') { // backspace = clear step ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, step); }); // keep last_entered_note_ unchanged diff --git a/src/ui/pages/pattern_edit_page.h b/src/ui/pages/pattern_edit_page.h index 642a68d..2a0cf55 100644 --- a/src/ui/pages/pattern_edit_page.h +++ b/src/ui/pages/pattern_edit_page.h @@ -1,5 +1,6 @@ #pragma once -#include // for std::function +#include +#include #include "../ui_core.h" #include "../ui_colors.h" #include "../ui_utils.h" @@ -52,6 +53,20 @@ class PatternEditPage : public IPage { void rotatePattern(int dir); // dir = +1 forward, -1 backward void duplicateTopRowToBottomRow(); + // --- undo/redo --- + struct PatternState { + int8_t notes[SEQ_STEPS]; + bool accent[SEQ_STEPS]; + bool slide[SEQ_STEPS]; + }; + static constexpr int kMaxHistory = 64; + void captureCurrentPattern(PatternState& out) const; + void applyPatternState(const PatternState& st); + void pushUndo(); + void clearRedo(); + bool undo(); + bool redo(); + IGfx& gfx_; MiniAcid& mini_acid_; AudioGuard& audio_guard_; @@ -72,4 +87,8 @@ class PatternEditPage : public IPage { bool accent[SEQ_STEPS] = {false}; bool slide[SEQ_STEPS] = {false}; } buffer_; + + // undo/redo stacks + std::vector undo_stack_; + std::vector redo_stack_; }; From e4ced90c12f9b7bca020b45ab790bada411c2f20 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 15:57:01 -0600 Subject: [PATCH 18/20] d - transpose up half-step; c - transpose down half-step; f - rotate forward/right; v - rotate backward/left; h - copy pattern; n - paste pattern; j - undo; m - redo; backslash - cut/clear; single quote - copy steps 0-7 into 8-15 --- src/ui/pages/drum_sequencer_page.cpp | 349 +++++++++++++++++---------- src/ui/pages/drum_sequencer_page.h | 33 ++- 2 files changed, 259 insertions(+), 123 deletions(-) diff --git a/src/ui/pages/drum_sequencer_page.cpp b/src/ui/pages/drum_sequencer_page.cpp index b5d750c..c508fb9 100644 --- a/src/ui/pages/drum_sequencer_page.cpp +++ b/src/ui/pages/drum_sequencer_page.cpp @@ -1,18 +1,16 @@ #include "drum_sequencer_page.h" - #include #include - #include "../help_dialog.h" DrumSequencerPage::DrumSequencerPage(IGfx& gfx, MiniAcid& mini_acid, AudioGuard& audio_guard) - : gfx_(gfx), - mini_acid_(mini_acid), - audio_guard_(audio_guard), - drum_step_cursor_(0), - drum_voice_cursor_(0), - drum_pattern_cursor_(0), - drum_pattern_focus_(true) { + : gfx_(gfx), + mini_acid_(mini_acid), + audio_guard_(audio_guard), + drum_step_cursor_(0), + drum_voice_cursor_(0), + drum_pattern_cursor_(0), + drum_pattern_focus_(true) { int drumIdx = mini_acid_.currentDrumPatternIndex(); if (drumIdx < 0 || drumIdx >= Bank::kPatterns) drumIdx = 0; drum_pattern_cursor_ = drumIdx; @@ -76,7 +74,6 @@ void DrumSequencerPage::moveDrumCursorVertical(int delta) { } return; } - int voice = activeDrumVoice(); int newVoice = voice + delta; if (newVoice < 0 || newVoice >= NUM_DRUM_VOICES) { @@ -87,74 +84,205 @@ void DrumSequencerPage::moveDrumCursorVertical(int delta) { drum_voice_cursor_ = newVoice; } -void DrumSequencerPage::focusPatternRow() { - setDrumPatternCursor(drum_pattern_cursor_); - drum_pattern_focus_ = true; +void DrumSequencerPage::focusPatternRow() { setDrumPatternCursor(drum_pattern_cursor_); drum_pattern_focus_ = true; } +void DrumSequencerPage::focusGrid() { drum_pattern_focus_ = false; drum_step_cursor_ = activeDrumStep(); drum_voice_cursor_ = activeDrumVoice(); } +bool DrumSequencerPage::patternRowFocused() const { if (mini_acid_.songModeEnabled()) return false; return drum_pattern_focus_; } + +int DrumSequencerPage::patternIndexFromKey(char key) const { + switch (std::tolower(static_cast(key))) { + case 'q': return 0; case 'w': return 1; case 'e': return 2; case 'r': return 3; + case 't': return 4; case 'y': return 5; case 'u': return 6; case 'i': return 7; + default: return -1; + } } -void DrumSequencerPage::focusGrid() { - drum_pattern_focus_ = false; - drum_step_cursor_ = activeDrumStep(); - drum_voice_cursor_ = activeDrumVoice(); +void DrumSequencerPage::withAudioGuard(const std::function& fn) { + if (audio_guard_) { audio_guard_(fn); return; } fn(); } -bool DrumSequencerPage::patternRowFocused() const { - if (mini_acid_.songModeEnabled()) return false; - return drum_pattern_focus_; +// --- buffer & undo helpers --- +static inline void fetchHits(const MiniAcid& ma, + const bool*& kick, const bool*& snare, const bool*& hat, const bool*& openHat, + const bool*& midTom, const bool*& highTom, const bool*& rim, const bool*& clap) { + kick = ma.patternKickSteps(); + snare = ma.patternSnareSteps(); + hat = ma.patternHatSteps(); + openHat = ma.patternOpenHatSteps(); + midTom = ma.patternMidTomSteps(); + highTom = ma.patternHighTomSteps(); + rim = ma.patternRimSteps(); + clap = ma.patternClapSteps(); } -int DrumSequencerPage::patternIndexFromKey(char key) const { - switch (std::tolower(static_cast(key))) { - case 'q': return 0; - case 'w': return 1; - case 'e': return 2; - case 'r': return 3; - case 't': return 4; - case 'y': return 5; - case 'u': return 6; - case 'i': return 7; - default: return -1; +void DrumSequencerPage::captureCurrentDrumPattern(DrumPatternState& out) const { + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) + for (int s = 0; s < SEQ_STEPS; ++s) + out.hits[v][s] = hits[v] ? hits[v][s] : false; +} + +void DrumSequencerPage::applyDrumPatternState(const DrumPatternState& st) { + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + bool have = hits[v] ? hits[v][s] : false; + bool want = st.hits[v][s]; + if (want != have) { + withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s); }); + } + } } } -void DrumSequencerPage::withAudioGuard(const std::function& fn) { - if (audio_guard_) { - audio_guard_(fn); - return; +void DrumSequencerPage::pushUndo() { + DrumPatternState st{}; captureCurrentDrumPattern(st); + undo_stack_.push_back(st); + if (undo_stack_.size() > static_cast(kMaxHistory)) undo_stack_.erase(undo_stack_.begin()); + clearRedo(); +} + +void DrumSequencerPage::clearRedo() { redo_stack_.clear(); } + +bool DrumSequencerPage::undo() { + if (undo_stack_.empty()) return false; + DrumPatternState cur{}; captureCurrentDrumPattern(cur); + redo_stack_.push_back(cur); + DrumPatternState prev = undo_stack_.back(); undo_stack_.pop_back(); + applyDrumPatternState(prev); + return true; +} + +bool DrumSequencerPage::redo() { + if (redo_stack_.empty()) return false; + DrumPatternState cur{}; captureCurrentDrumPattern(cur); + undo_stack_.push_back(cur); + DrumPatternState next = redo_stack_.back(); redo_stack_.pop_back(); + applyDrumPatternState(next); + return true; +} + +// --- pattern operations --- +void DrumSequencerPage::copyCurrentDrumPatternToBuffer() { + DrumPatternState st{}; captureCurrentDrumPattern(st); + for (int v = 0; v < NUM_DRUM_VOICES; ++v) + for (int s = 0; s < SEQ_STEPS; ++s) + buffer_.hits[v][s] = st.hits[v][s]; + buffer_.has_data = true; +} + +void DrumSequencerPage::cutCurrentDrumPatternToBuffer() { + pushUndo(); + copyCurrentDrumPatternToBuffer(); + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + if (hits[v] && hits[v][s]) { + withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s); }); + } + } + } +} + +void DrumSequencerPage::pasteBufferToCurrentDrumPattern() { + if (!buffer_.has_data) return; + pushUndo(); + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + bool have = hits[v] ? hits[v][s] : false; + bool want = buffer_.hits[v][s]; + if (want != have) { + withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s); }); + } + } } - fn(); } +void DrumSequencerPage::transposeDrumInstruments(int dir) { + // Rotate instrument rows: BD->SD->CH->OH->MT->HT->RS->CP + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + pushUndo(); + bool rotated[NUM_DRUM_VOICES][SEQ_STEPS]; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + int src = (v - dir) % NUM_DRUM_VOICES; if (src < 0) src += NUM_DRUM_VOICES; + for (int s = 0; s < SEQ_STEPS; ++s) rotated[v][s] = hits[src] ? hits[src][s] : false; + } + // Apply differences + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + bool have = hits[v] ? hits[v][s] : false; + bool want = rotated[v][s]; + if (want != have) withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s); }); + } + } +} + +void DrumSequencerPage::rotateDrumSteps(int dir) { + // Horizontal rotation of steps per instrument + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + pushUndo(); + bool rotated[NUM_DRUM_VOICES][SEQ_STEPS]; + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + int src = (s - dir) % SEQ_STEPS; if (src < 0) src += SEQ_STEPS; + rotated[v][s] = hits[v] ? hits[v][src] : false; + } + } + // Apply differences + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < SEQ_STEPS; ++s) { + bool have = hits[v] ? hits[v][s] : false; + bool want = rotated[v][s]; + if (want != have) withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s); }); + } + } +} + +void DrumSequencerPage::duplicateTopRowToBottomRow() { + // Copy steps 0..7 -> 8..15 for all voices + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); + const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; + pushUndo(); + for (int v = 0; v < NUM_DRUM_VOICES; ++v) { + for (int s = 0; s < 8; ++s) { + bool want = hits[v] ? hits[v][s] : false; + bool have = hits[v] ? hits[v][s + 8] : false; + if (want != have) withAudioGuard([&]() { mini_acid_.toggleDrumStep(v, s + 8); }); + } + } +} + +// --- event handling --- bool DrumSequencerPage::handleEvent(UIEvent& ui_event) { if (ui_event.event_type != MINIACID_KEY_DOWN) return false; bool handled = false; switch (ui_event.scancode) { - case MINIACID_LEFT: - moveDrumCursor(-1); - handled = true; - break; - case MINIACID_RIGHT: - moveDrumCursor(1); - handled = true; - break; - case MINIACID_UP: - moveDrumCursorVertical(-1); - handled = true; - break; - case MINIACID_DOWN: - moveDrumCursorVertical(1); - handled = true; - break; - default: - break; + case MINIACID_LEFT: moveDrumCursor(-1); handled = true; break; + case MINIACID_RIGHT: moveDrumCursor( 1); handled = true; break; + case MINIACID_UP: moveDrumCursorVertical(-1); handled = true; break; + case MINIACID_DOWN: moveDrumCursorVertical( 1); handled = true; break; + default: break; } - if (handled) return true; char key = ui_event.key; if (!key) return false; - if (key == '\n' || key == '\r') { + // Enter: commit selection (pattern row) or toggle hit (grid) + if ((key == '\n' || key == '\r')) { if (patternRowFocused()) { int cursor = activeDrumPatternCursor(); withAudioGuard([&]() { mini_acid_.setDrumPatternIndex(cursor); }); @@ -175,6 +303,29 @@ bool DrumSequencerPage::handleEvent(UIEvent& ui_event) { return true; } + // --- new keymap (matching synth page semantics) --- + // D: transpose up (rotate instrument rows up) + // C: transpose down (rotate instrument rows down) + // F: rotate steps forward (right) + // V: rotate steps backward (left) + // H: copy pattern + // N: paste pattern + // J: undo + // M: redo + // '\\': cut pattern + // '\'': duplicate 0..7 -> 8..15 + char lowerKey = static_cast(std::tolower(static_cast(key))); + if (lowerKey == 'd') { transposeDrumInstruments(+1); return true; } + if (lowerKey == 'c') { transposeDrumInstruments(-1); return true; } + if (lowerKey == 'f') { rotateDrumSteps(+1); return true; } + if (lowerKey == 'v') { rotateDrumSteps(-1); return true; } + if (lowerKey == 'h') { copyCurrentDrumPatternToBuffer(); return true; } + if (lowerKey == 'n') { pasteBufferToCurrentDrumPattern(); return true; } + if (lowerKey == 'j') { if (undo()) return true; } + if (lowerKey == 'm') { if (redo()) return true; } + if (key == '\\') { cutCurrentDrumPatternToBuffer(); return true; } + if (key == '\'\'') { duplicateTopRowToBottomRow(); return true; } + return false; } @@ -186,11 +337,8 @@ const std::string & DrumSequencerPage::getTitle() const { void DrumSequencerPage::drawHelpBody(IGfx& gfx, int x, int y, int w, int h) { if (w <= 0 || h <= 0) return; switch (help_page_index_) { - case 0: - drawHelpPageDrumPatternEdit(gfx, x, y, w, h); - break; - default: - break; + case 0: drawHelpPageDrumPatternEdit(gfx, x, y, w, h); break; + default: break; } drawHelpScrollbar(gfx, x, y, w, h, help_page_index_, total_help_pages_); } @@ -199,14 +347,9 @@ bool DrumSequencerPage::handleHelpEvent(UIEvent& ui_event) { if (ui_event.event_type != MINIACID_KEY_DOWN) return false; int next = help_page_index_; switch (ui_event.scancode) { - case MINIACID_UP: - next -= 1; - break; - case MINIACID_DOWN: - next += 1; - break; - default: - return false; + case MINIACID_UP: next -= 1; break; + case MINIACID_DOWN: next += 1; break; + default: return false; } if (next < 0) next = 0; if (next >= total_help_pages_) next = total_help_pages_ - 1; @@ -214,33 +357,25 @@ bool DrumSequencerPage::handleHelpEvent(UIEvent& ui_event) { return true; } -bool DrumSequencerPage::hasHelpDialog() { - return true; -} +bool DrumSequencerPage::hasHelpDialog() { return true; } void DrumSequencerPage::draw(IGfx& gfx, int x, int y, int w, int h) { int body_y = y + 2; int body_h = h - 2; if (body_h <= 0) return; - int pattern_label_h = gfx.fontHeight(); gfx.setTextColor(COLOR_LABEL); gfx.drawText(x, body_y, "PATTERN"); gfx.setTextColor(COLOR_WHITE); - int spacing = 4; - int pattern_size = (w - spacing * 7 - 2) / 8; - if (pattern_size < 12) pattern_size = 12; + int pattern_size = (w - spacing * 7 - 2) / 8; if (pattern_size < 12) pattern_size = 12; int pattern_height = pattern_size / 2; int pattern_row_y = body_y + pattern_label_h + 1; - int selectedPattern = mini_acid_.displayDrumPatternIndex(); int patternCursor = activeDrumPatternCursor(); bool songMode = mini_acid_.songModeEnabled(); bool patternFocus = !songMode && patternRowFocused(); if (songMode && selectedPattern >= 0) patternCursor = selectedPattern; - - // pattern boxes for (int i = 0; i < Bank::kPatterns; ++i) { int col = i % 8; int cell_x = x + col * (pattern_size + spacing); @@ -254,11 +389,8 @@ void DrumSequencerPage::draw(IGfx& gfx, int x, int y, int w, int h) { gfx.drawRect(cell_x - 1, pattern_row_y - 1, pattern_size + 2, pattern_height + 2, border); } gfx.drawRect(cell_x, pattern_row_y, pattern_size, pattern_height, songMode ? COLOR_LABEL : COLOR_WHITE); - if (isCursor) { - gfx.drawRect(cell_x - 2, pattern_row_y - 2, pattern_size + 4, pattern_height + 4, COLOR_STEP_SELECTED); - } - char label[4]; - snprintf(label, sizeof(label), "%d", i + 1); + if (isCursor) gfx.drawRect(cell_x - 2, pattern_row_y - 2, pattern_size + 4, pattern_height + 4, COLOR_STEP_SELECTED); + char label[4]; std::snprintf(label, sizeof(label), "%d", i + 1); int tw = textWidth(gfx, label); int tx = cell_x + (pattern_size - tw) / 2; int ty = pattern_row_y + pattern_height / 2 - gfx.fontHeight() / 2; @@ -266,68 +398,41 @@ void DrumSequencerPage::draw(IGfx& gfx, int x, int y, int w, int h) { gfx.drawText(tx, ty, label); gfx.setTextColor(COLOR_WHITE); } - - // labels for voices int grid_top = pattern_row_y + pattern_height + 6; - int grid_h = body_h - (grid_top - body_y); - if (grid_h <= 0) return; - + int grid_h = body_h - (grid_top - body_y); if (grid_h <= 0) return; int label_w = 18; int grid_x = x + label_w; - int grid_w = w - label_w; - if (grid_w < 8) grid_w = 8; - + int grid_w = w - label_w; if (grid_w < 8) grid_w = 8; const char* voiceLabels[NUM_DRUM_VOICES] = {"BD", "SD", "CH", "OH", "MT", "HT", "RS", "CP"}; - int labelStripeH = grid_h / NUM_DRUM_VOICES; - if (labelStripeH < 3) labelStripeH = 3; + int labelStripeH = grid_h / NUM_DRUM_VOICES; if (labelStripeH < 3) labelStripeH = 3; for (int v = 0; v < NUM_DRUM_VOICES; ++v) { int ly = grid_top + v * labelStripeH + (labelStripeH - gfx.fontHeight()) / 2; gfx.setTextColor(COLOR_LABEL); gfx.drawText(x, ly, voiceLabels[v]); } gfx.setTextColor(COLOR_WHITE); - int cursorStep = activeDrumStep(); int cursorVoice = activeDrumVoice(); bool gridFocus = !patternFocus; - - const int cell_w = grid_w / SEQ_STEPS; - if (cell_w < 2) return; - - const bool* kick = mini_acid_.patternKickSteps(); - const bool* snare = mini_acid_.patternSnareSteps(); - const bool* hat = mini_acid_.patternHatSteps(); - const bool* openHat = mini_acid_.patternOpenHatSteps(); - const bool* midTom = mini_acid_.patternMidTomSteps(); - const bool* highTom = mini_acid_.patternHighTomSteps(); - const bool* rim = mini_acid_.patternRimSteps(); - const bool* clap = mini_acid_.patternClapSteps(); + const int cell_w = grid_w / SEQ_STEPS; if (cell_w < 2) return; + const bool *kick, *snare, *hat, *openHat, *midTom, *highTom, *rim, *clap; + fetchHits(mini_acid_, kick, snare, hat, openHat, midTom, highTom, rim, clap); int highlight = mini_acid_.currentStep(); - const bool* hits[NUM_DRUM_VOICES] = {kick, snare, hat, openHat, midTom, highTom, rim, clap}; const IGfxColor colors[NUM_DRUM_VOICES] = {COLOR_DRUM_KICK, COLOR_DRUM_SNARE, COLOR_DRUM_HAT, COLOR_DRUM_OPEN_HAT, COLOR_DRUM_MID_TOM, COLOR_DRUM_HIGH_TOM, COLOR_DRUM_RIM, COLOR_DRUM_CLAP}; - - int stripe_h = grid_h / NUM_DRUM_VOICES; - if (stripe_h < 3) stripe_h = 3; - - // grid cells + int stripe_h = grid_h / NUM_DRUM_VOICES; if (stripe_h < 3) stripe_h = 3; for (int i = 0; i < SEQ_STEPS; ++i) { int cw = cell_w; - int ch = stripe_h; - if (ch < 3) ch = 3; + int ch = stripe_h; if (ch < 3) ch = 3; int cx = grid_x + i * cw; for (int v = 0; v < NUM_DRUM_VOICES; ++v) { int cy = grid_top + v * stripe_h; - bool hit = hits[v][i]; + bool hit = hits[v] ? hits[v][i] : false; IGfxColor fill = hit ? colors[v] : COLOR_GRAY; gfx.fillRect(cx, cy, cw - 1, ch - 1, fill); - if (highlight == i) { - gfx.drawRect(cx - 1, cy - 1, cw + 1, ch + 1, COLOR_STEP_HILIGHT); - } - if (gridFocus && i == cursorStep && v == cursorVoice) { - gfx.drawRect(cx, cy, cw - 1, ch - 1, COLOR_STEP_SELECTED); - } + if (highlight == i) gfx.drawRect(cx - 1, cy - 1, cw + 1, ch + 1, COLOR_STEP_HILIGHT); + if (gridFocus && i == cursorStep && v == cursorVoice) gfx.drawRect(cx, cy, cw - 1, ch - 1, COLOR_STEP_SELECTED); } } } diff --git a/src/ui/pages/drum_sequencer_page.h b/src/ui/pages/drum_sequencer_page.h index e78bb09..b42548d 100644 --- a/src/ui/pages/drum_sequencer_page.h +++ b/src/ui/pages/drum_sequencer_page.h @@ -1,5 +1,6 @@ #pragma once - +#include +#include #include "../ui_core.h" #include "../ui_colors.h" #include "../ui_utils.h" @@ -27,6 +28,26 @@ class DrumSequencerPage : public IPage { int patternIndexFromKey(char key) const; void withAudioGuard(const std::function& fn); + // pattern ops + void copyCurrentDrumPatternToBuffer(); + void cutCurrentDrumPatternToBuffer(); + void pasteBufferToCurrentDrumPattern(); + void transposeDrumInstruments(int dir); // dir = +1 up, -1 down + void rotateDrumSteps(int dir); // dir = +1 forward (right), -1 backward (left) + void duplicateTopRowToBottomRow(); // steps 0..7 -> 8..15 for all voices + + // undo/redo + struct DrumPatternState { + bool hits[NUM_DRUM_VOICES][SEQ_STEPS]; + }; + static constexpr int kMaxHistory = 64; + void captureCurrentDrumPattern(DrumPatternState& out) const; + void applyDrumPatternState(const DrumPatternState& st); + void pushUndo(); + void clearRedo(); + bool undo(); + bool redo(); + IGfx& gfx_; MiniAcid& mini_acid_; AudioGuard& audio_guard_; @@ -36,4 +57,14 @@ class DrumSequencerPage : public IPage { bool drum_pattern_focus_; int help_page_index_ = 0; int total_help_pages_ = 1; + + // buffer + struct DrumBuffer { + bool has_data = false; + bool hits[NUM_DRUM_VOICES][SEQ_STEPS] = { {false} }; + } buffer_; + + // undo/redo stacks + std::vector undo_stack_; + std::vector redo_stack_; }; From 33ed6f15f84a3979415ec81af9042b39b857c705 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Fri, 9 Jan 2026 18:10:31 -0600 Subject: [PATCH 19/20] drum seq features - fixed ' key for coping steps 0-7 to 8-16 --- src/ui/pages/drum_sequencer_page.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/pages/drum_sequencer_page.cpp b/src/ui/pages/drum_sequencer_page.cpp index c508fb9..f459743 100644 --- a/src/ui/pages/drum_sequencer_page.cpp +++ b/src/ui/pages/drum_sequencer_page.cpp @@ -324,7 +324,7 @@ bool DrumSequencerPage::handleEvent(UIEvent& ui_event) { if (lowerKey == 'j') { if (undo()) return true; } if (lowerKey == 'm') { if (redo()) return true; } if (key == '\\') { cutCurrentDrumPatternToBuffer(); return true; } - if (key == '\'\'') { duplicateTopRowToBottomRow(); return true; } + if (key == '\'') { duplicateTopRowToBottomRow(); return true; } return false; } From a6c1b842f794aac67a3810726a8c71873abd0248 Mon Sep 17 00:00:00 2001 From: Aaron Reichow Date: Tue, 13 Jan 2026 13:53:07 -0600 Subject: [PATCH 20/20] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c26ee2..5130da6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -# fork: ajrAcid +# DEPRECATED - please see [ajrAcid](https://github.com/areichow/ajrAcid/) for further developments! -This is a personal fork of MiniAcid, a tiny acid groovebox for the M5Stack Cardputer. I've forked it to make some changes and make this awesome little app more to my liking. A huge THANK YOU to [urtubia](https://github.com/urtubia/) for developing this and sharing it with the world! The UI is excellent, it's well thought out. +I'm archiving this for project for now - pleaes see ajrAcid for my current work. I'm new to using git and made a lot of n00b mistakes when putting making my changes. I've created a new project on github - ajrAcid - that is better set up for submitting patches to the [original project](https://github.com/urtubia/miniacid). My changes are now derived from miniacid 0.0.7-dev. urtubia has a lot of cool changes in the works, and many of my changes became moot. + +[ajrAcid](https://github.com/areichow/ajrAcid/) is incorporating many of my previous changes, and some new ones as well. I'll be uploading .bin files that are ready to be used on the CardPuter ADV as well! + +--- DONE: - Simple bus compressor for the drums to increase punch