DigitalJoel

2016/09/05

Robotic Xylophone – The Arduino Side

Filed under: arduino, development — digitaljoel @ 9:37 pm

As I said in my previous post, I was working on a project with a neighbor to create a bell kit that was controlled by an Arduino mega and used solenoids and magnets to strike the correct bells in the correct timing to make music.

The Java program in the previous post creates an event stream file in a binary format.  It’s not the most efficient, but it’s easier than parsing midi on the Arduino.  I’m sure I could have done it in 3 bytes per event instead of 4, but it is what it is.

Most of the code in the Arduino sample has to do with reading the event stream from an SD card.  Once it’s read, it simply inspects the event and performs one of three actions.

  1. Set a pin to HIGH
  2. Set a pin to LOW
  3. delay N milliseconds.

All the hard work of making the song was already done in the Java application.

The one problem I have had is that if there are too many solenoids fired at the same time the Arduino simply resets.  My guess is it’s because the solenoids power supply is drawing from the Arduino power supply.  If we were to give the solenoids a separate power supply then I believe it would work without issue.  This didn’t manifest itself until my daughter played a nice, jazzy version of silver bells.

Following is the documented code.  Sorry for whitespace problems, wordpress doesn’t make it easy…

/*
robotic xylophone player

created Nov 2015
by Joel Weight

SD card reading code based on ListFiles sample
with following credits:

created   Nov 2010
by David A. Mellis
modified 9 Apr 2012
by Tom Igoe
modified 2 Feb 2014
by Scott Fitzgerald
*/

#include <SPI.h>
#include <SD.h>

File root;

// SD information
int SDPIN = 53;

// delay identifier
int DELAY = 255;

// struct representing the Event that is written by the java file
struct Event {
  byte pin;
  byte value;
  int duration;
};

Event* playEvents = 0;
int playEventsSize = 0;

// called by arduino to setup everything.
void setup()
{
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }

  Serial.print("Initializing SD card...");
  // On the Ethernet Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output
  // or the SD library functions will not work.
  pinMode(SDPIN, OUTPUT);

  // initialize our output pins, 2-13 and 22-41, and make sure they are set low.
  for ( int i = 2; i < 14; i++ ) {
    pinMode(i, OUTPUT);
    digitalWrite( i, LOW );
  }
  for ( int i = 22; i < 42; i++ ) {
    pinMode( i, OUTPUT);
    digitalWrite( i, LOW );
  }

  // more checking for reading the file from the SD card reader.
  if (!SD.begin(SDPIN)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");

  root = SD.open("/");

  // debug code to print out the files in the root directory
  printDirectory(root, 0);

  Serial.println("done initializing!");
}

// Given an Event array and the number of Events, iterate through them
// processing each event sequentially.
void playSong( struct Event events[], int len ) {
  Serial.print( "song length: " );
  Serial.println( len);
  for ( int i = 0; i < len; i++ ) {
Event e = events[i];
// if it's a DELAY event, then simply delay for that amount of time.
if ( e.pin == DELAY ) {
Serial.println( "delay" );
delay( e.duration );
}
// otherwise write the specified value to the pin.
else {
printEvent( e );
digitalWrite( e.pin, e.value );
}
}
}

// debug method to print an event.
void printEvent( Event e ) {
Serial.print( e.pin );
Serial.print( " " );
Serial.print( e.value );
Serial.print( " " );
Serial.println( e.duration );
}

// method called over and over by arduino, which means we will loop through the song
// until the power is cut.
void loop() {
// load the song from the file.
// hard coded file name at this point.  Could put a UI on this or use
// hardware buttons to cycle from one song to the next, but that was more
// work than I was ready to do at this point.
loadSong( "/Joel.jwf" );
// once we have loaded the song, give a 5 second countdown.
for ( int i = 5; i < 0; i-- ) {
    Serial.println( i );
    delay(1000);
  }
  // and finally play the song.
  playSong( playEvents, playEventsSize );
}

