diff --git a/src/Noise.cpp b/src/Noise.cpp index d6c6bc9..af0e874 100644 --- a/src/Noise.cpp +++ b/src/Noise.cpp @@ -1,23 +1,27 @@ +/** Cytomic mod by Andy +https://github.com/VCVRack/Fundamental/pull/143 +*/ #include "plugin.hpp" /** Based on "The Voss algorithm" http://www.firstpr.com.au/dsp/pink-noise/ */ -template +template struct PinkNoiseGenerator { int frame = -1; - float values[QUALITY] = {}; + float values[MAX_OCTAVES] = {}; + int octaves = 8; float process() { int lastFrame = frame; frame++; - if (frame >= (1 << QUALITY)) + if (frame >= (1 << octaves)) frame = 0; int diff = lastFrame ^ frame; float sum = 0.f; - for (int i = 0; i < QUALITY; i++) { + for (int i = 0; i < octaves; i++) { if (diff & (1 << i)) { values[i] = random::uniform() - 0.5f; } @@ -38,7 +42,7 @@ struct InverseAWeightingFFTFilter { InverseAWeightingFFTFilter() : fft(BUFFER_LEN) {} - float process(float deltaTime, float x) { + float process(float sampleRate, float x) { inputBuffer[frame] = x; if (++frame >= BUFFER_LEN) { frame = 0; @@ -46,7 +50,7 @@ struct InverseAWeightingFFTFilter { fft.rfft(inputBuffer, freqBuffer); for (int i = 0; i < BUFFER_LEN; i++) { - float f = 1 / deltaTime / 2 / BUFFER_LEN * i; + float f = sampleRate / 2 / BUFFER_LEN * i; float amp = 0.f; if (80.f <= f && f <= 20000.f) { float f2 = f * f; @@ -64,6 +68,12 @@ struct InverseAWeightingFFTFilter { }; +template +static T softclip(T x) { + // rational approximation of 12*tanh(x/12) with derivative 0 at x = 24 + x = simd::clamp(x, -24.f, 24.f); + return x * (3888 + x * x) / (3888 + 9 * x * x); +} struct Noise : Module { @@ -88,10 +98,18 @@ struct Noise : Module { }; dsp::ClockDivider blackDivider; - PinkNoiseGenerator<8> pinkNoiseGenerator; + PinkNoiseGenerator<16> pinkNoiseGenerator; + dsp::IIRFilter<2, 2> whiteFilter; + dsp::IIRFilter<2, 2> pinkFilter; dsp::IIRFilter<2, 2> redFilter; + dsp::IIRFilter<2, 2> violetFilter; + dsp::IIRFilter<2, 2> blueFilter; float lastWhite = 0.f; float lastPink = 0.f; + float gainWhite = 0.f; + float gainWhiteDifferenced = 0.f; + float gainPink = 0.f; + float gainPinkDifferenced = 0.f; InverseAWeightingFFTFilter grayFilter; // For calibrating levels @@ -113,53 +131,78 @@ struct Noise : Module { outputInfos[GRAY_OUTPUT]->description = "Psychoacoustic equal loudness"; configOutput(BLACK_OUTPUT, "Black noise"); outputInfos[BLACK_OUTPUT]->description = "Uniform random numbers"; - - // Hard-code coefficients for Butterworth lowpass with cutoff 20 Hz @ 44.1kHz. - const float b[] = {0.00425611, 0.00425611}; - const float a[] = {-0.99148778}; - redFilter.setCoefficients(b, a); } - void process(const ProcessArgs& args) override { + void onSampleRateChange(const SampleRateChangeEvent& e) override { // All noise is calibrated to 1 RMS. // Then they should be scaled to match the RMS of a sine wave with 5V amplitude. - const float gain = 5.f / std::sqrt(2.f); + // Also normalised by bandwidth of sample rate + gainWhite = 5.f * std::sqrt(e.sampleRate)/(std::sqrt(44100.f)*std::sqrt(2.f)); + gainWhiteDifferenced = gainWhite*e.sampleRate/44100.f; + gainPink = 5.f/std::sqrt(2.f); + gainPinkDifferenced = gainPink*e.sampleRate/44100.f; + + // DF1 Butterworth lowpass with cutoff 18000 Hz to keep amplitude down + { + const float g = std::tan(M_PI*fmin(18000.f*e.sampleTime, 0.49f)); + const float gnrm = 1.f/(1.f + g); + const float b[] = {g*gnrm, g*gnrm}; + const float a[] = {(g - 1.f)*gnrm}; + whiteFilter.setCoefficients(b, a); + pinkFilter.setCoefficients(b, a); + violetFilter.setCoefficients(b, a); + blueFilter.setCoefficients(b, a); + } + + // DF1 Butterworth lowpass with cutoff 60 Hz + { + const float g = std::tan(M_PI*fmin(60.f*e.sampleTime, 0.49f)); + const float gnrm = 1.f/(1.f + g); + const float b[] = {g*gnrm, g*gnrm}; + const float a[] = {(g - 1.f)*gnrm}; + redFilter.setCoefficients(b, a); + } + const int octaves = clamp(int(std::log2(e.sampleRate) + 1) - 8, 4, 16); + pinkNoiseGenerator.octaves = octaves; + } + + void process(const ProcessArgs& args) override { if (outputs[WHITE_OUTPUT].isConnected() || outputs[RED_OUTPUT].isConnected() || outputs[VIOLET_OUTPUT].isConnected() || outputs[GRAY_OUTPUT].isConnected()) { // White noise: equal power density - float white = random::normal(); - outputs[WHITE_OUTPUT].setVoltage(white * gain); + float white = whiteFilter.process(random::normal()); + outputs[WHITE_OUTPUT].setVoltage(softclip(white * gainWhite)); // Red/Brownian noise: -6dB/oct if (outputs[RED_OUTPUT].isConnected()) { float red = redFilter.process(white) / 0.0645f; - outputs[RED_OUTPUT].setVoltage(red * gain); + outputs[RED_OUTPUT].setVoltage(softclip(red * gainWhite)); } // Violet/purple noise: 6dB/oct if (outputs[VIOLET_OUTPUT].isConnected()) { - float violet = (white - lastWhite) / 1.41f; + float violet = violetFilter.process((white - lastWhite) / 1.41f); lastWhite = white; - outputs[VIOLET_OUTPUT].setVoltage(violet * gain); + outputs[VIOLET_OUTPUT].setVoltage(softclip(violet * gainWhiteDifferenced)); } // Gray noise: psychoacoustic equal loudness curve, specifically inverted A-weighted if (outputs[GRAY_OUTPUT].isConnected()) { - float gray = grayFilter.process(args.sampleTime, white) / 1.67f; - outputs[GRAY_OUTPUT].setVoltage(gray * gain); + float gray = grayFilter.process(args.sampleRate, white) / 1.67f; + outputs[GRAY_OUTPUT].setVoltage(softclip(gray * gainWhite)); } } if (outputs[PINK_OUTPUT].isConnected() || outputs[BLUE_OUTPUT].isConnected()) { // Pink noise: -3dB/oct - float pink = pinkNoiseGenerator.process() / 0.816f; - outputs[PINK_OUTPUT].setVoltage(pink * gain); + float pink = pinkFilter.process(pinkNoiseGenerator.process() / 0.816f); + outputs[PINK_OUTPUT].setVoltage(softclip(pink * gainPink)); // Blue noise: 3dB/oct if (outputs[BLUE_OUTPUT].isConnected()) { - float blue = (pink - lastPink) / 0.705f; + float blue = blueFilter.process((pink - lastPink) / 0.705f); lastPink = pink; - outputs[BLUE_OUTPUT].setVoltage(blue * gain); + outputs[BLUE_OUTPUT].setVoltage(softclip(blue * gainPinkDifferenced)); } } @@ -180,7 +223,6 @@ struct NoiseWidget : ModuleWidget { NoiseWidget(Noise* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/Noise.svg"), asset::plugin(pluginInstance, "res/Noise-dark.svg"))); - addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));