Project 1: Dodeca – Part 2
This week Mai and I made some great progress on our musical Dodecahedron. Feeling pretty confident about the circuit on the breadboard we moved to a more compact arrangement on a soldered board.
We met in person on the floor at ITP to make a second Dodeca so we could both have one at home to code with. Mai was excited to solder for the first time and I shared what I have learned about soldering from Deqing Sun’s course Prototyping Electric Devices. She was a natural of course!
While Mai was constructing the second Dodeca I spent some time on the floor reviewing the Arduino Sketch we had put together. Our sketch makes use of a number of libraries and I added a bunch of comments in the code below to clarify what everything is doing. You can find the current code at the bottom of this post.
The goal of mapping specific facets of the dodeca to notes turned into a much bigger challenge than I expected and I learned a lot about IMU functions in the process. Using an enclosure with numbers on the facets matching the points in our code helped a lot with this.
We ended up using four different libraries to get from the point of reading the IMU sensor to determining which facet has the highest Y value in 3D space. Reading ahead to the IMU labs for physical computing Mai pointed out the “MadgwickAHRS.h” library that translates the sensor readings from the Arduino Nano 33 into Euler Angles (pitch, roll, yaw). Experimenting with this I found that the IMU of the Nano 33 IoT does not have a magnetometer which appeared to introduce a constant drift to the Z-axis, or yaw, readings, using the Madgwick library. Because the facets are mapped in a constant relationship to the sensor, as they are physically attached to one another, I realized that we could abandon the Z-axis/yaw readings all together and calculate rotations using only X-axis/pitch and Y-axis/roll.
After returning the values for roll and pitch from the Madgwick library in radians we can calculate the rotation of the 12 facet midpoints of the dodecahedron in 3D space then check through the midpoint of each facet to determine which one is up!
And our final dodeca with the duration of sound tied to movement:
#include <Arduino_LSM6DS3.h> // This library reads sensor values from the Intertial Measurement Unit (IMU). #include "MadgwickAHRS.h" // This library generates Euler angle measures from the IMU. #include <BasicLinearAlgebra.h> // This library is required by the Geometry library below. #include <Geometry.h> // This library uses BasicLinearAlgebra to calculate rotations of the points representing the 12 facets of the dodecahedron in 3D space. #include "pitches.h" // This tab holds values for pitches of different notes. /* Dodeca Instrument A musical instrument housed in a dodecahedron enclosure that runs on the Arduino Nano 33 IoT. Dodeca creates notes with tone output based on rotational measurements from the Nano 33 IoT internal IMU sensor. Made with help from: IMU lab from Tom Igoe IPC : https://itp.nyu.edu/physcomp/labs/lab-serial-imu-output-to-p5-js/ How to read Arduino Nano IoT 33 IMU: https://desertbot.io/blog/arduino-nano-33-iot-imu Arduino Floating Point Constants: https://forum.arduino.cc/index.php?topic=527608.0 Geometry Library: https://github.com/tomstewart89/Geometry Datasheet for LM386: https://www.jameco.com/Jameco/Products/ProdDS/839826.pdf Basic LM386 Amplifier Circuit: https://www.circuitbasics.com/build-a-great-sounding-audio-amplifier-with-bass-boost-from-the-lm386/ Musical Scales: https://en.wikipedia.org/wiki/Pentatonic_scale created 5 Oct 2020 by Brandon Roots & Magdalena Claro */ // Initialize a Madgwick filter that will be used to calculate the Euler angles pitch, roll, yaw Madgwick filter; // Set sensor's sample rate to 104 Hz to improve accuracy of Euler angle interpretations by Madgwick library const float sensorRate = 104.00; // Create points representing center of 12 facets of a dodecahedron in 3D space, plus additional 13-24 map to transforms Point p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21, p22, p23, p24; // Major pentatonic scale int notes[] = { NOTE_AS5, NOTE_GS5, NOTE_FS5, NOTE_DS5, NOTE_CS5, NOTE_AS4, NOTE_GS4, NOTE_FS4, NOTE_DS4, NOTE_CS4, NOTE_AS3, 0 }; // Variabel to track which facet was last facing up int lastUpwardFacet = 0; // LED indicator info int ledPin = 17; int brightness = 1; int fadeAmount = 1; // Movement threshold to make sound int movementThreshold = 7; void setup() { // Initialize serial Serial.begin(9600); // Setup LED pinMode(ledPin, OUTPUT); // Initialize the IMU sensor if (!IMU.begin()) { Serial.println("Failed to initialize IMU!"); while (true); // halt program } // Start the filter to run at the sample rate filter.begin(sensorRate); Serial.println("IMU initialized!"); // Map values to points in X(), Y() & Z() of dodecahedron with radius of 1 - found these values by making an object in Cinema 4D p1.X() = 0; p1.Y() = 79.5; p1.Z() = 5.8; p2.X() = 0; p2.Y() = 30.4; p2.Z() = -73.7; p3.X() = 64.5; p3.Y() = 39.7; p3.Z() = -24.5; p4.X() = 46.7; p4.Y() = 33.9; p4.Z() = 54.9; p5.X() = -46.7; p5.Y() = 33.9; p5.Z() = 54.9; p6.X() = -64.5; p6.Y() = 39.7; p6.Z() = -24.5; p7.X() = -46.7; p7.Y() = 33.9; p7.Z() = -54.9; p8.X() = 46.7; p8.Y() = 33.9; p8.Z() = -54.9; p9.X() = 64.5; p9.Y() = -39.7; p9.Z() = 24.5; p10.X() = 0; p10.Y() = -30.4; p10.Z() = 73.7; p11.X() = -64.5; p11.Y() = -39.7; p11.Z() = 24.5; p12.X() = 0; p12.Y() = -79.5; p12.Z() = -5.8; } void loop() { // Local variables for orientation data from IMU float aX, aY, aZ; float gX, gY, gZ; const char * spacer = ", "; // Local variables for orientation from Madgwick library float roll, pitch, heading; // Try to read the IMU sensor if ( IMU.accelerationAvailable() && IMU.gyroscopeAvailable() ) { IMU.readAcceleration(aX, aY, aZ); IMU.readGyroscope(gX, gY, gZ); // Update the filter, which computes orientation filter.updateIMU(gX, gY, gZ, aX, aY, aZ); // Print the heading, pitch and roll roll = filter.getRollRadians(); // 180 to -180 y leaves x unchanged pitch = filter.getPitchRadians(); // 90 to -90 x leaves y unchanged // Calculate rotation of 12 points in dodecahedron // Rotation represents a rotated coordinate frame (a set of x/y/z axes) expressed with respect to a base frame. Rotation R1; // Rotate points using Euler angle data from Madgwick library (pitch, yaw, roll) R1.FromEulerAngles(pitch, roll, M_PI_4); // Correct for Arduino Nano orientation in Dodeca R1.RotateX(-1.55); // Apply rotations to second set of points p13 = R1 * p1; p14 = R1 * p2; p15 = R1 * p3; p16 = R1 * p4; p17 = R1 * p5; p18 = R1 * p6; p19 = R1 * p7; p20 = R1 * p8; p21 = R1 * p9; p22 = R1 * p10; p23 = R1 * p11; p24 = R1 * p12; // Check which point+facet has largest Y value and is therefore facing UP // Create array holding only Y values from points float pointValuesY[12] = {p13.Y(), p14.Y(), p15.Y(), p16.Y(), p17.Y(), p18.Y(), p19.Y(), p20.Y(), p21.Y(), p22.Y(), p23.Y(), p24.Y()}; // Create array to hold values for point with largest value and that value float maximumValueY[2] = {pointValuesY[0], 0}; // Loop thorough array to check for largest Y value then store in maximumValueY array for (int i = 1; i < 12; i++) { if (pointValuesY[i] > maximumValueY[0]) { maximumValueY[0] = pointValuesY[i]; maximumValueY[1] = i; } } // Local variable to hold current upwardFacet int upwardFacet = maximumValueY[1]; // If a new facet is facing up play a tone if (lastUpwardFacet != upwardFacet) { // Do some things if a new facet is facing up Serial.print("Facet "); Serial.print(upwardFacet+1); Serial.println(" is facing up"); // Record new upward facet lastUpwardFacet = upwardFacet; } // Make a sound mapped to this point+facet if movement in gX, gY, or gZ sensed... if (gX > movementThreshold || gY > movementThreshold || gZ > movementThreshold) { // And not upsidedown... if (aX < .90){ tone(2, notes[upwardFacet], 500); // Make LED full brightness analogWrite(ledPin, 255); //noTone(8); } } } // Pulse LED if dodeca not rotated down if (aX < .90){ // And not moving very much... if (gX < movementThreshold && gY < movementThreshold && gZ < movementThreshold) { // Make LED pulse pulseLED(); } }else{ // Turn off LED analogWrite(ledPin, 0); // Reset fading value to 1 brightness = 2; } } void pulseLED(){ // Write brightness to LED analogWrite(ledPin, brightness); brightness = brightness + fadeAmount; // Reverse the direction of the fading at the ends of the fade if(brightness == 1 || brightness == 254) { fadeAmount = -fadeAmount ; } delay(2); }