Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions Software/GuitarPedal/Effect-Modules/harmonic_tremolo_module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
#include "harmonic_tremolo_module.h"
#include "../Util/audio_utilities.h"
#include <array>

using namespace bkshepherd;

namespace {

// LFO range. Slower than the plain tremolo module because the band sweep stays
// musical well below 1 Hz.
constexpr float kSpeedMin = 0.2f;
constexpr float kSpeedMax = 16.0f;

// The depth knob is scaled past unity on purpose. Above 0.8 the high band
// briefly inverts at the LFO peaks, which is where the throaty, almost phasey
// character of this effect comes from.
constexpr float kDepthScale = 1.25f;

// Recombining the two bands loses a little level, so the output is trimmed
// back up. Flick uses a flat 1.2 here, but it can afford to: downstream it
// feeds a reverb and a hard limiter. Standing alone the flat value is a
// problem, because the two counter phase bands sum to more energy as depth
// rises. Measured over the depth range that is about 3.8dB of drift, ending
// at a peak of 1.49, and the pedal writes straight out to the codec with
// nothing to catch it.
//
// So the gain is scaled by the RMS of the modulators instead, sqrt(1 + m^2/2)
// for a bipolar sine of amplitude m. That holds the output inside a 1.3dB
// window at every depth setting and keeps peaks under about 1.12. Turning
// depth up then changes the character rather than the volume.
constexpr float kMakeupGainBase = 1.2f;

// Band split, taken from the Fender 6G12-A schematic.
constexpr float kBandSplitLowCutoff = 144.0f; // 220K and 5nF low pass
constexpr float kBandSplitHighCutoff = 636.0f; // 1M and 250pF high pass

// Fixed EQ voicing applied after the bands are recombined.
constexpr float kEqHighPassCutoff = 63.0f;
constexpr float kEqLowPassCutoff = 11200.0f;
constexpr float kEqLowShelfFreq = 37.0f;
constexpr float kEqLowShelfGain = -10.5f; // in dB
constexpr float kEqLowShelfQ = 1.0f;
constexpr float kEqPeakLowMidFreq = 254.0f;
constexpr float kEqPeakLowMidGain = 2.0f; // in dB
constexpr float kEqPeakLowMidQ = 0.707f;
constexpr float kEqPeakPresenceFreq = 7500.0f;
constexpr float kEqPeakPresenceGain = -3.37f; // in dB
constexpr float kEqPeakPresenceQ = 0.263f;

// Dummy values that get overwritten in Init
constexpr float kDefaultSampleRate = 48000.0f;

/** Configures a low shelf from an explicit Q.
*
* cycfi::q::lowshelf accepts a q argument but discards it, hardcoding a shelf
* slope of S = 1 (its beta is sqrt(2A)). At -10.5dB that is a noticeably
* steeper shelf than the Q = 1 this effect is voiced around, so the standard
* audio EQ cookbook coefficients are computed here and pushed into a plain
* biquad instead. cycfi::q::peaking needs no such treatment, it already
* derives alpha from q the same way.
*/
void ConfigureLowShelf(cycfi::q::biquad &filter, float gainDb, float freq, float q, float sample_rate) {
const float a = powf(10.0f, gainDb / 40.0f);
const float omega = 2.0f * PI_F * freq / sample_rate;
const float sinOmega = sinf(omega);
const float cosOmega = cosf(omega);
const float alpha = sinOmega / (2.0f * q);
const float beta = 2.0f * sqrtf(a) * alpha;

const float b0 = a * ((a + 1.0f) - (a - 1.0f) * cosOmega + beta);
const float b1 = 2.0f * a * ((a - 1.0f) - (a + 1.0f) * cosOmega);
const float b2 = a * ((a + 1.0f) - (a - 1.0f) * cosOmega - beta);
const float a0 = (a + 1.0f) + (a - 1.0f) * cosOmega + beta;
const float a1 = -2.0f * ((a - 1.0f) + (a + 1.0f) * cosOmega);
const float a2 = (a + 1.0f) + (a - 1.0f) * cosOmega - beta;

filter.config(b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0);
}

/** Makeup gain for a given modulation amount, see kMakeupGainBase. */
float MakeupGainForDepth(float modulation) {
return kMakeupGainBase / sqrtf(1.0f + modulation * modulation * 0.5f);
}

} // namespace

static const auto s_metaData = [] {
std::array<ParameterMetaData, HarmonicTremoloModule::PARAM_COUNT> params{};

params[HarmonicTremoloModule::SPEED] = {
name : "Speed",
valueType : ParameterValueType::Float,
valueBinCount : 0,
defaultValue : {.float_value = 0.25f},
knobMapping : 0,
midiCCMapping : 14
};

params[HarmonicTremoloModule::DEPTH] = {
name : "Depth",
valueType : ParameterValueType::Float,
valueBinCount : 0,
defaultValue : {.float_value = 0.5f},
knobMapping : 1,
midiCCMapping : 15
};

return params;
}();

// Default Constructor
HarmonicTremoloModule::HarmonicTremoloModule()
: BaseEffectModule(), m_eqLowShelf{cycfi::q::biquad(1.0f, 0.0f, 0.0f, 0.0f, 0.0f), cycfi::q::biquad(1.0f, 0.0f, 0.0f, 0.0f, 0.0f)},
m_eqPeakLowMid{cycfi::q::peaking(kEqPeakLowMidGain, cycfi::q::frequency{kEqPeakLowMidFreq}, kDefaultSampleRate, kEqPeakLowMidQ),
cycfi::q::peaking(kEqPeakLowMidGain, cycfi::q::frequency{kEqPeakLowMidFreq}, kDefaultSampleRate, kEqPeakLowMidQ)},
m_eqPeakPresence{
cycfi::q::peaking(kEqPeakPresenceGain, cycfi::q::frequency{kEqPeakPresenceFreq}, kDefaultSampleRate, kEqPeakPresenceQ),
cycfi::q::peaking(kEqPeakPresenceGain, cycfi::q::frequency{kEqPeakPresenceFreq}, kDefaultSampleRate, kEqPeakPresenceQ)},
m_speedSmoothed(kSpeedMin), m_depthSmoothed(0.0f), m_makeupGain(kMakeupGainBase), m_lastLfoValue(0.0f) {
// Set the name of the effect
m_name = "Harm Trem";

// Setup the meta data reference for this Effect
m_paramMetaData = s_metaData.data();

// Initialize Parameters for this Effect
this->InitParams(static_cast<int>(s_metaData.size()));
}

// Destructor
HarmonicTremoloModule::~HarmonicTremoloModule() {
// No Code Needed
}

void HarmonicTremoloModule::Init(float sample_rate) {
BaseEffectModule::Init(sample_rate);

m_lfo.Init(sample_rate);
m_lfo.SetWaveform(Oscillator::WAVE_SIN);

for (int channel = 0; channel < 2; channel++) {
m_bandLow[channel].Init(kBandSplitLowCutoff, sample_rate);
m_bandHigh[channel].Init(kBandSplitHighCutoff, sample_rate);

m_eqHighPass[channel].Init(kEqHighPassCutoff, sample_rate);
m_eqLowPass[channel].Init(kEqLowPassCutoff, sample_rate);

ConfigureLowShelf(m_eqLowShelf[channel], kEqLowShelfGain, kEqLowShelfFreq, kEqLowShelfQ, sample_rate);
m_eqPeakLowMid[channel].config(kEqPeakLowMidGain, cycfi::q::frequency{kEqPeakLowMidFreq}, sample_rate, kEqPeakLowMidQ);
m_eqPeakPresence[channel].config(kEqPeakPresenceGain, cycfi::q::frequency{kEqPeakPresenceFreq}, sample_rate, kEqPeakPresenceQ);
}

// Seed the smoothed values so the first buffer starts at the knob
// positions instead of sliding up to them.
m_speedSmoothed = kSpeedMin + GetParameterAsFloat(SPEED) * (kSpeedMax - kSpeedMin);
m_depthSmoothed = GetParameterAsFloat(DEPTH) * kDepthScale;
m_makeupGain = MakeupGainForDepth(m_depthSmoothed);
m_lastLfoValue = 0.0f;
}

float HarmonicTremoloModule::ProcessChannel(int channel, float in, float lfo) {
// Split into the two bands. These overlap in the middle, which is what
// keeps the recombined signal from sounding scooped.
const float low = m_bandLow[channel].Process(in);
const float high = m_bandHigh[channel].Process(in);

// Modulate the bands against each other rather than modulating amplitude.
float out = low * (1.0f + lfo) + high * (1.0f - lfo);

// Fixed voicing. Order matters here, the shelf and peaks are trimming what
// the band pair leaves behind.
out = m_eqHighPass[channel].Process(out);
out = m_eqLowPass[channel].Process(out);
out = m_eqLowShelf[channel](out);
out = m_eqPeakLowMid[channel](out);
out = m_eqPeakPresence[channel](out);

return out;
}

void HarmonicTremoloModule::ProcessMono(float in) {
BaseEffectModule::ProcessMono(in);

const float speedTarget = kSpeedMin + GetParameterAsFloat(SPEED) * (kSpeedMax - kSpeedMin);
const float depthTarget = GetParameterAsFloat(DEPTH) * kDepthScale;

// Ease both into their targets to avoid zipper noise on knob moves.
fonepole(m_speedSmoothed, speedTarget, .01f);
fonepole(m_depthSmoothed, depthTarget, .01f);

m_lfo.SetFreq(m_speedSmoothed);
m_lfo.SetAmp(m_depthSmoothed);
m_lastLfoValue = m_lfo.Process();

// Track the gain against the smoothed depth so it never steps.
m_makeupGain = MakeupGainForDepth(m_depthSmoothed);

m_audioLeft = ProcessChannel(0, m_audioLeft, m_lastLfoValue) * m_makeupGain;
m_audioRight = m_audioLeft;
}

void HarmonicTremoloModule::ProcessStereo(float inL, float inR) {
// Calculate the mono effect, which also advances the LFO for both channels
ProcessMono(inL);

// Do the base stereo calculation (which resets the right signal to be the
// inputR instead of combined mono)
BaseEffectModule::ProcessStereo(m_audioLeft, inR);

// Reuse the LFO value and gain from the left channel so the two stay
// locked together instead of drifting into an unintended auto-pan.
m_audioRight = ProcessChannel(1, m_audioRight, m_lastLfoValue) * m_makeupGain;
}

void HarmonicTremoloModule::SetTempo(uint32_t bpm) {
float freq = tempo_to_freq(bpm);

if (freq <= kSpeedMin) {
SetParameterAsMagnitude(SPEED, 0.0f);
} else if (freq >= kSpeedMax) {
SetParameterAsMagnitude(SPEED, 1.0f);
} else {
float magnitude = (freq - kSpeedMin) / (kSpeedMax - kSpeedMin);
SetParameterAsMagnitude(SPEED, magnitude);
}
}

float HarmonicTremoloModule::GetBrightnessForLED(int led_id) const {
float value = BaseEffectModule::GetBrightnessForLED(led_id);

if (led_id == 1) {
// The LFO is bipolar and can exceed unity at high depth, so fold it
// into 0..1 for the LED.
return value * fclamp(0.5f + 0.5f * m_lastLfoValue, 0.0f, 1.0f);
}

return value;
}
110 changes: 110 additions & 0 deletions Software/GuitarPedal/Effect-Modules/harmonic_tremolo_module.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#pragma once
#ifndef HARMONIC_TREMOLO_MODULE_H
#define HARMONIC_TREMOLO_MODULE_H

#include "base_effect_module.h"
#include "daisysp.h"
#include <q/fx/biquad.hpp>
#include <stdint.h>
#ifdef __cplusplus

/** @file harmonic_tremolo_module.h */

using namespace daisysp;

