Do Teensy Synths with Sine Waves always click?

trevorbryden

Active member
Hi!
Before I go nuts chasing it down...
If i make basically the world's simplest synth in the design tool...a sine wave triggered by midi, no filter, no nothing, mixer level even down to 0.3, chip output 0.4, etc...there is an obvious click in the headphones on each note on and off.
I've tried setting the phase of the wave to 0 just before Osc1.begin() etc etc but...
Is this just something that happens for some reason? Are there known strategies to mitigate it (besides attack filter stuff)?
Thanks!
 
I think a little envelope will be the next step for sure. Was just wondering if the inability to launch and terminate a sine wave at phase zero was a known thing or i am screwing it up- and whether the elegant simple solution, if any, was widely known.
Will post code tonight.
Thanks
 
Play a simple continuous sinewave and use the midi note change command to change the frequency of the sinewave for the corresponding note
 
As an experiment? Does that get me closer to a playable synth via midi that doesn't click when notes start and stop (when keys are pressed)? That is what I am hearing and unable so far to get rid of.
 
There will be no clicks because continuous. To try it without midi use a for-next loop to increment up and down the frequency
 
Right, you mean as a way to test my audio circuit? I have done that and it is clean. Notes held are also happy. Its just the note ons and note offs
 
Post your code a simple version. Yes held notes are OK while playing, I assumed it was the changing of notes from say A to B to C etc was the problem. as a test do that by changing the frequency value of the sound....it should happen smoothly.
What is happening will depend how you are generating the sine wave .... wavetable or whatever and what process is in the noteon command
 
You need to use the envelope effect.

Changes you make from Arduino code take effect only every 128 audio samples. You just can't get the timing precision to turn the waveform off at a zero crossing... at least not without creating a completely different oscillator object in the library. Even then, the whole system is designed around a block update approach. The efficiency that brings, allowing you to create such complex designs (hundreds of oscillators on Teensy 4) also means certain limitations. It's just not designed to work the way you're trying to do things.

Use the envelope effect.
 
Last edited:
Definitely was not criticising the best and most fun design environment I've ever played with. I was just making sure that I wasn't doing something wrong because I'm very new to all of this. Thank you
 
Yes I understand envelopes etc are the way to go. Initial post was I assumed to get a basic sine wave playing different notes without clicks.
Code below is probably the most basic and play 5 notes no clicks

Code:
#include <Audio.h>

const int16_t tone1[256] = {
0,
628,
1256,
1883,
2509,
3133,
3756,
4376,
4994,
5608,
6220,
6827,
7431,
8030,
8624,
9213,
9796,
10374,
10945,
11510,
12067,
12618,
13160,
13696,
14222,
14740,
15249,
15750,
16240,
16721,
17192,
17652,
18102,
18540,
18968,
19384,
19789,
20181,
20562,
20930,
21285,
21628,
21957,
22274,
22577,
22866,
23142,
23403,
23651,
23884,
24103,
24307,
24497,
24672,
24832,
24977,
25108,
25223,
25322,
25407,
25476,
25530,
25569,
25592,
25600,
25592,
25569,
25530,
25476,
25407,
25322,
25223,
25108,
24977,
24832,
24672,
24497,
24307,
24103,
23884,
23651,
23403,
23142,
22866,
22577,
22274,
21957,
21628,
21285,
20930,
20562,
20181,
19789,
19384,
18968,
18540,
18102,
17652,
17192,
16721,
16240,
15750,
15249,
14740,
14222,
13696,
13160,
12618,
12067,
11510,
10945,
10374,
9796,
9213,
8624,
8030,
7431,
6827,
6220,
5608,
4994,
4376,
3756,
3133,
2509,
1883,
1256,
628,
0,
-629,
-1256,
-1884,
-2510,
-3134,
-3757,
-4377,
-4995,
-5609,
-6221,
-6828,
-7432,
-8031,
-8625,
-9214,
-9797,
-10375,
-10946,
-11511,
-12068,
-12619,
-13161,
-13696,
-14223,
-14741,
-15250,
-15751,
-16241,
-16722,
-17192,
-17653,
-18103,
-18541,
-18969,
-19385,
-19790,
-20182,
-20563,
-20931,
-21286,
-21629,
-21958,
-22275,
-22578,
-22867,
-23143,
-23404,
-23652,
-23885,
-24104,
-24308,
-24498,
-24673,
-24833,
-24978,
-25109,
-25224,
-25323,
-25408,
-25477,
-25531,
-25570,
-25593,
-25600,
-25593,
-25570,
-25531,
-25477,
-25408,
-25323,
-25224,
-25109,
-24978,
-24833,
-24673,
-24498,
-24308,
-24104,
-23885,
-23652,
-23404,
-23143,
-22867,
-22578,
-22275,
-21958,
-21629,
-21286,
-20931,
-20563,
-20182,
-19790,
-19385,
-18969,
-18541,
-18103,
-17653,
-17192,
-16722,
-16241,
-15751,
-15250,
-14741,
-14223,
-13696,
-13161,
-12619,
-12068,
-11511,
-10946,
-10375,
-9797,
-9214,
-8625,
-8031,
-7432,
-6828,
-6221,
-5609,
-4995,
-4377,
-3757,
-3134,
-2510,
-1884,
-1256,
-629
};


// GUItool: begin automatically generated code
AudioSynthWaveform       waveform1;      //xy=285,868
AudioOutputAnalog        dac1;           //xy=739,914
AudioConnection          patchCord1(waveform1, 0, dac1, 0);

// GUItool: end automatically generated code

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

 AudioMemory(15); //set memory allocation
      
  // by default the Teensy 3.1 DAC uses 3.3Vp-p output
  // if your 3.3V power has noise, switching to the
  // internal 1.2V reference can give you a clean signal
   dac1.analogReference(INTERNAL);
   // dac1.analogReference(EXTERNAL);
   
    delay(40);
    
     waveform1.begin(1.0, 1000, WAVEFORM_ARBITRARY);  
     waveform1.arbitraryWaveform(tone1, 1200); 
     
   }

void loop() {
  // put your main code here, to run repeatedly:
  
 waveform1.frequency(400);

delay(1000);
waveform1.frequency(600);
delay(1000);
waveform1.frequency(800);
delay(1000);
waveform1.frequency(1000);
delay(1000);
waveform1.frequency(1200);
delay(1000);


}
 
Yeah, I don't know. Now I get the click, and an initial transient, THEN you can hear the filter ramp up...
What I can't seem to do is get the MIDI.read() thing to happen but still be able to twist knobs and mess with waveform while notes are ringing unless I put the wave begin stuff in the main loop. When I put my Osc.begin stuff in the GetNoteOn part of the code, it works OK but you can't change any of the parameters while a note rings...
So I feel like I can't get the whole flow of things working at all and this clicking/envelope thing has to be something simple. HELP!

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

// GUItool: begin automatically generated code
AudioSynthWaveform       Osc1;      //xy=94.88890075683594,78.11111068725586
AudioSynthWaveform       Osc2;      //xy=96.88890075683594,113.11111068725586
AudioSynthWaveform       Osc3;      //xy=98.88890075683594,146.11111068725586
AudioMixer4              Premix;         //xy=214.88890075683594,120.11111068725586
AudioEffectEnvelope      ADSR;      //xy=347.88890838623047,120.11112403869629
AudioFilterStateVariable LowPassFilter;        //xy=499.8888931274414,126.88888359069824
AudioEffectFreeverb      Verb;      //xy=655.5555191040039,143.66667461395264
AudioMixer4              MasterR;         //xy=784.8889083862305,185.8888931274414
AudioMixer4              MasterL;         //xy=786.1110954284668,120.00000381469727
AudioOutputI2S           i2s1;           //xy=915,153
AudioConnection          patchCord1(Osc1, 0, Premix, 0);
AudioConnection          patchCord2(Osc2, 0, Premix, 1);
AudioConnection          patchCord3(Osc3, 0, Premix, 2);
AudioConnection          patchCord4(Premix, ADSR);
AudioConnection          patchCord5(ADSR, 0, LowPassFilter, 0);
AudioConnection          patchCord6(LowPassFilter, 0, MasterL, 0);
AudioConnection          patchCord7(LowPassFilter, 0, MasterR, 0);
AudioConnection          patchCord8(LowPassFilter, 0, Verb, 0);
AudioConnection          patchCord9(Verb, 0, MasterL, 1);
AudioConnection          patchCord10(Verb, 0, MasterR, 1);
AudioConnection          patchCord11(MasterR, 0, i2s1, 1);
AudioConnection          patchCord12(MasterL, 0, i2s1, 0);
AudioControlSGTL5000     sgtl5000_1;     //xy=863.2222442626953,263.44445037841797
// GUItool: end automatically generated code


