Bird Concert

1. Motivation

My attempts to imitate different bird calls ended up in a highly parameterizable function that can also serve as a sweep generator for other experiments. I also discovered that a lambda expression is useful to make the code more concise.

2. Parts

For my experiments I used a piezo disk buzzer directly connected to a pin of an ESP32 DevKit V1.

ESP32 DevKit V1 and Piezo Disk Buzzer
birdSongParts

3. Wiring

			
  ------------.
     ESP32    |
              |              ____||
  GPIO_NUM_4  o-------------|    || piezo
              |             |-¦¦-|| buzzer
          GND o-------------|____|| 
              |                  ||
			

4. Implementation

The basic function for sound generation is to turn a buzzer on and off. For simplicity, the on time and the off time should be the same. If we pack this functionality into a lambda expression and call it buz, we get:

			
  // lambda expression to toggle a buzzer once 
  // with a duty cycle of 50% |¨¨¨|___|
  //                           T0  T0
  auto buz = [](uint32_t usT0){ 
    digitalWrite(PIN_BUZZER, HIGH);
    delayMicroseconds(usT0); 
    digitalWrite(PIN_BUZZER, LOW);
    delayMicroseconds(usT0);};
			

If we call this function n times, we get a tone consisting of n square waves of the period 2*T0 microseconds. A bird call, however, is not a sound of a single frequency, but a periodic chirping with rising and falling frequencies.

So the chirp function I want to design surely needs the parameters "start frequency" and "end frequency". Additionally I want to be able to define in how many steps the end frequency should be reached. And as already mentioned, I can specify how many periods the tone of a single frequency should consist of.

I could do this by adding a small frequency difference at each step. However, it is also easy to divide the frequency range into equal intervals, as in the chromatic scale, and multiply the frequency of successive notes by a constant factor.

A chirp function that sweeps a certain frequency range once in n steps could therefore look like this:

			
  chirp(fStart, fEnd, nSteps, nPeriods)			
			

But wait, a bird chirps several times with small interruptions between each peep. So the function gets 2 more parameters, namely nChirps and msPause, which specify how many chirps should sound and in which time interval.

The whole conversion of frequencies into periods and the determination of the frequency multiplier are hidden in the function body and the "birdcall designer" can fully concentrate on the selection of the function parameters to imitate a specific bird.

After all these considerations I decided to make the duty cycle variable as well (1 .. 99%) and finally the chirp function looked like this:

Exponential Chirp

Because successive frequencies are multiplied by a constant factor at each step, the frequencies increase exponentially.

			
  void chirp(uint32_t fStart, uint32_t fStop, int nSteps, int nPeriods, int nChirps, int duty, uint32_t msPause)
  {
    double pStart = 1000000.0 / (double)fStart;
    double pStop  = 1000000.0 / (double)fStop;
    // We calculate the multiplicator k to get fStop in nSteps
    // fStop = fStart * k ^ nSteps ---> 1/pStop = 1/pStart * k ^ nSteps ---> pStart/pStop = k ^ nSteps
    // But we use the periods, because that fits better to toggle the buzzer 
    double k = log(pStart / pStop) / (double)nSteps;  
    k = exp(-k);  // here we get actually 1/k, because we will multiply in the buzzer loop (see beloW) ❗

    // lambda expression to toggle a buzzer once 
    // with a duty cycle of duty% |¨¨¨|______|
    //                             tOn  tOff
    auto buz = [](uint32_t usTon, uint32_t usToff){  
        digitalWrite(PIN_BUZZER, HIGH);  
        delayMicroseconds(usTon);       
        digitalWrite(PIN_BUZZER, LOW); 
        delayMicroseconds(usToff);};

    for (int n = 0; n < nChirps; n++) // output nChirps
    { 
      uint32_t p = (uint32_t)round((double)pStart);

      for (int s = 0; s <= nSteps; s++)
      {
        uint32_t tOn  = period * duty / 100;
        uint32_t tOff = period - tOn;

        // output nPulses with same pitch
        for (int n = 0; n < nPeriods; n++) buz(tOn, tOff); 
        period *= k; // calculate next period ❗
      }
      delay(msPause);
    } 
  }			
			

The diagram below shows the generated frequencies with a chirp over 3 octaves from 440 Hz to 3520 Hz in 12 steps per octave.

Exponential Chirp
chirp_exp

Let's take a look at some function calls

chirp(1, 1, 1, 1, 1, 50, 2000)
chirp1

Start and end frequency are each 1 Hz. The end frequency is reached in one step and the two frequencies each consist of one period and the chirp is executed once. The duty cycle is 50%. At the end of this chirp there is a 2 sec. wait. But because I call the function repeatedly in the main loop of the program, the pulse train also appears repeatedly in the screenshot from my scope (see below).

chirp(1, 2, 1, 1, 1, 50, 2000)
chirp2

Same as above, but the final frequency is 2 Hz. We see one pulse of 1Hz and one of 2Hz. The off-time of the second pulse is merged with the wait of 2 seconds.

chirp(1, 4, 4, 1, 1, 50, 2000)
chirp3

The start frequency is 1 HZ, the end frequency is 4 Hz. This is reached in 4 steps and each intermediate frequency consists of 1 period and 1 chirp is executed. At the end also 2000 ms are waited.

chirp(880, 440, 12, 10, 1, 50, 1000)
chirp4

Generates the 12 semitones of the chromatic scale starting at 880 Hz and ending at the concert pitch 440 Hz. Each tone consists of 10 periods. To hear the individual tones better, increase the number of periods from 10 to 1000.

Zoomed in at start (880Hz)
chirp5
Zoomed in at end (440Hz)
chirp6

Combined Chirps

Let's combine several chirps to imitate different birds, e.g. the cuckoo.

			
  void cuckoo()
  {
    const float third = 1.222;     // minorThird = 1.18 ... majorThird = 1.25
    const float cuc = 667;         //  E4
    const float koo = cuc / third; // ~C#4
    for (int i = 1; i < random(1,5); i++)
    {
      chirp(cuc, cuc, 1, 46, 1, 50, 200);
      chirp(koo, koo, 1, 52, 1, 50, 830);
    }
  }			
			
Cuckoo
cuckoo

Start and end frequency of "cuc" as well as of "koo" are the same. Therefore, the two calls sound 2 * 46 * 1000/667 = 138 ms and 2 * 52 * 1000/546 = 190 ms long with a pause of 200 ms in between, summing up to a total of ~530ms.

This artificial cuckoo does not sound very natural, because the feathered cuckoo calls not with rectangular but with almost pure sine tones.

The cawing of a raven comes quite close to the real call if we reduce the duty cycle to 20% and call the function like this:

			
  void raven()
  {
    chirp(75,65,8,4,random(2, 6), 20, 350);
  }			
			

Sinusoidal Chirp

Why would a bird change the frequencies of its beeps only exponentially? Interesting effects can be created if the frequencies of a chirp are changed sinusoidally.

We map the step interval (0 .. nSteps) to the range (0 .. 2Pi), so we run through this range in n steps. For each step we now calculate the corresponding frequency in the following way:

			
  f = fm + fa * sin(k * s)
  with
  fm = (fStart + fStop) / 2    The "middle" frequency
  fa = (fStop - fStart) / 2    The "frequency swing (amplitude)" around fm 
  k  = TWO_PI / nSteps;
  s  = step number, 0 .. nSteps			
			

But why should we limit ourselves to sinusoidally graduated frequencies? We can imagine any other mathematical rules to get from one frequency to the next.

If we pass the "frequency generator" as a parameter to our chirp function, we get a generalized chirp function. But first we define a type for the frequency generator function as follows:

			
  typedef double (*FreqGen)(int stepNbr, double fStart, double fStop,int nSteps);
			

Generalized Chirp

The generalized chirp function now looks like this:

			
  void chirp(double fStart, double fStop, 
             int nSteps, int nPeriods, int nChirps, 
             FreqGen fgen, int duty, uint32_t msPause)
  {
    auto buz = [](uint32_t usTon, uint32_t usToff){
      digitalWrite(PIN_BUZZER, HIGH);
      delayMicroseconds(usTon);
      digitalWrite(PIN_BUZZER, LOW);
      delayMicroseconds(usToff);};

    for (int n = 0; n < nChirps; n++) // output nChirps
    {
      for (int s = 0; s <= nSteps; s++)
      {
        double fNext = fgen(s, fStart, fStop, nSteps);  // ❗ call the frequency generator
        double p = 1000000.0 / fNext;
        uint32_t tOn  = p * duty / 100.0;
        uint32_t tOff = p - tOn;
        //log_i("%2d: f = %5.2f, ton = %d, toff = %d", s, fNext, tOn, tOff);
        for (int n = 0; n < nPeriods; n++) buz(tOn, tOff);
      }
      delay(msPause);
    }
  }			
			

