Last year I got my hands on an old, rusty 61-key frame. My father bought it at a clearance sale when he was a teenager, planning to turn it into a synthesizer. But he never got around using it, and the keyboard has just been collecting dust in the attic ever since.

Now I’m not that into experimental music, so I decided to use it to make an all-purpose MIDI keyboard. Fast forward a year and it has become quite a worthwhile project.

It features:

  • Velocity sensitive noteOn and noteOff
  • Selectable octave range
  • USB MIDI connection
  • Traditional MIDI in/out/trough connections
  • iOS-compatible bluetooth module

the lattermost being my favourite; no need for a computer or sound generator, just put your iPad on its stand and play! (Though as the HM10 doesn’t support the MIDI protocol,  it requires a separate app to interface with CoreMIDI apps.)

I will first go over how I built the case, continued with how I installed the 122 switches, the electronics, the Arduino code, and finally, the iOS interface and how to use it.

Building the Case

I decided on a very simple design of leftover 3mm MDF and a 9mm bottom plate. The keyframe was mounted on a hinge when I got it, which gave me the idea to make the top/back cover also hinge-able. This way you can put the electronics beneath the keys for a compact design, but have them easily accessible at the same time.

The plan was to polish it up to a mirror-finish, but I quit after making it only glossy. I used  a couple of $2 spray cans from the local discount store. For the money it still looks great though!

Note that these are organ-style keys that are meant to extend out on the front.

Adding Switches

It took me a while to find switches that were A) small B) silent and, because I needed 122 of them, C) cheap. Eventually I found these microswitches on AliExpress. 150 pieces for 9 dollars, not bad!

Each key has two microswitches underneath. When a key is played one is triggered just a little earlier than the other, and through calculating this interval we can work out the velocity of the key. That we hand over to the sound generator in the noteOn MIDI message. The end result is that the user can play a note either softly (piano) or firmly (forte).

The switches are mounted on two wooden rails, which allows for easy lateral/vertical adjustment. Every switch on the back row  has its own little pedestal so that it fits neatly inside the key. And finally, to dampen the plastic-on-plastic ‘tic’ noises I’ve hot-glued small bits of packaging foam to the contact areas.

They are wired up as a 6×22 matrix, and every button has its own diode (1N4007) to support concurrent keypresses.

This step took by far the longest. On the bright side, I could finally catch up with Dan Carlin’s Hardcore History 🙂

(Note that when I took these pictures I had forgotten the diodes.)

The Electronics

The main microcontroller is a ATmega328P-PU (same as on the Arduino UNO), with an ATmega8A-PU for USB controller and a HM10 module for bluetooth connectivity.  Additionally, three 74H595 shift registers for extra digital outputs and a 6N137 optocoupler for the MIDI IN port. The main circuit (excludes USB controller) is powered by two AA batteries boosted to 5V

The complete bill:

-- General --
1x ATmega328P-PU microcontroller
1x Tactile switch (reset)
1x 4.7kΩ resistor (reset-pullup)
1x 0.1uF capacitor (decoupling)
1x 16mHz crystal
2x 22pF capacitor (for crystal)
1x 3mm LED
1x 470Ω reistor (for LED)

-- Buttons --
3x 74H595 shift register
122x 8.5mm microswitch
128x 1n4001 diode
6x 10kΩ resistor
2x Tactile switch

-- Bluetooth --
1x HM10 on 5V adapter board

-- USB --
1x ATmega8A-PU microcontroller
1x Tactile switch (reset)
1x 4.7kΩ resistor (reset-pullup)
1x 10uF electrolytic capacitor (decoupling)
1x 16mHz crystal
2x 22pF capacitor (for crystal)
1x Female USB-B connector
2x 3V6 zener diode
1x 2k2Ω resistor
2x 68Ω resistor

-- MIDI --
1x 6N137 optocoupler
1x 1N4148 diode
3x 220Ω resistor
1x 270Ω resistor
2x Female 5-pin DIN connector

-- Power --
1x 5V voltage booster

-- Misc --
1x 5V buzzer (for debugging)
1x 270Ω resistor (for buzzer)
DIP sockets, 2.54mm pins and connectors
Plenty of wires!

