Audible clicks when switching waveforms

Status
Not open for further replies.
Hi, so I'm working on a wavetable synth and the biggest issue I'm having is audible clicks when switching between waveforms. This is happening in the waveform example sketch with the included waveforms (most noticeable with the sine, triangle, and variable triangle i.e. the ones with less harmonics) as well as my arbitrary waveforms, which are all vaguely sineish.

I looked at this up close in my DAW and it appears the clicking is caused by the waveforms switching at any particular point in their phase. So at any point in the phase when it switches, if the first waveform is at a higher or lower value than the second it's creating that sound from the abrupt transition.

So my question is how can I get the waveform to wait till the phase is 360.0 (when the waveform values are ~0) to switch? Something like this:
Code:
    if  (button0.fallingEdge ())
         count = count + 1;

    if  (count == 1)
       if (waveform1.phase (360.0))
          
         waveform1.arbitraryWaveform(wave1, 1000);

    if  (count == 2)
        if (waveform1.phase (360.0))
           
          waveform1.arbitraryWaveform(wave2, 1000);

//etc

Now obviously if I do this the void function won't return a Boolean and I'm not savvy enough to go about it another way, so does anyone have any ideas?

Thanks
 
One way would be to use AudioEffectEnvelope to shape the start and end of each wave so that it ramps up from, and down to, zero. Depending on the length of the ramp, it should at least soften the click.
The only problem with this is if the switch from one wave to another must occur at a specific time. If the ramp down at the end of the signal is, for example 3ms, the signal will play for 3ms after you've told it to stop. You then have to take this into account when switching to another waveform.

An alternative would be to use AudioRecordQueue to monitor the generated audio. Normally you would just throw these away but when the waveform is to be switched, you will know the value of the last sample. From that value you can determine what the phase of the new waveform should be and use that to set waveform1.phase(angle); just before you start the new signal. I haven't tried this but I think it should work and would give you more control than using AudioEffectEnvelope.

Pete
 
Thanks I will try these. I have tried various combinations of lowering the cutoff frequency and fade ins and outs when switching but none were totally successful.
 
So I've been trying these suggestions, which I greatly appreciate, and it's still not working. The envelope is acting about the same as the fade object. And the audiorecordqueue method requires way too much calculation for my feeble mind, seeing as I have multiple arbitrary waveforms.
I think that using the phase function as a Boolean would work well. Does anyone have any other methods/suggestions?
Thanks
 
It's probably simpler to fade quickly out the old waveform and to fade the new one in afterwards or simultaneously. IIRC there is an ADSR object in the audio lib for that.
 
Thanks I'll try that again. Actually I've got good results with fade ins and fade outs with a low pass filter set at 500 hertz. Just cant have high frequency fun.
 
But how you cou want to produce audio then with a LPF at 500Hz? Typically wavetables are faded the smoothed way anyway so, there should be only a little demand for cross fading to avoid such issues. Hwo many WTs are you using? With how many frequency partitions?
 
Not sure what you mean by 'faded the smoothed way' if you could elaborate. I've got 6 right now, I plan on adding more. If by frequency partitions you mean band- limited no they're not, the waveforms are all sine-like so I haven't had any aliasing issues yet, even without the filter.
 
Hello.
I encountered similar problem.
Let's start from the code (teensy 3.2 + audio shield)
As you can see, I downloaded it from youtube link

Code:
// Teensy-Synth Part 3
// Keyboard Test
// By Notes and Volts
// www.notesandvolts.com

//slightly modified by Alan (me)

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