Now we only have to program generators with different mathematical rules:

A Generator for linearly varying frequencies

			
  double linearScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double df = (fStop - fStart) / nSteps;
    double fNext = fStart + stepNbr * df;
    return fNext;
  }			
			

A Generator for exponentially (chromatically) varying frequencies

			
  double chromaticScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    // We calculate the multiplicator k to get fStop in nSteps
    // fStop = fStart * k ^ nSteps
    double k = log(fStop / fStart) / (double)nSteps;  
    double fNext = fStart * exp(k * stepNbr);
    return fNext;
  }			
			

2 Generators for frequencies following a sine

The range of steps 0 .. nSteps is mapped to 0 .. Pi or to 0 .. 2Pi
			
  double sinePiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double fa = (fStop - fStart);  // max. frequency swing
    const double k = PI / nSteps;
    double fNext = fStart + fa * sin(k * stepNbr); // get next frequency
    return fNext;
  }			
			
			
  double sine2PiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double fm = (fStart + fStop) / 2.0;  // arithmetic mean
    double fa = (fStop - fStart) / 2.0;  // max. frequency swing around fm
    const double k = TWO_PI / nSteps;
    double fNext = fm + fa * sin(k * stepNbr); // get next frequency
    return fNext;
  }			
			

2 Generators for frequencies following a cosine

The range of steps 0 .. nSteps is mapped to 0 .. Pi or to 0 .. 2Pi
			
  double cosinePiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double fm = (fStart + fStop) / 2.0;  // arithmetic mean
    double fa = (fStop - fStart) / 2.0;  // max. frequency swing around fm
    const double k = PI / nSteps;
    double fNext = fm - fa * cos(k * stepNbr); // get next frequency
    return fNext;
  }			
			
			
  double cosine2PiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double fm = (fStart + fStop) / 2.0;  // arithmetic mean
    double fa = (fStop - fStart) / 2.0;  // max. frequency swing around fm
    const double k = TWO_PI / nSteps;
    double fNext = fm - fa * cos(k * stepNbr); // get next frequency
    return fNext;
  }			
			

2 Generators for frequencies following a atan

The range of steps 0 .. nSteps is mapped to 0 .. Pi or to 0 .. 2Pi
			
  double atanPiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double k = (fStop - fStart)/atan(PI);
    double fNext = fStart + k * atan(PI/nSteps * stepNbr);
    return fNext;
  }			
			
			
  double atan2PiScale(int stepNbr, double fStart, double fStop,int nSteps)
  {
    double k = (fStop - fStart)/atan(TWO_PI);
    double fNext = fStart + k * atan(TWO_PI/nSteps * stepNbr);
    return fNext;
  }			
			

Summary

We have implemented a universal chirp function that can generate frequencies that follow a mathemathical rule. For comparison, we call all generators and plot the generated frequencies all together in one diagram. The frequency range is swept in 5 steps.

The first diagram shows the frequencies when fStart is smaller than fStop so that all chirps start with increasing frequencies.

Frequencies generated by all 8 generators starting with increasing frequencies
generators

The second diagram shows the frequencies when fStart and fStop are swapped so that all chirps start with decreasing frequencies.

Frequencies generated by all 8 generators starting with decreasing frequencies
generators

Addendum 1

I had always been fascinated by the function sinc(x) = sin(x)/x with its increasing and decreasing waveform. How does an artificial bird sound whose beep starts with an increasing vibrato, changes into a sharp beep and fades out with a decreasing vibrato? But remember, it's not the volume, it's the pitch.

sinc(x) = sin(x) / x
sincX

To which number range should I map my number of frequency steps? -5π..+5π or -3π..+3π maybe also 0..7π or -4π..0? Obviously, I need another parameter in my generator function to specify the multiples of π.

			
  typedef double (*FreqGenSinc)(int stepNbr, double fStart, double fStop,int nSteps, int nPi);
			

However, this function has a different signature than the previous definition and compiling the chirp function with a generator with this definition will result in an error message. However, thanks to polymorphism in C++ we can define a second chirp function and thus avoid the problem (see chirpmaker.h).

A generator spanning the range -nπ .. +nπ

			
  double sincScaleNpi_Npi(int stepNbr, double fStart, double fStop,int nSteps, int nPi)
  {
    double halfRange = nPi * PI;
    double range = 2 * halfRange;

    auto sinc = [](double x)
    {
      return fabs(x) < 0.001 ? 1.0 : sin(x)/x;
    };

    double fa = (fStop - fStart);  // max. frequency swing
    double k = range / nSteps;
    double fNext = fStart + fa * sinc(k * stepNbr - halfRange); // get next frequency
    return fNext;
  }			
			
sincScaleNpi_Npi with fStart < fStop
sincNpi-Npi
sincScaleNpi_Npi with fStart > fStop
sincNpi-Npiswapped

A generator spanning the range -nπ .. 0

			
  double sincScaleNpi_0(int stepNbr, double fStart, double fStop,int nSteps, int nPi)
  {
    double range = nPi * PI;

    auto sinc = [](double x)
    {
      return fabs(x) < 0.001 ? 1.0 : sin(x)/x;
    };

    double fa = (fStop - fStart);  // max. frequency swing
    double k = range / nSteps;
    double fNext = fStart + fa * sinc(k * stepNbr - range); // get next frequency
    return fNext;
  }						
			
sincScaleNpi_0 with fStart < fStop
sincNpi-0
sincScaleNpi_0 with fStart > fStop
sincNpi-0swapped

A generator spanning the range 0 .. nπ

			
  double sincScale0_Npi(int stepNbr, double fStart, double fStop, int nSteps, int nPi)
  {
    double range = nPi * PI;

    auto sinc = [](double x)
    {
      return fabs(x) < 0.001 ? 1.0 : sin(x)/x;
    };
    double swap = fStart; fStart = fStop; fStop = swap;

    double fa = (fStop - fStart);  // max. frequency swing
    double k = range / nSteps;
    double fNext = fStart + fa * sinc(k * stepNbr); // get next frequency
    return fNext;
  }			
			
sincScale0_Npi with fStart < fStop
sinc0_Npi
sincScale0_Npi with fStart > fStop
sinc0_Npiswapped

Addendum 2

The presented chirps change the frequency according to mathematical laws. But how does the sound change if the frequency remains the same and only the duty cycle of the square wave changes? Let's try it and write a new function:

Phaser

			
  void phaser(uint32_t freq, int nPeriods, 
              int dutyStart, int dutyEnd, int nChirps, uint32_t msPause)
  {
    uint32_t p = 1000000/freq;

    auto buz = [](uint32_t usTon, uint32_t usToff){
      digitalWrite(PIN_BUZZER, HIGH);
      delayMicroseconds(usTon);
      digitalWrite(PIN_BUZZER, LOW);
      delayMicroseconds(usToff);};

    for (int n = 0; n < nChirps; n++) // output nChirps
    {
      for (int d = dutyStart; d <= dutyEnd; d++)
      {
        uint32_t tOn  = p * d / 100;
        uint32_t tOff = p - tOn;
        for (int n = 0; n < nPeriods; n++) buz(tOn, tOff);
      } 
      delay(msPause);
    }    
  }			
			

If we call the function with  phaser(500, 20, 1, 99, 1, 2000) , we hear a tone of 500 Hz whose duty cycle is changed from 1 ... 99 %. Each of the 99 tones consists of 20 periods. If we listen carefully, we notice how the timbre of the selected frequency and also the volume changes. This is due to the fact that as the duty cycle changes, the composition of the harmonic frequencies of the sound changes.

5. The final Refinement

With all this knowledge, I implemented various "birds" in the program that are called to sing in random order. Now I wanted to let the birds whistle in another program. Soon I realized that my few lines of code would get lost in the extensive code of the sound generation. Therefore I put this part into a universal class Chirpmaker. Now the application of the sound functions was reduced to a few lines:

			
  #include "Chirpmaker.h"

  const uint8_t PIN_BUZZER = GPIO_NUM_4;
  Chirpmaker cm(PIN_BUZZER); // Creates an object that emits sounds at pin 4

  void setup()
  {
    // Your initialization code
    cm.signet();  // Call a signet to signal the end of setup
  }
  
  void loop() 
  {
    cm.phoneCall(7);
    cm.birdConcert(3000);
    cm.chirp(1000, 3000, 70, 100, 5, sincScaleNpi_Npi, 50, 500);
  }  
			

6. Program Code

I hope you enjoyed this little excursion into sound generation. Maybe a reader programs an alarm clock which greets him in the morning with a bird concert.

Interested? Please download the entire program code. The zip-file contains the complete PlatformIO project.

My programming environment is not the native Arduino™ IDE but PlatformIO™ on top of Microsoft's Visual Studio Code™. This combination offers many advantages and allows a much better structuring of the code into several modules especially when we adopt The Object Oriented way.