← Back to ESAP
jul 2026

Building out the Piano Player

Servo-actuated keyboard prototype with wiring and a PWM servo driver

After testing servos and solenoids separately, we started turning those actuator ideas into an actual music-playing machine. The first version of the concept used servos that hit piano keys directly. That made the mechanism easier to prototype because each servo could be mounted near a key and commanded to move between a released angle and a pressed angle. We chose servos over solenoids because they required much simpler electronics, less moving parts, and drew significantly less voltage than solenoids. This allows us to play larger chords and more complex pieces. A downside, however, is that servos are slower to respond than solenoids.

The first question was whether a small servo could press a key fast enough and hard enough to make useful sound. We found that the maximum rhythm frequency was around 5 notes per second, or about eighth notes at 150 BPM. The same test exposed a mechanical limitation: the servos were only strong enough to make meaningful sound when the piano was set to touchless, and they did not have enough power or velocity to play loudly with touch sensitivity enabled.

Testing a micro servo pressing a piano key
Early testing showed that a 9 g servo could press a key, but velocity and force were limiting.
PCA9685 servo driver connected to many micro servos on a keyboard prototype
Scaling up required a PCA9685 PWM driver instead of trying to run every servo directly from GPIO.

Once a single-servo press worked, the next task was coordinating multiple servos in rhythm. We used seven servos to play "Twinkle Twinkle Little Star" in two speeds. The code represented the song as timestamped events:

struct SongEvent {
    unsigned long time;
    int note;
    bool isOn;
};

const int NOTE_ON_ANGLE = 60;
const int NOTE_OFF_ANGLE = 0;
const unsigned long CHECK_INTERVAL_MS = 25;

This event structure was useful because the song was no longer a blocking sequence of delays. Each event had a timestamp, a note number, and an on/off state. The loop could compare the time index against each event, move the matching servo, and keep checking at a fixed interval. That made the timing easier to reason about than writing a long chain of delays.

As the prototype grew, direct GPIO control was no longer enough. We moved to an Adafruit PCA9685 PWM servo driver over I2C. That gave one board with 16 channels at address 0x41, mapped to natural MIDI notes from C2 upward. The playback loop waited until a song event's timestamp arrived, found the servo index from the note number, and wrote either the on-angle or off-angle to that channel.

Scaling also made the electronics problems more visible. Soldering issues limited us to one port expander, giving around two octaves and a few notes short of the desired range for the piano part. Eventually, however, we were able to solder together 4 port expanders in a daisy chain, allowing us to cover all notes of the piano.

The final timing task was measuring how long a servo press took to register. I used a sensor threshold to detect when the keypress actually happened, then used Welford's running mean and variance algorithm to keep statistics in code. We then were able to calculate how long before the intended note time should the servo start moving.

We are now able to play many songs requiring chords and wide ranges of notes