Final project – Hamdah AlSuwaidi

The project’s concept:

The allure of a jukebox, with its nostalgic charm and tangible interaction with music selection, inspires a unique blend of past and present in your project. This modern reinterpretation of the classic jukebox isn’t just about listening to music—it’s an experiential dive into the ritual of choosing sounds from different eras and styles, echoing the tactile joy that came from flipping through vinyl records or pressing the physical buttons on a jukebox. Your project revives this delightful sensory interaction by blending physical buttons and digital outputs, allowing users to actively engage with the music rather than passively streaming playlists. It embodies a revival of the golden age of jukeboxes but with a contemporary twist, using today’s technology to recreate a piece of the past that resonates with both nostalgia and the new digital era.

The decision to incorporate a variety of music channels such as English, Classical, and Al Khalidiya channels suggests a celebration of diversity and the rich tapestry of global music culture. It reflects a yearning to bring the world closer together through the universal language of music, wrapped in the classic format of a jukebox. This project does more than just play music; it invites users to journey through different cultures and time periods at the push of a button. It’s a bridge between generations and geographies, enabling a shared experience that is both educational and entertaining, which is likely what sparked the idea to reinvent the jukebox for a modern audience. This blend of educational purpose and entertainment, rooted in technological innovation, makes your jukebox project a meaningful nod to the past while eagerly embracing the future of interactive media.

P5js code:

// Initial state settings
let loading = true;
let channels = [[], [], []]; // Arrays for storing songs by channel: 0 - English, 1 - Classical, 2 - Al Khalidiya
let numberOfSongs = 0;
let numberOfSongsLoaded = 0; 
let coinSound; // Sound effect for the coin insert
let selectedChannel; // Currently selected channel
let playing = false; // Is a song currently playing?
let songToPlay; // Current song playing

// Preload function to load sounds and other resources before the program starts
function preload() {
  soundPaths = loadStrings("soundFileNames.txt"); // Load list of sound file names
  coinSound = loadSound("sounds/coin.mp3"); // Load coin sound effect
}

// Setup function to initialize the environment
function setup() {
  createCanvas(600, 400);
  textAlign(CENTER, CENTER);

  // Loop through the sound paths and assign them to channels based on file names
  for (let i = 0; i < soundPaths.length; i++) {
    let words = soundPaths[i].split("_");
    let channel = words[words.length - 2] + "_" + words[words.length - 1]; // Determine the channel from the file name
    switch (channel) {
      case "english_channel.mp3":
        channels[0].push("sounds/" + words.join("_"));
        break;
      case "classical_channel.mp3":
        channels[1].push("sounds/" + words.join("_"));
        break;
      case "khalidiya_channel.mp3":
        channels[2].push("sounds/" + words.join("_"));
        break;
    }
  }
  numberOfSongs = soundPaths.length; // Total number of songs loaded
  // Load each song in the channels array
  for (let i = 0; i < channels.length; i++) {
    for (let j = 0; j < channels[i].length; j++) {
      channels[i][j] = loadSound(channels[i][j], () => {
        numberOfSongsLoaded += 1; // Increment the count of loaded songs
      });
    }
  }
  ratioPos = { x: width / 2, y: height * 2 };
  selectedChannel = floor(random(3)); // Randomly select a channel to start
  fft = new p5.FFT(); // Initialize Fast Fourier Transform for audio visualization
}

