Digital Musical Instrument

June, 2025

The Fanjo

The FanJo is a digital musical instrument crafted from an ordinary hand fan, capable of producing eight distinct pitches using a cosine wave oscillator. Vibrato and volume are dynamically controlled through built-in sensors that respond to subtle movements.

Inspiration

I was inspired to create the FanJo after covering Glide, a top contender in the 2019 Guthman Awards. The Glide is a digital musical instrument invented by Keith Groover as a “musical instrument for all”. Groover developed the Glide because he realized that most instruments aren’t optimally designed for humans. They’re awkward to hold, require precise alignment and posture, and are tiring to play for extended periods of time. As someone who’s never played a traditional instrument, I was really drawn to how accessible and approachable it seemed.

Hardware

Max Patch

This Max patch is a digital musical instrument interface that connects to an Arduino via serial communication and uses sensor inputs to control sound in real time. The Arduino sends data from several sensors: a photosensor, four buttons, a bend sensor, and a slide potentiometer. Each sensor is unpacked and routed into different control pathways. The photosensor and buttons work together to determine which pitch to play by triggering one of several cosine wave oscillators at set frequencies. The bend sensor is used to modulate the vibrato of the sound, while the slide potentiometer modulates the amplitude. All of this sensor-driven modulation culminates in a signal that is sent to the speakers via an audio output toggle.

Arduino Code

This Arduino code continuously reads the state of each sensor, smooths out any noise from the button presses using a debounce algorithm, and calibrates the photosensor in real time to adjust for changing light conditions. Each analog sensor reading is split into two parts to make the data more compact for transmission. Once all the sensor values are gathered and encoded, the code sends a 7-byte message over the serial port to Max, where the values can control sound parameters.

int photosensorPin = A0;
int buttonPins[4] = {2, 3, 4, 5};  // red 2, blue 3, yellow 4, green 5
int photosensorValue = 0;
int bendSensorPin = A1;
int sliderPin = A2;                // sliding potentiometer on A2

int photoMin = 1023;
int photoMax = 0;

const unsigned long debounceDelay = 50;  // debounce time in milliseconds
unsigned long lastDebounceTime[4] = {0, 0, 0, 0};
int lastButtonState[4] = {HIGH, HIGH, HIGH, HIGH}; // previous stable state
int buttonState[4] = {HIGH, HIGH, HIGH, HIGH};     // current stable state

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

  for (int i = 0; i < 4; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }
}

void loop() {
  // === Read and debounce buttons ===
  for (int i = 0; i < 4; i++) {
    int reading = digitalRead(buttonPins[i]);

    if (reading != lastButtonState[i]) {
      lastDebounceTime[i] = millis();  // reset debounce timer
    }

    if ((millis() - lastDebounceTime[i]) > debounceDelay) {
      if (reading != buttonState[i]) {
        buttonState[i] = reading;
      }
    }

    lastButtonState[i] = reading;
  }

  // === Read analog photosensor ===
  int rawPhoto = analogRead(photosensorPin);

  // === Update min/max for dynamic calibration ===
  if (rawPhoto < photoMin) photoMin = rawPhoto;
  if (rawPhoto > photoMax) photoMax = rawPhoto;

  // === Scale rawPhoto to calibrated 0–1023 range ===
  int scaledPhoto = map(rawPhoto, photoMin, photoMax, 0, 1023);
  scaledPhoto = constrain(scaledPhoto, 0, 1023);  // ensure within bounds

  int b1 = scaledPhoto >> 3; // 7 MSB
  int b2 = scaledPhoto & 7;  // 3 LSB

  // === Read bend sensor ===
  int bendReading = analogRead(bendSensorPin);
  int b3 = bendReading >> 3;
  int b4 = bendReading & 7;

  // === Read sliding potentiometer ===
  int sliderReading = analogRead(sliderPin);
  int b5 = sliderReading >> 3;
  int b6 = sliderReading & 7;

  // === Encode button states ===
  byte buttonByte = 0;
  for (int i = 0; i < 4; i++) {
    buttonByte |= (buttonState[i] == LOW ? 1 : 0) << i;
  }

  // === Send over Serial ===
  Serial.write(b1);  // photo MSB
  Serial.write(b2);  // photo LSB
  Serial.write(buttonByte);
  Serial.write(b3);  // bend MSB
  Serial.write(b4);  // bend LSB
  Serial.write(b5);  // slider MSB
  Serial.write(b6);  // slider LSB
  delay(100); // sampling delay
}