13 Commits
dev ... triplex

Author SHA1 Message Date
6ed946aa48 initial implementation of sample data type with variable bit depth 2025-04-26 16:59:02 +02:00
e4a489ae48 Merge branch 'triplex' 2025-04-26 16:07:50 +02:00
5baec48c28 two wavefolding algorithms 2024-10-03 08:26:11 +02:00
bec6599f35 increase ring modulated output 2024-10-02 18:18:28 +02:00
2476bab44f add glide 2024-10-01 19:16:40 +02:00
023b8192a5 fix envelope calculation 2024-10-01 11:51:32 +02:00
5afc5ff18a nicer code 2024-10-01 11:23:03 +02:00
9b3e4deb30 add ring modulation capabilities 2024-10-01 08:42:20 +02:00
4dbf88afcb add destinations for each operator 2024-09-30 11:51:24 +02:00
22eea12b17 add wavefolder and envelope + velocity toggles 2024-09-30 11:50:32 +02:00
3ec3204cd8 pass sample by reference 2024-09-30 08:21:12 +02:00
e5e156ce32 add averager 2024-09-30 08:20:19 +02:00
9485c74961 add wavefolder 2024-09-30 07:03:51 +02:00
48 changed files with 4422 additions and 4468 deletions

View File

@@ -1,6 +1,7 @@
---
BasedOnStyle: LLVM
ColumnLimit: "90"
ColumnLimit: "120"
ConstructorInitializerIndentWidth: "0"
IndentWidth: "4"
TabWidth: "4"
UseTab: Always

View File

@@ -11,5 +11,4 @@ target_include_directories(trnr-lib INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/oversampling
${CMAKE_CURRENT_SOURCE_DIR}/synth
${CMAKE_CURRENT_SOURCE_DIR}/util
${CMAKE_CURRENT_SOURCE_DIR}/gfx
)

View File

@@ -1,6 +1,6 @@
The MIT License
Copyright 2025, Christopher Herb. All rights reserved.
Copyright 2024, Christopher Jan Herb. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -1,3 +1,3 @@
# trnr-lib
header-only dsp functions and utilities
A collection of header only classes used in the development of software by Ternär Music Technology.

123
clip/aw_cliponly2.h Normal file
View File

@@ -0,0 +1,123 @@
#pragma once
#include <cmath>
#include <cstdlib>
namespace trnr {
// Clipper based on ClipOnly2 by Chris Johnson
class aw_cliponly2 {
public:
aw_cliponly2()
{
samplerate = 44100;
lastSampleL = 0.0;
wasPosClipL = false;
wasNegClipL = false;
lastSampleR = 0.0;
wasPosClipR = false;
wasNegClipR = false;
for (int x = 0; x < 16; x++) {
intermediateL[x] = 0.0;
intermediateR[x] = 0.0;
}
// this is reset: values being initialized only once. Startup values, whatever they are.
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
template <typename t_sample>
void process_block(t_sample** inputs, t_sample** outputs, long sample_frames)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
int spacing = floor(overallscale); // should give us working basic scaling, usually 2 or 4
if (spacing < 1) spacing = 1;
if (spacing > 16) spacing = 16;
while (--sample_frames >= 0) {
double inputSampleL = *in1;
double inputSampleR = *in2;
// begin ClipOnly2 stereo as a little, compressed chunk that can be dropped into code
if (inputSampleL > 4.0) inputSampleL = 4.0;
if (inputSampleL < -4.0) inputSampleL = -4.0;
if (wasPosClipL == true) { // current will be over
if (inputSampleL < lastSampleL) lastSampleL = 0.7058208 + (inputSampleL * 0.2609148);
else lastSampleL = 0.2491717 + (lastSampleL * 0.7390851);
}
wasPosClipL = false;
if (inputSampleL > 0.9549925859) {
wasPosClipL = true;
inputSampleL = 0.7058208 + (lastSampleL * 0.2609148);
}
if (wasNegClipL == true) { // current will be -over
if (inputSampleL > lastSampleL) lastSampleL = -0.7058208 + (inputSampleL * 0.2609148);
else lastSampleL = -0.2491717 + (lastSampleL * 0.7390851);
}
wasNegClipL = false;
if (inputSampleL < -0.9549925859) {
wasNegClipL = true;
inputSampleL = -0.7058208 + (lastSampleL * 0.2609148);
}
intermediateL[spacing] = inputSampleL;
inputSampleL = lastSampleL; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) intermediateL[x - 1] = intermediateL[x];
lastSampleL = intermediateL[0]; // run a little buffer to handle this
if (inputSampleR > 4.0) inputSampleR = 4.0;
if (inputSampleR < -4.0) inputSampleR = -4.0;
if (wasPosClipR == true) { // current will be over
if (inputSampleR < lastSampleR) lastSampleR = 0.7058208 + (inputSampleR * 0.2609148);
else lastSampleR = 0.2491717 + (lastSampleR * 0.7390851);
}
wasPosClipR = false;
if (inputSampleR > 0.9549925859) {
wasPosClipR = true;
inputSampleR = 0.7058208 + (lastSampleR * 0.2609148);
}
if (wasNegClipR == true) { // current will be -over
if (inputSampleR > lastSampleR) lastSampleR = -0.7058208 + (inputSampleR * 0.2609148);
else lastSampleR = -0.2491717 + (lastSampleR * 0.7390851);
}
wasNegClipR = false;
if (inputSampleR < -0.9549925859) {
wasNegClipR = true;
inputSampleR = -0.7058208 + (lastSampleR * 0.2609148);
}
intermediateR[spacing] = inputSampleR;
inputSampleR = lastSampleR; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) intermediateR[x - 1] = intermediateR[x];
lastSampleR = intermediateR[0]; // run a little buffer to handle this
// end ClipOnly2 stereo as a little, compressed chunk that can be dropped into code
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
double lastSampleL;
double intermediateL[16];
bool wasPosClipL;
bool wasNegClipL;
double lastSampleR;
double intermediateR[16];
bool wasPosClipR;
bool wasNegClipR; // Stereo ClipOnly2
// default stuff
};
} // namespace trnr

109
clip/aw_clipsoftly.h Normal file
View File

@@ -0,0 +1,109 @@
#pragma once
#include <cstdlib>
#include <stdint.h>
namespace trnr {
// soft clipper based on ClipSoftly by Chris Johnson
class aw_clipsoftly {
public:
aw_clipsoftly()
{
samplerate = 44100;
lastSampleL = 0.0;
lastSampleR = 0.0;
for (int x = 0; x < 16; x++) {
intermediateL[x] = 0.0;
intermediateR[x] = 0.0;
}
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
// this is reset: values being initialized only once. Startup values, whatever they are.
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
template <typename t_sample>
void process_block(t_sample** inputs, t_sample** outputs, long sample_frames)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
int spacing = floor(overallscale); // should give us working basic scaling, usually 2 or 4
if (spacing < 1) spacing = 1;
if (spacing > 16) spacing = 16;
while (--sample_frames >= 0) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double softSpeed = fabs(inputSampleL);
if (softSpeed < 1.0) softSpeed = 1.0;
else softSpeed = 1.0 / softSpeed;
if (inputSampleL > 1.57079633) inputSampleL = 1.57079633;
if (inputSampleL < -1.57079633) inputSampleL = -1.57079633;
inputSampleL = sin(inputSampleL) * 0.9549925859; // scale to what cliponly uses
inputSampleL = (inputSampleL * softSpeed) + (lastSampleL * (1.0 - softSpeed));
softSpeed = fabs(inputSampleR);
if (softSpeed < 1.0) softSpeed = 1.0;
else softSpeed = 1.0 / softSpeed;
if (inputSampleR > 1.57079633) inputSampleR = 1.57079633;
if (inputSampleR < -1.57079633) inputSampleR = -1.57079633;
inputSampleR = sin(inputSampleR) * 0.9549925859; // scale to what cliponly uses
inputSampleR = (inputSampleR * softSpeed) + (lastSampleR * (1.0 - softSpeed));
intermediateL[spacing] = inputSampleL;
inputSampleL = lastSampleL; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) intermediateL[x - 1] = intermediateL[x];
lastSampleL = intermediateL[0]; // run a little buffer to handle this
intermediateR[spacing] = inputSampleR;
inputSampleR = lastSampleR; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) intermediateR[x - 1] = intermediateR[x];
lastSampleR = intermediateR[0]; // run a little buffer to handle this
// begin 64 bit stereo floating point dither
// int expon; frexp((double)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
// inputSampleL += ((double(fpdL)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// frexp((double)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
// inputSampleR += ((double(fpdR)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// end 64 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
double lastSampleL;
double intermediateL[16];
double lastSampleR;
double intermediateR[16];
uint32_t fpdL;
uint32_t fpdR;
// default stuff
};
} // namespace trnr

208
clip/aw_tube2.h Normal file
View File

@@ -0,0 +1,208 @@
#pragma once
#include <cstdlib>
#include <stdint.h>
namespace trnr {
// modeled tube preamp based on tube2 by Chris Johnson
class aw_tube2 {
public:
aw_tube2()
{
samplerate = 44100;
A = 0.5;
B = 0.5;
previousSampleA = 0.0;
previousSampleB = 0.0;
previousSampleC = 0.0;
previousSampleD = 0.0;
previousSampleE = 0.0;
previousSampleF = 0.0;
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
// this is reset: values being initialized only once. Startup values, whatever they are.
}
void set_input(double value) { A = clamp(value); }
void set_tube(double value) { B = clamp(value); }
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
template <typename t_sample>
void process_block(t_sample** inputs, t_sample** outputs, long sampleframes)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
double inputPad = A;
double iterations = 1.0 - B;
int powerfactor = (9.0 * iterations) + 1;
double asymPad = (double)powerfactor;
double gainscaling = 1.0 / (double)(powerfactor + 1);
double outputscaling = 1.0 + (1.0 / (double)(powerfactor));
while (--sampleframes >= 0) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
if (inputPad < 1.0) {
inputSampleL *= inputPad;
inputSampleR *= inputPad;
}
if (overallscale > 1.9) {
double stored = inputSampleL;
inputSampleL += previousSampleA;
previousSampleA = stored;
inputSampleL *= 0.5;
stored = inputSampleR;
inputSampleR += previousSampleB;
previousSampleB = stored;
inputSampleR *= 0.5;
} // for high sample rates on this plugin we are going to do a simple average
if (inputSampleL > 1.0) inputSampleL = 1.0;
if (inputSampleL < -1.0) inputSampleL = -1.0;
if (inputSampleR > 1.0) inputSampleR = 1.0;
if (inputSampleR < -1.0) inputSampleR = -1.0;
// flatten bottom, point top of sine waveshaper L
inputSampleL /= asymPad;
double sharpen = -inputSampleL;
if (sharpen > 0.0) sharpen = 1.0 + sqrt(sharpen);
else sharpen = 1.0 - sqrt(-sharpen);
inputSampleL -= inputSampleL * fabs(inputSampleL) * sharpen * 0.25;
// this will take input from exactly -1.0 to 1.0 max
inputSampleL *= asymPad;
// flatten bottom, point top of sine waveshaper R
inputSampleR /= asymPad;
sharpen = -inputSampleR;
if (sharpen > 0.0) sharpen = 1.0 + sqrt(sharpen);
else sharpen = 1.0 - sqrt(-sharpen);
inputSampleR -= inputSampleR * fabs(inputSampleR) * sharpen * 0.25;
// this will take input from exactly -1.0 to 1.0 max
inputSampleR *= asymPad;
// end first asym section: later boosting can mitigate the extreme
// softclipping of one side of the wave
// and we are asym clipping more when Tube is cranked, to compensate
// original Tube algorithm: powerfactor widens the more linear region of the wave
double factor = inputSampleL; // Left channel
for (int x = 0; x < powerfactor; x++) factor *= inputSampleL;
if ((powerfactor % 2 == 1) && (inputSampleL != 0.0)) factor = (factor / inputSampleL) * fabs(inputSampleL);
factor *= gainscaling;
inputSampleL -= factor;
inputSampleL *= outputscaling;
factor = inputSampleR; // Right channel
for (int x = 0; x < powerfactor; x++) factor *= inputSampleR;
if ((powerfactor % 2 == 1) && (inputSampleR != 0.0)) factor = (factor / inputSampleR) * fabs(inputSampleR);
factor *= gainscaling;
inputSampleR -= factor;
inputSampleR *= outputscaling;
if (overallscale > 1.9) {
double stored = inputSampleL;
inputSampleL += previousSampleC;
previousSampleC = stored;
inputSampleL *= 0.5;
stored = inputSampleR;
inputSampleR += previousSampleD;
previousSampleD = stored;
inputSampleR *= 0.5;
} // for high sample rates on this plugin we are going to do a simple average
// end original Tube. Now we have a boosted fat sound peaking at 0dB exactly
// hysteresis and spiky fuzz L
double slew = previousSampleE - inputSampleL;
if (overallscale > 1.9) {
double stored = inputSampleL;
inputSampleL += previousSampleE;
previousSampleE = stored;
inputSampleL *= 0.5;
} else previousSampleE = inputSampleL; // for this, need previousSampleC always
if (slew > 0.0) slew = 1.0 + (sqrt(slew) * 0.5);
else slew = 1.0 - (sqrt(-slew) * 0.5);
inputSampleL -= inputSampleL * fabs(inputSampleL) * slew * gainscaling;
// reusing gainscaling that's part of another algorithm
if (inputSampleL > 0.52) inputSampleL = 0.52;
if (inputSampleL < -0.52) inputSampleL = -0.52;
inputSampleL *= 1.923076923076923;
// hysteresis and spiky fuzz R
slew = previousSampleF - inputSampleR;
if (overallscale > 1.9) {
double stored = inputSampleR;
inputSampleR += previousSampleF;
previousSampleF = stored;
inputSampleR *= 0.5;
} else previousSampleF = inputSampleR; // for this, need previousSampleC always
if (slew > 0.0) slew = 1.0 + (sqrt(slew) * 0.5);
else slew = 1.0 - (sqrt(-slew) * 0.5);
inputSampleR -= inputSampleR * fabs(inputSampleR) * slew * gainscaling;
// reusing gainscaling that's part of another algorithm
if (inputSampleR > 0.52) inputSampleR = 0.52;
if (inputSampleR < -0.52) inputSampleR = -0.52;
inputSampleR *= 1.923076923076923;
// end hysteresis and spiky fuzz section
// begin 64 bit stereo floating point dither
// int expon; frexp((double)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
// inputSampleL += ((double(fpdL)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// frexp((double)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
// inputSampleR += ((double(fpdR)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// end 64 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
double previousSampleA;
double previousSampleB;
double previousSampleC;
double previousSampleD;
double previousSampleE;
double previousSampleF;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
float A;
float B;
double clamp(double& value)
{
if (value > 1) {
value = 1;
} else if (value < 0) {
value = 0;
}
return value;
}
};
} // namespace trnr

View File

@@ -1,160 +0,0 @@
/*
* clip.h
* Copyright (c) 2016 Chris Johnson
* Copyright (c) 2025 Christopher Herb
* Based on ClipOnly2 by Chris Johnson, 2016
* This file is a derivative of the above module.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Changes:
* - 2025-11-06 Christopher Herb:
* - Templated audio buffer i/o
* - Converted to procedural programming style
*/
#pragma once
#include <cmath>
#include <cstdlib>
namespace trnr {
struct clip {
double samplerate;
double last_sample_l;
double intermediate_l[16];
bool was_pos_clip_l;
bool was_neg_clip_l;
double last_sample_r;
double intermediate_r[16];
bool was_pos_clip_r;
bool was_neg_clip_r;
};
inline void clip_init(clip& c, double _samplerate)
{
c.samplerate = 44100;
c.last_sample_l = 0.0;
c.was_pos_clip_l = false;
c.was_neg_clip_l = false;
c.last_sample_r = 0.0;
c.was_pos_clip_r = false;
c.was_neg_clip_r = false;
for (int x = 0; x < 16; x++) {
c.intermediate_l[x] = 0.0;
c.intermediate_r[x] = 0.0;
}
}
template <typename t_sample>
inline void clip_process_block(clip& c, t_sample** inputs, t_sample** outputs,
long sample_frames)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= c.samplerate;
int spacing =
floor(overallscale); // should give us working basic scaling, usually 2 or 4
if (spacing < 1) spacing = 1;
if (spacing > 16) spacing = 16;
while (--sample_frames >= 0) {
double input_l = *in1;
double input_r = *in2;
// begin ClipOnly2 stereo as a little, compressed chunk that can be dropped into
// code
if (input_l > 4.0) input_l = 4.0;
if (input_l < -4.0) input_l = -4.0;
if (c.was_pos_clip_l == true) { // current will be over
if (input_l < c.last_sample_l)
c.last_sample_l = 0.7058208 + (input_l * 0.2609148);
else c.last_sample_l = 0.2491717 + (c.last_sample_l * 0.7390851);
}
c.was_pos_clip_l = false;
if (input_l > 0.9549925859) {
c.was_pos_clip_l = true;
input_l = 0.7058208 + (c.last_sample_l * 0.2609148);
}
if (c.was_neg_clip_l == true) { // current will be -over
if (input_l > c.last_sample_l)
c.last_sample_l = -0.7058208 + (input_l * 0.2609148);
else c.last_sample_l = -0.2491717 + (c.last_sample_l * 0.7390851);
}
c.was_neg_clip_l = false;
if (input_l < -0.9549925859) {
c.was_neg_clip_l = true;
input_l = -0.7058208 + (c.last_sample_l * 0.2609148);
}
c.intermediate_l[spacing] = input_l;
input_l =
c.last_sample_l; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) c.intermediate_l[x - 1] = c.intermediate_l[x];
c.last_sample_l = c.intermediate_l[0]; // run a little buffer to handle this
if (input_r > 4.0) input_r = 4.0;
if (input_r < -4.0) input_r = -4.0;
if (c.was_pos_clip_r == true) { // current will be over
if (input_r < c.last_sample_r)
c.last_sample_r = 0.7058208 + (input_r * 0.2609148);
else c.last_sample_r = 0.2491717 + (c.last_sample_r * 0.7390851);
}
c.was_pos_clip_r = false;
if (input_r > 0.9549925859) {
c.was_pos_clip_r = true;
input_r = 0.7058208 + (c.last_sample_r * 0.2609148);
}
if (c.was_neg_clip_r == true) { // current will be -over
if (input_r > c.last_sample_r)
c.last_sample_r = -0.7058208 + (input_r * 0.2609148);
else c.last_sample_r = -0.2491717 + (c.last_sample_r * 0.7390851);
}
c.was_neg_clip_r = false;
if (input_r < -0.9549925859) {
c.was_neg_clip_r = true;
input_r = -0.7058208 + (c.last_sample_r * 0.2609148);
}
c.intermediate_r[spacing] = input_r;
input_r =
c.last_sample_r; // Latency is however many samples equals one 44.1k sample
for (int x = spacing; x > 0; x--) c.intermediate_r[x - 1] = c.intermediate_r[x];
c.last_sample_r = c.intermediate_r[0]; // run a little buffer to handle this
// end ClipOnly2 stereo as a little, compressed chunk that can be dropped into
// code
*out1 = input_l;
*out2 = input_r;
in1++;
in2++;
out1++;
out2++;
}
}
} // namespace trnr

View File

@@ -1,63 +0,0 @@
/*
* fold.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
namespace trnr {
// folds the wave from -1 to 1
inline float fold(float& sample)
{
while (sample > 1.0 || sample < -1.0) {
if (sample > 1.0) {
sample = 2.0 - sample;
} else if (sample < -1.0) {
sample = -2.0 - sample;
}
}
return sample;
}
// folds the positive part of the wave independently from the negative part.
inline float fold_bipolar(float& sample)
{
// fold positive values
if (sample > 1.0) {
sample = 2.0 - sample;
if (sample < 0.0) { sample = -sample; }
return fold(sample);
}
// fold negative values
else if (sample < -1.0) {
sample = -2.0 - sample;
if (sample > 0.0) { sample = -sample; }
return fold(sample);
} else {
return sample;
}
}
}; // namespace trnr

View File

@@ -1,231 +0,0 @@
/*
* tube.h
* Copyright (c) 2016 Chris Johnson
* Copyright (c) 2025 Christopher Herb
* Based on Tube2 by Chris Johnson, 2016
* This file is a derivative/major refactor of the above module.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Changes:
* - 2025-11-06 Christopher Herb:
* - Templated audio buffer i/o
* - Converted to procedural programming style
*/
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <stdint.h>
using namespace std;
namespace trnr {
struct tube {
double samplerate;
double prev_sample_a;
double prev_sample_b;
double prev_sample_c;
double prev_sample_d;
double prev_sample_e;
double prev_sample_f;
uint32_t fdp_l;
uint32_t fdp_r;
float input_vol;
float tube_amt;
void set_input(double value) { input_vol = clamp(value, 0.0, 1.0); }
void set_tube(double value) { tube_amt = clamp(value, 0.0, 1.0); }
};
inline void tube_init(tube& t, double samplerate)
{
t.samplerate = 44100;
t.input_vol = 0.5;
t.tube_amt = 0.5;
t.prev_sample_a = 0.0;
t.prev_sample_b = 0.0;
t.prev_sample_c = 0.0;
t.prev_sample_d = 0.0;
t.prev_sample_e = 0.0;
t.prev_sample_f = 0.0;
t.fdp_l = 1.0;
while (t.fdp_l < 16386) t.fdp_l = rand() * UINT32_MAX;
t.fdp_r = 1.0;
while (t.fdp_r < 16386) t.fdp_r = rand() * UINT32_MAX;
}
template <typename t_sample>
inline void tube_process_block(tube& t, t_sample** inputs, t_sample** outputs,
long sampleframes)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= t.samplerate;
double input_pad = t.input_vol;
double iterations = 1.0 - t.tube_amt;
int powerfactor = (9.0 * iterations) + 1;
double asym_pad = (double)powerfactor;
double gainscaling = 1.0 / (double)(powerfactor + 1);
double outputscaling = 1.0 + (1.0 / (double)(powerfactor));
while (--sampleframes >= 0) {
double input_l = *in1;
double input_r = *in2;
if (fabs(input_l) < 1.18e-23) input_l = t.fdp_l * 1.18e-17;
if (fabs(input_r) < 1.18e-23) input_r = t.fdp_r * 1.18e-17;
if (input_pad < 1.0) {
input_l *= input_pad;
input_r *= input_pad;
}
if (overallscale > 1.9) {
double stored = input_l;
input_l += t.prev_sample_a;
t.prev_sample_a = stored;
input_l *= 0.5;
stored = input_r;
input_r += t.prev_sample_b;
t.prev_sample_b = stored;
input_r *= 0.5;
} // for high sample rates on this plugin we are going to do a simple average
if (input_l > 1.0) input_l = 1.0;
if (input_l < -1.0) input_l = -1.0;
if (input_r > 1.0) input_r = 1.0;
if (input_r < -1.0) input_r = -1.0;
// flatten bottom, point top of sine waveshaper L
input_l /= asym_pad;
double sharpen = -input_l;
if (sharpen > 0.0) sharpen = 1.0 + sqrt(sharpen);
else sharpen = 1.0 - sqrt(-sharpen);
input_l -= input_l * fabs(input_l) * sharpen * 0.25;
// this will take input from exactly -1.0 to 1.0 max
input_l *= asym_pad;
// flatten bottom, point top of sine waveshaper R
input_r /= asym_pad;
sharpen = -input_r;
if (sharpen > 0.0) sharpen = 1.0 + sqrt(sharpen);
else sharpen = 1.0 - sqrt(-sharpen);
input_r -= input_r * fabs(input_r) * sharpen * 0.25;
// this will take input from exactly -1.0 to 1.0 max
input_r *= asym_pad;
// end first asym section: later boosting can mitigate the extreme
// softclipping of one side of the wave
// and we are asym clipping more when Tube is cranked, to compensate
// original Tube algorithm: powerfactor widens the more linear region of the wave
double factor = input_l; // Left channel
for (int x = 0; x < powerfactor; x++) factor *= input_l;
if ((powerfactor % 2 == 1) && (input_l != 0.0))
factor = (factor / input_l) * fabs(input_l);
factor *= gainscaling;
input_l -= factor;
input_l *= outputscaling;
factor = input_r; // Right channel
for (int x = 0; x < powerfactor; x++) factor *= input_r;
if ((powerfactor % 2 == 1) && (input_r != 0.0))
factor = (factor / input_r) * fabs(input_r);
factor *= gainscaling;
input_r -= factor;
input_r *= outputscaling;
if (overallscale > 1.9) {
double stored = input_l;
input_l += t.prev_sample_c;
t.prev_sample_c = stored;
input_l *= 0.5;
stored = input_r;
input_r += t.prev_sample_d;
t.prev_sample_d = stored;
input_r *= 0.5;
} // for high sample rates on this plugin we are going to do a simple average
// end original Tube. Now we have a boosted fat sound peaking at 0dB exactly
// hysteresis and spiky fuzz L
double slew = t.prev_sample_e - input_l;
if (overallscale > 1.9) {
double stored = input_l;
input_l += t.prev_sample_e;
t.prev_sample_e = stored;
input_l *= 0.5;
} else t.prev_sample_e = input_l; // for this, need previousSampleC always
if (slew > 0.0) slew = 1.0 + (sqrt(slew) * 0.5);
else slew = 1.0 - (sqrt(-slew) * 0.5);
input_l -= input_l * fabs(input_l) * slew * gainscaling;
// reusing gainscaling that's part of another algorithm
if (input_l > 0.52) input_l = 0.52;
if (input_l < -0.52) input_l = -0.52;
input_l *= 1.923076923076923;
// hysteresis and spiky fuzz R
slew = t.prev_sample_f - input_r;
if (overallscale > 1.9) {
double stored = input_r;
input_r += t.prev_sample_f;
t.prev_sample_f = stored;
input_r *= 0.5;
} else t.prev_sample_f = input_r; // for this, need previousSampleC always
if (slew > 0.0) slew = 1.0 + (sqrt(slew) * 0.5);
else slew = 1.0 - (sqrt(-slew) * 0.5);
input_r -= input_r * fabs(input_r) * slew * gainscaling;
// reusing gainscaling that's part of another algorithm
if (input_r > 0.52) input_r = 0.52;
if (input_r < -0.52) input_r = -0.52;
input_r *= 1.923076923076923;
// end hysteresis and spiky fuzz section
// begin 64 bit stereo floating point dither
// int expon; frexp((double)inputSampleL, &expon);
t.fdp_l ^= t.fdp_l << 13;
t.fdp_l ^= t.fdp_l >> 17;
t.fdp_l ^= t.fdp_l << 5;
// inputSampleL += ((double(fpdL)-uint32_t(0x7fffffff)) * 1.1e-44l *
// pow(2,expon+62)); frexp((double)inputSampleR, &expon);
t.fdp_r ^= t.fdp_r << 13;
t.fdp_r ^= t.fdp_r >> 17;
t.fdp_r ^= t.fdp_r << 5;
// inputSampleR += ((double(fpdR)-uint32_t(0x7fffffff)) * 1.1e-44l *
// pow(2,expon+62)); end 64 bit stereo floating point dither
*out1 = input_l;
*out2 = input_r;
in1++;
in2++;
out1++;
out2++;
}
}
} // namespace trnr