// GUItool: begin automatically generated code
AudioSynthWaveform       waveform2;      //xy=405.00000762939453,377.5000057220459
AudioSynthWaveform       waveform1;      //xy=409.00000762939453,318.0000057220459
AudioSynthNoisePink      pink1;          //xy=417.50000762939453,428.7500066757202
AudioMixer4              mixer1;         //xy=600.0000114440918,378.75000381469727
AudioOutputI2S           i2s1;           //xy=747,377
AudioConnection          patchCord1(waveform2, 0, mixer1, 1);
AudioConnection          patchCord2(waveform1, 0, mixer1, 0);
AudioConnection          patchCord3(pink1, 0, mixer1, 2);
AudioConnection          patchCord4(mixer1, 0, i2s1, 0);
AudioConnection          patchCord5(mixer1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=588,480
// GUItool: end automatically generated code

// GLOBAL VARIABLES
const byte BUFFER = 8; //Size of keyboard buffer
const float noteFreqs[128] = {8.176, 8.662, 9.177, 9.723, 10.301, 10.913, 11.562, 12.25, 12.978, 13.75, 14.568, 15.434, 16.352, 17.324, 18.354, 19.445, 20.602, 21.827, 23.125, 24.5, 25.957, 27.5, 29.135, 30.868, 32.703, 34.648, 36.708, 38.891, 41.203, 43.654, 46.249, 48.999, 51.913, 55, 58.27, 61.735, 65.406, 69.296, 73.416, 77.782, 82.407, 87.307, 92.499, 97.999, 103.826, 110, 116.541, 123.471, 130.813, 138.591, 146.832, 155.563, 164.814, 174.614, 184.997, 195.998, 207.652, 220, 233.082, 246.942, 261.626, 277.183, 293.665, 311.127, 329.628, 349.228, 369.994, 391.995, 415.305, 440, 466.164, 493.883, 523.251, 554.365, 587.33, 622.254, 659.255, 698.456, 739.989, 783.991, 830.609, 880, 932.328, 987.767, 1046.502, 1108.731, 1174.659, 1244.508, 1318.51, 1396.913, 1479.978, 1567.982, 1661.219, 1760, 1864.655, 1975.533, 2093.005, 2217.461, 2349.318, 2489.016, 2637.02, 2793.826, 2959.955, 3135.963, 3322.438, 3520, 3729.31, 3951.066, 4186.009, 4434.922, 4698.636, 4978.032, 5274.041, 5587.652, 5919.911, 6271.927, 6644.875, 7040, 7458.62, 7902.133, 8372.018, 8869.844, 9397.273, 9956.063, 10548.08, 11175.3, 11839.82, 12543.85};
byte globalNote = 0;
byte globalVelocity = 0;
int octave = 0;
const float DIV127 = (1.0 / 127.0);

void setup() {
  AudioMemory(20);
  usbMIDI.setHandleControlChange(myControlChange);
  usbMIDI.setHandleNoteOff(myNoteOff);
  usbMIDI.setHandleNoteOn(myNoteOn);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.32);
  waveform1.begin(WAVEFORM_SAWTOOTH);
  waveform1.amplitude(0.75);
  waveform1.frequency(82.41);
  waveform1.pulseWidth(0.15);

  waveform2.begin(WAVEFORM_SINE);
  waveform2.amplitude(0.75);
  waveform2.frequency(123);
  waveform2.pulseWidth(0.15);

  pink1.amplitude(1.0);

  mixer1.gain(0, 1.0);
  mixer1.gain(1, 1.0);
  mixer1.gain(2, 1.0);
}


void loop() {
  usbMIDI.read();
}

void myNoteOn(byte channel, byte note, byte velocity) {
  if ( note > 23 && note < 108 ) {
    globalNote = note;
    globalVelocity = velocity;
    keyBuff(note, true);
  }
}

void myNoteOff(byte channel, byte note, byte velocity) {
  if ( note > 23 && note < 108 ) {
    keyBuff(note, false);
  }
}

void keyBuff(byte note, bool playNote) {
  static byte buff[BUFFER];
  static byte buffSize = 0;

  // Add Note
  if (playNote == true && (buffSize < BUFFER) ) {
    oscPlay(note);
    buff[buffSize] = note;
    buffSize++;
    return;
  }

  // Remove Note
  else if (playNote == false && buffSize != 0) {
    for (byte found = 0; found < buffSize; found++) {
      if (buff[found] == note) {
        for (byte gap = found; gap < (buffSize - 1); gap++) {
          buff[gap] = buff[gap + 1];
        }
        buffSize--;
        buff[buffSize] = 255;
        if (buffSize != 0) {
          oscPlay(buff[buffSize - 1]);
          return;
        }
        else {
          oscStop();
          return;
        }
      }
    }
  }
}

