16 Individual Buzzers

Hello,

I've been attempting to synthesize a voice using a MIDI output. I want it to use many monophonic tones to form a single, polyphonic voice. Here's an example of the type of audio illusion I expect: Speaking Piano - Now with (somewhat decent) captions! - YouTube

So far, I have been able to transfer the raw audio files into midi. Then transfer them into text with 16 individual channels of notes. I have been able to code my Arduino MEGA 2560 to read these txt files off a micro SD card. The Arduino then logs the notes to be played at the given time in an array.

I need the Arduino to direct the 16 separate buzzers to play their individual frequencies at the same time.

I have tried using tone(), but it does not support the use of so many buzzers at the same time.

I also tried timing the HIGH/LOW state changes of each digital pin using micros(), but that was not accurate enough to create the correct notes. It takes too long to scan through all of the channels, testing which buzzer needs to change states.

I need guidance on what hardware/software is needed to make this process run smoothly.

Thank you!

Have a look at these 16 channel PWM drivers

OP's link: https://www.youtube.com/watch?v=muCPjK4nGY4
I didn't get much from the video, I don't speak German, but it was interesting to hear a piano make those sounds. With the help of the English subtitles, I could understand the "words" made by the piano, but if I closed my eyes, I could not understand them, or hardly any.

I have tried using tone(), but it does not support the use of so many buzzers at the same time.

Yes, the mega2560 chip has 14 or 15 PWM pins, but only 6 timers. Two or three PWM pins share each timer. This means the pins that share the same timer can be set to different duty cycles, but share the same frequency. It is frequency, not duty cycle, that is important for tone().

TimMJN:
Have a look at these 16 channel PWM drivers

I don't think those will be suitable. I think all 16 channels share the same timer, so only a single frequency can be generated.

PaulRB:
I don't think those will be suitable. I think all 16 channels share the same timer, so only a single frequency can be generated.

My bad, you are right. OP needs 16 frequencies, not 16 pulse widths

PaulRB:
With the help of the English subtitles, I could understand the "words" made by the piano, but if I closed my eyes, I could not understand them, or hardly any.

haha yeah. I plan on using a small LCD display to display the subtitles as directed by the SD card.

Thanks for the replies!

So it looks like I need a chip with 16 separate timers...

Maybe post your attempt to do it with micros(). We may be able to improve the performance.

With the piano in the video, each key/string has a fixed note. For your 16 buzzers, will each have a fixed note, or will each be capable of playing any note at any time? How many buzzers will sound at the same time?

PaulRB:
For your 16 buzzers, will each have a fixed note, or will each be capable of playing any note at any time? How many buzzers will sound at the same time?

Each buzzer needs to be able to sound any note. At some points, all 16 buzzers will be playing different notes at one time.

Here's my attempt with micros(). The really important stuff is during the second to last function (Buzz) and the while loop within the main loop *I apologise if my syntax is wack:

#include <SPI.h>
#include <SD.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

//SD STUFF
#define MILLIS_PER_BEAT 12 //Measured in midi units rather than usual music time signature
char fileName[] = "test.txt";

//this is taken from parseLinetest.ino
char delimiter[] = " "; //how each integer in the file is seperated
char* parsePosition; //ths is a "pointer" that directs the code to a position within the rawSdInput variable

int sdNoteInput[] = {0,0,0}; //the input of one line of instructions given by (time,channel, note or message) channel 0 is for messages

//these inputs have to be arrays because each character is read as a byte
unsigned long inputTime;
byte channel;
String input;

File myFile;


//LCD STUFF
#define BACKLIGHT_PIN     13
char lcdText;
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address