// Helper to load the custom binary file that was written to the SD card by the java app.
void loadSong( const char* fileName ) {
  File file = SD.open(fileName);
  Serial.print( "Loading song " );
  Serial.println( file.name() );
  if ( file ) {
    if ( playEvents != 0) {
      delete [] playEvents;
    }
    // no real handling for a malformed file. Don't write a malformed file.
    playEventsSize = file.size()/sizeof(Event);
    playEvents = new Event[playEventsSize];
    Serial.print( "File contains " );
    Serial.print( playEventsSize );
    Serial.println( " events." );
    int i = 0;
    while ( file.available()) {
      playEvents[i++] = readEvent(file);
    }
    Serial.print( "Done loading file with " );
    Serial.print( playEventsSize );
    Serial.println( " events in it." );
  }
  else {
    Serial.println( "Unable to load file." );
  }
}

// Helper to read a single event from an opened file.  Handles parsing the format
// of byte, byte, int.
Event readEvent(File file ) {
  Event result;
  byte bytes[4];
  file.readBytes( bytes, sizeof(Event));
  result.pin = bytes[0];
  result.value = bytes[1];
  int duration = ((int)(bytes[2] << 8 ) | (int)bytes[3]);
  result.duration = duration;
  Serial.print( "Event read: " );
  printEvent( result );
  return result;
}

// Debug method to print the contents of a directory.
void printDirectory(File dir, int numTabs) {
   while(true) {

     File entry =  dir.openNextFile();
     if (! entry) {
       // no more files
       break;
     }
     for (uint8_t i=0; i<numTabs; i++) {
       Serial.print('\t');
     }
     Serial.print(entry.name());
     if (entry.isDirectory()) {
       Serial.println("/");
       printDirectory(entry, numTabs+1);
     } else {
       // files have sizes, directories do not
       Serial.print("\t\t");
       Serial.println(entry.size(), DEC);
     }
     entry.close();
   }
}

 

Advertisements

2016/09/03

Robotic Xylophone – The Java Side

Filed under: development, java — digitaljoel @ 4:54 pm

About a year ago a good friend called and asked if I would be interested in a little hobby program with him.  He wanted to create a robotic xylophone using an old bell kit he found in the local classifieds.  He wanted to control it using and Arduino Mega board.  His proposition was that he could do all the hardware work, but he didn’t know how to do the software. That’s where I came in.

He fabricated a mount for the bell kit.  It would hold the bell kit above solenoids mounted on a board.  There was one solenoid for each note in the bell kit.  He would then put neodymium magnets on top of the solenoid.  The magnets would be reversed from the polarity of the solenoid so that when current was applied to the solenoid it would shoot the pin up and so it would strike the bottom of the bell.  I thought it was pretty ingenious.

This was my first (and only) Arduino project, so I had a lot to learn.  We went through several iterations of ideas on how to get the music into the Arduino.  Maybe a web interface that would allow the user to “write” the music.  Sounded like a pain.

I have a child that I believe is talented musically (yeah, every dad will say that about their child) and I thought it would be fun for her to play a duet with herself.  The robotic bell kit replaying something she had already played, and then her playing the other part on her own bell kit.  We have a digital piano that allows us to record a track as it’s played and write it to USB in midi format.  So I decided that would be the way to get the music into the Arduino.

That meant I had to figure out how to parse the midi file on the Arduino.  It’s been a VERY long time since I have written any C code.  And I didn’t find any suitably easy libraries I could use to do it.  Finally, I didn’t want to learn the ins and outs of the midi format, so I looked to see if there was a midi parsing library for Java, which I’m very comfortable in.  Sure enough there was.  And even better, it was super easy to use and didn’t even require any other libraries, it’s part of core Java (maybe not once 9 comes out huh?)

So I decided what I would do is write a java program that would translate a midi file into a custom format that I could more easily read on the Arduino.  I would then write that file to an SD card which I would read from on the Arduino.  It took me longer to come to that design than it did to write all the code.

Speaking of code, here’s the Java side of things. I added a bunch of comments, so I won’t be doing any further explanation of it.


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;

/**
 * Class that reads a midi file and outputs a custom format that is then read in my arduino code.
 * The customer format consists for an event stream, where each event is 3 data members constituting
 *   a total of 4 bytes per event.
 * An event is [outPin, hi/lo, duration(ms)]
 * outPin : if -1 then command is to delay, otherwise it contains the pin to change hi/low value for
 * hi/lo : if not delay then set out pin to hi on 1, low on 0. Otherwise ignore.
 * duration: if delay, then this is duration, otherwise ignore
 *
 * So an event of [8,1,0] says to turn pin 8 high.
 *    an event of [-1,1,180] says to sleep for 180ms before handling the next event in the stream.
 */