#include <MIDI.h>

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

//variables that hold the values/states of the knobs and switches

int attackVal;
int decayVal;
int sustainVal;
int releaseVal;
int resoVal;
int cutoffVal;
int osc2FreqVal;
int osc2LevelVal;
int osc3FreqVal;
int osc3LevelVal;
int osc1Shape1Val;
int osc1Shape2Val;
int osc2Shape1Val;
int osc2Shape2Val;
int osc3Shape1Val;
int osc3Shape2Val;
int osc3Octave1Val;
int osc3Octave2Val;
int lfoRateVal;
int lfoDepthVal;
int lfoShape1Val;
int lfoShape2Val;
int lfoDest1Val;
int lfoDest2Val;
int lfoDest3Val;
int volumeVal;
int delayLengthVal;
int delayMixVal;
int reverbMixVal;


//Pin assignments

int attackPin = 21;
int decayPin = 20;
int sustainPin = 17;
int releasePin = 16;
int resoPin = 31;
int cutoffPin = 32;
int osc1ShapePin1 = 1;
int osc1ShapePin2 = 2;
int osc2ShapePin1 = 27;
int osc2ShapePin2 = 28;
int osc3ShapePin1 = 8;
int osc3ShapePin2 = 5;
int osc2FreqPin = A14;
int osc2LevelPin = A18;
int osc3FreqPin = A20;
int osc3LevelPin = A17;
int osc3OctavePin1 = 30;
int osc3OctavePin2 = 29;
int lfoRatePin = A15;
int lfoDepthPin = A19;
int lfoShapePin1 = 3;
int lfoShapePin2 = 4;
int lfoDestPin1 = 25;
int lfoDestPin2 = 24;
int lfoDestPin3 = 26;
int volumePin = A1;
int delayLengthPin = A22;
int delayMixPin = A16;
int reverbMixPin = A21;

//oscillator and audio library object values

int osc1current_waveform = WAVEFORM_SINE;
float osc1Pitch = 261.63;
int osc2current_waveform = 0;
float osc2Pitch = 130.86;
int osc3current_waveform = 0;
float osc3Pitch = 65.44;
float osc1Volume = 0.00;
float osc2Volume = 0.00;
float osc3Volume = 0.00;
float osc3Octave = 1.00;
float filterFreq = 6500.0;
float resAmount = 0.700;
float verbMix = 0.00;
float hertz = 1.000;

//midi code variables

void setup() {
  
  Serial.begin(9600);
  
  pinMode (osc1ShapePin1, INPUT_PULLUP);
  pinMode (osc1ShapePin2, INPUT_PULLUP);
  pinMode (osc2ShapePin1, INPUT_PULLUP);
  pinMode (osc2ShapePin2, INPUT_PULLUP);
  pinMode (osc3ShapePin1, INPUT_PULLUP);
  pinMode (osc3ShapePin2, INPUT_PULLUP);
  pinMode (osc3OctavePin1, INPUT_PULLUP);
  pinMode (osc3OctavePin2, INPUT_PULLUP);
  pinMode (lfoShapePin1, INPUT_PULLUP);
  pinMode (lfoShapePin2, INPUT_PULLUP);
  pinMode (lfoDestPin1, INPUT_PULLUP);
  pinMode (lfoDestPin2, INPUT_PULLUP);
  pinMode (lfoDestPin3, INPUT_PULLUP);
   
  AudioMemory(120);

  sgtl5000_1.enable();
  sgtl5000_1.volume(0.7);
  LowPassFilter.frequency(filterFreq);
  Verb.roomsize(0.75);
  Verb.damping(0.75);
  
  MIDI.begin(MIDI_CHANNEL_OMNI);
  MIDI.setHandleNoteOn(OnNoteOn);
  MIDI.setHandleNoteOff(OnNoteOff);
  
  Premix.gain(0, 0.7);
  Premix.gain(1, 0.7);
  Premix.gain(2, 0.7);

  ADSR.releaseNoteOn(50);

}

void loop() {
  
  getReadings();
  setWaveForms();
  applyFilters();
  reverbMix();
  MIDI.read();
  
  Osc1.begin(osc1Volume, osc1Pitch, osc1current_waveform);
  osc2Volume = ((osc2FreqVal / 1023.00)*osc1Volume); 
  osc3Volume = ((osc3LevelVal / 1023.00)*osc1Volume); 
  Osc2.begin(osc2Volume, osc2Pitch, osc2current_waveform);
  Osc3.begin(osc3Volume, osc3Pitch, osc3current_waveform);
  
}