Plus of course a programmer (I used my trusty old USBASP) and a USB to serial converter for debugging.

So here’s the schematic:

It was made on circuits.io — I probably should redo it sometime… A few extra points to clarify:

  • The ATmega8A is powered by the USB directly, so the USB VCC is not connected to the battery-powered circuit. Theoretically this should save a tiny bit of battery power when the USB controller is not needed… but I guess this is just me overthinking this 🙂
  • The LED indicates the input/output mode, which is cycled trough with a tactile switch. Another tactile switch changes the octave range, and the MIDI channel is selected through four jumper pins on the PCB. These are all connected as extra piano keys.
  • As I said, the microswitches are wired up as a 6×22 matrix. Each output of the shift registers is connected to six buttons. These are then connected to six input pins on the ATmega328P (Front Row 1/2/3, Back Row 1/2/3). Each of the input pins has a 10kΩ pull-down resistor.
  • The MIDI serial ports (DIN connectors) are wired as per the MIDI specs — I haven’t tested them though!
  • The USB MIDI controller is a design by Silvius on instructables.com.

The Arduino Sketch

The full sketch is on GitHub. I will provide some extra explanation here:

Setup
  1. First, I define some macros to set the IO registers directly (the Arduino functions are too slow for our intends and purposes, see here for more information).
  2. Next are the reference time intervals for the noteOn and noteOff events. The ‘SLOW’ values are for the white keys, the ‘FAST’ are for the black ones. I determined these trough trial and error.
  3. I define all the MIDI message codes, because for the pass-trough functionality we need to be able to interpret all possible messages.
  4. The two software serials are for the HM10 and the MIDI connections — the hardware serial is connected to the USB controller because I read it might not work otherwise.
  5. _btn_notes contains the MIDI note IDs of the buttons in the matrix. ’12’ is the note C0, and every increment is one semitone. So to shift the octave we simple need to add 12.  Normally we wouldn’t even need this lookup table, but I made a small mistake in wiring up the matrix which complicates things a little (hence the real_order array).
  6. _btn_types will tell us whether a certain button in the matrix is under a black key or a white key (as they require different time interval references).
Loop

Without even knowing it I wired up all of the matrix’ outputs to pin register B (pins PB0-PB5 on the ATmega328P). This makes looping over them even easier, because now we only need to increment the pin ID without switching pin registers.

If you don’t know how shift registers work, I recommend checking out this article. In the setup() function I used shiftOut() to check the channel jumpers, but to save precious clock cycles we will now handle the shift registers manually. The loop works as follows:

  1. First handle the LED and parse incoming MIDI data.
  2. Shift out ‘1’ by pulling up the data pin (D_HIGH), triggering one clock cycle (C_HIGH; C_LOW) and then triggering the latch pin (L_HIGH; L_LOW). Now we’ve set things up so the first set of six buttons can be read.
  3. Do the following twenty-two times:
    1. Read the first three inputs — the front row buttons:
      1. Read input
      2. Compare to previous state
      3. If it went from high to low, store the timestamp for use in the noteOff message
      4. If it went from low to high, calculate time since the corresponding back row button was triggered; convert to MIDI velocity; send MIDI noteOn message
    2. Read the last three inputs — the back row buttons:
      1. Read input
      2. Compare to previous state
      3. If it went from low to high, store the timestamp for use in the noteOn message
      4. If it went from high to low, calculate time since the corresponding front row button was released; convert to MIDI velocity; send MIDI noteOff message
    3. Insert ‘0’ into shift output to advance to the next six buttons (by keeping the data pin low, triggering one clock cycle (C_HIGH; C_LOW) and triggering the latch pin (L_HIGH; L_LOW))
  4. Now at the end of the loop, the shift registers are in place to read out the mode and octave buttons. We do this with debouncing.
  5. Repeat loop until the end of time, or the device is turned off. Whichever comes first.
MIDI parsing

The parse_incoming(byte b) function should be able to handle all possible incoming messages to provide pass-trough functionality. Though I have not tested this function, I translated it to Swift for the iOS app where it works just fine.