public class MidiToArduino {

    public static void main( String... args ) {
        if ( args.length <= 0 ) {
            System.out.println( "usage: java MidiToArduino <file1> <file2> ..." );
        }
        for ( String filename : args ) {
            MidiToArduino instance = new MidiToArduino( filename );
            System.out.println( (instance.convert() ? "Converted: " : "Failed: " ) + filename );
        }
    }

    // arduino is expecting short, short, int (1 bytes, 1 bytes, 2 bytes)
    // pin, value, duration.

    private static Map<Short, Short> pinMap = new HashMap<>();

    private static final short LOWEST = 55;
    private static final short HIGHEST = 84;
    private static final short OCTAVE = 13;

    // we need to map notes from the midi stream to out pins on the arduino mega.
    // We only map pins 55 through 84 because that's all the notes that were available on
    // the bell kit we were using.  We map them to pins 2-13, and 22-38 becuase those
    // are the pins we are going to be using to ouptut signale to the solenoid.
    {
        // 53 = F
        // 84 = C
        // middle C = 60
        pinMap.put((short)55, (short)2);
        pinMap.put((short)56, (short)3);
        pinMap.put((short)57, (short)4);
        pinMap.put((short)58, (short)5);
        pinMap.put((short)59, (short)6);
        pinMap.put((short)60, (short)7);
        pinMap.put((short)61, (short)8);
        pinMap.put((short)62, (short)9);
        pinMap.put((short)63, (short)10);
        pinMap.put((short)64, (short)11);
        pinMap.put((short)65, (short)12);
        pinMap.put((short)66, (short)13);
        pinMap.put((short)67, (short)22);
        pinMap.put((short)68, (short)23);
        pinMap.put((short)69, (short)24);
        pinMap.put((short)70, (short)25);
        pinMap.put((short)71, (short)26);
        pinMap.put((short)72, (short)27);
        pinMap.put((short)73, (short)28);
        pinMap.put((short)74, (short)29);
        pinMap.put((short)75, (short)30);
        pinMap.put((short)76, (short)31);
        pinMap.put((short)77, (short)32);
        pinMap.put((short)78, (short)33);
        pinMap.put((short)79, (short)34);
        pinMap.put((short)80, (short)35);
        pinMap.put((short)81, (short)36);
        pinMap.put((short)82, (short)37);
        pinMap.put((short)83, (short)38);
        pinMap.put((short)84, (short)39);

    }

    // These are the commands within the midi stream that we are interested in.
    private static final int NOTE_ON = 0x90;
    private static final int NOTE_OFF = 0x80;
    // This is how long we want to allow the out pin on the arduino to remain high in order to play a note.
    private static final int DELAY_MS = 10;
    // This is the key for a DELAY event in the feed to the arduino
    private static final short DELAY = -1;

    private final String inputFileName;
    private final String outputFileName;

    public MidiToArduino( String filename ) {
        inputFileName = filename;
        outputFileName = getOutputFilename( inputFileName );
    }

    /**
     * This is where the work happens.  Not thread safe. For each conversion you must create a new instance of MidiToArduino.
     */
    public boolean convert() {
        // when a note is played we will then add an event to this queue so that we stop applying a high
        // signal to that pin after the appropriate amount of time, as specified in DELAY_MS.
        Queue<Event> liftEvents = new LinkedList<>();
        // This list keeps the final event stream that will be sent to the arduino.  It will be a merging of the
        // midi events and our newly created lift events.
        List<Event> allEvents = new ArrayList<>();
        try {
            // most of this is boilerplate to read the midi file.
            Sequence sequence = MidiSystem.getSequence(new File(inputFileName));

            // We need to map from the 'tick' in midi, to milliseconds, since that's what our delay is on arduino.
            long microseconds = sequence.getMicrosecondLength();
            long tickLength = sequence.getTickLength();

            long msPerTick = (microseconds/(tickLength*1000));

            System.out.println( "msPerTick = " + msPerTick );

            // choose the longest track of any multitrack midi file, assuming it is the one with the music notes.
            Track track = getLongestTrack( sequence );

            int lastTick = 0;
            for ( int i = 0; i < track.size(); i++ ) {
                // iterate through and output each event, but not key offs.
                MidiEvent event = track.get(i);
                MidiMessage message = event.getMessage();
                // all this message stuff is from the midi parsing library.
                if ( message instanceof ShortMessage )
                    long tick = event.getTick()*msPerTick;
                    // before we process the next event from the midi stream, we have to see if there are any
                    // notes that we are currently playing that need to be lifted.
                    while ( liftEvents.peek() != null && liftEvents.peek().tick < tick ) {                         lastTick = addEvent( allEvents, liftEvents.poll(), lastTick );                     }                     ShortMessage sm = (ShortMessage)message;                     int command = sm.getCommand();                     int velocity = sm.getData2();                     if ( command == NOTE_ON && velocity > 0 ) {
                        // we only want to handle this event if it's where the player played a note.
                        int key = sm.getData1();
                        Event e = new Event( (int)tick, (short)key, (short)1 );
                        lastTick = addEvent( allEvents, e, lastTick );
                        // make sure we insert a new event to lift this note at the appropriate time.
                        liftEvents.add(new Event((int)tick + DELAY_MS, (short)key, (short)0));
                    }
                }
            }
        } catch (InvalidMidiDataException | IOException e) {
            // bail
            e.printStackTrace();
            return false;
        }

