Let's dance to the sound of piezos

Hi everyone.

As I wrote in the last post, I found out that vibration motors do not have much “expressive power”, not as much as I thought anyway. For this reason, I am switching to piezoelectric transducers, which offer a greater range of expression, are easy to control and wire up, and consume almost nothing.

At first, I wanted the piezos to produce a fixed pitch, and vary the volume of the sound as a function of the distance of the obstacles detected by the ultrasound sensors. Turns out, it is not that easy to do so with a piezoelectric transducer, as it would be with a buzzer.

Then I tried to vary the pitch of the produced sounds as a function of the distance of the obstacles, but I didn’t like the result. I thought it would be quite annoying to listen to this continuous change of pitch while using the glasses for some time. The good thing is that it worked quite well as a theremin!

(Sorry about the noise, but piezo transducers produce very low sounds)

In the end, I decided to keep volume and pitch fixed, and vary the beeping rhythm, like in parking sensors:

The circuit here is quite simple – the mess you can see in the video is actually just because I was too lazy to tidy everything up. Each ultrasonic sensor is connected to Ground, to the 5V pin, and to a single digital pin (one for each sensor). In fact, with the NewPing library, you can use a single pin to ping the sensor, and to receive the reading.
The piezo transducers were just connected each to a digital pin and to ground. I think I will add some resistance to adjust the volume later on, and maybe a linear potentiometer to control the volume manually.

Now I can guide you through the code I’ve used here.
I’ll explain the key functions and then show the complete code, I’m assuming you already know the basics for C coding.

void loop(){

curr_millis=millis(); //take time passed from arduino start

// if a millisecond has passed
if (curr_millis != prev_millis){
debugprint("starting.");
debugprintln(curr_millis);

debugprint("\n");
// ping ultrasound sensors and retrieve distances
ping_us_sensors(curr_millis);
l_read_array[reading_n] = distances[0]; //reading_n is incremented only when both sensors have been pinged
r_read_array[reading_n] = distances[1];

debugprint("now returning to loop \n");
debugprint("distances are: \n");
debugprintln(distances[0]);
debugprintln(distances[1]);

// average last READINGS readings
int l_sum = 0;
int r_sum = 0;
for (int i=0; i<READINGS; i++){
l_sum += l_read_array[i];
r_sum += r_read_array[i];
}
float l_ave_distance = l_sum/float(READINGS) - MIN_DISTANCE;
float r_ave_distance = r_sum/float(READINGS) - MIN_DISTANCE;
if(l_ave_distance<0) l_ave_distance=0; // make sure not to have negative distances
if(r_ave_distance<0) r_ave_distance=0;

debugprint("and average distances:\n");
debugprintln(r_ave_distance);
debugprintln(l_ave_distance);
debugprint("\n");

// make piezos beep depending on the distances of the obstacles
pulse_piezos(curr_millis, l_ave_distance, r_ave_distance);
prev_millis = curr_millis;
}
}

This is the function which is called continuously as the Arduino is turned on. What it does is:
– Ping the US sensors.
– Average out the last READINGS readings, where READING is a number defined before.
– Shift the distance down of MIN_DISTANCE. This is used so that the piezos can already beep at the highest speed when an obstacle is presented at MIN_DISTANCE.
– Make the piezos beep.
The first and last actions in this list are actually carried out by two functions which are external to the loop. Let’s have a look at the function which pings the sensors.

// ping the us sensors and retrieve the distances from obstacles
void ping_us_sensors(long curr_millis){
  
  debugprint("Entered ping_us_sensor fn\n");
  debugprint("time passed from last ping:");
  debugprintln(curr_millis - last_ping_time);
  
  // time in microseconds it takes for the ultrasonic sound wave
  // to travel from the ultrasonic sensor, hit an obstacle, and return
  unsigned int uS; 
  
  // some time must pass between a ping and the next one
  if(curr_millis - last_ping_time >= PING_INTERVAL){
    
    // if readings_n is equal to READINGS -> readings_n = 0
    // used to populate reading arrays 
    reading_n%=READINGS;
    
    // used to ping sequentially all US sensors (in this case, 2)
    pinger_id++;
    pinger_id %= US_NUM;
    
    debugprint("Pinging sensor number \n");
    debugprintln(pinger_id);
    
    uS = sonar[pinger_id].ping();
    last_ping_time = curr_millis;
    
    // compute distance
    distances[pinger_id] = uS/US_ROUNDTRIP_CM;
    // when the distance is too much, the sensor sends a 0
    // better to just set that to MAX_DISTANCE
    if (distances[pinger_id]==0) distances[pinger_id]=MAX_DISTANCE;
    
    debugprint("Distance: \n");
    debugprintln(distances[pinger_id]);
    
    if(pinger_id%US_NUM == 0)reading_n++;
  }
  
    debugprint("\n");
}

This function pings the sensors one at a time, retrieving the distance for each one. To do that, it makes sure that enough time has passed since the last ping (PING_INTERVAL).
Also, it both the US have been pinged, it updates the reading_n variable. That variable is used in the loop function to populate the reading arrays.

Now for the function that makes the piezos beep.

// This function makes the piezo beep based on the distance of the obstacles
void beep_piezos(long curr_millis, long l_ave_distance, long r_ave_distance) {
  
  int r_interval, l_interval;
  // from the distance, assign beep intervals for the piezo transducers
  l_interval = int(l_ave_distance*DIST_TO_BEEP_MULTIPLIER);
  r_interval = int(r_ave_distance*DIST_TO_BEEP_MULTIPLIER);
  
  debugprint("entering beep_piezos fn.\n");
  debugprint("time passed from last right beep:");
  debugprintln(curr_millis - r_last_beep);
  debugprint("time passed from last left beep:");
  debugprintln(curr_millis - l_last_beep);
  
  // if enough time has passed since the last beep of the right piezo,
  // a new beep is started in the right piezo
  if(curr_millis - r_last_beep > r_interval){
    debugprint("right beep sent\n");
    r_last_beep = curr_millis;
  }
  // controls the actual wave that constitues the beep
  if(curr_millis - r_last_beep < BEEP_DURATION){
    if(r_phase == R_BEEP_LOW_RATIO){
      digitalWrite(R_PIEZO, HIGH);
      r_phase=0;
    }
    else{
      digitalWrite(R_PIEZO,LOW);
      r_phase++;
    }
  }
  
  // if enough time has passed since the last beep of the left piezo,
  // a new beep is started in the left piezo
  if(curr_millis - l_last_beep > l_interval){
    debugprint("left beep sent\n");
    l_last_beep = curr_millis;
  }
  // controls the actual wave that constitues the beep
  if(curr_millis - l_last_beep < BEEP_DURATION){
    if(l_phase == L_BEEP_LOW_RATIO){
      digitalWrite(L_PIEZO, HIGH);
      l_phase=0;
    }
    else{
      digitalWrite(L_PIEZO,LOW);
      l_phase++;
    }
  }
  
  debugprint("\n");    
}

This function makes the piezo beep more or less frequently depending on the distance of the left and right obstacles.
When the right time interval has passed, the function starts a beep. A beep here is a square wave sent to a piezo for a certain PULSE_DURATION time.

The pitch of the beep sound is controlled by the R and L_BEEP_LOW_RATIO. Given that this function is called by the loop every 1 ms, if for example L_BEEP_LOW_RATIO = 2, then a series of HIGH – LOW – LOW is sent, where each signal lasts a millisecond.
The frequency of the resulting square wave is 1/3 of 1000 Hz (again, because the function is called every millisecond, 1000 times a second).

Finally the complete code, hopefully it should be easy enough now to read through.

#include

// pins
#define R_US 11 // right ultrasonic sensor pin
#define L_US 8  // left ultrasonic sensor pin
#define R_PIEZO 3 // right piezo pin
#define L_PIEZO 5 // left piezo pin

// parameters
#define US_NUM 2 // total number of US sensors
#define READINGS 4 // US readings to average
#define MIN_DISTANCE 20 // distance in cm which make the piezo beep like crazy
#define MAX_DISTANCE 300 // maximum distance for a valid reading from the US sensors
#define PING_INTERVAL 40 // interval between pings
#define BEEP_DURATION 30 // how much a beep lasts in ms
#define DIST_TO_BEEP_MULTIPLIER 5 // to adjust the beeping relative to distance
#define L_BEEP_LOW_RATIO 2 // this regulates the frequency of the note for the left piezo
#define R_BEEP_LOW_RATIO 1 //

// the next lines turn debugging on or off
//#define DEBUG
#ifdef DEBUG
  #define debugbegin(x) Serial.begin(x)
  #define debugprint(x) Serial.print(x)
  #define debugprintln(x) Serial.println(x)
#else
  #define debugbegin(x)
  #define debugprint(x)
  #define debugprintln(x)
#endif /*DEBUG*/


NewPing sonar[US_NUM] = {
  NewPing(R_US, R_US, MAX_DISTANCE), // NewPing setup.
  NewPing(L_US, L_US, MAX_DISTANCE) // NewPing setup.
};

// variables used to keep track of time
long curr_millis=0;
long prev_millis=0; 

// variables used to keep track of the phase of the square wave for each piezo
int r_phase = 0;
int l_phase = 0;

// keep track of the last time the piezos beeped
long r_last_beep = 0;
long l_last_beep = 0;


int reading_n = 0; // used to update l_read_array/right arrays
int pinger_id = 0; // used to alternate pinging between sensors
int l_read_array[READINGS]; // used to store and average readings
int r_read_array[READINGS]; 
long last_ping_time = 0; // last time a US was pinged

int distances[US_NUM]; // stores the distances retrieved by the US sensors

// This function makes the piezo beep based on the distance of the obstacles
void beep_piezos(long curr_millis, long l_ave_distance, long r_ave_distance) {
  
  int r_interval, l_interval;
  // from the distance, assign beep intervals for the piezo transducers
  l_interval = int(l_ave_distance*DIST_TO_BEEP_MULTIPLIER);
  r_interval = int(r_ave_distance*DIST_TO_BEEP_MULTIPLIER);
  
  debugprint("entering beep_piezos fn.\n");
  debugprint("time passed from last right beep:");
  debugprintln(curr_millis - r_last_beep);
  debugprint("time passed from last left beep:");
  debugprintln(curr_millis - l_last_beep);
  
  // if enough time has passed since the last beep of the right piezo,
  // a new beep is started in the right piezo
  if(curr_millis - r_last_beep > r_interval){
    debugprint("right beep sent\n");
    r_last_beep = curr_millis;
  }
  // controls the actual wave that constitues the beep
  if(curr_millis - r_last_beep < BEEP_DURATION){
    if(r_phase == R_BEEP_LOW_RATIO){
      digitalWrite(R_PIEZO, HIGH);
      r_phase=0;
    }
    else{
      digitalWrite(R_PIEZO,LOW);
      r_phase++;
    }
  }
  
  // if enough time has passed since the last beep of the left piezo,
  // a new beep is started in the left piezo
  if(curr_millis - l_last_beep > l_interval){
    debugprint("left beep sent\n");
    l_last_beep = curr_millis;
  }
  // controls the actual wave that constitues the beep
  if(curr_millis - l_last_beep < BEEP_DURATION){
    if(l_phase == L_BEEP_LOW_RATIO){
      digitalWrite(L_PIEZO, HIGH);
      l_phase=0;
    }
    else{
      digitalWrite(L_PIEZO,LOW);
      l_phase++;
    }
  }
  
  debugprint("\n");
    
}

// ping the us sensors and retrieve the distances from obstacles
void ping_us_sensors(long curr_millis){
  
  debugprint("Entered ping_us_sensor fn\n");
  debugprint("time passed from last ping:");
  debugprintln(curr_millis - last_ping_time);
  
  // time in microseconds it takes for the ultrasonic sound wave
  // to travel from the ultrasonic sensor, hit an obstacle, and return
  unsigned int uS; 
  
  // some time must pass between a ping and the next one
  if(curr_millis - last_ping_time >= PING_INTERVAL){
    reading_n%=READINGS; //used to populate the distance arrays
    
    // used to ping sequentially all US sensors (in this case, 2)
    pinger_id++;
    pinger_id %= US_NUM;
    
    debugprint("Pinging sensor number \n");
    debugprintln(pinger_id);
    
    uS = sonar[pinger_id].ping();
    last_ping_time = curr_millis;
    
    // compute distance
    distances[pinger_id] = uS/US_ROUNDTRIP_CM;
    // when the distance is too much, the sensor sends a 0
    // better to just set that to MAX_DISTANCE
    if (distances[pinger_id]==0) distances[pinger_id]=MAX_DISTANCE;
    
    debugprint("Distance: \n");
    debugprintln(distances[pinger_id]);
    
    if(pinger_id%US_NUM == 0)reading_n++;
  }
  
    debugprint("\n");
}
  

void setup() {
  
  debugbegin(115200);
  
  // set piezo pins to OUTPUT
  pinMode(R_PIEZO,OUTPUT);
  pinMode(L_PIEZO,OUTPUT);
  
  //initializing the arrays
  for (int i=0; i<READINGS; i++){
    l_read_array[i] = 0;
    r_read_array[i] = 0;
  }
}

void loop(){
  
  curr_millis=millis(); //take time passed from arduino start
    
  // if a millisecond has passed
  if (curr_millis != prev_millis){
    debugprint("starting.");
    debugprintln(curr_millis);

    debugprint("\n");    
    // ping ultrasound sensors and retrieve distances
    ping_us_sensors(curr_millis);
    l_read_array[reading_n] = distances[0]; //reading_n is incremented only when both sensors have been pinged 
    r_read_array[reading_n] = distances[1];
    
    debugprint("now returning to loop \n");
    debugprint("distances are: \n");
    debugprintln(distances[0]);
    debugprintln(distances[1]);
    
    
    // average last READINGS readings
    int l_sum = 0;
    int r_sum = 0;
    for (int i=0; i<READINGS; i++){
      l_sum += l_read_array[i];
      r_sum += r_read_array[i];
    }
    float l_ave_distance = l_sum/float(READINGS) - MIN_DISTANCE;
    float r_ave_distance = r_sum/float(READINGS) - MIN_DISTANCE;
    if(l_ave_distance<0) l_ave_distance=0; // make sure not to have negative distances
    if(r_ave_distance<0) r_ave_distance=0;
    
    debugprint("and average distances:\n");
    debugprintln(r_ave_distance);
    debugprintln(l_ave_distance);
    debugprint("\n");
      
    // make piezos beep depending on the distances of the obstacles
    beep_piezos(curr_millis, l_ave_distance, r_ave_distance);
    prev_millis = curr_millis;
  }
}

OK, that’s it for this post, hope you enjoyed it! For the next post, I will probably implement a third sensor to sense low-frontal objects (but I want to use only two piezo transducers).

I’ll keep you posted!

BAT NAVIGATOR
arduino bat navigator

This conversation is happening on Twitter

Leave a comment

Use the hashtag #LetsDanceTo0714, and mention me with @GCarmantini if you want a direct reply.