// Draw function to continuously execute and render the canvas
function draw() {
  background(40);
  if (loading) {
    // Show loading screen until all songs are loaded
    rectMode(CORNER);
    strokeWeight(2);
    textSize(34);
    fill(255);
    text("LOADING...", width / 2, height / 2 - 20);
    noStroke();
    fill(255);
    rect(width / 2 - 150, height / 2 + 20, 300, 40);
    fill(20);
    rect(
      width / 2 - 150,
      height / 2 + 20,
      map(numberOfSongsLoaded, 0, numberOfSongs, 0, 300),
      40
    );
    if (numberOfSongsLoaded == numberOfSongs) {
      loading = false;
    }
  } else {
    // Display the sound spectrum and UI once loading is complete
    let wave = fft.waveform();
    stroke(255, 50);
    noFill();
    beginShape();
    for (let i = 0; i < wave.length; i++) {
      let x = map(i, 0, wave.length, 0, width);
      let y = map(wave[i], -1, 1, height, 0);
      curveVertex(x, y);
    }
    endShape();

    rectMode(CENTER);
    ratioPos.y = lerp(ratioPos.y, height / 2, 0.1); // Smoothly move the UI element
    textSize(16);
    let channelName = "";
    switch (selectedChannel) { // Display the name of the selected channel
      case 0:
        channelName = " ENGLISH SONGS ";
        break;
      case 1:
        channelName = " CLASSICAL SONGS ";
        break;
      case 2:
        channelName = " KHALIDIYA SONGS ";
        break;
    }
    drawRadio(channelName, playing);
    drawChannels();
  }
}

// Event-driven functions to respond to keyboard presses for controlling the jukebox
function keyPressed() {
  switch (key) {
    case "n":
      nextChannel(); // Go to the next channel
      stopMusic(); // Stop the currently playing music
      break;
    case "b":
      prevChannel(); // Go to the previous channel
      stopMusic(); // Stop the music
      break;
    case " ":
      if (!playing) {
        playMusic(); // Start playing music if not already playing
      } else {
        stopMusic(); // Stop the music if playing
      }
      break;
  }
}

// Utility functions to control channels and playback
function nextChannel() {
  selectedChannel += 1; // Increment the channel index
  if (selectedChannel >= 3) {
    selectedChannel = 0; // Wrap around to the first channel
  }
}
function prevChannel() {
  selectedChannel -= 1; // Decrement the channel index
  if (selectedChannel < 0) {
    selectedChannel = 2; // Wrap around to the last channel
  }
}
function stopMusic() {
  if (songToPlay) {
    songToPlay.stop(); // Stop the currently playing song
  }
  playing = false;
}
function playMusic() {
  coinSound.play(); // Play the coin sound effect
  songToPlay = random(channels[selectedChannel]); // Select a random song from the current channel
  playing = true;
  songToPlay.loop(); // Start playing the selected song
}

// Drawing utility functions for UI elements
function drawChannels() {
  fill(100, 120, 100);
  rect(0, 150, 320, 70);
  fill(60, 70, 60);
  rect(0, 150, 300, 50);
  push();
  textAlign(LEFT, CENTER);
  let channels = ["English", "Classical", "Khalidiya"];
  textSize(12);
  for (let i = 0; i < 3; i++) {
    let x = 0;
    let y = 130 + 20 * i;
    noFill();
    if (selectedChannel == i) {
      fill(60, 90, 60);
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text("_" + channels[i], x - 150, y);
    } else {
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text(" " + channels[i], x - 150, y);
    }
  }
  pop();
}

// Function to draw the radio interface
function drawRadio(channel, playing = false) {
  translate(ratioPos.x, ratioPos.y);
  // Visual elements for the radio disk
  noStroke();
  fill(150, 150, 220, 100);
  circle(0, 100, 450);

  fill(20);
  circle(0, 100, 350);
  fill(200, 100, 100);
  circle(0, 100, 150);

  let channelName = channel.split("");
  push();
  translate(0, 100);
  if (playing) rotate(-frameCount / 60);
  push();
  for (let i = 0; i in channelName.length; i++) {
    rotate(TWO_PI / channelName.length);
    fill(255);
    text(channelName[i].toUpperCase(), 0, -50);
  }

  pop();
  pop();
  fill(180);
  circle(0, 100, 80);

  stroke(255);
  noFill();
  arc(0, 100, 420, 420, -PI / 2 + 0.4, -PI / 2 + 0.8);

  noStroke();
  strokeWeight(2);
  fill("#606A42");
  rect(0, 290, 500, 400, 40);
}

code with Arduino:

let loading = true;
let channels = [[], [], []]; //0 english channel , 1 - classical channel, 2 Al Khalidiya channel
let numberOfSongs = 0;
let numberOfSongsLoaded = 0;
let coinSound;
let selectedChannel;
let playing = false;
let songToPlay;
let toSend = 0;