void getReadings(){
  
  attackVal = analogRead(attackPin);
  decayVal = analogRead(decayPin);
  sustainVal = analogRead(sustainPin);
  releaseVal = analogRead(releasePin);
  resoVal = analogRead(resoPin);
  cutoffVal = analogRead(cutoffPin);
  osc1Shape1Val = digitalRead(osc1ShapePin1);
  osc1Shape2Val = digitalRead(osc1ShapePin2);
  osc2Shape1Val = digitalRead(osc2ShapePin1);
  osc2Shape2Val = digitalRead(osc2ShapePin2);
  osc3Shape1Val = digitalRead(osc3ShapePin1);
  osc3Shape2Val = digitalRead(osc3ShapePin2);
  osc3Octave1Val = digitalRead(osc3OctavePin1);
  osc3Octave2Val = digitalRead(osc3OctavePin2);
  osc2FreqVal = analogRead(osc2FreqPin);
  osc2LevelVal = analogRead(osc2LevelPin);
  osc3FreqVal = analogRead(osc3FreqPin);
  osc3LevelVal = analogRead(osc3LevelPin);
  lfoRateVal = analogRead(lfoRatePin);
  lfoDepthVal = analogRead(lfoDepthPin);
  lfoShape1Val = digitalRead(lfoShapePin1);
  lfoShape2Val = digitalRead(lfoShapePin2);
  lfoDest1Val = digitalRead(lfoDestPin1);
  lfoDest2Val = digitalRead(lfoDestPin2);
  lfoDest3Val = digitalRead(lfoDestPin3);
  volumeVal = analogRead(volumePin);  
  delayLengthVal = analogRead(delayLengthPin);
  delayMixVal = analogRead(delayMixPin);
  reverbMixVal = analogRead(reverbMixPin);  

}

void setWaveForms(){
 if (osc1Shape1Val == 0){
    osc1current_waveform = WAVEFORM_SQUARE;
    Serial.println("osc1 square");
  }
  else if (osc1Shape2Val == 0){
    osc1current_waveform = WAVEFORM_SAWTOOTH;
    Serial.println("osc1 saw");
  }
  else {
    osc1current_waveform = WAVEFORM_SINE;
    Serial.println("osc1 sine");
  } 

  if (osc2Shape1Val == 0){
    osc2current_waveform = WAVEFORM_SQUARE;
  }
  else if (osc2Shape2Val == 0){
    osc2current_waveform = WAVEFORM_SAWTOOTH;
  }
  else {
    osc2current_waveform = WAVEFORM_TRIANGLE;
  } 

   if (osc3Shape1Val == 0){
    osc3current_waveform = WAVEFORM_SQUARE;
  }
  else if (osc3Shape2Val == 0){
    osc3current_waveform = WAVEFORM_SAWTOOTH;
  }
  else {
    osc3current_waveform = WAVEFORM_TRIANGLE;
  } 
}

  
void applyFilters(){
  filterFreq =  expf((float)cutoffVal / 145.00) * 10.00 + 10.00;
  LowPassFilter.frequency(filterFreq);
  resAmount = (.690 + (resoVal / 235.000));
  LowPassFilter.resonance(resAmount);
  float attackamount = (15 + (attackVal*2));
  ADSR.attack(attackamount);
  Serial.println(attackamount);
}

void reverbMix(){
  verbMix = (reverbMixVal / 1023.00);
  MasterL.gain(0, .6);
  MasterR.gain(0, .6);
  MasterL.gain(1, .8 * verbMix);
  MasterR.gain(1, .8 * verbMix); 
}



void OnNoteOn(byte channel, byte note, byte velocity)
{
  ADSR.noteOn(); 
  hertz = (pow (2.0, ((float)(note-69)/12.0)))*440;
  osc1Pitch = hertz;
 
  osc2Pitch = (osc1Pitch * 1.16);
  
   if (osc3Octave1Val == 0){
    osc3Octave = 0.50;}
  
   else if (osc3Octave2Val == 0){
    osc3Octave = 2.00;}
  
   else {
    osc3Octave = 1.00;} 
    
  osc3Pitch = (osc1Pitch / osc3Octave); 
  

  osc1Volume = float(velocity/180.00);   
  
}

