: a five-sided structure integrating ultrasonic sensors and sound modulation to create a multi-sensory experience.
By: Chris
Instructor: Andy
CONCEPTION AND DESIGN:
We brainstormed a few project ideas but eventually landed on "Ultrasense," which was later rebranded to "The Sonic Pentagon." The key reason was the facilitation of interactions through touching and spatial gestures, which fit nicely for our interactive approach. In alignment with some of our earlier assignments and inspired by interactive installations, the project was to design kinetic wearables. We applied lessons learned from that project, particularly, intuitive input and responsive feedback. In our preparatory research, we emphasized using sound and visuals to create an immersive experience through feedback mechanisms. We applied ultrasonic sensors and sound oscillators, which combined translated physical gestures into real-time sound modulation to reach this goal. This was important as a pentagonal frame made sure that sensors were at an equal distance from all points of user interaction. This facilitates experimenting with sound intuitively and creatively.
Refining our design through user testing Talking to users led us to discover that where sensors were placed had a big impact on the usage. Initial feedback suggested that LED feedback should be more intuitive and sound transitions should not be jarring. In reply, we reconfigured the sensors for increased sensitivity, refined the sound synthesis algorithms to produce more resonant sounds, and adjusted the user interface and response system of the LEDs to more closely follow behavior. The changes provided a more natural interaction loop and polished the user experience. This enabled us to build a project that truly fulfilled the goals of combining art, technology, and human interaction into a seamless experimental instrument by focusing on how users intuitively used the prototype.
VIDEO DOCUMENTATION
Mechanical Design:
The structure frame was constructed with flexible blackboards, which are lightweight, and flexible. The pentagonal frame made it easy to place the ultrasonic sensors evenly while allowing enough room for clean wiring and adjustments. All sensors were hot glued in place and adjusted for optimal signal readings. Although other materials such as wood or acrylic could have been used, the flexible option was chosen for ease of assembly and suitability for prototyping. Its finish was a sleek black, which bonded the slaptop with the project’s impacts.
Electronic Integration:
The electronic system included five ultrasonic sensors, a NeoPixel LED strip, and a rotary encoder. Each component played a vital role in achieving the project goals:
Ultrasonic Sensors: Enabled distance detection to trigger sounds and lights based on user proximity. Infrared sensors were considered but rejected due to their limited range and sensitivity to ambient light.
NeoPixel LED Strip: Provided visual feedback by changing colors corresponding to active sensors. Regular RGB LEDs were rejected because they required more complex wiring and did not offer the same level of programmability.
Rotary Encoder: Allowed pitch adjustments and toggling of sound and background colors.
Key Code Segments for Functionality:
Arduino Code
The base Arduino code handles the detection of distance, control of NeoPixels, and communication to Processing. The main things to do are read the sensor distances and transmit data to Processing.
Important Arduino Code: Read Ultrasonic Sensors and Send data
This part computes the distances for each sensor, sending the values as a comma-separated string to Processing:
void loop() {
for (int i = 0; i < numberOfSensors; i++) {
digitalWrite(triggerPins[i], LOW);
delayMicroseconds(2);
digitalWrite(triggerPins[i], HIGH);
delayMicroseconds(10);
digitalWrite(triggerPins[i], LOW);
durations[i] = pulseIn(echoPins[i], HIGH);
distances[i] = durations[i] * 0.034 / 2;
}
// Send distance data to Processing
for (int i = 0; i < numberOfSensors; i++) {
Serial.print(distances[i]);
if (i < numberOfSensors - 1) {
Serial.print(",");
}
}
Why It’s Important:
This code ensures that Processing receives accurate, real-time data from the sensors to drive sound and visuals. Without this, the interaction loop between the hardware and software would break.
Visual Function of the Code:
Processing Code
The most important task in Processing is to read Arduino data and produce sounds based on distances detected by sensors. serialEvent() is the key code that must run to keep the system responsive and interactive.
Important Processing Code: Handling Serial Data
This section handles sensor distance data, toggles sound, and dynamically changes pitch in Processing:
void serialEvent(Serial myPort) {
String data = myPort.readStringUntil('\n'); // Read data from Arduino
if (data != null) {
data = trim(data); // Clean up the data
if (data.equals("Button Pressed")) {
soundOn = !soundOn;
redBackground = !redBackground;
} else if (data.startsWith("Counter: ")) {
int newCounter = int(data.substring(9)); // Get rotary encoder value
if (newCounter > counter) {
pitchOffset += 100;
} else {
pitchOffset -= 100;
}
pitchOffset = constrain(pitchOffset, -200, 600); // Constrain the pitch value
counter = newCounter;
} else {
String[] values = split(data, ',');
if (values.length == 5) {
for (int i = 0; i < 5; i++) {
distances[i] = int(values[i]);
}
}
}
}
}
Why It’s Important:
This function interprets data from Arduino and enables real-time control over sound and visuals. It ensures seamless interaction, making the system feel dynamic and responsive to user actions.
Challenges and Iterations:
Ultrasonic Sensors Failure: After connecting all 5 ultrasonic sensors and testing them, the wires were too long and everything tangled up. Clearing the wires and using matching color wires for different sensors resolved the issue.
Signal Interference: Initially, ultrasonic sensors interfered with each other, which was resolved by staggering trigger timings in the Arduino code and spacing sensors further apart.
LED Timing Issues: Early iterations struggled with syncing LED feedback to sensor inputs. Debugging the Arduino code resolved the issue.
Sketches and Drawings:
CONCLUSION:
Ultrasense - Sonic Pentagon was designed as an interactive device that merges art, technology, and human interaction allowing the artist to create compositions and explore sound with gestures. The project was successful in achieving its aims by translating movement into sound and visual feedback using ultrasonic sensors, sound oscillators, and NeonPixel. User testing indicated that the frame shape led to natural interactions; sensor placement and feedback refinements made the system intuitive and rewarding. This resonates with what we mean by interaction: a reactive, in-the-moment response to user inputs that embraces curiosity and creativity. If we had more time, we would work on improving sensor sensitivity and the range of the sound and lighting effects used to offer a more immersive interactive experience. Additional features, such as customizable sound profiles or integration with other devices, could also make it more versatile. Through challenges such as tangled wires, sensor interference, LED timing issues, we gained insight into the importance of iterative design, hardware organization, and user feedback. By the time the dust settled on The Sonic Pentagon, it did far more than achieve its objectives; it facilitated some useful learnings about how to create inviting and responsive systems that blend art and engineering.
DISASSEMBLY:
APPENDIX:
Development process: Step-by-step pictures
Development process: Videos
Full Code for Processing:
import processing.serial.*;
import processing.sound.*;
Serial myPort;
SqrOsc[] oscillators;
int[] distances = new int[5];
int threshold = 15;
int counter = 0;
boolean soundOn = true;
boolean redBackground = true;
float pitchOffset = 0;
float[] notes = {261.63, 329.63, 392.00, 523.25, 659.25};
void setup() {
size(800, 600);
oscillators = new SqrOsc[5];
for (int i = 0; i < 5; i++) {
oscillators[i] = new SqrOsc(this);
oscillators[i].play();
oscillators[i].amp(0);
}
myPort = new Serial(this, "COM10", 9600);
myPort.bufferUntil('\n');
}
void draw() {
if (redBackground) {
background(255, 0, 0);
} else {
background(0, 255, 0);
}
for (int i = 0; i < 5; i++) {
if (distances[i] > 0 && distances[i] <= threshold) {
float freq = notes[i] + pitchOffset;
float amp = map(distances[i], 2, threshold, 1.0, 0.5);
oscillators[i].freq(freq);
oscillators[i].amp(amp);
} else {
oscillators[i].amp(0);
}
}
}
void serialEvent(Serial myPort) {
String data = myPort.readStringUntil('\n');
if (data != null) {
data = trim(data);
if (data.equals("Button Pressed")) {
soundOn = !soundOn;
redBackground = !redBackground;
} else if (data.startsWith("Counter: ")) {
int newCounter = int(data.substring(9));
if (newCounter > counter) {
pitchOffset += 100;
} else {
pitchOffset -= 100;
}
pitchOffset = constrain(pitchOffset, -200, 600);
counter = newCounter;
} else {
String[] values = split(data, ',');
if (values.length == 5) {
for (int i = 0; i < 5; i++) {
distances[i] = int(values[i]);
}
}
}
}
}
Full Code for Arduino:
#include <Adafruit_NeoPixel.h>
int numberOfSensors = 5;
int numberOfPixels = 60;
int triggerPins[] = { 3, 11, 9, 7, 5 };
int echoPins[] = { 2, 10, 8, 6, 4 };
long durations[5];
int distances[5];
int neoPixelPin = 16;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(numberOfPixels, neoPixelPin, NEO_GRB + NEO_KHZ800);
int rotaryEncoderClockPin = 21;
int rotaryEncoderDataPin = 20;
int rotaryEncoderButtonPin = 19;
int lastStateClockPin = 0;
int currentStateClockPin = 0;
int encoderCount = 0;
void setup() {
Serial.begin(9600);
for (int i = 0; i < numberOfSensors; i++) {
pinMode(triggerPins[i], OUTPUT);
pinMode(echoPins[i], INPUT);
}
pinMode(rotaryEncoderClockPin, INPUT);
pinMode(rotaryEncoderDataPin, INPUT);
pinMode(rotaryEncoderButtonPin, INPUT);
strip.begin();
strip.show();
}
void loop() {
for (int i = 0; i < numberOfSensors; i++) {
digitalWrite(triggerPins[i], LOW);
delayMicroseconds(2);
digitalWrite(triggerPins[i], HIGH);
delayMicroseconds(10);
digitalWrite(triggerPins[i], LOW);
durations[i] = pulseIn(echoPins[i], HIGH);
distances[i] = durations[i] * 0.034 / 2;
}
for (int i = 0; i < numberOfSensors; i++) {
Serial.print(distances[i]);
if (i < numberOfSensors - 1) {
Serial.print(",");
}
}
Serial.println();
currentStateClockPin = digitalRead(rotaryEncoderClockPin);
if (currentStateClockPin != lastStateClockPin) {
if (digitalRead(rotaryEncoderDataPin) != currentStateClockPin) {
encoderCount++;
} else {
encoderCount--;
}
Serial.print("Encoder Count: ");
Serial.println(encoderCount);
}
if (digitalRead(rotaryEncoderButtonPin) == LOW) {
Serial.println("Button Pressed");
delay(300);
}
lastStateClockPin = currentStateClockPin;
if (Serial.available()) {
String inputData = Serial.readStringUntil('\n');
inputData.trim();
if (inputData.startsWith("L")) {
int sensorIndex = inputData.substring(1, 2).toInt();
updateNeoPixelLights(sensorIndex);
}
}
delay(50);
}
void updateNeoPixelLights(int sensorIndex) {
int red = 255, green = 0, blue = 0;
if (sensorIndex == 1) {
green = 255;
red = 0;
} else if (sensorIndex == 2) {
blue = 255;
red = 0;
green = 0;
} else if (sensorIndex == 3) {
red = 255;
green = 150;
blue = 0;
} else if (sensorIndex == 4) {
red = 255;
green = 0;
blue = 255;
}
for (int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, red, green, blue);
}
strip.show();
}
References:
Anon, et al. “Ultrasonic Sensor HC-SR04 and Arduino - Complete Guide.” How To Mechatronics, 18 Feb. 2022, howtomechatronics.com/tutorials/arduino/ultrasonic-sensor-hc-sr04.
LME Editorial Staff. “How Rotary Encoder Works and Interface It with Arduino.” Last Minute Engineers, 1 Feb. 2023, lastminuteengineers.com/rotary-encoder-arduino-tutorial.
Processing. (n.d.). processing-sound/src/processing/sound/Oscillator.java at main · processing/processing-sound. GitHub. github.com/processing/processing-sound/blob/main/src/processing/sound/Oscillator.java.
SinOsc / Libraries. (n.d.). Processing. processing.org/reference/libraries/sound/SinOsc.html.
Tj, Reac. YouTube. www.youtube.com/watch?v=Mgy1S8qymx0.