function preload() {
  soundPaths = loadStrings("soundFileNames.txt");
  coinSound = loadSound("sounds/coin.mp3");

  // for (let i = 0; i < soundPaths.length; i++) {
  //   let words = soundPaths[i].split("_");
  //   //here we'll store sound paths in different arrays as different channels
  //   let channel = words[words.length - 2] + "_" + words[words.length - 1];
  //   switch (channel) {
  //     case "english_channel.mp3":
  //       channels[0].push("sounds/" + words.join("_"));
  //       break;
  //     case "classical_channel.mp3":
  //       channels[1].push("sounds/" + words.join("_"));
  //       break;
  //     case "khalidiya_channel.mp3":
  //       channels[2].push("sounds/" + words.join("_"));
  //       break;
  //   }
  // }
  // print(channels)
  // numberOfSongs = soundPaths.length;
  // //load every song in channel
  // for (let i = 0; i < channels.length; i++) {
  //   for (let j = 0; j < channels[i].length; j++) {
  //     channels[i][j] = loadSound(channels[i][j], () => {
  //       numberOfSongsLoaded += 1;
  //     });
  //   }
  // }
  /*
  ahlam_al_khalidiya_channel.mp3
cello_suite_classical_channel.mp3
gabl _aaarfak_al_khalidiya_channel.mp3
gymnopédie_classical_channel.mp3
i_lived_english_channel.mp3
mushfi_jorouhi_al_khalidiya_channel.mp3
overture_classical_channel.mp3
raheeb__al_khalidiya_channel.mp3
rouhe_thebak_al_khalidiya_channel.mp3
sundress_english_channel.mp3
swan_lake_suite_classical_channel.mp3
// sza_english_channel.mp3
virginia_beach_english_channel.mp3
whats_on_ur_mind_english_channel.mp3
  */

  {
    let sound = loadSound("sounds/i_lived_english_channel.mp3");
    channels[0].push(sound);
  }

  {
    let sound = loadSound("sounds/virginia_beach_english_channel.mp3");
    channels[0].push(sound);
  }
  
   
 


  
  
  
  
  
  
  
  
}

function setup() {
  createCanvas(600, 400);
  textAlign(CENTER, CENTER);

  //
  ratioPos = { x: width / 2, y: height * 2 };
  selectedChannel = floor(random(3));
  fft = new p5.FFT();
}

function draw() {
  background(40);
  if (loading) {
    rectMode(CORNER);
    strokeWeight(2);
    textSize(34);
    fill(255);
    text("LOADING...", width / 2, height / 2 - 20);
    noStroke();
    fill(255);
    rect(width / 2 - 150, height / 2 + 20, 300, 40);
    fill(20);
    rect(
      width / 2 - 150,
      height / 2 + 20,
      map(numberOfSongsLoaded, 0, numberOfSongs, 0, 300),
      40
    );
    if (numberOfSongsLoaded == numberOfSongs) {
      loading = false;
    }
  } else {
    //
    //draw Sound Spectrum
    let wave = fft.waveform();
    stroke(255, 50);
    noFill();
    beginShape();
    let x, y;
    for (let i = 0; i < wave.length; i++) {
      x = map(i, 0, wave.length, 0, width);
      y = map(wave[i], -1, 1, height, 0);
      curveVertex(x, y);
    }
    endShape();

    rectMode(CENTER);
    ratioPos.y = lerp(ratioPos.y, height / 2, 0.1);
    textSize(16);
    let channelName = "";
    switch (selectedChannel) {
      case 0:
        channelName = " ENGLISH SONGS ";
        break;
      case 1:
        channelName = " CLASSICAL SONGS ";
        break;
      case 2:
        channelName = " KHALIDIYA SONGS ";
        break;
    }
    drawRadio(channelName, playing);
    drawChannels();
  }
}

//functions to call from arduino's button
function keyPressed() {
  switch (key) {
    case "n":
      nextChannel();
      stopMusic();
      break;
    case "b":
      prevChannel();
      stopMusic();
      break;
    case " ":
      if (!playing) {
        playMusuc();
      } else {
        stopMusic();
      }
      break;
  }
}

