Embedded systems and stuff

Hack a Guitar Hero drumset to use it with any computer over USB, Part 5

 

BitBucket repository is live: https://bitbucket.org/MostThingsWeb/usbdrumming/src
In the final post in this five part series, I’ll develop a client side application in Python to communicate with our drumset which was modified in part 4.

The final result will be a simple command line interface (for now) that looks like this:
 

 

Requirements

Before even choosing the language I wanted to use, I decided to list a few requirements for the client-side software.

  1. The software will automatically detect the drumset; the user will not have to manually choose a serial port.
  2. The software will support playing multiple drumpad sounds at once (like a real drumset). This will require audio mixing.
  3. The software must be cross-platform and easy to install.

 

My development process

I ended up developing this application three times: twice in C# and once in Python. There was also almost a C++ version. In this section, I’ll explain the initial prototype versions and why I decided upon Python in the end. To do this, I’ll go down the list of the requirements.

Requirement 1 is easy: if you remember, back in part 3 we added a feature to the embedded software that supports responding to “pings”:

// The client might want to know what we are, so let
// them ping us
if (Serial.available()){
  while (Serial.available()){
    Serial.read();
  }

  Serial.println("{ "msgType": "ping" }");
}

I didn’t want users to have to figure out which serial port to use. It should Just Work™. This is a very simple feature to implement, and because of this, requirement 1 didn’t actually steer me towards any one language.
 

Attempt #1: C# and MIDI synthesis

With no specific language in mind, I ended up first building this application in C#. My first attempt was to use MIDI synthesis (via the midi-dot-net project). The application worked, but it was very laggy. Also, the MIDI cymbals sucked. This first try taught me to be weary of MIDI synthesis; although I wasn’t using a brand new computer, I couldn’t imagine how it could be so slow to render a snare sound. From then on, I decided on prerendering each instrument sound. I feel like this was a good choice, because as you will see, the final product has barely any lag at all. I also wasn’t completely sure if I could reliably do MIDI synthesis cross-platform (and in a way that would allow users of lower end computers to join in on the fun).
 

Sidebar: Prerendering MIDI instrument sounds

How did I prerender those drum sounds? Very simple: by enslaving iTunes. iTunes exposes a very niceCOM API which I make use of to perform MIDI to wav conversions. MIDI files are first generated by the C# MIDI Toolkit. The code (below) is very simple but hackish. You can check out this code on BitBucket, under /dev_tools/MidiRecorder (no guarantees that it will work for you, though. I just put it up for completeness).

Console.WriteLine("Writing instrument files...");

var iTunes = new iTunesLib.iTunesAppClass();

for (int instrumentNum = 35; instrumentNum < 82; instrumentNum++) {
    if (instrumentNum == 49 || instrumentNum == 52 || instrumentNum == 55 || instrumentNum == 57) {
        continue;
    }

    using (Sequence sequence = new Sequence()) {
        Track track1 = new Track();
        sequence.Add(track1);

        ChannelMessageBuilder builder = new ChannelMessageBuilder();

        builder.Command = ChannelCommand.NoteOn;
        builder.MidiChannel = 9;
        builder.Data1 = instrumentNum;
        builder.Data2 = 127;
        builder.Build();

        track1.Insert(0, builder.Result);

        builder.Command = ChannelCommand.NoteOff;
        builder.Data2 = 0;
        builder.Build();

        track1.Insert(60, builder.Result);

        sequence.Save(String.Format("c:\instruments\i{0}.mid", instrumentNum));

    }

    Console.WriteLine("Adding file...");
    var newFile = iTunes.LibraryPlaylist.AddFile(@"C:instrumentsi" + instrumentNum.ToString() + ".mid");

    Console.WriteLine("Waiting...");
    while (newFile.InProgress) { }

    foreach (IITTrack newTrack in newFile.Tracks) {

        var convertedTrack = iTunes.ConvertTrack(newTrack);
        while (convertedTrack.InProgress) { }

        foreach (IITTrack t1 in convertedTrack.Tracks) {
            newTrack.Delete();
        }
        Console.WriteLine("Done");
    }
}

I exclude MIDI instruments #s 49, 52, 55, and 57 because those are cymbals (which, again, suck). Those four instrument sounds were taken from the NAudio project (and given proper attribution in the BitBucket repository).
 

Attempt #2: C# with prerendered instrument sounds

Next, I revisited using C# by incorporating my new prerendered instrument sounds. You can access attempt #2 at on BitBucket, under /dev_tools/USBDrumming Console. Here is a snippet of the application:

