Music has always been a big part of life. Without realizing it, we often look to music to express unspoken feelings or to help unwind after a long day. Because of the big influence music has had on us, we wanted to combine our two greatest passions, music and making, into a new project: MusBall. We felt that the idea of an instrument with a touchless-interface was a unique idea that we wanted to explore.The touchless surface also allows multiple people to play the instrument without having to worry about sanitation.
The basic idea is to use TOF sensors to measure the distance from your palms to the sensor and map those values to specific instruments and notes.
MaterialsWe use a Raspberry Pi 3B+, two Sparkfun ESP32 Thing, four Pololu VL6180 TOF sensors and a plastic ball.
We use the ESPNOW protocol to establish the communication with the instrument to the device that can perform the sound execution (RPi 3B+).
The ESPNow web defines the protocol as
ESP-NOW is yet another protocol developed by Espressif, which enables multiple devices to communicate with one another without using Wi-Fi. The protocol is similar to the low-power 2.4GHz wireless connectivity that is often deployed in wireless mouses. So, the pairing between devices is needed prior to their communication. After the pairing is done, the connection is safe and peer-to-peer, with no handshake being required
The protocol is an easy to use, designed for low range, fast data transmission.
We use one of the Sparkfun ESP32 Thing devices as an AP and the other one as a slave.
Using the example programs installed with the ESP32 board support, ESPNOW\Master.ino and ESPNOW\Slave.ino as templates we developed our version to transmit the sensor data.
We initialize both devices by using the espnow API as follows
esp_now_init()
On the Sparkfun ESP32 Thing device connected to the distance sensors, we scan for the network for available AP
int8_t scanResults = WiFi.scanNetworks();
For each found network we get their SSID and compare it to the AP we are looking to connect into.
String SSID = WiFi.SSID(i);
int32_t RSSI = WiFi.RSSI(i);
String BSSIDstr = WiFi.BSSIDstr(i);
// Check if the current device starts with `Slave`
if (SSID.indexOf("Slave") == 0) {
Once we found a suitable AP we save its MAC, and channel information so we can connect to them.
The program then registers the AP as a peer for each loop it wants to send data, by executing
const esp_now_peer_info_t *peer = &slaves[i];
const uint8_t *peer_addr = slaves[i].peer_addr;
bool exists = esp_now_is_peer_exist(peer_addr);
if (!exists) {
esp_err_t addStatus = esp_now_add_peer(peer);
delay(10);
}
Then it sends its data package by calling
const uint8_t *peer_addr = slaves[i].peer_addr;
esp_err_t result = esp_now_send(peer_addr, data, DATASIZE);
Where DATASIZE is the package length in bytes and data is a byte array with the payload.
On the Sparkfun ESP32 Thing device connected to the RPi 3B+, we start an AP on the slave of the device that connects to the RPi 3B+ as
char* SSID = "Slave_1";
bool result = WiFi.softAP(SSID, "Slave_1_Password", CHANNEL, 0);
Registering the callback function to receive data from the master.
esp_now_register_recv_cb(OnDataRecv);
And the callback function forwards the received data to the serial port.
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
char macStr[18];
//Split mac address
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
//Forward each byte to serial port, replace the data delimiter with a carriage return.
for (int i=0;i<data_len;i++)
{
Serial2.write((data[i]));
if (data[i] == '|')
{
Serial2.write('\n');
}
}
}
The function breaks the received package and adds a carriage return after the divider character "|" on the data set for easy consumption on the Python program.
TOF sensor arrayThe data that envelopes the payload is a package formatted with the distance taken from the four TOF sensors located on the ball separated by a 90 degree metric.
Setting the I2C addresses on the Pololu TOF sensors was not a trivial task. We have to do a bit of research in order to assign the new addresses to the devices.
Create independent global variables for each sensor available.
VL6180X sensor;
VL6180X sensor1;
VL6180X sensor2;
VL6180X sensor3;
First all the sensors start on an idle state, you turn them on by sending a HIGH setting to the GPIO0/CE of the sensor connected to a digital pin (in the example below pin 12). Then initialize the sensor, configure to the default settings, set some basic parameters and finally set the new address (in the example below 0x31).
In code it translates to:
//Initilize sensor and assign address.
digitalWrite(12, HIGH);
sensor1.init();
sensor1.configureDefault();
sensor1.setScaling(SCALING);
sensor1.writeReg(VL6180X::SYSRANGE__MAX_CONVERGENCE_TIME, 30);
sensor1.writeReg16Bit(VL6180X::SYSALS__INTEGRATION_PERIOD, 50);
sensor1.setTimeout(500);
sensor1.setAddress(0x31);
Repeat these same steps in that order for each of the other connected sensors.
Once the connections have being established start collecting data by reading the sensors output.
dLeft = sensor.readRangeContinuousMillimeters();
dFront = sensor1.readRangeContinuousMillimeters();
dBack = sensor2.readRangeContinuousMillimeters();
dRight = sensor3.readRangeContinuousMillimeters();
And encode the data into a package
//Format data into one package to be sent.
sprintf((char *)myData,"%05d,%05d,%05d,%05d,2|",dLeft,dFront,dBack,dRight);
Note the package delimiter "|". The last 2 indicates the ball id in case there is more than one ball sending data to the connected AP.
Python serviceOn the Raspberry Pi 3B+ we create a program than runs on the background reading from the serial port, mapping the data into one of the loaded instrument nodes.
To play our notes we use pygame mixer libraries
#Initialize the pygame sound engine
pygame.mixer.init()
The program opens the serial port to read
#Initialize the serial port for communication with the Matrix Voice ESP32
ser = serial.Serial(
port='/dev/ttyS0',
baudrate = 115200,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=300
)
Playing the notes, we took a similar approach to our previous project GloMusic. The following are parts of that page explanation.
We request 15 Channels to play our instrument sounds simultaneously.
# Set the number of channels to allow multiple simultaneous sounds
pygame.mixer.set_num_channels(15)
We took a logistic approach to handle the resources as easily as possible, by creating an array of file paths and an array that would create the sound objects so we would not need to load them every time we want to play. A snip of the chorus object is shown below. Notice that the chsndObj array contains all the sound objects for the chorus after loading the corresponding file.
chCEGmfPath = ['/home/pi/MusicOrch/Chorus/chorus-female-c5-PB-loop.wav',
'/home/pi/MusicOrch/Chorus/chorus-female-e5-PB-loop.wav',
'/home/pi/MusicOrch/Chorus/chorus-female-g5-PB-loop.wav']
# Create an array of Sound objects for each instrumment. This is the array for chorus.
# Chorus
chsndObj = [pygame.mixer.Sound(chADFmfPath[0]), pygame.mixer.Sound(chADFmfPath[1]),
pygame.mixer.Sound(chADFmfPath[2]),
pygame.mixer.Sound(chCEGMmfPath[0]), pygame.mixer.Sound(chCEGMmfPath[1]),
pygame.mixer.Sound(chCEGMmfPath[2]),
pygame.mixer.Sound(chCEGmfPath[0]), pygame.mixer.Sound(chCEGmfPath[1]),
pygame.mixer.Sound(chCEGmfPath[2])]
The selectSoundObj and playNote functions takes the task to select the correct sound according to the active instrument and play on the independent channel.
Finally, the data decoding is envelope in
#Read a line of data from the ESP32 device
x=ser.readline()
#Split elements into data.
group = re.split(r'\|',x.decode('ascii'))
singleData = re.split(r',+',group[0])
singleData[0] = singleData[0][1:]
singleData2 = re.split(r',+',group[1])
We read a line from the serial stream and split the package by using the delimiter "|", and further more by the ", " delimiter.
The data is then mapped to the available array elements, and the corresponding notes are played.
ResultsAlthough sound was possible, the creation of a melody still requires some more work.
We are looking forward to see what other projects are inspired by this work.
Thanks for reading through.
Comments