You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-06-13 13:12:09 +00:00
This is a [5/4]-Padé approximant, tweaked to also always limit the
result to the ±1.0 range.
It's basically accurate to within the float representation(*) in the
reasonable [-1,1] range, with the max error at ±3.647, where the
accuracy has fallen to "only" 9.5 bits (2.87 decimal digits).
This was triggered by discussion with Ricky Sheaves, who is tuning the
Echo King effect and the simplistic tanh() approximation using the
'limit_value()' code was audibly not good enough.
I didn't actually make the Echo King code use this new tanhf(), because
Ricky is doing other changes to it too, but I wanted to at least add the
improved infrastructure.
(*) Ok, really only 22 bits - we're not talking "0.5 ULP" kind of
precision here. It's still an approximation, just a very good one.
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
307 lines
10 KiB
Plaintext
307 lines
10 KiB
Plaintext
Some rough conversion guidelines from the open-source hothouse pedals
|
|
that use the C++ abstractions from the daisy seed library to the simple
|
|
direct forms that this pedal model uses:
|
|
|
|
- The Hothouse pedal has six analog potentiometers with floating point
|
|
values in the range [0,1[ and three toggle switches with the states
|
|
TOGGLESWITCH_UP, TOGGLESWITCH_MIDDLE and TOGGLESWITCH_DOWN.
|
|
|
|
This pedal project has a maximum of ten digital "pot" values. For
|
|
continuous parameters, these use the range [-100,+100] and are often
|
|
converted using "linear_pot()" or "POT_TO_FLOAT()" into a specific
|
|
floating point range.
|
|
|
|
For toggle switches, the UI supports discrete "enumerations". By
|
|
providing a NULL-terminated array of string pointers to the
|
|
`enum_names` field in `struct pot_descr`, the UI will automatically
|
|
restrict the pot value to the valid range (0, 1, 2, ...) and display
|
|
the selected option horizontally instead of drawing a slider.
|
|
|
|
Furthermore, because this project has a screen UI, it is highly
|
|
encouraged to provide real-world units for continuous pots instead of
|
|
just 0 to 1 ranges. The `convert` and `describe` functions in the pot
|
|
descriptor can use the current effect state (such as the value of an
|
|
enumeration toggle) to calculate and display the true value (e.g.
|
|
showing the actual delay time in "ms", or cutoff in "Hz").
|
|
|
|
- The Hothouse pedals are C++ code with complex header structures
|
|
brought in from typically the Daisy Seed open-source DSP library.
|
|
|
|
This pedal project instead relies on self-contained C 'headers' that
|
|
contain all the code itself, somewhat similar to so-called "header
|
|
libraries". So the effects just get included by the main program and
|
|
used in one single compilation unit.
|
|
|
|
That is not to say that there aren't libraries, but they are other
|
|
headers, particularly 'audio/util.h' for some core math helpers,
|
|
'audio/biquad.h' for second-order filters, and 'audio/lfo.h' for some
|
|
simple low-frequency oscillators. But the effects don't need to
|
|
include those headers, they are pre-included.
|
|
|
|
- The daisy seed library buffers up samples and does stereo signals, so
|
|
the Hothouse pedal sources use a pattern line
|
|
|
|
void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out,
|
|
size_t size) {
|
|
...
|
|
for (size_t i = 0; i < size; ++i) {
|
|
const float dry_in = in[0][i];
|
|
....
|
|
out[0][i] = out[1][i] = preamp_out * dry_gain + wet * wet_gain;
|
|
}
|
|
}
|
|
|
|
for the audio effect, while this guitar pedal just does mono, and one
|
|
sample at a time:
|
|
|
|
static inline float effect_step(float in)
|
|
{
|
|
float out = ...
|
|
...
|
|
return out;
|
|
}
|
|
|
|
instead.
|
|
|
|
- There are a lot more abstractions fairly deep in the libraries, so
|
|
Hothouse pedal source code does things like
|
|
|
|
record_lpf.SetCutoff(active->record_fc, sample_rate);
|
|
|
|
...
|
|
|
|
float record_signal =
|
|
record_lpf.Process(preamp_out * s_record_level.current);
|
|
|
|
where 'record_lpf' is an object (of type OnePoleLpf) that contains
|
|
the coefficients for the low-pass filter.
|
|
|
|
In contrast, this pedal project uses biquad filters instead, and
|
|
passes the state as explicit arguments where the biquad coefficients
|
|
encode the filter, so the equivalent would be
|
|
|
|
biquad_lpf(&record_biquad, record_fc, 0.707);
|
|
|
|
...
|
|
|
|
float record_signal = biquad_step(&record_biquad,
|
|
preamp_out * s_record_level.current);
|
|
|
|
instead. The 0.707 being the Q value for a single-pole filter, and
|
|
the sample rate is just implicit.
|
|
|
|
- The hothouse pedals use the full math library, while this pedal
|
|
project uses a few specialized functions:
|
|
|
|
fastsincos():
|
|
|
|
returns both sine and cosine values as a 'struct sincos', and
|
|
takes a 'phase' argument that is strictly positive.
|
|
|
|
So 'sinf(x)' could be written as 'fastsincos(x*2*pi).sin'
|
|
|
|
pow2() and log2f():
|
|
|
|
These are in powers-of-2, not natural or powers-of-10. There are
|
|
a couple of related helper macros, like
|
|
|
|
#define log10f(x) (log2f(x)/LOG2_10)
|
|
|
|
static inline float time_constant(float ms)
|
|
{
|
|
return pow2(-1.0 / SAMPLES_PER_MSEC / ms);
|
|
}
|
|
|
|
static inline float db_to_level(float db)
|
|
{
|
|
return pow2(LOG2_10 / 20.0f * db);
|
|
}
|
|
|
|
for common special cases.
|
|
|
|
tanhf():
|
|
|
|
[5/4]-Padé approximant, accurate to within 22 bits in [-1,1] and
|
|
9.5 bits globally.
|
|
|
|
u32_to_fraction() and fraction_to_u32():
|
|
|
|
These take a 32-bit integer and turn it into a fraction between
|
|
[0, 1[ (and the reverse). Typically used for the phase calculation
|
|
for sine/cosine, where the phase calculations are done in 32-bit
|
|
integers which is not only fast and simple but also gets us the
|
|
natural wrapping behavior we want.
|
|
|
|
For example, the Hothouse EchoKing effect has some code like this:
|
|
|
|
// Phase accumulator advance. Subtracting one full period (rather than fmodf
|
|
// or a zero-reset) keeps the input to sinf() in a well-conditioned range and
|
|
// avoids discontinuities at the wrap point.
|
|
static inline void AdvancePhase(float* phase, float inc) {
|
|
*phase += inc;
|
|
if (*phase >= kTwoPi) *phase -= kTwoPi;
|
|
}
|
|
|
|
but in this pedal project the way you'd do this would be to make
|
|
the phase and increment be 32-bit unsigned integers, and just
|
|
rely on the wrapping behavior of the integer math, with the
|
|
whole phase being that 0..4294967295 range turned into [-0, 1[.
|
|
|
|
rintf(), sqrtf(), fabsf() and floorf():
|
|
|
|
These - along with the usual addition, subtraction,
|
|
multiplication and division - are handled by the hardware and
|
|
are ok to use as-is.
|
|
|
|
Any other complex math should be approximated appropriately.
|
|
|
|
- WhiteNoise generators: The hothouse pedals may use 'WhiteNoise'
|
|
objects from the Daisy library.
|
|
|
|
This pedal project doesn't have a standard random number generator.
|
|
For things like tape hiss, a simple Linear Congruential Generator
|
|
(LCG) is fast and sufficient:
|
|
|
|
static uint32_t noise_state = 1;
|
|
static inline float get_white_noise()
|
|
{
|
|
noise_state = noise_state * 1664525 + 1013904223;
|
|
// return float from -1.0 to 1.0
|
|
return (float)noise_state / 2147483648.0f - 1.0f;
|
|
}
|
|
|
|
|
|
- The hothouse pedal code tends to use potentiometer objects:
|
|
|
|
Parameter p_blend;
|
|
...
|
|
p_blend.Init(hw.knobs[Hothouse::KNOB_1], 0.0f, 1.0f, Parameter::LINEAR);
|
|
|
|
whereas this pedal project uses C structures as "objects" for the
|
|
effect description and the effect itself, but uses a very explicit
|
|
pot model, typically something like
|
|
|
|
struct {
|
|
float mix;
|
|
...
|
|
} boost;
|
|
...
|
|
static float boost_mix(signed char pot) { return linear_pot(pot, 0, 1); }
|
|
...
|
|
void boost_init(signed char pot[10])
|
|
{
|
|
...
|
|
boost.mix = boost_mix(pot[4]);
|
|
}
|
|
|
|
static float boost_step(float in)
|
|
{
|
|
...
|
|
return linear(boost.mix, in, out);
|
|
}
|
|
|
|
without that "Parameter" abstraction model.
|
|
|
|
- Similarly to the 'Parameter' abstraction model, the Hothouse pedal
|
|
effects use things like a 'smoothing' abstraction, and the
|
|
potentiometer value is then smoothed at the sample rate frequency
|
|
using a model like:
|
|
|
|
Smoothed s_blend{0.5f, 0.5f, 0.0008f};
|
|
...
|
|
// --- Knob targets ---
|
|
s_blend.target = p_blend.Process();
|
|
...
|
|
s_blend.Tick();
|
|
...
|
|
float blend = preamp_only ? 0.0f : s_blend.current;
|
|
|
|
in order to avoid strange audio effects when moving the
|
|
potentiometers (or simply because the potentiometers are analog and
|
|
the values aren't stable)
|
|
|
|
In this pedal project, that smoothing is typically not done and if
|
|
there's a clicking from the rotation of the rotary switches changing
|
|
the value, it is acceptable.
|
|
|
|
I say "typically", because the delay values can be very noticeable
|
|
and annoying when they change in big steps, so sometimes there's very
|
|
explicit smoothing. See audio/echo.h and the 'target_delay' use as an
|
|
example of this, where we set 'target_delay' at effect init time, and
|
|
at the sample frequency we smoothly change 'echo.delay' towards that
|
|
number:
|
|
|
|
struct {
|
|
float delay, target_delay;
|
|
..
|
|
} echo;
|
|
...
|
|
echo.target_delay = echo_pot0(pot[0]) * SAMPLES_PER_MSEC;
|
|
...
|
|
echo.delay = linear(0.001, echo.delay, echo.target_delay);
|
|
|
|
- the Hothouse pedal is designed to run one effect at a time, so it has
|
|
a pattern of
|
|
|
|
int main() {
|
|
hw.Init();
|
|
...
|
|
|
|
while (true) {
|
|
// LED_1 lights when a non-standard mode is active (SOS or Preamp Only).
|
|
led_mode.Set((sos_mode || preamp_only) ? 1.0f : 0.0f);
|
|
led_bypass.Set(bypass ? 0.0f : 1.0f);
|
|
led_mode.Update();
|
|
led_bypass.Update();
|
|
|
|
System::Delay(10);
|
|
hw.CheckResetToBootloader();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
but in this pedal project, that is all done outside of the effects.
|
|
But the effects can set their LED to shine brighter as a status
|
|
signal like that "non-standard mode" thing, by setting the 'intense'
|
|
flag in the effect structure. For example, the boost effect will do
|
|
|
|
boost_effect.intense = 1;
|
|
|
|
when it is clipping the signal.
|
|
|
|
The effect initialization is done with the 'init' function that gets
|
|
called when the code starts or whenever pot values change, and that
|
|
init code is supposed to take the pot values and turn them into
|
|
whatever form is efficient to then use at the sample stepping time.
|
|
|
|
See 'audio/boost.h' for a simple example of this pattern.
|
|
|
|
- Finally: try to transfer over comments that are still relevant after
|
|
the code conversion.
|
|
|
|
So comments about issues specific to the Daisy Seed model or the
|
|
extra C++ object abstraction layers should just be dropped as
|
|
irrelevant after the conversion, but comments about the workings of
|
|
the effect should be converted over (possibly with updates when the
|
|
implementation changed)
|
|
|
|
One special case of comment is the README.md that most of the effects
|
|
have. The Hothouse source code is structured with each effect in its
|
|
own subdirectory, while this guiotar pedal project has a "one header
|
|
file per effect" model.
|
|
|
|
So transferring the salient points of the README.md file to be a
|
|
large comment at the top of the file is probably a good idea, at
|
|
least within reason.
|
|
|
|
And when some particular piece of code changes in big and noticeable
|
|
ways due to the conversion, that itself would merit a comment with a
|
|
note about why something is very different in the converted
|
|
implementation.
|
|
|
|
Note that the straightforward direct translations mostly documented
|
|
above are not worth elaborating on, but when the conversion adds a
|
|
new feature (like the "add real world units" to the output), that
|
|
kind of semantic new feature may be worth noting.
|