diff --git a/README.md b/README.md index 7a2c07c..5130da6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ +# DEPRECATED - please see [ajrAcid](https://github.com/areichow/ajrAcid/) for further developments! + +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 +- 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. diff --git a/src/dsp/mini_drumvoices.cpp b/src/dsp/mini_drumvoices.cpp index 6b46e6e..69fb8f5 100644 --- a/src/dsp/mini_drumvoices.cpp +++ b/src/dsp/mini_drumvoices.cpp @@ -1,84 +1,145 @@ #include "mini_drumvoices.h" - #include -#include + +static inline float fast_tanhf(float x) { + 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) { + : 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(); } void DrumSynthVoice::reset() { - kickPhase = 0.0f; - kickFreq = 60.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; - snareEnvAmp = 0.0f; - snareToneEnv = 0.0f; - snareActive = false; - snareBp = 0.0f; - snareLp = 0.0f; - 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; - - params[static_cast(DrumParamId::MainVolume)] = Parameter("vol", "", 0.0f, 1.0f, 0.8f, 1.0f / 128); + // 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; + hatHp = 0.0f; hatPrev = 0.0f; + for (int i = 0; i < 6; ++i) hatPh[i] = 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; midTomPitchEnv = 0.0f; midTomActive = false; + highTomPhase = 0.0f; highTomEnv = 0.0f; highTomPitchEnv = 0.0f; highTomActive = false; + + // Rim + rimPhase = 0.0f; rimEnv = 0.0f; rimBp = 0.0f; rimLp = 0.0f; rimActive = false; + + // 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; 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; + + // 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; + compDecimCounter = 0; + compLastGainAmp = 1.0f; + + // Params + 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) { 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; + } + + // 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 + 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; + + // 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)); +} + +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; } void DrumSynthVoice::triggerKick() { kickActive = true; kickPhase = 0.0f; - kickEnvAmp = 1.2f; + kickEnvAmp = 1.15f; kickEnvPitch = 1.0f; - kickFreq = 55.0f; + kickClickEnv = 1.0f; + kickFreq = 60.0f; } void DrumSynthVoice::triggerSnare() { snareActive = true; - snareEnvAmp = 1.1f; + snareEnvAmp = 1.1f; snareToneEnv = 1.0f; snareTonePhase = 0.0f; snareTonePhase2 = 0.0f; @@ -86,263 +147,361 @@ void DrumSynthVoice::triggerSnare() { void DrumSynthVoice::triggerHat() { hatActive = true; - hatEnvAmp = 0.7f; + hatEnvAmp = 0.85f; hatToneEnv = 1.0f; - hatPhaseA = 0.0f; - hatPhaseB = 0.25f; - // closing the hat chokes any ringing open-hat tail - openHatEnvAmp *= 0.3f; + openHatEnvAmp *= 0.25f; // choke + for (int i = 0; i < 6; ++i) hatPh[i] = 0.0f; } void DrumSynthVoice::triggerOpenHat() { openHatActive = true; - openHatEnvAmp = 0.9f; + openHatEnvAmp = 0.95f; openHatToneEnv = 1.0f; - openHatPhaseA = 0.0f; - openHatPhaseB = 0.37f; + for (int i = 0; i < 6; ++i) openHatPh[i] = 0.0f; } void DrumSynthVoice::triggerMidTom() { midTomActive = true; - midTomEnv = 1.0f; - midTomPhase = 0.0f; + midTomEnv = 1.0f; + midTomPitchEnv = 1.0f; + midTomPhase = 0.0f; } void DrumSynthVoice::triggerHighTom() { highTomActive = true; - highTomEnv = 1.0f; - highTomPhase = 0.0f; + highTomEnv = 1.0f; + highTomPitchEnv = 1.0f; + highTomPhase = 0.0f; } void DrumSynthVoice::triggerRim() { rimActive = true; - rimEnv = 1.0f; + 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; -} - -float DrumSynthVoice::frand() { - return (float)rand() / (float)RAND_MAX * 2.0f - 1.0f; + clapActive = true; + clapEnv = 1.0f; // global body envelope + clapTrans = 1.0f; // transient + clapTailEnv = 0.95f; // tail/body + clapNoiseSeed = frand(); + clapTime = 0.0f; + + // shaper states + clapHp = 0.0f; clapPrev = 0.0f; clapAirLp = 0.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; + for (int i = 0; i < clapTapLen; ++i) clapTapBuf[i] = 0.0f; } 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; - } + if (!kickActive) return 0.0f; + + kickEnvAmp *= 0.9965f; + kickEnvPitch *= 0.985f; + kickClickEnv *= 0.92f; - float pitchFactor = kickEnvPitch * kickEnvPitch; - float f = 42.0f + 170.0f * pitchFactor; + if (kickEnvAmp < 0.0006f) { kickActive = false; return 0.0f; } + + float p = kickEnvPitch * kickEnvPitch; + float f = 48.0f + 120.0f * p; kickFreq = f; + kickPhase += kickFreq * invSampleRate; - if (kickPhase >= 1.0f) - kickPhase -= 1.0f; + if (kickPhase >= 1.0f) kickPhase -= 1.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)); + float driven = fast_tanhf(body * (2.6f + 0.7f * kickEnvAmp)); + float click = (frand() * 0.5f + 0.5f) * kickClickEnv * 0.3f; - return (driven * 0.85f + transient) * kickEnvAmp; + return (driven * 0.9f + click) * kickEnvAmp; } - 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; - } - - // --- NOISE PROCESSING --- - float n = frand(); // assume 0.0–1.0 random + snareEnvAmp *= 0.9985f; + snareToneEnv *= 0.99999f; + if (snareEnvAmp < 0.0002f) { snareActive = false; return 0.0f; } - // 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; - + 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; } 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.996f; + hatToneEnv *= 0.90f; + if (hatEnvAmp < 0.0005f) { hatActive = false; return 0.0f; } + + 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); } + metal = (metal / 6.0f) * hatToneEnv; - 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; + float n = frand() * 0.6f; + + const float alpha = 0.93f; + hatHp = alpha * (hatHp + n + metal - hatPrev); + hatPrev = n + metal; + + float out = hatHp * 0.8f + metal * 0.35f; + return out * hatEnvAmp * 0.75f; } float DrumSynthVoice::processOpenHat() { - if (!openHatActive) - return 0.0f; + if (!openHatActive) return 0.0f; - openHatEnvAmp *= 0.9993f; + openHatEnvAmp *= 0.9988f; openHatToneEnv *= 0.94f; - if (openHatEnvAmp < 0.0004f) { - openHatActive = false; - return 0.0f; + if (openHatEnvAmp < 0.0004f) { openHatActive = false; return 0.0f; } + + float metal = 0.0f; + for (int i = 0; i < 6; ++i) { + openHatPh[i] += openHatInc[i]; + if (openHatPh[i] >= 1.0f) openHatPh[i] -= 1.0f; + metal += (openHatPh[i] < 0.5f ? 1.0f : -1.0f); } + metal = (metal / 6.0f) * openHatToneEnv; - 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 n = frand() * 0.5f; + + const float alpha = 0.94f; + openHatHp = alpha * (openHatHp + n + metal - openHatPrev); + openHatPrev = n + metal; + + float out = openHatHp * 0.65f + metal * 0.55f; + return out * openHatEnvAmp * 0.8f; } 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; - } + midTomEnv *= 0.9991f; + midTomPitchEnv *= 0.9975f; + 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; + 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; + 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; - if (highTomEnv < 0.0003f) { - highTomActive = false; - return 0.0f; - } + if (!highTomActive) return 0.0f; - float freq = 240.0f; + highTomEnv *= 0.9990f; + highTomPitchEnv *= 0.997f; + if (highTomEnv < 0.0003f) { highTomActive = false; return 0.0f; } - highTomPhase += freq * invSampleRate; - if (highTomPhase >= 1.0f) - highTomPhase -= 1.0f; + float base = 230.0f; + 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; } 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; - } + rimEnv *= 0.9978f; + if (rimEnv < 0.0006f) { rimActive = false; return 0.0f; } + + rimPhase += 1400.0f * invSampleRate; if (rimPhase >= 1.0f) rimPhase -= 1.0f; + float tick = sinf(2.0f * 3.14159265f * rimPhase) * 0.6f; - 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; + 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; } 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; - } + if (!clapActive) return 0.0f; + + // envelopes + clapEnv *= 0.99993f; // overall tail (slow) + clapTrans *= 0.995f; // transient (fast) + clapTailEnv *= 0.99990f; // tail/body (medium) + + // snaps & crack decay quickly (give “hand” crack, but die fast) + clapSnapEnv1 *= 0.92f; + clapSnapEnv2 *= 0.90f; + clapSnapEnv3 *= 0.88f; + clapCrackEnv *= 0.90f; + + 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)); + + // base noise + float w = (frand() * 0.55f + clapNoiseSeed * 0.45f); + + // 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; + + 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; + + // 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 + 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; + + 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; + + // extra transient crack (fast, only at burst peaks) + float crack = (bandInput - bandNarrow) * 0.40f * clapCrackEnv; + + // 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 + 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.55f + + clapTapBuf[i2] * 0.40f + + clapTapBuf[i3] * 0.28f + + clapTapBuf[i4] * 0.20f + + clapTapBuf[i5] * 0.13f + + clapTapBuf[i6] * 0.09f + + tail; + + clapTapIdx++; if (clapTapIdx >= clapTapLen) clapTapIdx = 0; + + return y * clapEnv; +} - // 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; +// Bus Compressor +float DrumSynthVoice::processBus(float mixSample) { + 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); + } - 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; + compDecimCounter = (compDecimCounter + 1) % kCompDecim; + return mixSample * compLastGainAmp; } const Parameter& DrumSynthVoice::parameter(DrumParamId id) const { @@ -351,5 +510,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 531ee96..ca25f07 100644 --- a/src/dsp/mini_drumvoices.h +++ b/src/dsp/mini_drumvoices.h @@ -1,20 +1,21 @@ -#pragma once +#pragma once #include - #include "mini_dsp_params.h" enum class DrumParamId : uint8_t { MainVolume = 0, + BusCompAmount, // 0..1 Count }; class DrumSynthVoice { public: explicit DrumSynthVoice(float sampleRate); - void reset(); void setSampleRate(float sampleRate); + + // Triggers void triggerKick(); void triggerSnare(); void triggerHat(); @@ -24,72 +25,100 @@ class DrumSynthVoice { void triggerRim(); void triggerClap(); + // 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 + // Bus processing + float processBus(float mixSample); + + // Snare + float snareHpPrev; // extra high-pass memory + + // Parameters - for later const Parameter& parameter(DrumParamId id) const; void setParameter(DrumParamId id, float value); private: + // Fast RNG [-1, 1] float frand(); - - float kickPhase; - float kickFreq; - float kickEnvAmp; - float kickEnvPitch; - bool kickActive; - - float snareEnvAmp; - float snareToneEnv; - bool snareActive; - float snareBp; - float snareLp; - float snareTonePhase; - float snareTonePhase2; - - float hatEnvAmp; - float hatToneEnv; - bool hatActive; - float hatHp; - float hatPrev; - float hatPhaseA; - float hatPhaseB; - - float openHatEnvAmp; - float openHatToneEnv; - bool openHatActive; - float openHatHp; - float openHatPrev; - float openHatPhaseA; - float openHatPhaseB; - - float midTomPhase; - float midTomEnv; - bool midTomActive; - - float highTomPhase; - float highTomEnv; - bool highTomActive; - - float rimPhase; - float rimEnv; - bool rimActive; - - float clapEnv; - float clapTrans; - float clapNoise; - bool clapActive; - float clapDelay; - - float sampleRate; - float invSampleRate; - + uint32_t rngState; + + // Kick (606-tight) + float kickPhase, kickFreq, kickEnvAmp, kickEnvPitch, kickClickEnv; + bool kickActive; + + // Snare + float snareEnvAmp, snareToneEnv; + bool snareActive; + float snareBp, snareLp, snareTonePhase, snareTonePhase2; + + // Closed Hat (metallic) + float hatEnvAmp, hatToneEnv; + bool hatActive; + float hatHp, hatPrev; + float hatPh[6], hatInc[6]; + + // Open Hat + float openHatEnvAmp, openHatToneEnv; + bool openHatActive; + float openHatHp, openHatPrev; + float openHatPh[6], openHatInc[6]; + + // Toms + float midTomPhase, midTomEnv, midTomPitchEnv; + bool midTomActive; + + float highTomPhase, highTomEnv, highTomPitchEnv; + bool highTomActive; + + // Rimshot + float rimPhase, rimEnv, rimBp, rimLp; + bool rimActive; + + // Clap (hollow, multi-hand) + float clapEnv; // overall body envelope (slow) + float clapTrans; // transient envelope (fast) + float clapTailEnv; // tail/body envelope (medium) + float clapNoiseSeed; // per-hit color + bool clapActive; + float clapTime; // seconds since trigger + + // 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 snaps (very short) + extra crack envelope + float clapSnapPhase1, clapSnapPhase2, clapSnapPhase3; + float clapSnapEnv1, clapSnapEnv2, clapSnapEnv3; + float clapCrackEnv; + + // 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, clapD5, clapD6; // sample delays + int clapTapLen; // ring size (<= kClapTapBufMax) + + // Sample rate + float sampleRate, invSampleRate; + + // Bus Compressor + float compEnv, compAttackCoeff, compReleaseCoeff; + float compGainDb, compMakeupDb, compThreshDb, compRatio, compKneeDb, compAmount; + 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/mini_tb303.cpp b/src/dsp/mini_tb303.cpp index ce48bb0..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.05f, 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); diff --git a/src/dsp/miniacid_engine.cpp b/src/dsp/miniacid_engine.cpp index f862a3b..0f232d1 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,34 @@ 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; - } - // Soft clipping/limiting - sample *= 0.65f; - if (sample > 1.0f) - sample = 1.0f; - if (sample < -1.0f) - sample = -1.0f; + 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 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); + + sampleOut = drumSum + sample303; + + // uncomment to use bus comp on the whole mix + // sampleOut = drums.processBus(sampleOut); + } + // soft clipping/limiting + 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; 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; diff --git a/src/ui/pages/drum_sequencer_page.cpp b/src/ui/pages/drum_sequencer_page.cpp index b5d750c..f459743 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_; }; diff --git a/src/ui/pages/pattern_edit_page.cpp b/src/ui/pages/pattern_edit_page.cpp index 9e2f30f..b16fcfe 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,265 @@ 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; +} + +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() { + pushUndo(); + copyCurrentPatternToBuffer(); + 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; + 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) { + 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; + pushUndo(); + for (int i = 0; i < SEQ_STEPS; ++i) { + int n = currentStepNote(i); + if (n >= 0) { + withAudioGuard([&]() { mini_acid_.adjust303StepNote(voice_index_, i, 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; + + pushUndo(); + + 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; + + pushUndo(); + + 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); }); + } +} + +// --- 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; + bool handled = false; switch (ui_event.scancode) { case MINIACID_LEFT: @@ -207,6 +463,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 +475,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(); @@ -238,51 +495,115 @@ bool PatternEditPage::handleEvent(UIEvent& ui_event) { }; char lowerKey = static_cast(std::tolower(static_cast(key))); + 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; } + + // per-step edits (unchanged) switch (lowerKey) { - case 'q': { + case 'q': { // slide toggle ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303SlideStep(voice_index_, step); }); return true; } - case 'w': { + case 'w': { // accent toggle ensureStepFocusAndCursor(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.toggle303AccentStep(voice_index_, step); }); return true; } - case 'a': { + case 'a': { // note + (with last-note on empty) ensureStepFocusAndCursor(); + pushUndo(); 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(); + pushUndo(); 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(); + pushUndo(); 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(); + pushUndo(); 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(); + pushUndo(); int step = activePatternStep(); withAudioGuard([&]() { mini_acid_.clear303StepNote(voice_index_, step); }); + // keep last_entered_note_ unchanged return true; } @@ -290,7 +611,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 +618,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 +655,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 +670,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..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 +#include #include "../ui_core.h" #include "../ui_colors.h" #include "../ui_utils.h" @@ -27,11 +28,45 @@ 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); + + // rotation + duplication + 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_; @@ -42,4 +77,18 @@ 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_; + + // undo/redo stacks + std::vector undo_stack_; + std::vector redo_stack_; }; 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);