Mixer and Amp gain really noisy

ghostintranslation

Well-known member
Hi,

I'm using Teensy 4.0 with the audio adapter.

I was working on a synth when I started to notice that varying the gain value of an amp object with a potentiometer was generating a side high pitch frequency. I also tried with a Mixer and changing the gain value is doing the same.

I first thought maybe that's ground noise, but I highly doubt it because I tried on 3 laptops, and on a usb power outlet too, and also because the noise only happen when varying the amp and otherwise there is no noise.

I made a simple reproductible scenario:

Code:
#include <Audio.h>

AudioOutputI2S i2s2;
AudioSynthWaveformModulated sineFM;         //xy=243,108
AudioAmplifier amp;           //xy=396,116
AudioConnection patchCord2(sineFM, 0, amp, 0);
AudioConnection patchCord3(amp, 0, i2s2, 0);
AudioConnection patchCord4(amp, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

unsigned int i;

void setup() {
  // put your setup code here, to run once:
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.6);

  sineFM.begin(WAVEFORM_SINE);
  sineFM.frequency(1); // Try values from 1 to 50
  sineFM.amplitude(1);
  
}

void loop() {
   // This won't generate any noise
//  amp.gain(1);

  // But constantly changing gain generates a high pitch frequency
  // Additionaly there is a strong click when the gain jumps from 1 to 0
  amp.gain((float)(millis()%1000)/1000);

  // This generates clicks a lot as the gain jumps from values to value
//  if(millis()%100 == 0){
//   i++; 
//  }
//  amp.gain((float)cos(i));

}

In the loop the 1st commented case is a stable gain that does no noise.

The 2nd uncommented case is the gain ramping from 0 to 1 in 1 second. it generates this high frequency that can be especially heard if the input sine is low but can still be heard with higher frequencies. Also it does a strong click when the gain jumps from 1 to 0 every second.

The 3rd commented case is with the gain jumping from values to values, that's to show the click mostly.

I'm more interested in getting rid of that high pitch noise than the click noise.

I have tried with Teensyduino 1.53 and 1.54#7 and get the same result.

In my more realistic use in my synth I am varying the gain of 4 amp objects for 4 sine waves and that generates a lot of noise when combined so it's really obvious.

Is this a known issue, is there a solution or workaround?
 
Last edited:
Wild guess here; the amplitude changes happen at a rate that matches the high pitch you hear... In which case the explanation would be that the amplitude change is done as a simple sudden step change. The output "wave" will get a sudden step in its waveform on each amplitude change, and it is thus practically a high frequency sawtooth wave (or similar) riding on top of that amplitude varying sine wave. That would also explain the "click" with gain jump from 1 to 0; if it happens at a random moment of the sinewave, when it doesn't happen at signal's zero-crossing, there will be sudden straight drop to 0-signal.

I'm more interested in getting rid of that high pitch noise than the click noise.

Once you attach your audio output in to decent PA + loudspeaker combination, you will be very much more interested in getting rid of that click (which will become more like loud pop, and can in some cases cause damage to loudspeakers).

I don't know enough of these digital domain audio tools/libraries to "fix" it. In analog world the volume potentiometer would of course cause smooth "infinite update rate" changes to the output amplitude, so no steps and no high pitch noise, and no way to turn it to zero so quickly to cause clicks/pops. In digital world, one could make the amplitude adjustments at very high rate (like 20kHz+), which both makes the steps much much smaller, and also makes their frequency high enough to not be heard (at least by humans). And add amplitude change smoothing/filtering/slowdown to prevent sudden changes that could cause clicks/pops. (There are smarter ways, though..) But I have no clue how to implement such with those libraries. Hopefully someone else can step in for that.

(And I could have guessed wrong, of course.)
 
I just tried something and that changes a little bit what I thought. I removed the amp and I'm varying the amplitude of the sine instead. That generates the same noise:

Code:
#include <Audio.h>

AudioOutputI2S i2s2;
AudioSynthWaveformModulated sineFM;         //xy=243,108
AudioConnection patchCord3(sineFM, 0, i2s2, 0);
AudioConnection patchCord4(sineFM, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

unsigned int i;

void setup() {
  // put your setup code here, to run once:
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.6);

  sineFM.begin(WAVEFORM_SINE);
  sineFM.frequency(1); // Try values from 1 to 50
  sineFM.amplitude(1);
  

}

void loop() {
sineFM.amplitude((float)(millis()%1000)/1000);
}


Also if you try higher frequencies on the sine wave like 100, this noise interacts with the sine like it is modulating it.
 
