Ummmm what?
As with many of us, I've always got all sorts of wacky ideas floating around inside my melon. When Music Time contest came up, I thought it would be the perfect time to make this one happen! I've got a player piano from the early 1900's just sitting around making a great storage shelf and I've often considered a way to control it using modern techniques. This was the perfect time to try out using a Festo pneumatic air manifold left over from a project at work to see if I can tie them together and run via a script.
Soooo what did I do?
I modified a player piano so that I can run it via a Python script. This involved developing a 3D printed bracket that could bolt on to the player piano and route the vacuum lines out such that they could be routed into a Festo air manifold. This also required using Modbus (TCP) within a Python script to be able to send commands. I don't remember the exact age of the piano, but I believe it was from the "19-teens" or so. It's been upgraded to an electric vacuum pump but still has the foot pedals to
One original goal was to be able to send RTTTL ringtones to this via MQTT. Perhaps I might roll this in later, but for now I ended up developing a somewhat similar; but more capable protocol for encoding the music and ended up just hard coding to save time. As you'll read below, there were multiple avenues of delays in this project and I was happy to get it to the current state; right at the end of the contest period. I'm not too far away from having this implemented in my script but ended up just coding the few piano tunes I know.
What were my challenges?
Well - lots! As with a lot of fun projects, we're all always pushing and expanding and trying new things. Of course, I've never created a part for a player piano before, so that was a thing. Using Modbus TCP (or any version of Modbus really) was also something I've never done. Another significant challenge was setting up a /very/ unusual pneumatic system. Each of these has a section below for more details.
Also - I travel fairly often for work and also am married with two kids WHO GO TO BED EARLY NO YOU CAN'T STAY UP LATE PLEASE GO TO BED DADDY NEEDS TO PRACTICE PROGRAMMING PIANOS! ... ... ... which was also something to keep all balanced out I leave in Wisconsin and have had about 3-4 trips to Illinois, plus trips to Louisville and St. Louis to balance out during this build.
First some background on player pianos
A player piano is based on a normal piano, but uses a fantastic add-on system of pneumatics to allow music to be played back via specialized version of punched tape - aka a music scroll. A song consists of a scroll of paper with a series of holes punched into it. As the song plays, the paper is pulled across the "air bar" (note- no idea if that's the actual name of the part; thats just what I call it). Where there is a hole punched in the paper, then the vacuum for that particular key drops off and causes the key to strike the strings.
The idea with this project is to replace the paper scroll with a pneumatic manifold that I can control electronically and cause the same effect. Old-school players required someone to sit there and pump two foot pedals attached to bellows to build up vacuum and allow the system to run completely mechanically. Player pianos have since been 'modernized' with a vacuum pump which now works at the flip of a switch to create the vacuum.
So for an 88-key piano, to play a note, the paper music scroll will have only a single small hole for the one key that needs to be played; allowing air into the system in one specific location. So to upgrade to pneumatics, I would need to seal off all unused keys, and allow venting to ambient air for the active key that we want to 'press'.
Also I wanted a 100% non-invasive method to do this. I needed something that was easily reversible as to not damage or alter the original piano.
Lemmie see that 3D Printed bracket!
One major component that caused much consternation was the 3D printed adapter bracket. I had about 7 renditions (it felt like 100) of it to get the spacing dialed in for the holes, the hole positions, the pneumatic tube size (to make a vacuum-tight seal via push fit). I had to measure up the 'air bar' part of the player piano, then use Fusion360 to design the bracket. I started with a snap-on design that would hold itself to the piano; but found that it this severely inadequate in terms of a good vacuum seal. In the end, I used a design with one flat-faced manifold and two 'pinch pieces' that secure with M3 bolts and can be snugged down to the air bar of the piano to make a decent seal. I found that the top surface of the 3D printed bracket wasn't smooth enough right off the printer, so had to take to the garage and sand down with 80 and then 200-grit sand paper. This seemed to make it smooth enough that I could get a good seal.
Each rendition took a couple of days to design, print, install, test, iterate. I also had to counter-sink the holes for the pneumatic tubes and do some additional design work on the air channels to make sure they weren't anemic to airflow. The pitch of the holes was about 1.5mm so there wasn't much room for error. I also had issues with my super-cheap 3D printer not being able to make airtight prints. I had to use 5 shells to allow enough plastic to hold back the vacuum without seeping through the plastic piece. This increased printing time to about 4 hours per test; so I couldn't iterate more than once per day; allowing for additional interruptions like work travel.
I'll take the modBUS to work
A second challenge was using Modbus to control this system. I've never personally used modbus before so I had to learn about how it works. In a nutshell, Modbus protocol is sending (or getting) a set of 'coil' commands out to a remote [terminal] unit (RTU). This was traditionally done via the various serial protocols, but now is done via TCP. A coil would represent an individual output relay coil. In the end, it works pretty easily; but with using the Festo manifold I spent more than a few days trying to figure out why my programs weren't working.
I originally started using an Arduino MKR 1010 Wifi for this project and had it using a Modbus TCP library but wasn't getting solid communication to the Festo manifold. I then switched to running Python from my PC using a different Modbus library instead and still had the same issues. I had hoped that I could do better debugging from Pycharm than Arduino IDE but it turned out that didn't help.
The Modbus TCP library mostly just requires the IP address of the destination device; plus the command. My pneumatic manifold has 12 physical positions for pneumatic valves; each position having 2 unique pneumatic solenoid positions (12x2= 24). So to send a command, I had to use a 24-bit command. I broke this out into a low-word (16 bits) and a high word (8 bits anyways) to total 24 bits which could activate the 24 individual coils.
As noted above, using the Arduino library from a MKR 1010 Wifi, I got what appeared to be extremely poor reliability on the Festo manifold side. 75% of commands appeared to be dropped and the other 25% would be activated but drop out immediately. I found a DIP switch on the manifold that would "retain outputs on communications fault" and that seemed to help quite a bit. I could finally send a command to open/close a valve and the manifold would react predictably. But this still meant there was some underlying problems - ONES THAT I NEVER SOLVED .
To create the 24-bit command which I needed to send, My process was to photograph my hand on the piano then consult an Excel spreadsheet I created which I could input the keys I pressed and result with a hex-coded value representing the chord. The selected cell in the screenshot above shows the Hex value that I need to send via Modbus to achieve the cord indicated.
I swear this is better than RTTTL!
Although I'm already using RTTTL in my home automation system, one thing I noted about it is a limitation of not being polyphonic. An RTTTL ringtone is monophonic meaning it can only play a single tone at one given time. So any notion of a chord is "out the window". Imagine playing piano with one single finger instead of two hands. Once I got this system operational, I realized this artificial limitation and started down a different path. I also had to really scrounge to find enough of the correct valve types to get two octaves (24 keys) worth of notes. A lot of the RTTTL ringtones use 3+ octaves of notes so I wouldn't be able to play them properly to start with.
Pneumatic - wha??
So one section of this build relates a lot to the specific pneumatic valves along with the manifold base. Pneumatics are used a lot in industrial automation and in concept; they're very very simple - open a valve and air pressure comes through and can 'do work'. Aren't pneumatics basically the same as realys just for air instead of electricity? That's mostly true; but there's a lot of nuance behind the scenes. Does the valve have a natural resting state? How does it get to the resting state? How does it get to the 'activated' state? Pneumatic valves have a concept of 'pilot' pressure which can help shuttle the physical valve between its multiple possible states. This means that the physical valve uses 24V to activate, but won't actually move unless a certain amount of air pressure is present in the pilot side of the valve. I spent a couple of days working with the manifold before I realized that they weren't doing what I expected because I didn't have any incoming air pressure. They will work starting at about 1.5 bar of pilot pressure; but the problem was that the manifold by default takes pilot pressure directly from an incoming air supply port (aka 80 PSI). The problem I have is that I can't use that same physical channel since I needed to use ambient air on that channel to vent each valve to atmosphere for the Player Piano to work.
Eventually I found that the Festo ecosystem has a device which can separate the pilot air pressure from the primary air channel which vents to ambient. I was able to get one of these and install in the manifold. This really bummed me out since requiring a pilot pressure means that the system requires and external pressurized air source. So I had to run a compressed air line out to my air compressor in the garage to make this work. I was expecting that I could run without any external air lines; but that wasn't the case.
The image shown here shows the first valve (top left) I wanted to use which is internally two individual valves (2x @ 3/2); but used a pneumatic return. The valve on the bottom/right uses a spring return but is a 5/3 valve introducing a new limitation on which keys I can hit at the same time.
This is only referring to getting the valve from 'resting' to 'activated' state. To go from 'activated' back to 'resting' is a different case. Some pneumatic valves have physical spring returns by itself; but some require (just like above) an external pilot pressure to shuttle the valve back over to the 'resting' state. The original 12(!!!) valves I had were all air-return and thus complete garbage for the application . I was able to eventually scrounge together enough spring-return valves to make this work.
Another downside of the physical valves that I ended up using was they are 5/3 valves. This means that they have 5 physical ports and 3 positions. So this introduced another limitation versus the valves I had before which were 2x individual 3/2 valves in a single body. That meant I can't activate a pair of keys next to each other at the same time. This was a minor inconvenience but still a weird limitation. If you haven't worked with pneumatics; they are a weird beast. In practice, there's a lot of consideration into the desired failure states and non-powered states and this doesn't even begin to consider how flow controls work or mass going over-center on a mechanical system.
So I had to designate the manifold port 1 as the 'ambient' channel; allocating for a port blocker for the pilot pressure. Ports 3 and 5 were dead-headed to allow for no airflow.
Sooooooo it works, right?
Why yes, it works!
Have a peek at this video to see a demo reel
Note that some keys don't always work. I believe that this is due to the 3D Printed adapter bracket possibly not aligning with the holes of the piano's air bar. I think a few more renditions and this could be resolved.
And here is another peek at the background on how it works:
Y U No Code Post?
Okay fine - Here's the Python code.
I essentially use two arrays to play the code. One gives the chord/note to play, and one gives the intonation or time for each chord. This is passed to a method which can send the command via ModbusTCB (library). As noted; there are some underlying issues and I was able to get around them via a DIP switch on the Festo manifold. But other than that it works
import time
from pymodbus.client import ModbusTcpClient
def __init__(self):
print("Starting...")
def playSong(msgtime, msgValue):
print("Playing song!")
client = ModbusTcpClient('192.168.1.24')
timestart = time.time()
n = 0
while n < len(msgValue):
if time.time() >= (timestart + msgtime[n]):
client.connect()
client.write_register(0, msgValue[n] & 0xFFFF) # send low word
client.write_register(1, msgValue[n] >> 16) # send high word
client.close()
# client.write_register(0, 0x6c)
n += 1
print(time.time())
# client.write_register(0, 0x6c)
# play song format using a note duration instead of overall time
def playSongND(msgtime, msgValue):
print("Playing song!")
client = ModbusTcpClient('192.168.1.24')
lastTime = time.time()
n = 0
while n < len(msgValue):
if time.time() >= (lastTime + msgtime[n]):
client.connect()
client.write_register(0, msgValue[n] & 0xFFFF) # send low word
client.write_register(1, msgValue[n] >> 16) # send high word
client.close()
lastTime = time.time()
# client.write_register(0, 0x6c)
n += 1
print(time.time())
# client.write_register(0, 0x6c)
# play song format using a note duration instead of overall time
def playSongND1(msgtime, msgValue, transpose):
client = ModbusTcpClient('192.168.1.24')
lastTime = time.time()
n = 0
while n < len(msgValue):
client.connect()
msgValue[n] = msgValue[n] << transpose
client.write_register(0, msgValue[n] & 0xFFFF) # send low word
client.write_register(1, msgValue[n] >> 16) # send high word
client.close()
# print(msgValue[n])
lastTime = time.time()
# client.write_register(0, 0x6c)
while time.time() < lastTime + msgtime[n]:
pass
n += 1
# print(time.time())
# client.write_register(0, 0x6c)
# if __name__ == '__main__':
def main():
NextMessage = [.25, .25, .25, .25, .25, .25, .25, .25,
.25, .25, .25, .25, .25, .25, .25, .25,
.25, .25, .25, .25, .25, .25, .25, .25]
NextCommand = [0x000001, 0x000002, 0x000004, 0x000008, 0x000010, 0x000020, 0x000040, 0x000080,
0x000100, 0x000200, 0x000400, 0x000800, 0x001000, 0x002000, 0x004000, 0x008000,
0x010000, 0x020000, 0x040000, 0x080000, 0x100000, 0x200000, 0x400000, 0x800000]
# currentTime = time.time()
# timestart = time.time()
playSongND1(NextMessage, NextCommand, 0)
n = 0
client = ModbusTcpClient('192.168.1.24')
# play batman song
NextMessage = [.1, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25,
0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.7]
NextCommand = [0x000000, 0x008000, 0x004000, 0x002000, 0x004000, 0x008000, 0x004000, 0x002000, 0x004000,
0x008000, 0x004000, 0x002000, 0x004000, 0x008000, 0x000000, 0x008000]
playSongND1(NextMessage, NextCommand, 0)
#play it again!
playSongND1(NextMessage, NextCommand, 0)
# play other song
NextMessage = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
NextCommand = [0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000]
playSongND1(NextMessage, NextCommand, 6)
NextMessage = [.5, .2, .2, .5, 0.2, 0.2, 0.5, 0.3,
0.2, 0.3, 0.5, 0.15, 0.2
]
NextCommand = [0x000400, 0x010000, 0x008000, 0x000400, 0x010000, 0x008000, 0x000400,
0x010000, 0x008000, 0x000400, 0x010000, 0x008000, 0x000400
]
playSongND1(NextMessage, NextCommand, 6)
# play other song
NextMessage = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
NextCommand = [0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000, 0x000412, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000,
0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000, 0x000422, 0x000000]
playSongND1(NextMessage, NextCommand, 6)
# play other song
NextMessage = [1, 2, .1, 3, .1]
NextCommand = [0x000000, 0x910000, 0x000000, 0xA10000, 0x000000]
playSongND1(NextMessage, NextCommand, 0)
main()
This protocol that I ended up developing also takes a parameter for transposing into different keys. In the demo reel video, some keys don't play. I think that this is a physical issue in the adapter piece; but ran out of time to diagnose. RTTTL gives a specific key/note to play; but my system isn't tied to a definitive note anywhere on the piano. The transpose parameter let me push 'up or down' any number of keys easily.
Pretty cool buh?
Okay - calm down! But welcome to the spaghetti mess inside my head! I was pretty pumped as with many projects to see them go from a ridiculous idea into something that actually works in the physical world. This project raised a lot of challenges to overcome and lots of fun things to figure out. Adding MQTT would be actually pretty awesome and only a few lines of code - I have actually use that in Python before and it isn't too crazy. Then I could play the piano whenever someone rings our doorbell or the cat uses the litter box. tariq.ahmad this project will likely /not/ get left set up and running for very long since it's got a pretty low WAF; and weirdly a pretty low HAF as well. But it was super fun to play with!