52
clip/wavefolder.h Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
namespace trnr {
class wavefolder {
public:
float amount = 1.f;
void process_sample(float& _sample)
{
if (amount > 1.f) { _sample = fold(_sample * amount); }
}
private:
// folds the wave from -1 to 1
float fold(float _sample)
{
while (_sample > 1.0 || _sample < -1.0) {
if (_sample > 1.0) {
_sample = 2.0 - _sample;
} else if (_sample < -1.0) {
_sample = -2.0 - _sample;
}
}
return _sample;
}
// folds the positive part of the wave independently from the negative part.
float fold_bipolar(float _sample)
{
// fold positive values
if (_sample > 1.0) {
_sample = 2.0 - _sample;
if (_sample < 0.0) { _sample = -_sample; }
return fold(_sample);
}
// fold negative values
else if (_sample < -1.0) {
_sample = -2.0 - _sample;
if (_sample > 0.0) { _sample = -_sample; }
return fold(_sample);
} else {
return _sample;
}
}
};
}; // namespace trnr

View File

@@ -1,62 +0,0 @@
/*
* fold.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <cmath>
namespace trnr {
constexpr float A_LAW_A = 87.6f;
inline float alaw_encode(float input)
{
float sign = (input >= 0.0f) ? 1.0f : -1.0f;
float abs_sample = std::fabs(input);
float output;
if (abs_sample < (1.0f / A_LAW_A)) {
output = sign * (A_LAW_A * abs_sample) / (1.0f + std::log(A_LAW_A));
} else {
output =
sign * (1.0f + std::log(A_LAW_A * abs_sample)) / (1.0f + std::log(A_LAW_A));
}
return output;
}
inline float alaw_decode(float input)
{
float sign = (input >= 0.0f) ? 1.0f : -1.0f;
float abs_comp = std::fabs(input);
float sample;
if (abs_comp < (1.0f / (1.0f + std::log(A_LAW_A)))) {
sample = sign * (abs_comp * (1.0f + std::log(A_LAW_A))) / A_LAW_A;
} else {
sample = sign * std::exp(abs_comp * (1.0f + std::log(A_LAW_A)) - 1.0f) / A_LAW_A;
}
return sample;
}
} // namespace trnr

48
companding/mulaw.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstdint>
namespace trnr {
// mulaw companding based on code by Emilie Gillet / Mutable Instruments
class mulaw {
public:
int8_t encode_samples(int16_t pcm_val)
{
int16_t mask;
int16_t seg;
uint8_t uval;
pcm_val = pcm_val >> 2;
if (pcm_val < 0) {
pcm_val = -pcm_val;
mask = 0x7f;
} else {
mask = 0xff;
}
if (pcm_val > 8159) pcm_val = 8159;
pcm_val += (0x84 >> 2);
if (pcm_val <= 0x3f) seg = 0;
else if (pcm_val <= 0x7f) seg = 1;
else if (pcm_val <= 0xff) seg = 2;
else if (pcm_val <= 0x1ff) seg = 3;
else if (pcm_val <= 0x3ff) seg = 4;
else if (pcm_val <= 0x7ff) seg = 5;
else if (pcm_val <= 0xfff) seg = 6;
else if (pcm_val <= 0x1fff) seg = 7;
else seg = 8;
if (seg >= 8) return static_cast<uint8_t>(0x7f ^ mask);
else {
uval = static_cast<uint8_t>((seg << 4) | ((pcm_val >> (seg + 1)) & 0x0f));
return (uval ^ mask);
}
}
int16_t decode_samples(uint8_t u_val)
{
int16_t t;
u_val = ~u_val;
t = ((u_val & 0xf) << 3) + 0x84;
t <<= ((unsigned)u_val & 0x70) >> 4;
return ((u_val & 0x80) ? (0x84 - t) : (t - 0x84));
}
};
} // namespace trnr

108
companding/ulaw.h Normal file
View File

@@ -0,0 +1,108 @@
#pragma once
#include <cmath>
#include <cstdint>
#include <stdlib.h>
namespace trnr {
// ulaw compansion based on code by Chris Johnson
class ulaw {
public:
ulaw()
{
fpd_l = 1.0;
while (fpd_l < 16386) fpd_l = rand() * UINT32_MAX;
fpd_r = 1.0;
while (fpd_r < 16386) fpd_r = rand() * UINT32_MAX;
}
void encode_samples(double& input_sample_l, double& input_sample_r)
{
// ulaw encoding
static int noisesource_l = 0;
static int noisesource_r = 850010;
int residue;
double applyresidue;
noisesource_l = noisesource_l % 1700021;
noisesource_l++;
residue = noisesource_l * noisesource_l;
residue = residue % 170003;
residue *= residue;
residue = residue % 17011;
residue *= residue;
residue = residue % 1709;
residue *= residue;
residue = residue % 173;
residue *= residue;
residue = residue % 17;
applyresidue = residue;
applyresidue *= 0.00000001;
applyresidue *= 0.00000001;
input_sample_l += applyresidue;
if (input_sample_l < 1.2e-38 && -input_sample_l < 1.2e-38) { input_sample_l -= applyresidue; }
noisesource_r = noisesource_r % 1700021;
noisesource_r++;
residue = noisesource_r * noisesource_r;
residue = residue % 170003;
residue *= residue;
residue = residue % 17011;
residue *= residue;
residue = residue % 1709;
residue *= residue;
residue = residue % 173;
residue *= residue;
residue = residue % 17;
applyresidue = residue;
applyresidue *= 0.00000001;
applyresidue *= 0.00000001;
input_sample_r += applyresidue;
if (input_sample_r < 1.2e-38 && -input_sample_r < 1.2e-38) { input_sample_r -= applyresidue; }
if (input_sample_l > 1.0) input_sample_l = 1.0;
if (input_sample_l < -1.0) input_sample_l = -1.0;
if (input_sample_r > 1.0) input_sample_r = 1.0;
if (input_sample_r < -1.0) input_sample_r = -1.0;
if (input_sample_l > 0) input_sample_l = log(1.0 + (255 * fabs(input_sample_l))) / log(256);
if (input_sample_l < 0) input_sample_l = -log(1.0 + (255 * fabs(input_sample_l))) / log(256);
if (input_sample_r > 0) input_sample_r = log(1.0 + (255 * fabs(input_sample_r))) / log(256);
if (input_sample_r < 0) input_sample_r = -log(1.0 + (255 * fabs(input_sample_r))) / log(256);
}
void decode_samples(double& input_sample_l, double& input_sample_r)
{
// ulaw decoding
if (fabs(input_sample_l) < 1.18e-23) input_sample_l = fpd_l * 1.18e-17;
if (fabs(input_sample_r) < 1.18e-23) input_sample_r = fpd_r * 1.18e-17;
if (input_sample_l > 1.0) input_sample_l = 1.0;
if (input_sample_l < -1.0) input_sample_l = -1.0;
if (input_sample_r > 1.0) input_sample_r = 1.0;
if (input_sample_r < -1.0) input_sample_r = -1.0;
if (input_sample_l > 0) input_sample_l = (pow(256, fabs(input_sample_l)) - 1.0) / 255;
if (input_sample_l < 0) input_sample_l = -(pow(256, fabs(input_sample_l)) - 1.0) / 255;
if (input_sample_r > 0) input_sample_r = (pow(256, fabs(input_sample_r)) - 1.0) / 255;
if (input_sample_r < 0) input_sample_r = -(pow(256, fabs(input_sample_r)) - 1.0) / 255;
// 64 bit stereo floating point dither
fpd_l ^= fpd_l << 13;
fpd_l ^= fpd_l >> 17;
fpd_l ^= fpd_l << 5;
fpd_r ^= fpd_r << 13;
fpd_r ^= fpd_r >> 17;
fpd_r ^= fpd_r << 5;
}
private:
uint32_t fpd_l;
uint32_t fpd_r;
};
} // namespace trnr

280
dynamics/aw_pop2.h Normal file
View File

@@ -0,0 +1,280 @@
#pragma once
#include <cstdlib>
#include <stdint.h>
namespace trnr {
// compressor based on pop2 by Chris Johnson
class aw_pop2 {
public:
aw_pop2()
{
samplerate = 44100;
A = 0.5;
B = 0.5;
C = 0.5;
D = 0.5;
E = 1.0;
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
lastSampleL = 0.0;
wasPosClipL = false;
wasNegClipL = false;
lastSampleR = 0.0;
wasPosClipR = false;
wasNegClipR = false;
for (int x = 0; x < 16; x++) {
intermediateL[x] = 0.0;
intermediateR[x] = 0.0;
}
muVaryL = 0.0;
muAttackL = 0.0;
muNewSpeedL = 1000.0;
muSpeedAL = 1000.0;
muSpeedBL = 1000.0;
muCoefficientAL = 1.0;
muCoefficientBL = 1.0;
muVaryR = 0.0;
muAttackR = 0.0;
muNewSpeedR = 1000.0;
muSpeedAR = 1000.0;
muSpeedBR = 1000.0;
muCoefficientAR = 1.0;
muCoefficientBR = 1.0;
flip = false;
// this is reset: values being initialized only once. Startup values, whatever they are.
}
void set_compression(double value) { A = clamp(value); }
void set_attack(double value) { B = clamp(value); }
void set_release(double value) { C = clamp(value); }
void set_drive(double value) { D = clamp(value); }
void set_drywet(double value) { E = clamp(value); }
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
template <typename t_sample>
void process_block(t_sample** inputs, t_sample** outputs, long sampleframes)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
int spacing = floor(overallscale); // should give us working basic scaling, usually 2 or 4
if (spacing < 1) spacing = 1;
if (spacing > 16) spacing = 16;
double threshold = 1.0 - ((1.0 - pow(1.0 - A, 2)) * 0.9);
double attack = ((pow(B, 4) * 100000.0) + 10.0) * overallscale;
double release = ((pow(C, 5) * 2000000.0) + 20.0) * overallscale;
double maxRelease = release * 4.0;
double muPreGain = 1.0 / threshold;
double muMakeupGain = sqrt(1.0 / threshold) * D;
double wet = E;
// compressor section
while (--sampleframes >= 0) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double drySampleL = inputSampleL;
double drySampleR = inputSampleR;
// begin compressor section
inputSampleL *= muPreGain;
inputSampleR *= muPreGain;
// adjust coefficients for L
if (flip) {
if (fabs(inputSampleL) > threshold) {
muVaryL = threshold / fabs(inputSampleL);
muAttackL = sqrt(fabs(muSpeedAL));
muCoefficientAL = muCoefficientAL * (muAttackL - 1.0);
if (muVaryL < threshold) muCoefficientAL = muCoefficientAL + threshold;
else muCoefficientAL = muCoefficientAL + muVaryL;
muCoefficientAL = muCoefficientAL / muAttackL;
muNewSpeedL = muSpeedAL * (muSpeedAL - 1.0);
muNewSpeedL = muNewSpeedL + release;
muSpeedAL = muNewSpeedL / muSpeedAL;
if (muSpeedAL > maxRelease) muSpeedAL = maxRelease;
} else {
muCoefficientAL = muCoefficientAL * ((muSpeedAL * muSpeedAL) - 1.0);
muCoefficientAL = muCoefficientAL + 1.0;
muCoefficientAL = muCoefficientAL / (muSpeedAL * muSpeedAL);
muNewSpeedL = muSpeedAL * (muSpeedAL - 1.0);
muNewSpeedL = muNewSpeedL + attack;
muSpeedAL = muNewSpeedL / muSpeedAL;
}
} else {
if (fabs(inputSampleL) > threshold) {
muVaryL = threshold / fabs(inputSampleL);
muAttackL = sqrt(fabs(muSpeedBL));
muCoefficientBL = muCoefficientBL * (muAttackL - 1);
if (muVaryL < threshold) muCoefficientBL = muCoefficientBL + threshold;
else muCoefficientBL = muCoefficientBL + muVaryL;
muCoefficientBL = muCoefficientBL / muAttackL;
muNewSpeedL = muSpeedBL * (muSpeedBL - 1.0);
muNewSpeedL = muNewSpeedL + release;
muSpeedBL = muNewSpeedL / muSpeedBL;
if (muSpeedBL > maxRelease) muSpeedBL = maxRelease;
} else {
muCoefficientBL = muCoefficientBL * ((muSpeedBL * muSpeedBL) - 1.0);
muCoefficientBL = muCoefficientBL + 1.0;
muCoefficientBL = muCoefficientBL / (muSpeedBL * muSpeedBL);
muNewSpeedL = muSpeedBL * (muSpeedBL - 1.0);
muNewSpeedL = muNewSpeedL + attack;
muSpeedBL = muNewSpeedL / muSpeedBL;
}
}
// got coefficients, adjusted speeds for L
// adjust coefficients for R
if (flip) {
if (fabs(inputSampleR) > threshold) {
muVaryR = threshold / fabs(inputSampleR);
muAttackR = sqrt(fabs(muSpeedAR));
muCoefficientAR = muCoefficientAR * (muAttackR - 1.0);
if (muVaryR < threshold) muCoefficientAR = muCoefficientAR + threshold;
else muCoefficientAR = muCoefficientAR + muVaryR;
muCoefficientAR = muCoefficientAR / muAttackR;
muNewSpeedR = muSpeedAR * (muSpeedAR - 1.0);
muNewSpeedR = muNewSpeedR + release;
muSpeedAR = muNewSpeedR / muSpeedAR;
if (muSpeedAR > maxRelease) muSpeedAR = maxRelease;
} else {
muCoefficientAR = muCoefficientAR * ((muSpeedAR * muSpeedAR) - 1.0);
muCoefficientAR = muCoefficientAR + 1.0;
muCoefficientAR = muCoefficientAR / (muSpeedAR * muSpeedAR);
muNewSpeedR = muSpeedAR * (muSpeedAR - 1.0);
muNewSpeedR = muNewSpeedR + attack;
muSpeedAR = muNewSpeedR / muSpeedAR;
}
} else {
if (fabs(inputSampleR) > threshold) {
muVaryR = threshold / fabs(inputSampleR);
muAttackR = sqrt(fabs(muSpeedBR));
muCoefficientBR = muCoefficientBR * (muAttackR - 1);
if (muVaryR < threshold) muCoefficientBR = muCoefficientBR + threshold;
else muCoefficientBR = muCoefficientBR + muVaryR;
muCoefficientBR = muCoefficientBR / muAttackR;
muNewSpeedR = muSpeedBR * (muSpeedBR - 1.0);
muNewSpeedR = muNewSpeedR + release;
muSpeedBR = muNewSpeedR / muSpeedBR;
if (muSpeedBR > maxRelease) muSpeedBR = maxRelease;
} else {
muCoefficientBR = muCoefficientBR * ((muSpeedBR * muSpeedBR) - 1.0);
muCoefficientBR = muCoefficientBR + 1.0;
muCoefficientBR = muCoefficientBR / (muSpeedBR * muSpeedBR);
muNewSpeedR = muSpeedBR * (muSpeedBR - 1.0);
muNewSpeedR = muNewSpeedR + attack;
muSpeedBR = muNewSpeedR / muSpeedBR;
}
}
// got coefficients, adjusted speeds for R
if (flip) {
inputSampleL *= pow(muCoefficientAL, 2);
inputSampleR *= pow(muCoefficientAR, 2);
} else {
inputSampleL *= pow(muCoefficientBL, 2);
inputSampleR *= pow(muCoefficientBR, 2);
}
inputSampleL *= muMakeupGain;
inputSampleR *= muMakeupGain;
flip = !flip;
// end compressor section
if (wet < 1.0) {
inputSampleL = (drySampleL * (1.0 - wet)) + (inputSampleL * wet);
inputSampleR = (drySampleR * (1.0 - wet)) + (inputSampleR * wet);
}
// begin 64 bit stereo floating point dither
// int expon; frexp((double)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
// inputSampleL += ((double(fpdL)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// frexp((double)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
// inputSampleR += ((double(fpdR)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// end 64 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
double muVaryL;
double muAttackL;
double muNewSpeedL;
double muSpeedAL;
double muSpeedBL;
double muCoefficientAL;
double muCoefficientBL;
double muVaryR;
double muAttackR;
double muNewSpeedR;
double muSpeedAR;
double muSpeedBR;
double muCoefficientAR;
double muCoefficientBR;
bool flip;
double lastSampleL;
double intermediateL[16];
bool wasPosClipL;
bool wasNegClipL;
double lastSampleR;
double intermediateR[16];
bool wasPosClipR;
bool wasNegClipR; // Stereo ClipOnly2
float A;
float B;
float C;
float D;
float E; // parameters. Always 0-1, and we scale/alter them elsewhere.
double clamp(double& value)
{
if (value > 1) {
value = 1;
} else if (value < 0) {
value = 0;
}
return value;
}
};
} // namespace trnr

View File

@@ -1,142 +0,0 @@
/*
* oneknob.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "../util/audio_math.h"
#include "rms_detector.h"
#include <algorithm>
#include <cmath>
namespace trnr {
struct hp_filter {
float a0, a1, b1;
float z1; // filter state
};
inline void hp_filter_init(hp_filter& f, float samplerate)
{
const float cutoff = 100.0f;
float w0 = 2.0f * 3.14159265359f * cutoff / samplerate;
float alpha = (1.0f - std::tan(w0 / 2.0f)) / (1.0f + std::tan(w0 / 2.0f));
f.a0 = 0.5f * (1.0f + alpha);
f.a1 = -0.5f * (1.0f + alpha);
f.b1 = alpha;
f.z1 = 0.0f;
}
inline float hp_filter_process(hp_filter& f, float x)
{
float y = f.a0 * x + f.a1 * f.z1 - f.b1 * f.z1;
f.z1 = x;
return y;
}
struct oneknob_comp {
// params
float amount = 0.f;
bool multiplied = false;
// state
rms_detector detector;
hp_filter filter;
float attack_coef;
float release_coef;
float envelope_level;
float sidechain_in;
double samplerate;
};
inline void oneknob_init(oneknob_comp& c, double samplerate, float window_ms)
{
rms_init(c.detector, samplerate, window_ms);
hp_filter_init(c.filter, samplerate);
const float attack_ms = 0.2f;
const float release_ms = 150.f;
c.samplerate = samplerate;
c.attack_coef = expf(-1.0f / (attack_ms * 1e-6 * samplerate));
c.release_coef = expf(-1.0f / (release_ms * 1e-3 * samplerate));
c.envelope_level = -60.f;
c.sidechain_in = 0.f;
}
template <typename sample>
inline void oneknob_process_block(oneknob_comp& c, sample** audio, int frames)
{
const float min_user_ratio = 1.0f;
const float max_user_ratio = 20.0f;
const float threshold_db = -12.f;
const float amount = fmaxf(0.0f, fminf(powf(c.amount, 2.f), 1.0f));
float ratio = min_user_ratio + amount * (max_user_ratio - min_user_ratio);
const float fast_attack = 0.1f;
const float slow_attack = 0.6f;
const float fast_release = 90.f;
const float slow_release = 300.f;
const float max_gr = 12.f;
for (int i = 0; i < frames; ++i) {
float rms_value = rms_process<sample>(c.detector, c.sidechain_in);
float envelope_in = lin_2_db(fmaxf(fabs(rms_value), 1e-20f));
// attack
if (envelope_in > c.envelope_level) {
c.envelope_level =
envelope_in + c.attack_coef * (c.envelope_level - envelope_in);
}
// release
else {
c.envelope_level =
envelope_in + c.release_coef * (c.envelope_level - envelope_in);
}
float x = c.envelope_level;
float y;
if (x < threshold_db) y = x;
else y = threshold_db + (x - threshold_db) / ratio;
float gain_reduction_db = y - x;
float gain_reduction_lin = db_2_lin(gain_reduction_db);
audio[0][i] *= gain_reduction_lin;
audio[1][i] *= gain_reduction_lin;
// calculate program-dependent attack/release times
float norm_gr = std::clamp(gain_reduction_db / max_gr, 0.f, 1.f);
float release_ms = fast_release + (slow_release - fast_release) * (1.f - norm_gr);
float attack_ms = fast_attack + (slow_attack - fast_attack) * (1.f - norm_gr);
c.attack_coef = expf(-1.0f / (attack_ms * 1e-6 * c.samplerate));
c.release_coef = expf(-1.0f / (release_ms * 1e-3 * c.samplerate));
// feedback compression
float sum = sqrtf(0.5f * (audio[0][i] * audio[0][i] + audio[1][i] * audio[1][i]));
if (c.multiplied) sum *= 3.f;
c.sidechain_in = hp_filter_process(c.filter, sum);
}
}
} // namespace trnr

View File

@@ -1,115 +1,63 @@
/*
* pump.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "../util/audio_math.h"
#include <cmath>
namespace trnr {
struct pump {
double samplerate;
float threshold_db = 0.f;
float attack_ms = 10.f;
float release_ms = 100.f;
float hp_filter = 80.f;
float ratio = 1000.f;
float filter_frq = 40000.f;
float filter_exp = 0.f;
float treble_boost = 0.f;
float filtered = 0.f;
float filtered_l = 0.f;
float filtered_r = 0.f;
float boosted_l = 0.f;
float boosted_r = 0.f;
float attack_coef = 0.f;
float release_coef = 0.f;
float envelope_db = 0.f;
};
enum pump_param {
PUMP_THRESHOLD,
PUMP_ATTACK,
PUMP_RELEASE,
PUMP_HP_FILTER,
PUMP_RATIO,
PUMP_FILTER_FRQ,
PUMP_FILTER_EXP,
PUMP_TREBLE_BOOST
};
inline void pump_set_param(pump& p, pump_param param, float value)
{
switch (param) {
case PUMP_THRESHOLD:
p.threshold_db = value;
break;
case PUMP_ATTACK:
p.attack_ms = value;
p.attack_coef = exp(-1000.0 / (p.attack_ms * p.samplerate));
break;
case PUMP_RELEASE:
p.release_ms = value;
p.release_coef = exp(-1000.0 / (p.release_ms * p.samplerate));
break;
case PUMP_HP_FILTER:
p.hp_filter = value;
break;
case PUMP_RATIO:
p.ratio = value;
break;
case PUMP_FILTER_FRQ:
p.filter_frq = value;
break;
case PUMP_FILTER_EXP:
p.filter_exp = value;
break;
case PUMP_TREBLE_BOOST:
p.treble_boost = value;
break;
default:
break;
}
}
inline void pump_init(pump& p, double samplerate)
{
p.samplerate = samplerate;
pump_set_param(p, PUMP_ATTACK, p.attack_ms);
pump_set_param(p, PUMP_RELEASE, p.release_ms);
}
template <typename sample>
inline void pump_process_block(pump& p, sample** audio, sample** sidechain, int frames)
class pump {
public:
pump()
: attack_ms(10.f)
, release_ms(100.f)
, hp_filter(80.0f)
, threshold_db(0.f)
, ratio(1000.0f)
, filter_frq(40000.f)
, filter_exp(0.f)
, treble_boost(0.f)
{
set_samplerate(44100);
}
void set_samplerate(double _samplerate)
{
samplerate = _samplerate;
set_attack(attack_ms);
set_release(release_ms);
}
void set_ratio(float value) { ratio = value; }
/* set threshold in db */
void set_threshold(float value) { threshold_db = value; }
/* set attack in ms */
void set_attack(float value)
{
attack_ms = value;
attack_coef = exp(-1000.0 / (attack_ms * samplerate));
}
/* set release in ms */
void set_release(float value)
{
release_ms = value;
release_coef = exp(-1000.0 / (release_ms * samplerate));
}
/* lowpass filter frequency in hz */
void set_filter_frq(float value) { filter_frq = value; }
/* accentuates the filter pumping effect */
void set_filter_exp(float value) { filter_exp = value; }
void process_block(sample** audio, sample** sidechain, int frames)
{
// highpass filter coefficients
float hp_x = std::exp(-2.0 * M_PI * p.hp_filter / p.samplerate);
float hp_x = std::exp(-2.0 * M_PI * hp_filter / samplerate);
float hp_a0 = 1.0 - hp_x;
float hp_b1 = -hp_x;
// top end boost filter coefficients
float bst_x = exp(-2.0 * M_PI * 5000.0 / p.samplerate);
float bst_x = exp(-2.0 * M_PI * 5000.0 / samplerate);
float bst_a0 = 1.0 - bst_x;
float bst_b1 = -bst_x;
@@ -121,55 +69,76 @@ inline void pump_process_block(pump& p, sample** audio, sample** sidechain, int
sample sidechain_in = (sidechain[0][i] + sidechain[1][i]) / 2.0;
// highpass filter sidechain signal
p.filtered = hp_a0 * sidechain_in - hp_b1 * p.filtered;
sidechain_in = sidechain_in - p.filtered;
filtered = hp_a0 * sidechain_in - hp_b1 * filtered;
sidechain_in = sidechain_in - filtered;
// rectify sidechain input for envelope following
float link = std::fabs(sidechain_in);
float linked_db = trnr::lin_2_db(link);
// cut envelope below threshold
float overshoot_db = linked_db - (p.threshold_db - 10.0);
float overshoot_db = linked_db - (threshold_db - 10.0);
if (overshoot_db < 0.0) overshoot_db = 0.0;
// process envelope
if (overshoot_db > p.envelope_db) {
p.envelope_db = overshoot_db + p.attack_coef * (p.envelope_db - overshoot_db);
if (overshoot_db > envelope_db) {
envelope_db = overshoot_db + attack_coef * (envelope_db - overshoot_db);
} else {
p.envelope_db = overshoot_db + p.release_coef * (p.envelope_db - overshoot_db);
envelope_db = overshoot_db + release_coef * (envelope_db - overshoot_db);
}
float slope = 1.f / p.ratio;
float slope = 1.f / ratio;
// transfer function
float gain_reduction_db = p.envelope_db * (slope - 1.0);
float gain_reduction_lin = db_2_lin(gain_reduction_db);
float gain_reduction_db = envelope_db * (slope - 1.0);
float gain_reduction_lin = trnr::db_2_lin(gain_reduction_db);
// compress left and right signals
sample output_l = input_l * gain_reduction_lin;
sample output_r = input_r * gain_reduction_lin;
if (p.filter_exp > 0.f) {
if (filter_exp > 0.f) {
// one pole lowpass filter with envelope applied to frequency for pumping effect
float freq = p.filter_frq * pow(gain_reduction_lin, p.filter_exp);
float lp_x = exp(-2.0 * M_PI * freq / p.samplerate);
float freq = filter_frq * pow(gain_reduction_lin, filter_exp);
float lp_x = exp(-2.0 * M_PI * freq / samplerate);
float lp_a0 = 1.0 - lp_x;
float lp_b1 = -lp_x;
p.filtered_l = lp_a0 * output_l - lp_b1 * p.filtered_l;
p.filtered_r = lp_a0 * output_r - lp_b1 * p.filtered_r;
filtered_l = lp_a0 * output_l - lp_b1 * filtered_l;
filtered_r = lp_a0 * output_r - lp_b1 * filtered_r;
}
// top end boost
p.boosted_l = bst_a0 * p.filtered_l - bst_b1 * p.boosted_l;
p.boosted_r = bst_a0 * p.filtered_r - bst_b1 * p.boosted_r;
output_l = p.filtered_l + (p.filtered_l - p.boosted_l) * p.treble_boost;
output_r = p.filtered_r + (p.filtered_r - p.boosted_r) * p.treble_boost;
boosted_l = bst_a0 * filtered_l - bst_b1 * boosted_l;
boosted_r = bst_a0 * filtered_r - bst_b1 * boosted_r;
output_l = filtered_l + (filtered_l - boosted_l) * treble_boost;
output_r = filtered_r + (filtered_r - boosted_r) * treble_boost;
// calculate makeup gain
float makeup_lin = trnr::db_2_lin(-p.threshold_db / 5.f);
float makeup_lin = trnr::db_2_lin(-threshold_db / 5.f);
audio[0][i] = input_l * gain_reduction_lin * makeup_lin;
audio[1][i] = input_r * gain_reduction_lin * makeup_lin;
}
}
private:
double samplerate;
float threshold_db = 0.f;
float attack_ms;
float release_ms;
float hp_filter;
float ratio;
float filter_frq;
float filter_exp;
float treble_boost;
sample filtered;
sample filtered_l = 0;
sample filtered_r = 0;
sample boosted_l = 0;
sample boosted_r = 0;
float attack_coef;
float release_coef;
float envelope_db = 0;
};
} // namespace trnr

