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 }