Raya Tabassum: FINAL PROJECT “Interactive Musical Garden”

Concept: Interactive Musical Garden is an innovative interactive art installation that marries technology with natural aesthetics. It incorporates ultrasonic sensors embedded with 3D-printed transparent roses, allowing each rose to respond to user interaction by lighting up, playing music, and spawning a digital flower on a p5.js canvas. This project aims to create a communal yet personalized musical and visual experience where each interaction contributes to a growing digital garden.

Arduino Code Overview: The Arduino code controls the ultrasonic sensors and LEDs. It reads the distance measurements from the sensors and turns on an LED if an object (e.g., a user’s hand) is detected within a specified range. It also sends a signal to the p5.js application via serial communication when a flower should be spawned.

#include <Arduino.h>

// Define pins for the ultrasonic sensors and LEDs
#define NUM_SENSORS 5
int trigPins[NUM_SENSORS] = {2, 3, 4, 5, 6};
int echoPins[NUM_SENSORS] = {7, 8, 9, 10, 11};
int ledPins[NUM_SENSORS] = {12, 13, A0, A1, A2};

// Function to measure distance
long readDistance(int triggerPin, int echoPin) {
    digitalWrite(triggerPin, LOW);
    delayMicroseconds(2);
    digitalWrite(triggerPin, HIGH);
    delayMicroseconds(10);
    digitalWrite(triggerPin, LOW);
    long duration = pulseIn(echoPin, HIGH);
    return duration * 0.034 / 2; // Convert to distance in cm
}

void setup() {
    Serial.begin(9600);
    for (int i = 0; i < NUM_SENSORS; i++) {
        pinMode(trigPins[i], OUTPUT);
        pinMode(echoPins[i], INPUT);
        pinMode(ledPins[i], OUTPUT);
    }
}

void loop() {
    for (int i = 0; i < NUM_SENSORS; i++) {
        long distance = readDistance(trigPins[i], echoPins[i]);
        if (distance < 20) {
            digitalWrite(ledPins[i], HIGH);
            Serial.print("Bloom ");
            Serial.println(i + 1); // Send sensor number to p5.js
        } else {
            digitalWrite(ledPins[i], LOW);
        }
    }
    delay(100); // Debouncing
}

p5.js Code Overview: The p5.js application runs in a web browser and uses the serial communication data to create flowers on the screen each time a sensor is triggered. It also manages the playback of sound for each interaction.

// Define the Flower class for visual representation
class Flower {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.size = 5;
        this.growthRate = random(0.05, 0.2);
        this.fullSize = random(30, 70);
        this.petals = floor(random(4, 9));
        this.petalSize = this.fullSize / 2;
        this.color = [random(100, 255), random(100, 255), random(100, 255)];
    }

    grow() {
        if (this.size < this.fullSize) {
            this.size += this.growthRate;
        }
    }

    show() {
        push();
        translate(this.x, this.y);
        noStroke();
        fill(this.color[0], this.color[1], this.color[2]);
        for (let i = 0; i < this.petals; i++) {
            rotate(TWO_PI / this.petals);
            ellipse(0, this.size / 4, this.petalSize, this.size);
        }
        fill(255, 204, 0);
        ellipse(0, 0, this.size / 4, this.size / 4);
        pop();
    }
}

let flowers = [];
let serial;
let flowerSound;

function preload() {
    flowerSound = loadSound('bells.wav');
}

function setup() {
    let canvas = createCanvas(windowWidth, windowHeight);
    canvas.style('display', 'block');
    background(0);

    serial = new p5.SerialPort();
    serial.open('/dev/tty.usbmodem1101');
    serial.on('data', serialEvent);
}

function draw() {
    background(0);
    flowers.forEach(flower => {
        flower.grow();
        flower.show();
    });
}

function serialEvent() {
    let data = serial.readStringUntil('\n').trim();
    if (data.startsWith("Bloom")) {
        let parts = data.split(" ");
        if (parts.length === 2) {
            let index = parseInt(parts[1]) - 1;
            if (!isNaN(index) && index >= 0 && index < 5) {
                createFlower();
            }
        }
    }
}

function createFlower() {
    let x = random(width);
    let y = random(height);
    let flower = new Flower(x, y);
    flowers.push(flower);
    playSound();
}

function playSound() {
    if (flowerSound.isPlaying()) {
        flowerSound.stop();
    }
    flowerSound.play();
}

function keyPressed() {
    if (key === 'f' || key === 'F') {
        let fs = fullscreen();
        fullscreen(!fs);
    }
}

function windowResized() {
    resizeCanvas(windowWidth, windowHeight);
}

How the Code Works:
Serial Communication: p5.js uses the p5.serialport library to establish a serial connection with the Arduino. This connection allows it to receive data (like sensor triggers) from the Arduino.
Flower Generation: When a “Bloom” command is received via serial (indicating that a sensor was triggered), p5.js generates a digital flower at a random location on the canvas.
Sound Playback: Simultaneously with the flower generation, a sound file is played to provide auditory feedback, making the experience more immersive.

Planning the Interaction Flow:
Detection: A user places their hand over one of the 3D-printed roses.
Sensor Activation: The corresponding ultrasonic sensor detects the presence based on the distance and triggers a response.
LED Feedback: The LED beneath the detected rose lights up, providing immediate visual feedback.
Visual and Auditory Display: The user sees a new flower appearing on the screen and hears a sound, linking their physical interaction with a digital outcome.

Acknowledgements: Special thanks to Stefania for helping me with the idea and the implementation and to my fiancé for helping me setup a beautiful garden using a pizza box 🙂

Stressed Out? – Final Project

Link to sketch: https://editor.p5js.org/Hazpaz/full/1zy63dQBC

Concept

The finals week is here and stress is something that comes along with it. To soothe everyone’s nerves, I have come up with this project through which anyone can unleash their stress. Simple yet powerful. Inspired by one of the previous works related to stress done by previous students, I’ve decided to take advantage of the stress and anger of the students to create beautiful patterns. The idea is to use a stress ball to measure stress levels. When you squeeze the ball, it tells you how much stress you’re letting out. The project then shows a spiral pattern in action according to the pressure applied.

Images

User testing Video

How does the implementation work

Materials Used

  1. Arduino Uno
  2. Breadboard
  3. flex sensor
  4. jumper wires
  5. 330 ohms resistors
  6. Cardboards
Description of interaction design

When you squeeze the the stress ball, the spiral pattern in p5 starts animating and when the pressure applied on the stress ball increases, the Arduino reads it using the flex sensor and the speed of the spiral animation increases and the color changes to a dark red from a soft color, thus making it feel intense.

Arduino code
const int flexPin = A0;  // Analog pin for the flex sensor

int flexValue = 0; // Variable to store flex sensor value
int pressure = 0;  // Variable to store pressure level (mapped from flex sensor value)
int pressureMin = 10; // Minimum pressure value
int pressureMax = 1023; // Maximum pressure value

void setup() {
  Serial.begin(9600);
}

void loop() {
  // Read flex sensor value
  flexValue = analogRead(flexPin);
  
  // Map flex sensor value to pressure scale (0-100)
  pressure = map(flexValue, pressureMin, pressureMax, 0, 100);
  
  // Send pressure value to p5.js
  Serial.println(pressure);

  
  delay(100); // Adjust delay as needed
}

The A0 pin is used as the analog pin for the flex sensor.

// Map flex sensor value to pressure scale (0-100) 
pressure = map(flexValue, pressureMin, pressureMax, 0, 100);

This code maps the flex sensor reading to a range between 0-100.

// Send pressure value to p5.js 
Serial.println(pressure);

the above code is the communication from Arduino to p5.

p5.js code
function draw() {
  
  if (!patternsPage) {
    
    // Display starting page
    image(startingPage, 0, 0, windowWidth, windowHeight);
    
  } else {
    
    // Display patterns page
    image(bg, 0, 0, windowWidth, windowHeight); // Background image

    // Update and draw circles
    for (let i = 0; i < cols; i++) {
      
      for (let j = 0; j < rows; j++) {
        
        circles[i][j].display();
        circles[i][j].move(circleSpeed * pressure); // Update speed based on pressure
        
      }
    }

    
    // Draw pressure scales
    pressureMeterLeft.draw(pressure);
    pressureMeterRight.draw(pressure);

    
    // Draw texts
    fill('#704402'); // Set text color
    textAlign(CENTER, CENTER); // Align text to center
    text("Pressure: " + pressure, width / 2, 30); // Display pressure value
    

    // Display connection status
    if (!serialActive) { // If serial connection is not active
      fill('red'); // Set text color to red
      text("Press Space Bar to select Serial Port", windowWidth / 2, windowHeight - 20); // Display message to select serial port
    } 
    else {
      
      // If serial connection is active
      fill(0, 255, 0); // Set text color to green
      text("Connected", windowWidth / 2, windowHeight - 20); // Display connected message
      
    }
  }
}

This function controls the visualization and text elements. If on the starting page, it displays the `startingPage` image, and if on the patterns page, it shows the `bg` image. It updates and draws stress-relief circles, adjusting their movement speed according to the pressure level. Pressure scales are drawn on the left and right sides using `pressureMeterLeft` and `pressureMeterRight`, representing the pressure level visually. The pressure value is displayed at the top center, and a message about the status of the serial port connection is shown at the bottom center. If the serial connection is inactive, it prompts the user to press the space bar to select the serial port; otherwise, it indicates that the connection is established.

// Circle class for moving circles
class Circle {
  
  constructor(cx, cy, angle) {
    
    this.angle = angle;
    this.cx = cx;
    this.cy = cy;
    this.baseColor = color('#F0D6B0'); // Green color for low pressure
    this.highPressureColor = color('rgb(179,1,1)'); // Red color for high pressure/
    
  }

  display() {
    
    push();
    translate(this.cx, this.cy);
    noFill();
    let c = map(abs(this.angle % TWO_PI), 0, TWO_PI, 0, 255);
    c -= map(pressure, 0, maxPressure, 0, 100); // Darken color based on pressure
    c = constrain(c, 0, 255);
    let currentColor = lerpColor(this.baseColor, this.highPressureColor, pressure / maxPressure); // Interpolate between green and red based on pressure
    
    noStroke();
    fill(currentColor);
    let x = r * cos(this.angle);
    let y = r * sin(this.angle);
    arc(x, y, size, size, this.angle, this.angle + PI / 2);
    pop();
    
  }

  move(speed) {
    
    this.angle -= speed;
    
  }
}

The `Circle` class defines the behavior and appearance of the patterns. Each circle is constructed with parameters for its center position (`cx` and `cy`) and its starting angle (`angle`). It has properties for colors representing low and high pressure, where green indicates low pressure and red indicates high pressure. The `display()` method draws the circle, adjusting its color based on the current pressure level. The circle’s position is translated to its center, and its color is determined by interpolating between the base color and high pressure color. The `move()` method updates the angle of the circle, causing it to rotate at a speed determined by the pressure level.

// PressureMeter class for the pressure scale
class PressureMeter {
  
  constructor(x, y, width, height) {
    
    this.x = x; // X position
    this.y = y; // Y position
    this.width = width; // Width
    this.height = height; // Height
    
  }

  draw(pressure) {
    
    // Draw pressure scale box
    noFill();
    stroke(10);
    rect(this.x, this.y, this.width, this.height); // Draw the rectangle outline\
    

    // Fill the pressure scale rectangle based on pressure
    let fillAmount = map(pressure, 0, maxPressure, 0, this.height); // Map pressure to the height of the rectangle
    
    fill('#985B00'); // Set fill color to brown
    noStroke();
    rect(this.x, this.y + this.height - fillAmount, this.width, fillAmount); // Draw the filled rectangle
    
  }
}

The `PressureMeter` class contains the visual representation of the pressure scale. It is initialized with parameters for its position (`x` and `y`), as well as its width and height. The `draw()` method is responsible for rendering the pressure scale on the canvas. It first draws the outline of the pressure scale box using the provided position, width, and height. Then, it calculates the fill amount based on the current pressure level, mapping it to the height of the rectangle. The fill color is set to brown, and a filled rectangle is drawn inside the outline, representing the current pressure level.

Embedded sketch

Description of communication between Arduino and p5.js

A stress ball is connected to a flex sensor, which is connected to A0 pin in Arduino Uno. The Arduino is connected to p5 using serial connection. When the stress ball is squeezed, the Arduino reads it with the help of the flex sensor and then maps the sensor reading between 0-100. The Arduino sends this mapped sensor readings to p5 through serial connection and the p5 uses this sensor readings to adjust the speed and color of the animation of the spiral pattern.

// Send pressure value to p5.js 
Serial.println(pressure);

the above code is the communication from Arduino to p5.

Aspects of the project I’m proud of

I’m especially proud of the pressure meter scale and the responsive pattern animation in the project.

// Initializing the two pressure scales
let scaleRectXLeft = 100; // X position of the pressure scale rectangle for the left side
let scaleRectXRight = windowWidth - 100 - rectWidth; // X position of the pressure scale rectangle for the right side

pressureMeterLeft = new PressureMeter(scaleRectXLeft, scaleRectY, rectWidth, rectHeight);
pressureMeterRight = new PressureMeter(scaleRectXRight, scaleRectY, rectWidth, rectHeight);

This block of code shows the initialization two pressure scales, one for the left side and one for the right side of the canvas. I like this because it allows for a visually balanced presentation of the pressure levels on both sides of the screen, creating a sense of symmetry and organization. By positioning the scales at specific x-coordinates, we ensure they are consistently placed regardless of the canvas size. This contributes to the overall aesthetic appeal and user experience of the project.

