Conditional Parameters (Pod)

Hello all, I have the Daisy Pod, and i’m having some issues attempting to map different parameters to the two knobs per encoder increment.

ie:

let’s say their are two modes, mode0 and mode1,
when you increment the encoder, it will cycle between the two,

mode0:

  • maps oscillator frequency to knob1
  • maps oscillator’s detune to knob2

mode1:

  • maps filter cutoff to knob1
  • maps filter res to knob2

I’ve been able to implement this using the same methodology in the SynthVoice example for the pod in the github repo, however, when i change modes with the encoder, it updates the values on the knobs for the parameters for that given mode to whatever the knobs are currently at from adjusting them in the previous mode.

Does anyone have any suggestions for handling this?

Essentially just need to map multiple parameters to two knob, and be able to not change the values of them to whatever the knob is currently set at.

To simplify this scenario to make sure i’m explaining myself correctly, let’s say we just have one knob.
theres two modes for the knob. one mode the knob adjusts the oscillator’s frequency, the other mode adjusts the filter’s cutoff. let’s say i set the frequency of the oscillator to a pitch that i’m satisfied with. I then increment the encoder to mode two, and adjust the knob to a filter cutoff value i’m satisfied. everything’s good. BUT, when i move the encoder back to mode1, i don’t want the oscillator frequency to immediately update to whatever value the knob’s position is at from adjusting the filter cutoff amount previously, i only want it to change if i move the knob, and then it updates the oscillator frequency.

I’m a mid level software engineer but i’m a little new to the embedded/mcu realm, so perhaps there’s just some general paradigms i’m not grasping. any help is appreciated. thank you!

This has nothing with embedded programming. You just need to implement a solution that handles soft takeover over parameters and there are different ways to achieve something like that. As an example, make a class that support being in locked/unlocked state. Then you can have separate instances of it controlling different parameters, but ADC values would be propagated only if current parameter controller is in unlocked mode.

1 Like

gotcha. that makes sense. i’ve done a lot of concurrency in Go before, my C/C++ is a little rusty. i’ll have to look into implementing mutexes in C++.

Sounds like Go runtime causes more confusion than necessary for you. There’s no concurrency involved if your code is only running in audio callback. So you don’t need mutexes, atomic access or anything fancy. Just a stupid boolean flag to check parameter state is perfectly adequate.

I’m still having a hard time implementing this. I tried starting with a clean slate, so I grabbed the example code from DaisyExamples/pod/SynthVoice. It uses the mode variable to determine what params are set to the knobs, but even this example code still suffers from the original problem of keeping the value of the knob untouched when traversing through modes. ie, if i go to mode 2, change a value on a knob, and then go back to mode 1, whatever the changes i made on the knobs in mode 2 translate to mode 1 because of the placement of the knobs changed in mode 2.

#include "daisysp.h"
#include "daisy_pod.h"

using namespace daisysp;
using namespace daisy;

static DaisyPod   pod;
static Oscillator osc, lfo;
static MoogLadder flt;
static AdEnv      ad;
static Parameter  pitchParam, cutoffParam, lfoParam;

int   wave, mode;
float vibrato, oscFreq, lfoFreq, lfoAmp, attack, release, cutoff;
float oldk1, oldk2, k1, k2;
bool  selfCycle;

void ConditionalParameter(float  oldVal,
                          float  newVal,
                          float &param,
                          float  update);

void Controls();

void NextSamples(float &sig)
{
    float ad_out = ad.Process();
    vibrato      = lfo.Process();

    osc.SetFreq(oscFreq + vibrato);

    sig = osc.Process();
    sig = flt.Process(sig);
    sig *= ad_out;
}

static void AudioCallback(AudioHandle::InterleavingInputBuffer  in,
                          AudioHandle::InterleavingOutputBuffer out,
                          size_t                                size)
{
    Controls();

    for(size_t i = 0; i < size; i += 2)
    {
        float sig;
        NextSamples(sig);

        // left out
        out[i] = sig;

        // right out
        out[i + 1] = sig;
    }
}