void OnNoteOff(byte channel, byte note, byte velocity)
{
  osc1Volume = 0;
  osc2Volume = 0;
  osc3Volume = 0;
  ADSR.noteOff();
}
 
Cutting a sine wave off at a zero-crossing is going to reduce the audible transient, but not eliminate it completely.

To do that you need to multiply the sine-wave with a smooth envelope to reduce it to zero with basically
no discontinuities. Ditto for starting a sine wave. The standard envelope object in the Audio library is rather
simplistic but will do better than a sudden switch-off, especially with suitable long attack and decay times.

It does have problems when you retrigger the envelope before its completely decayed as ideally you'd mix
together the old decaying wave with the new attack-phase of the new note - but that's more complex processing
than a simple envelope.

What you see on a spectrum analyzer when a sine wave starts or stops suddenly (even if at a zero crossing),
is a pulse of energy across a wide range of frequencies, not just that of the sinewave - the transient at
start or stop has a wide bandwidth, and that is what we hear as a click (or thump when lower frequencies
predominate).

One approach you can use to tame this is to pass the waveform through a band-pass filter so that the
start-up and stop transients are band-limited - this can work well, but won't help so much it you want
to change to a completely different note suddenly - the filter is tuned to the current note, not the new note.

For that it may be best to have two oscilators and two bandpass filters and mix them together - in other words don't
reuse a voice immediately for a new note, but have a second voice for the new note, and retire the old voice
for reuse after a suitable period to allow it to ring down smoothly.

Of course for general waveforms you need the envelope approach, not the bandpass filter, but the same idea
applies - don't reuse the same envelope for two successive notes if you want the cleanest transition - allow
both notes their own envelopes so one can tail off as the other ramps up...
 
So to start fresh, for my simple three oscillator synth, I am going to focus on oscillator 1, the one that the keyboard directly controls, and it is actually going to be two oscillators under the hood. And for the monophonic version, a note on will pick the unused of the two oscillator-bandpass-adsr signals to use. The adsr proper the synth, the one with knobs for the user to twiddle before the amplifier, won't be part of this...these are just to tame transients during note on and note offs. Does this sound right? And is the knob controlled ADSR in a synth with a low pass filter controllable in real time with resonance and frequency knobs usually located ahead of the ADSR knob section in the signal path?
 
I don't know a great deal about synth architectures though, but I do know that sometimes these clicks are wanted -
for example the Hammond organ, with its electro-mechanical nature, has switching transients (clicks) due to the
simple use of switches for each note. However each key has 9 switches that switch at about the same time, depending
on how fast the key is moving, one for each drawbar - apparently when creating a Hammond emulations, for an
really authentic sound you have to emulate this multiple-transient nature of the key switching - the ear is very
good at characterizing the nature of transients, so its probably best to be quite flexible about how much transient
and transient bandwidth you have as controlling these parameters will enable a wider range of effects.

Consider the difference between a harpsichord and piano and dulcimer - the transients make a lot of the character.
 
For sure. These are clicks that are digital and bad, not attack sounds or cool instrument impacts. They sound like digital clipping or when you truncate a wave off of a zero crossing or the clicks you get down an ADAT line when your clocks aren't synced or...
 
Aren't doing Osc1.begin in the main loop a little to fast changing/starting the waveform?
You should only call the .begin function once when the waveform change command is received.
 
but then when I look at the AudioSynthWaveform source code that should not matter,
as the begin function don't do any special things except setting the values,
which if not changed should not introduce those clicks/pops.

But if I read your code
you are using the raw analogRead value which can fluctuate in value,
maybe that is what is causing the clicks.
If that analog read value is rounded or averaged then you get more stable values.

Also you could separate out the .begin function so that is only called when changing the waveform,

and then call the frequency and amplitude functions in the main loop,

you could also if wanted have a "old" value to check against and only call the
frequency and amplitude functions when needed.
 
Thanks! I have started over and did sorta wander towards what you are outlining...getting a few waves going vs. a playable instrument is quite the difference! Going to try to just get a single voice going with two oscillators responsible so that successive notes use different oscillators, and envelope filter each one, etc.
I figure this will also teach me to allocate voices for a bit of polyphony during the next step, so time well spent.
Thanks a million for your help so far. I'm at least 5% done with the 'simple synth' intended to get me ready to build the one with more features.
 
These clicks would also happen in an analog Oscillator if you "switch" it on fast enough. Its not a "digital" problem, its a physical limitation.
 
Back
Top