Wild guess here; the amplitude changes happen at a rate that matches the high pitch you hear... In which case the explanation would be that the amplitude change is done as a simple sudden step change. The output "wave" will get a sudden step in its waveform on each amplitude change, and it is thus practically a high frequency sawtooth wave (or similar) riding on top of that amplitude varying sine wave. That would also explain the "click" with gain jump from 1 to 0; if it happens at a random moment of the sinewave, when it doesn't happen at signal's zero-crossing, there will be sudden straight drop to 0-signal.

You mean the extreme click would also be happening at much lower intensity when going from 0.01 to 0.02, but doing it fast enough would then generate this frequency?
That could be...


Once you attach your audio output in to decent PA + loudspeaker combination, you will be very much more interested in getting rid of that click (which will become more like loud pop, and can in some cases cause damage to loudspeakers).

I agree, but the click in this example is not something I stumble on in my synth because I'm using a potentiometer to vary the value and I smooth the curve so it cannot jump like that.
 
I agree, but the click in this example is not something I stumble on in my synth because I'm using a potentiometer to vary the value and I smooth the curve so it cannot jump like that.
Until one evening, tired enough, simply decide "enough of this noise" and stop for the night, forget to turn the volume down before pushing that virtual "off" button while the sounds still continue... *pop*! :p
 
I just tried something and that changes a little bit what I thought. I removed the amp and I'm varying the amplitude of the sine instead. That generates the same noise:

Code:
#include <Audio.h>

AudioOutputI2S i2s2;
AudioSynthWaveformModulated sineFM;         //xy=243,108
AudioConnection patchCord3(sineFM, 0, i2s2, 0);
AudioConnection patchCord4(sineFM, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

unsigned int i;

void setup() {
  // put your setup code here, to run once:
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.6);

  sineFM.begin(WAVEFORM_SINE);
  sineFM.frequency(1); // Try values from 1 to 50
  sineFM.amplitude(1);
  

}

void loop() {
sineFM.amplitude((float)(millis()%1000)/1000);
}


Also if you try higher frequencies on the sine wave like 100, this noise interacts with the sine like it is modulating it.

Hi, your code is updating the object every cycle of the loop - on a teensy 4 this will be very often, if you update it less often does the problem go away? cheers Paul
 
Hi, your code is updating the object every cycle of the loop - on a teensy 4 this will be very often, if you update it less often does the problem go away? cheers Paul

So it depends on the step increment and the frequency of increment.

For exemple with an increment of 0.0001 I need to increment a step every 1ms to stop hearing it, which means it takes 10s to go from 0 to 1, which is insane in the case of turning a potentiometer from min to max because in that case it should take at most 1s after smoothing to be usable. If I change it to increment every 100us then it takes 1s to go from 0 to 1 but then the noise is really hearable:

Code:
#include <Audio.h>

AudioOutputI2S i2s2;
AudioSynthWaveformSine sine;         //xy=243,108
AudioConnection patchCord3(sine, 0, i2s2, 0);
AudioConnection patchCord4(sine, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

elapsedMicros counter;
float i=0;

void setup() {
  while (!Serial && millis() < 5000); // wait for serial monitor
  Serial.begin(115200);
  
  // put your setup code here, to run once:
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.4);

  sine.frequency(1); // Try values from 1 to 50
  sine.amplitude(1);
}


void loop() {
  
  if(counter >= 1000){ // Change that to 100 so that it takes 1s to go from 0 to 1
    i+=0.0001;
    counter = 0;
    sine.amplitude(i);
    Serial.println(i);
  }
  if(i>=1){
    i = 0;
  }
}
 