View File

@@ -1,47 +0,0 @@
/*
* rms_detector.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <cmath>
namespace trnr {
struct rms_detector {
float alpha;
float rms_squared;
};
inline void rms_init(rms_detector& d, float samplerate, float window_ms)
{
float window_seconds = 0.001f * window_ms;
d.alpha = 1.0f - expf(-1.0f / (samplerate * window_seconds));
d.rms_squared = 0.0f;
}
template <typename sample>
inline sample rms_process(rms_detector& d, sample input)
{
d.rms_squared = (1.0f - d.alpha) * d.rms_squared + d.alpha * (input * input);
return sqrtf(d.rms_squared);
}
} // namespace trnr

649
filter/aw_eq.h Normal file
View File

@@ -0,0 +1,649 @@
#pragma once
#include <cstdlib>
#include <stdint.h>
namespace trnr {
// 3 band equalizer with high/lowpass filters based on EQ by Chris Johnson.
class aw_eq {
public:
aw_eq()
{
samplerate = 44100;
A = 0.5; // Treble -12 to 12
B = 0.5; // Mid -12 to 12
C = 0.5; // Bass -12 to 12
D = 1.0; // Lowpass 16.0K log 1 to 16 defaulting to 16K
E = 0.4; // TrebFrq 6.0 log 1 to 16 defaulting to 6K
F = 0.4; // BassFrq 100.0 log 30 to 1600 defaulting to 100 hz
G = 0.0; // Hipass 30.0 log 30 to 1600 defaulting to 30
H = 0.5; // OutGain -18 to 18
lastSampleL = 0.0;
last2SampleL = 0.0;
lastSampleR = 0.0;
last2SampleR = 0.0;
iirHighSampleLA = 0.0;
iirHighSampleLB = 0.0;
iirHighSampleLC = 0.0;
iirHighSampleLD = 0.0;
iirHighSampleLE = 0.0;
iirLowSampleLA = 0.0;
iirLowSampleLB = 0.0;
iirLowSampleLC = 0.0;
iirLowSampleLD = 0.0;
iirLowSampleLE = 0.0;
iirHighSampleL = 0.0;
iirLowSampleL = 0.0;
iirHighSampleRA = 0.0;
iirHighSampleRB = 0.0;
iirHighSampleRC = 0.0;
iirHighSampleRD = 0.0;
iirHighSampleRE = 0.0;
iirLowSampleRA = 0.0;
iirLowSampleRB = 0.0;
iirLowSampleRC = 0.0;
iirLowSampleRD = 0.0;
iirLowSampleRE = 0.0;
iirHighSampleR = 0.0;
iirLowSampleR = 0.0;
tripletLA = 0.0;
tripletLB = 0.0;
tripletLC = 0.0;
tripletFactorL = 0.0;
tripletRA = 0.0;
tripletRB = 0.0;
tripletRC = 0.0;
tripletFactorR = 0.0;
lowpassSampleLAA = 0.0;
lowpassSampleLAB = 0.0;
lowpassSampleLBA = 0.0;
lowpassSampleLBB = 0.0;
lowpassSampleLCA = 0.0;
lowpassSampleLCB = 0.0;
lowpassSampleLDA = 0.0;
lowpassSampleLDB = 0.0;
lowpassSampleLE = 0.0;
lowpassSampleLF = 0.0;
lowpassSampleLG = 0.0;
lowpassSampleRAA = 0.0;
lowpassSampleRAB = 0.0;
lowpassSampleRBA = 0.0;
lowpassSampleRBB = 0.0;
lowpassSampleRCA = 0.0;
lowpassSampleRCB = 0.0;
lowpassSampleRDA = 0.0;
lowpassSampleRDB = 0.0;
lowpassSampleRE = 0.0;
lowpassSampleRF = 0.0;
lowpassSampleRG = 0.0;
highpassSampleLAA = 0.0;
highpassSampleLAB = 0.0;
highpassSampleLBA = 0.0;
highpassSampleLBB = 0.0;
highpassSampleLCA = 0.0;
highpassSampleLCB = 0.0;
highpassSampleLDA = 0.0;
highpassSampleLDB = 0.0;
highpassSampleLE = 0.0;
highpassSampleLF = 0.0;
highpassSampleRAA = 0.0;
highpassSampleRAB = 0.0;
highpassSampleRBA = 0.0;
highpassSampleRBB = 0.0;
highpassSampleRCA = 0.0;
highpassSampleRCB = 0.0;
highpassSampleRDA = 0.0;
highpassSampleRDB = 0.0;
highpassSampleRE = 0.0;
highpassSampleRF = 0.0;
flip = false;
flipthree = 0;
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
// this is reset: values being initialized only once. Startup values, whatever they are.
}
void set_treble(double value) { A = clamp(value); }
void set_mid(double value) { B = clamp(value); }
void set_bass(double value) { C = clamp(value); }
void set_lowpass(double value) { D = clamp(value); }
void set_treble_frq(double value) { E = clamp(value); }
void set_bass_frq(double value) { F = clamp(value); }
void set_hipass(double value) { G = clamp(value); }
void set_out_gain(double value) { H = clamp(value); }
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
template <typename t_sample>
void process_block(t_sample** inputs, t_sample** outputs, long sampleframes)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
double overallscale = 1.0;
overallscale /= 44100.0;
double compscale = overallscale;
overallscale = samplerate;
compscale = compscale * overallscale;
// compscale is the one that's 1 or something like 2.2 for 96K rates
double inputSampleL;
double inputSampleR;
double highSampleL = 0.0;
double midSampleL = 0.0;
double bassSampleL = 0.0;
double highSampleR = 0.0;
double midSampleR = 0.0;
double bassSampleR = 0.0;
double densityA = (A * 12.0) - 6.0;
double densityB = (B * 12.0) - 6.0;
double densityC = (C * 12.0) - 6.0;
bool engageEQ = true;
if ((0.0 == densityA) && (0.0 == densityB) && (0.0 == densityC)) engageEQ = false;
densityA = pow(10.0, densityA / 20.0) - 1.0;
densityB = pow(10.0, densityB / 20.0) - 1.0;
densityC = pow(10.0, densityC / 20.0) - 1.0;
// convert to 0 to X multiplier with 1.0 being O db
// minus one gives nearly -1 to ? (should top out at 1)
// calibrate so that X db roughly equals X db with maximum topping out at 1 internally
double tripletIntensity = -densityA;
double iirAmountC = (((D * D * 15.0) + 1.0) * 0.0188) + 0.7;
if (iirAmountC > 1.0) iirAmountC = 1.0;
bool engageLowpass = false;
if (((D * D * 15.0) + 1.0) < 15.99) engageLowpass = true;
double iirAmountA = (((E * E * 15.0) + 1.0) * 1000) / overallscale;
double iirAmountB = (((F * F * 1570.0) + 30.0) * 10) / overallscale;
double iirAmountD = (((G * G * 1570.0) + 30.0) * 1.0) / overallscale;
bool engageHighpass = false;
if (((G * G * 1570.0) + 30.0) > 30.01) engageHighpass = true;
// bypass the highpass and lowpass if set to extremes
double bridgerectifier;
double outA = fabs(densityA);
double outB = fabs(densityB);
double outC = fabs(densityC);
// end EQ
double outputgain = pow(10.0, ((H * 36.0) - 18.0) / 20.0);
while (--sampleframes >= 0) {
inputSampleL = *in1;
inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
last2SampleL = lastSampleL;
lastSampleL = inputSampleL;
last2SampleR = lastSampleR;
lastSampleR = inputSampleR;
flip = !flip;
flipthree++;
if (flipthree < 1 || flipthree > 3) flipthree = 1;
// counters
// begin highpass
if (engageHighpass) {
if (flip) {
highpassSampleLAA = (highpassSampleLAA * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLAA;
highpassSampleLBA = (highpassSampleLBA * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLBA;
highpassSampleLCA = (highpassSampleLCA * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLCA;
highpassSampleLDA = (highpassSampleLDA * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLDA;
} else {
highpassSampleLAB = (highpassSampleLAB * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLAB;
highpassSampleLBB = (highpassSampleLBB * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLBB;
highpassSampleLCB = (highpassSampleLCB * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLCB;
highpassSampleLDB = (highpassSampleLDB * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLDB;
}
highpassSampleLE = (highpassSampleLE * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLE;
highpassSampleLF = (highpassSampleLF * (1.0 - iirAmountD)) + (inputSampleL * iirAmountD);
inputSampleL -= highpassSampleLF;
if (flip) {
highpassSampleRAA = (highpassSampleRAA * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRAA;
highpassSampleRBA = (highpassSampleRBA * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRBA;
highpassSampleRCA = (highpassSampleRCA * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRCA;
highpassSampleRDA = (highpassSampleRDA * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRDA;
} else {
highpassSampleRAB = (highpassSampleRAB * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRAB;
highpassSampleRBB = (highpassSampleRBB * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRBB;
highpassSampleRCB = (highpassSampleRCB * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRCB;
highpassSampleRDB = (highpassSampleRDB * (1.0 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRDB;
}
highpassSampleRE = (highpassSampleRE * (1 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRE;
highpassSampleRF = (highpassSampleRF * (1 - iirAmountD)) + (inputSampleR * iirAmountD);
inputSampleR -= highpassSampleRF;
}
// end highpass
// begin EQ
if (engageEQ) {
switch (flipthree) {
case 1:
tripletFactorL = last2SampleL - inputSampleL;
tripletLA += tripletFactorL;
tripletLC -= tripletFactorL;
tripletFactorL = tripletLA * tripletIntensity;
iirHighSampleLC = (iirHighSampleLC * (1.0 - iirAmountA)) + (inputSampleL * iirAmountA);
highSampleL = inputSampleL - iirHighSampleLC;
iirLowSampleLC = (iirLowSampleLC * (1.0 - iirAmountB)) + (inputSampleL * iirAmountB);
bassSampleL = iirLowSampleLC;
tripletFactorR = last2SampleR - inputSampleR;
tripletRA += tripletFactorR;
tripletRC -= tripletFactorR;
tripletFactorR = tripletRA * tripletIntensity;
iirHighSampleRC = (iirHighSampleRC * (1.0 - iirAmountA)) + (inputSampleR * iirAmountA);
highSampleR = inputSampleR - iirHighSampleRC;
iirLowSampleRC = (iirLowSampleRC * (1.0 - iirAmountB)) + (inputSampleR * iirAmountB);
bassSampleR = iirLowSampleRC;
break;
case 2:
tripletFactorL = last2SampleL - inputSampleL;
tripletLB += tripletFactorL;
tripletLA -= tripletFactorL;
tripletFactorL = tripletLB * tripletIntensity;
iirHighSampleLD = (iirHighSampleLD * (1.0 - iirAmountA)) + (inputSampleL * iirAmountA);
highSampleL = inputSampleL - iirHighSampleLD;
iirLowSampleLD = (iirLowSampleLD * (1.0 - iirAmountB)) + (inputSampleL * iirAmountB);
bassSampleL = iirLowSampleLD;
tripletFactorR = last2SampleR - inputSampleR;
tripletRB += tripletFactorR;
tripletRA -= tripletFactorR;
tripletFactorR = tripletRB * tripletIntensity;
iirHighSampleRD = (iirHighSampleRD * (1.0 - iirAmountA)) + (inputSampleR * iirAmountA);
highSampleR = inputSampleR - iirHighSampleRD;
iirLowSampleRD = (iirLowSampleRD * (1.0 - iirAmountB)) + (inputSampleR * iirAmountB);
bassSampleR = iirLowSampleRD;
break;
case 3:
tripletFactorL = last2SampleL - inputSampleL;
tripletLC += tripletFactorL;
tripletLB -= tripletFactorL;
tripletFactorL = tripletLC * tripletIntensity;
iirHighSampleLE = (iirHighSampleLE * (1.0 - iirAmountA)) + (inputSampleL * iirAmountA);
highSampleL = inputSampleL - iirHighSampleLE;
iirLowSampleLE = (iirLowSampleLE * (1.0 - iirAmountB)) + (inputSampleL * iirAmountB);
bassSampleL = iirLowSampleLE;
tripletFactorR = last2SampleR - inputSampleR;
tripletRC += tripletFactorR;
tripletRB -= tripletFactorR;
tripletFactorR = tripletRC * tripletIntensity;
iirHighSampleRE = (iirHighSampleRE * (1.0 - iirAmountA)) + (inputSampleR * iirAmountA);
highSampleR = inputSampleR - iirHighSampleRE;
iirLowSampleRE = (iirLowSampleRE * (1.0 - iirAmountB)) + (inputSampleR * iirAmountB);
bassSampleR = iirLowSampleRE;
break;
}
tripletLA /= 2.0;
tripletLB /= 2.0;
tripletLC /= 2.0;
highSampleL = highSampleL + tripletFactorL;
tripletRA /= 2.0;
tripletRB /= 2.0;
tripletRC /= 2.0;
highSampleR = highSampleR + tripletFactorR;
if (flip) {
iirHighSampleLA = (iirHighSampleLA * (1.0 - iirAmountA)) + (highSampleL * iirAmountA);
highSampleL -= iirHighSampleLA;
iirLowSampleLA = (iirLowSampleLA * (1.0 - iirAmountB)) + (bassSampleL * iirAmountB);
bassSampleL = iirLowSampleLA;
iirHighSampleRA = (iirHighSampleRA * (1.0 - iirAmountA)) + (highSampleR * iirAmountA);
highSampleR -= iirHighSampleRA;
iirLowSampleRA = (iirLowSampleRA * (1.0 - iirAmountB)) + (bassSampleR * iirAmountB);
bassSampleR = iirLowSampleRA;
} else {
iirHighSampleLB = (iirHighSampleLB * (1.0 - iirAmountA)) + (highSampleL * iirAmountA);
highSampleL -= iirHighSampleLB;
iirLowSampleLB = (iirLowSampleLB * (1.0 - iirAmountB)) + (bassSampleL * iirAmountB);
bassSampleL = iirLowSampleLB;
iirHighSampleRB = (iirHighSampleRB * (1.0 - iirAmountA)) + (highSampleR * iirAmountA);
highSampleR -= iirHighSampleRB;
iirLowSampleRB = (iirLowSampleRB * (1.0 - iirAmountB)) + (bassSampleR * iirAmountB);
bassSampleR = iirLowSampleRB;
}
iirHighSampleL = (iirHighSampleL * (1.0 - iirAmountA)) + (highSampleL * iirAmountA);
highSampleL -= iirHighSampleL;
iirLowSampleL = (iirLowSampleL * (1.0 - iirAmountB)) + (bassSampleL * iirAmountB);
bassSampleL = iirLowSampleL;
iirHighSampleR = (iirHighSampleR * (1.0 - iirAmountA)) + (highSampleR * iirAmountA);
highSampleR -= iirHighSampleR;
iirLowSampleR = (iirLowSampleR * (1.0 - iirAmountB)) + (bassSampleR * iirAmountB);
bassSampleR = iirLowSampleR;
midSampleL = (inputSampleL - bassSampleL) - highSampleL;
midSampleR = (inputSampleR - bassSampleR) - highSampleR;
// drive section
highSampleL *= (densityA + 1.0);
bridgerectifier = fabs(highSampleL) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityA > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (highSampleL > 0) highSampleL = (highSampleL * (1 - outA)) + (bridgerectifier * outA);
else highSampleL = (highSampleL * (1 - outA)) - (bridgerectifier * outA);
// blend according to densityA control
highSampleR *= (densityA + 1.0);
bridgerectifier = fabs(highSampleR) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityA > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (highSampleR > 0) highSampleR = (highSampleR * (1 - outA)) + (bridgerectifier * outA);
else highSampleR = (highSampleR * (1 - outA)) - (bridgerectifier * outA);
// blend according to densityA control
midSampleL *= (densityB + 1.0);
bridgerectifier = fabs(midSampleL) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityB > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (midSampleL > 0) midSampleL = (midSampleL * (1 - outB)) + (bridgerectifier * outB);
else midSampleL = (midSampleL * (1 - outB)) - (bridgerectifier * outB);
// blend according to densityB control
midSampleR *= (densityB + 1.0);
bridgerectifier = fabs(midSampleR) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityB > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (midSampleR > 0) midSampleR = (midSampleR * (1 - outB)) + (bridgerectifier * outB);
else midSampleR = (midSampleR * (1 - outB)) - (bridgerectifier * outB);
// blend according to densityB control
bassSampleL *= (densityC + 1.0);
bridgerectifier = fabs(bassSampleL) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityC > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (bassSampleL > 0) bassSampleL = (bassSampleL * (1 - outC)) + (bridgerectifier * outC);
else bassSampleL = (bassSampleL * (1 - outC)) - (bridgerectifier * outC);
// blend according to densityC control
bassSampleR *= (densityC + 1.0);
bridgerectifier = fabs(bassSampleR) * 1.57079633;
if (bridgerectifier > 1.57079633) bridgerectifier = 1.57079633;
// max value for sine function
if (densityC > 0) bridgerectifier = sin(bridgerectifier);
else bridgerectifier = 1 - cos(bridgerectifier);
// produce either boosted or starved version
if (bassSampleR > 0) bassSampleR = (bassSampleR * (1 - outC)) + (bridgerectifier * outC);
else bassSampleR = (bassSampleR * (1 - outC)) - (bridgerectifier * outC);
// blend according to densityC control
inputSampleL = midSampleL;
inputSampleL += highSampleL;
inputSampleL += bassSampleL;
inputSampleR = midSampleR;
inputSampleR += highSampleR;
inputSampleR += bassSampleR;
}
// end EQ
// EQ lowpass is after all processing like the compressor that might produce hash
if (engageLowpass) {
if (flip) {
lowpassSampleLAA = (lowpassSampleLAA * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLAA;
lowpassSampleLBA = (lowpassSampleLBA * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLBA;
lowpassSampleLCA = (lowpassSampleLCA * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLCA;
lowpassSampleLDA = (lowpassSampleLDA * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLDA;
lowpassSampleLE = (lowpassSampleLE * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLE;
lowpassSampleRAA = (lowpassSampleRAA * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRAA;
lowpassSampleRBA = (lowpassSampleRBA * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRBA;
lowpassSampleRCA = (lowpassSampleRCA * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRCA;
lowpassSampleRDA = (lowpassSampleRDA * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRDA;
lowpassSampleRE = (lowpassSampleRE * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRE;
} else {
lowpassSampleLAB = (lowpassSampleLAB * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLAB;
lowpassSampleLBB = (lowpassSampleLBB * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLBB;
lowpassSampleLCB = (lowpassSampleLCB * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLCB;
lowpassSampleLDB = (lowpassSampleLDB * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLDB;
lowpassSampleLF = (lowpassSampleLF * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleL = lowpassSampleLF;
lowpassSampleRAB = (lowpassSampleRAB * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRAB;
lowpassSampleRBB = (lowpassSampleRBB * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRBB;
lowpassSampleRCB = (lowpassSampleRCB * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRCB;
lowpassSampleRDB = (lowpassSampleRDB * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRDB;
lowpassSampleRF = (lowpassSampleRF * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleR = lowpassSampleRF;
}
lowpassSampleLG = (lowpassSampleLG * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
lowpassSampleRG = (lowpassSampleRG * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
inputSampleL = (lowpassSampleLG * (1.0 - iirAmountC)) + (inputSampleL * iirAmountC);
inputSampleR = (lowpassSampleRG * (1.0 - iirAmountC)) + (inputSampleR * iirAmountC);
}
// built in output trim and dry/wet if desired
if (outputgain != 1.0) {
inputSampleL *= outputgain;
inputSampleR *= outputgain;
}
// begin 64 bit stereo floating point dither
// int expon; frexp((double)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
// inputSampleL += ((double(fpdL)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// frexp((double)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
// inputSampleR += ((double(fpdR)-uint32_t(0x7fffffff)) * 1.1e-44l * pow(2,expon+62));
// end 64 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
*in1++;
*in2++;
*out1++;
*out2++;
}
}
private:
double samplerate;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
double lastSampleL;
double last2SampleL;
double lastSampleR;
double last2SampleR;
// begin EQ
double iirHighSampleLA;
double iirHighSampleLB;
double iirHighSampleLC;
double iirHighSampleLD;
double iirHighSampleLE;
double iirLowSampleLA;
double iirLowSampleLB;
double iirLowSampleLC;
double iirLowSampleLD;
double iirLowSampleLE;
double iirHighSampleL;
double iirLowSampleL;
double iirHighSampleRA;
double iirHighSampleRB;
double iirHighSampleRC;
double iirHighSampleRD;
double iirHighSampleRE;
double iirLowSampleRA;
double iirLowSampleRB;
double iirLowSampleRC;
double iirLowSampleRD;
double iirLowSampleRE;
double iirHighSampleR;
double iirLowSampleR;
double tripletLA;
double tripletLB;
double tripletLC;
double tripletFactorL;
double tripletRA;
double tripletRB;
double tripletRC;
double tripletFactorR;
double lowpassSampleLAA;
double lowpassSampleLAB;
double lowpassSampleLBA;
double lowpassSampleLBB;
double lowpassSampleLCA;
double lowpassSampleLCB;
double lowpassSampleLDA;
double lowpassSampleLDB;
double lowpassSampleLE;
double lowpassSampleLF;
double lowpassSampleLG;
double lowpassSampleRAA;
double lowpassSampleRAB;
double lowpassSampleRBA;
double lowpassSampleRBB;
double lowpassSampleRCA;
double lowpassSampleRCB;
double lowpassSampleRDA;
double lowpassSampleRDB;
double lowpassSampleRE;
double lowpassSampleRF;
double lowpassSampleRG;
double highpassSampleLAA;
double highpassSampleLAB;
double highpassSampleLBA;
double highpassSampleLBB;
double highpassSampleLCA;
double highpassSampleLCB;
double highpassSampleLDA;
double highpassSampleLDB;
double highpassSampleLE;
double highpassSampleLF;
double highpassSampleRAA;
double highpassSampleRAB;
double highpassSampleRBA;
double highpassSampleRBB;
double highpassSampleRCA;
double highpassSampleRCB;
double highpassSampleRDA;
double highpassSampleRDB;
double highpassSampleRE;
double highpassSampleRF;
bool flip;
int flipthree;
// end EQ
float A;
float B;
float C;
float D;
float E;
float F;
float G;
float H;
double clamp(double& value)
{
if (value > 1) {
value = 1;
} else if (value < 0) {
value = 0;
}
return value;
}
};
} // namespace trnr

View File

@@ -1,28 +1,4 @@
/*
* chebyshev.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#define _USE_MATH_DEFINES
#include <array>
#include <math.h>

View File

@@ -1,613 +0,0 @@
/*
* spliteq.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "../util/audio_math.h"
#include "../util/smoother.h"
#include <cmath>
#include <vector>
namespace trnr {
// Filter type enum
enum filter_type {
LOWPASS = 0,
HIGHPASS = 1
};
struct cascade_filter {
filter_type type;
int stages; // Number of cascaded stages
double cutoff; // Cutoff frequency (Hz)
double samplerate; // Sample rate (Hz)
double alpha; // Filter coefficient
std::vector<double> state; // State per stage
};
inline void cascade_filter_setup(cascade_filter& f, filter_type _type, int _stages,
double _cutoff, double _samplerate)
{
f.type = _type;
f.stages = _stages;
f.cutoff = _cutoff;
f.samplerate = _samplerate;
// alpha = iirAmount = exp(-2 * pi * cutoff / samplerate);
double x = exp(-2.0 * M_PI * f.cutoff / f.samplerate);
f.alpha = 1.0 - x;
f.state.resize(f.stages, 0.0);
}
// Process one sample
inline double cascade_filter_process(cascade_filter& f, double input)
{
double out = input;
for (int i = 0; i < f.stages; ++i) {
if (f.type == LOWPASS) {
f.state[i] = (f.state[i] * (1.0 - f.alpha)) + (out * f.alpha);
out = f.state[i];
} else { // CASCADE_HIGHPASS
f.state[i] = (f.state[i] * (1.0 - f.alpha)) + (out * f.alpha);
out -= f.state[i];
}
}
return out;
}
// 2nd order Butterworth biquad filter
struct butterworth {
filter_type type;
double cutoff;
double a1, a2;
double b0, b1, b2;
double x1, x2; // previous inputs
double y1, y2; // previous outputs
};
// Biquad coefficient calculation
inline void butterworth_biquad_coeffs(butterworth& b, double samplerate)
{
double omega = 2.0 * M_PI * b.cutoff / samplerate;
double sin_omega = sin(omega);
double cos_omega = cos(omega);
double Q = 1.0 / sqrt(2.0); // Butterworth Q for 2nd order
double alpha = sin_omega / (2.0 * Q);
double a0 = 1.0 + alpha;
switch (b.type) {
case LOWPASS:
b.b0 = (1.0 - cos_omega) / 2.0 / a0;
b.b1 = (1.0 - cos_omega) / a0;
b.b2 = (1.0 - cos_omega) / 2.0 / a0;
b.a1 = -2.0 * cos_omega / a0;
b.a2 = (1.0 - alpha) / a0;
break;
case HIGHPASS:
b.b0 = (1.0 + cos_omega) / 2.0 / a0;
b.b1 = -(1.0 + cos_omega) / a0;
b.b2 = (1.0 + cos_omega) / 2.0 / a0;
b.a1 = -2.0 * cos_omega / a0;
b.a2 = (1.0 - alpha) / a0;
break;
}
}
// Biquad sample processing
inline double butterworth_biquad_process(butterworth& b, double input)
{
double y = b.b0 * input + b.b1 * b.x1 + b.b2 * b.x2 - b.a1 * b.y1 - b.a2 * b.y2;
b.x2 = b.x1;
b.x1 = input;
b.y2 = b.y1;
b.y1 = y;
return y;
}
struct aw_filter {
filter_type type;
float amount;
bool flip;
double sampleLAA;
double sampleLAB;
double sampleLBA;
double sampleLBB;
double sampleLCA;
double sampleLCB;
double sampleLDA;
double sampleLDB;
double sampleLE;
double sampleLF;
double sampleLG;
double samplerate;
};
inline void aw_filter_init(aw_filter& f, filter_type type, float amount,
double samplerate)
{
f.type = type;
f.amount = amount;
f.samplerate = samplerate;
f.sampleLAA = 0.0;
f.sampleLAB = 0.0;
f.sampleLBA = 0.0;
f.sampleLBB = 0.0;
f.sampleLCA = 0.0;
f.sampleLCB = 0.0;
f.sampleLDA = 0.0;
f.sampleLDB = 0.0;
f.sampleLE = 0.0;
f.sampleLF = 0.0;
f.sampleLG = 0.0;
f.flip = false;
}
inline void aw_filter_process_block(aw_filter& f, float* audio, int frames)
{
double overallscale = 1.0;
overallscale /= 44100.0;
double compscale = overallscale;
overallscale = f.samplerate;
compscale = compscale * overallscale;
bool engage = false;
double iir_amt = 0.0;
if (f.type == LOWPASS) {
iir_amt = (((f.amount * f.amount * 15.0) + 1.0) * 0.0188) + 0.7;
if (iir_amt > 1.0) iir_amt = 1.0;
if (((f.amount * f.amount * 15.0) + 1.0) < 15.99) engage = true;
} else if (f.type == HIGHPASS) {
iir_amt = (((f.amount * f.amount * 1570.0) + 30.0) * 1.0) / overallscale;
if (((f.amount * f.amount * 1570.0) + 30.0) > 30.01) engage = true;
}
for (int i = 0; i < frames; i++) {
float input = audio[i];
f.flip = !f.flip;
if (engage) {
switch (f.type) {
case LOWPASS:
if (f.flip) {
f.sampleLAA = (f.sampleLAA * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLAA;
f.sampleLBA = (f.sampleLBA * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLBA;
f.sampleLCA = (f.sampleLCA * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLCA;
f.sampleLDA = (f.sampleLDA * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLDA;
f.sampleLE = (f.sampleLE * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLE;
} else {
f.sampleLAB = (f.sampleLAB * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLAB;
f.sampleLBB = (f.sampleLBB * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLBB;
f.sampleLCB = (f.sampleLCB * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLCB;
f.sampleLDB = (f.sampleLDB * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLDB;
f.sampleLF = (f.sampleLF * (1.0 - iir_amt)) + (input * iir_amt);
input = f.sampleLF;
}
f.sampleLG = (f.sampleLG * (1.0 - iir_amt)) + (input * iir_amt);
input = (f.sampleLG * (1.0 - iir_amt)) + (input * iir_amt);
break;
case HIGHPASS:
if (f.flip) {
f.sampleLAA = (f.sampleLAA * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLAA;
f.sampleLBA = (f.sampleLBA * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLBA;
f.sampleLCA = (f.sampleLCA * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLCA;
f.sampleLDA = (f.sampleLDA * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLDA;
} else {
f.sampleLAB = (f.sampleLAB * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLAB;
f.sampleLBB = (f.sampleLBB * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLBB;
f.sampleLCB = (f.sampleLCB * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLCB;
f.sampleLDB = (f.sampleLDB * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLDB;
}
f.sampleLE = (f.sampleLE * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLE;
f.sampleLF = (f.sampleLF * (1.0 - iir_amt)) + (input * iir_amt);
input -= f.sampleLF;
break;
}
}
audio[i] = input;
}
}
enum spliteq_mode {
CASCADE_SUM,
LINKWITZ_RILEY
};
struct spliteq {
aw_filter lp_l, lp_r, hp_l, hp_r; // lowpass and highpass filters
// cascaded filters
cascade_filter bass_l, bass_r;
cascade_filter treble_l, treble_r;
// butterworth biquads
butterworth bass1_l, bass2_l, bass1_r, bass2_r;
// Mid: two cascaded highpass THEN two cascaded lowpass per channel
butterworth mid_hp1_l, mid_hp2_l, mid_lp1_l, mid_lp2_l;
butterworth mid_hp1_r, mid_hp2_r, mid_lp1_r, mid_lp2_r;
// Treble: two cascaded highpass filters per channel
butterworth treble1_l, treble2_l, treble1_r, treble2_r;
double low_mid_crossover = 150.0; // Hz
double mid_high_crossover = 1700.0; // Hz
// adjusted crossover frequencies for cascade filters
double low_mid_crossover_adj = 150.0;
double mid_high_crossover_adj = 1700.0;
double samplerate = 48000.0;
float bass_gain = 1.0f;
float mid_gain = 1.0f;
float treble_gain = 1.0f;
// adjusted gain for cascade filters
float bass_gain_adj = 1.0f;
float mid_gain_adj = 1.0f;
float treble_gain_adj = 1.0f;
spliteq_mode current_mode = CASCADE_SUM;
spliteq_mode target_mode = CASCADE_SUM;
bool transitioning = false;
smoother transition_smoother;
};
inline void spliteq_init(spliteq& eq, double samplerate, double low_mid_crossover,
double mid_high_crossover)
{
low_mid_crossover /= 2.0;
mid_high_crossover /= 2.0;
eq.samplerate = samplerate;
eq.low_mid_crossover = low_mid_crossover;
eq.mid_high_crossover = mid_high_crossover;
eq.low_mid_crossover_adj = low_mid_crossover;
eq.mid_high_crossover_adj = mid_high_crossover;
// initialize lp/hp filters
aw_filter_init(eq.lp_l, LOWPASS, 1.0f, samplerate);
aw_filter_init(eq.lp_r, LOWPASS, 1.0f, samplerate);
aw_filter_init(eq.hp_l, HIGHPASS, 0.0f, samplerate);
aw_filter_init(eq.hp_r, HIGHPASS, 0.0f, samplerate);
// init cascade filters
cascade_filter_setup(eq.bass_l, LOWPASS, 2, low_mid_crossover, samplerate);
cascade_filter_setup(eq.bass_r, LOWPASS, 2, low_mid_crossover, samplerate);
cascade_filter_setup(eq.treble_l, HIGHPASS, 2, mid_high_crossover, samplerate);
cascade_filter_setup(eq.treble_r, HIGHPASS, 2, mid_high_crossover, samplerate);
// init butterworth filters
// bass filters
eq.bass1_l.type = LOWPASS;
eq.bass1_l.cutoff = low_mid_crossover;
eq.bass1_l.x1 = eq.bass1_l.x2 = eq.bass1_l.y1 = eq.bass1_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.bass1_l, samplerate);
eq.bass2_l.type = LOWPASS;
eq.bass2_l.cutoff = low_mid_crossover;
eq.bass2_l.x1 = eq.bass2_l.x2 = eq.bass2_l.y1 = eq.bass2_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.bass2_l, samplerate);
eq.bass1_r.type = LOWPASS;
eq.bass1_r.cutoff = low_mid_crossover;
eq.bass1_r.x1 = eq.bass1_r.x2 = eq.bass1_r.y1 = eq.bass1_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.bass1_r, samplerate);
eq.bass2_r.type = LOWPASS;
eq.bass2_r.cutoff = low_mid_crossover;
eq.bass2_r.x1 = eq.bass2_r.x2 = eq.bass2_r.y1 = eq.bass2_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.bass2_r, samplerate);
// mid filters (HPF x2, then LPF x2)
eq.mid_hp1_l.type = HIGHPASS;
eq.mid_hp1_l.cutoff = low_mid_crossover;
eq.mid_hp1_l.x1 = eq.mid_hp1_l.x2 = eq.mid_hp1_l.y1 = eq.mid_hp1_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_hp1_l, samplerate);
eq.mid_hp2_l.type = HIGHPASS;
eq.mid_hp2_l.cutoff = low_mid_crossover;
eq.mid_hp2_l.x1 = eq.mid_hp2_l.x2 = eq.mid_hp2_l.y1 = eq.mid_hp2_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_hp2_l, samplerate);
eq.mid_lp1_l.type = LOWPASS;
eq.mid_lp1_l.cutoff = mid_high_crossover;
eq.mid_lp1_l.x1 = eq.mid_lp1_l.x2 = eq.mid_lp1_l.y1 = eq.mid_lp1_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_lp1_l, samplerate);
eq.mid_lp2_l.type = LOWPASS;
eq.mid_lp2_l.cutoff = mid_high_crossover;
eq.mid_lp2_l.x1 = eq.mid_lp2_l.x2 = eq.mid_lp2_l.y1 = eq.mid_lp2_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_lp2_l, samplerate);
eq.mid_hp1_r.type = HIGHPASS;
eq.mid_hp1_r.cutoff = low_mid_crossover;
eq.mid_hp1_r.x1 = eq.mid_hp1_r.x2 = eq.mid_hp1_r.y1 = eq.mid_hp1_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_hp1_r, samplerate);
eq.mid_hp2_r.type = HIGHPASS;
eq.mid_hp2_r.cutoff = low_mid_crossover;
eq.mid_hp2_r.x1 = eq.mid_hp2_r.x2 = eq.mid_hp2_r.y1 = eq.mid_hp2_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_hp2_r, samplerate);
eq.mid_lp1_r.type = LOWPASS;
eq.mid_lp1_r.cutoff = mid_high_crossover;
eq.mid_lp1_r.x1 = eq.mid_lp1_r.x2 = eq.mid_lp1_r.y1 = eq.mid_lp1_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_lp1_r, samplerate);
eq.mid_lp2_r.type = LOWPASS;
eq.mid_lp2_r.cutoff = mid_high_crossover;
eq.mid_lp2_r.x1 = eq.mid_lp2_r.x2 = eq.mid_lp2_r.y1 = eq.mid_lp2_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.mid_lp2_r, samplerate);
// treble filters
eq.treble1_l.type = HIGHPASS;
eq.treble1_l.cutoff = mid_high_crossover;
eq.treble1_l.x1 = eq.treble1_l.x2 = eq.treble1_l.y1 = eq.treble1_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.treble1_l, samplerate);
eq.treble2_l.type = HIGHPASS;
eq.treble2_l.cutoff = mid_high_crossover;
eq.treble2_l.x1 = eq.treble2_l.x2 = eq.treble2_l.y1 = eq.treble2_l.y2 = 0.0;
butterworth_biquad_coeffs(eq.treble2_l, samplerate);
eq.treble1_r.type = HIGHPASS;
eq.treble1_r.cutoff = mid_high_crossover;
eq.treble1_r.x1 = eq.treble1_r.x2 = eq.treble1_r.y1 = eq.treble1_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.treble1_r, samplerate);
eq.treble2_r.type = HIGHPASS;
eq.treble2_r.cutoff = mid_high_crossover;
eq.treble2_r.x1 = eq.treble2_r.x2 = eq.treble2_r.y1 = eq.treble2_r.y2 = 0.0;
butterworth_biquad_coeffs(eq.treble2_r, samplerate);
smoother_init(eq.transition_smoother, samplerate, 50.0f, 1.0f);
}
inline void spliteq_set_mode(spliteq& eq, spliteq_mode mode)
{
if (eq.target_mode != mode) {
eq.target_mode = mode;
eq.transitioning = true;
smoother_set_target(eq.transition_smoother, 0.0f);
}
}
inline void linkwitz_riley_process(spliteq& eq, float**& audio, int& i)
{
// left channel
double input_l = audio[0][i];
// bass
double bass_l = butterworth_biquad_process(eq.bass1_l, input_l);
bass_l = butterworth_biquad_process(eq.bass2_l, bass_l);
// mid
double mid_l = butterworth_biquad_process(eq.mid_hp1_l, input_l);
mid_l = butterworth_biquad_process(eq.mid_hp2_l, mid_l);
mid_l = butterworth_biquad_process(eq.mid_lp1_l, mid_l);
mid_l = butterworth_biquad_process(eq.mid_lp2_l, mid_l);
// treble
double treble_l = butterworth_biquad_process(eq.treble1_l, input_l);
treble_l = butterworth_biquad_process(eq.treble2_l, treble_l);
// apply gains
bass_l *= eq.bass_gain;
mid_l *= eq.mid_gain;
treble_l *= eq.treble_gain;
// sum bands
audio[0][i] = bass_l + mid_l + treble_l;
// right channel
double input_r = audio[1][i];
double bass_r = butterworth_biquad_process(eq.bass1_r, input_r);
bass_r = butterworth_biquad_process(eq.bass2_r, bass_r);
double mid_r = butterworth_biquad_process(eq.mid_hp1_r, input_r);
mid_r = butterworth_biquad_process(eq.mid_hp2_r, mid_r);
mid_r = butterworth_biquad_process(eq.mid_lp1_r, mid_r);
mid_r = butterworth_biquad_process(eq.mid_lp2_r, mid_r);
double treble_r = butterworth_biquad_process(eq.treble1_r, input_r);
treble_r = butterworth_biquad_process(eq.treble2_r, treble_r);
bass_r *= eq.bass_gain;
mid_r *= eq.mid_gain;
treble_r *= eq.treble_gain;
audio[1][i] = bass_r + mid_r + treble_r;
}
inline void cascade_sum_process(spliteq& eq, float**& audio, int& i)
{
double input_l = audio[0][i];
double input_r = audio[1][i];
double bass_l = cascade_filter_process(eq.bass_l, input_l);
double bass_r = cascade_filter_process(eq.bass_r, input_r);
double treble_l = cascade_filter_process(eq.treble_l, input_l);
double treble_r = cascade_filter_process(eq.treble_r, input_r);
double mid_l = input_l - bass_l - treble_l;
double mid_r = input_r - bass_r - treble_r;
// apply gains
bass_l *= eq.bass_gain_adj;
bass_r *= eq.bass_gain_adj;
mid_l *= eq.mid_gain_adj;
mid_r *= eq.mid_gain_adj;
treble_l *= eq.treble_gain_adj;
treble_r *= eq.treble_gain_adj;
// sum bands
audio[0][i] = bass_l + mid_l + treble_l;
audio[1][i] = bass_r + mid_r + treble_r;
}
inline void spliteq_process_block(spliteq& eq, float** audio, int frames)
{
// highpass filters
aw_filter_process_block(eq.hp_l, audio[0], frames);
aw_filter_process_block(eq.hp_r, audio[1], frames);
for (int i = 0; i < frames; i++) {
float smooth_gain = 1.0f;
if (eq.transitioning) {
smooth_gain = smoother_process_sample(eq.transition_smoother);
if (smooth_gain == 0.f) {
smoother_set_target(eq.transition_smoother, 1.0);
eq.current_mode = eq.target_mode;
} else if (smooth_gain == 1.f) {
eq.transitioning = false;
}
}
if (eq.current_mode == LINKWITZ_RILEY) {
linkwitz_riley_process(eq, audio, i);
} else if (eq.current_mode == CASCADE_SUM) {
cascade_sum_process(eq, audio, i);
}
audio[0][i] *= smooth_gain;
audio[1][i] *= smooth_gain;
}
// lowpass filters
aw_filter_process_block(eq.lp_l, audio[0], frames);
aw_filter_process_block(eq.lp_r, audio[1], frames);
}
inline void spliteq_update(spliteq& eq, double hp_freq, double lp_freq,
double low_mid_crossover, double mid_high_crossover,
double bass_gain, double mid_gain, double treble_gain)
{
low_mid_crossover /= 2.0;
mid_high_crossover /= 2.0;
eq.bass_gain = db_2_lin(bass_gain);
eq.mid_gain = db_2_lin(mid_gain);
eq.treble_gain = db_2_lin(treble_gain);
if (bass_gain > 0.f) {
eq.bass_gain = eq.bass_gain_adj = db_2_lin(bass_gain * 0.85f);
eq.low_mid_crossover_adj = low_mid_crossover;
} else {
eq.bass_gain_adj = db_2_lin(bass_gain);
eq.low_mid_crossover_adj = low_mid_crossover * 2.0;
}
if (mid_gain > 0.0f) eq.mid_gain_adj = db_2_lin(mid_gain * 0.85f);
else eq.mid_gain_adj = db_2_lin(mid_gain * 0.74f);
if (treble_gain > 0.f) {
eq.treble_gain_adj = db_2_lin(treble_gain * 1.1f);
eq.mid_high_crossover_adj = mid_high_crossover;
} else {
eq.treble_gain_adj = db_2_lin(treble_gain);
eq.mid_high_crossover_adj = mid_high_crossover / 2.0;
}
eq.low_mid_crossover = low_mid_crossover;
eq.mid_high_crossover = mid_high_crossover;
eq.hp_l.amount = hp_freq;
eq.hp_r.amount = hp_freq;
eq.lp_l.amount = lp_freq;
eq.lp_r.amount = lp_freq;
cascade_filter_setup(eq.bass_l, LOWPASS, 2, eq.low_mid_crossover_adj, eq.samplerate);
cascade_filter_setup(eq.bass_r, LOWPASS, 2, eq.low_mid_crossover_adj, eq.samplerate);
cascade_filter_setup(eq.treble_l, HIGHPASS, 2, eq.mid_high_crossover_adj,
eq.samplerate);
cascade_filter_setup(eq.treble_r, HIGHPASS, 2, eq.mid_high_crossover_adj,
eq.samplerate);
eq.bass1_l.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.bass1_l, eq.samplerate);
eq.bass2_l.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.bass2_l, eq.samplerate);
eq.bass1_r.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.bass1_r, eq.samplerate);
eq.bass2_r.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.bass2_r, eq.samplerate);
eq.mid_hp1_l.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.mid_hp1_l, eq.samplerate);
eq.mid_hp2_l.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.mid_hp2_l, eq.samplerate);
eq.mid_lp1_l.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.mid_lp1_l, eq.samplerate);
eq.mid_lp2_l.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.mid_lp2_l, eq.samplerate);
eq.mid_hp1_r.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.mid_hp1_r, eq.samplerate);
eq.mid_hp2_r.cutoff = low_mid_crossover;
butterworth_biquad_coeffs(eq.mid_hp2_r, eq.samplerate);
eq.mid_lp1_r.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.mid_lp1_r, eq.samplerate);
eq.mid_lp2_r.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.mid_lp2_r, eq.samplerate);
eq.treble1_l.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.treble1_l, eq.samplerate);
eq.treble2_l.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.treble2_l, eq.samplerate);
eq.treble1_r.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.treble1_r, eq.samplerate);
eq.treble2_r.cutoff = mid_high_crossover;
butterworth_biquad_coeffs(eq.treble2_r, eq.samplerate);
}
inline void spliteq_update(spliteq& eq, double bass_gain, double mid_gain,
double treble_gain)
{
trnr::spliteq_update(eq, eq.hp_l.amount, eq.lp_l.amount, eq.low_mid_crossover * 2.0,
eq.mid_high_crossover * 2.0, bass_gain, mid_gain, treble_gain);
}
} // namespace trnr

280
filter/ybandpass.h Normal file
View File

@@ -0,0 +1,280 @@
#pragma once
#define _USE_MATH_DEFINES
#include <array>
#include <math.h>
#include <vector>
namespace trnr {
template <typename t_sample>
// Bandpass filter based on YBandpass by Chris Johnson
class ybandpass {
public:
ybandpass(double _samplerate)
: samplerate {_samplerate}
, A {0.1f}
, B {1.0f}
, C {0.0f}
, D {0.1f}
, E {0.9f}
, F {1.0f}
, fpdL {0}
, fpdR {0}
, biquad {0}
{
for (int x = 0; x < biq_total; x++) { biquad[x] = 0.0; }
powFactorA = 1.0;
powFactorB = 1.0;
inTrimA = 0.1;
inTrimB = 0.1;
outTrimA = 1.0;
outTrimB = 1.0;
for (int x = 0; x < fix_total; x++) {
fixA[x] = 0.0;
fixB[x] = 0.0;
}
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
void set_drive(float value) { A = value * 0.9 + 0.1; }
void set_frequency(float value) { B = value; }
void set_resonance(float value) { C = value; }
void set_edge(float value) { D = value; }
void set_output(float value) { E = value; }
void set_mix(float value) { F = value; }
void processblock(t_sample** inputs, t_sample** outputs, int blockSize)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
int inFramesToProcess = blockSize;
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
inTrimA = inTrimB;
inTrimB = A * 10.0;
biquad[biq_freq] = pow(B, 3) * 20000.0;
if (biquad[biq_freq] < 15.0) biquad[biq_freq] = 15.0;
biquad[biq_freq] /= samplerate;
biquad[biq_reso] = (pow(C, 2) * 15.0) + 0.5571;
biquad[biq_aA0] = biquad[biq_aB0];
// biquad[biq_aA1] = biquad[biq_aB1];
biquad[biq_aA2] = biquad[biq_aB2];
biquad[biq_bA1] = biquad[biq_bB1];
biquad[biq_bA2] = biquad[biq_bB2];
// previous run through the buffer is still in the filter, so we move it
// to the A section and now it's the new starting point.
double K = tan(M_PI * biquad[biq_freq]);
double norm = 1.0 / (1.0 + K / biquad[biq_reso] + K * K);
biquad[biq_aB0] = K / biquad[biq_reso] * norm;
// biquad[biq_aB1] = 0.0; //bandpass can simplify the biquad kernel: leave out this multiply
biquad[biq_aB2] = -biquad[biq_aB0];
biquad[biq_bB1] = 2.0 * (K * K - 1.0) * norm;
biquad[biq_bB2] = (1.0 - K / biquad[biq_reso] + K * K) * norm;
// for the coefficient-interpolated biquad filter
powFactorA = powFactorB;
powFactorB = pow(D + 0.9, 4);
// 1.0 == target neutral
outTrimA = outTrimB;
outTrimB = E;
double wet = F;
fixA[fix_freq] = fixB[fix_freq] = 20000.0 / samplerate;
fixA[fix_reso] = fixB[fix_reso] = 0.7071; // butterworth Q
K = tan(M_PI * fixA[fix_freq]);
norm = 1.0 / (1.0 + K / fixA[fix_reso] + K * K);
fixA[fix_a0] = fixB[fix_a0] = K * K * norm;
fixA[fix_a1] = fixB[fix_a1] = 2.0 * fixA[fix_a0];
fixA[fix_a2] = fixB[fix_a2] = fixA[fix_a0];
fixA[fix_b1] = fixB[fix_b1] = 2.0 * (K * K - 1.0) * norm;
fixA[fix_b2] = fixB[fix_b2] = (1.0 - K / fixA[fix_reso] + K * K) * norm;
// for the fixed-position biquad filter
for (int s = 0; s < blockSize; s++) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double drySampleL = inputSampleL;
double drySampleR = inputSampleR;
double temp = (double)s / inFramesToProcess;
biquad[biq_a0] = (biquad[biq_aA0] * temp) + (biquad[biq_aB0] * (1.0 - temp));
// biquad[biq_a1] = (biquad[biq_aA1]*temp)+(biquad[biq_aB1]*(1.0-temp));
biquad[biq_a2] = (biquad[biq_aA2] * temp) + (biquad[biq_aB2] * (1.0 - temp));
biquad[biq_b1] = (biquad[biq_bA1] * temp) + (biquad[biq_bB1] * (1.0 - temp));
biquad[biq_b2] = (biquad[biq_bA2] * temp) + (biquad[biq_bB2] * (1.0 - temp));
// this is the interpolation code for the biquad
double powFactor = (powFactorA * temp) + (powFactorB * (1.0 - temp));
double inTrim = (inTrimA * temp) + (inTrimB * (1.0 - temp));
double outTrim = (outTrimA * temp) + (outTrimB * (1.0 - temp));
inputSampleL *= inTrim;
inputSampleR *= inTrim;
temp = (inputSampleL * fixA[fix_a0]) + fixA[fix_sL1];
fixA[fix_sL1] = (inputSampleL * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sL2];
fixA[fix_sL2] = (inputSampleL * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixA[fix_a0]) + fixA[fix_sR1];
fixA[fix_sR1] = (inputSampleR * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sR2];
fixA[fix_sR2] = (inputSampleR * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, powFactor);
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, powFactor);
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, powFactor);
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, powFactor);
temp = (inputSampleL * biquad[biq_a0]) + biquad[biq_sL1];
biquad[biq_sL1] = -(temp * biquad[biq_b1]) + biquad[biq_sL2];
biquad[biq_sL2] = (inputSampleL * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleL = temp; // coefficient interpolating biquad filter
temp = (inputSampleR * biquad[biq_a0]) + biquad[biq_sR1];
biquad[biq_sR1] = -(temp * biquad[biq_b1]) + biquad[biq_sR2];
biquad[biq_sR2] = (inputSampleR * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleR = temp; // coefficient interpolating biquad filter
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, (1.0 / powFactor));
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, (1.0 / powFactor));
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, (1.0 / powFactor));
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, (1.0 / powFactor));
inputSampleL *= outTrim;
inputSampleR *= outTrim;
temp = (inputSampleL * fixB[fix_a0]) + fixB[fix_sL1];
fixB[fix_sL1] = (inputSampleL * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sL2];
fixB[fix_sL2] = (inputSampleL * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixB[fix_a0]) + fixB[fix_sR1];
fixB[fix_sR1] = (inputSampleR * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sR2];
fixB[fix_sR2] = (inputSampleR * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
if (wet < 1.0) {
inputSampleL = (inputSampleL * wet) + (drySampleL * (1.0 - wet));
inputSampleR = (inputSampleR * wet) + (drySampleR * (1.0 - wet));
}
// begin 32 bit stereo floating point dither
int expon;
frexpf((float)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
inputSampleL += ((double(fpdL) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
frexpf((float)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
inputSampleR += ((double(fpdR) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
// end 32 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
enum {
biq_freq,
biq_reso,
biq_a0,
biq_a1,
biq_a2,
biq_b1,
biq_b2,
biq_aA0,
biq_aA1,
biq_aA2,
biq_bA1,
biq_bA2,
biq_aB0,
biq_aB1,
biq_aB2,
biq_bB1,
biq_bB2,
biq_sL1,
biq_sL2,
biq_sR1,
biq_sR2,
biq_total
}; // coefficient interpolating biquad filter, stereo
std::array<double, biq_total> biquad;
double powFactorA;
double powFactorB;
double inTrimA;
double inTrimB;
double outTrimA;
double outTrimB;
enum {
fix_freq,
fix_reso,
fix_a0,
fix_a1,
fix_a2,
fix_b1,
fix_b2,
fix_sL1,
fix_sL2,
fix_sR1,
fix_sR2,
fix_total
}; // fixed frequency biquad filter for ultrasonics, stereo
std::array<double, fix_total> fixA;
std::array<double, fix_total> fixB;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
float A;
float B;
float C;
float D;
float E;
float F; // parameters. Always 0-1, and we scale/alter them elsewhere.
};
} // namespace trnr

280
filter/yhighpass.h Normal file
View File

@@ -0,0 +1,280 @@
#pragma once
#define _USE_MATH_DEFINES
#include <array>
#include <math.h>
#include <vector>
namespace trnr {
template <typename t_sample>
// Highpass filter based on YHighpass by Chris Johnson
class yhighpass {
public:
yhighpass(double _samplerate)
: samplerate {_samplerate}
, A {0.1f}
, B {1.0f}
, C {0.0f}
, D {0.1f}
, E {0.9f}
, F {1.0f}
, fpdL {0}
, fpdR {0}
, biquad {0}
{
for (int x = 0; x < biq_total; x++) { biquad[x] = 0.0; }
powFactorA = 1.0;
powFactorB = 1.0;
inTrimA = 0.1;
inTrimB = 0.1;
outTrimA = 1.0;
outTrimB = 1.0;
for (int x = 0; x < fix_total; x++) {
fixA[x] = 0.0;
fixB[x] = 0.0;
}
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
void set_drive(float value) { A = value * 0.9 + 0.1; }
void set_frequency(float value) { B = value; }
void set_resonance(float value) { C = value; }
void set_edge(float value) { D = value; }
void set_output(float value) { E = value; }
void set_mix(float value) { F = value; }
void processblock(t_sample** inputs, t_sample** outputs, int blockSize)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
int inFramesToProcess = blockSize;
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
inTrimA = inTrimB;
inTrimB = A * 10.0;
biquad[biq_freq] = pow(B, 3) * 20000.0;
if (biquad[biq_freq] < 15.0) biquad[biq_freq] = 15.0;
biquad[biq_freq] /= samplerate;
biquad[biq_reso] = (pow(C, 2) * 15.0) + 0.5571;
biquad[biq_aA0] = biquad[biq_aB0];
biquad[biq_aA1] = biquad[biq_aB1];
biquad[biq_aA2] = biquad[biq_aB2];
biquad[biq_bA1] = biquad[biq_bB1];
biquad[biq_bA2] = biquad[biq_bB2];
// previous run through the buffer is still in the filter, so we move it
// to the A section and now it's the new starting point.
double K = tan(M_PI * biquad[biq_freq]);
double norm = 1.0 / (1.0 + K / biquad[biq_reso] + K * K);
biquad[biq_aB0] = norm;
biquad[biq_aB1] = -2.0 * biquad[biq_aB0];
biquad[biq_aB2] = biquad[biq_aB0];
biquad[biq_bB1] = 2.0 * (K * K - 1.0) * norm;
biquad[biq_bB2] = (1.0 - K / biquad[biq_reso] + K * K) * norm;
// for the coefficient-interpolated biquad filter
powFactorA = powFactorB;
powFactorB = pow(D + 0.9, 4);
// 1.0 == target neutral
outTrimA = outTrimB;
outTrimB = E;
double wet = F;
fixA[fix_freq] = fixB[fix_freq] = 20000.0 / samplerate;
fixA[fix_reso] = fixB[fix_reso] = 0.7071; // butterworth Q
K = tan(M_PI * fixA[fix_freq]);
norm = 1.0 / (1.0 + K / fixA[fix_reso] + K * K);
fixA[fix_a0] = fixB[fix_a0] = K * K * norm;
fixA[fix_a1] = fixB[fix_a1] = 2.0 * fixA[fix_a0];
fixA[fix_a2] = fixB[fix_a2] = fixA[fix_a0];
fixA[fix_b1] = fixB[fix_b1] = 2.0 * (K * K - 1.0) * norm;
fixA[fix_b2] = fixB[fix_b2] = (1.0 - K / fixA[fix_reso] + K * K) * norm;
// for the fixed-position biquad filter
for (int s = 0; s < blockSize; s++) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double drySampleL = inputSampleL;
double drySampleR = inputSampleR;
double temp = (double)s / inFramesToProcess;
biquad[biq_a0] = (biquad[biq_aA0] * temp) + (biquad[biq_aB0] * (1.0 - temp));
biquad[biq_a1] = (biquad[biq_aA1] * temp) + (biquad[biq_aB1] * (1.0 - temp));
biquad[biq_a2] = (biquad[biq_aA2] * temp) + (biquad[biq_aB2] * (1.0 - temp));
biquad[biq_b1] = (biquad[biq_bA1] * temp) + (biquad[biq_bB1] * (1.0 - temp));
biquad[biq_b2] = (biquad[biq_bA2] * temp) + (biquad[biq_bB2] * (1.0 - temp));
// this is the interpolation code for the biquad
double powFactor = (powFactorA * temp) + (powFactorB * (1.0 - temp));
double inTrim = (inTrimA * temp) + (inTrimB * (1.0 - temp));
double outTrim = (outTrimA * temp) + (outTrimB * (1.0 - temp));
inputSampleL *= inTrim;
inputSampleR *= inTrim;
temp = (inputSampleL * fixA[fix_a0]) + fixA[fix_sL1];
fixA[fix_sL1] = (inputSampleL * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sL2];
fixA[fix_sL2] = (inputSampleL * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixA[fix_a0]) + fixA[fix_sR1];
fixA[fix_sR1] = (inputSampleR * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sR2];
fixA[fix_sR2] = (inputSampleR * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, powFactor);
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, powFactor);
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, powFactor);
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, powFactor);
temp = (inputSampleL * biquad[biq_a0]) + biquad[biq_sL1];
biquad[biq_sL1] = (inputSampleL * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sL2];
biquad[biq_sL2] = (inputSampleL * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleL = temp; // coefficient interpolating biquad filter
temp = (inputSampleR * biquad[biq_a0]) + biquad[biq_sR1];
biquad[biq_sR1] = (inputSampleR * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sR2];
biquad[biq_sR2] = (inputSampleR * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleR = temp; // coefficient interpolating biquad filter
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, (1.0 / powFactor));
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, (1.0 / powFactor));
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, (1.0 / powFactor));
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, (1.0 / powFactor));
inputSampleL *= outTrim;
inputSampleR *= outTrim;
temp = (inputSampleL * fixB[fix_a0]) + fixB[fix_sL1];
fixB[fix_sL1] = (inputSampleL * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sL2];
fixB[fix_sL2] = (inputSampleL * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixB[fix_a0]) + fixB[fix_sR1];
fixB[fix_sR1] = (inputSampleR * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sR2];
fixB[fix_sR2] = (inputSampleR * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
if (wet < 1.0) {
inputSampleL = (inputSampleL * wet) + (drySampleL * (1.0 - wet));
inputSampleR = (inputSampleR * wet) + (drySampleR * (1.0 - wet));
}
// begin 32 bit stereo floating point dither
int expon;
frexpf((float)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
inputSampleL += ((double(fpdL) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
frexpf((float)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
inputSampleR += ((double(fpdR) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
// end 32 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
enum {
biq_freq,
biq_reso,
biq_a0,
biq_a1,
biq_a2,
biq_b1,
biq_b2,
biq_aA0,
biq_aA1,
biq_aA2,
biq_bA1,
biq_bA2,
biq_aB0,
biq_aB1,
biq_aB2,
biq_bB1,
biq_bB2,
biq_sL1,
biq_sL2,
biq_sR1,
biq_sR2,
biq_total
}; // coefficient interpolating biquad filter, stereo
std::array<double, biq_total> biquad;
double powFactorA;
double powFactorB;
double inTrimA;
double inTrimB;
double outTrimA;
double outTrimB;
enum {
fix_freq,
fix_reso,
fix_a0,
fix_a1,
fix_a2,
fix_b1,
fix_b2,
fix_sL1,
fix_sL2,
fix_sR1,
fix_sR2,
fix_total
}; // fixed frequency biquad filter for ultrasonics, stereo
std::array<double, fix_total> fixA;
std::array<double, fix_total> fixB;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
float A;
float B;
float C;
float D;
float E;
float F; // parameters. Always 0-1, and we scale/alter them elsewhere.
};
} // namespace trnr

280
filter/ylowpass.h Normal file
View File

@@ -0,0 +1,280 @@
#pragma once
#define _USE_MATH_DEFINES
#include <array>
#include <math.h>
#include <vector>
namespace trnr {
template <typename t_sample>
// Lowpass filter based on YLowpass by Chris Johnson
class ylowpass {
public:
ylowpass(double _samplerate)
: samplerate {_samplerate}
, A {0.1f}
, B {1.0f}
, C {0.0f}
, D {0.1f}
, E {0.9f}
, F {1.0f}
, fpdL {0}
, fpdR {0}
, biquad {0}
{
for (int x = 0; x < biq_total; x++) { biquad[x] = 0.0; }
powFactorA = 1.0;
powFactorB = 1.0;
inTrimA = 0.1;
inTrimB = 0.1;
outTrimA = 1.0;
outTrimB = 1.0;
for (int x = 0; x < fix_total; x++) {
fixA[x] = 0.0;
fixB[x] = 0.0;
}
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
void set_drive(float value) { A = value * 0.9 + 0.1; }
void set_frequency(float value) { B = value; }
void set_resonance(float value) { C = value; }
void set_edge(float value) { D = value; }
void set_output(float value) { E = value; }
void set_mix(float value) { F = value; }
void processblock(t_sample** inputs, t_sample** outputs, int blockSize)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
int inFramesToProcess = blockSize;
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
inTrimA = inTrimB;
inTrimB = A * 10.0;
biquad[biq_freq] = pow(B, 3) * 20000.0;
if (biquad[biq_freq] < 15.0) biquad[biq_freq] = 15.0;
biquad[biq_freq] /= samplerate;
biquad[biq_reso] = (pow(C, 2) * 15.0) + 0.5571;
biquad[biq_aA0] = biquad[biq_aB0];
biquad[biq_aA1] = biquad[biq_aB1];
biquad[biq_aA2] = biquad[biq_aB2];
biquad[biq_bA1] = biquad[biq_bB1];
biquad[biq_bA2] = biquad[biq_bB2];
// previous run through the buffer is still in the filter, so we move it
// to the A section and now it's the new starting point.
double K = tan(M_PI * biquad[biq_freq]);
double norm = 1.0 / (1.0 + K / biquad[biq_reso] + K * K);
biquad[biq_aB0] = K * K * norm;
biquad[biq_aB1] = 2.0 * biquad[biq_aB0];
biquad[biq_aB2] = biquad[biq_aB0];
biquad[biq_bB1] = 2.0 * (K * K - 1.0) * norm;
biquad[biq_bB2] = (1.0 - K / biquad[biq_reso] + K * K) * norm;
// for the coefficient-interpolated biquad filter
powFactorA = powFactorB;
powFactorB = pow(D + 0.9, 4);
// 1.0 == target neutral
outTrimA = outTrimB;
outTrimB = E;
double wet = F;
fixA[fix_freq] = fixB[fix_freq] = 20000.0 / samplerate;
fixA[fix_reso] = fixB[fix_reso] = 0.7071; // butterworth Q
K = tan(M_PI * fixA[fix_freq]);
norm = 1.0 / (1.0 + K / fixA[fix_reso] + K * K);
fixA[fix_a0] = fixB[fix_a0] = K * K * norm;
fixA[fix_a1] = fixB[fix_a1] = 2.0 * fixA[fix_a0];
fixA[fix_a2] = fixB[fix_a2] = fixA[fix_a0];
fixA[fix_b1] = fixB[fix_b1] = 2.0 * (K * K - 1.0) * norm;
fixA[fix_b2] = fixB[fix_b2] = (1.0 - K / fixA[fix_reso] + K * K) * norm;
// for the fixed-position biquad filter
for (int s = 0; s < blockSize; s++) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double drySampleL = inputSampleL;
double drySampleR = inputSampleR;
double temp = (double)s / inFramesToProcess;
biquad[biq_a0] = (biquad[biq_aA0] * temp) + (biquad[biq_aB0] * (1.0 - temp));
biquad[biq_a1] = (biquad[biq_aA1] * temp) + (biquad[biq_aB1] * (1.0 - temp));
biquad[biq_a2] = (biquad[biq_aA2] * temp) + (biquad[biq_aB2] * (1.0 - temp));
biquad[biq_b1] = (biquad[biq_bA1] * temp) + (biquad[biq_bB1] * (1.0 - temp));
biquad[biq_b2] = (biquad[biq_bA2] * temp) + (biquad[biq_bB2] * (1.0 - temp));
// this is the interpolation code for the biquad
double powFactor = (powFactorA * temp) + (powFactorB * (1.0 - temp));
double inTrim = (inTrimA * temp) + (inTrimB * (1.0 - temp));
double outTrim = (outTrimA * temp) + (outTrimB * (1.0 - temp));
inputSampleL *= inTrim;
inputSampleR *= inTrim;
temp = (inputSampleL * fixA[fix_a0]) + fixA[fix_sL1];
fixA[fix_sL1] = (inputSampleL * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sL2];
fixA[fix_sL2] = (inputSampleL * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixA[fix_a0]) + fixA[fix_sR1];
fixA[fix_sR1] = (inputSampleR * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sR2];
fixA[fix_sR2] = (inputSampleR * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, powFactor);
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, powFactor);
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, powFactor);
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, powFactor);
temp = (inputSampleL * biquad[biq_a0]) + biquad[biq_sL1];
biquad[biq_sL1] = (inputSampleL * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sL2];
biquad[biq_sL2] = (inputSampleL * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleL = temp; // coefficient interpolating biquad filter
temp = (inputSampleR * biquad[biq_a0]) + biquad[biq_sR1];
biquad[biq_sR1] = (inputSampleR * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sR2];
biquad[biq_sR2] = (inputSampleR * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleR = temp; // coefficient interpolating biquad filter
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, (1.0 / powFactor));
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, (1.0 / powFactor));
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, (1.0 / powFactor));
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, (1.0 / powFactor));
inputSampleL *= outTrim;
inputSampleR *= outTrim;
temp = (inputSampleL * fixB[fix_a0]) + fixB[fix_sL1];
fixB[fix_sL1] = (inputSampleL * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sL2];
fixB[fix_sL2] = (inputSampleL * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixB[fix_a0]) + fixB[fix_sR1];
fixB[fix_sR1] = (inputSampleR * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sR2];
fixB[fix_sR2] = (inputSampleR * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
if (wet < 1.0) {
inputSampleL = (inputSampleL * wet) + (drySampleL * (1.0 - wet));
inputSampleR = (inputSampleR * wet) + (drySampleR * (1.0 - wet));
}
// begin 32 bit stereo floating point dither
int expon;
frexpf((float)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
inputSampleL += ((double(fpdL) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
frexpf((float)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
inputSampleR += ((double(fpdR) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
// end 32 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
enum {
biq_freq,
biq_reso,
biq_a0,
biq_a1,
biq_a2,
biq_b1,
biq_b2,
biq_aA0,
biq_aA1,
biq_aA2,
biq_bA1,
biq_bA2,
biq_aB0,
biq_aB1,
biq_aB2,
biq_bB1,
biq_bB2,
biq_sL1,
biq_sL2,
biq_sR1,
biq_sR2,
biq_total
}; // coefficient interpolating biquad filter, stereo
std::array<double, biq_total> biquad;
double powFactorA;
double powFactorB;
double inTrimA;
double inTrimB;
double outTrimA;
double outTrimB;
enum {
fix_freq,
fix_reso,
fix_a0,
fix_a1,
fix_a2,
fix_b1,
fix_b2,
fix_sL1,
fix_sL2,
fix_sR1,
fix_sR2,
fix_total
}; // fixed frequency biquad filter for ultrasonics, stereo
std::array<double, fix_total> fixA;
std::array<double, fix_total> fixB;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
float A;
float B;
float C;
float D;
float E;
float F; // parameters. Always 0-1, and we scale/alter them elsewhere.
};
} // namespace trnr

280
filter/ynotch.h Normal file
View File

@@ -0,0 +1,280 @@
#pragma once
#define _USE_MATH_DEFINES
#include <array>
#include <math.h>
#include <vector>
namespace trnr {
template <typename t_sample>
// Notch filter based on YNotch by Chris Johnson
class ynotch {
public:
ynotch(double _samplerate)
: samplerate {_samplerate}
, A {0.1f}
, B {1.0f}
, C {0.0f}
, D {0.1f}
, E {0.9f}
, F {1.0f}
, fpdL {0}
, fpdR {0}
, biquad {0}
{
for (int x = 0; x < biq_total; x++) { biquad[x] = 0.0; }
powFactorA = 1.0;
powFactorB = 1.0;
inTrimA = 0.1;
inTrimB = 0.1;
outTrimA = 1.0;
outTrimB = 1.0;
for (int x = 0; x < fix_total; x++) {
fixA[x] = 0.0;
fixB[x] = 0.0;
}
fpdL = 1.0;
while (fpdL < 16386) fpdL = rand() * UINT32_MAX;
fpdR = 1.0;
while (fpdR < 16386) fpdR = rand() * UINT32_MAX;
}
void set_samplerate(double _samplerate) { samplerate = _samplerate; }
void set_drive(float value) { A = value * 0.9 + 0.1; }
void set_frequency(float value) { B = value; }
void set_resonance(float value) { C = value; }
void set_edge(float value) { D = value; }
void set_output(float value) { E = value; }
void set_mix(float value) { F = value; }
void processblock(t_sample** inputs, t_sample** outputs, int blockSize)
{
t_sample* in1 = inputs[0];
t_sample* in2 = inputs[1];
t_sample* out1 = outputs[0];
t_sample* out2 = outputs[1];
int inFramesToProcess = blockSize;
double overallscale = 1.0;
overallscale /= 44100.0;
overallscale *= samplerate;
inTrimA = inTrimB;
inTrimB = A * 10.0;
biquad[biq_freq] = pow(B, 3) * 20000.0;
if (biquad[biq_freq] < 15.0) biquad[biq_freq] = 15.0;
biquad[biq_freq] /= samplerate;
biquad[biq_reso] = (pow(C, 2) * 15.0) + 0.0001;
biquad[biq_aA0] = biquad[biq_aB0];
biquad[biq_aA1] = biquad[biq_aB1];
biquad[biq_aA2] = biquad[biq_aB2];
biquad[biq_bA1] = biquad[biq_bB1];
biquad[biq_bA2] = biquad[biq_bB2];
// previous run through the buffer is still in the filter, so we move it
// to the A section and now it's the new starting point.
double K = tan(M_PI * biquad[biq_freq]);
double norm = 1.0 / (1.0 + K / biquad[biq_reso] + K * K);
biquad[biq_aB0] = (1.0 + K * K) * norm;
biquad[biq_aB1] = 2.0 * (K * K - 1) * norm;
biquad[biq_aB2] = biquad[biq_aB0];
biquad[biq_bB1] = biquad[biq_aB1];
biquad[biq_bB2] = (1.0 - K / biquad[biq_reso] + K * K) * norm;
// for the coefficient-interpolated biquad filter
powFactorA = powFactorB;
powFactorB = pow(D + 0.9, 4);
// 1.0 == target neutral
outTrimA = outTrimB;
outTrimB = E;
double wet = F;
fixA[fix_freq] = fixB[fix_freq] = 20000.0 / samplerate;
fixA[fix_reso] = fixB[fix_reso] = 0.7071; // butterworth Q
K = tan(M_PI * fixA[fix_freq]);
norm = 1.0 / (1.0 + K / fixA[fix_reso] + K * K);
fixA[fix_a0] = fixB[fix_a0] = K * K * norm;
fixA[fix_a1] = fixB[fix_a1] = 2.0 * fixA[fix_a0];
fixA[fix_a2] = fixB[fix_a2] = fixA[fix_a0];
fixA[fix_b1] = fixB[fix_b1] = 2.0 * (K * K - 1.0) * norm;
fixA[fix_b2] = fixB[fix_b2] = (1.0 - K / fixA[fix_reso] + K * K) * norm;
// for the fixed-position biquad filter
for (int s = 0; s < blockSize; s++) {
double inputSampleL = *in1;
double inputSampleR = *in2;
if (fabs(inputSampleL) < 1.18e-23) inputSampleL = fpdL * 1.18e-17;
if (fabs(inputSampleR) < 1.18e-23) inputSampleR = fpdR * 1.18e-17;
double drySampleL = inputSampleL;
double drySampleR = inputSampleR;
double temp = (double)s / inFramesToProcess;
biquad[biq_a0] = (biquad[biq_aA0] * temp) + (biquad[biq_aB0] * (1.0 - temp));
biquad[biq_a1] = (biquad[biq_aA1] * temp) + (biquad[biq_aB1] * (1.0 - temp));
biquad[biq_a2] = (biquad[biq_aA2] * temp) + (biquad[biq_aB2] * (1.0 - temp));
biquad[biq_b1] = (biquad[biq_bA1] * temp) + (biquad[biq_bB1] * (1.0 - temp));
biquad[biq_b2] = (biquad[biq_bA2] * temp) + (biquad[biq_bB2] * (1.0 - temp));
// this is the interpolation code for the biquad
double powFactor = (powFactorA * temp) + (powFactorB * (1.0 - temp));
double inTrim = (inTrimA * temp) + (inTrimB * (1.0 - temp));
double outTrim = (outTrimA * temp) + (outTrimB * (1.0 - temp));
inputSampleL *= inTrim;
inputSampleR *= inTrim;
temp = (inputSampleL * fixA[fix_a0]) + fixA[fix_sL1];
fixA[fix_sL1] = (inputSampleL * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sL2];
fixA[fix_sL2] = (inputSampleL * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixA[fix_a0]) + fixA[fix_sR1];
fixA[fix_sR1] = (inputSampleR * fixA[fix_a1]) - (temp * fixA[fix_b1]) + fixA[fix_sR2];
fixA[fix_sR2] = (inputSampleR * fixA[fix_a2]) - (temp * fixA[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, powFactor);
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, powFactor);
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, powFactor);
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, powFactor);
temp = (inputSampleL * biquad[biq_a0]) + biquad[biq_sL1];
biquad[biq_sL1] = (inputSampleL * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sL2];
biquad[biq_sL2] = (inputSampleL * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleL = temp; // coefficient interpolating biquad filter
temp = (inputSampleR * biquad[biq_a0]) + biquad[biq_sR1];
biquad[biq_sR1] = (inputSampleR * biquad[biq_a1]) - (temp * biquad[biq_b1]) + biquad[biq_sR2];
biquad[biq_sR2] = (inputSampleR * biquad[biq_a2]) - (temp * biquad[biq_b2]);
inputSampleR = temp; // coefficient interpolating biquad filter
// encode/decode courtesy of torridgristle under the MIT license
if (inputSampleL > 1.0) inputSampleL = 1.0;
else if (inputSampleL > 0.0) inputSampleL = 1.0 - pow(1.0 - inputSampleL, (1.0 / powFactor));
if (inputSampleL < -1.0) inputSampleL = -1.0;
else if (inputSampleL < 0.0) inputSampleL = -1.0 + pow(1.0 + inputSampleL, (1.0 / powFactor));
if (inputSampleR > 1.0) inputSampleR = 1.0;
else if (inputSampleR > 0.0) inputSampleR = 1.0 - pow(1.0 - inputSampleR, (1.0 / powFactor));
if (inputSampleR < -1.0) inputSampleR = -1.0;
else if (inputSampleR < 0.0) inputSampleR = -1.0 + pow(1.0 + inputSampleR, (1.0 / powFactor));
inputSampleL *= outTrim;
inputSampleR *= outTrim;
temp = (inputSampleL * fixB[fix_a0]) + fixB[fix_sL1];
fixB[fix_sL1] = (inputSampleL * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sL2];
fixB[fix_sL2] = (inputSampleL * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleL = temp; // fixed biquad filtering ultrasonics
temp = (inputSampleR * fixB[fix_a0]) + fixB[fix_sR1];
fixB[fix_sR1] = (inputSampleR * fixB[fix_a1]) - (temp * fixB[fix_b1]) + fixB[fix_sR2];
fixB[fix_sR2] = (inputSampleR * fixB[fix_a2]) - (temp * fixB[fix_b2]);
inputSampleR = temp; // fixed biquad filtering ultrasonics
if (wet < 1.0) {
inputSampleL = (inputSampleL * wet) + (drySampleL * (1.0 - wet));
inputSampleR = (inputSampleR * wet) + (drySampleR * (1.0 - wet));
}
// begin 32 bit stereo floating point dither
int expon;
frexpf((float)inputSampleL, &expon);
fpdL ^= fpdL << 13;
fpdL ^= fpdL >> 17;
fpdL ^= fpdL << 5;
inputSampleL += ((double(fpdL) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
frexpf((float)inputSampleR, &expon);
fpdR ^= fpdR << 13;
fpdR ^= fpdR >> 17;
fpdR ^= fpdR << 5;
inputSampleR += ((double(fpdR) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62));
// end 32 bit stereo floating point dither
*out1 = inputSampleL;
*out2 = inputSampleR;
in1++;
in2++;
out1++;
out2++;
}
}
private:
double samplerate;
enum {
biq_freq,
biq_reso,
biq_a0,
biq_a1,
biq_a2,
biq_b1,
biq_b2,
biq_aA0,
biq_aA1,
biq_aA2,
biq_bA1,
biq_bA2,
biq_aB0,
biq_aB1,
biq_aB2,
biq_bB1,
biq_bB2,
biq_sL1,
biq_sL2,
biq_sR1,
biq_sR2,
biq_total
}; // coefficient interpolating biquad filter, stereo
std::array<double, biq_total> biquad;
double powFactorA;
double powFactorB;
double inTrimA;
double inTrimB;
double outTrimA;
double outTrimB;
enum {
fix_freq,
fix_reso,
fix_a0,
fix_a1,
fix_a2,
fix_b1,
fix_b2,
fix_sL1,
fix_sL2,
fix_sR1,
fix_sR2,
fix_total
}; // fixed frequency biquad filter for ultrasonics, stereo
std::array<double, fix_total> fixA;
std::array<double, fix_total> fixB;
uint32_t fpdL;
uint32_t fpdR;
// default stuff
float A;
float B;
float C;
float D;
float E;
float F; // parameters. Always 0-1, and we scale/alter them elsewhere.
};
} // namespace trnr

File diff suppressed because it is too large Load Diff

View File

@@ -1,172 +0,0 @@
/*
* dice.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <array>
using namespace std;
namespace trnr {
struct point {
float x;
float y;
};
struct rect {
float x;
float y;
float width;
float height;
};
struct quad {
point p1, p2, p3, p4;
};
struct dice {
int width;
int height;
array<point, 6> border_points;
array<rect, 7> pips;
array<quad, 6> segments_left;
};
// caclulates coordinates for an isometric dice control with bar segments
inline void dice_init(dice& d, float width, float height)
{
const float shortening = 0.866f;
float face_height, dice_width;
face_height = dice_width = height / 2.f;
float mid_x = width / 2.f;
float face_half_height = face_height / 2.f;
float face_width = dice_width * shortening;
// border points
point b_p1 = point {mid_x, 0};
point b_p2 = point {mid_x + face_width, face_half_height};
point b_p3 = point {mid_x + face_width, face_half_height + face_height};
point b_p4 = point {mid_x, height};
point b_p5 = point {mid_x - face_width, face_half_height + face_height};
point b_p6 = point {mid_x - face_width, face_half_height};
d.border_points = {b_p1, b_p2, b_p3, b_p4, b_p5, b_p6};
const float padding = face_height * 0.09f;
const float pad_x = padding * shortening;
const float pad_y = padding;
const float padded_face_height = face_height - 2 * pad_y;
const int segments = d.segments_left.size();
// define the ratio between segments and gaps as 3:1
const int segment_parts = 3;
const int gap_part = 1;
// the number of parts per segment
const int parts_per_segment = segment_parts + gap_part;
const int parts = segments * parts_per_segment - 2; // remove last gap
const float segment_height = (padded_face_height / parts) * segment_parts;
const float gap_height = padded_face_height / parts;
// calculate segments of the left face
for (int i = 0; i < d.segments_left.size(); i++) {
const point base_p1 =
point {mid_x - face_width + pad_x, face_half_height + pad_y};
const point base_p2 = point {mid_x - pad_x, face_height};
float seg_y = i * (segment_height + gap_height);
point p1 = {base_p1.x, base_p1.y + seg_y};
point p2 = {base_p2.x, base_p2.y + seg_y};
point p3 = {base_p2.x, base_p2.y + seg_y + segment_height};
point p4 = {base_p1.x, base_p1.y + seg_y + segment_height};
d.segments_left[i] = {p1, p2, p3, p4};
}
// calculate pip positions for top face (30-degree isometric perspective)
// correct center of the diamond face
// the diamond spans from b_p1 (top) to the middle of the left face
float diamond_center_x = mid_x;
float diamond_center_y =
face_half_height / 2.0f + face_half_height / 2.0f; // move down to actual center
// for 30-degree isometric, the grid directions are:
float cos30 = 1.f; // cos(30°) = √3/2 ≈ 0.866
float sin30 = 0.5f; // sin(30°) = 1/2
// scale the grid vectors to fit properly within the diamond
float grid_scale = 0.25f; // smaller scale to fit within diamond bounds
// grid vector 1: 30 degrees from horizontal (toward top-left of diamond)
float grid1_x = -cos30 * face_width * grid_scale;
float grid1_y = -sin30 * face_height * grid_scale;
// grid vector 2: -30 degrees from horizontal (toward top-right of diamond)
float grid2_x = cos30 * face_width * grid_scale;
float grid2_y = -sin30 * face_height * grid_scale;
const float pip_h = face_height * 0.1f;
const float pip_w = pip_h * 2 * shortening;
// position pips in the 30-degree isometric grid
for (int i = 0; i < 7; i++) {
float u = 0, v = 0; // grid coordinates
switch (i) {
case 0:
u = 0;
v = 0;
break; // center
case 1:
u = -1;
v = 1;
break; // top-left
case 2:
u = 1;
v = -1;
break; // bottom-right
case 3:
u = 1;
v = 1;
break; // top-right
case 4:
u = -1;
v = -1;
break; // bottom-left
case 5:
u = -1;
v = 0;
break; // center-left
case 6:
u = 1;
v = 0;
break; // center-right
}
// convert grid coordinates to world coordinates using 30-degree vectors
float pip_x = diamond_center_x + u * grid1_x + v * grid2_x;
float pip_y = diamond_center_y + u * grid1_y + v * grid2_y;
d.pips[i] = {pip_x - pip_w / 2, pip_y - pip_h / 2, pip_w, pip_h};
}
}
} // namespace trnr

View File

@@ -1,26 +1,3 @@
/*
* oversampler.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <array>

View File

@@ -1,64 +0,0 @@
/*
* combine_seq.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "simple_seq.h"
namespace trnr {
struct combine_note {
int probability;
int pitch;
int octave;
int velocity;
};
struct combine_seq {
simple_seq probability_seq;
simple_seq pitch_seq;
simple_seq octave_seq;
simple_seq velocity_seq;
combine_note current_note;
};
inline void combine_seq_init(combine_seq& c, size_t max_len)
{
simple_seq_init(c.probability_seq, max_len);
simple_seq_init(c.pitch_seq, max_len);
simple_seq_init(c.octave_seq, max_len);
simple_seq_init(c.velocity_seq, max_len);
}
inline combine_note& combine_seq_process_step(combine_seq& c)
{
c.current_note.probability = simple_seq_process_step(c.probability_seq);
c.current_note.pitch = simple_seq_process_step(c.pitch_seq);
c.current_note.octave = simple_seq_process_step(c.octave_seq);
c.current_note.velocity = simple_seq_process_step(c.velocity_seq);
return c.current_note;
}
struct timing_info {};
} // namespace trnr

View File

@@ -1,89 +0,0 @@
/*
* simple_seq.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <cstddef>
#include <cstdlib>
#include <vector>
using namespace std;
namespace trnr {
enum playback_dir {
PB_FORWARD,
PB_BACKWARD,
PB_PENDULUM,
PB_RANDOM
};
struct simple_seq {
size_t current_pos;
size_t length;
playback_dir playback_dir;
vector<int> data;
bool pendulum_forward;
};
inline void simple_seq_init(simple_seq& s, size_t length)
{
s.current_pos = 0;
s.length = length;
s.playback_dir = PB_FORWARD;
s.data.resize(s.length);
s.data.assign(s.length, 0);
s.pendulum_forward = true;
}
inline int simple_seq_process_step(simple_seq& s)
{
// play head advancement
if (s.playback_dir == PB_FORWARD ||
s.playback_dir == PB_PENDULUM && s.pendulum_forward)
s.current_pos++;
else if (s.playback_dir == PB_BACKWARD ||
s.playback_dir == PB_PENDULUM && !s.pendulum_forward)
s.current_pos--;
else if (s.playback_dir == PB_RANDOM) s.current_pos = rand() % s.length;
// play head reset
switch (s.playback_dir) {
case PB_FORWARD:
if (s.current_pos > s.length - 1) s.current_pos = 0;
break;
case PB_BACKWARD:
if (s.current_pos < 0) s.current_pos = s.length - 1;
break;
case PB_PENDULUM:
if (s.pendulum_forward && s.current_pos >= s.length - 1)
s.pendulum_forward = false;
else if (!s.pendulum_forward && s.current_pos <= 0) s.pendulum_forward = true;
break;
case PB_RANDOM:
break;
}
return s.data[s.current_pos];
}
} // namespace trnr

26
synth/ivoice.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
namespace trnr {
template <typename t_sample>
struct ivoice {
virtual ~ivoice() = default;
virtual void process_samples(t_sample** _outputs, int _start_index, int _block_size) = 0;
virtual bool is_busy() = 0;
virtual void set_samplerate(double samplerate) = 0;
virtual void note_on(int _note, float _velocity) = 0;
virtual void note_off() = 0;
virtual void modulate_pitch(float _pitch) = 0;
};
// check if a template derives from ivoice
template <class derived, typename sample>
struct is_convertible {
template <class T>
static char test(T*);
template <class T>
static double test(...);
static const bool value = sizeof(test<ivoice<sample>>(static_cast<derived*>(0))) == 1;
};
} // namespace trnr

48
synth/midi_event.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
namespace trnr {
enum midi_event_type {
note_on = 0,
note_off,
pitch_wheel,
mod_wheel
};
class midi_event {
public:
midi_event_type type;
int offset = 0;
int midi_note = 0;
float velocity = 1.f;
double data = 0;
void make_note_on(int _midi_note, float _velocity, int _offset = 0)
{
type = midi_event_type::note_on;
midi_note = _midi_note;
velocity = _velocity;
offset = _offset;
}
void make_note_off(int _midi_note, float _velocity, int _offset = 0)
{
type = midi_event_type::note_off;
midi_note = _midi_note;
velocity = _velocity;
offset = _offset;
}
void make_pitch_wheel(double _pitch, int _offset = 0)
{
type = midi_event_type::pitch_wheel;
data = _pitch;
}
void make_mod_wheel(double _mod, int _offset = 0)
{
type = midi_event_type::pitch_wheel;
data = _mod;
}
};
} // namespace trnr

93
synth/midi_synth.h Normal file
View File

@@ -0,0 +1,93 @@
#pragma once
#include "ivoice.h"
#include "midi_event.h"
#include "voice_allocator.h"
#include <cstring>
#include <memory>
#include <vector>
namespace trnr {
// a generic midi synth base class with sample accurate event handling.
// the templated type t_voice must derive from ivoice
template <typename t_voice, typename t_sample>
class midi_synth : public voice_allocator<t_voice, t_sample> {
public:
midi_synth()
: m_voices_active {false}
{
// checks whether template derives from ivoice
typedef t_voice assert_at_compile_time[is_convertible<t_voice, t_sample>::value ? 1 : -1];
}
void set_samplerate_blocksize(double _samplerate, int _block_size)
{
m_block_size = _block_size;
voice_allocator<t_voice, t_sample>::set_samplerate(_samplerate);
}
void process_block(t_sample** _outputs, int _n_frames)
{
// clear outputs
for (auto i = 0; i < 2; i++) { memset(_outputs[i], 0, _n_frames * sizeof(t_sample)); }
// sample accurate event handling based on the iPlug2 synth by Oli Larkin
if (m_voices_active || !m_event_queue.empty()) {
int block_size = m_block_size;
int samples_remaining = _n_frames;
int start_index = 0;
while (samples_remaining > 0) {
if (samples_remaining < block_size) block_size = samples_remaining;
while (!m_event_queue.empty()) {
midi_event event = m_event_queue.front();
// we assume the messages are in chronological order. If we find one later than the current block we
// are done.
if (event.offset > start_index + block_size) break;
// send performance messages to the voice allocator
// message offset is relative to the start of this process_samples() block
event.offset -= start_index;
voice_allocator<t_voice, t_sample>::add_event(event);
m_event_queue.erase(m_event_queue.begin());
}
voice_allocator<t_voice, t_sample>::process_samples(_outputs, start_index, block_size);
samples_remaining -= block_size;
start_index += block_size;
}
m_voices_active = voice_allocator<t_voice, t_sample>::voices_active();
flush_event_queue(_n_frames);
} else {
for (int s = 0; s < _n_frames; s++) {
_outputs[0][s] = 0.;
_outputs[1][s] = 0.;
}
}
}
void add_event(midi_event event)
{
if (event.type == midi_event_type::note_on) m_voices_active = true;
m_event_queue.push_back(event);
}
void flush_event_queue(int frames)
{
for (int i = 0; i < m_event_queue.size(); i++) { m_event_queue.at(i).offset -= frames; }
}
private:
std::vector<midi_event> m_event_queue;
int m_block_size;
bool m_voices_active;
};
} // namespace trnr

View File

@@ -1,817 +0,0 @@
/*
* triplex.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "../util/audio_buffer.h"
#include "../util/audio_math.h"
#include "voice_allocator.h"
#include <cmath>
#include <random>
namespace trnr {
//////////////
// SINE OSC //
//////////////
struct tx_sineosc {
bool phase_reset;
double samplerate;
float phase_resolution;
float phase;
float history;
};
inline void tx_randomize_phase(float& phase)
{
std::random_device random;
std::mt19937 gen(random());
std::uniform_real_distribution<> dis(0.0, 1.0);
phase = dis(gen);
}
inline void tx_sineosc_init(tx_sineosc& s, double samplerate)
{
s.phase_reset = false;
s.samplerate = samplerate;
s.phase_resolution = 16.f;
s.phase = 0.f;
s.history = 0.f;
tx_randomize_phase(s.phase);
}
inline float tx_wrap(float& phase)
{
while (phase < 0.) phase += 1.;
while (phase >= 1.) phase -= 1.;
return phase;
}
inline float tx_sineosc_process_sample(tx_sineosc& s, bool trigger, float frequency,
float phase_modulation = 0.f)
{
if (trigger) {
if (s.phase_reset) s.phase = 0.f;
else tx_randomize_phase(s.phase);
}
float lookup_phase = s.phase + phase_modulation;
tx_wrap(lookup_phase);
s.phase += frequency / s.samplerate;
tx_wrap(s.phase);
// redux
s.phase = static_cast<int>(s.phase * s.phase_resolution) / s.phase_resolution;
// x is scaled 0<=x<4096
float output;
float x = lookup_phase;
const float a = -0.40319426317E-08;
const float b = 0.21683205691E+03;
const float c = 0.28463350538E-04;
const float d = -0.30774648337E-02;
float y;
bool negate = false;
if (x > 2048) {
negate = true;
x -= 2048;
}
if (x > 1024) x = 2048 - x;
y = (a + x) / (b + c * x * x) + d * x;
if (negate) output = (-y);
else output = y;
// filter
output = 0.5 * (output + s.history);
s.history = output;
return output;
}
//////////////
// ENVELOPE //
//////////////
enum tx_env_state {
idle = 0,
attack1,
attack2,
hold,
decay1,
decay2,
sustain,
release1,
release2
};
struct tx_envelope {
tx_env_state state = idle;
float attack1_rate = 0;
float attack1_level = 0;
float attack2_rate = 0;
float hold_rate = 0;
float decay1_rate = 0;
float decay1_level = 0;
float decay2_rate = 0;
float sustain_level = 0;
float release1_rate = 0;
float release1_level = 0;
float release2_rate = 0;
bool skip_sustain = false;
double samplerate = 44100.;
size_t phase = 0;
float level = 0.f;
float start_level = 0.f;
float h1 = 0.f;
float h2 = 0.f;
float h3 = 0.f;
bool retrigger;
};
inline void tx_envelope_init(tx_envelope& e, double samplerate, bool retrigger = false)
{
e.samplerate = samplerate;
e.retrigger = retrigger;
}
inline size_t tx_mtos(float ms, double samplerate)
{
return static_cast<size_t>(ms * samplerate / 1000.f);
}
inline float tx_lerp(float x1, float y1, float x2, float y2, float x)
{
return y1 + (((x - x1) * (y2 - y1)) / (x2 - x1));
}
inline float tx_envelope_process_sample(tx_envelope& e, bool gate, bool trigger,
float _attack_mod = 0, float _decay_mod = 0)
{
size_t attack_mid_x1 = tx_mtos(e.attack1_rate + (float)_attack_mod, e.samplerate);
size_t attack_mid_x2 = tx_mtos(e.attack2_rate + (float)_attack_mod, e.samplerate);
size_t hold_samp = tx_mtos(e.hold_rate, e.samplerate);
size_t decay_mid_x1 = tx_mtos(e.decay1_rate + (float)_decay_mod, e.samplerate);
size_t decay_mid_x2 = tx_mtos(e.decay2_rate + (float)_decay_mod, e.samplerate);
size_t release_mid_x1 = tx_mtos(e.release1_rate + (float)_decay_mod, e.samplerate);
size_t release_mid_x2 = tx_mtos(e.release2_rate + (float)_decay_mod, e.samplerate);
// if note on is triggered, transition to attack phase
if (trigger) {
if (e.retrigger) e.start_level = 0.f;
else e.start_level = e.level;
e.phase = 0;
e.state = attack1;
}
// attack 1st half
if (e.state == attack1) {
// while in attack phase
if (e.phase < attack_mid_x1) {
e.level = tx_lerp(0, e.start_level, (float)attack_mid_x1, e.attack1_level,
(float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > attack_mid_x1) { e.phase = attack_mid_x1; }
// if attack phase is done, transition to decay phase
if (e.phase == attack_mid_x1) {
e.state = attack2;
e.phase = 0;
}
}
// attack 2nd half
if (e.state == attack2) {
// while in attack phase
if (e.phase < attack_mid_x2) {
e.level =
tx_lerp(0, e.attack1_level, (float)attack_mid_x2, 1, (float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > attack_mid_x2) { e.phase = attack_mid_x2; }
// if attack phase is done, transition to decay phase
if (e.phase == attack_mid_x2) {
e.state = hold;
e.phase = 0;
}
}
// hold
if (e.state == hold) {
if (e.phase < hold_samp) {
e.level = 1.0;
e.phase += 1;
}
if (e.phase > hold_samp) { e.phase = hold_samp; }
if (e.phase == hold_samp) {
e.state = decay1;
e.phase = 0;
}
}
// decay 1st half
if (e.state == decay1) {
// while in decay phase
if (e.phase < decay_mid_x1) {
e.level = tx_lerp(0, 1, (float)decay_mid_x1, e.decay1_level, (float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > decay_mid_x1) { e.phase = decay_mid_x1; }
// if decay phase is done, transition to sustain phase
if (e.phase == decay_mid_x1) {
e.state = decay2;
e.phase = 0;
}
}
// decay 2nd half
if (e.state == decay2) {
// while in decay phase
if (e.phase < decay_mid_x2) {
e.level = tx_lerp(0, e.decay1_level, (float)decay_mid_x2, e.sustain_level,
(float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > decay_mid_x2) { e.phase = decay_mid_x2; }
// if decay phase is done, transition to sustain phase
if (e.phase == decay_mid_x2) {
e.state = sustain;
e.phase = 0;
e.level = e.sustain_level;
}
}
// while sustain phase: if note off is triggered, transition to release phase
if (e.state == sustain && (!gate || e.skip_sustain)) {
e.state = release1;
e.level = e.sustain_level;
}
// release 1st half
if (e.state == release1) {
// while in release phase
if (e.phase < release_mid_x1) {
e.level = tx_lerp(0, e.sustain_level, (float)release_mid_x1, e.release1_level,
(float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > release_mid_x1) { e.phase = release_mid_x1; }
// transition to 2nd release half
if (e.phase == release_mid_x1) {
e.phase = 0;
e.state = release2;
}
}
// release 2nd half
if (e.state == release2) {
// while in release phase
if (e.phase < release_mid_x2) {
e.level =
tx_lerp(0, e.release1_level, (float)release_mid_x2, 0, (float)e.phase);
e.phase += 1;
}
// reset phase if parameter was changed
if (e.phase > release_mid_x2) { e.phase = release_mid_x2; }
// reset
if (e.phase == release_mid_x2) {
e.phase = 0;
e.state = idle;
e.level = 0;
}
}
// smooth output
e.h3 = e.h2;
e.h2 = e.h1;
e.h1 = e.level;
return (e.h1 + e.h2 + e.h3) / 3.f;
}
//////////////
// OPERATOR //
//////////////
struct tx_operator {
tx_envelope envelope;
tx_sineosc oscillator;
float ratio = 1.f;
float amplitude = 1.f;
};
inline void tx_operator_init(tx_operator& op, double samplerate)
{
tx_envelope_init(op.envelope, samplerate);
tx_sineosc_init(op.oscillator, samplerate);
}
inline float tx_operator_process_sample(tx_operator& op, bool gate, bool trigger,
float frequency, float velocity, float pm = 0.f)
{
float env = tx_envelope_process_sample(op.envelope, gate, trigger);
// drifts and sounds better!
if (op.envelope.state != idle) {
float osc = tx_sineosc_process_sample(op.oscillator, trigger, frequency, pm);
return osc * env * velocity;
} else {
return 0.f;
}
}
////////////
// VOICE //
////////////
constexpr float MOD_INDEX_COEFF = 4.f;
struct tx_state {
float additional_pitch_mod = 0.f; // modulates pitch in frequency
int algorithm = 0;
float pitch_env_amt = 0.f;
float pitch_mod = 0.f;
float feedback_amt = 0.f;
float bit_resolution = 12.f;
tx_sineosc feedback_osc;
tx_envelope pitch_env;
tx_operator op1;
tx_operator op2;
tx_operator op3;
};
inline void tx_voice_init(tx_state& s, double samplerate)
{
tx_sineosc_init(s.feedback_osc, samplerate);
tx_envelope_init(s.pitch_env, samplerate);
tx_operator_init(s.op1, samplerate);
tx_operator_init(s.op2, samplerate);
tx_operator_init(s.op3, samplerate);
}
inline void tx_voice_process_block(tx_state& t, voice_state& s, float** audio,
size_t num_frames,
const vector<audio_buffer<float>>& mods = {})
{
float frequency =
midi_to_frequency(s.midi_note + t.pitch_mod + t.additional_pitch_mod);
for (int i = 0; i < num_frames; i++) {
voice_process_event_for_frame(s, i);
float pitch_env_signal =
tx_envelope_process_sample(t.pitch_env, s.gate, s.trigger) * t.pitch_env_amt;
float pitched_freq = frequency + pitch_env_signal;
float output = 0.f;
// mix operator signals according to selected algorithm
if (t.algorithm == 0) {
float fb_freq = frequency * t.op3.ratio;
float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF);
float fb_signal =
tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) *
fb_mod_index;
float op3_Freq = frequency * t.op3.ratio;
float op3_mod_index = (t.op3.amplitude * MOD_INDEX_COEFF);
float op3_signal =
tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_Freq, s.velocity,
fb_signal) *
op3_mod_index;
float op2_freq = frequency * t.op2.ratio;
float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF);
float op2_signal =
tx_operator_process_sample(t.op2, s.gate, s.trigger, op2_freq, s.velocity,
op3_signal) *
op2_mod_index;
float op1_freq = frequency * t.op1.ratio;
output = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq,
s.velocity, op2_signal) *
t.op1.amplitude;
} else if (t.algorithm == 1) {
float fb_freq = frequency * t.op3.ratio;
float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF);
float fb_signal =
tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) *
fb_mod_index;
float op3_freq = frequency * t.op3.ratio;
float op3_signal =
tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity,
fb_signal) *
t.op3.amplitude;
float op2_freq = frequency * t.op2.ratio;
float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF);
float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger,
op2_freq, s.velocity) *
op2_mod_index;
float op1_freq = frequency * t.op1.ratio;
float op1_signal =
tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq, s.velocity,
op2_signal) *
t.op1.amplitude;
output = op1_signal + op3_signal;
} else if (t.algorithm == 2) {
float fb_freq = frequency * t.op3.ratio;
float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF);
float fb_signal =
tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) *
fb_mod_index;
float op3_freq = frequency * t.op3.ratio;
float op3_signal =
tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity,
fb_signal) *
t.op3.amplitude;
float op2_freq = frequency * t.op2.ratio;
float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger,
op2_freq, s.velocity) *
t.op2.amplitude;
float op1_freq = frequency * t.op1.ratio;
float op1_signal = tx_operator_process_sample(t.op1, s.gate, s.trigger,
op1_freq, s.velocity) *
t.op1.amplitude;
output = op1_signal + op2_signal + op3_signal;
} else if (t.algorithm == 3) {
float fb_freq = frequency * t.op3.ratio;
float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF);
float fb_signal =
tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) *
fb_mod_index;
float op3_freq = frequency * t.op3.ratio;
float op3_mod_index = (t.op3.amplitude * MOD_INDEX_COEFF);
float op3_signal =
tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity,
fb_signal) *
op3_mod_index;
float op2_freq = frequency * t.op2.ratio;
float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF);
float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger,
op2_freq, s.velocity) *
op2_mod_index;
float op1_freq = frequency * t.op1.ratio;
output = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq,
s.velocity, op2_signal + op3_signal) *
t.op1.amplitude;
}
// reset trigger
s.trigger = false;
float res = powf(2, t.bit_resolution);
output = roundf(output * res) / res;
audio[0][i] += output / 3.;
audio[1][i] = audio[0][i];
}
}
enum tx_parameter {
BIT_RESOLUTION = 0,
FEEDBACKOSC_PHASE_RESOLUTION,
FEEDBACK,
ALGORITHM,
PITCH_ENVELOPE_AMOUNT,
PITCH_ENVELOPE_SKIP_SUSTAIN,
PITCH_ENVELOPE_ATTACK1_RATE,
PITCH_ENVELOPE_ATTACK1_LEVEL,
PITCH_ENVELOPE_ATTACK2_RATE,
PITCH_ENVELOPE_HOLD_RATE,
PITCH_ENVELOPE_DECAY1_RATE,
PITCH_ENVELOPE_DECAY1_LEVEL,
PITCH_ENVELOPE_DECAY2_RATE,
PITCH_ENVELOPE_SUSTAIN_LEVEL,
PITCH_ENVELOPE_RELEASE1_RATE,
PITCH_ENVELOPE_RELEASE1_LEVEL,
PITCH_ENVELOPE_RELEASE2_RATE,
OP1_RATIO,
OP1_AMPLITUDE,
OP1_PHASE_RESOLUTION,
OP1_ENVELOPE_SKIP_SUSTAIN,
OP1_ENVELOPE_ATTACK1_RATE,
OP1_ENVELOPE_ATTACK1_LEVEL,
OP1_ENVELOPE_ATTACK2_RATE,
OP1_ENVELOPE_HOLD_RATE,
OP1_ENVELOPE_DECAY1_RATE,
OP1_ENVELOPE_DECAY1_LEVEL,
OP1_ENVELOPE_DECAY2_RATE,
OP1_ENVELOPE_SUSTAIN_LEVEL,
OP1_ENVELOPE_RELEASE1_RATE,
OP1_ENVELOPE_RELEASE1_LEVEL,
OP1_ENVELOPE_RELEASE2_RATE,
OP2_RATIO,
OP2_AMPLITUDE,
OP2_PHASE_RESOLUTION,
OP2_ENVELOPE_SKIP_SUSTAIN,
OP2_ENVELOPE_ATTACK1_RATE,
OP2_ENVELOPE_ATTACK1_LEVEL,
OP2_ENVELOPE_ATTACK2_RATE,
OP2_ENVELOPE_HOLD_RATE,
OP2_ENVELOPE_DECAY1_RATE,
OP2_ENVELOPE_DECAY1_LEVEL,
OP2_ENVELOPE_DECAY2_RATE,
OP2_ENVELOPE_SUSTAIN_LEVEL,
OP2_ENVELOPE_RELEASE1_RATE,
OP2_ENVELOPE_RELEASE1_LEVEL,
OP2_ENVELOPE_RELEASE2_RATE,
OP3_RATIO,
OP3_AMPLITUDE,
OP3_PHASE_RESOLUTION,
OP3_ENVELOPE_SKIP_SUSTAIN,
OP3_ENVELOPE_ATTACK1_RATE,
OP3_ENVELOPE_ATTACK1_LEVEL,
OP3_ENVELOPE_ATTACK2_RATE,
OP3_ENVELOPE_HOLD_RATE,
OP3_ENVELOPE_DECAY1_RATE,
OP3_ENVELOPE_DECAY1_LEVEL,
OP3_ENVELOPE_DECAY2_RATE,
OP3_ENVELOPE_SUSTAIN_LEVEL,
OP3_ENVELOPE_RELEASE1_RATE,
OP3_ENVELOPE_RELEASE1_LEVEL,
OP3_ENVELOPE_RELEASE2_RATE,
};
struct tx_parameter_mapping {
float range_min;
float range_max;
float exponent;
tx_parameter parameter;
float apply(float _input) const
{
if (range_min == range_max && exponent == 1.f) return _input;
return powf(_input, exponent) * (range_max - range_min) + range_min;
}
};
struct tx_synth {
voice_allocator allocator;
array<tx_state, MAX_VOICES> voices;
};
inline void tx_synth_init(tx_synth& s, double samplerate)
{
voice_allocator_init(s.allocator);
for (int i = 0; i < MAX_VOICES; i++) { tx_voice_init(s.voices[i], samplerate); }
}
inline void tx_synth_process_block(tx_synth& s, float** audio, size_t num_frames,
const vector<midi_event>& midi_events,
const vector<audio_buffer<float>>& mods = {})
{
for (int i = 0; i < num_frames; i++) {
audio[0][i] = audio[1][i] = 0.f;
} // clear audio buffers
voice_allocator_process_block(s.allocator, midi_events);
for (int i = 0; i < s.allocator.active_voice_count; i++) {
tx_voice_process_block(s.voices[i], s.allocator.voices[i], audio, num_frames,
mods);
}
}
inline void tx_apply_parameter_mapping(array<tx_state, MAX_VOICES>& v,
tx_parameter_mapping& m, float value)
{
if (m.range_min != m.range_max || m.exponent != 1.f)
value = powf(value, m.exponent) * (m.range_max - m.range_min) + m.range_min;
for (int i = 0; i < MAX_VOICES; i++) {
tx_state& s = v[i];
switch (m.parameter) {
case tx_parameter::BIT_RESOLUTION:
s.bit_resolution = value;
break;
case tx_parameter::FEEDBACKOSC_PHASE_RESOLUTION:
s.feedback_osc.phase_resolution = value;
break;
case tx_parameter::FEEDBACK:
s.feedback_amt = value;
break;
case tx_parameter::ALGORITHM:
s.algorithm = value;
break;
case tx_parameter::PITCH_ENVELOPE_AMOUNT:
s.pitch_env_amt = value;
break;
case tx_parameter::PITCH_ENVELOPE_SKIP_SUSTAIN:
s.pitch_env.skip_sustain = value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK1_RATE:
s.pitch_env.attack1_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK1_LEVEL:
s.pitch_env.attack1_level = value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK2_RATE:
s.pitch_env.attack2_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_HOLD_RATE:
s.pitch_env.hold_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY1_RATE:
s.pitch_env.decay1_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY1_LEVEL:
s.pitch_env.decay1_level = value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY2_RATE:
s.pitch_env.decay2_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_SUSTAIN_LEVEL:
s.pitch_env.sustain_level = value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE1_RATE:
s.pitch_env.release1_rate = value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE1_LEVEL:
s.pitch_env.release1_level = value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE2_RATE:
s.pitch_env.release2_rate = value;
break;
case tx_parameter::OP1_RATIO:
s.op1.ratio = value;
break;
case tx_parameter::OP1_AMPLITUDE:
s.op1.amplitude = value;
break;
case tx_parameter::OP1_PHASE_RESOLUTION:
s.op1.oscillator.phase_resolution = value;
break;
case tx_parameter::OP1_ENVELOPE_SKIP_SUSTAIN:
s.op1.envelope.skip_sustain = value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK1_RATE:
s.op1.envelope.attack1_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK1_LEVEL:
s.op1.envelope.attack1_level = value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK2_RATE:
s.op1.envelope.attack2_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_HOLD_RATE:
s.op1.envelope.hold_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY1_RATE:
s.op1.envelope.decay1_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY1_LEVEL:
s.op1.envelope.decay1_level = value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY2_RATE:
s.op1.envelope.decay2_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_SUSTAIN_LEVEL:
s.op1.envelope.sustain_level = value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE1_RATE:
s.op1.envelope.release1_rate = value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE1_LEVEL:
s.op1.envelope.release1_level = value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE2_RATE:
s.op1.envelope.release2_rate = value;
break;
case tx_parameter::OP2_RATIO:
s.op2.ratio = value;
break;
case tx_parameter::OP2_AMPLITUDE:
s.op2.amplitude = value;
break;
case tx_parameter::OP2_PHASE_RESOLUTION:
s.op2.oscillator.phase_resolution = value;
break;
case tx_parameter::OP2_ENVELOPE_SKIP_SUSTAIN:
s.op2.envelope.skip_sustain = value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK1_RATE:
s.op2.envelope.attack1_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK1_LEVEL:
s.op2.envelope.attack1_level = value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK2_RATE:
s.op2.envelope.attack2_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_HOLD_RATE:
s.op2.envelope.hold_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY1_RATE:
s.op2.envelope.decay1_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY1_LEVEL:
s.op2.envelope.decay1_level = value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY2_RATE:
s.op2.envelope.decay2_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_SUSTAIN_LEVEL:
s.op2.envelope.sustain_level = value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE1_RATE:
s.op2.envelope.release1_rate = value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE1_LEVEL:
s.op2.envelope.release1_level = value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE2_RATE:
s.op2.envelope.release2_rate = value;
break;
case tx_parameter::OP3_RATIO:
s.op3.ratio = value;
break;
case tx_parameter::OP3_AMPLITUDE:
s.op3.amplitude = value;
break;
case tx_parameter::OP3_PHASE_RESOLUTION:
s.op3.oscillator.phase_resolution = value;
break;
case tx_parameter::OP3_ENVELOPE_SKIP_SUSTAIN:
s.op3.envelope.skip_sustain = value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK1_RATE:
s.op3.envelope.attack1_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK1_LEVEL:
s.op3.envelope.attack1_level = value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK2_RATE:
s.op3.envelope.attack2_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_HOLD_RATE:
s.op3.envelope.hold_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY1_RATE:
s.op3.envelope.decay1_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY1_LEVEL:
s.op3.envelope.decay1_level = value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY2_RATE:
s.op3.envelope.decay2_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_SUSTAIN_LEVEL:
s.op3.envelope.sustain_level = value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE1_RATE:
s.op3.envelope.release1_rate = value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE1_LEVEL:
s.op3.envelope.release1_level = value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE2_RATE:
s.op3.envelope.release2_rate = value;
break;
}
}
}
inline void tx_apply_parameter_mappings(array<tx_state, MAX_VOICES>& v,
std::vector<tx_parameter_mapping>& m, float value)
{
for (int i = 0; i < m.size(); i++) { tx_apply_parameter_mapping(v, m[i], value); }
}
} // namespace trnr

238
synth/tx_envelope.h Normal file
View File

@@ -0,0 +1,238 @@
#pragma once
#include <array>
namespace trnr {
enum env_state {
idle = 0,
attack1,
attack2,
hold,
decay1,
decay2,
sustain,
release1,
release2
};
class tx_envelope {
public:
env_state state = idle;
float attack1_rate = 0;
float attack1_level = 0;
float attack2_rate = 0;
float hold_rate = 0;
float decay1_rate = 0;
float decay1_level = 0;
float decay2_rate = 0;
float sustain_level = 0;
float release1_rate = 0;
float release1_level = 0;
float release2_rate = 0;
bool skip_sustain = false;
tx_envelope(bool _retrigger = false)
: retrigger {_retrigger}
{
}
float process_sample(bool gate, bool trigger) { return process_sample<float>(gate, trigger, 0, 0); }
template <typename t_sample>
float process_sample(bool gate, bool trigger, t_sample _attack_mod, t_sample _decay_mod)
{
size_t attack_mid_x1 = ms_to_samples(attack1_rate + (float)_attack_mod);
size_t attack_mid_x2 = ms_to_samples(attack2_rate + (float)_attack_mod);
size_t hold_samp = ms_to_samples(hold_rate);
size_t decay_mid_x1 = ms_to_samples(decay1_rate + (float)_decay_mod);
size_t decay_mid_x2 = ms_to_samples(decay2_rate + (float)_decay_mod);
size_t release_mid_x1 = ms_to_samples(release1_rate + (float)_decay_mod);
size_t release_mid_x2 = ms_to_samples(release2_rate + (float)_decay_mod);
// if note on is triggered, transition to attack phase
if (trigger) {
if (retrigger) start_level = 0.f;
else start_level = level;
phase = 0;
state = attack1;
}
// attack 1st half
if (state == attack1) {
// while in attack phase
if (phase < attack_mid_x1) {
level = lerp(0, start_level, (float)attack_mid_x1, attack1_level, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > attack_mid_x1) { phase = attack_mid_x1; }
// if attack phase is done, transition to decay phase
if (phase == attack_mid_x1) {
state = attack2;
phase = 0;
}
}
// attack 2nd half
if (state == attack2) {
// while in attack phase
if (phase < attack_mid_x2) {
level = lerp(0, attack1_level, (float)attack_mid_x2, 1, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > attack_mid_x2) { phase = attack_mid_x2; }
// if attack phase is done, transition to decay phase
if (phase == attack_mid_x2) {
state = hold;
phase = 0;
}
}
// hold
if (state == hold) {
if (phase < hold_samp) {
level = 1.0;
phase += 1;
}
if (phase > hold_samp) { phase = hold_samp; }
if (phase == hold_samp) {
state = decay1;
phase = 0;
}
}
// decay 1st half
if (state == decay1) {
// while in decay phase
if (phase < decay_mid_x1) {
level = lerp(0, 1, (float)decay_mid_x1, decay1_level, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > decay_mid_x1) { phase = decay_mid_x1; }
// if decay phase is done, transition to sustain phase
if (phase == decay_mid_x1) {
state = decay2;
phase = 0;
}
}
// decay 2nd half
if (state == decay2) {
// while in decay phase
if (phase < decay_mid_x2) {
level = lerp(0, decay1_level, (float)decay_mid_x2, sustain_level, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > decay_mid_x2) { phase = decay_mid_x2; }
// if decay phase is done, transition to sustain phase
if (phase == decay_mid_x2) {
state = sustain;
phase = 0;
level = sustain_level;
}
}
// while sustain phase: if note off is triggered, transition to release phase
if (state == sustain && (!gate || skip_sustain)) {
state = release1;
level = sustain_level;
}
// release 1st half
if (state == release1) {
// while in release phase
if (phase < release_mid_x1) {
level = lerp(0, sustain_level, (float)release_mid_x1, release1_level, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > release_mid_x1) { phase = release_mid_x1; }
// transition to 2nd release half
if (phase == release_mid_x1) {
phase = 0;
state = release2;
}
}
// release 2nd half
if (state == release2) {
// while in release phase
if (phase < release_mid_x2) {
level = lerp(0, release1_level, (float)release_mid_x2, 0, (float)phase);
phase += 1;
}
// reset phase if parameter was changed
if (phase > release_mid_x2) { phase = release_mid_x2; }
// reset
if (phase == release_mid_x2) {
phase = 0;
state = idle;
level = 0;
}
}
return smooth(level);
}
bool is_busy() { return state != 0; }
void set_samplerate(double sampleRate) { this->samplerate = sampleRate; }
// converts the x/y coordinates of the envelope points as a list for graphical representation.
std::array<float, 18> calc_coordinates(float _max_attack, float _max_decay, float _max_release)
{
auto scale = [](float _value, float _max) { return powf(_value / _max, 0.25) * _max; };
float a_x = 0;
float a_y = 0;
float b_x = scale(attack1_rate, _max_attack / 2);
float b_y = attack1_level;
float c_x = b_x + scale(attack2_rate, _max_attack / 2);
float c_y = 1;
float d_x = c_x + hold_rate;
float d_y = 1;
float e_x = d_x + scale(decay1_rate, _max_decay / 2);
float e_y = decay1_level;
float f_x = e_x + scale(decay2_rate, _max_decay / 2);
float f_y = sustain_level;
float g_x = _max_attack + _max_decay;
float g_y = sustain_level;
float h_x = g_x + scale(release1_rate, _max_decay / 2);
float h_y = release1_level;
float i_x = h_x + scale(release2_rate, _max_decay / 2);
float i_y = 0;
float total = _max_attack + _max_decay + _max_release;
return {a_x, a_y, b_x / total, b_y, c_x / total, c_y, d_x / total, d_y, e_x / total, e_y,
f_x / total, f_y, g_x / total, g_y, h_x / total, h_y, i_x / total, i_y};
}
private:
double samplerate = 44100.;
size_t phase = 0;
float level = 0.f;
float start_level = 0.f;
float h1 = 0.f;
float h2 = 0.f;
float h3 = 0.f;
bool retrigger;
float lerp(float x1, float y1, float x2, float y2, float x) { return y1 + (((x - x1) * (y2 - y1)) / (x2 - x1)); }
float smooth(float sample)
{
h3 = h2;
h2 = h1;
h1 = sample;
return (h1 + h2 + h3) / 3.f;
}
size_t ms_to_samples(float ms) { return static_cast<size_t>(ms * samplerate / 1000.f); }
};
} // namespace trnr

49
synth/tx_operator.h Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include "../clip/wavefolder.h"
#include "tx_envelope.h"
#include "tx_sineosc.h"
namespace trnr {
class tx_operator {
public:
tx_operator()
: ratio {1}
, amplitude {1.0f}
, envelope_enabled {true}
, velocity_enabled {true}
{
}
tx_envelope envelope;
tx_sineosc oscillator;
wavefolder folder;
float ratio;
float amplitude;
bool envelope_enabled;
bool velocity_enabled;
float process_sample(const bool& gate, const bool& trigger, const float& frequency, const float& velocity,
const float& pm = 0)
{
float env = 1.f;
if (envelope_enabled) {
env = envelope.process_sample(gate, trigger);
if (!envelope.is_busy()) return 0.f;
}
float osc = oscillator.process_sample(trigger, frequency, pm);
folder.process_sample(osc);
float adjusted_velocity = velocity_enabled ? velocity : 1.f;
return osc * env * adjusted_velocity;
}
void set_samplerate(double samplerate)
{
this->envelope.set_samplerate(samplerate);
this->oscillator.set_samplerate(samplerate);
}
};
} // namespace trnr

View File

@@ -0,0 +1,89 @@
#include <cmath>
namespace trnr {
enum tx_parameter {
BIT_RESOLUTION = 0,
FEEDBACKOSC_PHASE_RESOLUTION,
FEEDBACK,
ALGORITHM,
PITCH_ENVELOPE_AMOUNT,
PITCH_ENVELOPE_SKIP_SUSTAIN,
PITCH_ENVELOPE_ATTACK1_RATE,
PITCH_ENVELOPE_ATTACK1_LEVEL,
PITCH_ENVELOPE_ATTACK2_RATE,
PITCH_ENVELOPE_HOLD_RATE,
PITCH_ENVELOPE_DECAY1_RATE,
PITCH_ENVELOPE_DECAY1_LEVEL,
PITCH_ENVELOPE_DECAY2_RATE,
PITCH_ENVELOPE_SUSTAIN_LEVEL,
PITCH_ENVELOPE_RELEASE1_RATE,
PITCH_ENVELOPE_RELEASE1_LEVEL,
PITCH_ENVELOPE_RELEASE2_RATE,
OP1_RATIO,
OP1_AMPLITUDE,
OP1_PHASE_RESOLUTION,
OP1_ENVELOPE_SKIP_SUSTAIN,
OP1_ENVELOPE_ATTACK1_RATE,
OP1_ENVELOPE_ATTACK1_LEVEL,
OP1_ENVELOPE_ATTACK2_RATE,
OP1_ENVELOPE_HOLD_RATE,
OP1_ENVELOPE_DECAY1_RATE,
OP1_ENVELOPE_DECAY1_LEVEL,
OP1_ENVELOPE_DECAY2_RATE,
OP1_ENVELOPE_SUSTAIN_LEVEL,
OP1_ENVELOPE_RELEASE1_RATE,
OP1_ENVELOPE_RELEASE1_LEVEL,
OP1_ENVELOPE_RELEASE2_RATE,
OP2_RATIO,
OP2_AMPLITUDE,
OP2_PHASE_RESOLUTION,
OP2_ENVELOPE_SKIP_SUSTAIN,
OP2_ENVELOPE_ATTACK1_RATE,
OP2_ENVELOPE_ATTACK1_LEVEL,
OP2_ENVELOPE_ATTACK2_RATE,
OP2_ENVELOPE_HOLD_RATE,
OP2_ENVELOPE_DECAY1_RATE,
OP2_ENVELOPE_DECAY1_LEVEL,
OP2_ENVELOPE_DECAY2_RATE,
OP2_ENVELOPE_SUSTAIN_LEVEL,
OP2_ENVELOPE_RELEASE1_RATE,
OP2_ENVELOPE_RELEASE1_LEVEL,
OP2_ENVELOPE_RELEASE2_RATE,
OP3_RATIO,
OP3_AMPLITUDE,
OP3_PHASE_RESOLUTION,
OP3_ENVELOPE_SKIP_SUSTAIN,
OP3_ENVELOPE_ATTACK1_RATE,
OP3_ENVELOPE_ATTACK1_LEVEL,
OP3_ENVELOPE_ATTACK2_RATE,
OP3_ENVELOPE_HOLD_RATE,
OP3_ENVELOPE_DECAY1_RATE,
OP3_ENVELOPE_DECAY1_LEVEL,
OP3_ENVELOPE_DECAY2_RATE,
OP3_ENVELOPE_SUSTAIN_LEVEL,
OP3_ENVELOPE_RELEASE1_RATE,
OP3_ENVELOPE_RELEASE1_LEVEL,
OP3_ENVELOPE_RELEASE2_RATE,
};
struct tx_parameter_mapping {
float range_min;
float range_max;
float exponent;
tx_parameter parameter;
tx_parameter_mapping(float _range_min, float _range_max, float _exponent, tx_parameter _parameter)
: range_min(_range_min), range_max(_range_max), exponent(_exponent), parameter(_parameter)
{}
float apply(float _input) const
{
if (range_min == range_max && exponent == 1.f) return _input;
return powf(_input, exponent) * (range_max - range_min) + range_min;
}
};
}

101
synth/tx_sineosc.h Normal file
View File

@@ -0,0 +1,101 @@
#pragma once
#include <cmath>
#include <random>
namespace trnr {
class tx_sineosc {
public:
bool phase_reset;
tx_sineosc()
: samplerate {44100}
, phase_resolution {16.f}
, phase {0.}
, history {0.}
, phase_reset {false}
{
randomize_phase();
}
void set_phase_resolution(float res) { phase_resolution = powf(2, res); }
float process_sample(bool trigger, float frequency, float phase_modulation = 0.f)
{
if (trigger) {
if (phase_reset) phase = 0.f;
else randomize_phase();
}
float lookup_phase = phase + phase_modulation;
wrap(lookup_phase);
phase += frequency / samplerate;
wrap(phase);
redux(lookup_phase);
float output = sine(lookup_phase * 4096.);
filter(output);
return output;
}
void set_samplerate(double _samplerate) { this->samplerate = _samplerate; }
void randomize_phase()
{
std::random_device random;
std::mt19937 gen(random());
std::uniform_real_distribution<> dis(0.0, 1.0);
phase = dis(gen);
}
private:
double samplerate;
float phase_resolution;
float phase;
float history;
float sine(float x)
{
// x is scaled 0<=x<4096
const float a = -0.40319426317E-08;
const float b = 0.21683205691E+03;
const float c = 0.28463350538E-04;
const float d = -0.30774648337E-02;
float y;
bool negate = false;
if (x > 2048) {
negate = true;
x -= 2048;
}
if (x > 1024) x = 2048 - x;
y = (a + x) / (b + c * x * x) + d * x;
if (negate) return (float)(-y);
else return (float)y;
}
float wrap(float& phase)
{
while (phase < 0.) phase += 1.;
while (phase >= 1.) phase -= 1.;
return phase;
}
float filter(float& value)
{
value = 0.5 * (value + history);
history = value;
return value;
}
float redux(float& value)
{
value = static_cast<int>(value * phase_resolution) / phase_resolution;
return value;
}
};
} // namespace trnr

389
synth/tx_voice.h Normal file
View File

@@ -0,0 +1,389 @@
#pragma once
#include "../util/audio_math.h"
#include "ivoice.h"
#include "tx_envelope.h"
#include "tx_operator.h"
#include "tx_sineosc.h"
#include "tx_parameter_mapping.h"
namespace trnr {
enum mod_dest {
mod_dest_out = 0,
mod_dest_fm,
mod_dest_am
};
template <typename t_sample>
class tx_voice : public ivoice<t_sample> {
public:
tx_voice()
: pitch_env_amt {0.f}
, feedback_amt {0.f}
, bit_resolution(12.f)
{
set_glide_time(0.f);
}
bool gate = false;
bool trigger = false;
int midi_note = 0;
float velocity = 1.f;
float additional_pitch_mod = 0.f; // modulates pitch in frequency
mod_dest op2_dest = mod_dest_fm;
mod_dest op3_dest = mod_dest_fm;
float pitch_env_amt;
float feedback_amt;
float bit_resolution;
tx_sineosc feedback_osc;
tx_envelope pitch_env;
tx_operator op1;
tx_operator op2;
tx_operator op3;
void set_glide_time(float time_ms) { glide = 1 - exp(-1.0 / (time_ms * samplerate / 1000.f)); }
void note_on(int _note, float _velocity) override
{
this->gate = true;
this->trigger = true;
midi_note = _note;
velocity = _velocity;
}
void note_off() override { this->gate = false; }
// modulates the pitch in semitones
void modulate_pitch(float _pitch) override { this->pitch_mod = _pitch; }
void process_samples(t_sample** _outputs, int _start_index, int _block_size) override
{
target_frequency = midi_to_frequency(midi_note + pitch_mod + additional_pitch_mod);
for (int s = _start_index; s < _start_index + _block_size; s++) {
// calculate moving average for portamento
current_frequency = (1 - glide) * current_frequency + glide * target_frequency;
float pitch_env_signal = pitch_env.process_sample(gate, trigger) * pitch_env_amt;
float pitched_freq = current_frequency + pitch_env_signal;
float signal = process_operators(pitched_freq);
// reset trigger
trigger = false;
redux(signal, bit_resolution);
_outputs[0][s] += signal / 3.;
_outputs[1][s] = _outputs[0][s];
}
}
bool is_busy() override
{
return gate || op1.envelope.is_busy() || op2.envelope.is_busy() || op3.envelope.is_busy();
}
void set_samplerate(double _samplerate) override
{
samplerate = _samplerate;
pitch_env.set_samplerate(_samplerate);
feedback_osc.set_samplerate(_samplerate);
op1.set_samplerate(_samplerate);
op2.set_samplerate(_samplerate);
op3.set_samplerate(_samplerate);
}
void set_phase_reset(bool phase_reset)
{
op1.oscillator.phase_reset = phase_reset;
op2.oscillator.phase_reset = phase_reset;
op3.oscillator.phase_reset = phase_reset;
feedback_osc.phase_reset = phase_reset;
}
void update_parameters(float _value, const std::vector<tx_parameter_mapping>& _mappings)
{
for (const tx_parameter_mapping& mapping : _mappings) {
float normalized = mapping.apply(_value);
map_parameter(mapping, normalized);
}
}
private:
double samplerate;
const float MOD_INDEX_COEFF = 4.f;
float pitch_mod = 0.f; // modulates pitch in semi-tones
float current_frequency;
float target_frequency;
float glide;
float process_op3(const float frequency)
{
float fb_freq = frequency * op3.ratio;
float fb_mod_index = feedback_amt * MOD_INDEX_COEFF;
float fb_signal = feedback_osc.process_sample(trigger, fb_freq) * fb_mod_index;
float op3_Freq = frequency * op3.ratio;
return op3.process_sample(gate, trigger, op3_Freq, velocity, fb_signal) * op3.amplitude;
}
float process_op2(const float frequency, const float modulator)
{
// if patched, op3 modulates the phase of op2
float pm = op3_dest == mod_dest_fm ? modulator : 0.f;
float adjusted_freq = frequency * op2.ratio;
float signal = op2.process_sample(gate, trigger, adjusted_freq, velocity, pm * MOD_INDEX_COEFF) * op2.amplitude;
// if patched, op3 modulated the amplitude of op2
if (op3_dest == mod_dest_am) ring_mod(signal, modulator, op3.amplitude);
return signal;
}
float process_op1(const float frequency, const float modulator)
{
// if patched, op2 modulates the phase of op1
float pm = op2_dest == mod_dest_fm ? modulator : 0.f;
float op1_freq = frequency * op1.ratio;
float signal = op1.process_sample(gate, trigger, op1_freq, velocity, pm * MOD_INDEX_COEFF) * op1.amplitude;
// if patched, op2 modulates the amplitude of op1
if (op2_dest == mod_dest_am) ring_mod(signal, modulator, op2.amplitude);
return signal;
}
float process_operators(float frequency)
{
float op3_signal = process_op3(frequency);
float op2_signal = process_op2(frequency, op3_signal);
float op1_signal = process_op1(frequency, op2_signal);
float signal_mix = op1_signal;
if (op3_dest == mod_dest_out) { signal_mix += op3_signal; }
if (op2_dest == mod_dest_out) { signal_mix += op2_signal; }
return signal_mix;
}
void ring_mod(float& carrier, float modulator, float blend)
{
float dry_lvl = 1.f - blend;
float wet_lvl = blend;
float dry_signal = carrier;
float wet_signal = carrier * modulator * 2.0f;
carrier = dry_lvl * dry_signal + wet_lvl * wet_signal;
}
float redux(float& value, float resolution)
{
float res = powf(2, resolution);
value = roundf(value * res) / res;
return value;
}
void map_parameter(const tx_parameter_mapping& _mapping, const float _value)
{
switch (_mapping.parameter) {
case tx_parameter::BIT_RESOLUTION:
bit_resolution = _value;
break;
case tx_parameter::FEEDBACKOSC_PHASE_RESOLUTION:
feedback_osc.set_phase_resolution(_value);
break;
case tx_parameter::FEEDBACK:
feedback_amt = _value;
break;
case tx_parameter::ALGORITHM:
algorithm = _value;
break;
case tx_parameter::PITCH_ENVELOPE_AMOUNT:
pitch_env_amt = _value;
break;
case tx_parameter::PITCH_ENVELOPE_SKIP_SUSTAIN:
pitch_env.skip_sustain = _value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK1_RATE:
pitch_env.attack1_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK1_LEVEL:
pitch_env.attack1_level = _value;
break;
case tx_parameter::PITCH_ENVELOPE_ATTACK2_RATE:
pitch_env.attack2_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_HOLD_RATE:
pitch_env.hold_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY1_RATE:
pitch_env.decay1_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY1_LEVEL:
pitch_env.decay1_level = _value;
break;
case tx_parameter::PITCH_ENVELOPE_DECAY2_RATE:
pitch_env.decay2_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_SUSTAIN_LEVEL:
pitch_env.sustain_level = _value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE1_RATE:
pitch_env.release1_rate = _value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE1_LEVEL:
pitch_env.release1_level = _value;
break;
case tx_parameter::PITCH_ENVELOPE_RELEASE2_RATE:
pitch_env.release2_rate = _value;
break;
case tx_parameter::OP1_RATIO:
op1.ratio = _value;
break;
case tx_parameter::OP1_AMPLITUDE:
op1.amplitude = _value;
break;
case tx_parameter::OP1_PHASE_RESOLUTION:
op1.oscillator.set_phase_resolution(_value);
break;
case tx_parameter::OP1_ENVELOPE_SKIP_SUSTAIN:
op1.envelope.skip_sustain = _value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK1_RATE:
op1.envelope.attack1_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK1_LEVEL:
op1.envelope.attack1_level = _value;
break;
case tx_parameter::OP1_ENVELOPE_ATTACK2_RATE:
op1.envelope.attack2_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_HOLD_RATE:
op1.envelope.hold_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY1_RATE:
op1.envelope.decay1_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY1_LEVEL:
op1.envelope.decay1_level = _value;
break;
case tx_parameter::OP1_ENVELOPE_DECAY2_RATE:
op1.envelope.decay2_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_SUSTAIN_LEVEL:
op1.envelope.sustain_level = _value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE1_RATE:
op1.envelope.release1_rate = _value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE1_LEVEL:
op1.envelope.release1_level = _value;
break;
case tx_parameter::OP1_ENVELOPE_RELEASE2_RATE:
op1.envelope.release2_rate = _value;
break;
case tx_parameter::OP2_RATIO:
op2.ratio = _value;
break;
case tx_parameter::OP2_AMPLITUDE:
op2.amplitude = _value;
break;
case tx_parameter::OP2_PHASE_RESOLUTION:
op2.oscillator.set_phase_resolution(_value);
break;
case tx_parameter::OP2_ENVELOPE_SKIP_SUSTAIN:
op2.envelope.skip_sustain = _value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK1_RATE:
op2.envelope.attack1_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK1_LEVEL:
op2.envelope.attack1_level = _value;
break;
case tx_parameter::OP2_ENVELOPE_ATTACK2_RATE:
op2.envelope.attack2_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_HOLD_RATE:
op2.envelope.hold_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY1_RATE:
op2.envelope.decay1_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY1_LEVEL:
op2.envelope.decay1_level = _value;
break;
case tx_parameter::OP2_ENVELOPE_DECAY2_RATE:
op2.envelope.decay2_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_SUSTAIN_LEVEL:
op2.envelope.sustain_level = _value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE1_RATE:
op2.envelope.release1_rate = _value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE1_LEVEL:
op2.envelope.release1_level = _value;
break;
case tx_parameter::OP2_ENVELOPE_RELEASE2_RATE:
op2.envelope.release2_rate = _value;
break;
case tx_parameter::OP3_RATIO:
op3.ratio = _value;
break;
case tx_parameter::OP3_AMPLITUDE:
op3.amplitude = _value;
break;
case tx_parameter::OP3_PHASE_RESOLUTION:
op3.oscillator.set_phase_resolution(_value);
break;
case tx_parameter::OP3_ENVELOPE_SKIP_SUSTAIN:
op3.envelope.skip_sustain = _value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK1_RATE:
op3.envelope.attack1_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK1_LEVEL:
op3.envelope.attack1_level = _value;
break;
case tx_parameter::OP3_ENVELOPE_ATTACK2_RATE:
op3.envelope.attack2_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_HOLD_RATE:
op3.envelope.hold_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY1_RATE:
op3.envelope.decay1_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY1_LEVEL:
op3.envelope.decay1_level = _value;
break;
case tx_parameter::OP3_ENVELOPE_DECAY2_RATE:
op3.envelope.decay2_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_SUSTAIN_LEVEL:
op3.envelope.sustain_level = _value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE1_RATE:
op3.envelope.release1_rate = _value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE1_LEVEL:
op3.envelope.release1_level = _value;
break;
case tx_parameter::OP3_ENVELOPE_RELEASE2_RATE:
op3.envelope.release2_rate = _value;
break;
}
}
};
} // namespace trnr

View File

@@ -1,194 +1,167 @@
/*
* voice_allocator.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <array>
#include <cstddef>
#include "ivoice.h"
#include "midi_event.h"
#include <vector>
using namespace std;
#include <memory>
#include <algorithm>
#include <functional>
namespace trnr {
enum midi_event_type {
NOTE_ON = 0,
NOTE_OFF,
PITCH_WHEEL,
MOD_WHEEL
template <typename t_voice, typename t_sample>
class voice_allocator {
public:
std::vector<std::shared_ptr<t_voice>> voicePtrs;
voice_allocator()
{
// checks whether template derives from ivoice
typedef t_voice assert_at_compile_time[is_convertible<t_voice, t_sample>::value ? 1 : -1];
voicePtrs.reserve(8);
}
void set_voice_count(const int& voice_count)
{
if (voice_count > voicePtrs.size()) {
for (int i = voicePtrs.size(); i < voice_count; ++i) {
if (voicePtrs.size() > 0) {
voicePtrs.emplace_back(std::make_shared<t_voice>(*voicePtrs.at(0)));
} else {
voicePtrs.emplace_back(std::make_shared<t_voice>());
}
}
} else if (voice_count < voicePtrs.size()) {
voicePtrs.resize(voice_count);
}
}
void note_on(const midi_event& event)
{
auto voice = get_free_voice(event.midi_note);
if (!voice) { voice = steal_voice(); }
if (voice) { voice->note_on(event.midi_note, event.velocity); }
}
void note_off(const midi_event& event)
{
for (const auto& v : voicePtrs) {
if (v->midi_note == event.midi_note) v->note_off();
}
}
void access(std::function<void(t_voice*)> f)
{
std::for_each(voicePtrs.begin(), voicePtrs.end(), [&](std::shared_ptr<t_voice> ptr) {
if (ptr) {
f(ptr.get()); // Call the function with the raw pointer
}
});
}
void process_samples(t_sample** _outputs, int _start_index, int _block_size)
{
for (int b = _start_index; b < _start_index + _block_size; b += internal_block_size) {
// process all events in the block (introduces potential inaccuracy of up to 16 samples)
process_events(b, internal_block_size);
for (const auto& v : voicePtrs) { v->process_samples(_outputs, b, internal_block_size); }
}
}
void add_event(midi_event event) { input_queue.push_back(event); }
bool voices_active()
{
bool voices_active = false;
for (const auto& v : voicePtrs) {
bool busy = v->is_busy();
voices_active |= busy;
}
return voices_active;
}
void set_samplerate(double _samplerate)
{
for (const auto& v : voicePtrs) { v->set_samplerate(_samplerate); }
}
private:
std::vector<midi_event> input_queue;
int index_to_steal = 0;
const int internal_block_size = 16;
std::shared_ptr<t_voice> get_free_voice(float frequency)
{
std::shared_ptr<t_voice> voice = nullptr;
for (const auto& v : voicePtrs) {
if (!v->is_busy()) voice = v;
break;
}
return voice;
}
std::shared_ptr<t_voice> steal_voice()
{
std::shared_ptr<t_voice> free_voice = nullptr;
for (const auto& v : voicePtrs) {
if (!v->gate) {
free_voice = v;
break;
}
}
if (!free_voice) {
free_voice = voicePtrs.at(index_to_steal);
if (index_to_steal < voicePtrs.size() - 1) {
index_to_steal++;
} else {
index_to_steal = 0;
}
}
return free_voice;
}
void process_events(int _start_index, int _block_size)
{
for (int s = _start_index; s < _start_index + _block_size; s++) {
auto iterator = input_queue.begin();
while (iterator != input_queue.end()) {
midi_event& event = *iterator;
if (event.offset == s) {
switch (event.type) {
case midi_event_type::note_on:
note_on(event);
break;
case midi_event_type::note_off:
note_off(event);
break;
case midi_event_type::pitch_wheel:
access([&event](t_voice* voice) { voice->modulate_pitch(event.data); });
break;
default:
break;
}
iterator = input_queue.erase(iterator);
} else {
iterator++;
}
}
}
}
};
struct midi_event {
midi_event_type type;
int offset;
int midi_note;
float velocity;
double data;
};
inline void make_note_on(midi_event& ev, int _midi_note, float _velocity, int _offset = 0)
{
ev.type = midi_event_type::NOTE_ON;
ev.midi_note = _midi_note;
ev.velocity = _velocity;
ev.offset = _offset;
}
inline void make_note_off(midi_event& ev, int _midi_note, float _velocity,
int _offset = 0)
{
ev.type = midi_event_type::NOTE_OFF;
ev.midi_note = _midi_note;
ev.velocity = _velocity;
ev.offset = _offset;
}
inline void make_pitch_wheel(midi_event& ev, double _pitch, int _offset = 0)
{
ev.type = midi_event_type::PITCH_WHEEL;
ev.data = _pitch;
}
inline void make_mod_wheel(midi_event& ev, double _mod, int _offset = 0)
{
ev.type = midi_event_type::PITCH_WHEEL;
ev.data = _mod;
}
#ifndef MAX_VOICES
#define MAX_VOICES 16
#endif
#ifndef MAX_EVENTS_PER_VOICE
#define MAX_EVENTS_PER_VOICE 32
#endif
struct voice_state {
int midi_note;
bool is_busy = false;
bool gate = false;
bool trigger = false;
float velocity = 0.0f;
array<midi_event, MAX_EVENTS_PER_VOICE> events;
size_t event_count = 0;
};
struct voice_allocator {
array<voice_state, MAX_VOICES> voices;
int active_voice_count = 1;
size_t index_to_steal = 0;
};
inline void voice_allocator_init(voice_allocator& va)
{
for (size_t i = 0; i < MAX_VOICES; ++i) { va.voices[i].event_count = 0; }
}
inline void voice_allocator_process_block(voice_allocator& va,
const vector<midi_event>& midi_events)
{
// reset voice events and counts
for (int i = 0; i < MAX_VOICES; i++) {
for (int j = 0; j < MAX_EVENTS_PER_VOICE; j++) {
va.voices[i].events[j] = midi_event {};
}
va.voices[i].event_count = 0;
}
for (const auto& ev : midi_events) {
switch (ev.type) {
case NOTE_ON: {
bool found = false;
// attempt to find a free voice
for (size_t i = 0; i < va.active_voice_count; ++i) {
if (!va.voices[i].is_busy) {
voice_state& found_voice = va.voices[i];
found_voice.is_busy = true;
found_voice.midi_note = ev.midi_note;
found_voice.velocity = ev.velocity;
found_voice.events[va.voices[i].event_count++] = ev;
found = true;
break;
}
}
if (found) break;
// if all voices are busy, steal one round-robin
voice_state& found_voice = va.voices[va.index_to_steal];
found_voice.is_busy = true;
found_voice.midi_note = ev.midi_note;
found_voice.velocity = ev.velocity;
found_voice.events[va.voices[va.index_to_steal].event_count++] = ev;
va.index_to_steal++;
if (va.index_to_steal >= va.active_voice_count) va.index_to_steal = 0;
break;
}
case NOTE_OFF: {
for (size_t i = 0; i < va.active_voice_count; ++i) {
if (va.voices[i].midi_note == ev.midi_note)
va.voices[i].events[va.voices[i].event_count++] = ev;
}
break;
}
case PITCH_WHEEL:
case MOD_WHEEL: {
for (size_t i = 0; i < va.active_voice_count; ++i) {
va.voices[i].events[va.voices[i].event_count++] = ev;
}
break;
}
}
}
}
inline void voice_process_event_for_frame(voice_state& v, size_t frame)
{
const midi_event* best_event = nullptr;
for (int i = 0; i < v.event_count; i++) {
const midi_event& ev = v.events[i];
if (ev.offset == frame) {
best_event = &ev;
if (ev.type == NOTE_ON) break;
}
}
if (best_event) switch (best_event->type) {
case NOTE_ON:
v.midi_note = best_event->midi_note;
v.velocity = best_event->velocity;
v.is_busy = true;
v.gate = true;
v.trigger = true;
break;
case NOTE_OFF:
v.gate = false;
break;
// TODO: handle pitch wheel and mod wheel events
case PITCH_WHEEL:
case MOD_WHEEL:
break;
}
}
} // namespace trnr

View File

@@ -1,57 +0,0 @@
/*
* audio_buffer.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <vector>
using namespace std;
namespace trnr {
template <typename t_sample>
struct audio_buffer {
size_t channels;
size_t frames;
vector<t_sample> flat_data;
vector<t_sample*> channel_ptrs;
};
template <typename t_sample>
void audio_buffer_init(audio_buffer<t_sample>& a, size_t channels, size_t frames)
{
a.channels = channels;
a.frames = frames;
a.flat_data.resize(channels * frames);
a.channel_ptrs.resize(channels);
audio_buffer_update_ptrs(a);
}
template <typename t_sample>
void audio_buffer_update_ptrs(audio_buffer<t_sample>& a)
{
for (int ch = 0; ch < a.channels; ++ch) {
a.channel_ptrs[ch] = a.flat_data.data() + ch * a.frames;
}
}
} // namespace trnr

View File

@@ -1,47 +1,17 @@
/*
* audio_math.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <math.h>
namespace trnr {
inline double lin_2_db(double lin)
{
if (lin <= 1e-20) lin = 1e-20; // avoid log(0)
return 20.0 * log10(lin);
static inline double lin_2_db(double lin) {
return 20 * log(lin);
}
inline double db_2_lin(double db) { return pow(10.0, db / 20.0); }
static inline double db_2_lin(double db) {
return pow(10, db/20);
}
inline float midi_to_frequency(float midi_note)
{
static inline float midi_to_frequency(float midi_note) {
return 440.0 * powf(2.0, ((float)midi_note - 69.0) / 12.0);
}
inline float ms_to_samples(float ms, double sample_rate)
{
return (ms * 0.001f) * (float)sample_rate;
}
} // namespace trnr

33
util/averager.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <array>
namespace trnr {
template <typename t_sample, unsigned int sample_size>
class averager {
public:
averager() { samples.fill(0); }
t_sample process_sample(t_sample& _sample)
{
t_sample sum = t_sample(0);
for (unsigned int i = 0; i < sample_size; i++) {
if (i < sample_size - 1) {
// shift to the left
samples[i] = samples[i + 1];
} else {
// put new sample last
samples[i] = _sample;
}
sum += samples[i];
}
return sum / sample_size;
}
private:
std::array<t_sample, sample_size> samples;
};
} // namespace trnr

View File

@@ -1,66 +1,38 @@
/*
* demo_noise.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#define _USE_MATH_DEFINES
#include <math.h>
#include <cstdlib>
namespace trnr {
template <typename t_sample>
class demo_noise {
public:
void set_samplerate(double _samplerate) {
samplerate = _samplerate;
}
struct demo_noise {
double samplerate;
int counter = 0;
// initialize with sane defaults
int noise_len_sec = 3;
int pause_len_sec = 17;
float noise_gain = 0.1f;
void process_block(t_sample** samples, long sample_frames) {
for (int s = 0; s < sample_frames; s++) {
demo_counter++;
if (demo_counter == samplerate * 20) {
demo_counter = 0;
}
if (demo_counter > samplerate * 17) {
t_sample r1 = static_cast<t_sample>(rand()) / static_cast<t_sample>(RAND_MAX);
t_sample r2 = static_cast<t_sample>(rand()) / static_cast<t_sample>(RAND_MAX);
t_sample noise = static_cast<t_sample>(sqrt(-2.0 * log(r1)) * cos(2.0 * M_PI * r2));
samples[0][s] = noise / 10.0;
samples[1][s] = noise / 10.0;
}
}
}
private:
double samplerate = 44100;
int demo_counter = 0;
};
inline void demo_nose_init(demo_noise& d, double samplerate)
{
d.samplerate = samplerate;
d.counter = 0;
}
// overwrites the input buffer with noise in the specified time frame
inline void process_block(demo_noise& d, float** samples, int blocksize)
{
int noise_len_samples = d.noise_len_sec * d.samplerate;
int pause_len_samples = d.pause_len_sec * d.samplerate;
int total_len_samples = noise_len_samples + pause_len_samples;
for (int s = 0; s < blocksize; s++) {
d.counter++;
if (d.counter > total_len_samples) { d.counter = 0; }
if (d.counter > pause_len_samples) {
float r1 = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
float r2 = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
float noise = static_cast<float>(sqrt(-2.0 * log(r1)) * cos(2.0 * M_PI * r2));
samples[0][s] = samples[1][s] = noise * d.noise_gain;
}
}
}
} // namespace trnr

View File

@@ -1,77 +0,0 @@
/*
* format.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <algorithm>
#include <cstdarg>
#include <iomanip>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
namespace trnr {
// Formats a string (like printf) using a format string and variable arguments.
inline string format(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
vector<char> buf(256);
int needed = vsnprintf(buf.data(), buf.size(), fmt, args);
va_end(args);
if (needed < 0) { return {}; }
if (static_cast<size_t>(needed) < buf.size()) { return string(buf.data(), needed); }
// Resize and try again if the buffer was too small
buf.resize(needed + 1);
va_start(args, fmt);
vsnprintf(buf.data(), buf.size(), fmt, args);
va_end(args);
return string(buf.data(), needed);
}
inline string float_to_string_trimmed(float value)
{
ostringstream out;
out << fixed << setprecision(2) << value;
string str = out.str();
// Remove trailing zeros
str.erase(str.find_last_not_of('0') + 1, string::npos);
// If the last character is a decimal point, remove it as well
if (!str.empty() && str.back() == '.') { str.pop_back(); }
return str;
}
inline string to_upper(string& str)
{
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::toupper(c); });
return str;
}
} // namespace trnr

View File

@@ -1,30 +1,8 @@
/*
* retro_buf.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "../companding/alaw.h"
#include "../filter/chebyshev.h"
#include "../companding/ulaw.h"
#include <iostream>
namespace trnr {
@@ -41,33 +19,35 @@ struct retro_buf_modulation {
double deviation;
};
// base class for accessing a sample buffer with adjustable samplerate, bitrate and other
// options.
// base class for accessing a sample buffer with adjustable samplerate, bitrate and other options.
class retro_buf {
public:
void set_host_samplerate(double _samplerate)
{
void set_host_samplerate(double _samplerate) {
m_host_samplerate = _samplerate;
m_imaging_filter_l.set_samplerate(_samplerate);
m_imaging_filter_r.set_samplerate(_samplerate);
}
void set_buf_samplerate(double _samplerate) { m_buf_samplerate = _samplerate; }
void set_buf_samplerate(double _samplerate) {
m_buf_samplerate = _samplerate;
}
void set_buffer_size(size_t _buffer_size) { m_buffer_size = _buffer_size; }
void set_buffer_size(size_t _buffer_size) {
m_buffer_size = _buffer_size;
}
void set_channel_count(size_t _channel_count) { m_channel_count = _channel_count; }
void set_channel_count(size_t _channel_count) {
m_channel_count = _channel_count;
}
void start_playback()
{
void start_playback() {
if (m_modulation.reset || (!m_modulation.reset && m_playback_pos == -1)) {
m_playback_pos = (double)m_modulation.start;
}
}
// @return is active
bool process_block(double** _outputs, size_t _block_size, retro_buf_modulation _mod)
{
bool process_block(double** _outputs, size_t _block_size, retro_buf_modulation _mod) {
m_modulation = _mod;
@@ -80,19 +60,12 @@ public:
// quantize index
double samplerate_divisor = m_host_samplerate / _mod.samplerate;
size_t quantized_index = static_cast<size_t>(
static_cast<size_t>(m_playback_pos / samplerate_divisor) *
samplerate_divisor);
size_t quantized_index = static_cast<size_t>(static_cast<size_t>(m_playback_pos / samplerate_divisor) * samplerate_divisor);
// get sample for each channel
output_l = get_sample(
(size_t)wrap(quantized_index + jitterize(_mod.jitter), m_buffer_size),
0);
output_l = get_sample((size_t)wrap(quantized_index + jitterize(_mod.jitter), m_buffer_size), 0);
if (m_channel_count > 0) {
output_r =
get_sample((size_t)wrap(quantized_index + jitterize(_mod.jitter),
m_buffer_size),
1);
output_r = get_sample((size_t)wrap(quantized_index + jitterize(_mod.jitter), m_buffer_size), 1);
} else {
output_r = output_l;
}
@@ -104,8 +77,7 @@ public:
reduce_bitrate(output_l, output_r, _mod.bitrate);
// calculate imaging filter frequency + deviation
double filter_frequency =
((_mod.samplerate / 2) * note_ratio) * ((_mod.deviation * 9) + 1);
double filter_frequency = ((_mod.samplerate / 2) * note_ratio) * ((_mod.deviation * 9) + 1);
m_imaging_filter_l.process_sample(output_l, filter_frequency);
m_imaging_filter_r.process_sample(output_r, filter_frequency);
@@ -139,16 +111,15 @@ private:
chebyshev m_imaging_filter_l;
chebyshev m_imaging_filter_r;
ulaw m_compander;
retro_buf_modulation m_modulation;
float midi_to_ratio(double midi_note)
{
float midi_to_ratio(double midi_note) {
return powf(powf(2, (float)midi_note - 60.f), 1.f / 12.f);
}
template <typename T>
T clamp(T& value, T min, T max)
{
T clamp(T& value, T min, T max) {
if (value < min) {
value = min;
} else if (value > max) {
@@ -157,14 +128,14 @@ private:
return value;
}
double wrap(double value, double max)
{
while (value > max) { value -= max; }
double wrap(double value, double max) {
while (value > max) {
value =- max;
}
return value;
}
int jitterize(int jitter)
{
int jitterize(int jitter) {
if (jitter > 0) {
return static_cast<int>(rand() % jitter);
} else {
@@ -172,17 +143,14 @@ private:
}
}
void reduce_bitrate(double& value1, double& value2, double bit)
{
value1 = alaw_encode(value1);
value2 = alaw_encode(value2);
void reduce_bitrate(double& value1, double& value2, double bit) {
m_compander.encode_samples(value1, value2);
float resolution = powf(2, bit);
value1 = round(value1 * resolution) / resolution;
value2 = round(value2 * resolution) / resolution;
value1 = alaw_decode(value1);
value2 = alaw_decode(value2);
m_compander.decode_samples(value1, value2);
}
};
} // namespace trnr
}

104
util/sample.h Normal file
View File

@@ -0,0 +1,104 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace trnr {
class sample {
public:
sample(int16_t initial_value = 0) {
set_value(initial_value);
instances.push_back(this); // track this instance
}
~sample() {
// remove this instance from the tracking vector
auto it = std::find(instances.begin(), instances.end(), this);
if (it != instances.end()) {
instances.erase(it);
}
}
// set value while ensuring the global bit depth
void set_value(int16_t new_value) {
int16_t max_val = (1 << (global_bit_depth - 1)) - 1; // Max value for signed bit depth
int16_t min_val = -(1 << (global_bit_depth - 1)); // Min value for signed bit depth
// Clamp the value to the allowed range
if (new_value > max_val) {
value = max_val;
} else if (new_value < min_val) {
value = min_val;
} else {
value = new_value;
}
}
int16_t get_value() const {
return value;
}
static void set_global_bit_depth(int depth) {
if (depth < 1 || depth > 16) {
throw std::invalid_argument("Bit depth must be between 1 and 16.");
}
// rescale all existing values if the bit depth changes
float scaling_factor = get_scaling_factor(previous_bit_depth, global_bit_depth);
// Rescale all existing instances
for (auto* instance : instances) {
instance->value = std::round(instance->value * scaling_factor);
}
previous_bit_depth = global_bit_depth; // Store the old bit depth
global_bit_depth = depth; // Update the global bit depth
}
static int get_global_bit_depth() {
return global_bit_depth;
}
// arithmetic operators (all respect global bit depth)
sample operator+(const sample& other) const {
return sample(this->value + other.value);
}
sample operator-(const sample& other) const {
return sample(this->value - other.value);
}
sample operator*(const sample& other) const {
return sample(this->value * other.value);
}
sample operator/(const sample& other) const {
if (other.value == 0) {
throw std::runtime_error("division by zero.");
}
return sample(this->value / other.value);
}
// Cast to int16_t for convenience
operator int16_t() const {
return value;
}
private:
int16_t value;
static int global_bit_depth; // global bit depth
static int previous_bit_depth; // previous bit depth
static std::vector<sample*> instances; // track all instances for rescaling
// helper function to calculate the scaling factor
static float get_scaling_factor(int old_bit_depth, int new_bit_depth) {
if (old_bit_depth == new_bit_depth) {
return 1.0f; // No scaling needed
}
float old_max = (1 << (old_bit_depth - 1)) - 1;
float new_max = (1 << (new_bit_depth - 1)) - 1;
return new_max / old_max;
}
};
}

View File

@@ -1,84 +0,0 @@
/*
* smoother.h
* Copyright (c) 2025 Christopher Herb
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "audio_math.h"
namespace trnr {
struct smoother {
float samplerate;
float time_samples;
float current;
float target;
float increment;
int32_t remaining;
};
inline void smoother_init(smoother& s, double samplerate, float time_ms,
float initial_value = 0.0f)
{
s.samplerate = fmax(0.0, samplerate);
s.time_samples = ms_to_samples(time_ms, s.samplerate);
s.current = initial_value;
s.target = initial_value;
s.increment = 0.0f;
s.remaining = 0;
}
inline void smoother_set_target(smoother& s, float newTarget)
{
s.target = newTarget;
// immediate if time is zero or too short
if (s.time_samples <= 1.0f) {
s.current = s.target;
s.increment = 0.0f;
s.remaining = 0;
return;
}
int32_t n = static_cast<int32_t>(fmax(1.0f, ceilf(s.time_samples)));
s.remaining = n;
// protect against denormals / tiny differences, but we want exact reach at the end
s.increment = (s.target - s.current) / static_cast<float>(s.remaining);
}
// process a single sample and return the smoothed value
inline float smoother_process_sample(smoother& s)
{
if (s.remaining > 0) {
s.current += s.increment;
--s.remaining;
if (s.remaining == 0) {
// ensure exact target at the end to avoid FP drift
s.current = s.target;
s.increment = 0.0f;
}
}
return s.current;
}
} // namespace trnr