The MIDI protocol is quite a simple and restrained. With the exception of the SystemExclusive command, all messages have a fixed length. The interpreter works as follows:

  1. If it is a new message, determine the expected length (or 64 for SystemExclusive).
  2. Add to the buffer until we reach the expected length (or EndOfExclusive for SystemExclusive).
  3. Send buffer & reset variables

That’s it!

The USB Controller

No coding was required for the USB MIDI controller, Silvius on instructables.com has already done all the necessary work. So all I did was flash his pre-compiled hex file onto the ATmega8A, and set the fuses, to get it working. It does so right out of the box with applications like Garageband. It receives the MIDI messages from the ATmega328P over a serial connection at 31250bps.

The iOS app

Apple has defined its own Bluetooth LE MIDI protocol that has since been adopted by the MIDI Association. However, the HM10 does not support it because it only lets you configure short 16 bit UUIDs, not the 128 bit one required. While the $50 ReadBearLab module would work, I chose to stick with the $2 HM10 and use an extra app to interface with CoreMIDI.

This page has been most helpful in figuring out how to use CoreMIDI with Swift, along with the CoreMIDI documentation.

The full source code is once again available on GitHub, but the code of interest is exclusively in TableViewController.swift.

Creating a MIDI output port

To send MIDI messages via CoreMIDI we need to create a MIDIClientRef and a MIDIPortRef:

MIDIClientCreate("BleMidiClient" as CFString, nil, nil, &midiClient)
MIDIOutputPortCreate(midiClient, "BleMidiClient" as CFString, &midiOutputPort)
Finding midi destinations

When the user opens the app we need to retrieve all available MIDI destinations (e.g. the iGrand piano app), so in applicationDidBecomeActive we do the following:

midiDestinations = []

for i in 0 ..< MIDIGetNumberOfDestinations() {
    let destination = MIDIGetDestination(i)
    if destination != 0 {
        midiDestinations.append(destination)
    }
}

We also check if selectedDestination is still valid:

if selectedDestination != nil && selectedDestination! >= midiDestinations.count {
    selectedDestination = nil
}
Connecting to bluetooth devices

The app shows newly discovered devices in the second section of the tableView. When connected, the device moves to the first section. The name and identifier of the device are stored using UserDefaults so we can connect automatically the next time. I have a tutorial on the use of CoreBluetooth with HM10s here.

parsing incoming messages

The incoming bytes from the HM10 are parsed in peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?), using pretty much the same code as from Arduino sketch.

sending midi messages

Once we have received the full MIDI message, we send it over to the MIDI destination selected by the user.

let dest = midiDestinations[index]
var packet = UnsafeMutablePointer<MIDIPacket>.allocate(capacity: 1)
let packetList = UnsafeMutablePointer<MIDIPacketList>.allocate(capacity: 1)
packet = MIDIPacketListInit(packetList)
packet = MIDIPacketListAdd(packetList, 1024, packet, 0, buffer.count, buffer)
MIDISend(midiOutputPort, dest, packetList)
packet.deinitialize(count: 1)
packetList.deinitialize(count: 1)
packetList.deallocate(capacity: 1)

This was the hardest part to figure out. For some reason Apple made the data property of MIDIPacket a tuple, forcing us to access it like packet.data.0, packet.data.1, etc. The code above is a workaround I found on Stackoverflow to create packets of variable length.

In retrospect, I could just have created one packet for every byte — though that would not work with SystemExclusive messages.

Putting it all together

To connect it to an iOS device:

  1. Launch the CoreMIDI-enabled music app (e.g. iGrand)
  2. Launch the BluetoothMIDI app
  3. Connect to the HM10, select the CoreMIDI destination
  4. Switch back to the CoreMIDI app and play!

In this order, to prevent the memory-heavy music app from killing the bluetooth app.

It initially would not send the MIDI messages properly if four or more keys were played at the exact same time, but some extra optimisations inside the loop took care of that. This problem still persists with the USB connection though, but I hope that exchanging the two 3V3 zener diodes I used with 3V6 zener diodes will resolve that.

Conclusion

It has become quite a sleek looking keyboard — I like to call it the Stein;Way. Bonus points if you get the [double] reference. I worked on and off for a whole year on it and definitely learned a lot from this project.

The only problem?

I can’t play the piano.

Yeah, I know.