Both your examples attempt to update every millisecond, but the Audio Engine only runs every 2.9ms, which will add to the roughness. Then the gain will be fixed for the entire 128 samples processed during that 2.9ms. What’s needed here is a new “gain slew” object which does a gradual change from old to new gain values over the course of the 128 samples. I don’t think it’s possible to mock that up from existing objects, unfortunately :(

Cheers

Jonathan
 
Both your examples attempt to update every millisecond, but the Audio Engine only runs every 2.9ms, which will add to the roughness. Then the gain will be fixed for the entire 128 samples processed during that 2.9ms. What’s needed here is a new “gain slew” object which does a gradual change from old to new gain values over the course of the 128 samples. I don’t think it’s possible to mock that up from existing objects, unfortunately :(

Cheers

Jonathan

I see,

An object for a gain slew could be a solution, that sounds a little out of my capabilities though but I can have a look.

and there is no way to boost the Audio processing so it updates more often than every 2.9ms?
 
Hi,

You can change AUDIO_BLOCK_SAMPLES (which sets the block size (default is 128 samples, it is set in AudioStream.h).

I generally use 16 which updates every 0.3ms.

I still think you are trying to update it too often if it is gain control on a pot. How often do you want to update the gain?

Cheers, Paul
 
Hi,

You can change AUDIO_BLOCK_SAMPLES (which sets the block size (default is 128 samples, it is set in AudioStream.h).

I generally use 16 which updates every 0.3ms.

Ah I will definitely try that!

I still think you are trying to update it too often if it is gain control on a pot. How often do you want to update the gain?

Cheers, Paul

Well I would like to be able to do a change from 0 to 1 of the gain in no more than 1 second and smooth enough that it doesn't feel steppy.

In my example above, I get noise when I update every 100us with steps of 0.0001, most likely because of the 2.9ms for the Audio to process from.
So I guess if I change AUDIO_BLOCK_SAMPLES like you and I'm then able to update every 300us, I should be able to have steps of 0.0003 without noise this time and should be smooth enough.

I will give that a try today, thanks.
 
In my example above, I get noise when I update every 100us with steps of 0.0001, most likely because of the 2.9ms for the Audio to process from.
So I guess if I change AUDIO_BLOCK_SAMPLES like you and I'm then able to update every 300us, I should be able to have steps of 0.0003 without noise this time and should be smooth enough.

I will give that a try today, thanks.

I'd recommend trying a bigger step size.

0.0001 is 10,000 steps and well beyond what you will be able to accurately measure on a pot (and I think what you will be able to hear).
Midi CC moves from 0 to max in 127 steps.

Could you try reading the pot every 20ms and updating the gains based on that and see if it sounds ok (i.e. forget the step size)?

regards Paul
 
I'd recommend trying a bigger step size.

0.0001 is 10,000 steps and well beyond what you will be able to accurately measure on a pot (and I think what you will be able to hear).
Midi CC moves from 0 to max in 127 steps.

Could you try reading the pot every 20ms and updating the gains based on that and see if it sounds ok (i.e. forget the step size)?

regards Paul

Yes well beyond what a pot do if it is directly linked to that setting, but I do smoothing on it to avoid noise and hearable steps. I read the pots every so many ms to set a "value target" and then the actual value reaches the target little by little so that we can't hear any steps.

In the case I would turn the pot in half a second from min to max (which is an actual use case), and that pot adjust the gain, without that smoothing the steps are very very noticeable. And with that smoothing of steps of 0.0001 over 1s, steps are not noticeable but there is now noise. But I can try a few values, see what is exactly the min step increase I can use without hearing any step.
 
So first with this code:
Code:
#include <Audio.h>

AudioOutputI2S i2s2;
AudioSynthWaveformSine sine;         //xy=243,108
AudioConnection patchCord3(sine, 0, i2s2, 0);
AudioConnection patchCord4(sine, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

elapsedMicros counter;
float i=0;

void setup() {
  while (!Serial && millis() < 5000); // wait for serial monitor
  Serial.begin(115200);
  
  // put your setup code here, to run once:
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.4);

  sine.frequency(1); // Try values from 1 to 50
  sine.amplitude(1);
}


void loop() {
  
  if(counter >= 1000){ // Change that to 100 so that it takes 1s to go from 0 to 1
    i+=0.0001;
    counter = 0;
    sine.amplitude(i);
    Serial.println(i);
  }
  if(i>=1){
    i = 0;
  }
}

With steps of 0.0001 I don't hear any noise, but with 0.0002 I start hearing the noise.

Then I tried setting AUDIO_BLOCK_SAMPLES to 16. With this I start hearing it with steps of 0.0003, it's much higher pitch and lower volume though, but steps of 0.0005 is really obvious then.

Also I didn't realize but there is no way to redefine AUDIO_BLOCK_SAMPLES from the sketch right? The only way is to change it in the core file?
I first tried to undef it and define it then, but I suppose it's already too late in the compilation and it doesn't work that way then.
So this solution is not one I can use anyway because my code needs to be "all included" as in if someone downloads it it doesn't require anything more than just uploading it to Teensy.

So in the end I don't have much choice but to set a very small step and live with the fact that gain cannot be changed faster without noise. Or else make a new object, could be interesting but I'm not sure I'm capable enough.

It's only with gain that it happens otherwise when varying frequencies I don't hear the same noise.

In my precise case this is for a drone synth so slower gain changes is not so much an issue but I'm concerned for a later project I had in mind.
 
There could be a workaround:
One could change the input waveform "manually" for the next period, basically applying an ultra-smooth gain slew right into the samples. That would require knowing the behavior and timing of those ready systems so that the manual and automatic parts can be made to match well. That kind of defeats the purpose of having those easy building blocks, but then again, if those blocks do not work in proper way...

For simpler waveforms, like that sine, using some math, it might be possible to do a two-step gain change by calculating the corresponding frequency change to the sinewave that would make it go smoothly from the earlier amplitude to the next, and then return the frequency back to the correct one. Again, needs some knowledge of the systems...


Would probably be easier (and less work) to implement a proper version of amplitude/gain control block that does the gradual amplitude change at the output sample rate (can't get smoother than that), since such component would have all the needed information easily available to it.
 
Would probably be easier (and less work) to implement a proper version of amplitude/gain control block that does the gradual amplitude change at the output sample rate (can't get smoother than that), since such component would have all the needed information easily available to it.

Indeed, I did a little something starting from the mixer object, might need some tweaking but it works, no more noise and adjustable slew:

Code:
#ifndef audiosmoothamplifer_h_
#define audiosmoothamplifer_h_

#include "Arduino.h"
#include "AudioStream.h"
#include "utility/dspinst.h"

class AudioSmoothAmplifier : public AudioStream
{
  public:
    AudioSmoothAmplifier(void) : AudioStream(1, inputQueueArray), multiplierTarget(65536) {
    }
    virtual void update(void);
    void gain(float n, float s) {
      if (n > 32767.0f) n = 32767.0f;
      else if (n < -32767.0f) n = -32767.0f;
      multiplierTarget = n * 65536.0f;
      smoothness = map((float)constrain(s, 0, 1), 0, 1, 1, 0.01);
    }
  private:
    int32_t multiplierTarget;
    float multiplier;
    audio_block_t *inputQueueArray[1];
    float smoothness = 1;
    void applyGain(int16_t *data);
};



#if defined(__ARM_ARCH_7EM__)
#define MULTI_UNITYGAIN 65536

inline void AudioSmoothAmplifier::applyGain(int16_t *data)
{
  uint32_t *p = (uint32_t *)data;
  const uint32_t *end = (uint32_t *)(data + AUDIO_BLOCK_SAMPLES);

  do {
    multiplier += (smoothness * (multiplierTarget - multiplier)) / 1000;
    
    uint32_t tmp32 = *p; // read 2 samples from *data
    int32_t val1 = signed_multiply_32x16b(multiplier, tmp32);
    int32_t val2 = signed_multiply_32x16t(multiplier, tmp32);
    val1 = signed_saturate_rshift(val1, 16, 0);
    val2 = signed_saturate_rshift(val2, 16, 0);
    *p++ = pack_16b_16b(val2, val1);
  } while (p < end);
}

#elif defined(KINETISL)
#define MULTI_UNITYGAIN 256

inline void AudioSmoothAmplifier::applyGain(int16_t *data)
{
  const int16_t *end = data + AUDIO_BLOCK_SAMPLES;

  do {
    multiplier += (smoothness * (multiplierTarget - multiplier)) / 1000;
    
    int32_t val = *data * multiplier;
    *data++ = signed_saturate_rshift(val, 16, 0);
  } while (data < end);
}

#endif

inline void AudioSmoothAmplifier::update(void)
{
  audio_block_t *block;

  if (multiplier == 0 && multiplierTarget == 0) {
    // zero gain, discard any input and transmit nothing
    block = receiveReadOnly(0);
    if (block) release(block);
  } else if (multiplier == MULTI_UNITYGAIN && multiplierTarget == MULTI_UNITYGAIN) {
    // unity gain, pass input to output without any change
    block = receiveReadOnly(0);
    if (block) {
      transmit(block);
      release(block);
    }
  } else {
    // apply gain to signal
    block = receiveWritable(0);
    if (block) {
      applyGain(block->data);
      transmit(block);
      release(block);
    }
  }
}

#endif

Code:
// Example.ino

#include <Audio.h>
#include "AudioSmoothAmplifier.h"

AudioOutputI2S i2s2;
AudioSynthWaveformSine sine;         //xy=243,108
AudioSmoothAmplifier amp;
AudioConnection patchCord1(sine, 0, amp, 0);
AudioConnection patchCord2(amp, 0, i2s2, 0);
AudioConnection patchCord3(amp, 0, i2s2, 1);
AudioControlSGTL5000 audioBoard;

elapsedMillis counter;
byte i=1;

void setup() {
  while (!Serial && millis() < 5000); // wait for serial monitor
  Serial.begin(115200);
  
  // Audio connections require memory to work.
  AudioMemory(48);
  
  audioBoard.enable();
  audioBoard.volume(0.4);

  sine.frequency(1);
  sine.amplitude(1);
}

void loop() {
  
  if(counter >= 2000){
    i++;
    i = i %2;
    counter = 0;
    amp.gain(i, 0);
  }
}
 
Another approach to reducing this zipper noise (as its called - its also happens in hardware if you
use digital potentiometer chips) is to do zero-crossing synchronization - any pending gain change
is held-off until the next signal zero-cross point. This reduces the total energy of the artifacts
significantly.

This ought to be fairly easy to implement, but will increase the CPU demand for a mixer or amp
object as a conditional test per-sample is needed to detect zero-crossing
 
Another approach to reducing this zipper noise (as its called - its also happens in hardware if you
use digital potentiometer chips) is to do zero-crossing synchronization - any pending gain change
is held-off until the next signal zero-cross point. This reduces the total energy of the artifacts
significantly.

This ought to be fairly easy to implement, but will increase the CPU demand for a mixer or amp
object as a conditional test per-sample is needed to detect zero-crossing

I see, I guess if that were to be added to the library that could be the right implementation. In my case my simple "multiplier += (smoothness * (multiplierTarget - multiplier)) / 1000;" seems to do the trick.

That was interesting though, thank you all for the help, I'll post a video of what I use it for soon :)
 
Hi folks

Integer-only version of doing a gradual change can be found at https://github.com/h4yn0nnym0u5e/Audio/tree/features/slew-gain, which includes updated documentation in the GUI tool. Not fully tested by any means, and I haven't tested the KINETISL code at all.

Gain slewing can be turned on or off by the AudioAmplifier::slew(bool enable) function, which acts as you'd expect - enable=true enables it: I'll leave you to guess what happens if you give it a "false" parameter! If the level hasn't changed or slewing is disabled then the existing gain code is called, so any performance hit only occurs when the gain is changing. I didn't do the AudioMixer4 object: the same principles would apply, though there's also an opportunity there to accumulate an intermediate 32-bit result to avoid loss of LSBs in the final 16-bit output.

Cheers

Jonathan
 
Hi folks

Integer-only version of doing a gradual change can be found at https://github.com/h4yn0nnym0u5e/Audio/tree/features/slew-gain, which includes updated documentation in the GUI tool. Not fully tested by any means, and I haven't tested the KINETISL code at all.

Gain slewing can be turned on or off by the AudioAmplifier::slew(bool enable) function, which acts as you'd expect - enable=true enables it: I'll leave you to guess what happens if you give it a "false" parameter! If the level hasn't changed or slewing is disabled then the existing gain code is called, so any performance hit only occurs when the gain is changing. I didn't do the AudioMixer4 object: the same principles would apply, though there's also an opportunity there to accumulate an intermediate 32-bit result to avoid loss of LSBs in the final 16-bit output.

Cheers

Jonathan

I haven't tried your version yet but I intend to.

In the meantime here is a video of the synth I had the gain issue with, the Mix parameter does a "spiral motion" to mix the 4 voices in little by little and that was the one that was getting noisy. It is using my AudioSmoothAmplifier from above:

 
Thanks @ghost, it’d be good to know it works in Real Life! With luck you won’t be able to tell the difference, I just wrote it to prove it could be done, and for use cases where floating point calculations within the audio engine weren’t an option.

I’d be interested to see the GUI design for your synth, plus an explanation of how your controls (especially the Mix) influence the various parts

Cheers

Jonathan
 
Thanks @ghost, it’d be good to know it works in Real Life! With luck you won’t be able to tell the difference, I just wrote it to prove it could be done, and for use cases where floating point calculations within the audio engine weren’t an option.

I will try it later today :)

I’d be interested to see the GUI design for your synth, plus an explanation of how your controls (especially the Mix) influence the various parts

You can get the code here: https://github.com/ghostintranslation/drone , if you look into Voice.h there is the GUI design of one voice, and then this synth has 4 of them.

As you will see, I made also other modules, all are based of the same platform, the Motherboard, that comes in 3 variants, 6/9/12 controls. So the low level code is always the same and it's easy to create new modules. I'm currently developing a new version though with SMT components to fit enough for 32 inputs and 16 outputs and fix everything I've learned, so I'll probably be posting about that in a different channel later :)
 
Thanks @ghost, it’d be good to know it works in Real Life! With luck you won’t be able to tell the difference, I just wrote it to prove it could be done, and for use cases where floating point calculations within the audio engine weren’t an option.

So I tried your solution, I set "slew(true);", but I can still hear some noise, it's very low volume though so I have to pay attention to hear it, it's already an improvement. I compared it again with mine to be sure and with mine I hear nothing.
 
Last edited:
Back
Top