Willow (Makenzie’s Version)
-
Objective
Design a musical instrument, an interface/controller, or an installation. The only technical requirement is that it should make sound and use sensors and a microcontroller. The sound should be made using Faust and can be controlled with sensors using Arduino.
-
Outcome
Willow is an installation inspired by the whimsy of a weeping willow. Using steel tubing, the form was created through tube bending and TIG welding. Peaceful sounds are created by a combination of both human and environmental interaction with the sculpture.
Tube Bending
I first started with making the base of the tree, which I wanted to be a circle. Instead of making two half circles and welding them together, I opted into the challenge of forming a full circle on the ring roller. Using the side rollers, you can deflect the tube slightly out of plane in order to get the tube to loop back onto itself. However, as I quickly learned eventually the ring will start to run into the machine for this same reason. After lots of trial and error, I finally walked away with a full circle out of one piece of tubing! The final step before welding was to cut the flat end sections off on the bandsaw. The trunk pieces (to be attached to the circular base) were also given slight curvature on the ring roller.
Welding
This was my first large welding project and my biggest takeaway was how critical fixturing is to the success of a weld. Given the organic shapes I was working with, I had to be fairly creative in how to best fixture them to the table (note: the bucket in the middle picture). Also, not pictured but critical for the attachment of the trunk pieces to the circular base were the fish mouths I machined on the mill using a hole saw and automatic z feed.
It was then time to move onto the branches, my favorite part of the whole project! Instead of forming the curvature on my own, I chose to repurpose the scrap metal lying around the shop. I cut up old projects that had used steel tubing and pieced them together like a puzzle to form organic shapes that I liked. This meant that sometimes a branch was made up of around ten pieces!
I wanted the tree to be large enough for people to walk under so that they could fully engage with it but I also wanted it to be easily transportable. In order to achieve this, I decided to make the branches of the tree removable. I did this on the lathe by turning down the OD of smaller tube to match the ID of the branches. I then welded the two tubes together and grinded down the weld so that the branches could sit relatively flush with the trunk (pic of attachment point shown below).
Interaction Design
Once the physical form of the sculpture was complete it was time to think about how the outside world would interact with it. In terms of human interaction, I hoped to capture a sense of whimsy as people approached the tree. To accomplish this, six ultrasonic distance sensors circumscribe the trunk with each correlating to a different voice. As you move closer to the tree radially, that particular voice gets louder. The more people that interact with the tree, the more interesting the sound. It was also important to me for my installation to also include environmental interaction so that it could take on a more living and evolving form. Thus, a wind sensor detects fluctuations in wind speed, which then modulates the cutoff frequency of a low pass filter. This plays nicely with the mechanical fluctuations of the sculpture in large gusts of wind.
Faust Code
This code generates the sounds that are played from the tree. Six voices play in parallel and then are passed through a low pass filter. All voices were made with additive synthesis and consist of a combination of sawtooth and triangle waves. Each voice has a fixed frequency associated with it while the gain and the gate can be modulated via sliders. The low pass filter can also be manipulated via the cutoff frequency slider. The audio is output as stereo. Having absolutely zero musical ability going into this class I was incredibly proud of the sound I was able to create!
-
import("stdfaust.lib");
voice1 = hgroup("Voice 1", (sum(i,4,os.sawtooth(freq1*(i+1))*(1/(i+2))) + os.triangle(freq1)) * env1 : si.smoo)
with{
freq1 = 392;
env1 = en.adsr(1, 0.5, 0.5, 5, gate1) * gain1
with {
gain1 = hslider("[1]gain1",1,0,1,0.01);
gate1 = checkbox("[2]gate1");
};
};
voice2 = hgroup("Voice 2", (sum(i,4,os.sawtooth(freq2*(i+1))*(1/(i+2))) + os.triangle(freq2)) * env2 : si.smoo)
with{
freq2 = 932.32;
env2 = en.adsr(1, 0.5, 0.5, 5, gate2) * gain2
with {
gain2 = hslider("[3]gain2",1,0,1,0.01);
gate2 = checkbox("[4]gate2");
};
};
voice3 = hgroup("Voice 3", (sum(i,4,os.sawtooth(freq3*(i+1))*(1/(i+2))) + os.triangle(freq3)) * env3 : si.smoo)
with{
freq3 = 587.33;
env3 = en.adsr(1, 0.5, 0.5, 5, gate3) * gain3
with {
gain3 = hslider("[3]gain3",1,0,1,0.01);
gate3 = checkbox("[4]gate3");
};
};
voice4 = hgroup("Voice 4", (sum(i,4,os.sawtooth(freq4*(i+1))*(1/(i+2))) + os.triangle(freq4)) * env4: si.smoo)
with{
freq4 = 659.25;
env4 = en.adsr(1, 0.5, 0.5, 5, gate4) * gain4
with {
release4 = hslider("[2]release4",0.5,0.01,2,0.01);
gain4 = hslider("[3]gain4",1,0,1,0.01);
gate4 = checkbox("[4]gate4");
};
};
voice5 = hgroup("Voice 5", (sum(i,4,os.sawtooth(freq5*(i+1))*(1/(i+2))) + os.triangle(freq5)) * env5 : si.smoo)
with{
freq5 = 261.6;
env5 = en.adsr(1, 0.5, 0.5, 5, gate5) * gain5
with {
gain5 = hslider("[3]gain5",1,0,1,0.01);
gate5 = checkbox("[4]gate5");
};
};
voice6 = hgroup("Voice 6", (sum(i,4,os.sawtooth(freq6*(i+1))*(1/(i+2))) + os.triangle(freq6)) * env6 : si.smoo)
with{
freq6 = 294;
env6 = en.adsr(1, 0.5, 0.5, 5, gate6) * gain6
with {
gain6 = hslider("[3]gain6",1,0,1,0.01);
gate6 = checkbox("[4]gate6");
};
};
lowPass = fi.resonlp(ctFreq,5,1)
with{
ctFreq = hslider("ctFreq",750,50,1500,0.1) : si.smoo;
qual = hslider("qual",5,1,30,0.1) : si.smoo;
};
process = voice1, voice2, voice3, voice4, voice5, voice6 :> _ : *(5) : lowPass <: _,_;
Arduino Code
This code maps the sensors and uses those mapped values to modulate the sound from the Faust code. First, the values from the ultrasonic sensors and the wind sensor must be read. The ultrasonic sensors used in this project are HCSR04 and they have their own arduino library that must be downloaded. The wind sensor used was Modern Device’s Wind Sensor Rev C. This sensor requires calibration and also some math in order to output wind speed in MPH. The readWindSensor() function in this project is from the GitHub linked on the Modern Sensor website and takes care of the necessary math in order to output useable data. Once the values have been read in, the code then maps these values to the audio variables in Faust. The six ultrasonic sensors each correspond to a different voice from Faust. The values from the ultrasonic sensor are used to control both the gate and gain of a particular voice. The gain is controlled so that as you move closer to the sensor the voice is louder. However, if the sensor does not read anything in its path or the object is too far away, then the gain is left as the previous reading until something comes back into range. This is so that there can be more continuous sound and the release of each note can be heard. The gate is controlled in a similar fashion. If nothing is sensed or the object is too far away, then the gate is zero. If something is sensed within the specified operating range, then the gate is one. Finally, the wind sensor is mapped to the low pass filter’s cutoff frequency. The mapped range is kept between 150-2000 Hz with faster wind speeds having lower cutoff frequencies so that more sound distortion occurs.
-
#include <Audio.h>
#include "Drone.h"
#include "HCSR04.h"
Drone drone;
AudioOutputI2S out;
AudioControlSGTL5000 audioShield;
AudioConnection patchCord0(drone,0,out,0);
AudioConnection patchCord1(drone,1,out,1);
UltraSonicDistanceSensor distanceSensor1(0,1);
UltraSonicDistanceSensor distanceSensor2(2,3);
UltraSonicDistanceSensor distanceSensor3(4,5);
UltraSonicDistanceSensor distanceSensor4(9, 28);
UltraSonicDistanceSensor distanceSensor5(29, 30);
UltraSonicDistanceSensor distanceSensor6(31, 32);
#define analogPinForRV A10
#define analogPinForTMP A11
// ultrasonic
float ultra1;
float ultra2;
float ultra3;
float ultra4;
float ultra5;
float ultra6;
float mappedUltra1;
float mappedUltra2;
float mappedUltra3;
float mappedUltra4;
float mappedUltra5;
float mappedUltra6;
float ultraMaxDist = 150;
float ultraMinDist = 50;
//wind
const float zeroWindAdjustment = -0.2; // negative numbers yield smaller wind speeds and vice versa.
int TMP_Therm_ADunits; //temp termistor value from wind sensor
float RV_Wind_ADunits; //RV output from wind sensor
float RV_Wind_Volts;
unsigned long lastMillis;
int TempCtimes100;
float zeroWind_ADunits;
float zeroWind_volts;
float WindSpeed_MPH;
float mappedWind;
void setup() {
Serial.begin(9600);
AudioMemory(2);
audioShield.enable();
audioShield.volume(1.0);
initializeUltraSonics();
}
void loop() {
readUltraSensors();
readWindSensor();
controlGain();
controlGate();
controlCutoffFreq();
}
void readUltraSensors() {
ultra1 = distanceSensor1.measureDistanceCm();
ultra2 = distanceSensor2.measureDistanceCm();
ultra3 = distanceSensor3.measureDistanceCm();
ultra4 = distanceSensor4.measureDistanceCm();
ultra5 = distanceSensor5.measureDistanceCm();
ultra6 = distanceSensor6.measureDistanceCm();
}
void controlGain() {
controlGain1();
controlGain2();
controlGain3();
controlGain4();
controlGain5();
controlGain6();
}
void controlGain1() {
if (ultra1 == -1.00 || ultra1 > ultraMaxDist) mappedUltra1 = mappedUltra1;
else mappedUltra1 = 1- max(0, min(1,map(ultra1, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain1", mappedUltra1);
}
void controlGain2() {
if (ultra2 == -1.00 || ultra2 > ultraMaxDist) mappedUltra2 = mappedUltra2;
else mappedUltra2 = 1- max(0, min(1,map(ultra2, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain2", mappedUltra2);
}
void controlGain3() {
if (ultra3 == -1.00 || ultra3 > ultraMaxDist) mappedUltra3 = mappedUltra3;
else mappedUltra3 = 1- max(0, min(1, map(ultra3, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain3", mappedUltra3);
}
void controlGain4() {
if (ultra4 == -1.00 || ultra4 > ultraMaxDist) mappedUltra4 = mappedUltra4;
else mappedUltra4 = 1- max(0, min(1, map(ultra4, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain4", mappedUltra4);
}
void controlGain5() {
if (ultra5 == -1.00 || ultra5 > ultraMaxDist) mappedUltra5 = mappedUltra5;
else mappedUltra5 = 1- max(0, min(1, map(ultra5, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain5", mappedUltra5);
}
void controlGain6() {
if (ultra6 == -1.00 || ultra6 > ultraMaxDist) mappedUltra6 = mappedUltra6;
else mappedUltra6 = 1- max(0, min(1, map(ultra6, ultraMinDist, ultraMaxDist, 0, 1)));
drone.setParamValue("gain6", mappedUltra6);
}
void controlGate() {
controlGate1();
controlGate2();
controlGate3();
controlGate4();
controlGate5();
controlGate6();
}
void controlGate1() {
if(ultra1 == -1.00 || ultra1 > ultraMaxDist){
drone.setParamValue("gate1", 0);
} else if (ultra1 != -1.00) drone.setParamValue("gate1", 1);
}
void controlGate2() {
if(ultra2 == -1.00 || ultra2 > ultraMaxDist){
drone.setParamValue("gate2", 0);
} else if (ultra2 != -1.00) drone.setParamValue("gate2", 1);
}
void controlGate3() {
if(ultra3 == -1.00 || ultra3 > ultraMaxDist){
drone.setParamValue("gate6", 0);
} else if (ultra3 != -1.00) drone.setParamValue("gate3", 1);
}
void controlGate4() {
if(ultra4 == -1.00 || ultra4 > ultraMaxDist){
drone.setParamValue("gate4", 0);
} else if (ultra4 != -1.00) drone.setParamValue("gate4", 1);
}
void controlGate5() {
if(ultra5 == -1.00 || ultra5 > ultraMaxDist){
drone.setParamValue("gate5", 0);
} else if (ultra5 != -1.00) drone.setParamValue("gate5", 1);
}
void controlGate6() {
if(ultra6 == -1.00 || ultra6 > ultraMaxDist){
drone.setParamValue("gate6", 0);
} else if (ultra6 != -1.00) drone.setParamValue("gate6", 1);
}
void controlCutoffFreq() {
mappedWind = max(50, min(2000, map(WindSpeed_MPH, 20, 0, 500, 1000)));
if(mappedWind <= 2000 && mappedWind >= 200) drone.setParamValue("ctFreq", mappedWind);
}
void readWindSensor(){
if (millis() - lastMillis > 200){ // read every 200 ms - printing slows this down further
TMP_Therm_ADunits = analogRead(analogPinForTMP);
RV_Wind_ADunits = analogRead(analogPinForRV);
RV_Wind_Volts = (RV_Wind_ADunits * 0.0048828125);
// these are all derived from regressions from raw data as such they depend on a lot of experimental factors
// such as accuracy of temp sensors, and voltage at the actual wind sensor, (wire losses) which were unaccouted for.
TempCtimes100 = (0.005 *((float)TMP_Therm_ADunits * (float)TMP_Therm_ADunits)) - (16.862 * (float)TMP_Therm_ADunits) + 9075.4;
zeroWind_ADunits = -0.0006*((float)TMP_Therm_ADunits * (float)TMP_Therm_ADunits) + 1.0727 * (float)TMP_Therm_ADunits + 47.172; // 13.0C 553 482.39
zeroWind_volts = (zeroWind_ADunits * 0.0048828125) - zeroWindAdjustment;
// This from a regression from data in the form of
// Vraw = V0 + b * WindSpeed ^ c
// V0 is zero wind at a particular temperature
// The constants b and c were determined by some Excel wrangling with the solver.
WindSpeed_MPH = pow(((RV_Wind_Volts - zeroWind_volts) /.2300) , 2.7265);
lastMillis = millis();
}
}
void initializeUltraSonics() {
mappedUltra1 = 0;
mappedUltra2 = 0;
mappedUltra3 = 0;
mappedUltra4 = 0;
mappedUltra5 = 0;
mappedUltra6 = 0;
}
void printGains() {
Serial.print("Gain1: " );
Serial.println(mappedUltra1);
Serial.print("Gain2: " );
Serial.println(mappedUltra2);
Serial.print("Gain3: ");
Serial.println(mappedUltra3);
Serial.print("Gain4: ");
Serial.println(mappedUltra4);
Serial.print("Gain5: ");
Serial.println(mappedUltra5);
Serial.print("Gain6: ");
Serial.println(mappedUltra6);
}
void printSensorVals() {
Serial.print(" WindSpeed MPH ");
Serial.println((float)WindSpeed_MPH);
Serial.print("Ultra1: ");
Serial.println(ultra1);
Serial.print("Ultra2: ");
Serial.println(ultra2);
Serial.print("Ultra3: ");
Serial.println(ultra3);
Serial.print("Ultra4: ");
Serial.println(ultra4);
Serial.print("Ultra5: ");
Serial.println(ultra5);
Serial.print("Ultra6: ");
Serial.println(ultra6);
}