int main(void)
{
    // Set global variables
    float sample_rate;
    mode    = 0;
    vibrato = 0.0f;
    oscFreq = 1000.0f;
    oldk1 = oldk2 = 0;
    k1 = k2   = 0;
    attack    = .01f;
    release   = .2f;
    cutoff    = 10000;
    lfoAmp    = 1.0f;
    lfoFreq   = 0.1f;
    selfCycle = false;

    //Init everything
    pod.Init();

    sample_rate = pod.AudioSampleRate();
    osc.Init(sample_rate);
    flt.Init(sample_rate);
    ad.Init(sample_rate);
    lfo.Init(sample_rate);

    //Set filter parameters
    flt.SetFreq(10000);
    flt.SetRes(0.8);

    // Set parameters for oscillator
    osc.SetWaveform(osc.WAVE_SAW);
    wave = osc.WAVE_SAW;
    osc.SetFreq(440);
    osc.SetAmp(1);

    // Set parameters for lfo
    lfo.SetWaveform(osc.WAVE_SIN);
    lfo.SetFreq(0.1);
    lfo.SetAmp(1);

    //Set envelope parameters
    ad.SetTime(ADENV_SEG_ATTACK, 0.01);
    ad.SetTime(ADENV_SEG_DECAY, .2);
    ad.SetMax(1);
    ad.SetMin(0);
    ad.SetCurve(0.5);

    //set parameter parameters
    cutoffParam.Init(pod.knob1, 100, 20000, cutoffParam.LOGARITHMIC);
    pitchParam.Init(pod.knob2, 50, 5000, pitchParam.LOGARITHMIC);
    lfoParam.Init(pod.knob1, 0.25, 1000, lfoParam.LOGARITHMIC);

    // start callback
    pod.StartAdc();
    pod.StartAudio(AudioCallback);

    while(1) {}
}

//Updates values if knob had changed
void ConditionalParameter(float  oldVal,
                          float  newVal,
                          float &param,
                          float  update)
{
    if(abs(oldVal - newVal) > 0.00005)
    {
        param = update;
    }
}


//Controls Helpers
void UpdateEncoder()
{
    wave += pod.encoder.RisingEdge();
    wave %= osc.WAVE_POLYBLEP_TRI;

    //skip ramp since it sounds like saw
    if(wave == 3)
    {
        wave = 4;
    }

    osc.SetWaveform(wave);

    mode += pod.encoder.Increment();
    mode = (mode % 3 + 3) % 3;
}

void UpdateKnobs()
{
    k1 = pod.knob1.Process();
    k2 = pod.knob2.Process();

    switch(mode)
    {
        case 0:
            ConditionalParameter(oldk1, k1, cutoff, cutoffParam.Process());
            ConditionalParameter(oldk2, k2, oscFreq, pitchParam.Process());
            flt.SetFreq(cutoff);
            break;
        case 1:
            ConditionalParameter(oldk1, k1, attack, pod.knob1.Process());
            ConditionalParameter(oldk2, k2, release, pod.knob2.Process());
            ad.SetTime(ADENV_SEG_ATTACK, attack);
            ad.SetTime(ADENV_SEG_DECAY, release);
            break;
        case 2:
            ConditionalParameter(oldk1, k1, lfoFreq, lfoParam.Process());
            ConditionalParameter(oldk2, k2, lfoAmp, pod.knob2.Process());
            lfo.SetFreq(lfoFreq);
            lfo.SetAmp(lfoAmp * 100);
        default: break;
    }
}

void UpdateLeds()
{
    pod.led1.Set(mode == 2, mode == 1, mode == 0);
    pod.led2.Set(0, selfCycle, selfCycle);

    oldk1 = k1;
    oldk2 = k2;

    pod.UpdateLeds();
}

void UpdateButtons()
{
    if(pod.button1.RisingEdge() || (selfCycle && !ad.IsRunning()))
    {
        ad.Trigger();
    }

    if(pod.button2.RisingEdge())
    {
        selfCycle = !selfCycle;
    }
}

void Controls()
{
    pod.ProcessAnalogControls();
    pod.ProcessDigitalControls();

    UpdateEncoder();

    UpdateKnobs();

    UpdateLeds();

    UpdateButtons();
}

The switch being here:

void UpdateKnobs()
{
    k1 = pod.knob1.Process();
    k2 = pod.knob2.Process();

    switch(mode)
    {
        case 0:
            ConditionalParameter(oldk1, k1, cutoff, cutoffParam.Process());
            ConditionalParameter(oldk2, k2, oscFreq, pitchParam.Process());
            flt.SetFreq(cutoff);
            break;
        case 1:
            ConditionalParameter(oldk1, k1, attack, pod.knob1.Process());
            ConditionalParameter(oldk2, k2, release, pod.knob2.Process());
            ad.SetTime(ADENV_SEG_ATTACK, attack);
            ad.SetTime(ADENV_SEG_DECAY, release);
            break;
        case 2:
            ConditionalParameter(oldk1, k1, lfoFreq, lfoParam.Process());
            ConditionalParameter(oldk2, k2, lfoAmp, pod.knob2.Process());
            lfo.SetFreq(lfoFreq);
            lfo.SetAmp(lfoAmp * 100);
        default: break;
    }
}

using this function here:

//Updates values if knob had changed
void ConditionalParameter(float  oldVal,
                          float  newVal,
                          float &param,
                          float  update)
{
    if(abs(oldVal - newVal) > 0.00005)
    {
        param = update;
    }
}

And I’m just confused. Feel like the original author’s intent with the ConditionalParameter function was to preserve the previous state of the knobs when traversing modes so that your “patch” doesn’t change every time you switch a mode. But when flashing this code onto the daisy pod, it still suffers from the the parameters changing every time a mode is changed.

If you can point me in the right direction, even with just some general pseudocode, it would be immensely appreciated. I’ve looked all over the internet attempting to find a resolution with no luck. And honestly it’s a very good implementation to know just because no matter how limited you are with your inputs, as long as you have one button, and one knob, theoretically you can treat it as a parameter input for as many parameters as you’d like. IE Pamela’s New Workout, contains an entire plethora of menus you can traverse literally just using one rotary encoder.

The code does what it suppose to - change one a specific parameter based on selected mode.

In order to have a working soft takeover, you must keep old parameter values and have a flag that stores lock state. The general idea:

  • unlocked objects store and output new values
  • locked objects always output previously stored values
  • current parameter becomes locked if you switched to a new mode
  • current parameter becomes unlocked only if you’re in correct mode and new value is within unlocking threshold from the old value

I did it :smiling_face_with_tear: Immensely appreciate you taking the time to guide me in the right direction!

Awesome! Would be nice to share some code in case if someone else finds it useful.

I’ve been reading this thread with interest because I’d been having similar problems - parameters jumping when changing function “pages” with the encoder. There was some great info here, and this is how I used it:

#define CATCH_THRESH      0.05f

static Parameter mix;
float cur_mix;


float CatchParam(float old, float cur, float thresh)
{
  return (abs(old - cur) < thresh) ? cur : old;
}

...

void UpdateEncoder()
{

  cur_page = (page)(cur_page + hw.encoder.Increment());
  if (cur_page >= LAST) { cur_page = MIX; }
  switch(cur_page)
  {
    case MIX:
      cur_mix = CatchParam(cur_mix, mix.Process(), CATCH_THRESH);
...
}

Then cur_mix gets used in the Audio Callback. Maybe not the most elegant way, but it does the trick for me.

Cheers

Hi would you be willing to share your solution? I’m having some trouble with this myself currently. Thanks!

What was your solution? Currently struggling with this

void LockParams(float cur_knobVal, float &cur_paramVal, float &old_paramVal, float thresh, int lock)
{
if ((lock) == (mode)){
if(abs(old_paramVal - cur_knobVal) < thresh){
old_paramVal = cur_paramVal;
cur_paramVal = cur_knobVal;
}
}
if ((lock) != (mode)){
if(old_paramVal == 0){cur_paramVal = cur_knobVal;}
old_paramVal = cur_paramVal;
}
}

heres a function I wrote to handle soft takeover of the knobs while using the pod.
it’s could probably use some tidying up! but it works. I opted to put a lock and pass the mode through into the function so I can call it in my Controls() function which is called every audio callback.
the if old_paramVal == 0 line is just there for startup reasons… thought I’d contribute to this thread as I found it on my path to figuring this out!

calling the function looks like this…
LockParams(deltime.Process(), delayTarget, old_delayTarget, 500, DELAY);
LockParams(k2, feedback, old_feedback, 0.007f, DELAY);

playing with the threshold helps to achieve best results!