function nextChannel() {
  selectedChannel += 1;
  if (selectedChannel >= 3) {
    selectedChannel = 0;
  }
}
function prevChannel() {
  selectedChannel -= 1;
  if (selectedChannel < 0) {
    selectedChannel = 2;
  }
}
function stopMusic() {
  if (songToPlay) {
    songToPlay.stop();
  }
  playing = false;
}
function playMusuc() {
  if (!playing) {
    coinSound.play();
    print("sel: " + selectedChannel);
    songToPlay = channels[selectedChannel][Math.floor(random(3))]; //random(channels[selectedChannel]);
    playing = true;
    songToPlay.loop();
  }
}

function drawChannels() {
  fill(100, 120, 100);
  rect(0, 150, 320, 70);
  fill(60, 70, 60);
  rect(0, 150, 300, 50);
  push();
  textAlign(LEFT, CENTER);
  let channels = ["English", "Classical", "Khalidiya"];
  textSize(12);
  for (let i = 0; i < 3; i++) {
    // text(channels[i],20,40+15*i);
    let x = 0;
    let y = 130 + 20 * i;
    noFill();
    if (selectedChannel == i) {
      fill(60, 90, 60);
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text("_" + channels[i], x - 150, y);
    } else {
      rect(x, y, 300, 15);
      fill(120, 150, 120);
      text(" " + channels[i], x - 150, y);
    }
  }
  pop();
}