SoundPlayer bassDrum = new SoundPlayer(@"C:instrumentsi36.wav");
SoundPlayer closedHiHat = new SoundPlayer(@"C:instrumentsi42.wav");
SoundPlayer snare = new SoundPlayer(@"C:instrumentsi38.wav");
SoundPlayer midTom = new SoundPlayer(@"C:instrumentsi47.wav");
SoundPlayer rideCymbal = new SoundPlayer(@"C:instrumentsi51.wav");
SoundPlayer crash = new SoundPlayer(@"C:instrumentsi49.wav");

closedHiHat.Load();
snare.Load();
midTom.Load();
rideCymbal.Load();
crash.Load();
bassDrum.Load();

while (true) {
    var a = port.ReadLine();
    dynamic json = j.Deserialize(a, typeof(object)) as dynamic;

    Dictionary<string, object> c = json.hits[0];

    // var b = json.hits[0];
    Console.WriteLine(c["pad"]);
    switch ((int)c["pad"]) {
        case 45:
            midTom.Play();
            break;
        case 48:
            snare.Play();
            break;
            // etc..

This was a very hackish solution (as evidenced by the terrible variable names and lazy file locations), and it almost worked. The problem? Requirement 2. SoundPlayer doesn’t support sound mixing, so the end result sounded very unrealistic. For example, if you hit a cymbal and then immediately hit the snare, the cymbal sound would be interrupted before it completed. I couldn’t find a single C# library that supported multi-channel audio or a way to interop to such a library in a cross-platform manner, so I abandoned C# altogether
 

Attempt #2.5: C++

I can’t really call this attempt #3, since I never actually wrote the application, but I did consider using C++. Specifically, the openFrameworks project seemed very attractive to me. Plans to implement the application in C++ were complicated by the fact that I couldn’t get openFrameworks working. For various reasons, C++ was ruled out.
 

Attempt #3: Python

If you read the first sentence of this post, you’ll know that this attempt was destined to work. So how does Python solve our two remaining requirements (#2 and #3)?

  1. pygame supports multi-channel audio mixing and, like Python,
  2. is cross platform

 

The application

Third-party modules

Four thrid-party Python modules are used by the application:

I used setuptools/disutils to manage these modules, so if you’re on Linux or OSX, these three modules will be automatically installed when you invoke setup.py install. On Windows, you need to first install pygame manually (because automatic installation of that module on Windows doesn’t work for some reason), and then invoke setup.py install to install the remaining two modules.
 

Configuring pygame

I’m just going to talk about the important parts of the application, beginning with configuring pygame for multi-channel audio mixing.

# Find the location of our wav file directory
wav_directory = normpath(join(dirname(__file__), "resources/wavs"))
    
# Initialize the mixer
    
# IMPORTANT NOTE!: If the drum sounds are laggy or don't sound right, then
# play with the frequency and buffer parameters below. These are what seem to
# work for me on my laptop. If sounds just aren't playing, try changing the
# set_num_channels number from 16 to something higher
    
pygame.mixer.init(frequency=44100, size=-16, channels=8, buffer=1028)
pygame.mixer.set_num_channels(16)
pygame.init()

Like the comment says, you might need to play with the arguments of pygame.mixer.init or the number of channels to see what works with your computer. wav_directory is a global variable. Each drumpad hit will result in playing its corresponding sound on a new channel. In the case of hitting multiple pads really fast successively, that could result in many channels being used. I chose 16 for this reason, but you might need to increase it depending on how fast you can drum.
 

Automatic drumset detection

Let’s make use of this ping feature that I keep talking about.

ports = list_serial_ports()
ports_len = len(ports)
  
if ports_len == 0:
    write(Fore.RED + Style.BRIGHT)
    writeln("Failed!")
    print ""
    print "Error: No serial ports detected. Please make sure the drumset is plugged in. If you have just plugged in the drumset, wait a few seconds before trying again. If it still doesn't work, then unplug and then replug it in."
    print Fore.RESET + Style.RESET_ALL
    enter_quit()
    
found_drumset = False
drumset_port_id = None
    
# Scan ports for drumsets
for port_id in ports:
    port = configure_port(port_id)
    # Add a 5 second timeout so we aren't waiting forever for some serial-interfaced 
    # toaster to respond
    port.timeout = 5
    port.open()
    if test_port(port):
        found_drumset = True
        drumset_port_id = port_id

First we check that any serial ports exist at all. If there are, then we ping each one and wait for a response. The test_port function is defined as follows:


def test_port(port):
    port.write("ping")
    port.flush()
    
    try:
        response = port.readline()
        j = json.loads(response)
        msgType = j["msgType"]
        if msgType == "ping":
            return True
    except:
        return False
        
    return False

 

Handling pad hits

The final important piece of the puzzle is actually responding to pad hits.

# Only one of each of the hihats can be playing at a time, so store their sound objects
orange_hihat = None
yellow_hihat = None
    
# Note: two lines omitted here for brevity
    
while True:
    response = None
        
    try:
        response = json.loads(port.readline())
    except:
        # Note: a few lines omitted here for brevity
        continue
        
    for hit in response["hits"]:
        instrument = hit["pad"]

        if instrument == 49:
            # Orange hihat hit
            if orange_hihat is not None:
                orange_hihat.stop()
            orange_hihat = pygame.mixer.Sound(instrument_path(49))
            orange_hihat.play()
        elif instrument == 46:
            # Yellow hihat hit
            if yellow_hihat is not None:
                yellow_hihat.stop()
            yellow_hihat = pygame.mixer.Sound(instrument_path(46))
            yellow_hihat.play()
        else:
            # Some other pad/pedal hit
            sound = pygame.mixer.Sound(instrument_path(instrument))
            sound.play()

Instruments 46 and 49 are treated specially because those are the hihats. We only want to hear each hihat “playing” at most once, so we store the sound object for each hihat and interrupt already-playing sounds when a hit is detected. The other pads don’t need this treatment because the sounds they “play” are so short that it’s not likely for anyone but a trained musician to perceive too many “drum sounds” playing at once.

 

The final product

Check out the full Python application on BitBucket, under /software/pydrumming.
 

Installing and using the application

Installation

These steps assume that you already have Python 2.x installed (I developed on 2.7), and that you’ve cloned or downloaded the latest BitBucket repository. Follow these steps:

  1. Install setuptools if not already present.
  2. Windows only: Install the latest pygame binary.
  3. Navigate to softwarepydrumming and, at your command prompt, run setup.py install.
  4. pydrumming.exe (a different file extension on Linux and OSX, naturally) will be generated at C:Python27Scripts, or wherever your version of Python lives.

The pydrumming binary is what you should run whenever you want to use the application.
 

Usage

Pretty simple. Plug in the drumset to your USB port, and run the application.
 

Troubleshooting

My drumset is found, but the RBG LED flashes bloody murder and/or random drum sounds are playing even when I don’t hit pads:
Follow this troubleshooting checklist:

  1. Make sure the USB cable is properly inserted into the Pro Micro.
  2. Even if you only want to use the drumset with your computer, batteries must still be present in the control box. This is due to the primitive power switching (or lack thereof) on the PCB.
  3. It’s possible that you accidentally turned on the control box when the USB cable was plugged in the your computer. In that case, unplug the cable, remove the batteries, and wait 60 seconds. Then put the batteries back in and reconnect the cable.
  4. If the above doesn’t work, then a connection might be lose, or you assembled the PCB incorrectly (in which case only desoldering braid can save you now).

Error: No serial ports detected…: You didn’t install the Pro Micro driver correctly, or you didn’t plug in a drumset.
Error: One or more serial devices were detected, but did not respond to the ping…: You probably have other serial devices connected, but the Pro Micro drivers are still not installed correctly.
Warning! Pygame is not installed…: You’re running Windows but didn’t install pygame before running setup.py.
Some other error message:: You didn’t install something correctly (e.g. Python, pygame, setuptools, your window treatments, etc.)
 

Future improvements

There are a couple of things that I wish I had done differently or that I didn’t do because of time constraints:

  • Proper manual routing on the PCB
  • Make the PCB single-sided (no components on the bottom)
  • Right angle LED connector, to make it more space efficient and easier to fit into the drumset
  • Panel mounted micro-B connector
  • Report fault detection on client-side: the electronics are there, but I never implemented it in the Python program. It’s a simple addition but I ran out of time.

Hopefully one day.

That’s it

I hope you enjoyed this series of posts as much as I enjoyed working on this project. Thanks for reading along! If anyone follows along with the guide, please let me know in the comments below. And of course, if you make any modifications or improvements to any aspect of the project, please share them with me on BitBucket.

2 responses to “Hack a Guitar Hero drumset to use it with any computer over USB, Part 5”

  1. Jeremy Avatar
    Jeremy

    I have a set of these pads if anyone wants them for a reasonable price plus shipping. I was going to tackle something like this, but it appears that can not simply be turned into a simply controller – at least not easily.

    Anyway, I simply do not have time, and have many other (musical) ways to spend my days. But I need to thank you for posting all of this and making it available so it saved me the trouble. Visit me at shivasongster.com if you are interested in the pads.

    1. MostThingsWeb Avatar
      MostThingsWeb

      I agree, this is somewhat of an undertaking :). It’s very much still a “beta” project. I don’t have the time now, but I am very interested in simplifying the project. I’ve also toyed with the idea of selling it all as a kit.

      Have you considered selling the pads on Ebay or Amazon?

      Thanks for writing!

Leave a Reply

%d bloggers like this: