Skip to content

Commit

Permalink
Env: Use cytomic bandpass as the pitch pre-filter.
Browse files Browse the repository at this point in the history
Experimenting, I found that Gravy made a good pre-filter
for Env's pitch detector. So instead of using the simple
lo/hi-cut pair of filters for bandpass, use the same Cytomic
filter as I use in Gravy. Use bandpass mode for now, and
expose FREQ and RES control groups just like Gravy does.
FREQ helps center on the expected detected pitches, and
RES helps squeeze the frequency response near that center
frequency.
  • Loading branch information
cosinekitty committed Feb 11, 2025
1 parent 5cf93e1 commit 0339dd8
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 75 deletions.
Binary file modified images/env.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions res/env.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 12 additions & 28 deletions src/env_pitch_detect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
#include <cmath>
#include <vector>
#include "sapphire_engine.hpp"
#include "sauce_engine.hpp"

namespace Sapphire
{
const int NO_PITCH_VOLTS = -10; // V/OCT = -10V indicates no pitch detected at all
const float EnvCutFreqMin = 15;
const float EnvCutFreqMax = 25000;

template <typename value_t>
struct EnvPitchChannelInfo
Expand All @@ -21,9 +20,8 @@ namespace Sapphire
value_t filteredWaveLength;
bool first_thresh;

using filter_t = StagedFilter<value_t, 1>;
filter_t loCutFilter;
filter_t hiCutFilter;
using filter_t = Gravy::SingleChannelGravyEngine<value_t>;
filter_t pitchFilter;

value_t envAttack = 0;
value_t envDecay = 0;
Expand All @@ -44,23 +42,10 @@ namespace Sapphire
rawWaveLengthDescend = 0;
filteredWaveLength = 0;
first_thresh = true;

// Reset all filters in case they went non-finite.
loCutFilter.Reset();
hiCutFilter.Reset();

pitchFilter.initialize();
envelope = 0;
}

value_t bandpass(value_t input, value_t loFreq, value_t hiFreq, int sampleRateHz)
{
loCutFilter.SetCutoffFrequency(loFreq);
value_t locut = loCutFilter.UpdateHiPass(input, sampleRateHz);

hiCutFilter.SetCutoffFrequency(hiFreq);
return hiCutFilter.UpdateLoPass(locut, sampleRateHz);
}

value_t pitch(int sampleRateHz, value_t centerFrequencyHz) const
{
// Convert wavelength [samples] to frequency [Hz] to pitch [V/OCT].
Expand Down Expand Up @@ -99,8 +84,6 @@ namespace Sapphire

int currentSampleRate = 0;
value_t centerFrequencyHz = 261.6255653005986; // note C4 = 440 / (2**(3/4))
value_t loCutFrequency = EnvCutFreqMin;
value_t hiCutFrequency = EnvCutFreqMax;
int recoveryCountdown = 0; // how many samples remain before trying to filter again (CPU usage limiter)
const int smallestWavelength = 16;
value_t thresh = 0; // amplitude to reach before considering pitch to be significant
Expand Down Expand Up @@ -148,9 +131,8 @@ namespace Sapphire

outEnvelope = q.updateAmplitude(input, currentSampleRate);

// Feed through a bandpass filter that rejects DC and other frequencies below 20 Hz,
// and also rejects very high frequencies.
value_t signal = q.bandpass(input, loCutFrequency, hiCutFrequency, currentSampleRate);
FilterResult<float> result = q.pitchFilter.process(currentSampleRate, input);
value_t signal = result.bandpass;

// Make sure we have a normal numeric value for our signal.
if (!std::isfinite(signal))
Expand Down Expand Up @@ -225,14 +207,16 @@ namespace Sapphire
speed = 0.9999 - (qs*0.0999 / 128);
}

void setLoCut(value_t loCutHz)
void setFrequency(value_t knob = 0)
{
loCutFrequency = std::clamp(loCutHz, static_cast<value_t>(EnvCutFreqMin), static_cast<value_t>(EnvCutFreqMax));
for (info_t& q : info)
q.pitchFilter.setFrequency(knob);
}

void setHiCut(value_t hiCutHz)
void setResonance(value_t knob = 0)
{
hiCutFrequency = std::clamp(hiCutHz, static_cast<value_t>(EnvCutFreqMin), static_cast<value_t>(EnvCutFreqMax));
for (info_t& q : info)
q.pitchFilter.setResonance(knob);
}

int process(
Expand Down
28 changes: 14 additions & 14 deletions src/env_vcv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ namespace Sapphire
THRESHOLD_ATTEN,
SPEED_PARAM,
SPEED_ATTEN,
LOCUT_PARAM,
LOCUT_ATTEN,
HICUT_PARAM,
HICUT_ATTEN,
FREQ_PARAM,
FREQ_ATTEN,
RES_PARAM,
RES_ATTEN,
PARAMS_LEN
};

Expand All @@ -28,8 +28,8 @@ namespace Sapphire
AUDIO_INPUT,
THRESHOLD_CV_INPUT,
SPEED_CV_INPUT,
LOCUT_CV_INPUT,
HICUT_CV_INPUT,
FREQ_CV_INPUT,
RES_CV_INPUT,
INPUTS_LEN
};

Expand Down Expand Up @@ -57,8 +57,8 @@ namespace Sapphire
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configControlGroup("Threshold", THRESHOLD_PARAM, THRESHOLD_ATTEN, THRESHOLD_CV_INPUT, -96, 0, -24, " dB");
configControlGroup("Speed", SPEED_PARAM, SPEED_ATTEN, SPEED_CV_INPUT, 0, 1, 0.5);
configControlGroup("Lo Cut", LOCUT_PARAM, LOCUT_ATTEN, LOCUT_CV_INPUT, EnvCutFreqMin, EnvCutFreqMax, EnvCutFreqMin);
configControlGroup("Hi Cut", HICUT_PARAM, HICUT_ATTEN, HICUT_CV_INPUT, EnvCutFreqMin, EnvCutFreqMax, EnvCutFreqMax);
configControlGroup("Frequency", FREQ_PARAM, FREQ_ATTEN, FREQ_CV_INPUT, -Gravy::OctaveRange, +Gravy::OctaveRange, Gravy::DefaultFrequencyKnob);
configControlGroup("Resonance", RES_PARAM, RES_ATTEN, RES_CV_INPUT, 0, 1, Gravy::DefaultResonanceKnob);
configInput(AUDIO_INPUT, "Audio");
configOutput(ENVELOPE_OUTPUT, "Envelope");
configOutput(PITCH_OUTPUT, "Pitch V/OCT");
Expand Down Expand Up @@ -100,11 +100,11 @@ namespace Sapphire
float speed = getControlValue(SPEED_PARAM, SPEED_ATTEN, SPEED_CV_INPUT, 0, 1);
detector.setSpeed(speed);

float locut = getControlValue(LOCUT_PARAM, LOCUT_ATTEN, LOCUT_CV_INPUT, EnvCutFreqMin, EnvCutFreqMax);
detector.setLoCut(locut);
float freq = getControlValueVoltPerOctave(FREQ_PARAM, FREQ_ATTEN, FREQ_CV_INPUT, -Gravy::OctaveRange, +Gravy::OctaveRange);
detector.setFrequency(freq);

float hicut = getControlValue(HICUT_PARAM, HICUT_ATTEN, HICUT_CV_INPUT, EnvCutFreqMin, EnvCutFreqMax);
detector.setHiCut(hicut);
float res = getControlValue(RES_PARAM, RES_ATTEN, RES_CV_INPUT, 0, 1);
detector.setResonance(res);

detector.process(nc, args.sampleRate, inFrame, outEnvelope, outPitchVoct);

Expand Down Expand Up @@ -135,8 +135,8 @@ namespace Sapphire
addSapphireOutput(PITCH_OUTPUT, "pitch_output");
addSapphireFlatControlGroup("thresh", THRESHOLD_PARAM, THRESHOLD_ATTEN, THRESHOLD_CV_INPUT);
addSapphireFlatControlGroup("speed", SPEED_PARAM, SPEED_ATTEN, SPEED_CV_INPUT);
addSapphireFlatControlGroup("locut", LOCUT_PARAM, LOCUT_ATTEN, LOCUT_CV_INPUT);
addSapphireFlatControlGroup("hicut", HICUT_PARAM, HICUT_ATTEN, HICUT_CV_INPUT);
addSapphireFlatControlGroup("frequency", FREQ_PARAM, FREQ_ATTEN, FREQ_CV_INPUT);
addSapphireFlatControlGroup("resonance", RES_PARAM, RES_ATTEN, RES_CV_INPUT);
}
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/gravy_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Sapphire
namespace Gravy
{
const int OctaveRange = 5; // +/- octave range around default frequency
const float DefaultFrequencyHz = 523.2511306011972; // C5 = 440*(2**0.25)
const double DefaultFrequencyHz = 523.2511306011972; // C5 = 440*(2**0.25)
const int FrequencyFactor = 1 << OctaveRange;

const float DefaultFrequencyKnob = 0.0;
Expand Down
12 changes: 6 additions & 6 deletions src/sapphire_panel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ namespace Sapphire
{"_panel", { 30.480, 128.500}},
{"audio_input", { 15.240, 83.667}},
{"envelope_output", { 15.240, 98.833}},
{"hicut_atten", { 15.240, 68.500}},
{"hicut_cv", { 6.240, 68.500}},
{"hicut_knob", { 24.240, 68.500}},
{"locut_atten", { 15.240, 53.333}},
{"locut_cv", { 6.240, 53.333}},
{"locut_knob", { 24.240, 53.333}},
{"frequency_atten", { 15.240, 53.333}},
{"frequency_cv", { 6.240, 53.333}},
{"frequency_knob", { 24.240, 53.333}},
{"pitch_output", { 15.240, 114.000}},
{"resonance_atten", { 15.240, 68.500}},
{"resonance_cv", { 6.240, 68.500}},
{"resonance_knob", { 24.240, 68.500}},
{"speed_atten", { 15.240, 38.167}},
{"speed_cv", { 6.240, 38.167}},
{"speed_knob", { 24.240, 38.167}},
Expand Down
31 changes: 16 additions & 15 deletions src/sauce_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ namespace Sapphire
{
namespace Gravy
{
template <typename value_t>
class SingleChannelGravyEngine
{
private:
float freqKnob = DefaultFrequencyKnob;
float resKnob = DefaultResonanceKnob;
float mixKnob = DefaultMixKnob;
float gainKnob = DefaultGainKnob;
value_t freqKnob = DefaultFrequencyKnob;
value_t resKnob = DefaultResonanceKnob;
value_t mixKnob = DefaultMixKnob;
value_t gainKnob = DefaultGainKnob;

StateVariableFilter<float> filter;
StateVariableFilter<value_t> filter;

float setKnob(float &v, float k, int lo = 0, int hi = 1)
float setKnob(value_t &v, value_t k, int lo = 0, int hi = 1)
{
if (std::isfinite(k))
{
Expand All @@ -35,33 +36,33 @@ namespace Sapphire
filter.initialize();
}

float setFrequency(float k)
value_t setFrequency(value_t k)
{
return setKnob(freqKnob, k, -OctaveRange, +OctaveRange);
}

float setResonance(float k)
value_t setResonance(value_t k)
{
return setKnob(resKnob, k);
}

float setMix(float k)
value_t setMix(value_t k)
{
return setKnob(mixKnob, k);
}

float setGain(float k)
value_t setGain(value_t k)
{
return setKnob(gainKnob, k);
}

FilterResult<float> process(float sampleRateHz, const float inSample)
FilterResult<value_t> process(value_t sampleRateHz, const value_t inSample)
{
float cornerFreqHz = std::pow(2.0f, freqKnob) * DefaultFrequencyHz;
float gain = Cube(gainKnob * 2); // 0.5, the default value, should have unity gain.
float mix = 1-Cube(1-mixKnob);
value_t cornerFreqHz = std::pow(static_cast<value_t>(2), freqKnob) * DefaultFrequencyHz;
value_t gain = Cube(gainKnob * 2); // 0.5, the default value, should have unity gain.
value_t mix = 1-Cube(1-mixKnob);

FilterResult<float> result = filter.process(sampleRateHz, cornerFreqHz, resKnob, inSample);
FilterResult<value_t> result = filter.process(sampleRateHz, cornerFreqHz, resKnob, inSample);
result.lowpass = gain * (mix*result.lowpass + (1-mix)*inSample);
result.bandpass = gain * (mix*result.bandpass + (1-mix)*inSample);
result.highpass = gain * (mix*result.highpass + (1-mix)*inSample);
Expand Down
2 changes: 1 addition & 1 deletion src/sauce_vcv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ namespace Sapphire

struct SauceModule : SapphireModule
{
Gravy::SingleChannelGravyEngine engine[PORT_MAX_CHANNELS];
Gravy::SingleChannelGravyEngine<float> engine[PORT_MAX_CHANNELS];
AgcLevelQuantity *agcLevelQuantity{};
AutomaticGainLimiter agcLow;
AutomaticGainLimiter agcBand;
Expand Down
16 changes: 8 additions & 8 deletions util/make_sapphire_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1871,8 +1871,8 @@ def GenerateEnvPitchPanel(cdict:Dict[str, ControlLayer], target:Target) -> int:
yFence = FencePost(23.0, 114.0, 7)
yThresh = yFence.value(0)
ySpeed = yFence.value(1)
yLoCut = yFence.value(2)
yHiCut = yFence.value(3)
yFreq = yFence.value(2)
yRes = yFence.value(3)
yPolyAudioIn = yFence.value(4)
yEnvelopeOut = yFence.value(5)
yPitchOut = yFence.value(6)
Expand All @@ -1883,11 +1883,11 @@ def GenerateEnvPitchPanel(cdict:Dict[str, ControlLayer], target:Target) -> int:
with Font(SAPPHIRE_FONT_FILENAME) as font:
pl.append(BorderRect(PANEL_WIDTH, SAPPHIRE_PANEL_COLOR, SAPPHIRE_BORDER_COLOR))

defs.append(Gradient(yThresh-artSpaceAboveKnob, yHiCut+artSpaceBelowKnob, SAPPHIRE_AZURE_COLOR, SAPPHIRE_PANEL_COLOR, 'gradient_blue'))
defs.append(Gradient(yThresh-artSpaceAboveKnob, yRes+artSpaceBelowKnob, SAPPHIRE_AZURE_COLOR, SAPPHIRE_PANEL_COLOR, 'gradient_blue'))
defs.append(Gradient(yPolyAudioIn-artSpaceAboveKnob, yPolyAudioIn+artSpaceBelowKnob, SAPPHIRE_MAGENTA_COLOR, SAPPHIRE_PANEL_COLOR, 'gradient_purple'))
defs.append(Gradient(yEnvelopeOut-artSpaceAboveKnob, yPitchOut+artSpaceBelowKnob, SAPPHIRE_TEAL_COLOR, SAPPHIRE_PANEL_COLOR, 'gradient_out'))

pl.append(ControlGroupArt(name, 'control_art', panel, yThresh-artSpaceAboveKnob, yHiCut+artSpaceBelowKnob, 'gradient_blue'))
pl.append(ControlGroupArt(name, 'control_art', panel, yThresh-artSpaceAboveKnob, yRes+artSpaceBelowKnob, 'gradient_blue'))
pl.append(ControlGroupArt(name, 'audio_art', panel, yPolyAudioIn-artSpaceAboveKnob, yPolyAudioIn+artSpaceBelowKnob, 'gradient_purple'))
pl.append(ControlGroupArt(name, 'out_art', panel, yEnvelopeOut-artSpaceAboveKnob, yPitchOut+artSpaceBelowKnob, 'gradient_out'))

Expand All @@ -1909,11 +1909,11 @@ def GenerateEnvPitchPanel(cdict:Dict[str, ControlLayer], target:Target) -> int:
AddFlatControlGroup(pl, controls, xmid, ySpeed, 'speed')
pl.append(CenteredControlTextPath(font, 'SPEED', xmid, ySpeed - dyText))

AddFlatControlGroup(pl, controls, xmid, yLoCut, 'locut')
pl.append(CenteredControlTextPath(font, 'LO CUT', xmid, yLoCut - dyText))
AddFlatControlGroup(pl, controls, xmid, yFreq, 'frequency')
pl.append(CenteredControlTextPath(font, 'FREQ', xmid, yFreq - dyText))

AddFlatControlGroup(pl, controls, xmid, yHiCut, 'hicut')
pl.append(CenteredControlTextPath(font, 'HI CUT', xmid, yHiCut - dyText))
AddFlatControlGroup(pl, controls, xmid, yRes, 'resonance')
pl.append(CenteredControlTextPath(font, 'RES', xmid, yRes - dyText))
return Save(panel, svgFileName)


Expand Down

0 comments on commit 0339dd8

Please sign in to comment.