//BUZZER STUFF
#define NUMBER_OF_BUZZERS 16
const byte buzzer[] = {2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18}; //the digital pins of each buzzer
int buzzerFrequency[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //records the frequency that all buzzers are playing
long buzzerLastPeek[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //records the last time each buzzer switched between its LOW/High states
int buzzerState[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //records if the pin was last set high or low

#define NOTE_OFFSET -2 //needs to equal 10 +/- 12*(offset octaves)


//GENERAL STUFF
unsigned long loopStart; //the time the current loop started
unsigned long loopTime = 0; //the time during a loop (arbitrary to how many loops have occurred)
unsigned long microLoopTime = 0; //used for frequency calculations
unsigned long startTimer;

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect.
  }

  Serial.print("Initializing SD card...");

  if (!SD.begin(4)) {
    Serial.println("initialization failed!");
    while (1);
  }
  Serial.println("initialization done.");

  // Switch on the backlight
  pinMode ( BACKLIGHT_PIN, OUTPUT );
  digitalWrite ( BACKLIGHT_PIN, HIGH );

  lcd.begin(20,4); // initialize the lcd
  lcd.setCursor(7,1); // go home (,Buddy. I work alone.)
  lcd.print("START!");
  //tone(2,262,500);
  delay(1500);
  lcd.clear();

  //set all buzzer pins to output
  for (byte i=0; i < NUMBER_OF_BUZZERS; i++){
    pinMode(buzzer[i],OUTPUT);
    digitalWrite(buzzer[i],LOW);
  }

}

void loop(){
  
  loopStart = millis();
  
  //Open the file for reading:
  myFile = SD.open(fileName);
  if (myFile) {
    Serial.println(fileName);

    while (myFile.available()) {
      
      microLoopTime = micros() - (loopStart*1000);
      loopTime = microLoopTime/1000; //the current time is relative to the start time
      
      if(1){ //WILL TEST IF IT IS A NOTE OR A MESSAGE
        ReadNoteInput();
        
        inputTime = sdNoteInput[0];

        channel = sdNoteInput[1];
        float freq = KeyToFrequency(sdNoteInput[2]);

/////////////////////////////////////////////////////////////////////////////this 'while' is looped for at least 12 milliseconds at a time
        while (sdNoteInput[0] > loopTime/MILLIS_PER_BEAT){
           microLoopTime = micros() - (loopStart*1000);
           loopTime = microLoopTime/1000;
           
           Buzz(microLoopTime);                                                    ///the buzz function is called
           //Serial.println(micros()-startTimer);
           //startTimer = micros();
        }
/////////////////////////////////////////////////////////////////////////////end 'while'
        lcd.clear();
        lcd.setCursor(2,1);
        lcd.print("Time: ");
        lcd.setCursor(8,1);
        lcd.print(loopTime);
      
        lcd.setCursor(2,2);
        lcd.print("Note: ");
        lcd.setCursor(8,2);
        lcd.print(buzzerFrequency[0]);
  
        if (buzzerFrequency[channel-1] == freq){ //if this frequency is being played by this channel already, turn it off
          buzzerFrequency[channel-1] = 0;
          digitalWrite(buzzer[channel-1],LOW); //when off, the buzzer pin is set to low
          
        } else{
          buzzerFrequency[channel-1] = freq;
        }
      }

      //THIS IS THE PART WHERE THEY BEEP AWAY
      microLoopTime = micros() - (loopStart*1000);
      Buzz(microLoopTime); //buzz the beepers with the input of the current time to compare to
      
      for (byte j =1; j<4;j++){ //clear the recoded sd input
        sdNoteInput[j] = 0;
      }
    }
    
  }else{
    Serial.println("couldn't open file");
    while (1);
  }

  myFile.close(); //close the file
  delay(1500);
}

void ReadNoteInput(){

  String inputString = myFile.readStringUntil('\n'); //read the line and store it as a string
  int stringLength = inputString.length();
  char rawSdInput[stringLength+1];
  
  inputString.toCharArray(rawSdInput,stringLength+1); //convert the string into char type of the same length as the string it is reading
  
  parsePosition = strtok(rawSdInput, delimiter);
  long value; //the value of the field of the array we are on

  Serial.print("input array: ");
  for (byte i=0;i<3;i++){ //repeat for each array field
    value = atoi(parsePosition);
    sdNoteInput[i] = value;
    Serial.print(value);
    Serial.print(", ");
    parsePosition = strtok(NULL, delimiter); //move to next position. If we read a seperator don't store it as a value
  }
  Serial.println("");
}

void Buzz(long currentMicroTime){

  for (byte i=0; i<3;i++){
  
    //if this buzzer is set to be playing and the last time this buzzer switched states was longer than its frequency then switch its state
    float nextBeep = buzzerLastPeek[i] + (buzzerFrequency[i] / 2); //this is when this buzzer should beep. the frequency is divided in half to find the high/low time of the wave
    if ((buzzerFrequency[i] > 0) && (nextBeep < currentMicroTime)){
      //switch the buzzer from whatever state it is currently in
      if (buzzerState[i] < 0){
        digitalWrite(buzzer[i],HIGH);
        buzzerState[i] = 1;
      } else {
        digitalWrite(buzzer[i],LOW);
        buzzerState[i] = -1;
      }
      buzzerLastPeek[channel-1] = microLoopTime; //log now as the time the buzzer compares to to time beeps
    }
  }

}

long KeyToFrequency(float key){
  key += NOTE_OFFSET; //shift the note
  key = double((key-49)/12);
  Serial.print("key power: ");
  Serial.println(key);
  //this gives how many waves per second
  float convertedFreq = double(pow(2,key))*440; //https://en.wikipedia.org/wiki/Piano_key_frequencies

  //return how many microseconds between each wave
  return double(1/convertedFreq)*1000000;
}

If you are curious, here's what the SD card txt file looks like:

0 1 60
100 2 70
120 1 60
200 2 70
500 16 65
540 14 53
600 16 65
600 14 53

Hopefully, this mess was of some help

Ok, might take me a little time to get my head around it...

I did notice at first reading a lot of *1000 and /1000 going on. Can those be removed, keeping everything in microseconds?

Avoid using float variables, unless you absolutely have to, they are very slow. For example nextBeep. You can use integer maths without loosing precision if you do it right. For example

  //this gives how many waves per second
  long convertedFreq = (1 << (key-1))*440; //https://en.wikipedia.org/wiki/Piano_key_frequencies

  //return how many microseconds between each wave
  return 1000000/convertedFreq;

Keep variable sizes down to a minimum. Use byte or char instead of int if the values are only ever small, eg. buzzerState.

Use bit shifting rather than multiplying and especially when dividing by 2 or any power of 2. For example (buzzerFrequency[ i ] >> 1) instead of (buzzerFrequency[ i ] / 2)