namespace bkshepherd {

/** First order RC low pass, alpha = exp(-2 * pi * fc / fs).
*
* The band split this module is built around comes from a Fender 6G12-A, whose
* filters are single RC pairs. A biquad would be twice as steep and would not
* blend the two bands the same way, so the one pole form is kept deliberately.
*/
struct OnePoleLowPass {
void Init(float cutoff, float sample_rate) {
alpha = expf(-2.0f * PI_F * cutoff / sample_rate);
prevY = 0.0f;
}

float Process(float in) {
prevY = (1.0f - alpha) * in + alpha * prevY;
return prevY;
}

float alpha = 0.0f;
float prevY = 0.0f;
};

/** First order RC high pass, matching OnePoleLowPass. */
struct OnePoleHighPass {
void Init(float cutoff, float sample_rate) {
alpha = expf(-2.0f * PI_F * cutoff / sample_rate);
prevX = 0.0f;
prevY = 0.0f;
}

float Process(float in) {
prevY = (1.0f + alpha) * 0.5f * (in - prevX) + alpha * prevY;
prevX = in;
return prevY;
}

float alpha = 0.0f;
float prevX = 0.0f;
float prevY = 0.0f;
};

/** Harmonic tremolo.
*
* Splits the signal into a low and a high band and modulates them with
* opposite LFO phase, so the tone sweeps between dark and bright instead of
* simply getting louder and quieter. A fixed EQ chain after the split supplies
* the rest of the vintage voicing.
*/
class HarmonicTremoloModule : public BaseEffectModule {
public:
enum Param {
SPEED = 0,
DEPTH,
PARAM_COUNT
};

HarmonicTremoloModule();
~HarmonicTremoloModule();

void Init(float sample_rate) override;
void ProcessMono(float in) override;
void ProcessStereo(float inL, float inR) override;
void SetTempo(uint32_t bpm) override;
float GetBrightnessForLED(int led_id) const override;

private:
/** Runs one channel's band split, modulation and EQ chain.
\param channel 0 for left, 1 for right.
\param in Dry input sample for that channel.
\param lfo Current LFO value, shared by both channels.
\return The processed sample, before makeup gain.
*/
float ProcessChannel(int channel, float in, float lfo);

Oscillator m_lfo;

// Band splitting filters, [0] is left and [1] is right.
OnePoleLowPass m_bandLow[2];
OnePoleHighPass m_bandHigh[2];

// Fixed EQ voicing applied after the two bands are recombined.
OnePoleHighPass m_eqHighPass[2];
OnePoleLowPass m_eqLowPass[2];
cycfi::q::biquad m_eqLowShelf[2];
cycfi::q::peaking m_eqPeakLowMid[2];
cycfi::q::peaking m_eqPeakPresence[2];

float m_speedSmoothed;
float m_depthSmoothed;
float m_makeupGain;
float m_lastLfoValue;
};
} // namespace bkshepherd
#endif
#endif
1 change: 1 addition & 0 deletions Software/GuitarPedal/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ CPP_SOURCES += Effect-Modules/flanger_module.cpp
CPP_SOURCES += Effect-Modules/geq_module.cpp
CPP_SOURCES += Effect-Modules/granulardelay_module.cpp
CPP_SOURCES += Effect-Modules/distortion_module.cpp
CPP_SOURCES += Effect-Modules/harmonic_tremolo_module.cpp
CPP_SOURCES += Effect-Modules/ImpulseResponse/ImpulseResponse.cpp
CPP_SOURCES += Effect-Modules/ImpulseResponse/ir_data.cpp
CPP_SOURCES += Effect-Modules/ir_module.cpp
Expand Down
2 changes: 2 additions & 0 deletions Software/GuitarPedal/loaded_effects.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "Effect-Modules/flanger_module.h"
#include "Effect-Modules/geq_module.h"
#include "Effect-Modules/granulardelay_module.h"
#include "Effect-Modules/harmonic_tremolo_module.h"
#include "Effect-Modules/ir_module.h"
#include "Effect-Modules/looper_module.h"
#include "Effect-Modules/metro_module.h"
Expand Down Expand Up @@ -84,6 +85,7 @@ void load_effects(int &availableEffectsCount, BaseEffectModule **&availableEffec
new PhaserModule(),
new FlangerModule(),
new CrusherModule(),
new HarmonicTremoloModule(),

// The following require a MIDI keyboard
// new MidiKeysModule(),
Expand Down