void oscPlay(byte note) {
  waveform1.frequency(noteFreqs[note]);
  waveform2.frequency(noteFreqs[note + octave]);
  float velo = (globalVelocity * DIV127);
  waveform1.amplitude(velo);
  waveform2.amplitude(velo);
  pink1.amplitude(velo);
}

void oscStop() {
  waveform1.amplitude(0.0);
  waveform2.amplitude(0.0);
  pink1.amplitude(0.0);
}

void myControlChange(byte channel, byte control, byte value) {
  byte elabvalue;
  switch (control) {
    /*case 100:*/
      case 01:
      mixer1.gain(0, (value * DIV127));
      break;

    /*case 101:*/
    case 02:
      mixer1.gain(1, (value * DIV127));
      break;

    /*case 102:*/
    case 03:
      mixer1.gain(2, (value * DIV127));
      break;

    /*case 103:*/
    case 04:
       elabvalue = value / 30; 
      switch (elabvalue) {
        case 0:
          octave = 24;
          break;
        case 1:
          octave = 12;
          break;
        case 2:
          octave = 0;
          break;
        case 3:
          octave = -12;
          break;
        case 4:
          octave = -24;
          break;
      }
      break;
  }
}

To make it all work, teensy is connected to pc usb port (windows 10)
AKAI MPK mini keyboard is connected to same pc (other usb port)
MIDI-OX utility (www.midiox.com) is running on same pc so as to route midi signals coming from akai keyboard to teensy usb/midi port.
I turn knobs on akai to control volume of waveforms on teensy with midi.
I just put max volume on sine wave, and zero sawtooth and pink, so I can hear only sine wave.
Then I just play notes.
The sound comes out, but there is clicking noise at start and stop of every note.
I captured a recording of the notes, and as you can see the clicks are caused by the waveform not starting and ending at zero crossing.

I suppose this depends on the code of audio library.

Here are two screenshop of the waveform (starting and stopping sound by pressing and depressing akay piano keys)


bad1.PNG
bad2.PNG

And this is how they should be (I edited the wave with audactity)

good1.PNG
good2.PNG

If you play the bad waveform, you hear the very annoing clicks.
The good waveform has no clicks.

The clicks are less audible with the sawtooth obviously.

So: not only envelope causes the problem, but also just playing simple notes with no adsr, etc causes these clicks.

I think audio library is great, but this problem, if it depends from it, make it musically unusable for me.

BTW, anyone trying to record audio output from audio shield: DO NOT connect headphone output to pc audio input jack - the earth of the headphone jack is a virtual earth, floating at about 1 volt in respect to audio shield earth.
And the real earth of the teensy + audio shield is already connected to the pc through the usb cable!!!
Use instead line out of the shield (the solder pads)

solder.jpg

Bye



Thanks
 
I think we need a command to start and stop the generation of the waveform at zero crossing, and not abruptly changing the volume.
i.e. to do something similar to: wait and change volume at zero crossing of the wave.
a.
 
Ideally, a Note On or Off event would only happen at, or wait for the very moment when the amplitude value is zero, or at least very low. This is a technique called zero cross switching. Unfortunately, this technique is not easy to implement in the Teensy audio library because the latter works with blocks of 128 samples, and looking through all samples of a block to find the zero crossing point and modifying all samples afterwards "on the fly" might be extremely time consuming and complicated.

So: not only envelope causes the problem, but also just playing simple notes with no adsr, etc causes these clicks.

That's the second best answer. A complex ADSR is even not needed, but your Note On and Off handler code should implement a very short fade in and fade out for each note, which might happen within a few milliseconds. Seen that the 16bit resolution gives a dynamic of 96dB, a starting point could be trying 12dB steps which would allow do fade in or out over 8 audio blocks. Each block runs for about 2.9ms, so that the fading would take 23ms. You'll have to decide by ear if that works for you, or if you need finer dB steps.
 
Last edited:
Try to isolate the problem as to what part is causing it. Your wave diagrams show very large gaps...there could be delays elsewhere and not in teensy.
This sketch will let you see how fast teensy on its own with the audio adaptor can switch a waveform between each channel of I2S.

Code:
  //
// This example code is in the public domain.
// test to see if waveforms can switch without a click
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
//#include <Bounce.h>

AudioSynthWaveform       waveform1;      //xy=171,84
AudioSynthWaveform       waveform2;      //xy=178,148
AudioOutputI2S           i2s1;           //xy=360,98   
AudioConnection          patchCord1(waveform1, 0, i2s1, 0);
AudioConnection          patchCord3(waveform2, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=239,232

void setup() {
  
   // Audio connections require memory to work.  For more
  // detailed information, see the MemoryAndCpuUsage example
  AudioMemory(20);

  // Comment these out if not using the audio adaptor board.
 
   sgtl5000_1.enable();
   sgtl5000_1.volume(0.6); 


  waveform1.frequency(1000.0);
  waveform2.frequency(220.0);
  waveform1.phase(140);
  waveform1.amplitude(0.6);
  waveform2.amplitude(0.6);
 
}  // setup end

void loop() {
 
          delay(500);

    waveform1.begin(WAVEFORM_SINE);
    waveform2.begin(WAVEFORM_SQUARE);
 
       delay(500);
  
     waveform1.begin(WAVEFORM_SQUARE);
     waveform2.begin(WAVEFORM_TRIANGLE);
   
}  //loop end
 
Ideally, a Note On or Off event would only happen at, or wait for the very moment when the amplitude value is zero, or at least very low. This is a technique called zero cross switching. Unfortunately, this technique is not easy to implement in the Teensy audio library because the latter works with blocks of 128 samples, and looking through all samples of a block to find the zero crossing point and modifying all samples afterwards "on the fly" might be extremely time consuming and complicated.
[/COLOR]


Pity, but maybe there is a way, i'll try and elaborate during the weekend.


That's the second best answer. A complex ADSR is even not needed, but your Note On and Off handler code should implement a very short fade in and fade out for each note, which might happen within a few milliseconds. Seen that the 16bit resolution gives a dynamic of 96dB, a starting point could be trying 12dB steps which would allow do fade in or out over 8 audio blocks. Each block runs for about 2.9ms, so that the fading would take 23ms. You'll have to decide by ear if that works for you, or if you need finer dB steps.[/QUOTE]

I don't know, i'll give it a try.
Btw in \arduino\hardware\teensy\avr\libraries\Audio\synth_waveform.md I found the following paragraph:

void set_ramp_length(int16_t r_length)
When a tone starts, or ends, playing it can generate an audible "thump" which can
be very distracting, especially when playing musical notes. This function specifies
a "ramp" length (in number of samples) and the beginning of the generated waveform
will be ramped up in volume from zero to t_amp over the course of r_length samples.
When the tone is switched off, by changing its volume to zero, instead of ending
abruptly it will be ramped down to zero over the next r_length samples.
For example, if r_length is 44, the beginning and end of the wave will have a ramp
of approximately one millisecond.

which gave me some hope.
but then i did not find function set_ramp_length mentioned anywhere else
maybe a feature in the future ?
or a lost one?

Anyway, thank you for the kind clarifications .. merci beaucoup ;-)
Alan
 
Hello TeenFor3.

The gaps you are seeing are not caused by the program. It's ME playing the piano (midi keyboard). So i press the piano key, midi sends note on, some time passing, take off the finger from the piano, midi sends note off , other time passes, note on again etc.
Mi focus is on the glitches that occur at the moment the note starts to play, and when it's turned off.
The waveform should start from zero, and not 'suddenly' with a random value which depends on the moment the 'sound on' command was issued.
Inversely when the sound off commands arrives, waveform should gracefully return to zero and only then volume should be set to zero.
Thank you in any case
a.
 
