In this small Arduino side project, I explain how I made to create the effect of lighting 7 bowls detecting the sound frequency of each bowl with a microphone and using different colours to illuminate it.
Bowls are widely used in sound healing treatments (if they can be called treatments) and they sound really really loud! Wooww!
I started detecting the sound intensity with the microphone, setting a threshold with a potentiometer… but when you play more than 1 bowl at the same time, everything is mixed up!! So, another strategy must be used.
Of course with the nice spectrum analyzer I had in my lab, each note or bowl can be pretty straightforward detected… but this is an expensive option. So we have to move to Arduino &Co. microcontrollers.
The Bowls
I have 7 bowls corresponding with the 7 notes corresponding with the natural C scale: C, D, E, F, G, A and B. Although, they are tuned in 432Hz and not in the regular 440Hz tuning.
The goal is to detect when each bowl is played and light it. As many bowls can play at the same time, the detection must be based on the frequency of the sound and not due to the volumen.
The frequency detection is based on a simplified Fast Fourier Transform (FFT) algorithm… I still remember when I had to program the butterflies algorithm at college, nice to do at least once in your life, but nothing funny to enjoy with.
Hardware
Hardware setup
Two implementations are possible:
→Centralized: A central Arduino detects the note and lights each led. So each bowl is wired to light the led below.
We would need 1 Arduino + 1 mic + 7 mosfets + 7 power led
→Distributed: Each bowl has its own Arduino, and each of them detects its note.
We would need 7 Arduino + 7 mics + 7 mosfets + 7 power led
The second implementation utilizes much more hardware, but with the advantage of being totally wireless. No wires between bowls. Each bowl has its own battery, control and mic placed under the bowl. It makes it visually nice and in harmony with the atmosphere.
Mic
A standard mic PCB I found on the internet. The mic I use does not have an amplifier stage, although some of them do. Any you find should work, like the MAX4466 among others.
The wiring is easy: the signal must be attached to Analog0 pin (or any you define in your code). And then the supply voltage 3.3V and GND.
Power Stage
Mosfet power stage is needed to power the leds. I used the classical 3W star leds in different colours. I supply them with lithium batteries.
To simplify, and because I could get them easily, I used commercial MOSFET modules, although it can be done with regular discrete MOSFETs.
Some of this MOSFETs modules had galvanic isolation, which I bridge for simplicity.
Arduino
I tested with Arduino UNO and Nano. Mega should also work fine. The FHT library need a bit of computational power and memory to process the simplified FFT.
The board needs at least 2kb of RAM
Code
For coding, I reused all the useful codes I could find at that time on the internet. Code Reuse, therefore I posted here my code available for everyone who needs it!
It is mostly based on the FHT library from Open Music Labs and the project of ArduinoSoundMeter in GitHub.
// //////////////////////////////////////////////////////////////////////////// // Project: Singing bowls illumination // Description: It turns on a different colour depending on which Chakra is played // Engineer: Alberto Lopez // More: https://misCircuitos.com // Date: 16 - nov - 2022 Chiang Mai (Thailand) /////////////////////////////////////////////////////////////////////////////// #define DEBUG_MODE //Comment when finish debuging //---------------------------------------------------------------------------// int in[128]; byte NoteV[13]={8,23,40,57,76,96,116,138,162,187,213,241,255}; //data for note detection based on frequency //byte NoteV[13]={8,23,40,57,76,96,116,138,162,187,213,241,255}; //data for note detection based on frequency 440Hz tuning float f_peaks[5]; // top 5 frequencies peaks in descending order // pins const int Mic_pin = A0; // change as per Microphone pin const int red_pin = 2; //C const int orange_pin = 3; const int yellow_pin =4; //E const int green_pin = 5; const int blue_pin = 6; //G const int blueDark_pin = 7; const int purple_pin = 8; //B const int MINIMUM_SOUND = 290; //minimim sound level threshold to start lighting //variables int note; //---------------------------------------------------------------------------// void setup() { Serial.begin(9600); Serial.println("Singing Bowls correctly initialized"); Serial.println("v1.0 17-nov-2022 Alberto Lopez: misCircuitos.com \n"); //Declare the LED pins as output pinMode(red_pin, OUTPUT); pinMode(orange_pin, OUTPUT); pinMode(yellow_pin, OUTPUT); pinMode(green_pin, OUTPUT); pinMode(blue_pin, OUTPUT); pinMode(blueDark_pin, OUTPUT); pinMode(purple_pin, OUTPUT); //testLED(); //Debug LED and light all } void loop() { //delay(10); //slow down the loop //testLED(); //debug mic //int lecturaMic = analogRead(Mic_pin); //Serial.print("volumen:"); Serial.println(lecturaMic); note = ReadNote(); resetLED(); switch(note) { case 0: //RED //Serial.println("The Bowl number C"); digitalWrite(red_pin,HIGH); break; case 2: //ORANGE //Serial.println("The Bowl number D"); digitalWrite(orange_pin,HIGH); break; case 4: //YELLOW //Serial.println("The Bowl number E"); digitalWrite(yellow_pin,HIGH); break; case 5: //GREEN //Serial.println("The Bowl number F"); digitalWrite(green_pin,HIGH); break; case 7: //LIGHT BLUE //Serial.println("The Bowl number G"); digitalWrite(blue_pin,HIGH); break; case 9: //DARK BLUE //Serial.println("The Bowl number A"); digitalWrite(blueDark_pin,HIGH); break; case 11: //PURPLE //Serial.println("The Bowl number B"); digitalWrite(purple_pin,HIGH); break; default: resetLED(); } #ifdef DEBUG_MODE switch(note) { case 0: Serial.println("The Bowl number C"); break; case 2: Serial.println("The Bowl number D"); break; case 4: Serial.println("The Bowl number E"); break; case 5: Serial.println("The Bowl number F"); break; case 7: Serial.println("The Bowl number G"); break; case 9: Serial.println("The Bowl number A"); break; case 11: Serial.println("The Bowl number B"); break; } #endif }//close loop void resetLED() { digitalWrite(red_pin,LOW); digitalWrite(orange_pin,LOW); digitalWrite(yellow_pin,LOW); digitalWrite(green_pin,LOW); digitalWrite(blue_pin,LOW); digitalWrite(blueDark_pin,LOW); digitalWrite(purple_pin,LOW); } void testLED(){ Serial.println("LED test debug mode"); int speed = 2000; while(1){ resetLED(); digitalWrite(red_pin,HIGH); Serial.println("RED"); delay(speed); digitalWrite(orange_pin,HIGH); Serial.println("ORANGE"); delay(speed); digitalWrite(yellow_pin,HIGH); Serial.println("YELLOW"); delay(speed); digitalWrite(green_pin,HIGH); Serial.println("GREEN"); delay(speed); digitalWrite(blue_pin,HIGH); Serial.println("BLUE"); delay(speed); digitalWrite(blueDark_pin,HIGH); Serial.println("BLUEDARK"); delay(speed); digitalWrite(purple_pin,HIGH); delay(speed); } } //-----------------------------Tone Detection Function----------------------------------------------// // This code won't work for any board having RAM less than 2kb, int ReadNote() { long unsigned int a1,b,a2; float a; float sum1=0,sum2=0; float sampling; a1=micros(); for(int i=0;i<128;i++) //DATA ACQUISITION { a=analogRead(Mic_pin)-500; //rough zero shift //utilising time between two sample for windowing & amplitude calculation sum1=sum1+a; //to average value sum2=sum2+a*a; // to RMS value a=a*(sin(i*3.14/128)*sin(i*3.14/128)); // Hann window in[i]=10*a; // scaling for float to int conversion delayMicroseconds(195); // based on operation frequency range } b=micros(); sum1=sum1/128; // Average amplitude sum2=sqrt(sum2/128); // RMS sampling= 128000000/(b-a1); // real time sampling frequency //for very low or no amplitude, this code wont start //it takes very small aplitude of sound to initiate for value sum2-sum1>3, //change sum2-sum1 threshold based on requirement if(sum2-sum1>3){ //THRESHOLD TO START FFT(128,sampling); //EasyFFT based optimised FFT code, //this code updates f_peaks array with 5 most dominent frequency in descending order for(int i=0;i<12;i++){in[i]=0;} // utilising in[] array for further calculation int j=0,k=0; //below loop will convert frequency value to note for(int i=0; i<5;i++) { if(f_peaks[i]>1040){f_peaks[i]=0;} if(f_peaks[i]>=65.4 && f_peaks[i]<=130.8) {f_peaks[i]=255*((f_peaks[i]/65.4)-1);} if(f_peaks[i]>=130.8 && f_peaks[i]<=261.6) {f_peaks[i]=255*((f_peaks[i]/130.8)-1);} if(f_peaks[i]>=261.6 && f_peaks[i]<=523.25){f_peaks[i]=255*((f_peaks[i]/261.6)-1);} if(f_peaks[i]>=523.25 && f_peaks[i]<=1046) {f_peaks[i]=255*((f_peaks[i]/523.25)-1);} if(f_peaks[i]>=1046 && f_peaks[i]<=2093) {f_peaks[i]=255*((f_peaks[i]/1046)-1);} if(f_peaks[i]>255){f_peaks[i]=254;} j=1;k=0; while(j==1) { if(f_peaks[i]<NoteV[k]){f_peaks[i]=k;j=0;} k++; // a note with max peaks (harmonic) with aplitude priority is selected if(k>15){j=0;} } if(f_peaks[i]==12){f_peaks[i]=0;} k=f_peaks[i]; in[k]=in[k]+(5-i); } k=0;j=0; for(int i=0;i<12;i++) { if(k<in[i]){k=in[i];j=i;} //Max value detection } // Note print // if you need to use note value for some application, use of note number recomendded // where, 0=c;1=c#,2=D;3=D#;.. 11=B; //a2=micros(); // time check k=j; /* Serial.print("note:"); Serial.print(k); Serial.println(","); */ /* if(k==0) {Serial.println('C');} if(k==1) {Serial.print('C');Serial.println('#');} if(k==2) {Serial.println('D');} if(k==3) {Serial.print('D');Serial.println('#');} if(k==4) {Serial.println('E');} if(k==5) {Serial.println('F');} if(k==6) {Serial.print('F');Serial.println('#');} if(k==7) {Serial.println('G');} if(k==8) {Serial.print('G');Serial.println('#');} if(k==9) {Serial.println('A');} if(k==10){Serial.print('A');Serial.println('#');} if(k==11){Serial.println('B');} */ return k; } } //-----------------------------FFT Function----------------------------------------------// // EasyFFT code optimised for 128 sample size to reduce mamory consumtion float FFT(byte N,float Frequency) { byte data[8]={1,2,4,8,16,32,64,128}; int a,c1,f,o,x; a=N; for(int i=0;i<8;i++) //calculating the levels { if(data[i]<=a){o=i;} } o=7; byte in_ps[data[o]]={}; //input for sequencing float out_r[data[o]]={}; //real part of transform float out_im[data[o]]={}; //imaginory part of transform x=0; for(int b=0;b<o;b++) // bit reversal { c1=data[b]; f=data[o]/(c1+c1); for(int j=0;j<c1;j++) { x=x+1; in_ps[x]=in_ps[j]+f; } } for(int i=0;i<data[o];i++) // update input array as per bit reverse order { if(in_ps[i]<a) {out_r[i]=in[in_ps[i]];} if(in_ps[i]>a) {out_r[i]=in[in_ps[i]-a];} } int i10,i11,n1; float e,c,s,tr,ti; for(int i=0;i<o;i++) //fft { i10=data[i]; // overall values of sine cosine i11=data[o]/data[i+1]; // loop with similar sine cosine e=6.283/data[i+1]; e=0-e; n1=0; for(int j=0;j<i10;j++) { c=cos(e*j); s=sin(e*j); n1=j; for(int k=0;k<i11;k++) { tr=c*out_r[i10+n1]-s*out_im[i10+n1]; ti=s*out_r[i10+n1]+c*out_im[i10+n1]; out_r[n1+i10]=out_r[n1]-tr; out_r[n1]=out_r[n1]+tr; out_im[n1+i10]=out_im[n1]-ti; out_im[n1]=out_im[n1]+ti; n1=n1+i10+i10; } } } //---> here onward out_r contains amplitude and our_in conntains frequency (Hz) for(int i=0;i<data[o-1];i++) // getting amplitude from compex number { out_r[i]=sqrt((out_r[i]*out_r[i])+(out_im[i]*out_im[i])); // to increase the speed delete sqrt out_im[i]=(i*Frequency)/data[o]; /* Serial.print(out_im[i],2); Serial.print("Hz"); Serial.print("\t"); // uncomment to print freuency bin Serial.println(out_r[i]); */ } x=0; // peak detection for(int i=1;i<data[o-1]-1;i++) { if(out_r[i]>out_r[i-1] && out_r[i]>out_r[i+1]) {in_ps[x]=i; //in_ps array used for storage of peak number x=x+1;} } s=0; c=0; for(int i=0;i<x;i++) // re arraange as per magnitude { for(int j=c;j<x;j++) { if(out_r[in_ps[i]]<out_r[in_ps[j]]) {s=in_ps[i]; in_ps[i]=in_ps[j]; in_ps[j]=s;} } c=c+1; } for(int i=0;i<5;i++) // updating f_peak array (global variable)with descending order { f_peaks[i]=(out_im[in_ps[i]-1]*out_r[in_ps[i]-1]+out_im[in_ps[i]]*out_r[in_ps[i]]+out_im[in_ps[i]+1]*out_r[in_ps[i]+1]) /(out_r[in_ps[i]-1]+out_r[in_ps[i]]+out_r[in_ps[i]+1]); } } //------------------------------------------------------------------------------------//
Note: This code is only the first prototype. It need some improvements and debuging. Let me know if you make some upgrade! 🙂
Improvements: Create a filter which sees the last 5 notes, and when they coincide = take the note as true. The same to switch off. It needs 5 times in a row different notes to turn off the bowl. As the refresh time is not large, it would not create perceptible latency.
A nice upgrade is to light the leds with a flicker or candle effect, instead of a straight and cold ON/OFF control. This add some hassle to the code, but nothing that can’t be handled!
Issues to be debugged:
Only one note can be detected at the same time → (easy) Distributed hardware option would solve this problem. Each Arduino is in charge to detect one note.
(complex) Another option is to change the FFT function, which now analyzes the top 5 frequencies (inclusive harmonics). Make the function manage more frequencies and take into account the multiple notes.
When any sound is played, they still light randomly
create a threshold to auto-turn on and off. The analog read of the mic pin does not help to create a reliable threshold. → Not working properly. The sound is very wide and the level is not easily detected.
Put an external ON/OFF switch
the last 5 elements filter would solve the issue
Paul
Wow! Thank you! Your explanation is great and your code works perfectly. I have tried a number of other FFT code samples for Arduino on the web and few, if any are as succinct and elegantly structured. Well done.
Alberto L.
Thanks!! 🙂