diff --git a/src/NoisePlethora.cpp b/src/NoisePlethora.cpp index 4bc9f06..d31ff14 100644 --- a/src/NoisePlethora.cpp +++ b/src/NoisePlethora.cpp @@ -172,6 +172,12 @@ struct NoisePlethora : Module { ProgramSelector programSelector; // tracks banks and programs for both sections A/B, including which is the "active" section ProgramSelector programSelectorWithCV; // as above, but also with CV for program applied as an offset - works like Plaits Model CV input + + // Stereo mode: B mirrors A's algorithm, XB/YB become stereo pan width/speed + bool stereoMode = false; + float stereoLFOPhase = 0.f; + float stereoGainL = 1.f; + float stereoGainR = 1.f; // UI / UX for A/B std::string textDisplayA = " ", textDisplayB = " "; bool isDisplayActiveA = false, isDisplayActiveB = false; @@ -274,6 +280,31 @@ struct NoisePlethora : Module { updateParamsTimer.trigger(updateTimeSecs); } + // Stereo mode: sync B's program to A and update stereo LFO + if (stereoMode && updateParams) { + // In stereo mode A is the master: ensure the program knob always edits A, + // even if mode was flipped to B by a context-menu selection + programSelector.setMode(SECTION_A); + + // Sync B to A's algorithm + std::string_view aName = programSelectorWithCV.getA().getCurrentProgramName(); + if (aName != algorithmName[SECTION_B]) { + // Copy A's bank/program to B + programSelector.getB().setBank(programSelector.getA().getBank()); + programSelector.getB().setProgram(programSelector.getA().getProgram()); + } + + // Update stereo LFO: XB = pan width, YB = pan speed + // Real L/R panning via amplitude modulation + float width = params[X_B_PARAM].getValue(); + float speed = params[Y_B_PARAM].getValue(); + stereoLFOPhase += speed * speed * 1.5f * updateTimeSecs; // quadratic: max 1.5 Hz + if (stereoLFOPhase > 1.f) stereoLFOPhase -= 1.f; + float pan = width * std::sin(2.f * M_PI * stereoLFOPhase); + stereoGainL = clamp(1.f - pan, 0.f, 2.f) * 0.5f; + stereoGainR = clamp(1.f + pan, 0.f, 2.f) * 0.5f; + } + // process A, B and C processTopSection(SECTION_A, X_A_PARAM, Y_A_PARAM, FILTER_TYPE_A_PARAM, CUTOFF_A_PARAM, CUTOFF_CV_A_PARAM, RES_A_PARAM, @@ -335,8 +366,17 @@ struct NoisePlethora : Module { float out = 0.f; if (algorithm[SECTION] && outputs[OUTPUT].isConnected()) { - float cvX = params[X_PARAM].getValue() + rescale(inputs[X_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); - float cvY = params[Y_PARAM].getValue() + rescale(inputs[Y_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); + float cvX, cvY; + + if (stereoMode && SECTION == SECTION_B) { + // Stereo mode: B reads A's params (same pitch and timbre) + cvX = params[X_A_PARAM].getValue() + rescale(inputs[X_A_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); + cvY = params[Y_A_PARAM].getValue() + rescale(inputs[Y_A_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); + } + else { + cvX = params[X_PARAM].getValue() + rescale(inputs[X_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); + cvY = params[Y_PARAM].getValue() + rescale(inputs[Y_INPUT].getVoltage(), -10.f, +10.f, -1.f, 1.f); + } // update parameters of the algorithm if (updateParams) { @@ -371,7 +411,12 @@ struct NoisePlethora : Module { } } - outputs[OUTPUT].setVoltage(Saturator::process(out) * 5.f); + // Apply stereo panning gain + float stereoGain = 1.f; + if (stereoMode) { + stereoGain = (SECTION == SECTION_A) ? stereoGainL : stereoGainR; + } + outputs[OUTPUT].setVoltage(Saturator::process(out) * 5.f * stereoGain); } // process section C @@ -425,15 +470,19 @@ struct NoisePlethora : Module { else if (programKnobMode == BANK_MODE) { textDisplayA = 'A' + programSelectorWithCV.getA().getBank(); } - isDisplayActiveA = programSelectorWithCV.getMode() == SECTION_A; + isDisplayActiveA = stereoMode || (programSelectorWithCV.getMode() == SECTION_A); - if (programKnobMode == PROGRAM_MODE) { + if (stereoMode) { + // In stereo mode, B shows same as A + textDisplayB = textDisplayA; + } + else if (programKnobMode == PROGRAM_MODE) { textDisplayB = std::to_string(programSelectorWithCV.getB().getProgram()); } else if (programKnobMode == BANK_MODE) { textDisplayB = 'A' + programSelectorWithCV.getB().getBank(); } - isDisplayActiveB = programSelectorWithCV.getMode() == SECTION_B; + isDisplayActiveB = stereoMode || (programSelectorWithCV.getMode() == SECTION_B); } // handle convoluted logic for the multifunction Program knob @@ -567,6 +616,11 @@ struct NoisePlethora : Module { if (blockDCJ) { blockDC = json_boolean_value(blockDCJ); } + + json_t* stereoModeJ = json_object_get(rootJ, "stereoMode"); + if (stereoModeJ) { + stereoMode = json_boolean_value(stereoModeJ); + } } json_t* dataToJson() override { @@ -577,6 +631,7 @@ struct NoisePlethora : Module { json_object_set_new(rootJ, "bypassFilters", json_boolean(bypassFilters)); json_object_set_new(rootJ, "blockDC", json_boolean(blockDC)); + json_object_set_new(rootJ, "stereoMode", json_boolean(stereoMode)); return rootJ; } @@ -833,7 +888,7 @@ struct NoisePlethoraWidget : ModuleWidget { // build the two algorithm selection menus programmatically menu->addChild(createMenuLabel("Algorithms")); - std::vector bankAliases = {"Textures", "HH Clusters", "Harsh & Wild", "Test"}; + std::vector bankAliases = {"Textures", "HH Clusters", "Harsh & Wild", "Resonant Bodies", "Chaos Machines", "Stochastic"}; char programNames[] = "AB"; for (int sectionId = 0; sectionId < 2; ++sectionId) { @@ -859,7 +914,10 @@ struct NoisePlethoraWidget : ModuleWidget { if (implemented) { menu->addChild(createMenuItem(algorithmName.data(), currentProgramAndBank ? CHECKMARK_STRING : "", [ = ]() { - module->setAlgorithm(sectionId, algorithmName); + // In stereo mode A is the master and B mirrors it, so route + // Program B picks to A to avoid silently-overridden selections + const int targetSection = module->stereoMode ? 0 : sectionId; + module->setAlgorithm(targetSection, algorithmName); })); } else { @@ -874,6 +932,9 @@ struct NoisePlethoraWidget : ModuleWidget { } + menu->addChild(createMenuLabel("Stereo")); + menu->addChild(createBoolPtrMenuItem("Stereo Mode (B mirrors A, XB=pan width, YB=pan speed)", "", &module->stereoMode)); + menu->addChild(createMenuLabel("Filters")); menu->addChild(createBoolPtrMenuItem("Remove DC", "", &module->blockDC)); menu->addChild(createBoolPtrMenuItem("Bypass Filters", "", &module->bypassFilters)); diff --git a/src/noise-plethora/plugins/Banks.cpp b/src/noise-plethora/plugins/Banks.cpp index 70558b6..9160144 100644 --- a/src/noise-plethora/plugins/Banks.cpp +++ b/src/noise-plethora/plugins/Banks.cpp @@ -77,36 +77,55 @@ int Bank::getSize() { #include "P_Rwalk_BitCrushPW.hpp" #include "P_Rwalk_LFree.hpp" -// Bank D: Test / other -//#include "P_TestPlugin.hpp" -//#include "P_TeensyAlt.hpp" -//#include "P_WhiteNoise.hpp" -//#include "P_Rwalk_LBit.hpp" -//#include "P_Rwalk_SineFM.hpp" -//#include "P_VarWave.hpp" -//#include "P_RwalkVarWave.hpp" -//#include "P_Rwalk_ModWave.hpp" -//#include "P_Rwalk_WaveTwist.hpp" +// Bank D: Resonant Bodies +#include "P_CombNoise.hpp" +#include "P_PluckCloud.hpp" +#include "P_TubeResonance.hpp" +#include "P_FormantNoise.hpp" +#include "P_BowedMetal.hpp" +#include "P_FlangeNoise.hpp" +#include "P_NoiseBells.hpp" +#include "P_NoiseHarmonics.hpp" +#include "P_IceRain.hpp" +#include "P_DroneBody.hpp" + +// Bank E: Chaos Machines +#include "P_LogisticNoise.hpp" +#include "P_HenonDust.hpp" +#include "P_CellularSynth.hpp" +#include "P_StochasticPulse.hpp" +#include "P_BitShift.hpp" +#include "P_FeedbackFM.hpp" +#include "P_RunawayFilter.hpp" +#include "P_GlitchLoop.hpp" +#include "P_SubHarmonic.hpp" +#include "P_DualAttractor.hpp" + +// Bank F: Stochastic +#include "P_PulseWander.hpp" +#include "P_TwinPulse.hpp" +#include "P_QuantPulse.hpp" +#include "P_ShapedPulse.hpp" +#include "P_ShiftPulse.hpp" +#include "P_MetallicNoise.hpp" +#include "P_NoiseSlew.hpp" +#include "P_NoiseBurst.hpp" +#include "P_LFSRNoise.hpp" +#include "P_DualPulse.hpp" static const Bank bank1 BANKS_DEF_1; // Banks_Def.hpp static const Bank bank2 BANKS_DEF_2; static const Bank bank3 BANKS_DEF_3; -//static const Bank bank4 BANKS_DEF_4; -//static const Bank bank5 BANKS_DEF_5; -static std::array banks { bank1, bank2, bank3 }; //, bank5 }; - -// static const Bank bank6 BANKS_DEF_6; -// static const Bank bank7 BANKS_DEF_7; -// static const Bank bank8 BANKS_DEF_8; -// static const Bank bank9 BANKS_DEF_9; -// static const Bank bank10 BANKS_DEF_10; -// static std::array banks { bank1, bank2, bank3, bank4, bank5, bank6, bank7, bank8, bank9, bank10 }; +static const Bank bank4 BANKS_DEF_4; +static const Bank bank5 BANKS_DEF_5; +static const Bank bank6 BANKS_DEF_6; +static std::array banks { bank1, bank2, bank3, bank4, bank5, bank6 }; Bank& getBankForIndex(int i) { if (i < 0) i = 0; - if (i >= programsPerBank) - i = (programsPerBank - 1); + if (i >= numBanks) + i = (numBanks - 1); return banks[i]; } diff --git a/src/noise-plethora/plugins/Banks.hpp b/src/noise-plethora/plugins/Banks.hpp index c7a703b..4dfea6e 100644 --- a/src/noise-plethora/plugins/Banks.hpp +++ b/src/noise-plethora/plugins/Banks.hpp @@ -6,7 +6,7 @@ #include static const int programsPerBank = 10; -static const int numBanks = 3; +static const int numBanks = 6; struct Bank { diff --git a/src/noise-plethora/plugins/Banks_Def.hpp b/src/noise-plethora/plugins/Banks_Def.hpp index 41c1f63..0c91c7f 100644 --- a/src/noise-plethora/plugins/Banks_Def.hpp +++ b/src/noise-plethora/plugins/Banks_Def.hpp @@ -40,13 +40,44 @@ } #define BANKS_DEF_4 { \ - { "TestPlugin", 1.0 }, \ - { "WhiteNoise", 1.0 }, \ - { "TeensyAlt", 1.0 } \ + { "CombNoise", 1.0 }, \ + { "PluckCloud", 1.0 }, \ + { "TubeResonance", 1.0 }, \ + { "FormantNoise", 1.0 }, \ + { "BowedMetal", 1.0 }, \ + { "FlangeNoise", 1.0 }, \ + { "NoiseBells", 0.8 }, \ + { "NoiseHarmonics", 1.0 }, \ + { "IceRain", 1.0 }, \ + { "DroneBody", 1.0 } \ + } + +#define BANKS_DEF_5 { \ + { "LogisticNoise", 1.0 }, \ + { "HenonDust", 1.0 }, \ + { "CellularSynth", 1.0 }, \ + { "StochasticPulse", 1.0 }, \ + { "BitShift", 1.0 }, \ + { "FeedbackFM", 1.0 }, \ + { "RunawayFilter", 1.0 }, \ + { "GlitchLoop", 1.0 }, \ + { "SubHarmonic", 1.0 }, \ + { "DualAttractor", 1.0 } \ + } + +#define BANKS_DEF_6 { \ + { "PulseWander", 1.0 }, \ + { "TwinPulse", 1.0 }, \ + { "QuantPulse", 1.0 }, \ + { "ShapedPulse", 1.0 }, \ + { "ShiftPulse", 1.0 }, \ + { "MetallicNoise", 1.0 }, \ + { "NoiseSlew", 1.0 }, \ + { "NoiseBurst", 1.0 }, \ + { "LFSRNoise", 1.0 }, \ + { "DualPulse", 1.0 } \ } -#define BANKS_DEF_5 -// #define BANKS_DEF_6 // #define BANKS_DEF_7 // #define BANKS_DEF_8 // #define BANKS_DEF_9 diff --git a/src/noise-plethora/plugins/P_BitShift.hpp b/src/noise-plethora/plugins/P_BitShift.hpp new file mode 100644 index 0000000..6f4f2f2 --- /dev/null +++ b/src/noise-plethora/plugins/P_BitShift.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class BitShift : public NoisePlethoraPlugin { + +public: + + BitShift() { } + + ~BitShift() override {} + + BitShift(const BitShift&) = delete; + BitShift& operator=(const BitShift&) = delete; + + void init() override { + // sawtooths produce diverse bit patterns — XOR creates rich digital artifacts + // (square waves only have 2 values, XOR of ±32767 produces near-silence) + waveform1.begin(1.0f, 100, WAVEFORM_SAWTOOTH); + waveform2.begin(1.0f, 200, WAVEFORM_SAWTOOTH); + combine1.setCombineMode(1); // XOR + } + + void process(float k1, float k2) override { + float freq1 = 20.0f + pow(k1, 2) * 5000.0f; + float freq2 = freq1 * (1.0f + k2 * 7.0f); + waveform1.frequency(freq1); + waveform2.frequency(freq2); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + waveform1.update(nullptr, nullptr, &wf1Block); + waveform2.update(nullptr, nullptr, &wf2Block); + combine1.update(&wf1Block, &wf2Block, &combineBlock); + + blockBuffer.pushBuffer(combineBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return combine1; + } + unsigned char getPort() override { + return 0; + } + +private: + audio_block_t wf1Block, wf2Block, combineBlock; + + AudioSynthWaveformModulated waveform1; + AudioSynthWaveformModulated waveform2; + AudioEffectDigitalCombine combine1; +}; + +REGISTER_PLUGIN(BitShift); diff --git a/src/noise-plethora/plugins/P_BowedMetal.hpp b/src/noise-plethora/plugins/P_BowedMetal.hpp new file mode 100644 index 0000000..91099eb --- /dev/null +++ b/src/noise-plethora/plugins/P_BowedMetal.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +// BowedMetal: White noise excites 8 high-Q resonators at dense, inharmonic +// metallic ratios. More resonators + closer spacing than NoiseBells = denser, +// more reverberant metallic character. Like bowing a cymbal or singing bowl. + +class BowedMetal : public NoisePlethoraPlugin { + +public: + + BowedMetal() { } + + ~BowedMetal() override {} + + BowedMetal(const BowedMetal&) = delete; + BowedMetal& operator=(const BowedMetal&) = delete; + + void init() override { + noise1.amplitude(0.5f); + + for (int i = 0; i < NUM_RES; i++) { + y1[i] = 0.0f; + y2[i] = 0.0f; + } + + updateResonators(200.0f, 0.5f, 0.5f); + } + + void process(float k1, float k2) override { + float freq = 30.0f + pow(k1, 2) * 2000.0f; + + // k2 low = struck (short ring, louder noise burst) + // k2 high = bowed (long ring, quieter continuous noise) + float ringTime = 0.9985f + k2 * 0.0013f; // r: 0.9985 → 0.9998 (higher baseline) + float noiseLevel = 0.4f - k2 * 0.32f; // 0.4 → 0.08 (less noise overall) + noise1.amplitude(noiseLevel); + + updateResonators(freq, k2, ringTime); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float input = noiseBlock.data[i] / 32767.0f; + + float mixed = 0.0f; + + for (int r = 0; r < NUM_RES; r++) { + float out = coeff_a[r] * y1[r] - coeff_b[r] * y2[r] + coeff_g[r] * input; + + y2[r] = y1[r]; + y1[r] = out; + + if (!std::isfinite(out)) { + y1[r] = 0.0f; + y2[r] = 0.0f; + out = 0.0f; + } + + mixed += out * gains[r]; + } + + mixed *= 0.1f; + + if (mixed > 1.0f) mixed = 1.0f; + if (mixed < -1.0f) mixed = -1.0f; + outputBlock.data[i] = (int16_t)(mixed * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + + static const int NUM_RES = 8; + + // Metal plate / cymbal mode ratios (based on circular plate vibration modes) + // These are denser and more irregular than bell ratios + static constexpr float modeRatios[NUM_RES] = { + 1.000f, 1.183f, 1.506f, 1.741f, + 2.098f, 2.534f, 2.917f, 3.483f + }; + + void updateResonators(float fundamental, float k2, float ringTime) { + for (int i = 0; i < NUM_RES; i++) { + float f = fundamental * modeRatios[i]; + if (f > 19000.0f) f = 19000.0f; + if (f < 20.0f) f = 20.0f; + + // higher modes decay faster + float r = ringTime - i * 0.0002f; + if (r < 0.99f) r = 0.99f; + + float w = 2.0f * M_PI * f / 44100.0f; + + coeff_a[i] = 2.0f * r * std::cos(w); + coeff_b[i] = r * r; + coeff_g[i] = 1.0f - r; + } + + // dense amplitude distribution (all modes audible, slight rolloff) + gains[0] = 1.0f; + gains[1] = 0.9f; + gains[2] = 0.85f; + gains[3] = 0.75f; + gains[4] = 0.65f; + gains[5] = 0.55f; + gains[6] = 0.45f; + gains[7] = 0.35f; + } + + AudioSynthNoiseWhite noise1; + AudioSynthWaveformDc dc1; + + audio_block_t noiseBlock, outputBlock; + + float y1[NUM_RES] = {}; + float y2[NUM_RES] = {}; + + float coeff_a[NUM_RES] = {}; + float coeff_b[NUM_RES] = {}; + float coeff_g[NUM_RES] = {}; + float gains[NUM_RES] = {}; +}; + +REGISTER_PLUGIN(BowedMetal); diff --git a/src/noise-plethora/plugins/P_CellularSynth.hpp b/src/noise-plethora/plugins/P_CellularSynth.hpp new file mode 100644 index 0000000..fcd54dd --- /dev/null +++ b/src/noise-plethora/plugins/P_CellularSynth.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class CellularSynth : public NoisePlethoraPlugin { + +public: + + CellularSynth() { } + + ~CellularSynth() override {} + + CellularSynth(const CellularSynth&) = delete; + CellularSynth& operator=(const CellularSynth&) = delete; + + void init() override { + // Initialize CA with single cell in center + for (int i = 0; i < 32; i++) { + cells[i] = false; + } + cells[16] = true; + + clockCounter = 0; + clockSamples = 128; + rule = 110; + + float fundamental = 55.0f; + float masterVolume = 0.2f; + + for (int i = 0; i < 8; i++) { + osc[i].begin(masterVolume, fundamental * (i + 1), WAVEFORM_SAWTOOTH); + osc[i].frequencyModulation(0.1f); + } + + // Mixer gains: each sub-mixer has 4 inputs + for (int ch = 0; ch < 4; ch++) { + mixerA.gain(ch, 0.5f); + mixerB.gain(ch, 0.5f); + } + // Master mixer combines two sub-mixers + masterMixer.gain(0, 0.7f); + masterMixer.gain(1, 0.7f); + masterMixer.gain(2, 0.0f); + masterMixer.gain(3, 0.0f); + + // Set initial amplitudes from CA state + updateOscAmplitudes(); + } + + void process(float k1, float k2) override { + rule = (int)(k1 * 255.0f); + clockSamples = (int)(128 + (1.0f - k2) * 128.0f * 50.0f); + + clockCounter += AUDIO_BLOCK_SAMPLES; + if (clockCounter >= clockSamples) { + evolveCA(); + updateOscAmplitudes(); + clockCounter = 0; + } + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + // Update all 8 oscillators (no modulation input) + for (int i = 0; i < 8; i++) { + osc[i].update(nullptr, nullptr, &oscBlock[i]); + } + + // Mix through hierarchy: 4 oscs per sub-mixer + mixerA.update(&oscBlock[0], &oscBlock[1], &oscBlock[2], &oscBlock[3], &mixerABlock); + mixerB.update(&oscBlock[4], &oscBlock[5], &oscBlock[6], &oscBlock[7], &mixerBBlock); + + // Master mix + masterMixer.update(&mixerABlock, &mixerBBlock, nullptr, nullptr, &masterBlock); + + blockBuffer.pushBuffer(masterBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return masterMixer; + } + unsigned char getPort() override { + return 0; + } + +private: + + void evolveCA() { + bool newCells[32]; + for (int i = 0; i < 32; i++) { + int left = (i - 1 + 32) % 32; + int right = (i + 1) % 32; + + int pattern = (cells[left] ? 4 : 0) + | (cells[i] ? 2 : 0) + | (cells[right] ? 1 : 0); + + newCells[i] = ((rule >> pattern) & 1) != 0; + } + for (int i = 0; i < 32; i++) { + cells[i] = newCells[i]; + } + } + + void updateOscAmplitudes() { + // 32 cells grouped into 8 groups of 4 + for (int g = 0; g < 8; g++) { + int count = 0; + for (int c = 0; c < 4; c++) { + if (cells[g * 4 + c]) count++; + } + float amp = count * 0.25f; // 0, 0.25, 0.5, 0.75, 1.0 + osc[g].amplitude(amp); + } + } + + bool cells[32] = {}; + int clockCounter = 0; + int clockSamples = 128; + int rule = 110; + + audio_block_t oscBlock[8] = {}; + audio_block_t mixerABlock, mixerBBlock, masterBlock; + + AudioSynthWaveformModulated osc[8]; + AudioMixer4 mixerA; + AudioMixer4 mixerB; + AudioMixer4 masterMixer; +}; + +REGISTER_PLUGIN(CellularSynth); diff --git a/src/noise-plethora/plugins/P_CombNoise.hpp b/src/noise-plethora/plugins/P_CombNoise.hpp new file mode 100644 index 0000000..abe8dec --- /dev/null +++ b/src/noise-plethora/plugins/P_CombNoise.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class CombNoise : public NoisePlethoraPlugin { + +public: + + CombNoise() { } + + ~CombNoise() override {} + + // delete copy constructors + CombNoise(const CombNoise&) = delete; + CombNoise& operator=(const CombNoise&) = delete; + + void init() override { + noise1.amplitude(1); + + for (int c = 0; c < 4; c++) { + writePos[c] = 0; + for (int i = 0; i < 2048; i++) { + delayBuffer[c][i] = 0.0f; + } + } + } + + void process(float k1, float k2) override { + float delayInSamples = 44100.0f / (40.0f + pow(k1, 2) * 4960.0f); + feedback = k2 * 0.95f; + + // 4 combs at delay ratios 1.0, 0.7, 0.5, 0.35 + delaySamples[0] = delayInSamples * 1.0f; + delaySamples[1] = delayInSamples * 0.7f; + delaySamples[2] = delayInSamples * 0.5f; + delaySamples[3] = delayInSamples * 0.35f; + + // clamp delay lengths to valid buffer range + for (int c = 0; c < 4; c++) { + if (delaySamples[c] < 1.0f) delaySamples[c] = 1.0f; + if (delaySamples[c] > 2047.0f) delaySamples[c] = 2047.0f; + } + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + // generate noise block + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float input = noiseBlock.data[i] / 32767.0f; + float mixed = 0.0f; + + for (int c = 0; c < 4; c++) { + int delay = (int)delaySamples[c]; + if (delay < 1) delay = 1; + if (delay > 2047) delay = 2047; + + int readPos = writePos[c] - delay; + if (readPos < 0) readPos += 2048; + + float out = input + feedback * delayBuffer[c][readPos]; + delayBuffer[c][writePos[c]] = out; + + writePos[c] = (writePos[c] + 1) & 2047; // mod 2048 + + mixed += out; + } + + // mix 4 combs equally (scale by 0.25) + mixed *= 0.25f; + + // clamp and convert to int16 + if (mixed > 1.0f) mixed = 1.0f; + if (mixed < -1.0f) mixed = -1.0f; + outputBlock.data[i] = (int16_t)(mixed * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthNoiseWhite noise1; + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t noiseBlock, outputBlock; + + float delayBuffer[4][2048] = {}; + int writePos[4] = {}; + float delaySamples[4] = {100.0f, 70.0f, 50.0f, 35.0f}; + float feedback = 0.0f; +}; + +REGISTER_PLUGIN(CombNoise); // this is important, so that we can include the plugin in a bank diff --git a/src/noise-plethora/plugins/P_DroneBody.hpp b/src/noise-plethora/plugins/P_DroneBody.hpp new file mode 100644 index 0000000..07ed040 --- /dev/null +++ b/src/noise-plethora/plugins/P_DroneBody.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class DroneBody : public NoisePlethoraPlugin { + +public: + + DroneBody() + // : patchCord1(sine_fm1, 0, mixer1, 0) + // , patchCord2(sine_fm2, 0, mixer1, 1) + // , patchCord3(mixer1, 0, wavefolder1, 0) + // , patchCord4(dc1, 0, wavefolder1, 1) + { } + + ~DroneBody() override {} + + // delete copy constructors + DroneBody(const DroneBody&) = delete; + DroneBody& operator=(const DroneBody&) = delete; + + void init() override { + sine_fm1.frequency(100); + sine_fm1.amplitude(1.0); + + sine_fm2.frequency(101); + sine_fm2.amplitude(1.0); + + dc1.amplitude(0.3); + + mixer1.gain(0, 0.5); + mixer1.gain(1, 0.5); + + // Zero out the feedback blocks + prevSine1Block.zeroAudioBlock(); + prevSine2Block.zeroAudioBlock(); + } + + void process(float k1, float k2) override { + float freq = 20.0f + pow(k1, 2) * 500.0f; + float detune_ratio = 1.0f + k2 * 0.05f; + float dc_fold = 0.1f + k2 * 0.5f; + + sine_fm1.frequency(freq); + sine_fm2.frequency(freq * detune_ratio); + dc1.amplitude(dc_fold); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + // Cross-modulation: each sine is modulated by the previous block's output of the other + sine_fm1.update(&prevSine2Block, &sine1Block); + sine_fm2.update(&prevSine1Block, &sine2Block); + + // Save copies for next block's cross-feedback + memcpy(prevSine1Block.data, sine1Block.data, sizeof(int16_t) * AUDIO_BLOCK_SAMPLES); + memcpy(prevSine2Block.data, sine2Block.data, sizeof(int16_t) * AUDIO_BLOCK_SAMPLES); + + // Mix both sines + mixer1.update(&sine1Block, &sine2Block, nullptr, nullptr, &mixBlock); + + // Wavefold with DC bias + dc1.update(&dcBlock); + wavefolder1.update(&mixBlock, &dcBlock, &wfBlock); + + blockBuffer.pushBuffer(wfBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + audio_block_t sine1Block, sine2Block, mixBlock, dcBlock, wfBlock; + audio_block_t prevSine1Block, prevSine2Block; + + AudioSynthWaveformSineModulated sine_fm1; + AudioSynthWaveformSineModulated sine_fm2; + AudioSynthWaveformDc dc1; + AudioEffectWaveFolder wavefolder1; + AudioMixer4 mixer1; +}; + +REGISTER_PLUGIN(DroneBody); diff --git a/src/noise-plethora/plugins/P_DualAttractor.hpp b/src/noise-plethora/plugins/P_DualAttractor.hpp new file mode 100644 index 0000000..60b380b --- /dev/null +++ b/src/noise-plethora/plugins/P_DualAttractor.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class DualAttractor : public NoisePlethoraPlugin { + +public: + + DualAttractor() { } + + ~DualAttractor() override {} + + DualAttractor(const DualAttractor&) = delete; + DualAttractor& operator=(const DualAttractor&) = delete; + + void init() override { + // Lorenz system 1 + x1 = 1.0f; + y1 = 1.0f; + z1 = 1.0f; + + // Lorenz system 2 + x2 = -1.0f; + y2 = 0.0f; + z2 = 2.0f; + + coupling = 0.0f; + baseFreq = 100.0f; + + sine1.amplitude(0.7f); + sine1.frequency(100.0f); + sine2.amplitude(0.7f); + sine2.frequency(150.0f); + + mixer.gain(0, 1.0f); + mixer.gain(1, 1.0f); + mixer.gain(2, 0.0f); + mixer.gain(3, 0.0f); + } + + void process(float k1, float k2) override { + coupling = k1 * 5.0f; + baseFreq = 30.0f + std::pow(k2, 2.0f) * 2000.0f; + + // Lorenz parameters + const float sigma = 10.0f; + const float rho = 28.0f; + const float beta = 8.0f / 3.0f; + const float dt = 0.001f; + + // Run 10 integration steps for stability + for (int step = 0; step < 10; step++) { + float dx1 = sigma * (y1 - x1) + coupling * (x2 - x1); + float dy1 = x1 * (rho - z1) - y1; + float dz1 = x1 * y1 - beta * z1; + + float dx2 = sigma * (y2 - x2) + coupling * (x1 - x2); + float dy2 = x2 * (rho - z2) - y2; + float dz2 = x2 * y2 - beta * z2; + + x1 += dx1 * dt; + y1 += dy1 * dt; + z1 += dz1 * dt; + + x2 += dx2 * dt; + y2 += dy2 * dt; + z2 += dz2 * dt; + } + + // Check for NaN/infinity and reset if needed + if (!std::isfinite(x1) || !std::isfinite(y1) || !std::isfinite(z1)) { + x1 = 1.0f; + y1 = 1.0f; + z1 = 1.0f; + } + if (!std::isfinite(x2) || !std::isfinite(y2) || !std::isfinite(z2)) { + x2 = -1.0f; + y2 = 0.0f; + z2 = 2.0f; + } + + // Map Lorenz x outputs to frequency offsets — wide modulation for chaotic character + float freq1 = baseFreq + x1 * baseFreq * 0.4f; + float freq2 = baseFreq * 1.5f + x2 * baseFreq * 0.4f; + + // Clamp frequencies to reasonable range + if (freq1 < 20.0f) freq1 = 20.0f; + if (freq1 > 20000.0f) freq1 = 20000.0f; + if (freq2 < 20.0f) freq2 = 20.0f; + if (freq2 > 20000.0f) freq2 = 20000.0f; + + sine1.frequency(freq1); + sine2.frequency(freq2); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + sine1.update(&s1Block); + sine2.update(&s2Block); + mixer.update(&s1Block, &s2Block, nullptr, nullptr, &mixBlock); + + blockBuffer.pushBuffer(mixBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return mixer; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformSine sine1; + AudioSynthWaveformSine sine2; + AudioMixer4 mixer; + + audio_block_t s1Block, s2Block, mixBlock; + + // Lorenz system 1 + float x1 = 1.0f; + float y1 = 1.0f; + float z1 = 1.0f; + + // Lorenz system 2 + float x2 = -1.0f; + float y2 = 0.0f; + float z2 = 2.0f; + + float coupling = 0.0f; + float baseFreq = 100.0f; +}; + +REGISTER_PLUGIN(DualAttractor); diff --git a/src/noise-plethora/plugins/P_DualPulse.hpp b/src/noise-plethora/plugins/P_DualPulse.hpp new file mode 100644 index 0000000..336184b --- /dev/null +++ b/src/noise-plethora/plugins/P_DualPulse.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class DualPulse : public NoisePlethoraPlugin { + +public: + + DualPulse() { } + + ~DualPulse() override {} + + DualPulse(const DualPulse&) = delete; + DualPulse& operator=(const DualPulse&) = delete; + + void init() override { + value1 = 0.0f; + value2 = 0.0f; + counter1 = 0; + counter2 = 0; + samplesPerClock1 = 441; + samplesPerClock2 = 441; + } + + void process(float k1, float k2) override { + float rate1 = 5.0f + std::pow(k1, 2) * 495.0f; + float rate2 = 5.0f + std::pow(k2, 2) * 495.0f; + samplesPerClock1 = (int)(AUDIO_SAMPLE_RATE_EXACT / rate1); + if (samplesPerClock1 < 1) samplesPerClock1 = 1; + samplesPerClock2 = (int)(AUDIO_SAMPLE_RATE_EXACT / rate2); + if (samplesPerClock2 < 1) samplesPerClock2 = 1; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + counter1++; + if (counter1 >= samplesPerClock1) { + value1 = random::uniform() * 2.0f - 1.0f; + counter1 = 0; + } + + counter2++; + if (counter2 >= samplesPerClock2) { + value2 = random::uniform() * 2.0f - 1.0f; + counter2 = 0; + } + + float out = 0.5f * value1 + 0.5f * value2; + + // Add dither + out += (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + outputBlock.data[i] = (int16_t)(out * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float value1 = 0.0f; + float value2 = 0.0f; + int counter1 = 0; + int counter2 = 0; + int samplesPerClock1 = 441; + int samplesPerClock2 = 441; +}; + +REGISTER_PLUGIN(DualPulse); diff --git a/src/noise-plethora/plugins/P_FeedbackFM.hpp b/src/noise-plethora/plugins/P_FeedbackFM.hpp new file mode 100644 index 0000000..620f351 --- /dev/null +++ b/src/noise-plethora/plugins/P_FeedbackFM.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class FeedbackFM : public NoisePlethoraPlugin { + +public: + + FeedbackFM() { } + + ~FeedbackFM() override {} + + FeedbackFM(const FeedbackFM&) = delete; + FeedbackFM& operator=(const FeedbackFM&) = delete; + + void init() override { + phase = 0.0f; + prevOut = 0.0f; + currentFreq = 440.0f; + currentFeedback = 0.0f; + } + + void process(float k1, float k2) override { + currentFreq = 20.0f + std::pow(k1, 2.0f) * 5000.0f; + currentFeedback = k2 * 2.5f; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + const float twoPi = 2.0f * 3.14159265358979323846f; + const float phaseInc = twoPi * currentFreq / 44100.0f; + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + phase += phaseInc; + phase = std::fmod(phase, twoPi); + + float out = std::sin(phase + currentFeedback * prevOut); + prevOut = out; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float phase = 0.0f; + float prevOut = 0.0f; + float currentFreq = 440.0f; + float currentFeedback = 0.0f; +}; + +REGISTER_PLUGIN(FeedbackFM); diff --git a/src/noise-plethora/plugins/P_FlangeNoise.hpp b/src/noise-plethora/plugins/P_FlangeNoise.hpp new file mode 100644 index 0000000..7587af5 --- /dev/null +++ b/src/noise-plethora/plugins/P_FlangeNoise.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +#define FLANGE_NOISE_DELAY_LENGTH (8 * AUDIO_BLOCK_SAMPLES) + +class FlangeNoise : public NoisePlethoraPlugin { + +public: + + FlangeNoise() + // : patchCord1(pink1, flange1) + { } + + ~FlangeNoise() override {} + + // delete copy constructors + FlangeNoise(const FlangeNoise&) = delete; + FlangeNoise& operator=(const FlangeNoise&) = delete; + + void init() override { + pink1.amplitude(1.0); + flange1.begin(flangeDelayLine, FLANGE_NOISE_DELAY_LENGTH, s_offset, s_depth, s_rate); + } + + void process(float k1, float k2) override { + float rate = 0.05f + pow(k1, 2) * 5.0f; + int depth = (int)(2 + k2 * 120); // wider sweep range for audible flanging + int offset = (int)(2 + k2 * 80); + + flange1.voices(offset, depth, rate); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + pink1.update(&pinkBlock); + flange1.update(&pinkBlock, &flangeBlock); + + blockBuffer.pushBuffer(flangeBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return pink1; + } + unsigned char getPort() override { + return 0; + } + +private: + audio_block_t pinkBlock, flangeBlock; + + AudioSynthNoisePink pink1; + AudioEffectFlange flange1; + + short flangeDelayLine[FLANGE_NOISE_DELAY_LENGTH]; + int s_offset = 2 * FLANGE_NOISE_DELAY_LENGTH / 4; + int s_depth = FLANGE_NOISE_DELAY_LENGTH / 4; + float s_rate = 0.5; +}; + +REGISTER_PLUGIN(FlangeNoise); diff --git a/src/noise-plethora/plugins/P_FormantNoise.hpp b/src/noise-plethora/plugins/P_FormantNoise.hpp new file mode 100644 index 0000000..51967e3 --- /dev/null +++ b/src/noise-plethora/plugins/P_FormantNoise.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class FormantNoise : public NoisePlethoraPlugin { + +public: + + FormantNoise() { } + + ~FormantNoise() override {} + + // delete copy constructors + FormantNoise(const FormantNoise&) = delete; + FormantNoise& operator=(const FormantNoise&) = delete; + + void init() override { + noise1.amplitude(1); + + filter1.resonance(2.0f); + filter1.octaveControl(0); + filter2.resonance(2.0f); + filter2.octaveControl(0); + filter3.resonance(2.0f); + filter3.octaveControl(0); + + mixer1.gain(0, 0.5f); + mixer1.gain(1, 0.5f); + mixer1.gain(2, 0.5f); + mixer1.gain(3, 0); + } + + void process(float k1, float k2) override { + // Vowel formant table (Hz): + // A=[730,1090,2440], E=[660,1720,2410], I=[270,2290,3010], + // O=[570,840,2410], U=[300,870,2240] + static const float formants[5][3] = { + {730.0f, 1090.0f, 2440.0f}, // A + {660.0f, 1720.0f, 2410.0f}, // E + {270.0f, 2290.0f, 3010.0f}, // I + {570.0f, 840.0f, 2410.0f}, // O + {300.0f, 870.0f, 2240.0f} // U + }; + + // continuous vowel morph: 0=A, 0.25=E, 0.5=I, 0.75=O, 1.0=U + float pos = k1 * 4.0f; // 0..4 + int idx = (int)pos; + if (idx < 0) idx = 0; + if (idx > 3) idx = 3; + float frac = pos - (float)idx; + if (frac < 0.0f) frac = 0.0f; + if (frac > 1.0f) frac = 1.0f; + + // interpolate F1, F2, F3 between adjacent vowels + float f1 = formants[idx][0] + frac * (formants[idx + 1][0] - formants[idx][0]); + float f2 = formants[idx][1] + frac * (formants[idx + 1][1] - formants[idx][1]); + float f3 = formants[idx][2] + frac * (formants[idx + 1][2] - formants[idx][2]); + + filter1.frequency(f1); + filter2.frequency(f2); + filter3.frequency(f3); + + // resonance: q = 1.0 + k2 * 4.0 (range 1-5) + float q = 1.0f + k2 * 4.0f; + filter1.resonance(q); + filter2.resonance(q); + filter3.resonance(q); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + // generate noise + noise1.update(&noiseBlock); + + // process through 3 bandpass filters (use bandpass output) + filter1.update(&noiseBlock, nullptr, &filterLP1, &filterBP1, &filterHP1); + filter2.update(&noiseBlock, nullptr, &filterLP2, &filterBP2, &filterHP2); + filter3.update(&noiseBlock, nullptr, &filterLP3, &filterBP3, &filterHP3); + + // mix the 3 bandpass outputs + mixer1.update(&filterBP1, &filterBP2, &filterBP3, nullptr, &mixerOut); + + blockBuffer.pushBuffer(mixerOut.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return mixer1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthNoiseWhite noise1; + AudioFilterStateVariable filter1; + AudioFilterStateVariable filter2; + AudioFilterStateVariable filter3; + AudioMixer4 mixer1; + + audio_block_t noiseBlock; + audio_block_t filterLP1, filterBP1, filterHP1; + audio_block_t filterLP2, filterBP2, filterHP2; + audio_block_t filterLP3, filterBP3, filterHP3; + audio_block_t mixerOut; +}; + +REGISTER_PLUGIN(FormantNoise); // this is important, so that we can include the plugin in a bank diff --git a/src/noise-plethora/plugins/P_GlitchLoop.hpp b/src/noise-plethora/plugins/P_GlitchLoop.hpp new file mode 100644 index 0000000..68a64f0 --- /dev/null +++ b/src/noise-plethora/plugins/P_GlitchLoop.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +#define GLITCHLOOP_MAX_SAMPLES 8820 + +class GlitchLoop : public NoisePlethoraPlugin { + +public: + + GlitchLoop() { } + + ~GlitchLoop() override {} + + GlitchLoop(const GlitchLoop&) = delete; + GlitchLoop& operator=(const GlitchLoop&) = delete; + + void init() override { + currentLoopLen = GLITCHLOOP_MAX_SAMPLES; + loopPos = 0; + bitDepthAccum = 16.0f; + bitsToLose = 0.0f; + + // Fill buffer with white noise + for (int i = 0; i < GLITCHLOOP_MAX_SAMPLES; i++) { + loopBuffer[i] = random::uniform() * 2.0f - 1.0f; + } + } + + void process(float k1, float k2) override { + currentLoopLen = (int)(441.0f + std::pow(k1, 2.0f) * 8379.0f); + if (currentLoopLen > GLITCHLOOP_MAX_SAMPLES) { + currentLoopLen = GLITCHLOOP_MAX_SAMPLES; + } + if (currentLoopLen < 1) { + currentLoopLen = 1; + } + + bitsToLose = k2 * 0.05f; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float sample = loopBuffer[loopPos]; + + // Apply bit depth reduction + int effectiveBits = (int)bitDepthAccum; + if (effectiveBits < 1) effectiveBits = 1; + if (effectiveBits > 16) effectiveBits = 16; + + // Bit crush: quantize to fewer bits + int shift = 16 - effectiveBits; + if (shift > 0) { + // Convert to int16 range, shift right then left to lose bits + int32_t intSample = (int32_t)(sample * 32767.0f); + intSample = (intSample >> shift) << shift; + sample = (float)intSample / 32767.0f; + } + + // Write crushed sample back into buffer for progressive degradation + loopBuffer[loopPos] = sample; + + int32_t out = (int32_t)(sample * 32767.0f); + if (out > 32767) out = 32767; + if (out < -32767) out = -32767; + outputBlock.data[i] = (int16_t)out; + + loopPos++; + if (loopPos >= currentLoopLen) { + loopPos = 0; + bitDepthAccum -= bitsToLose; + + // When bit depth is exhausted, refill with fresh noise + if (bitDepthAccum < 1.0f) { + bitDepthAccum = 16.0f; + for (int j = 0; j < GLITCHLOOP_MAX_SAMPLES; j++) { + loopBuffer[j] = random::uniform() * 2.0f - 1.0f; + } + } + } + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float loopBuffer[GLITCHLOOP_MAX_SAMPLES]; + int loopPos = 0; + int currentLoopLen = GLITCHLOOP_MAX_SAMPLES; + float bitDepthAccum = 16.0f; + float bitsToLose = 0.0f; +}; + +REGISTER_PLUGIN(GlitchLoop); diff --git a/src/noise-plethora/plugins/P_HenonDust.hpp b/src/noise-plethora/plugins/P_HenonDust.hpp new file mode 100644 index 0000000..a1cc1f5 --- /dev/null +++ b/src/noise-plethora/plugins/P_HenonDust.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class HenonDust : public NoisePlethoraPlugin { + +public: + + HenonDust() { } + + ~HenonDust() override {} + + HenonDust(const HenonDust&) = delete; + HenonDust& operator=(const HenonDust&) = delete; + + void init() override { + hx = 0.1f; + hy = 0.1f; + a = 1.4f; + b = 0.3f; + } + + void process(float k1, float k2) override { + // focus k1 on the interesting chaos transition zone (1.15-1.4) + a = 1.15f + k1 * 0.25f; + b = 0.15f + k2 * 0.2f; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float new_x = 1.0f - a * hx * hx + hy; + float new_y = b * hx; + hx = new_x; + hy = new_y; + + // Reset on divergence or NaN + if (std::isnan(hx) || std::isinf(hx) || std::isnan(hy) || std::isinf(hy)) { + hx = random::uniform() - 0.5f; + hy = random::uniform() - 0.5f; + } + + // Normalize hx from [-1.5, 1.5] to [-1, 1] + float out = hx; + if (out > 1.5f) out = 1.5f; + if (out < -1.5f) out = -1.5f; + out /= 1.5f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float hx = 0.1f; + float hy = 0.1f; + float a = 1.4f; + float b = 0.3f; +}; + +REGISTER_PLUGIN(HenonDust); diff --git a/src/noise-plethora/plugins/P_IceRain.hpp b/src/noise-plethora/plugins/P_IceRain.hpp new file mode 100644 index 0000000..460e378 --- /dev/null +++ b/src/noise-plethora/plugins/P_IceRain.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class IceRain : public NoisePlethoraPlugin { + +public: + + IceRain() + // : patchCord1(waveformMod1, freeverb1) + // , patchCord2(waveformMod1, 0, mixer1, 0) + // , patchCord3(freeverb1, 0, mixer1, 1) + { } + + ~IceRain() override {} + + // delete copy constructors + IceRain(const IceRain&) = delete; + IceRain& operator=(const IceRain&) = delete; + + void init() override { + waveformMod1.begin(1.0, 5, WAVEFORM_SAMPLE_HOLD); + waveformMod1.frequencyModulation(10); + + freeverb1.roomsize(0.8); + freeverb1.damping(0.8); + + mixer1.gain(0, 0.3); + mixer1.gain(1, 1.0); + } + + void process(float k1, float k2) override { + float freq = 0.5f + pow(k1, 2) * 200.0f; + float roomsize = 0.3f + k2 * 0.7f; + + waveformMod1.frequency(freq); + freeverb1.roomsize(roomsize); + + mixer1.gain(0, 0.3f * (1.0f - k2)); + mixer1.gain(1, k2 * 2.0f); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + waveformMod1.update(nullptr, nullptr, &shBlock); + freeverb1.update(&shBlock, &reverbBlock); + mixer1.update(&shBlock, &reverbBlock, nullptr, nullptr, &mixBlock); + + blockBuffer.pushBuffer(mixBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return mixer1; + } + unsigned char getPort() override { + return 0; + } + +private: + audio_block_t shBlock, reverbBlock, mixBlock; + + AudioSynthWaveformModulated waveformMod1; + AudioEffectFreeverb freeverb1; + AudioMixer4 mixer1; +}; + +REGISTER_PLUGIN(IceRain); diff --git a/src/noise-plethora/plugins/P_LFSRNoise.hpp b/src/noise-plethora/plugins/P_LFSRNoise.hpp new file mode 100644 index 0000000..b06bfaf --- /dev/null +++ b/src/noise-plethora/plugins/P_LFSRNoise.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class LFSRNoise : public NoisePlethoraPlugin { + +public: + + LFSRNoise() { } + + ~LFSRNoise() override {} + + LFSRNoise(const LFSRNoise&) = delete; + LFSRNoise& operator=(const LFSRNoise&) = delete; + + void init() override { + lfsr = 0xACE1; + clockCounter = 0; + samplesPerClock = 441; + currentTaps = taps[0]; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 495.0f; + samplesPerClock = (int)(AUDIO_SAMPLE_RATE_EXACT / rate); + if (samplesPerClock < 1) samplesPerClock = 1; + + int tapIndex = (int)(k2 * 7.99f); + if (tapIndex < 0) tapIndex = 0; + if (tapIndex > 7) tapIndex = 7; + currentTaps = taps[tapIndex]; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + clockCounter++; + if (clockCounter >= samplesPerClock) { + uint16_t masked = lfsr & currentTaps; + uint16_t feedback = __builtin_popcount(masked) & 1; + lfsr = (lfsr >> 1) | (feedback << 15); + if (lfsr == 0) lfsr = 0xACE1; // prevent permanent lockup + clockCounter = 0; + } + + // Output top 4 bits: 16 discrete levels + int16_t quantized = (int16_t)(lfsr >> 12); + float out = quantized / 7.5f - 1.0f; + + // Add dither + out += (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + outputBlock.data[i] = (int16_t)(out * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + static constexpr uint16_t taps[8] = { + 0x002D, 0x0039, 0xD008, 0xB400, + 0x6000, 0xD295, 0xB002, 0xE100 + }; + + uint16_t lfsr = 0xACE1; + int clockCounter = 0; + int samplesPerClock = 441; + uint16_t currentTaps = 0x002D; +}; + +REGISTER_PLUGIN(LFSRNoise); diff --git a/src/noise-plethora/plugins/P_LogisticNoise.hpp b/src/noise-plethora/plugins/P_LogisticNoise.hpp new file mode 100644 index 0000000..e237a0e --- /dev/null +++ b/src/noise-plethora/plugins/P_LogisticNoise.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class LogisticNoise : public NoisePlethoraPlugin { + +public: + + LogisticNoise() { } + + ~LogisticNoise() override {} + + LogisticNoise(const LogisticNoise&) = delete; + LogisticNoise& operator=(const LogisticNoise&) = delete; + + void init() override { + x = 0.5f; + holdCounter = 0; + samplesPerIteration = 1; + r = 3.8f; + } + + void process(float k1, float k2) override { + r = 3.5f + k1 * 0.5f; + samplesPerIteration = std::max(1, (int)(1 + (1.0f - k2) * 30)); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + holdCounter++; + if (holdCounter >= samplesPerIteration) { + x = r * x * (1.0f - x); + holdCounter = 0; + + // Reset on numerical instability + if (x < 0.0f || x > 1.0f || std::isnan(x) || std::isinf(x)) { + x = 0.5f + (random::uniform() - 0.5f) * 0.1f; + } + } + + float out = x * 2.0f - 1.0f; + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float x = 0.5f; + float r = 3.8f; + int holdCounter = 0; + int samplesPerIteration = 1; +}; + +REGISTER_PLUGIN(LogisticNoise); diff --git a/src/noise-plethora/plugins/P_MetallicNoise.hpp b/src/noise-plethora/plugins/P_MetallicNoise.hpp new file mode 100644 index 0000000..e6edf86 --- /dev/null +++ b/src/noise-plethora/plugins/P_MetallicNoise.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class MetallicNoise : public NoisePlethoraPlugin { + +public: + + MetallicNoise() { } + + ~MetallicNoise() override {} + + MetallicNoise(const MetallicNoise&) = delete; + MetallicNoise& operator=(const MetallicNoise&) = delete; + + void init() override { + for (int i = 0; i < 6; i++) { + osc[i].begin(0.15f, baseFreqs[i], WAVEFORM_SQUARE); // 6*0.15=0.9 max sum, no clipping + } + + mixer1.gain(0, 1.0f); + mixer1.gain(1, 1.0f); + mixer1.gain(2, 1.0f); + mixer1.gain(3, 1.0f); + + mixer2.gain(0, 1.0f); + mixer2.gain(1, 1.0f); + mixer2.gain(2, 0.0f); + mixer2.gain(3, 0.0f); + + mixer3.gain(0, 1.0f); + mixer3.gain(1, 1.0f); + mixer3.gain(2, 0.0f); + mixer3.gain(3, 0.0f); + + filter1.frequency(5000); + filter1.resonance(0.7f); + filter1.octaveControl(2.0f); + } + + void process(float k1, float k2) override { + float pitchMult = 0.5f + k1 * 2.0f; + for (int i = 0; i < 6; i++) { + osc[i].frequency(baseFreqs[i] * pitchMult); + } + + float filterFreq = 500.0f + std::pow(k2, 2) * 15000.0f; + filter1.frequency(filterFreq); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + for (int i = 0; i < 6; i++) { + osc[i].update(nullptr, nullptr, &oscBlock[i]); + } + + mixer1.update(&oscBlock[0], &oscBlock[1], &oscBlock[2], &oscBlock[3], &mixerBlock[0]); + mixer2.update(&oscBlock[4], &oscBlock[5], nullptr, nullptr, &mixerBlock[1]); + mixer3.update(&mixerBlock[0], &mixerBlock[1], nullptr, nullptr, &mixerBlock[2]); + + filter1.update(&mixerBlock[2], nullptr, &filterLP, &filterBP, &filterHP); + + blockBuffer.pushBuffer(filterHP.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return filter1; + } + unsigned char getPort() override { + return 0; + } + +private: + static constexpr float baseFreqs[6] = {205.3f, 304.4f, 369.6f, 522.7f, 800.6f, 1053.4f}; + + AudioSynthWaveformModulated osc[6]; + AudioMixer4 mixer1; + AudioMixer4 mixer2; + AudioMixer4 mixer3; + AudioFilterStateVariable filter1; + + audio_block_t oscBlock[6]; + audio_block_t mixerBlock[3]; + audio_block_t filterLP, filterBP, filterHP; +}; + +REGISTER_PLUGIN(MetallicNoise); diff --git a/src/noise-plethora/plugins/P_NoiseBells.hpp b/src/noise-plethora/plugins/P_NoiseBells.hpp new file mode 100644 index 0000000..e409545 --- /dev/null +++ b/src/noise-plethora/plugins/P_NoiseBells.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +// NoiseBells: White noise excites 4 sharp 2-pole resonators at bell-like +// inharmonic ratios. The resonators have very high Q (~500+), creating +// clear ringing tones from noise. Like striking a metal bar or bell. +// +// 2-pole resonator: y[n] = 2*r*cos(w)*y[n-1] - r^2*y[n-2] + (1-r)*x[n] +// r close to 1 = long ring, sharp peak. w = 2*pi*freq/samplerate. + +class NoiseBells : public NoisePlethoraPlugin { + +public: + + NoiseBells() { } + + ~NoiseBells() override {} + + NoiseBells(const NoiseBells&) = delete; + NoiseBells& operator=(const NoiseBells&) = delete; + + void init() override { + noise1.amplitude(1.0); + + for (int i = 0; i < NUM_RESONATORS; i++) { + y1[i] = 0.0f; + y2[i] = 0.0f; + coeff_a[i] = 0.0f; + coeff_b[i] = 0.0f; + coeff_g[i] = 0.0f; + } + + // initial resonator setup + updateResonators(500.0f, 0.5f); + } + + void process(float k1, float k2) override { + float freq = 80.0f + pow(k1, 2) * 4000.0f; + updateResonators(freq, k2); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float input = noiseBlock.data[i] / 32767.0f; + + float mixed = 0.0f; + + for (int r = 0; r < NUM_RESONATORS; r++) { + // 2-pole resonator + float out = coeff_a[r] * y1[r] - coeff_b[r] * y2[r] + coeff_g[r] * input; + + y2[r] = y1[r]; + y1[r] = out; + + // safety: reset on NaN/inf + if (!std::isfinite(out)) { + y1[r] = 0.0f; + y2[r] = 0.0f; + out = 0.0f; + } + + mixed += out * gains[r]; + } + + // scale down (resonators can be loud) + mixed *= 0.15f; + + if (mixed > 1.0f) mixed = 1.0f; + if (mixed < -1.0f) mixed = -1.0f; + outputBlock.data[i] = (int16_t)(mixed * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + + static const int NUM_RESONATORS = 4; + + void updateResonators(float fundamental, float inharmonicity) { + // bell-like inharmonic ratios (circular membrane / bar modes) + // at inharmonicity=0: nearly harmonic. at 1: very stretched. + float stretch = 1.0f + inharmonicity * 2.0f; + float freqs[NUM_RESONATORS]; + freqs[0] = fundamental; + freqs[1] = fundamental * (1.0f + 0.59f * stretch); // ~1.59 - 2.77 + freqs[2] = fundamental * (1.0f + 1.33f * stretch); // ~2.33 - 4.99 + freqs[3] = fundamental * (1.0f + 2.14f * stretch); // ~3.14 - 7.42 + + // r controls ring time. closer to 1 = longer ring = sharper peak + // higher partials decay faster (lower r) + float baseR = 0.9992f; + + for (int i = 0; i < NUM_RESONATORS; i++) { + float f = freqs[i]; + if (f > 20000.0f) f = 20000.0f; + if (f < 20.0f) f = 20.0f; + + float r = baseR - i * 0.0003f; // higher partials decay slightly faster + float w = 2.0f * M_PI * f / 44100.0f; + + coeff_a[i] = 2.0f * r * std::cos(w); + coeff_b[i] = r * r; + coeff_g[i] = 1.0f - r; // input gain scales with (1-r) for normalization + } + + // amplitude rolloff for higher partials + gains[0] = 1.0f; + gains[1] = 0.7f; + gains[2] = 0.4f; + gains[3] = 0.25f; + } + + AudioSynthNoiseWhite noise1; + AudioSynthWaveformDc dc1; // dummy for getStream + + audio_block_t noiseBlock, outputBlock; + + // resonator state + float y1[NUM_RESONATORS] = {}; + float y2[NUM_RESONATORS] = {}; + + // resonator coefficients + float coeff_a[NUM_RESONATORS] = {}; + float coeff_b[NUM_RESONATORS] = {}; + float coeff_g[NUM_RESONATORS] = {}; + float gains[NUM_RESONATORS] = {}; +}; + +REGISTER_PLUGIN(NoiseBells); diff --git a/src/noise-plethora/plugins/P_NoiseBurst.hpp b/src/noise-plethora/plugins/P_NoiseBurst.hpp new file mode 100644 index 0000000..126a2fd --- /dev/null +++ b/src/noise-plethora/plugins/P_NoiseBurst.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class NoiseBurst : public NoisePlethoraPlugin { + +public: + + NoiseBurst() { } + + ~NoiseBurst() override {} + + NoiseBurst(const NoiseBurst&) = delete; + NoiseBurst& operator=(const NoiseBurst&) = delete; + + void init() override { + noise1.amplitude(1.0f); + burstCounter = 0; + currentBurstLen = 200; + burstProb = 0.001f; + } + + void process(float k1, float k2) override { + burstProb = 0.00002f + std::pow(k1, 2) * 0.003f; + currentBurstLen = (int)(44 + k2 * 4400); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + if (burstCounter > 0) { + // Envelope fade-out on last 20 samples + float gain = 1.0f; + if (burstCounter < 20) { + gain = burstCounter / 20.0f; + } + outputBlock.data[i] = (int16_t)(noiseBlock.data[i] * gain); + burstCounter--; + } + else { + if (random::uniform() < burstProb) { + burstCounter = currentBurstLen; + // Output this sample as part of the burst + float gain = 1.0f; + if (burstCounter < 20) { + gain = burstCounter / 20.0f; + } + outputBlock.data[i] = (int16_t)(noiseBlock.data[i] * gain); + burstCounter--; + } + else { + outputBlock.data[i] = 0; + } + } + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + AudioSynthNoiseWhite noise1; + + audio_block_t noiseBlock, outputBlock; + + int burstCounter = 0; + int currentBurstLen = 200; + float burstProb = 0.001f; +}; + +REGISTER_PLUGIN(NoiseBurst); diff --git a/src/noise-plethora/plugins/P_NoiseHarmonics.hpp b/src/noise-plethora/plugins/P_NoiseHarmonics.hpp new file mode 100644 index 0000000..8ca7977 --- /dev/null +++ b/src/noise-plethora/plugins/P_NoiseHarmonics.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class NoiseHarmonics : public NoisePlethoraPlugin { + +public: + + NoiseHarmonics() + // : patchCord1(noise1, 0, wavefolder1, 0) + // , patchCord2(dc1, 0, wavefolder1, 1) + // , patchCord3(wavefolder1, 0, filter1, 0) + { } + + ~NoiseHarmonics() override {} + + // delete copy constructors + NoiseHarmonics(const NoiseHarmonics&) = delete; + NoiseHarmonics& operator=(const NoiseHarmonics&) = delete; + + void init() override { + noise1.amplitude(1.0); + dc1.amplitude(0.5); + + filter1.frequency(1000); + filter1.resonance(1.5); // lower Q = wider, grittier character + filter1.octaveControl(2.0); + } + + void process(float k1, float k2) override { + float dcAmp = 0.1f + pow(k1, 2) * 1.5f; // wider range, more aggressive folding + float filterFreq = 60.0f + pow(k2, 2) * 8000.0f; + + dc1.amplitude(dcAmp); + filter1.frequency(filterFreq); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + dc1.update(&dcBlock); + wavefolder1.update(&noiseBlock, &dcBlock, &wfBlock); + filter1.update(&wfBlock, nullptr, &lpBlock, &bpBlock, &hpBlock); + + // lowpass instead of bandpass — lets all wavefolder harmonics through + blockBuffer.pushBuffer(lpBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return filter1; + } + unsigned char getPort() override { + return 0; + } + +private: + audio_block_t noiseBlock, dcBlock, wfBlock; + audio_block_t lpBlock, bpBlock, hpBlock; + + AudioSynthNoiseWhite noise1; + AudioSynthWaveformDc dc1; + AudioEffectWaveFolder wavefolder1; + AudioFilterStateVariable filter1; +}; + +REGISTER_PLUGIN(NoiseHarmonics); diff --git a/src/noise-plethora/plugins/P_NoiseSlew.hpp b/src/noise-plethora/plugins/P_NoiseSlew.hpp new file mode 100644 index 0000000..137f20e --- /dev/null +++ b/src/noise-plethora/plugins/P_NoiseSlew.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" +#include + +class NoiseSlew : public NoisePlethoraPlugin { + +public: + + NoiseSlew() { } + + ~NoiseSlew() override {} + + NoiseSlew(const NoiseSlew&) = delete; + NoiseSlew& operator=(const NoiseSlew&) = delete; + + void init() override { + noise1.amplitude(1.0f); + slewState = 0.0f; + alpha = 0.01f; + mix = 0.0f; + } + + void process(float k1, float k2) override { + alpha = 0.0003f + std::pow(k1, 3) * 0.15f; + mix = k2; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float noiseSample = noiseBlock.data[i] / 32767.0f; + + slewState = slewState * (1.0f - alpha) + noiseSample * alpha; + + // NaN guard + if (std::isnan(slewState) || std::isinf(slewState)) { + slewState = 0.0f; + } + + float out = slewState * (1.0f - mix * 0.7f) + noiseSample * mix * 0.3f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + outputBlock.data[i] = (int16_t)(out * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; // dummy for getStream() + AudioSynthNoiseWhite noise1; + + audio_block_t noiseBlock, outputBlock; + + float slewState = 0.0f; + float alpha = 0.01f; + float mix = 0.0f; +}; + +REGISTER_PLUGIN(NoiseSlew); diff --git a/src/noise-plethora/plugins/P_PluckCloud.hpp b/src/noise-plethora/plugins/P_PluckCloud.hpp new file mode 100644 index 0000000..eb0b433 --- /dev/null +++ b/src/noise-plethora/plugins/P_PluckCloud.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class PluckCloud : public NoisePlethoraPlugin { + +public: + + PluckCloud() { } + + ~PluckCloud() override {} + + // delete copy constructors + PluckCloud(const PluckCloud&) = delete; + PluckCloud& operator=(const PluckCloud&) = delete; + + void init() override { + for (int v = 0; v < 6; v++) { + writePos[v] = 0; + filterState[v] = 0.0f; + triggered[v] = false; + for (int i = 0; i < 1024; i++) { + buf[v][i] = 0.0f; + } + } + damping = 0.5f; + baseDelay = 100.0f; + } + + void process(float k1, float k2) override { + float freq = 60.0f + pow(k1, 2) * 2940.0f; + baseDelay = 44100.0f / freq; + damping = 0.98f - k2 * 0.58f; // k2=0: heavy LP (dull thud), k2=1: light LP (bright ring) + + // random retrigger: each voice has ~2% chance per process() call + for (int v = 0; v < 6; v++) { + if (random::uniform() < 0.02f) { + triggerVoice(v); + } + } + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + // voice detune ratios + static const float detuneRatio[6] = {1.0f, 1.005f, 0.995f, 1.01f, 0.99f, 1.015f}; + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float mixed = 0.0f; + + for (int v = 0; v < 6; v++) { + float voiceDelay = baseDelay * detuneRatio[v]; + int delay = (int)voiceDelay; + if (delay < 1) delay = 1; + if (delay > 1023) delay = 1023; + + int readPos = writePos[v] - delay; + if (readPos < 0) readPos += 1024; + + // read from delay and apply one-pole LP filter + float delayOut = buf[v][readPos]; + float filtered = damping * filterState[v] + (1.0f - damping) * delayOut; + filterState[v] = filtered; + + // write filtered output back into delay line + buf[v][writePos[v]] = filtered; + writePos[v] = (writePos[v] + 1) & 1023; // mod 1024 + + mixed += filtered; + } + + // scale by number of voices + mixed *= (1.0f / 6.0f); + + // clamp and convert to int16 + if (mixed > 1.0f) mixed = 1.0f; + if (mixed < -1.0f) mixed = -1.0f; + outputBlock.data[i] = (int16_t)(mixed * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + + void triggerVoice(int v) { + // fill delay buffer BEHIND writePos so it's immediately read + float voiceDelay = baseDelay; + int delay = (int)voiceDelay; + if (delay < 1) delay = 1; + if (delay > 1023) delay = 1023; + + for (int i = 0; i < delay; i++) { + int pos = (writePos[v] - delay + i + 1024) & 1023; + buf[v][pos] = random::uniform() * 2.0f - 1.0f; + } + filterState[v] = 0.0f; + triggered[v] = true; + } + + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t outputBlock; + + float buf[6][1024] = {}; + int writePos[6] = {}; + float filterState[6] = {}; + bool triggered[6] = {}; + float damping = 0.5f; + float baseDelay = 100.0f; +}; + +REGISTER_PLUGIN(PluckCloud); // this is important, so that we can include the plugin in a bank diff --git a/src/noise-plethora/plugins/P_PulseWander.hpp b/src/noise-plethora/plugins/P_PulseWander.hpp new file mode 100644 index 0000000..b13b2aa --- /dev/null +++ b/src/noise-plethora/plugins/P_PulseWander.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include "NoisePlethoraPlugin.hpp" + +class PulseWander : public NoisePlethoraPlugin { + +public: + + PulseWander() { } + + ~PulseWander() override {} + + PulseWander(const PulseWander&) = delete; + PulseWander& operator=(const PulseWander&) = delete; + + void init() override { + currentValue = random::uniform() * 2.0f - 1.0f; + targetValue = random::uniform() * 2.0f - 1.0f; + smoothAlpha = 0.1f; + sampleCounter = 0; + samplesPerTarget = 441; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 395.0f; + samplesPerTarget = std::max(1, (int)(44100.0f / rate)); + + float minAlpha = 3.0f / (float)samplesPerTarget; + smoothAlpha = minAlpha + (1.0f - k2) * (1.0f - minAlpha); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + sampleCounter++; + if (sampleCounter >= samplesPerTarget) { + targetValue = random::uniform() * 2.0f - 1.0f; + sampleCounter = 0; + } + + currentValue = currentValue * (1.0f - smoothAlpha) + targetValue * smoothAlpha; + + // NaN/Inf guard + if (!std::isfinite(currentValue)) { + currentValue = 0.0f; + } + + float out = currentValue + (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; + + audio_block_t outputBlock; + + float currentValue = 0.0f; + float targetValue = 0.0f; + float smoothAlpha = 0.1f; + int sampleCounter = 0; + int samplesPerTarget = 441; +}; + +REGISTER_PLUGIN(PulseWander); diff --git a/src/noise-plethora/plugins/P_QuantPulse.hpp b/src/noise-plethora/plugins/P_QuantPulse.hpp new file mode 100644 index 0000000..defcc39 --- /dev/null +++ b/src/noise-plethora/plugins/P_QuantPulse.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include "NoisePlethoraPlugin.hpp" + +class QuantPulse : public NoisePlethoraPlugin { + +public: + + QuantPulse() { } + + ~QuantPulse() override {} + + QuantPulse(const QuantPulse&) = delete; + QuantPulse& operator=(const QuantPulse&) = delete; + + void init() override { + currentValue = 0.0f; + sampleCounter = 0; + samplesPerClock = 441; + bits = 3.0f; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 495.0f; + samplesPerClock = std::max(1, (int)(44100.0f / rate)); + + bits = 1.0f + k2 * 5.0f; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + sampleCounter++; + if (sampleCounter >= samplesPerClock) { + float raw = random::uniform() * 2.0f - 1.0f; + float levels = std::pow(2.0f, bits); + currentValue = std::round(raw * levels) / levels; + + if (currentValue > 1.0f) currentValue = 1.0f; + if (currentValue < -1.0f) currentValue = -1.0f; + + sampleCounter = 0; + } + + // NaN/Inf guard + if (!std::isfinite(currentValue)) { + currentValue = 0.0f; + } + + float out = currentValue + (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; + + audio_block_t outputBlock; + + float currentValue = 0.0f; + int sampleCounter = 0; + int samplesPerClock = 441; + float bits = 3.0f; +}; + +REGISTER_PLUGIN(QuantPulse); diff --git a/src/noise-plethora/plugins/P_RunawayFilter.hpp b/src/noise-plethora/plugins/P_RunawayFilter.hpp new file mode 100644 index 0000000..4e5ed1d --- /dev/null +++ b/src/noise-plethora/plugins/P_RunawayFilter.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class RunawayFilter : public NoisePlethoraPlugin { + +public: + + RunawayFilter() { } + + ~RunawayFilter() override {} + + RunawayFilter(const RunawayFilter&) = delete; + RunawayFilter& operator=(const RunawayFilter&) = delete; + + void init() override { + noise1.amplitude(0.5f); + filter1.frequency(1000.0f); + filter1.resonance(4.8f); + filter1.octaveControl(3.0f); + } + + void process(float k1, float k2) override { + float freq = 30.0f + std::pow(k1, 2.0f) * 10000.0f; + float noiseAmp = 0.01f + k2 * 0.99f; + float resonance = 4.5f + (1.0f - k2) * 0.49f; + + noise1.amplitude(noiseAmp); + filter1.frequency(freq); + filter1.resonance(resonance); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + noise1.update(&noiseBlock); + filter1.update(&noiseBlock, nullptr, &lpBlock, &bpBlock, &hpBlock); + + blockBuffer.pushBuffer(bpBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return filter1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthNoiseWhite noise1; + AudioFilterStateVariable filter1; + + audio_block_t noiseBlock; + audio_block_t lpBlock, bpBlock, hpBlock; +}; + +REGISTER_PLUGIN(RunawayFilter); diff --git a/src/noise-plethora/plugins/P_ShapedPulse.hpp b/src/noise-plethora/plugins/P_ShapedPulse.hpp new file mode 100644 index 0000000..7f22982 --- /dev/null +++ b/src/noise-plethora/plugins/P_ShapedPulse.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include "NoisePlethoraPlugin.hpp" + +class ShapedPulse : public NoisePlethoraPlugin { + +public: + + ShapedPulse() { } + + ~ShapedPulse() override {} + + ShapedPulse(const ShapedPulse&) = delete; + ShapedPulse& operator=(const ShapedPulse&) = delete; + + void init() override { + currentValue = 0.0f; + sampleCounter = 0; + samplesPerClock = 441; + shape = 0.0f; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 495.0f; + samplesPerClock = std::max(1, (int)(44100.0f / rate)); + + shape = k2; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + sampleCounter++; + if (sampleCounter >= samplesPerClock) { + float val; + + if (shape < 0.5f) { + // Blend uniform and triangular + float blend = shape * 2.0f; + float uniform = random::uniform() * 2.0f - 1.0f; + float tri = (random::uniform() + random::uniform()) - 1.0f; + val = uniform * (1.0f - blend) + tri * blend; + } + else { + // Blend triangular and peaked (pseudo-gaussian) + float blend = (shape - 0.5f) * 2.0f; + float tri = (random::uniform() + random::uniform()) - 1.0f; + float peaked = (random::uniform() + random::uniform() + random::uniform() + random::uniform()) * 0.5f - 1.0f; + val = tri * (1.0f - blend) + peaked * blend; + } + + if (val > 1.0f) val = 1.0f; + if (val < -1.0f) val = -1.0f; + + currentValue = val; + sampleCounter = 0; + } + + // NaN/Inf guard + if (!std::isfinite(currentValue)) { + currentValue = 0.0f; + } + + float out = currentValue + (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; + + audio_block_t outputBlock; + + float currentValue = 0.0f; + int sampleCounter = 0; + int samplesPerClock = 441; + float shape = 0.0f; +}; + +REGISTER_PLUGIN(ShapedPulse); diff --git a/src/noise-plethora/plugins/P_ShiftPulse.hpp b/src/noise-plethora/plugins/P_ShiftPulse.hpp new file mode 100644 index 0000000..64327ea --- /dev/null +++ b/src/noise-plethora/plugins/P_ShiftPulse.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include "NoisePlethoraPlugin.hpp" + +class ShiftPulse : public NoisePlethoraPlugin { + +public: + + ShiftPulse() { } + + ~ShiftPulse() override {} + + ShiftPulse(const ShiftPulse&) = delete; + ShiftPulse& operator=(const ShiftPulse&) = delete; + + void init() override { + for (int i = 0; i < 4; i++) { + stages[i] = 0.0f; + stageWeights[i] = 0.0f; + } + stageWeights[0] = 1.0f; + sampleCounter = 0; + samplesPerClock = 441; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 295.0f; + samplesPerClock = std::max(1, (int)(44100.0f / rate)); + + // Crossfade stage weights based on k2 + stageWeights[0] = 1.0f; + stageWeights[1] = std::min(1.0f, k2 * 3.0f); + stageWeights[2] = std::min(1.0f, std::max(0.0f, k2 * 3.0f - 1.0f)); + stageWeights[3] = std::min(1.0f, std::max(0.0f, k2 * 3.0f - 2.0f)); + + // Normalize weights + float sum = stageWeights[0] + stageWeights[1] + stageWeights[2] + stageWeights[3]; + if (sum > 0.0f) { + for (int i = 0; i < 4; i++) { + stageWeights[i] /= sum; + } + } + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + sampleCounter++; + if (sampleCounter >= samplesPerClock) { + // Shift register: shift down + stages[3] = stages[2]; + stages[2] = stages[1]; + stages[1] = stages[0]; + stages[0] = random::uniform() * 2.0f - 1.0f; + + sampleCounter = 0; + } + + float out = 0.0f; + for (int s = 0; s < 4; s++) { + out += stageWeights[s] * stages[s]; + } + + // NaN/Inf guard + if (!std::isfinite(out)) { + out = 0.0f; + } + + out += (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; + + audio_block_t outputBlock; + + float stages[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + float stageWeights[4] = {1.0f, 0.0f, 0.0f, 0.0f}; + int sampleCounter = 0; + int samplesPerClock = 441; +}; + +REGISTER_PLUGIN(ShiftPulse); diff --git a/src/noise-plethora/plugins/P_StochasticPulse.hpp b/src/noise-plethora/plugins/P_StochasticPulse.hpp new file mode 100644 index 0000000..1371aed --- /dev/null +++ b/src/noise-plethora/plugins/P_StochasticPulse.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class StochasticPulse : public NoisePlethoraPlugin { + +public: + + StochasticPulse() { } + + ~StochasticPulse() override {} + + StochasticPulse(const StochasticPulse&) = delete; + StochasticPulse& operator=(const StochasticPulse&) = delete; + + void init() override { + density = 50.0f; + lastPulseSign = false; + dryMix = 0.3f; + + filter1.frequency(1000); + filter1.resonance(3.5f); // lower Q = shorter ring, more percussive + filter1.octaveControl(1.0f); + } + + void process(float k1, float k2) override { + density = 5.0f + pow(k1, 2) * 500.0f; + float filterFreq = 80.0f + pow(k2, 2) * 8000.0f; + filter1.frequency(filterFreq); + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + // Generate stochastic impulses + float probability = density / 44100.0f; + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + if (random::uniform() < probability) { + lastPulseSign = !lastPulseSign; + impulseBlock.data[i] = lastPulseSign ? 32767 : -32767; + } + else { + impulseBlock.data[i] = 0; + } + } + + // Filter the impulses + mix dry clicks for percussive attack + filter1.update(&impulseBlock, nullptr, &lpBlock, &bpBlock, &hpBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + // Mix: dry click + resonant ring + int32_t mixed = (int32_t)(impulseBlock.data[i] * dryMix) + bpBlock.data[i]; + if (mixed > 32767) mixed = 32767; + if (mixed < -32767) mixed = -32767; + bpBlock.data[i] = (int16_t)mixed; + } + + blockBuffer.pushBuffer(bpBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return filter1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioFilterStateVariable filter1; + + audio_block_t impulseBlock, lpBlock, bpBlock, hpBlock; + + float density = 50.0f; + float dryMix = 0.3f; + bool lastPulseSign = false; +}; + +REGISTER_PLUGIN(StochasticPulse); diff --git a/src/noise-plethora/plugins/P_SubHarmonic.hpp b/src/noise-plethora/plugins/P_SubHarmonic.hpp new file mode 100644 index 0000000..c936670 --- /dev/null +++ b/src/noise-plethora/plugins/P_SubHarmonic.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class SubHarmonic : public NoisePlethoraPlugin { + +public: + + SubHarmonic() { } + + ~SubHarmonic() override {} + + SubHarmonic(const SubHarmonic&) = delete; + SubHarmonic& operator=(const SubHarmonic&) = delete; + + void init() override { + source.begin(1.0f, 440.0f, WAVEFORM_SQUARE); + + for (int i = 0; i < 4; i++) { + divCounters[i] = 0; + divStates[i] = false; + } + + divWeights[0] = 1.0f; + divWeights[1] = 0.0f; + divWeights[2] = 0.0f; + divWeights[3] = 0.0f; + + prevSamplePositive = true; + } + + void process(float k1, float k2) override { + float freq = 100.0f + std::pow(k1, 2.0f) * 4000.0f; + source.frequency(freq); + + // Crossfade subharmonic levels based on k2 + divWeights[0] = 1.0f; // div2 always active + divWeights[1] = std::min(1.0f, std::max(0.0f, k2 * 3.0f)); // div3 + divWeights[2] = std::min(1.0f, std::max(0.0f, k2 * 3.0f - 1.0f)); // div5 + divWeights[3] = std::min(1.0f, std::max(0.0f, k2 * 3.0f - 2.0f)); // div7 + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + source.update(&srcBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + int16_t srcSample = srcBlock.data[i]; + bool currentPositive = (srcSample >= 0); + + // Detect zero crossing (sign change) + if (currentPositive != prevSamplePositive) { + for (int d = 0; d < 4; d++) { + divCounters[d]++; + if (divCounters[d] >= divRatios[d]) { + divStates[d] = !divStates[d]; + divCounters[d] = 0; + } + } + } + prevSamplePositive = currentPositive; + + // Mix: source + subharmonics, normalized to prevent clipping + float srcFloat = (float)srcSample / 32767.0f; + float totalWeight = 0.15f; + for (int d = 0; d < 4; d++) totalWeight += divWeights[d] * 0.35f; + float norm = 0.9f / totalWeight; + + float mix = 0.15f * norm * srcFloat; + for (int d = 0; d < 4; d++) { + float divValue = divStates[d] ? 0.35f : -0.35f; + mix += divWeights[d] * norm * divValue; + } + + int32_t out = (int32_t)(mix * 32767.0f); + if (out > 32767) out = 32767; + if (out < -32767) out = -32767; + outputBlock.data[i] = (int16_t)out; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return source; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveform source; + + audio_block_t srcBlock; + audio_block_t outputBlock; + + int divRatios[4] = {2, 3, 5, 7}; + int divCounters[4] = {0, 0, 0, 0}; + bool divStates[4] = {false, false, false, false}; + float divWeights[4] = {1.0f, 0.0f, 0.0f, 0.0f}; + + bool prevSamplePositive = true; +}; + +REGISTER_PLUGIN(SubHarmonic); diff --git a/src/noise-plethora/plugins/P_TubeResonance.hpp b/src/noise-plethora/plugins/P_TubeResonance.hpp new file mode 100644 index 0000000..71f3427 --- /dev/null +++ b/src/noise-plethora/plugins/P_TubeResonance.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include "NoisePlethoraPlugin.hpp" + +class TubeResonance : public NoisePlethoraPlugin { + +public: + + TubeResonance() { } + + ~TubeResonance() override {} + + // delete copy constructors + TubeResonance(const TubeResonance&) = delete; + TubeResonance& operator=(const TubeResonance&) = delete; + + void init() override { + noise1.amplitude(1); + + for (int c = 0; c < 4; c++) { + writePos[c] = 0; + for (int i = 0; i < 2048; i++) { + buf[c][i] = 0.0f; + } + } + fundamentalFreq = 200.0f; + feedback = 0.85f; + excitation = 0.5f; + } + + void process(float k1, float k2) override { + fundamentalFreq = 40.0f + pow(k1, 2) * 960.0f; + excitation = k2; + feedback = 0.7f + (1.0f - k2) * 0.25f; + + // set noise amplitude proportional to excitation + noise1.amplitude(excitation); + + // compute delay lengths for 4 harmonics + for (int c = 0; c < 4; c++) { + float harmonic = (float)(c + 1); + float freq = fundamentalFreq * harmonic; + float delaySamples = 44100.0f / freq; + delayLen[c] = (int)delaySamples; + if (delayLen[c] < 1) delayLen[c] = 1; + if (delayLen[c] > 2047) delayLen[c] = 2047; + + // feedback per harmonic scaled by 1/harmonic_number + harmonicFeedback[c] = feedback / harmonic; + } + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + // generate noise block + noise1.update(&noiseBlock); + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + float input = noiseBlock.data[i] / 32767.0f; + float mixed = 0.0f; + + // process 4 comb filters in parallel + for (int c = 0; c < 4; c++) { + int readPos = writePos[c] - delayLen[c]; + if (readPos < 0) readPos += 2048; + + float delayOut = buf[c][readPos]; + float out = input + harmonicFeedback[c] * delayOut; + buf[c][writePos[c]] = out; + + writePos[c] = (writePos[c] + 1) & 2047; // mod 2048 + + mixed += out; + } + + // mix 4 combs equally + mixed *= 0.25f; + + // clamp and convert to int16 + if (mixed > 1.0f) mixed = 1.0f; + if (mixed < -1.0f) mixed = -1.0f; + outputBlock.data[i] = (int16_t)(mixed * 32767.0f); + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthNoiseWhite noise1; + AudioSynthWaveformDc dc1; // dummy for getStream() + + audio_block_t noiseBlock, outputBlock; + + float buf[4][2048] = {}; + int writePos[4] = {}; + int delayLen[4] = {100, 50, 33, 25}; + float harmonicFeedback[4] = {}; + float fundamentalFreq = 200.0f; + float feedback = 0.85f; + float excitation = 0.5f; +}; + +REGISTER_PLUGIN(TubeResonance); // this is important, so that we can include the plugin in a bank diff --git a/src/noise-plethora/plugins/P_TwinPulse.hpp b/src/noise-plethora/plugins/P_TwinPulse.hpp new file mode 100644 index 0000000..90ad90e --- /dev/null +++ b/src/noise-plethora/plugins/P_TwinPulse.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include "NoisePlethoraPlugin.hpp" + +class TwinPulse : public NoisePlethoraPlugin { + +public: + + TwinPulse() { } + + ~TwinPulse() override {} + + TwinPulse(const TwinPulse&) = delete; + TwinPulse& operator=(const TwinPulse&) = delete; + + void init() override { + currentA = random::uniform() * 2.0f - 1.0f; + targetA = random::uniform() * 2.0f - 1.0f; + currentB = random::uniform() * 2.0f - 1.0f; + targetB = random::uniform() * 2.0f - 1.0f; + counterA = 0; + counterB = 0; + alpha = 0.1f; + correlation = 0.0f; + samplesPerTarget = 441; + } + + void process(float k1, float k2) override { + float rate = 5.0f + std::pow(k1, 2) * 395.0f; + samplesPerTarget = std::max(1, (int)(44100.0f / rate)); + + alpha = 4.0f / (float)samplesPerTarget; + if (alpha > 0.5f) alpha = 0.5f; + + correlation = k2; + } + + void processGraphAsBlock(TeensyBuffer& blockBuffer) override { + + for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + counterA++; + if (counterA >= samplesPerTarget) { + targetA = random::uniform() * 2.0f - 1.0f; + counterA = 0; + } + + counterB++; + if (counterB >= samplesPerTarget) { + targetB = random::uniform() * 2.0f - 1.0f; + counterB = 0; + } + + currentA = currentA * (1.0f - alpha) + targetA * alpha; + currentB = currentB * (1.0f - alpha) + targetB * alpha; + + // NaN/Inf guard + if (!std::isfinite(currentA)) currentA = 0.0f; + if (!std::isfinite(currentB)) currentB = 0.0f; + + float blendedB = correlation * currentA + (1.0f - correlation) * currentB; + float out = 0.5f * currentA + 0.5f * blendedB; + + out += (random::uniform() - 0.5f) * 0.01f; + + if (out > 1.0f) out = 1.0f; + if (out < -1.0f) out = -1.0f; + + int32_t sample = (int32_t)(out * 32767.0f); + if (sample > 32767) sample = 32767; + if (sample < -32767) sample = -32767; + outputBlock.data[i] = (int16_t)sample; + } + + blockBuffer.pushBuffer(outputBlock.data, AUDIO_BLOCK_SAMPLES); + } + + AudioStream& getStream() override { + return dc1; + } + unsigned char getPort() override { + return 0; + } + +private: + AudioSynthWaveformDc dc1; + + audio_block_t outputBlock; + + float currentA = 0.0f; + float targetA = 0.0f; + float currentB = 0.0f; + float targetB = 0.0f; + int counterA = 0; + int counterB = 0; + int samplesPerTarget = 441; + float alpha = 0.1f; + float correlation = 0.0f; +}; + +REGISTER_PLUGIN(TwinPulse);