This week, I wanted to continue my piano project, officially started last week with servo and solenoid test boards. The eventual plan is to have 88 actuators over the keys of a piano (likely the grand piano in the MIT SAE library). Since this week’s assignment was to make something big with the CNC, I decided it was fitting to build a housing for these actuators to sit over the piano keys.
I began by measuring out the piano at the house, capturing all the details of its structure.
Next, I designed two 5-foot-long beams, each 3 inches tall, to maximize the "h" term in the \( \frac{bh^3}{12} \) formula. One beam is designated for white key actuators and the other for black key actuators. Here’s the design:
I cut the design on OSB wood using the CNC machine. The entire job took less than 10 minutes. Afterward, I used a bandsaw and file for some final touches.
After assembling the pieces, I brought it to New Vassar's music room to test it on a piano. Here’s an image of the cover placed over the keys:
I glued the solenoid driver PCB and two solenoids (aligned with the F4 and Bb4 keys) onto the wooden cover. I programmed it to play "One-Note Samba." Spotify link to One-Note Samba.
Here’s the code I used:
#define NUM_SOLENOIDS 5
#define BPM 120
int solenoidPins[NUM_SOLENOIDS] = {D0, D1, D2, D3, D4}; // solenoid GPIO pins
enum noteTypes {
EIGHTH, // Eighth note
QUARTER, // Quarter note
HALF,
DOT_QUARTER,
EIGHTH_HALF, // 8th + 1/2
REST // Rest note
};
void setup() {
for (int i = 0; i < NUM_SOLENOIDS; i++) {
pinMode(solenoidPins[i], OUTPUT); // Set each solenoid pin as output
}
}
// Function to calculate the duration of a note based on BPM
int calculateNoteDuration(enum noteTypes note) {
int beatDuration = 60000 / BPM; // Calculate duration of a quarter note in milliseconds
switch (note) {
case EIGHTH:
return beatDuration / 2; // Eighth note is half of a quarter note
case QUARTER:
return beatDuration; // Quarter note is the full beat duration
case HALF:
return beatDuration * 2;
case DOT_QUARTER:
return beatDuration * 3 / 2;
case EIGHTH_HALF:
return beatDuration * 5 / 2;
case REST:
return beatDuration / 2; // eighth note rest
default:
return 0; // Default to no duration if undefined
}
}
// Helper function to play a note on a specified solenoid pin
void playNote(int solenoidPin, enum noteTypes note) {
int duration = calculateNoteDuration(note);
if (note != REST) { // Activate only if it's not a rest
digitalWrite(solenoidPin, HIGH); // Activate the solenoid
delay(duration/2); // Wait for the note duration
digitalWrite(solenoidPin, LOW); // Deactivate the solenoid
delay(duration/2);
} else {
delay(duration); // Wait for the rest duration without activating the solenoid
}
}
void samba(int solenoidPin) {
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, EIGHTH);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, EIGHTH);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, DOT_QUARTER);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, EIGHTH);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, EIGHTH);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, QUARTER);
playNote(solenoidPin, EIGHTH_HALF); // half + eighth
}
void loop() {
// One-note samba
for (int i = 0; i < 2; i++) {
samba(solenoidPins[0]);
}
samba(solenoidPins[1]);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], EIGHTH);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], EIGHTH);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], DOT_QUARTER);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], EIGHTH);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], EIGHTH);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[0], QUARTER);
playNote(solenoidPins[1], EIGHTH_HALF); // half + eighth
delay(2000);
}
I used the 3D-printed piano finger beneath the solenoid rod to prevent damage to the keys. Here’s what the finger looked like after repetitive use: