Audio clicks when changing mixer gain too fast

sonaben

Active member
I want to control a mixer's gain with a joystick, but I get a lot of audio clicking even though I'm changing the gain smoothly (increase/decrease of 0.001).

I guess this is because this isn't meant to be called too fast, but is there a way to avoid the clicks and have a smooth update?
Unfortunately I cannot use faders as I know these work well.

I tried adjusting the change rate (0.1, 0.05 etc.) and the frequency of the update (500 us, 1ms, 10ms etc.) but can't get anything decent.

See simple code below that reproduces this issue with a sine wave.

Thanks for your help :)

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveformSine   sine1;          //xy=244.1999969482422,133.1999969482422
AudioMixer4              mixer1;         //xy=429.1999969482422,134.1999969482422
AudioOutputTDM           tdm1;           //xy=610.2000122070312,208.1999969482422
AudioConnection          patchCord1(sine1, 0, mixer1, 0);
AudioConnection          patchCord2(mixer1, 0, tdm1, 0);
AudioControlCS42448      cs42448_1;      //xy=273.20001220703125,249.20001220703125
// GUItool: end automatically generated code

unsigned long prevMillis = 0;
float gVal = 0.;
bool goUp = true;

void setup() {
  AudioMemory(64);
  cs42448_1.enable();
  cs42448_1.volume(1);
  sine1.frequency(440);
}

void loop() {
  if ((millis() - prevMillis) > 2) {
    if (goUp) {
        gVal += 0.01;
    }
    else {
        gVal -= 0.01;
    }
    
    if (gVal > 0.9) {
      goUp = false;
    }
    else if (gVal < 0) {
      goUp = true;
      gVal = 0.;
    }

    mixer1.gain(0, gVal);

    prevMillis = millis();
  }
}
 
Last edited:
Try changing the first line in your loop() function to the following (note RED parentheses) & see if that makes any positive difference:

Code:
  if (millis[COLOR="#FF0000"]()[/COLOR] - prevMillis > 10) {

Without those parentheses, I'd guess that your gain is being set everytime thru the loop, rather than periodically as you intended.

Hope that helps . . .

Mark J Culross
KD5RXT
 
Thanks Mark,

This was a mistake from the simple code but the issue remains the same:

I need to update the gain from A to B (e.g 0.1 to 0.9) within a minimum amount of time (ideally < 50ms to be reactive enough).

But combinations of values that allow a quick update e.g
- change rate of 0.01 and frequency of update of 1ms (which means 100ms to change gain from 0 to 1)
- change rate of 0.001 and frequency of update of 100 microseconds (again which means 100ms to change gain from 0 to 1)
produce a lot of clicking.

If I increase the gain by 0.001 every 1ms yes it's fine there's no audio clicking, but then it takes 1 second to proceed from 0 to 1!

I'm not sure I can solve this issue with updating the gains like this...
 
What if you made larger changes initially, then smaller changes as you get closer to the target. In other words (this could be really rough, so you might be back to experiencing some clicks), take the difference between the current value and the target value, divide that by 2 (or divide by 3, or divide by 4, etc), & add/subtract that resulting value.

For example (using a divisor of 3), if the current value is 0.2 & the target value is 0.8, then the difference would be 0.6. Take the 0.6, divide it by 3 & the change would be 0.2. Therefore, the new value would be (current + calculated change) 0.2 + 0.2, or 0.4.

For the next step change, the current value is 0.4 & the target value is 0.8, then the difference would be 0.4. Take the 0.4, divide it by 3 & the change would be 0.1333. Therefore, the new value would be 0.4 + 0.1333, or 0.5333.

For the next step change, the current value is 0.5333 & the target value is 0.8, then the difference would be 0.2667. Take the 0.2667, divide it by 3 & the change would be 0.0889. Therefore, the new value would be 0.5333 + 0.0889, or 0.6222.

For the next step change, the current value is 0.6222 & the target value is 0.8, then the difference would be 0.1778. Take the 0.1778, divide it by 3 & the change would be 0.0593. Therefore, the new value would be 0.6222 + 0.0593, or 0.6815.

For the next step change, the current value is 0.6815 & the target value is 0.8, then the difference would be 0.1185. Take the 0.1185, divide it by 3 & the change would be 0.0395. Therefore, the new value would be 0.6815 + 0.0395, or 0.7210.

For the next step change, the current value is 0.7210 & the target value is 0.8, then the difference would be 0.079. Take the 0.079, divide it by 3 & the change would be 0.0263. Therefore, the new value would be 0.7210 + 0.0263, or 0.7473.

For the next step change, the current value is 0.7473 & the target value is 0.8, then the difference would be 0.0527. Take the 0.0527, divide it by 3 & the change would be 0.0176. Therefore, the new value would be 0.7473 + 0.0176, or 0.7649.

You can see how, in only 7 steps, you are already 95% of the way towards your desired target. You can experiment with different divisors. The larger the divisor, the smaller the step changes, so the slower you'll approach the target, & the less likelihood of experiencing clicks. The smaller the divisor, the larger the steps, so the quicker you'll approach the target, but the likelihood of experiencing clicks increases.

Hope that helps . . .

Mark J Culross
KD5RXT
 
Thanks again Mark for an interesting answer!

I implemented it and it helps a little bit (I found that 5ms with a dividor of 10 does a decent job) but there's still a considerable amount of noticeable clicking unfortunately.

I don't think there's a magic answer with this problem, what I'd like to understand if someone knows is how the faders work, as they don't seem to produce any clicking even for quick transitions.
I can see they use a fader_table but I don't really understand how the code updates the volume.

I was wondering if something similar could be done in this case.

Cheers
 
I think fader is your way to go. fader increments/decrements on each pass through the sample loop, all oyu have to do is give ti a new target value and a time to do it in.

Code:
    if (midiChange(DELAY1_TIME)){
      if (_delayfade1 % 2 == 0){
        AudioNoInterrupts();
        delay1.delay(1,(float)midivals[DELAY1_TIME] * 6);
        dlyfade0.fadeOut(2);
        dlyfade1.fadeIn(2);
        AudioInterrupts();
      } else {
        AudioNoInterrupts();
        delay1.delay(0,(float)midivals[DELAY1_TIME] * 6);
        dlyfade1.fadeOut(2);
        dlyfade0.fadeIn(2);
        AudioInterrupts();
      }
      _delayfade1 = _delayfade1 + 1;
      sendmessage("Delay1 mS:",(float)midivals[DELAY1_TIME] * 6);
    }

What you do here is have two copies of your signal - in this case they're "delayfade0" and "delayfade1" each has an input of your signal, one at the current output amplitude and one at your new amplitude. each time you change the setting, you alternately set the new amplitude on the other channel and allow delayfade to ramp into it. In the above example this is 2ms and it works great on my delay line wihtout hearing clicks when you change delay time settings.
 
Last edited:
The mixer gain only takes effect every block, so you get audible artifacts at about 345Hz (for standard settings).

You can mitigate this by using smaller block size, or eliminate by using a technique that updates every sample such as fader, or feeding a slow ramp waveform into a multiply (which is what fader does internally, basically).
 
Right I understand why the clicks happen now, thank you Mark I didn't know about the gains updating only at every block.

@boxxofrobots, I get the principle and I tried and I it's working much better, I still get some clicks but it's more like pulsations because of the rapid gain changes and I guess it's unavoidable.

See below example that I tried emulating a gain change every 100ms.

I wasn't sure why you were using delays as well, but I suppose it was part of your project which is unrelated to this.

Cheers

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>


// GUItool: begin automatically generated code
AudioSynthWaveformSine   sine1;          //xy=204.1999969482422,110.19999694824219
AudioAmplifier           amp1;           //xy=389.20001220703125,80.19999694824219
AudioAmplifier           amp2;           //xy=389.20001220703125,140.1999969482422
AudioEffectFade          fade1;          //xy=531.1999969482422,79.19999694824219
AudioEffectFade          fade2;          //xy=532.1999969482422,141.1999969482422
AudioMixer4              mixer1;         //xy=684.2000122070312,101.19999694824219
AudioOutputTDM           tdm1;           //xy=858.2000122070312,166.1999969482422
AudioConnection          patchCord1(sine1, amp1);
AudioConnection          patchCord2(sine1, amp2);
AudioConnection          patchCord3(amp1, fade1);
AudioConnection          patchCord4(amp2, fade2);
AudioConnection          patchCord5(fade1, 0, mixer1, 0);
AudioConnection          patchCord6(fade2, 0, mixer1, 1);
AudioConnection          patchCord7(mixer1, 0, tdm1, 0);
AudioControlCS42448      cs42448_1;      //xy=293.1999969482422,220.1999969482422
// GUItool: end automatically generated code

long unsigned int prevMillis = 0;
bool line1 = true;
float gains1[5] = {0.9, 0.8, 0.7, 0.6, 0.5};
float gains2[5] = {0.05, 0.35, 0.25, 0.45, 0.15};
int cnt=0;

void setup() {
  AudioMemory(64);
  cs42448_1.enable();
  cs42448_1.volume(1);
  sine1.frequency(440);
}

void loop() {
  if ((millis() - prevMillis) > 100) {
    if (line1)
      cnt++;
    if (cnt == 5)
      cnt = 0;
    
    AudioNoInterrupts();
    if (line1) {
      fade1.fadeOut(2);
      amp2.gain(gains2[cnt]);
      fade2.fadeIn(2);
    }
    else {
      fade2.fadeOut(2);
      amp1.gain(gains1[cnt]);
      fade1.fadeIn(2);
    }
    AudioInterrupts();
    
    line1 = !line1;
    prevMillis = millis();
  }
}

Thank you.
 
Update on this,

I've tried with multiplying 2 sine waves and changing the phase of the amplitude modulation one (sine1: frequency: 0), but still no luck and getting clicks...

Is it the same issue where it processes blocks at a time, not samples??

See code below, thank you I really don't know what to do now!

Capture.PNG

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveformSine   sine1;          //xy=118.19999694824219,171.1999969482422
AudioSynthWaveformSine   sine2;          //xy=120.19999694824219,241.1999969482422
AudioEffectMultiply      multiply1;      //xy=359.1999969482422,175.1999969482422
AudioOutputTDM           tdm1;           //xy=510.20001220703125,271.1999969482422
AudioAnalyzePeak         peak1;          //xy=511.20001220703125,81.19999694824219
AudioConnection          patchCord1(sine1, peak1);
AudioConnection          patchCord2(sine1, 0, multiply1, 0);
AudioConnection          patchCord3(sine2, 0, multiply1, 1);
AudioConnection          patchCord4(multiply1, 0, tdm1, 0);
AudioControlCS42448      cs42448_1;      //xy=96.19999694824219,80.19999694824219
// GUItool: end automatically generated code


long unsigned int prevMillis = 0;
bool line1 = true;
int cnt=0;
bool up = true;

void setup() {
  AudioMemory(8);
  cs42448_1.enable();
  cs42448_1.volume(1);
  sine1.frequency(0);
  sine2.frequency(432);
  sine2.amplitude(1);
  //Serial.begin(9600);
}

void loop() {
  if ((millis() - prevMillis) > 10) {
    //if (peak1.available())
    //  Serial.println(peak1.read(), 4);
    prevMillis = millis();
    sine1.phase(cnt);
    if (up)
      cnt++;
    else
      cnt--;
    if (up && cnt == 90) {
      up = false;
    } else if (!up && cnt == 0)
      up = true;
  }
}
 
You need to use the fader blocks or you'll get clicks when the volume is changed and the instantaneous amplitudes of the two waves don't line up. The example code you gave earlier should work like this. An osc with a freq of 0 is a dc signal, so you're back to instanteoulsy changing dc signals on block boundaries.
Screenshot from 2023-07-08 11-05-21.png
 

Attachments

  • Screenshot from 2023-06-06 13-24-07.png
    Screenshot from 2023-06-06 13-24-07.png
    28.8 KB · Views: 69
Anti click should be implemented in the mixer itself.

The goal is to avoid discontinuities.

- Slow/Smooth update of the gain.

- allow instantaneous update of the gain on signal zero crossings.

I think that's the best way to have fast and click free gain modulations.
 
the mixer can be used for all sorts of things where you need instant response. Using the faders is very easy, why not use that?
 
Here's something I use very effectively in knobby to prevent sudden control changes from clicking. Program the biquad to be a lowpass with about a 10hz response, and run your joystick value into the dc input.

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveformDc     dc1;            //xy=305,526
AudioSynthNoiseWhite     noise1;         //xy=340,364
AudioFilterBiquad        biquad1;        //xy=474,526
AudioEffectMultiply      multiply1;      //xy=602,396
AudioOutputI2S           i2s1;           //xy=799,401

AudioConnection          patchCord1(dc1, biquad1);
AudioConnection          patchCord2(noise1, 0, multiply1, 0);
AudioConnection          patchCord3(biquad1, 0, multiply1, 1);
AudioConnection          patchCord4(multiply1, 0, i2s1, 0);

// GUItool: end automatically generated code
 
Back
Top