If you are playing a midi keyboard....it is sending out the note on and note off to teensy I assume...I assume teensy is the synthesiser. your code will need to be setup to receive these and will play accordingly......attached is a teensy library example sketch for a simple drum playing from a timed note basis...it could be modified to respond to the signals from your keyboard by pressing 4 note keys one for each sound. also attached is screen dump of the waveform and no gaps or clicks. also screen dump of waveform from earlier post.....it too could switch the waveforms by pressing key on the keyboard if mods to control it.
wavesswitch.jpg
DrumExample.jpg

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

#include <synth_simple_drum.h>

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

// GUItool: begin automatically generated code
AudioSynthSimpleDrum     drum2;          //xy=399,244
AudioSynthSimpleDrum     drum3;          //xy=424,310
AudioSynthSimpleDrum     drum1;          //xy=431,197
AudioSynthSimpleDrum     drum4;          //xy=464,374
AudioMixer4              mixer1;         //xy=737,265
AudioOutputI2S           i2s1;           //xy=979,214
AudioConnection          patchCord1(drum2, 0, mixer1, 1);
AudioConnection          patchCord2(drum3, 0, mixer1, 2);
AudioConnection          patchCord3(drum1, 0, mixer1, 0);
AudioConnection          patchCord4(drum4, 0, mixer1, 3);
AudioConnection          patchCord5(mixer1, 0, i2s1, 0);
AudioConnection          patchCord6(mixer1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=930,518
// GUItool: end automatically generated code

static uint32_t next;

void setup() {
  // put your setup code here, to run once:

  Serial.begin(115200);

  // audio library init
  AudioMemory(15);

  next = millis() + 1000;

  AudioNoInterrupts();

  drum1.frequency(60);
  drum1.length(1500);
  drum1.secondMix(0.0);
  drum1.pitchMod(0.55);
  
  drum2.frequency(60);
  drum2.length(300);
  drum2.secondMix(0.0);
  drum2.pitchMod(1.0);
  
  drum3.frequency(550);
  drum3.length(400);
  drum3.secondMix(1.0);
  drum3.pitchMod(0.5);

  drum4.frequency(1200);
  drum4.length(150);
  drum4.secondMix(0.0);
  drum4.pitchMod(0.0);
  
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  AudioInterrupts();

}

void loop() {
  // put your main code here, to run repeatedly:

  static uint32_t num = 0;

  if(millis() == next)
  {
    next = millis() + 300;

    switch(num % 4)
    {
      case 0:
        drum1.noteOn();
        break;
      case 1:
        drum2.noteOn();
        break;
      case 2:
        drum3.noteOn();
        break;
      case 3:
        drum4.noteOn();
        break;
    }
    num++;

    Serial.print("Diagnostics: ");
    Serial.print(AudioProcessorUsageMax());
    Serial.print(" ");
    Serial.println(AudioMemoryUsageMax());
    AudioProcessorUsageMaxReset();
  }
}
 
@Alan
You just need to insert AudioEffectEnvelope between AudioMixer4 and AudioOutputI2S. Then you trigger it with noteOn and noteOff in stead of adjusting the volume of your oscillators.
 
Maybe or maybe not, I misunderstood the earlier posts. I thought the problem was the switching or starting and stopping the actual waveforms and how clicks or large gaps appear in the sound output. My earlier post attempted to show in a simple way that even switching "raw" waveforms with no frills added, actually happens very quickly and cleanly in teensy with minimal clicks and gaps. Envelopes will control the amplitude over the duration of the sound in a more controlled and smoother way. Between the mixer and I2S it will allow the mixed already switched waveforms to be controlled on or off. If between the waveforms and the mixer it will control which or combinations of waveforms are on or off and sent to the mixer for output. envelopes can be in both places.
 
Status
Not open for further replies.
Back
Top