        writeToFile( allEvents );

        return true;
    }

    private void writeToFile( List<Event> events ) {
        try (FileOutputStream out = new FileOutputStream( outputFileName )) {
            // we write just the bytes because it was easy to read on the arduino
            out.write(getAllBytes(events));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private byte[] getAllBytes(List<Event> allEvents) {
        // we know that each event will take 4 bytes, so we can easily create an array of the appropriate size.
        byte[] allBytes = new byte[allEvents.size()*4];
        int pos = 0;
        for ( Event e : allEvents ) {
            System.out.println( e + "," );
            for ( byte b : e.getBytes()) {
                allBytes[pos++] = b;
            }
        }
        return allBytes;
    }

    /**
     * Because I don't know which track contains the actual music, I just pick the track with the most events and assume.
     */
    private Track getLongestTrack( Sequence sequence ) {
        Track result = null;
        Track[] tracks = sequence.getTracks();
        for ( int i = 0; i < tracks.length; i++ ) {
            if ( result == null ) {
                result = tracks[i];
            }
            else if ( tracks[i].size() > result.size() ) {
                result = tracks[i];
            }
        }
        return result;
    }

    /**
     * Add an event and return the time of the last event.
     * @param events
     * @param newEvent
     * @return
     */
    private int addEvent( List<Event> events, Event newEvent, int lastTick ) {
        int duration = newEvent.tick - lastTick;
        if ( duration > 0 ) {
            events.add( new Event(duration, DELAY, (short)1 ));
        }
        events.add(new Event( 0, getPin( newEvent.pin ), newEvent.value));
        return lastTick + duration;
    }

    /**
     * Get the output pin that will be used to play a note.  If the note from the midi stream is too high
     * or too low, it will be adjusted by an octave in the right direction until it is within the range
     * that can be played by our bell kit.
     */
    private short getPin( short note ) {
      while ( note < LOWEST ) {
          System.out.println( "raising " + note );
          note += OCTAVE;
      }
      while ( note > HIGHEST ) {
          System.out.println( "lowering " + note );
          note -= OCTAVE;
      }
      return pinMap.get(note);
    }

    private String getOutputFilename( String input ) {
        String base = input;
        int index = input.lastIndexOf(".");
        if ( index > 0 ) {
            base = input.substring(0, index + 1 );
        }
        return base + "jwf";
    }

    /**
     * Simple representation of an event within our event stream.
     */
    private class Event {
        public final short pin;
        public final short value;
        public final int tick;

        public Event( int tick, short pin, short value) {
          this.pin = pin;
          this.value = value;
          this.tick = tick;
        }

        /**
         * Return the bytes as they should be written to the file.
         * [ pin (1 byte), value (1 byte), duration (2 bytes) ]
         * @return
         */
        public byte[] getBytes() {
            return new byte[] { (byte)pin, (byte)value, (byte)(tick >> 8), (byte)tick };
        }

        /**
         * Output the note for debugging in a format that is easy to copy and paste into an array in the arduino code
         * for testing a static event stream.
         */
        @Override
        public String toString() {
            return "{ " + (pin != DELAY ? pin : "DELAY") + ", " + value + ", " + tick + "}";
        }
    }
}

 

Create a free website or blog at WordPress.com.