function drawRadio(channel, playing = false) {
  translate(ratioPos.x, ratioPos.y);
  //disk
  noStroke();
  fill(150, 150, 220, 100);
  circle(0, 100, 450);

  fill(20);
  circle(0, 100, 350);
  fill(200, 100, 100);
  circle(0, 100, 150);

  let channelName = channel.split("");
  push();
  translate(0, 100);
  if (playing) rotate(-frameCount / 60);
  push();
  for (let i = 0; i < 20; i++) {
    rotate(TWO_PI / 20);
    fill(255);
    text("_", 0, -170);
  }
  pop();
  push();
  for (let i = 0; i < channelName.length; i++) {
    rotate(TWO_PI / channelName.length);
    fill(255);
    text(channelName[i].toUpperCase(), 0, -50);
  }

  pop();
  pop();
  fill(180);
  circle(0, 100, 80);

  stroke(255);
  noFill();
  arc(0, 100, 420, 420, -PI / 2 + 0.4, -PI / 2 + 0.8);

  noStroke();
  strokeWeight(2);
  fill("#606A42");
  rect(0, 290, 500, 400, 40);
}

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

  if (data != null) {
    print(data);
    // make sure there is actually a message
    // split the message
    let fromArduino = split(trim(data), ",");
    // if the right length, then proceed
    if (fromArduino.length == 4) {
      // 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
      let start = int(fromArduino[0]);
      let stop = int(fromArduino[1]);
      let next = int(fromArduino[2]);
      let prev = int(fromArduino[3]);

      if (start == 1) {
        print("start");
        playMusuc();
      } else if (stop == 1) {
        print("stop");
        stopMusic();
      } else if (next == 1) {
        print("next");
        nextChannel();
        stopMusic();
      } else if (prev == 1) {
        print("prev");
        prevChannel();
        stopMusic();
      }

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

function keyPressed() {
  if (key == " ") {
    // important to have in order to start the serial connection!!
    setUpSerial();
  }
}

Showcase:

Struggles:

When I initially incorporated Arduino buttons into the program, I envisioned a seamless integration that would enhance the user experience by providing tangible controls for the digital jukebox. However, this integration proved to be more challenging than anticipated. The entire program malfunctioned, leading to unexpected behaviors where buttons would either not respond or trigger incorrect actions. This technical hurdle was a significant setback, as it compromised the core functionality of the project—interacting with the music playlist through physical controls.

Faced with these difficulties, I realized that a pivot was necessary to maintain the integrity and usability of the project. I revisited the drawing board, reevaluating the interfacing between the Arduino and the software. This required stripping down complex parts of the code, simplifying the communication protocols, and implementing more robust error handling mechanisms to ensure that each button press accurately corresponded to the intended action. Although this pivot was a detour from my original vision, it was a crucial learning experience that emphasized the importance of adaptability and thorough testing in the development process. The revamped approach not only resolved the issues but also reinforced the project’s functionality, making it more reliable and user-friendly.

Future Improvements:

Based on the experiences and challenges encountered with the integration of Arduino buttons and the development of the digital jukebox, several future improvements can be outlined to enhance the project further:

1. Enhanced Error Handling and Debugging Tools: Implementing more sophisticated error handling mechanisms can help identify and resolve issues more efficiently when they arise during the interaction between the Arduino hardware and the software. Additionally, developing a suite of debugging tools or visual indicators in the software can help monitor the state and health of the system in real-time.

2. User Interface Improvements: Enhancing the user interface to provide clearer feedback and more intuitive controls can significantly improve the user experience. This could include visual indicators of button presses, more responsive animations, or a more aesthetically pleasing layout that mimics the classic jukebox style.

3. Expanded Music Library and Categorization: Expanding the music library to include more diverse genres and new channels can cater to a broader audience. Implementing a more dynamic categorization system where users can create custom playlists or choose from themed channels could add a new layer of interaction.

4. Wireless Control Options: Introducing wireless control options such as Bluetooth or Wi-Fi connectivity could allow users to control the jukebox from their smartphones or other devices. This could be particularly useful for accessibility purposes and to accommodate larger venues.

5. Improved Audio Quality and Effects: Upgrading the sound output hardware or integrating software that allows for better sound equalization and effects can enhance the overall listening experience. This might include features like bass boost, echo, and balance adjustments that users can control directly.

6. Sustainability and Maintenance: Considering the long-term sustainability and ease of maintenance in the design can ensure the jukebox remains functional and enjoyable for years. This could involve using more durable materials for the hardware, making the system modular for easy repairs, or providing software updates to keep the system secure and efficient.

7. Interactive Features and Gamification: Introducing interactive features such as music trivia, user contests, or gamification elements where users can earn points or rewards for their interaction can increase engagement and provide a more entertaining experience.

These improvements aim to refine the functionality, broaden the appeal, and ensure the longevity of the digital jukebox project, making it not only a nostalgic piece but a cutting-edge feature for any social or personal space.

Arduino code:

// Define the pin numbers for the buttons
const int pinGreen = 11; // Start
const int pinYellow = 9; // Stop
const int pinBlue = 5; // Next Channel
const int pinBlack = 3; // Previous Channel

void setup() {
  // Initialize the buttons as inputs
  pinMode(pinGreen, INPUT);
  pinMode(pinYellow, INPUT);
  pinMode(pinBlue, INPUT);
  pinMode(pinBlack, INPUT);

  // Start serial communication at 9600 bps
  Serial.begin(9600);
  // start the handshake
  while (Serial.available() <= 0) {
    Serial.println("0,0,0,0"); // send a starting message
    delay(50);
  }
}

void loop() {
  // Read the state of each button
  int stateGreen = digitalRead(pinGreen);
  int stateYellow = digitalRead(pinYellow);
  int stateBlue = digitalRead(pinBlue);
  int stateBlack = digitalRead(pinBlack);
  // Serial.println(toSend);

  // String toSend;

  // Send different commands based on button presses
  // if (stateGreen == HIGH) {
  //   toSend = "start";
  // }
  // else if (stateYellow == HIGH) {
  //   Serial.println("stop"); // Command to stop the radio
  // }
  // else if (stateBlue == HIGH) {
  //   Serial.println("next"); // Command to go to next channel
  // }
  // else if (stateBlack == HIGH) {
  //   Serial.println("prev"); // Command to go to previous channel
  // }

  while (Serial.available()) {
    int fromP5 = Serial.parseInt();
   
    if (Serial.read() == '\n') {
      Serial.print(stateGreen);
      Serial.print(",");
      Serial.print(stateYellow);
      Serial.print(",");
      Serial.print(stateBlue);
      Serial.print(",");
      Serial.println(stateBlack);
    }
  }

  // delay(100); // Delay to debounce and prevent multiple sends
}

 

Leave a Reply