Browse Source

Sample rate normalised most noise powers

Updated the gain of white, pink, red, violet, and blue noise sources so that they depend on sample rate, and the perceived loudness of each source is independent as much as possible between sample rates. Left grey noise alone since I don't understand the algorithm, but it also needs fixing for sample rate independence.
pull/143/head
Andrew Simper 3 years ago
parent
commit
5b42c0c99a
1 changed files with 69 additions and 24 deletions
  1. +69
    -24
      src/Noise.cpp

+ 69
- 24
src/Noise.cpp View File

@@ -4,20 +4,21 @@
/** Based on "The Voss algorithm" /** Based on "The Voss algorithm"
http://www.firstpr.com.au/dsp/pink-noise/ http://www.firstpr.com.au/dsp/pink-noise/
*/ */
template <int QUALITY = 8>
template <int MAX_OCTAVES = 16>
struct PinkNoiseGenerator { struct PinkNoiseGenerator {
int frame = -1; int frame = -1;
float values[QUALITY] = {};
float values[MAX_OCTAVES] = {};
int octaves = 8;


float process() { float process() {
int lastFrame = frame; int lastFrame = frame;
frame++; frame++;
if (frame >= (1 << QUALITY))
if (frame >= (1 << octaves))
frame = 0; frame = 0;
int diff = lastFrame ^ frame; int diff = lastFrame ^ frame;


float sum = 0.f; float sum = 0.f;
for (int i = 0; i < QUALITY; i++) {
for (int i = 0; i < octaves; i++) {
if (diff & (1 << i)) { if (diff & (1 << i)) {
values[i] = random::uniform() - 0.5f; values[i] = random::uniform() - 0.5f;
} }
@@ -38,7 +39,7 @@ struct InverseAWeightingFFTFilter {


InverseAWeightingFFTFilter() : fft(BUFFER_LEN) {} InverseAWeightingFFTFilter() : fft(BUFFER_LEN) {}


float process(float deltaTime, float x) {
float process(float sampleRate, float x) {
inputBuffer[frame] = x; inputBuffer[frame] = x;
if (++frame >= BUFFER_LEN) { if (++frame >= BUFFER_LEN) {
frame = 0; frame = 0;
@@ -46,7 +47,7 @@ struct InverseAWeightingFFTFilter {
fft.rfft(inputBuffer, freqBuffer); fft.rfft(inputBuffer, freqBuffer);


for (int i = 0; i < BUFFER_LEN; i++) { 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; float amp = 0.f;
if (80.f <= f && f <= 20000.f) { if (80.f <= f && f <= 20000.f) {
float f2 = f * f; float f2 = f * f;
@@ -64,6 +65,12 @@ struct InverseAWeightingFFTFilter {
}; };




template <typename T>
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 { struct Noise : Module {
@@ -88,10 +95,18 @@ struct Noise : Module {
}; };


dsp::ClockDivider blackDivider; 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> redFilter;
dsp::IIRFilter<2, 2> violetFilter;
dsp::IIRFilter<2, 2> blueFilter;
float lastWhite = 0.f; float lastWhite = 0.f;
float lastPink = 0.f; float lastPink = 0.f;
float gainWhite = 0.f;
float gainWhiteDifferenced = 0.f;
float gainPink = 0.f;
float gainPinkDifferenced = 0.f;
InverseAWeightingFFTFilter grayFilter; InverseAWeightingFFTFilter grayFilter;


// For calibrating levels // For calibrating levels
@@ -114,52 +129,82 @@ struct Noise : Module {
configOutput(BLACK_OUTPUT, "Black noise"); configOutput(BLACK_OUTPUT, "Black noise");
outputInfos[BLACK_OUTPUT]->description = "Uniform random numbers"; 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);
SampleRateChangeEvent e;
e.sampleRate = 44100.f;
e.sampleTime = 1.f/44100.f;
onSampleRateChange(e);
} }


void process(const ProcessArgs& args) override {
void onSampleRateChange(const SampleRateChangeEvent& e) override {
// All noise is calibrated to 1 RMS. // All noise is calibrated to 1 RMS.
// Then they should be scaled to match the RMS of a sine wave with 5V amplitude. // 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()) { if (outputs[WHITE_OUTPUT].isConnected() || outputs[RED_OUTPUT].isConnected() || outputs[VIOLET_OUTPUT].isConnected() || outputs[GRAY_OUTPUT].isConnected()) {
// White noise: equal power density // 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 // Red/Brownian noise: -6dB/oct
if (outputs[RED_OUTPUT].isConnected()) { if (outputs[RED_OUTPUT].isConnected()) {
float red = redFilter.process(white) / 0.0645f; 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 // Violet/purple noise: 6dB/oct
if (outputs[VIOLET_OUTPUT].isConnected()) { if (outputs[VIOLET_OUTPUT].isConnected()) {
float violet = (white - lastWhite) / 1.41f;
float violet = violetFilter.process((white - lastWhite) / 1.41f);
lastWhite = white; 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 // Gray noise: psychoacoustic equal loudness curve, specifically inverted A-weighted
if (outputs[GRAY_OUTPUT].isConnected()) { 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()) { if (outputs[PINK_OUTPUT].isConnected() || outputs[BLUE_OUTPUT].isConnected()) {
// Pink noise: -3dB/oct // 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 // Blue noise: 3dB/oct
if (outputs[BLUE_OUTPUT].isConnected()) { if (outputs[BLUE_OUTPUT].isConnected()) {
float blue = (pink - lastPink) / 0.705f;
float blue = blueFilter.process((pink - lastPink) / 0.705f);
lastPink = pink; lastPink = pink;
outputs[BLUE_OUTPUT].setVoltage(blue * gain);
outputs[BLUE_OUTPUT].setVoltage(softclip(blue * gainPinkDifferenced));
} }
} }




Loading…
Cancel
Save