// Set initial positions
 scaleRectY = height / 2 - rectHeight / 2; // Y position of the pressure scale rectangle
 cols = floor(width / (size * 1.5)); // Number of columns
 rows = floor(height / (size * 1.5)); // Number of rows

 
 // Initialize circles array
 circles = [];
 
 // Loop for the columns
 for (let i = 0; i < cols; i++) {
   
   // Initialize an array for each column
   circles[i] = [];
   
   // Loop for the rows
   for (let j = 0; j < rows; j++) {
     
     // Calculate the x and y positions for the circle
     let x = size / 2 + i * (size * 1.5);
     let y = size / 2 + j * (size * 1.5);
    
     // calculate the distance from the center of the canvas
     let d = dist(x, y, width / 2, height / 2);
     
     // Calculate the angle based on the distance and constant k
     let angle = d / k;
     
     //storing in the array
     circles[i][j] = new Circle(x, y, angle);

This block of code sets the initial positions for the pressure scale and initializes the circles for the pattern animation. I like it because it ensures that the pattern is evenly distributed across the canvas, regardless of its size. By calculating the number of columns and rows based on the canvas dimensions and the size of the circles, it dynamically adjusts to fit the available space. This allows for a consistent and visually pleasing pattern layout, enhancing the overall aesthetic appeal of the project.

Schematics

resources used

Challenges faced and overcoming them

I’ve faced a number of challenges with this project.

  • Initially, there were challenges with implementing a circle pattern. The code for the circle pattern didn’t function correctly and didn’t respond to the sensor readings as intended.
  • To address this issue, I decided to draw inspiration from a spiral pattern which I found online, which proved to be more responsive to the sensor readings.
  • Another challenge was adding the option for users to choose from a variety of patterns. Despite attempts, I encountered difficulties in running multiple patterns simultaneously.

future improvements

  • Offer a variety of stress-relief patterns for users to choose from, catering to different preferences and moods.
  • Enhance the experience by adding soothing music or relaxing sounds to accompany the patterns. For example, if users select a pattern resembling waves or clouds, they can listen to calming nature sounds that match the theme.

IM show documentation

Reviving Retro Gaming: Atari Classics with a Modern Twist

Reviving Retro Gaming: Atari Classics with a Modern Twist

Concept:
This project aims to breathe new life into classic Atari games by reimagining them with interactive physical controls and a unique display using flip-dot technology. The initial implementation features two iconic games, Tetris and Snake, with the potential for future expansion.

The IM Showcase Documentation & Video: 

Implementation:
– Arduino Uno
– Buttons
– Wires
– Resistors
– Flip-dots

Interaction Design:
– Buttons are connected to the Arduino and act as input devices for controlling the games.
– The Arduino communicates with the Processing program via serial communication, sending button press information.
– Processing handles game logic and generates the visuals displayed on the flip-dots.
– Another serial communication channel is established between Processing and the flip-dots to send display data.

Arduino Code:
– The Arduino code reads the button states and sends them to Processing via serial communication.

// Arduino Sketch
void setup() {
  Serial.begin(9600);         // Start serial communication at 9600 baud rate
  pinMode(3, INPUT_PULLUP);   // Set pin 3 as an input with an internal pull-up resistor
  pinMode(4, INPUT_PULLUP);   // Set pin 4 as an input with an internal pull-up resistor
  pinMode(5, INPUT_PULLUP);   // Set pin 5 as an input with an internal pull-up resistor
  pinMode(6, INPUT_PULLUP);   // Set pin 6 as an input with an internal pull-up resistor
}

void loop() {
  // Read the state of the buttons
  int buttonState3 = digitalRead(3);
  int buttonState4 = digitalRead(4);
  int buttonState5 = digitalRead(5);
  int buttonState6 = digitalRead(6);

  // Send the button states over serial, separated by commas
  Serial.print(buttonState3);
  Serial.print(",");
  Serial.print(buttonState4);
  Serial.print(",");
  Serial.print(buttonState5);
  Serial.print(",");
  Serial.println(buttonState6);  // 'println' for a new line at the end

  delay(100);  // Delay for a short period to avoid sending too much data
}

Processing Code:
The Processing code performs several key tasks:
– Game Logic: Manages game mechanics, including player movement, collision detection, and game state.
– Visuals: Generates the graphics for each game to be displayed on the flip-dots.
– Serial Communication: Receives button input data from the Arduino and sends display data to the flip-dots.

Casting to Flipdots:

The Processing code utilizes a custom library to communicate with the flipdots and send the display data.

import processing.net.*;
import processing.serial.*;

void cast_setup() {
  if (!config_cast) return;
  if (castOver == 1) {
    for (int i = 0; i < netAdapters.length; i++) {
      String[] adapterAddress = split(netAdapters[i], ':');
      adaptersNet[i] = new Client(this, adapterAddress[0], int(adapterAddress[1]));
    }
  }

  else if (castOver == 2) {
    // printArray(Serial.list());
    for (int i = 0; i < serialAdapters.length; i++) {
      String[] adapterAddress = split(serialAdapters[i], ':');
      adaptersSerial[i] = new Serial(this, adapterAddress[0], int(adapterAddress[1]));
    }
  }
}

void cast_broadcast() {
  if (!config_cast) return;
  int adapterCount = netAdapters.length;
  if (castOver == 2) {
    adapterCount = serialAdapters.length;
  }

  for (int adapter = 0; adapter < adapterCount; adapter++) {
    for (int i = 0; i < panels.length; i++) {
      if (panels[i].adapter != adapter) continue;
      cast_write(adapter, 0x80);
      cast_write(adapter, (config_video_sync) ? 0x84 : 0x83);
      cast_write(adapter, panels[i].id);
      cast_write(adapter, panels[i].buffer);
      cast_write(adapter, 0x8F);
    }
  }

  if (config_video_sync) {
    for (int adapter = 0; adapter < adapterCount; adapter++) {
      cast_write(adapter, 0x80);
      cast_write(adapter, 0x82);
      cast_write(adapter, 0x8F);
    }
  }
}


void cast_write(int adapter, int data) {
  if (castOver == 1) {
    adaptersNet[adapter].write(data);
  }
  else if(castOver == 2) {
    adaptersSerial[adapter].write(data);
  }
}
void cast_write(int adapter, byte data) {
  cast_write(adapter, data);
}
void cast_write(int adapter, byte[] data) {
  if (castOver == 1) {
    adaptersNet[adapter].write(data);
  }
  else if(castOver == 2) {
    adaptersSerial[adapter].write(data);
  }
}

Challenges and Solutions:
Initially, the project aimed to use p5.js to handle the game logic and visuals. However, challenges arose in establishing reliable communication and sending data to the flip-dots. To overcome this, the decision was made to switch to Processing, which provided a more stable environment for serial communication and flip-dot control.

Proud Achievements:
Successful Integration of Hardware and Software: The project combines Arduino, buttons, and flip-dots with Processing to create a unique gaming experience.

Retro Games with a Modern Twist: Classic Atari games are revitalized with physical controls and a visually appealing flip-dot display.

Future Improvements:
Expand Game Library: Add more classic Atari games or even explore the possibility of creating new games specifically designed for this platform.

Enhance Visuals: Experiment with different animation techniques and graphical styles to further enhance the visual appeal of the games on the flip-dot display.

Refine User Interface: Explore additional input methods or create a more intuitive menu system for navigating between games.

Explore p5.js Integration: Revisit the possibility of using p5.js in the future if more robust libraries or solutions for flip-dot communication become available.

Instructions

 Check the full code on GitHub: https://github.com/pavlyhalim/Atari

 

Luke Nguyen – Final Project – Can You See the Music?

Link to p5js code: https://editor.p5js.org/luke.s.ng/sketches/U8Xmnxnwu

Concept:

My final project is a Audio Workstation that can allow users to input music from Arduino using a special glove into p5 and visualize it. I was inspired by this Mimu glove created by MI.MU GLOVES LIMITED, which allows users to make music through movement using only a pair of glove with a lot of physical computing programmed into the device. My project is a simplified version of this, which only utilizes Arduino. I used p5.js to enhance the user’s experience visually, allowing them to see how their music is seen to the eyes.

User testing:

Some pictures / video of project interactions & comments + feedback:

What parts of your project did you feel the need to explain? How could you make these areas more clear to someone that is experiencing your project for the first time?

I got more than 10 people who came and interacted with my device, and a few of them were my classmates and friends from other classes.

Ume passed by, and I also invited her to test it. Afterward, she suggested that for future improvement, I could consider implementing this device in a way that it can trigger many musical instruments at the same time. She commented that this project of mine is already advanced enough, and her suggestion can be considered for a later, more advanced stage of learning interactive media.

Other than Ume, your friend also suggested that I can keep this device as a simple music maker but develop it into one that can incorporate more musical instruments.

Implementation/Description of interaction design:

Users can create music notes using Arduino and trigger them to appear inside p5. The Arduino board makes use of two major components: four buttons representing four fingers and one distance sensor. Each finger is assigned a different music instrument: index finger – piano, middle finger – drum, ring finger – guitar, pinky – synthesizer pulses.

The program currently supports the C major scale, consisting of 7 basic notes: C, D, E, F, G, A, B. Piano notes range from C4 to B5. Drum rhythm ranges from heavy kick to high crash. Guitar notes range from C3 to A3. And synthesizer pulses range from C6 to A6.

The user can control one instrument using two simultaneous actions: pressing the buttons attached to the tip of the glove they wear, users can trigger music notes inside p5.js. Simultaneously, by VERTICALLY controlling a distance sensor placed on the table, they can select/control the notes by changing the frequency of the buttons/their fingertips. Users can press other buttons at the same time to play with other musical instruments.

Additionally, they can press “R” and then play one instrument to record, then press “S” to stop, then press “L” to loop and save the first instrument. They can then input another instrument. The process can be repeated for other instruments. Users can keep doing that until they have generated something either funky, following a stream of consciousness, or a nice melody.

p5js will take the notes users input, put them through an amplitude analyzer and create a visualization on screen.

Arduino code: 

const int trigPin = 7;
const int echoPin = 8;

void setup() {
  // Start serial communication so we can send data
  // over the USB connection to our p5js sketch
  Serial.begin(9600);

  pinMode(trigPin,OUTPUT);
  pinMode(echoPin,INPUT);

  // start the handshake
  while (Serial.available() <= 0) {
    Serial.println("0,0"); // send a starting message
    delay(300);            // wait 1/3 second
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}

void loop() {
  // wait for data from p5 before doing something
  while (Serial.available()) {
    // digitalWrite(LED_BUILTIN, HIGH); // led on while receiving data
    if (Serial.read() == '\n') {
      // detecting the pressure sensor
      int PressureSensor = analogRead(A0);
       delay(5);

      int PressureSensor2 = analogRead(A2);
      delay(5);

      int PressureSensor3 = analogRead(A4);
      delay(5);

      int PressureSensor4 = analogRead(A6);
      delay(5);


      long duration, inches, cm;

  // The PING))) is triggered by a HIGH pulse of 2 or more microseconds.
  // Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  // The same pin is used to read the signal from the PING))): a HIGH pulse
  // whose duration is the time (in microseconds) from the sending of the ping
  // to the reception of its echo off of an object.
  duration = pulseIn(echoPin, HIGH);

  // convert the time into a distance
  inches = microsecondsToInches(duration);

  Serial.print(PressureSensor);
  Serial.print(',');
  Serial.print(inches);
  Serial.print(',');
  Serial.print(PressureSensor2);
  Serial.print(',');
  Serial.print(PressureSensor3);
  Serial.print(',');
  Serial.println(PressureSensor4);

  delay(100);
    }
    // digitalWrite(LED_BUILTIN, LOW);
  }

}

long microsecondsToInches(long microseconds) {
  // According to Parallax's datasheet for the PING))), there are 73.746
  // microseconds per inch (i.e. sound travels at 1130 feet per second).
  // This gives the distance travelled by the ping, outbound and return,
  // so we divide by 2 to get the distance of the obstacle.
  // See: https://www.parallax.com/package/ping-ultrasonic-distance-sensor-downloads/
  return microseconds / 74 / 2;
}

long microsecondsToCentimeters(long microseconds) {
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the object we
  // take half of the distance travelled.
  return microseconds / 29 / 2;
}

p5.js code:

The p5js code is too long to post here, so I’ll include aspects of code that I’m really proud of.

function startPlayingGame() {

// remember to calibrate the note accordingly
background(0);

console.log(note_piano);
if (note_piano > 200){
        if (freq_piano >= 1 && freq_piano <= 2){
          c4_piano.play(); c4_piano.setVolume(0.1);    
    }
        else if (freq_piano >= 3 && freq_piano <= 4){
            d4_piano.play(); d4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 5 && freq_piano <= 6){
            e4_piano.play(); e4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 7 && freq_piano <= 8){
            f4_piano.play(); f4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 9 && freq_piano <= 10){
            g4_piano.play(); g4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 11 && freq_piano <= 12){
            a4_piano.play(); a4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 13 && freq_piano <= 14){
            b4_piano.play(); b4_piano.setVolume(0.1);
    }
        else if (freq_piano >= 15 && freq_piano <= 16){
            c5_piano.play(); c5_piano.setVolume(0.1);
    }
        else if (freq_piano >= 17 && freq_piano <= 18){
            d5_piano.play(); d5_piano.setVolume(0.1);
    }
        else if (freq_piano >= 19 && freq_piano <= 20){
            e5_piano.play(); e5_piano.setVolume(0.1);
    }
        else if (freq_piano >= 21 && freq_piano <= 22){
            f5_piano.play(); f5_piano.setVolume(0.1);
    }
        else if (freq_piano >= 23 && freq_piano <= 24){
            g5_piano.play(); g5_piano.setVolume(0.1);
    }
        else if (freq_piano >= 25 && freq_piano <= 26){
            a5_piano.play(); a5_piano.setVolume(0.1);
    }
    else if (freq_piano >= 27 && freq_piano <= 28){
            b5_piano.play(); b5_piano.setVolume(0.1);
    }
  }


  if (drum > 200){
    if (freq_piano >= 1 && freq_piano <= 3){
      heavykick.play(); heavykick.setVolume(0.1);
    }
        else if (freq_piano >= 4 && freq_piano <= 6){
            lightkick.play(); 
            lightkick.setVolume(0.1);}

            else if (freq_piano >= 7 && freq_piano <= 9){
              snaresidekick.play(); 
              snaresidekick.setVolume(0.1);}

              else if (freq_piano >= 10 && freq_piano <= 12){
                lowtom.play(); 
                lowtom.setVolume(0.1);}

                else if (freq_piano >= 13 && freq_piano <= 15){
                  snarecenter.play(); 
                  snarecenter.setVolume(0.1);}

                  else if (freq_piano >= 16 && freq_piano <= 18){
                    hihatopen.play(); 
                    hihatopen.setVolume(0.1);}

                    else if (freq_piano >= 19 && freq_piano <= 21){
                      hitom.play(); 
                      hitom.setVolume(0.1);}

                      else if (freq_piano >= 22 && freq_piano <= 24){
                        crash.play(); 
                        crash.setVolume(0.1);}
    
  }

  if (guitar > 200){
    if (freq_piano >= 1 && freq_piano <= 3){
      c3_guitar.play();
      c3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 4 && freq_piano <= 6){
      d3_guitar.play();
      d3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 7 && freq_piano <= 9){
      e3_guitar.play();
      e3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 10 && freq_piano <= 12){
      f3_guitar.play();
      f3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 13 && freq_piano <= 15){
      g3_guitar.play();
      g3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 16 && freq_piano <= 18){
      a3_guitar.play();
      a3_guitar.setVolume(0.1);
    }

    else if (freq_piano >= 19 && freq_piano <= 21){
      b3_guitar.play();
      b3_guitar.setVolume(0.1);
    }
  }



  if (synth > 200){
    if (freq_piano >= 1 && freq_piano <= 3){
      c6_synth.play();
      c6_synth.setVolume(0.1);
    }

    else if (freq_piano >= 4 && freq_piano <= 6){
      d6_synth.play();
      d6_synth.setVolume(0.1);
    }
    else if (freq_piano >= 7 && freq_piano <= 9){
      e6_synth.play();
      e6_synth.setVolume(0.1);
    }

    else if (freq_piano >= 10 && freq_piano <= 12){
      f6_synth.play();
      f6_synth.setVolume(0.1);
    }

    else if (freq_piano >= 13 && freq_piano <= 15){
      g6_synth.play();
      g6_synth.setVolume(0.1);
    }

    else if (freq_piano >= 16 && freq_piano <= 18){
      a6_synth.play();
      a6_synth.setVolume(0.1);
    }

    else if (freq_piano >= 19 && freq_piano <= 21){
      b6_synth.play();
      b6_synth.setVolume(0.1);
    }
  }
// Visualization for the drum 
  push()
    angleMode(DEGREES);
    colorMode(HSB);
    spectrum2 = fft2.analyze();

    // background(0);

    noStroke();
    translate(windowWidth / 2, windowHeight / 2);
    //beginShape();
    for (let i = 0; i < spectrum2.length; i++) {
      let angle = map(i, 0, spectrum2.length, 0, 360);
      let amp = spectrum2[i];
      
    //change the shape of the visualizer
      let r2 = map(amp, 0, 512, 50, 500);
      
    //create the circle
      let x2 = (r2 + 50) * cos(angle);
      let y2 = (r2 + 50) * sin(angle);
      
    //color the bar
      stroke(i, 100, 100);
      line(0, 0, x2, y2);
      }
  pop()

    angleMode(RADIANS)


// Visualization for the piano
  push()
  fill(0, 0, 0, 5);
  stroke(0, 255, 255);
  angleMode(RADIANS)

    for (let i = 0; i < n; i++) {
      theta.push(random(0, 2 * PI));
      dir.push(1);
      r.push(random(30, 380));
      rdir.push(1);
      c.push(createVector(windowWidth/2, windowHeight/2));
    }

    rect(0, 0, windowWidth, windowHeight);
    let spectrum = fft.analyze();

    // Calculate average amplitude to detect beats
    let level = amplitude.getLevel();

    // Adjust animation based on music intensity or beats
    for (let i = 0; i < n; i++) {
      theta[i] = theta[i] + (PI / 100) * dir[i];
      rdir[i] = checkr(rdir[i], r[i]);
      r[i] = r[i] + rdir[i];
      x = c[i].x + r[i] * cos(theta[i]);
      y = c[i].y + r[i] * sin(theta[i]);
      point(x, y);

      // modify animation based on beat detection
      let bass = fft.getEnergy("bass");
      let treble = fft.getEnergy("treble");

      // adjust wave parameters based on music energy
      r[i] = map(bass, 0, 255, 30, 380);
      dir[i] = map(treble, 0, 255, -1, 1);
  }
  pop()

// waveform visualization
  push()
    angleMode(DEGREES);
    stroke('rgb(255,182,222)');
    noFill();
    translate(windowWidth / 2, windowHeight / 2);

    let wave = fft_waveform.waveform();

    for (let k = -1; k <= 1; k += 2) {
      beginShape();
      for (let i = 0; i <= 180; i+= 0.7) {
        let j = floor(map(i, 0, windowWidth, 0, wave.length - 1));

        let r3 = map(wave[j], -1, 1, 100, 250);

        let x3 = r3 * sin(i) * k;
        let y3 = r3 * cos(i);
        vertex(x3, y3);
      }
      endShape();
    }
  pop()

// waveform of the particles
  push()
    translate(windowWidth/2, windowHeight/2);
    
    let spectrum4 = fft_particles.analyze(); // Analyze frequency spectrum
    
    // console.log(spectrum);

    let currentHasFrequency = spectrum4.some(freq => freq > 0);
    
    // If frequency detected and no particle created for this frequency yet
    if (currentHasFrequency && !particleCreated && spectrum4[0]) {
      let p = new Particle();
      particles.push(p);
      particleCreated = true; // Set flag to true to prevent continuous particle creation for this frequency
    }

    // Update and display particles
    for (let i = particles.length - 1; i >= 0; i--) {
      particles[i].show();
      particles[i].update();
    }
    
    // Reset flag after particle creation
    if (particleCreated && particles.length > 0) {
      particleCreated = false;
    }
  pop()
// function to initiate commands.
function keyPressed() {
  if (key == " ") {
    setUpSerial();
  }

  if (key == 'r') {
    startRecording();
  } else if (key == 's') {
    stopRecording();
  } else if (key == 'p') {
    playRecording();
  } else if (key == 'l') {
    loopRecording();
  }
}


// functions to initiate music-making options
function startRecording() {
  recorder.record(soundFile);
  console.log('Recording started...');
  recordingStarted = true;
}

function stopRecording() {
  recorder.stop();
  console.log('Recording stopped.');
}

function playRecording() {
  soundFile.play();
  console.log('Playback started...');
}

function loopRecording(){
  soundFile.loop();
    isLooping = true;
}

 

Communication between Arduino and p5.js:

From p5js side:

function readSerial(data) {
  ////////////////////////////////////
  //READ FROM ARDUINO HERE
  ////////////////////////////////////

  if (data != null) {
    // make sure there is actually a message
    // split the message
    let fromArduino = split(trim(data), ",");
    print(fromArduino);
    // if the right length, then proceed
    if (fromArduino.length == 5) {
      // only store values here
      // do everything with those values in the main draw loop

      // We take the string we get from Arduino and explicitly
      // convert it to a number by using int()
      // e.g. "103" becomes 103
      
      if (isLooping == false){
        note_piano = int(fromArduino[0]);
        drum = int(fromArduino[2]);
        guitar = int(fromArduino[3]);
        synth = int(fromArduino[4]);
      }

      // get frequency for all types of sound
      freq_piano = int(fromArduino[1]);

      if (!recordingStarted && freq_piano != 0) {
        startRecording(); // Start recording
      }
    }
    //////////////////////////////////
    //SEND TO ARDUINO HERE (handshake) - MUST have these lines for Arduino to send to p5
    //////////////////////////////////
    let sendToArduino = fromArduino + "\n";
    writeSerial(sendToArduino);
  }

From Arduino side:

Serial.print(PressureSensor);
  Serial.print(',');
  Serial.print(inches);
  Serial.print(',');
  Serial.print(PressureSensor2);
  Serial.print(',');
  Serial.print(PressureSensor3);
  Serial.print(',');
  Serial.println(PressureSensor4);

Schematics:

Some issues and aspects of the project I’m particularly proud of:

I was particularly proud of creating the visualizer. I took some time to learn how to use the p5.FFT function inside p5js. Initially, I watched this Sound Visualization: Frequency Analysis with FFT – p5 tutorial from Daniel Shiffman: https://www.youtube.com/watch?v=2O3nm0Nvbi4. But the tutorial is about syncing the visualizer with a played sound. I had a lot of trouble making the visualizer sync to every time the music note is triggered. I also had some issues combining the visualizers onto one plane and arranging their positions. I had to be really careful with controlling the variables.

I also wanted the users to have the option to record what they play, and figuring out how to record took me a lot of time. I kept failing at this part as I initially approached the code from the perspective of adding each note and each sound byte to an array and playing it back later.

BUT the aspect that I was struggling with the most was about using a pressure sensor. Its variable range varied way too much. The threshold for triggering the music notes when I pressed kept changing from time to time. The music generated inside p5 was not good at all. I was so fixated on this idea as I thought it was the only possible option. Until I discovered the button. It provided a much more stable parameter range of extremely high values and extremely low values for controlling the threshold as a digital device. The buttons I found are really soft to the touch, and they can be attached to the tip of the glove.

Another issue I ran into was about establishing a connection between Arduino and p5js:

I kept running into this error:

and

Pressure sensor doesn’t run when adding while (Serial.available() <= 0) {} and while (Serial.available()) {}

Future improvement:

I approached the project hoping to have only one visualizer for each instrument, but it seems that the object p5.FFT is a universal object that can’t take the frequency of any individual musical instrument. Therefore, I have to think of ways to make that happen for the future.

Also, I hope to be able to incorporate the accelerometer or gyrometer to monitor the motion of wrists and use that to trigger drum pattern/rhythm.

Observations from IM Showcase interactions for future improvements:

Everyone intuitively followed the screen instructions, but almost everyone seems to not know how to proceed at the connecting p5js with Arduino part.

There are two aspects where users are mostly confused about: how to use the distance sensor and how to connect p5js to Arduino. In terms of the distance sensor, it doesn’t come to them intuitively that they can hover their left hand up and down above the distance sensor to trigger different frequencies, hence different musical notes. For a few users, I had to explain to them how they should operate the project, and for them, I had to provide very detailed instructions. I’ve implemented the text “Press Space Bar to connect to the glove,” but it seems that only those who know how to set up serial communication understand it; those who don’t seem not. So for an area of improvement, I’m thinking of having it run as soon as the user presses the “ROCK ON” button to start the visualizer.

Reading Reflection – Week 11

This reading challenges the current trend in technology interfaces, which he criticizes as merely “Pictures Under Glass.” He argues that this approach severely underutilizes the complex capabilities of human hands, which are not only meant for touching but also for manipulating objects in rich and varied ways. This is intriguing because it prompts us to rethink how we interact with technology. Victor’s perspective is a wake-up call to consider more innovative and natural interactions beyond the confines of screens and opens up exciting possibilities for future technological developments that genuinely enhance human abilities rather than constrain them.

What I find particularly interesting is the emphasis on the need for inspired people to drive this change. It’s a reminder of the power of visionairy thinking in technology and the responsibility of creators and funders to strive for meaningful advancements, not just incremental changes, not just accepting technology as it is, but imagining and working towards what it could be to enhance human interaction.

Final Project Presentation

Concept

Inspired by “GarageBand”, an application on Apple devices, I wanted to create an interface that allows the users to play musical instruments wherever they are at. As the title of the program suggests (“Corner of the Room Band”), the goal of this interaction was to provide access to instruments whether the users are in their room or anywhere else. When I was younger, I used to play with GarageBand. While it provided access to instruments such as piano and drum, I always hoped a different instrument to be added. However, my hope was not met through GarageBand and hence my project was made to satisfy my desire.

User Test

After I have built and created the project, I asked my friend to use the program. The video and image below illustrates my friend using the program:

User Testing

After the user testing, I made no big changes to p5 or Arduino because everything was pretty much set. My friend successfully navigated and used the program without difficulties.

Implementation

The overall interaction looks like this: p5 changes pages or the “gameState”, buttons on Arduino sends signal to p5 when pressed, sound files that correspond to each button is played through the laptop.

More precisely, the user navigates through different pages by clicking on the screen made through p5. When the user enters either the piano or flute page, sound files that are tied to each button on Arduino will be played when pressed. The potentiometer on Arduino is used to control the volume of the sound that is projected through the laptop.

  1. p5.js

For this project, p5 was heavily used. It was used to create the game interface and to call up the sounds files of each instrument. Although p5 was mainly used to code for the project, nothing too complex or complicated was performed.

function homePage () {
  imageMode(CORNER);
  image(homeImg, 0, 0, windowWidth, windowHeight);
  
  //start button
  fill('white');
  rectMode(CENTER);
  rect(windowWidth / 2, windowHeight * 0.8, windowWidth * 0.11, windowWidth * 0.05,30);
  
  //start button text
  fill('black');
  strokeWeight(3);
  textSize(windowWidth / 30);
  text("Start", windowWidth / 2.14, windowHeight * 0.812);
}

Like the code shown above, I created functions for the different pages of the project. Under each function, I made buttons that would help the users navigate through the interface.

function mousePressed () {
  //transitioning from home page to inst. page
  if (gameState == "home" && 
    mouseX > windowWidth / 2 - windowWidth * 0.2 &&
    mouseX < windowWidth / 2 + windowWidth * 0.2 &&
    mouseY > windowHeight * 0.8 - windowWidth * 0.05 &&
    mouseY < windowHeight * 0.8 + windowWidth * 0.05
    ) {
      gameState = "inst";
  }

Function mousePressed was written to actually allow the user to transition from one page to another by clicking on the buttons that were created previously.

function serialEvent() {
  //chords
  playC=int(fromArduino[0])
  playD=int(fromArduino[1])
  playE=int(fromArduino[2])
  playF=int(fromArduino[3])
  playG=int(fromArduino[4])
  volume=int(fromArduino[5])
  
  // Check if the message is "ButtonC"
  if (playC==0 && gameState == "piano") { 
    
    //controlling the volume 
    pianoC.setVolume (realVolume);
    
    // Play the pianoC sound file
    if (pianoC.isPlaying()) {
      // Stop the sound if it's already playing
      pianoC.stop();
    }
    pianoC.play();
  }
  

  // Check if the message is "ButtonD"
  if (playD==0 && gameState == "piano") { 
    
    //controlling the volume 
    pianoD.setVolume (realVolume);
    
    // Play the pianoD sound file
    if (pianoD.isPlaying()) {
      // Stop the sound if it's already playing
      pianoD.stop(); 
    }
    pianoD.play();
  }
  
  
  // Check if the message is "ButtonE"
  if (playE==0 && gameState == "piano") { 
    
    //controlling the volume 
    pianoE.setVolume (realVolume);
    
    // Play the pianoE sound file
    if (pianoE.isPlaying()) {
      // Stop the sound if it's already playing
      pianoE.stop(); 
    }
    pianoE.play();
  }
  
  
  // Check if the message is "ButtonF"
  if (playF==0 && gameState == "piano") { 
    
    //controlling the volume 
    pianoF.setVolume (realVolume);
    
    // Play the pianoF sound file
    if (pianoF.isPlaying()) {
      // Stop the sound if it's already playing
      pianoF.stop(); 
    }
    pianoF.play();
  }
  
  
  // Check if the message is "ButtonG"
  if (playG==0&& gameState == "piano") { 
    
    //controlling the volume 
    pianoG.setVolume (realVolume);
    
    // Play the pianoC sound file
    if (pianoG.isPlaying()) {
      // Stop the sound if it's already playing
      pianoG.stop(); 
    }
    pianoG.play();
  }

The code above is what is essential to the project and what I am proud of. Using the data received from Arduino (which tells p5 when a certain button is pressed and what the value of the potentiometer is), p5 plays the sound file and controls the volume of the sound being projected. For instance, when p5 understands that ButtonC on Arduino is pressed at the same time the gameState is “piano“, a sound file “pianoC” will be played.  The same thing was done for other notes and the flute.

//this is under function draw ()

//mapping the range of the volume
 realVolume = map(volume, 0, 1023, 0.0, 1.0);
 print(realVolume);

Furthermore, using the value collected by potentiometer in Arduino, the value was mapped to fit in the range of the volume on p5.

function readSerial(data) {
  ////////////////////////////////////
  //READ FROM ARDUINO HERE
  ////////////////////////////////////
  if (data != null) {
     fromArduino = split(trim(data), ",");
     message = fromArduino;
     //print(message);
     serialEvent ();      
    }

Lastly, using function readSerial, p5 read the data sent from Arduino and used the data to perform the tasks mentioned above.

Home page:

Inst page:

Piano page:

Flute page:

2. Arduino

With Arduino, buttons and potentiometer were used. Each button pin was given a const int name that corresponded to its pin number and each button an int name that digital reads the information from its pin. For example, the button for the note C was plugged into pin no. 2 (const int buttonCPin) and the data collected from pin no. 2 (int ButtonC) was used to send a string of information to p5.

Furthermore, const int potPin was used to collect data from A0, which is where potentiometer was connected. Then int volume was used to analog read the potentiometer value which was sent to p5 as a string.

//Define the button pins
const int buttonCPin = 2;
const int buttonDPin = 3;
const int buttonEPin = 4;
const int buttonFPin = 5;
const int buttonGPin = 6;
const int potPin = A0;
char button = 0;


void setup() {
  // Set the button pins as input with internal pull-up resistor
  pinMode(buttonCPin, INPUT_PULLUP); 
  pinMode(buttonDPin, INPUT_PULLUP);
  pinMode(buttonEPin, INPUT_PULLUP);
  pinMode(buttonFPin, INPUT_PULLUP);
  pinMode(buttonGPin, INPUT_PULLUP);
  // Initialize serial communication
  Serial.begin(9600); 


  // start the handshake
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH); // on/blink while waiting for serial data
    Serial.println("0,0"); // send a starting message
    delay(300);            // wait 1/3 second
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}


void loop() {
  while (Serial.available()) {
    digitalWrite(LED_BUILTIN, HIGH); // led on while receiving data
  int volume = analogRead(potPin); // Read the potentiometer value

//creating int and string to send to p5.js
int ButtonC = digitalRead(buttonCPin);
int ButtonD = digitalRead(buttonDPin);
int ButtonE = digitalRead(buttonEPin);
int ButtonF = digitalRead(buttonFPin);
int ButtonG = digitalRead(buttonGPin);

  Serial.println(String(ButtonC) + "," + String(ButtonD) + "," + String(ButtonE)+ "," +String(ButtonF) + "," + String(ButtonG)+ "," + String(volume)); 
  }

  }

As the code above shows, code for Arduino is comparatively short. It simply reads signal from the pins of the button and the potentiometer and creates a string to send to p5.

Aspects of the Project that I am Proud of

First, I am glad I changed from using the speaker on Arduino to the PC speaker. The sound quality was poor when projected through the small speaker on Arduino and to enhance the sound quality and user experience, I removed the Arduino speaker and used the PC speaker instead.

Second, using string to communicate data between p5 and Arduino was very convenient and helpful. If I were to use other methods, the code for both p5 and Arduino would have been longer. However, because I used strings, the data that is being sent from Arduino to p5 was concise and very organized. In this way, when I printed the data on p5, I received one array with all the information I need to run the interaction.

Also, I am proud of the console/button box that I created. While it is a very simple box with the buttons attached, the box is well designed to fit the theme of the project.

Below are pictures that show the process of building the box:

Challenged Faced and How I Overcame Them

Initially, I was going to use the potentiometer to control the frequency of the musical notes. However, I changed my plan and used it to control the volume instead because I could not find Arduino sound files that plays the flute sound. Therefore, I had to upload piano and flute sound files on p5, which prohibits the control of the frequency. Although I had to give up controlling the frequency of the sound, controlling of the volume seems like a good replacement of what the potentiometer could do.

Also, I struggled to write the code for controlling the volume on p5. Now that I have written all the code, the code for controlling the volume is actually very simple. The mistake I made was not mapping the range of the volume. From Arduino, values ranging from 0-1023 was being sent to p5 and in order for me to use this value to control the volume, I had to map it so that it fits under the range of 0-1. After I had this code written, I was able to control the volume.

Lastly, I encountered difficulties finding sound flies. At first, my plan was to have piano and guitar on the program. However, I was not able to find a good sound file for the guitar on the internet. Therefore, I changed the guitar to flute instead and successfully found sound files for the flute.

Areas for Future Improvement

For improvements, I would like to add more buttons so that the user can play more various musical notes. As of now, I only used 5 buttons for the first 5 musical notes (C, D, E, F, G). If I could add more buttons, more various sounds would be playable, enhancing user experience.

Also, I would like to add more instruments on the program. For now, there are only piano and flute but if I had more time and if more sound files were available, then I would like to add more instruments such as the drum, guitar, and trumpet.

Additionally, if I could remake the box, I would like to use laser cutting to create a more solid and neat box. Although the current box performs all the necessary tasks, it can fall apart in any moment because cardboard instead of plywood or acrylic was used. Therefore, I would like to improve the box by using laser cutting to create a more strong and durable box.

One thing that bothers me a little with my project is the delay or lag in the play of the sound files. When the user presses a button on Arduino, the corresponding sound file does not play immediately and hence creates a gap. I removed all delay functions on Arduino and checked individual sound files on p5, but for some reason, the delay between the button press and play of the file did not resolve.

Schematics

IM Showcase

IM Showcase

During the showcase, I was able to explain and display my final project to many people. Due to the loud noise in the arts center, I had to borrow a set of headphones so that the users can clearly hear the sounds. Many people liked the flute sound and were entertained by the interaction. I also had so much fun experiencing the works of peers. Everyone had such an interesting project and overall, the IM Showcase was a great opportunity to share our skills, talents, and projects.

Week 12 Assignment

Assignment 1:

For this assignment, it displays an ellipse whose horizontal position is controlled by a potentiometer connected to an Arduino board. When the space bar is pressed, it establishes a serial connection. The Arduino continuously reads the potentiometer value and sends it to the P5.js sketch via serial communication, allowing real-time adjustment of the ellipse’s position.

P5 code:

let ellipseHorizental;
function setup() {
  createCanvas(640, 480);
  textSize(18);
  ellipseHorizental = width/2; 
}
function draw() {
  background(220);
  // Draw ellipse with width based on potentiometer value
  fill("green");
  ellipse(ellipseHorizental, height / 2, 100, 150);
  if (!serialActive) {
    text("Press Space Bar to select Serial Port", 20, 30);
  } else {
    text("Connected", 20, 30);
    // Print the current potentiometer value
    text('Potentiometer Value = ' + str(ellipseHorizental), 20, 50);
  }
}
function keyPressed() {
  if (key == " ") {
    setUpSerial();
  }
}
function readSerial(data) {
  if (data != null) {
    // convert the string to a number using int()
    let fromArduino = split(trim(data), ",");
    // Map the potentiometer value to the ellipse width
    ellipseHorizental = map(int(fromArduino[0]), 0, 1023, 0, 640); 
  }
}

Arduino code:

const int potPin = A0;  // Analog pin connected to the potentiometer
void setup() {
  Serial.begin(9600);
}
void loop() {
    int potValue = analogRead(potPin);  // Read the value from the potentiometer
      // Send the potentiometer value to p5.js
      Serial.println(potValue);
}

Assignment 2:

For assignment 2 it establishes communication between a P5.js sketch and an Arduino board for controlling LED brightness. The P5.js sketch allows users to adjust brightness by dragging the mouse horizontally, with instructions displayed when the serial connection is active. The Arduino board continuously waits for serial data from the P5.js sketch, adjusting the LED brightness accordingly. During setup, a handshake process is initiated, blinking the built-in LED while waiting for serial data.

P5 code:

let brightness = 0; //variable for brightness control

function setup()
{
createCanvas(400, 400); //create canvas
}

function textDisplay() //display text in the starting
{
text("PRESS SPACE TO START SERIAL PORT", width/2 - 109, height/2 - 5);
}

function draw()
{

background(220); //grey background

if (serialActive) //if serial is active
{
text("connected", width/2 - 27, height/2 - 5); //tell the user that it is connected
text("Drag the mouse horizontally to change brighthess", width/2 - 130, height/2 + 15); //give instructions on how to control brightness
}
else
{
textDisplay(); //display instructions on how to start serial is not active
}
  
if (mouseX >= 0 && mouseX<=10){
  brightness = 0;
}
else if (mouseX >= 3 && mouseX<=width/5){
  brightness = 51;
}
else if(mouseX >= width/5 && mouseX<=2*width/5){
  brightness = 102;
}
else if(mouseX >= 2*width/5 && mouseX<=3*width/5){
  brightness = 153;
}
else if(mouseX >= 3*width/5 && mouseX<=4*width/5){
  brightness = 204;
}
else if (mouseX>=4*width/5){
  brightness = 255;
}
else{
  brightness = 0;
}
  
}

function keyPressed() //built in function
{
if (key == " ") //if space is pressed then
{
setUpSerial(); //setup the serial
}


}

//callback function
function readSerial(data)
{
let sendToArduino = brightness + "\n"; //add the next line to dimness counter
writeSerial(sendToArduino); //write serial and send to arduino
}

Arduino code:

int LED = 5; // Digital pin connected to the LED
void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(LED, OUTPUT);
  // start the handshake
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH); // on/blink while waiting for serial data
    Serial.println("0");             // send a starting message
    delay(300);                       // wait 1/3 second
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}
void loop() {
  // wait for data from p5 before doing something
  while (Serial.available()) {
    digitalWrite(LED_BUILTIN, HIGH); // led on while receiving data
    int brightnessValue = Serial.parseInt();
    if (Serial.read() == '\n') {
      delay(5);
      Serial.println(brightnessValue);
    }
    analogWrite(LED, brightnessValue);
    digitalWrite(LED_BUILTIN, LOW);
  }
}

Assignment 3:

For this assignment, we tried to establish communication between a P5.js sketch and an Arduino board (bi-directional). The P5.js sketch simulates a ball’s motion affected by wind and gravity, with its behavior controlled by sensor data received from the Arduino. The Arduino reads data from an ultrasonic sensor to determine distance, sending LED control signals back to the P5.js sketch based on the received distance data, influencing wind direction and LED turning on.

P5 code:

//declare variables
let velocity;
let gravity;
let position;
let acceleration;
let wind;
let drag = 0.99;
let mass = 50;
let LEDvalue = 1401;
let goRight = 0;
let goLeft = 0;
let first

function setup() {
createCanvas(1000, 360); //create canvas
noFill();
position = createVector(width/2, 0);
velocity = createVector(0,0);
acceleration = createVector(0,0);
gravity = createVector(0, 0.5*mass);
wind = createVector(0,0);
}

function textDisplay()
{
text("PRESS SPACE TO START SERIAL PORT", width/2 - 109, height/2 - 5); //display the appropriate text in the start
}

function draw() {
background(255);
if (serialActive) //if the serial is active
{
applyForce(wind);
applyForce(gravity);
velocity.add(acceleration);
velocity.mult(drag);
position.add(velocity);
acceleration.mult(0);
ellipse(position.x,position.y,mass,mass);
if (position.y > height-mass/2) //if the ball touches the bottom
{
velocity.y *= -0.9; // A little dampening when hitting the bottom
position.y = height-mass/2;
LEDvalue = 1401; //take LED value to be 1401 because we dont want the value (1) to be lurking in the serial and then affecting the wind values

}
else
{
LEDvalue = 1400; //when the LED is off
}

}
else
{
fill(0);
textDisplay();
}
}

function applyForce(force){
// Newton's 2nd law: F = M * A
// or A = F / M
let f = p5.Vector.div(force, mass);
acceleration.add(f);
}

function keyPressed(){
if (key==' '){ //if space is pressed, then serial is set up
setUpSerial();
}

if (keyCode == DOWN_ARROW) //if down arrow is pressed
{
//mass etc is changed
mass=random(15,80);
position.y=-mass;
velocity.mult(0);
}
}

function readSerial(data) //call back function
{
let sendToArduino = LEDvalue + "\n"; //sends value of LED to Arduino with \n added
writeSerial(sendToArduino); //write to Arduino

if (data != null) //if the data is not null and something is received
{
console.log(data);
if (data > 1450) //if the distance is greater than 1450, then
{
wind.x = 1; //the ball/wind goes right
}
else if (data < 1350) //if the distance is less than 1350
{
wind.x = -1; //the ball/wind goes left
}
}
}

 

Arduino code:

//declare variables
const int LED_PIN = 4;
int LEDvalue = 0; //will contain whether or not the LED should be on or off
int distance = 0; //will contain the distance by ultrasonic sensor
const int pingPin = 2; //Trigger Pin of Ultrasonic Sensor
const int echoPin = 3; //Echo Pin of Ultrasonic Sensor

void setup()
{
Serial.begin(9600); // Start serial communication at 9600 baud

pinMode(LED_PIN, OUTPUT); //pin mode is output

//Set the ultrasonic sensor pins as output and input respectively
pinMode(pingPin, OUTPUT);
pinMode(echoPin, INPUT);

while (Serial.available() <= 0)
{
Serial.println(1400); //connection establishment. 1400 so that the wind values do not change
}
}

void loop()
{
//wait for p5js
while (Serial.available())
{
sensorReading(); //reads data from the sensor

LEDvalue = Serial.parseInt(); //parsing from the serial written data from p5js

if (LEDvalue == 1400) //if the LED value is 1400
{
digitalWrite(LED_PIN, LOW); //then turn off the LED
}
else if (LEDvalue == 1401) //if the LED value is 1401
{
digitalWrite(LED_PIN, HIGH); //then turn on the LED
}
}
}

//Function to read the ultrasonic sensor and measure distance
void sensorReading()
{
//Send a short low pulse
digitalWrite(pingPin, LOW);
delay(2); //delay to avoid complications
digitalWrite(pingPin, HIGH); //sends a high pulse for 10 microseconds
delay(10);
digitalWrite(pingPin, LOW); //turn off the ping pin
distance = pulseIn(echoPin, HIGH); //Measure the duration of the ultrasonic pulse and calculate the distance
Serial.println(distance); //print the serial from distance
}

Final Project – Signify

CONCEPT

My final project is a Sign Language glove that translates American Sign Language (ASL) to English and vice versa. The aim is to facilitate communication and improve accessibility for individuals who are deaf or hard of hearing. This is an idea I have had for years but I finally have the technical skills to implement it. My motivation arises from my aim to break down the boundaries that hinder people with disabilities in society. Unfortunately, sign language is not a common skill for hearing people. On the other hand, while some people with hearing impairment know lipreading, for most of them, Sign Language is their first language.

This interactive system enables individuals that use sign language to have two-way communication with non-sign language users effectively. The user wearing the glove can fingerspell words using the American Sign Language alphabet. The program then vocalizes the word to assist Sign Language users with speech. On the other hand, a hearing person can type their word into the program which will display the signs for each letter so the Sign Language user can interpret it.

IMPLEMENTATION

p5 sketch full screen mode: https://editor.p5js.org/aneekap/full/ZHrr0suY-

The glove incorporates flex sensors on each finger which detects how much the finger is bent. Arduino processes this data and sends the finger configurations to the p5.js sketch.

//fingers
int flexPin1 = A1; 
int flexPin2 = A2; 
int flexPin3 = A3;
int flexPin4 = A4; 
int flexPin5 = A5;

void setup() {
  // Start serial communication so we can send data
  // over the USB connection to our p5js sketch
  Serial.begin(9600);
}

void loop() {
  // Read flex sensor values
  int pinky = analogRead(flexPin1);
  int ring = analogRead(flexPin2);
  int middle = analogRead(flexPin3);
  int index = analogRead(flexPin4);
  int thumb = analogRead(flexPin5);

  // Send flex sensor values to p5.js
  Serial.print(pinky);
  Serial.print(",");
  Serial.print(ring);
  Serial.print(",");
  Serial.print(middle);
  Serial.print(",");
  Serial.print(index);
  Serial.print(",");
  Serial.print(thumb);
  Serial.println(); 

  delay(100); 
}

The p5.js sketch interprets the gestures to recognize the corresponding letters of the alphabet. This is done using the signRecognition function below which checks whether each flex sensor value is in the appropriate range.

function signRecognition() {
  //letter recognition
  if ((120<pinky && pinky<200) && (90<ring && ring<400) && (160<middle && middle<400) && (100<index && index<300) && (240<thumb && thumb<280)) {
      text('a', 102, 255); 
      letter = 'a';
    } 
  else if ((230<pinky && pinky<255) && (0<=ring && ring<50) && (0<=middle && middle<50) && (0<=index && index<50) && (175<thumb && thumb<250)) {
      text('b', 102, 255); 
      letter = 'b';
    } 
  
  else if ((220<pinky && pinky<250) && (0<=ring && ring<100) && (0<=middle && middle<100) && (30<index && index<190) && (220<thumb && thumb<270)) {
      text('f', 102, 255);
      letter = 'f';
    }
  else if ((130<pinky && pinky<250) && (100<ring && ring<270) && (135<middle && middle<280) && (index==0) && (250<thumb && thumb<283)) {
      text('g', 102, 255); 
      letter = 'g';
    }
  else if ((205<pinky && pinky<245) && (70<ring && ring<280) && (80<middle && middle<220) && (70<index && index<240) && (210<thumb && thumb<265)) {
      text('i', 102, 255); 
      letter = 'i';
    }
  else if ((120<pinky && pinky<210) && (60<ring && ring<330) && (50<middle && middle<300) && (30<index && index<300) && (190<thumb && thumb<240)) {
      text('m', 102, 255); 
      letter = 'm';
    }
  else if ((150<pinky && pinky<220) && (0<=ring && ring<100) && (0<=middle && middle<110) && (0<=index && index<50) && (220<thumb && thumb<250)) {
      text('o', 102, 255); 
      letter = 'o';
    }
  else if ((135<pinky && pinky<220) && (80<ring && ring<220) && (0<=middle && middle<20) && (0<=index && index<50) && (230<thumb && thumb<290)) {
      text('p', 102, 255); 
      letter = 'p';
    }
  else if ((170<pinky && pinky<200) && (20<ring && ring<220) && (0<=middle && middle<190) && (0<=index && index<100) && (195<thumb && thumb<260)) {
      text('u', 102, 255); 
      letter = 'u';
    }
  else {
      text('-', 102, 255); // Display '-' if no specific configuration is matched
      letter = ' ';
    }
  
}

It is limited to only 9 letters for now. I did implement a few more letters but later removed it to avoid clashes between the letter ranges. The reason for this is a lot of ASL signs have very similar finger configurations and I would require additional or more accurate sensors to implement all 26 letters.

There will be two options the user can select from: translating ASL to English and translating English to ASL. For the first program, the user spells out a word using the sign for each letter and pressing right arrow to confirm the letter and move to next position.  You can edit the word if you made a mistake by using backspace, and to add a space you input no letter.  This is done using the keyPressed() function.

function keyPressed() {
  if (key == " ") {
    setUpSerial();
  }
  
  if (keyCode === ENTER) {
    if (page === 1) {
      page = 2;
    } else if (page === 2) {
      page = 3;
    } else if (page === 4) {
      finalizeWord();
      // page = 3; // Go back to options page
    }
  } else if (keyCode === BACKSPACE && page === 4) {
      Word = Word.substring(0, Word.length - 1);
  } else if (keyCode === RIGHT_ARROW && page === 4) {
      Word += letter;
  } else if (keyCode === LEFT_ARROW && (page === 4 || page === 5)) {
      page = 3; // Go back to options page
      Word = '';
  }
  
  if (keyCode >= 65 && keyCode <= 90) { // Check if the pressed key is a letter
    enteredWord += key.toLowerCase(); // Add the lowercase letter to the entered word
  } else if (keyCode === BACKSPACE) { // Handle backspace key
    enteredWord = enteredWord.slice(0, -1); // Remove the last character from the entered word
  }
}

The p5.js screen reads the word aloud using text-to-speech, using the SpeechSynthesis interface which is a part of the Web Speech API.

For the second program, users will have the option to input a word via keyboard to display the corresponding ASL sign for each letter on the screen below the word.

function translateEnglishPage() {  
  image(eng, 0, 0, width, height);
  text(enteredWord, width/2 - 120, height/2+5); 

  // Check each letter of the entered word and display the corresponding sign
  let startX = width/2 - 130; 
  let startY = height/2 - 70; 
  let letterSpacing = 35; // Spacing between images
  for (let imgIndex = 0; imgIndex < enteredWord.length; imgIndex++) {
    let currentLetter = enteredWord.charAt(imgIndex).toLowerCase(); 
    //calculate position of image based on letter
    let imageX = startX + imgIndex * letterSpacing; 
    let imageY = startY+120;
    
    // Display the image corresponding to the current letter
    if (currentLetter === 'a') {
        image(sign_a, imageX, imageY, 35, 50); }

    // and so on for each letter ...
}

USER TESTING

User testing was helpful but also a bit worrying. The gesture configurations were calibrated to my hand and fingers. I later noticed that it wasn’t working exactly the same with other people’s hands. I thus had to make the ranges less strict to incorporate other hand shapes. However, editing these ranges caused more issues such as introducing clashes between the letters.

challenges and improvements:

The main challenge was calculating the gesture configurations one by one. The flex sensors are pretty sensitive and tend to randomly give different values. I am using two types of flex sensors: 3 thin film pressure sensors and 2 short flex sensors, so I had to calibrate them differently as well. On top of that, one of my flex sensors stopped working midway so my project came to a stop. Thankfully, Professor came to the rescue and bought a new flex sensor for me promptly. Soldering and arranging the wires were also a hassle but I finally got them to look neat.

I am proud of coming up with the idea in the first place. I wanted to create something that was unique and something I am passionate about. I am also proud of sticking to it despite the challenges and making it as accurate as possible.

There is a lot to improve and I started this as a prototype for a long-term project. One major issue is that since some of the finger configurations are so similar, it mixes up between the letters. I also couldn’t implement the entire alphabet. I could add an accelerometer to detect movements as well.  I could alternatively try using ML5 for more accurate configurations. I hope to get it to work for entire words as well. I aim to one day create a fully functional portable Sign Language glove.

IM Showcase

I made a few changes before I presented my project at the showcase: I recalibrated the ranges for the letters to make it work smoother, I removed a few letters according to Professor’s advice to reduce clashes between letters, and I improved the UI.

During the IM show, when a few people tried on my glove, the tape and wires started coming off, and I had to run back to the IM lab to fix it. Moreover, most of the letters were not working for them since it was still only optimal for my hand. This was because the bending of the flex sensors vary a lot between different hand shapes and sizes. I unfortunately had to resort to only providing them a demonstration after that point and instead gave them the challenge to provide me a word using those letters.

Nevertheless, I had a fun time at the showcase presenting my project and engaging with other people’s projects. I also thoroughly enjoyed taking this course overall and using my creativity and technical skills to come up with projects every week.

 

Rashed’s Final Project – A S A P B A B Y

 

Concept:

As soon as I heard that I would get to pick a topic again, I decided to go back to my old idea that did not cooperate with me during the midterm period: The Dance Dance Revolution arcade machine. I wanted to do this but in my own way. I’ve been really into looking at different aesthetics and, so far, my favorite aesthetic is the CottageCore aesthetic whic his basically the theme of living in the forest, fantasy, fairies, ..etc.

As I mentioned in my midterm, I have been really into this group named NewJeans and one of their music videos was focused mainly on the cottagecore aesthetic which I was really into. The song’s name is ASAP and I would highly recommend everyone to watch if you want your day to instantly be 1000 times better.

This is what a DDR machine looks like:

 

The game goes as follows:

Arrows spawn from the bottom of the screen and move to the top according to the rhythm of the song picked. The player must click/ step on the corresponding arrow button when it reaches a certain point.

NewJeans’ ASAP:

 

Challenges :

This project was a nightmare to make:

  1. The Circle Incident:

Writing the p5 code, I wanted to first start with ellipses that would spawn in four different x coordinates and that would disappear when I would click the arrow keys as they reach y 100. However, p5 had other plans. It just would not work no matter what I tried. Turns out, I just forgot to use the term “key ==” .

2. Mapping : (

I have a past with creating beat maps for the VR game Beat Saber and I knew that people got that idea from mapping DDR games. So, I looked up what DDR mapping looks like and I created one for the song ASAP. But after countless hours of trying to add the mapping for ASAP to p5, I realized that mapping was not for p5. Is it possible? Probably. Would I have tried even longer if I just had more time? Probably.

What I did to consider time was try to have the arrows spawn according to the song’s BPM(Beats Per Minute) and I just now realized as I’m writing this that I had the arrows SPAWN with the BPM, not hit the targetZone according to the BPM which makes so much sense now but it still is very fun.

 

3. The Horror of Serial Communication :0

For the weekend, I decided to go home and work on this there and that was one of the biggest mistakes I have ever made because I don’t like the serial communication part. I spent the whole weekend working on something that I don’t fully understand. I spent 5 hours my first night home and made zero progress. The following day, I caved and asked my brother who has a good amount of experience with C++ and even he could not figure out a solution.

Sunday evening I’m back on campus and I see Professor Aya who told me that my p5 sketch is only receiving 2 inputs instead of 4…… She fixed it in less than 2 minutes.

 

4. The Forbidden Restart Button :0

After getting familiar and actually understanding what and how Serial communication work, I wanted to add a simple button. So cute, I know :3.

Little did I know that button was made in the deepest, darkest pits of hell. For some reason, it just would not register the 1. I would check my arduino code and everything would work fine on the serial monitor. I spent hours on that and after asking my very nice classmate Marcus, he looked at my arduino code and figured out I added a teensy tiny “ln” in my println function…..

Not my proudest Rashed moment.

 

Game Design:

my game has four states for the title screen, game, gameOver, and YouWin.

I wanted to make my own backgrounds for these states instead of using pictures from the group. I used procreate  and a bunch of different elements from the theme and I just put them all together and I created these:

 

Title:

Game Over:

You Win:

Game:

 

I also designed the arrows myself 🙂

Code:

p5js Sketch:

This code outlines a rhythm-based game where players must synchronize their inputs with arrows moving on the screen to the beat of a background song. The game functions in various states such as “start,” “game,” “gameOver,” and “win,” each presenting unique visuals and interactions.

The preload function loads all necessary assets, including images and sounds, preparing the game for a smooth launch. Following this, the setup function establishes the gaming environment by creating a canvas and configuring text settings.

During gameplay, the draw function operates as the continuous loop that directs the flow of the game. It adjusts what’s displayed based on the game state and also checks the status of the serial port for hardware connectivity, crucial for linking external controllers like buttons.

Player interaction involves using buttons to accurately match onscreen arrows as they align with a designated hit zone. Successful matches increase the player’s score, whereas missed arrows result in a loss of lives. This mechanic tests both rhythm and timing.

The game mechanics are finely tuned; arrows are generated at intervals determined by the song’s BPM, creating a consistent rhythmic challenge. The game monitors for end conditions, either when the player runs out of lives or when the song concludes, leading to different game states like “win” or “gameOver.”

The integration with the Arduino through serial communication allows for the use of specialized controllers.

 

let arrows = [];
let images = {};
let gameState = "start";
let score = 0;
let lives = 5;
const arrowSpeed = 2;
const hitZoneY = 100;
const tolerance = 30;
let leftButton = 0;
let rightButton = 0;
let upButton = 0;
let downButton = 0;
let restartButton = 0;
let buttonispressed = 0;
// Song and BPM (beats per minute)
let song;
let songBPM = 134; // song's Beats Per Minute
// Images
let titleImage, gameOverImage, youWinImage, gameImage;
// Interval ID for arrow spawns
let arrowInterval;

// Preload function to load assets
function preload() {
  song = loadSound("NewjeansASAP.mp3");
  images.left = loadImage("Left.PNG");
  images.up = loadImage("Up.PNG");
  images.down = loadImage("Down.PNG");
  images.right = loadImage("Right.PNG");
  titleImage = loadImage("TitleBG.PNG");
  gameOverImage = loadImage("GameOverBG.PNG");
  youWinImage = loadImage("YouWinBG.PNG");
  gameImage = loadImage("GameBG.JPG");
}

// Setup function
function setup() {
  // Create canvas
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER, CENTER);
  textSize(32);
}

function draw() {
  // Display connection status if serial port is not active
  if (!serialActive) {
    text("Press Space Bar to select Serial Port", 20, 30);
  } else {
    text("Connected", 20, 30);
  }
  background(0);

  // Draw different game screens based on game state
  switch (gameState) {
    case "start":
      image(titleImage, 0, 0, width, height);
      drawStartScreen();
      break;
    case "game":
      image(gameImage, 0, 0, width, height);
      playGame();
      break;
    case "gameOver":
      image(gameOverImage, 0, 0, width, height);
      drawGameOver();
      break;
    case "win":
      image(youWinImage, 0, 0, width, height);
      drawWin();
      break;
  }
}

// Function to draw start screen
function drawStartScreen() {
  if (gameState === "start" && restartButton == 1 && buttonispressed == 0) {
    startGame(); // Start the game when restart button is pressed
    buttonispressed = 1;
  } else if (restartButton == 0 && buttonispressed == 1) {
    buttonispressed = 0;
  }
}

// Function to play the game
function playGame() {
  //Read data from input

  if (gameState === "game") {
    // Check if the key pressed matches the arrow type
    for (let i = arrows.length - 1; i >= 0; i--) {
      let arrow = arrows[i];
      if (arrow.y >= hitZoneY - tolerance && arrow.y <= hitZoneY + tolerance) {
        if (
          (leftButton == 1 && arrow.type === "left") ||
          (upButton == 1 && arrow.type === "up") ||
          (downButton == 1 && arrow.type === "down") ||
          (rightButton == 1 && arrow.type === "right")
        ) {
          arrows.splice(i, 1); // Successful hit, remove arrow
          score++; // Increment score
        }
      }
    }
  }

  // Draw hit zones
  fill(255, 255, 255, 100);
  ellipse(width * 0.2, hitZoneY, 70);
  ellipse(width * 0.4, hitZoneY, 70);
  ellipse(width * 0.6, hitZoneY, 70);
  ellipse(width * 0.8, hitZoneY, 70);

  // Display score and lives
  fill(255);
  text(`Score: ${score}`, 70, 30);
  text(`Lives: ${lives}`, width - 70, 30);

  // Move and display arrows
  for (let i = arrows.length - 1; i >= 0; i--) {
    let arrow = arrows[i];
    image(images[arrow.type], arrow.x - 35, arrow.y - 35, 70, 70);
    arrow.y -= arrowSpeed;

    // Remove arrow if it goes out of screen and reduce lives
    if (arrow.y < 0) {
      arrows.splice(i, 1);
      lives--;
    }
  }

  // Check for game over or win conditions
  if (lives < 1) {
    gameState = "gameOver";
    song.stop(); // Stop the song if the player loses
  } else if (song.isPlaying() === false) {
    gameState = "win";
  }
}

// Function to draw game over screen
function drawGameOver() {
   if (restartButton == 1 && buttonispressed == 0){
    gameState = "start"
     buttonispressed = 1;
  } else if (restartButton == 0 && buttonispressed == 1){
    buttonispressed = 0;
  }
}

// Function to draw win screen
function drawWin() {
   if (restartButton == 1 && buttonispressed == 0){
    gameState = "start"
     buttonispressed = 1;
  } else if (restartButton == 0 && buttonispressed == 1){
    buttonispressed = 0;
  }
}

// Function to handle key presses
function keyPressed() {
  if (key == " ") {
    // Start the serial connection
    setUpSerial();
  }
}

// Function to start the game
function startGame() {
  gameState = "game";
  restartButton = 0;
  score = 0;
  lives = 5;
  arrows = [];
  clearInterval(arrowInterval); // Clear any existing interval
  initiateArrowSpawns();
  song.play(); // Start playing the music when the game starts
}

// Function to restart the game
function restartGame() {
  gameState = "start"; // Change gameState to 'start' to return to title screen
  score = 0;
  lives = 5;
  arrows = [];
  clearInterval(arrowInterval); // Clear any existing interval
  initiateArrowSpawns();
  restartButton = 0; // Reset restart button state
}


// Function to initiate arrow spawns
function initiateArrowSpawns() {
  let interval = 60000 / (songBPM / 2 ); // Halve the BPM to spawn arrows at a slower rate
  arrowInterval = setInterval(() => {
    let direction = random(["left", "up", "down", "right"]);
    spawnArrow(direction);
  }, interval);
}

// Function to spawn an arrow
function spawnArrow(direction) {
  let xPosition;
  switch (direction) {
    case "left":
      xPosition = width * 0.2;
      break;
    case "up":
      xPosition = width * 0.4;
      break;
    case "down":
      xPosition = width * 0.6;
      break;
    case "right":
      xPosition = width * 0.8;
      break;
  }
  arrows.push({ x: xPosition, y: height, type: direction });
}

// This function will be called by the web-serial library
// with each new *line* of data. The serial library reads
// the data until the newline and then gives it to us through
// this callback function
function readSerial(data) {
  //////////////////////////////////////////////////
  // READ FROM ARDUINO HERE
  //////////////////////////////////////////////////

  if (data != null) {
    // Make sure there is actually a message
    // Split the message
    let fromArduino = split(trim(data), ",");
    // If the right length, then proceed
    if (fromArduino.length == 5) {
      // Only store values here
      // Do everything with those values in the main draw loop

      // We take the string we get from Arduino and explicitly
      // convert it to a number by using int()
      // e.g. "103" becomes 103
      leftButton = int(fromArduino[0]);
      upButton = int(fromArduino[1]);
      downButton = int(fromArduino[2]);
      rightButton = int(fromArduino[3]);
      restartButton = int(fromArduino[4]);
      //console.log("left button is" + leftButton);
      //console.log("right button is" + rightButton);
      //console.log("up button is" + upButton);
      //console.log("down button is" + downButton);
      //console.log("restart is" + restartButton);
    }

    /////////////////////////////////////////////////
    // SEND TO ARDUINO HERE (handshake)
    /////////////////////////////////////////////////
    let sendToArduino = "\n";
    writeSerial(sendToArduino);
  }
}

 

Arduino Code:

This section of code is designed to interface with hardware buttons, specifically set up for a gaming application. It starts by defining pin numbers connected to various buttons on an Arduino board: left, up, down, right, and a restart button. Each button is associated with a specific pin number ranging from 2 to 6.

The setup() function initializes the serial communication at a baud rate of 9600 to enable data transfer between the Arduino and a computer. It also configures each button pin as an input, preparing the Arduino to read the states of these buttons.

In the `oop() function, the code continuously reads the state of each button using the digitalRead() function. It checks whether each button is pressed or not, producing a digital high or low signal. These states are then formatted into a comma-separated string and sent over the serial connection. This allows another system, such as a computer running a game, to receive real-time input from these hardware buttons, integrating physical interactions into digital applications.

// Define pin numbers for buttons
const int leftButtonPin = 2;
const int upButtonPin = 3;
const int downButtonPin = 4;
const int rightButtonPin = 5;
const int restartButtonPin = 6;


void setup() {
  Serial.begin(9600);
  // Set button pins as inputs
  pinMode(leftButtonPin, INPUT);
  pinMode(upButtonPin, INPUT);
  pinMode(downButtonPin, INPUT);
  pinMode(rightButtonPin, INPUT);
  pinMode(restartButtonPin, INPUT);
}

void loop() {
  // Read button states and send data over serial
  int leftButton = digitalRead(leftButtonPin);
  int upButton = digitalRead(upButtonPin);
  int downButton = digitalRead(downButtonPin);
  int rightButton = digitalRead(rightButtonPin);
  int restartButton = digitalRead(restartButtonPin);

  // Send button states to serial
  Serial.print(leftButton);
  Serial.print(",");
  Serial.print(upButton);
  Serial.print(",");
  Serial.print(downButton);
  Serial.print(",");
  Serial.print(rightButton);
  Serial.print(",");
  Serial.println(restartButton);

}

 

The Game:

Here’s a video of my friend playing it:

FullScreen Link

Overall:

I am very proud of myself for pushing myself into an area that I previously left unexplored. Considering how I could not even make the normal game for my midterm, and I was able to do it for my final and even taking it a step further for my final. I am truly happy I was given the freedom to do this. I would like to particularly mention that without the help and support I got from professor Aya, I would have given up a long time ago.

I feel like one thing I wanted to do with this project that I could not was upload my own custom map for the songs and have different difficulties and  songs (just like a normal DDR arcade machine). I think that would be important for me as it allows to input even more of my creativity into this project.

Final Project – Cats Survival

Concept:

My inspiration for this project was one questions I have asked myself really often during the rainy days: “Where do the campus cats go?” and “How do they survive the rain?”. Based on this, I created “CATS SURVIVAL”, inspired also by the classic arcade games where players navigate through obstacles to achieve a high score. In this game, players engage with Arduino push buttons to control the cat attempting to avoid falling water drops while traversing a colorful campus setting.

Final Setup:

IM Showcase:

How it works:

Players start by launching the game, where they are greeted with a vibrant start page featuring the game’s logo. Once the game begins, the cat automatically appears at the center of the screen, and the player’s objective is to keep the cat from being hit by falling water drops.

Using a connected serial input device (Arduino), players can move the cat left or right, dodging incoming obstacles. Each successful dodge increases the player’s score, while collision with a water drop ends the game.

As the game progresses, the speed, and frequency of falling water drops increase, challenging the player’s reflexes and agility. Upon game over, players can restart the game by pressing any key, offering them the opportunity to beat their previous high score and continue the thrilling dodge-and-survive gameplay.

Images of the project (1st draft):

User testing:

p5 Game:

Link for full screen

Arduino Code:

// Constants won't change. They're used here to set pin numbers:
const int buttonPin1 = 2;  // The number of the first pushbutton pin
const int buttonPin2 = 3;  // The number of the second pushbutton pin
const int ledPin1 = 13;    // The number of the first LED pin
const int ledPin2 = 12;    // The number of the second LED pin

// Variables will change:
int buttonState1 = 0;  // Variable for reading the first pushbutton status
int buttonState2 = 0;  // Variable for reading the second pushbutton status

void setup() {
  // Initialize the LED pins as outputs:
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);

  // Initialize the pushbutton pins as inputs:
  pinMode(buttonPin1, INPUT_PULLUP); // Changed to INPUT_PULLUP
  pinMode(buttonPin2, INPUT_PULLUP); // Changed to INPUT_PULLUP
  
  // Start serial communication:
  Serial.begin(9600);
}

void loop() {
  // Read the state of the first pushbutton value:
  buttonState1 = digitalRead(buttonPin1);

  // Check if the first pushbutton is pressed. If it is, the buttonState is LOW:
  if (buttonState1 == LOW) {
    // Turn the first LED on:
    digitalWrite(ledPin1, HIGH);
  } else {
    // Turn the first LED off:
    digitalWrite(ledPin1, LOW);
  }

  // Read the state of the second pushbutton value:
  buttonState2 = digitalRead(buttonPin2);

  // Check if the second pushbutton is pressed. If it is, the buttonState is LOW:
  if (buttonState2 == LOW) {
    // Turn the second LED on:
    digitalWrite(ledPin2, HIGH);
  } else {
    // Turn the second LED off:
    digitalWrite(ledPin2, LOW);
  }
  
  // Send button states to the p5 sketch
  Serial.print(buttonState1);
  Serial.print(",");
  Serial.println(buttonState2);
  delay(100); // Adjust delay as needed
}

p5 snippet code:

Reading serial data

This function reads data from the serial port, interprets it as button states, and updates the cat’s position accordingly. It ensures that the cat remains within the canvas bounds while moving left or right based on the received data.

This snippet demonstrate how the game can interact with an Arduino board via serial communication to control the cat’s movement.

function readSerial(data) {
  if (data != null) {
    let buttonStates = split(trim(data), ',');
    let buttonState1 = int(buttonStates[0]);
    let buttonState2 = int(buttonStates[1]);
    
    // Update cat position based on button states
    if (buttonState1 == 1) {
      catX -= 22; // Move left
    }
    if (buttonState2 == 1) {
      catX += 22; // Move right
    }
    
    // Ensure cat stays within canvas bounds
    catX = constrain(catX, 0, width - catImg.width);
  }
}

Challenges:

The challenge of this game is designing the obstacle mechanics to appropriately balance the game’s difficulty. Since the game operates in full-screen mode, ensuring that the falling obstacles provide a challenging, yet enjoyable experience for players can be tricky. Balancing factors such as the speed, frequency, and size of the obstacles requires careful consideration to prevent the game from becoming too easy or too difficult. Additionally, transitioning from the initial idea of using a potentiometer for input to utilizing two push buttons might pose challenges in terms of code adaptation and player control dynamics.

Future improvements:

  • Enhance the complexity of the game mechanics and integrating additional features into the circuit in order to elevate the player experience. Adding new gameplay elements such as power-ups, varying obstacle patterns can provide players with more engaging challenges and keep them invested in the game for longer durations.
  • Incorporating a speaker into the Arduino circuit to synchronize with button presses could add a wider dimension to the gameplay, enhancing immersion and feedback for players. By integrating sound effects or background music that reacts to player actions, such as cat movements and obstacle collisions, the overall gaming experience can be enriched, making it more dynamic and enjoyable.