Pages

Saturday 26 October 2013

Arduino MIDI Music Box

It's time to do something useful with our small 8-bit computer. Music box is a fun and easy project. It's not demanding from the hardware point of view but it can be a little bit tricky from the software point of view itself - ideal project to learn a lot and not to spend the whole day soldering. What we will need: 

  • an Arduino (of course) 
  • Piezo buzzer

 ... and that's all. Now a couple of words of what we want to achieve. 

Requirements 


Let's point them out:

  • play monophonic melodies by generating tones (1 channel MIDI instrument)
  • the notes will be send by the PC through the serial port 
  • our musicbox should be visible to the PC as a MIDI port 
  • be able to use MIDI player of choice on PC to play MIDI files on our musicbox 


The requirements are specified. Unlike most of the simple projects around which play the same melody everytime, which most of the times is hardcoded in the program memory and in order to change it one must modify the tone frequencies and the timing between them and then re-flash the Arduino, our musicbox will simply act as a simple MIDI device and become a "MIDI sink" for incoming serial data. That means that more or less we can play any MIDI file with it that comes in our hands. The only limitation is the number of channels - the firmware which I'm going to discuss will play only one - the user will have to select what channel data should be send & played, but in general it's not impossible to enhance the implementation to support more, for demonstration purposes one is enough though. 

Technologies 


I need to discuss what kind of tools and technologies will be used. No matter how trivial a musicbox can look in the first place there will be quite a lot going on under the hood. Here are the things that will be used (in terms of software) on the Arduino side: 

  • beeper module from libpca - the timer will be used for tone generation 
  • SLIP module from libpca - the data will be send in binary format in SLIP frames from PC to the Arduino 

Everything has been already covered in my previous posts on this blog. If you're following the blog any of the above require no explanation. At the time being creating a more sophisticated project like this one is only a matter of putting the building blocks together. On the PC side I will use Perl to do the dirty work. No matter what you have heard or have been told, Perl is a great language far more superior than Python and I hope that again I'll prove it to every of you. Things that will be needed: 

  • Device::SerialPort - serial port communication 
  • MIDI::ALSA - for doing all the MIDI dirty work 
  • Digest::CRC - to calculate CRC checksum of the outgoing data PC side


PC side


Let's start with the PC side first. The Perl script will register new MIDI port and wait for the data written to it which then will be send through the Serial port to Arduino - pretty simple. The data I'm interested in from the MIDI stream incoming are so called "note_on/note_off" events. Unfortunately their not exactly in a format convenient and appropriate for this project. The note_on event defines the beginning of a particular not (a trigger to start generating tone of given key), we should stop generation when receiving note_off. This is not acceptable since it blocks possibility to generate any other tone in between and we would have to track when to really stop the generation on each incoming note_off event. 

Fortunately enough again Perl and CPAN saves the day. The MIDI::ALSA module defines a notion of "score" - a set of midi tracks in which every event has it's own length defined, so instead of two events note_on/note_off we have only one - note, which provides the following information:

{'note', start_time, duration, channel, note, velocity}

Exactly what we need. The bad news is that it doesn't work very well. In fact it seems that duration has some constantly increasing value along with the position in the MIDI file and in result isn't very useful at all, but at least instead of processing two events we have only one. From the data above I'm interested only in the event type -> 'note' the 'note' itself of course and the channel - I'll filter the data from one or couple of channels only to be send and played by Arduino since it would produce a complete chaos if we would want to try to play everything using only one instrument at once.

Since we will be sending the frequency to the Arduino, we need to convert the MIDI note number (0-127) into frequency domain. After doing a quick Google search for MIDI note frequencies I come up declaring the following array:

# midi notes
my @notes = (
8.1757989156,
8.6619572180,
9.1770239974,
10.3008611535,
10.3008611535,
10.9133822323,
11.5623257097,
12.2498573744,
12.9782717994,
13.7500000000,
14.5676175474,
15.4338531643,
16.3515978313,
17.3239144361,
18.3540479948,
19.4454364826,
20.6017223071,
21.8267644646,
23.1246514195,
24.4997147489,
25.9565435987,
27.5000000000,
29.1352350949,
30.8677063285,
32.7031956626,
34.6478288721,
36.7080959897,
38.8908729653,
41.2034446141,
43.6535289291,
46.2493028390,
48.9994294977,
51.9130871975,
55.0000000000,
58.2704701898,
61.7354126570,
65.4063913251,
69.2956577442,
73.4161919794,
77.7817459305,
82.4068892282,
87.3070578583,
92.4986056779,
97.9988589954,
103.8261743950,
110.0000000000,
116.5409403795,
123.4708253140,
130.8127826503,
138.5913154884,
146.8323839587,
155.5634918610,
164.8137784564,
174.6141157165,
184.9972113558,
195.9977179909,
207.6523487900,
220.0000000000,
233.0818807590,
246.9416506281,
261.6255653006,
277.1826309769,
293.6647679174,
311.1269837221,
329.6275569129,
349.2282314330,
369.9944227116,
391.9954359817,
415.3046975799,
440.0000000000,
466.1637615181,
493.8833012561,
523.2511306012,
554.3652619537,
587.3295358348,
622.2539674442,
659.2551138257,
698.4564628660,
739.9888454233,
783.9908719635,
830.6093951599,
880.0000000000,
932.3275230362,
987.7666025122,
1_046.5022612024,
1_108.7305239075,
1_174.6590716696,
1_244.5079348883,
1_318.5102276515,
1_396.9129257320,
1_479.9776908465,
1_567.9817439270,
1_661.2187903198,
1_760.0000000000,
1_864.6550460724,
1_975.5332050245,
2_093.0045224048,
2_217.4610478150,
2_349.3181433393,
2_489.0158697766,
2_637.0204553030,
2_793.8258514640,
2_959.9553816931,
3_135.9634878540,
3_322.4375806396,
3_520.0000000000,
3_729.3100921447,
3_951.0664100490,
4_186.0090448096,
4_434.9220956300,
4_698.6362866785,
4_978.0317395533,
5_274.0409106059,
5_587.6517029281,
5_919.9107633862,
5_919.9107633862,
6_644.8751612791,
7_040.0000000000,
7_458.6201842894,
7_902.1328200980,
8_372.0180896192,
8_869.8441912599,
9_397.2725733570,
9_956.0634791066,
10_548.0818212118,
11_175.3034058561,
11_839.8215267723,
12_543.8539514160,
); # notes

Great, one problem less. 

The MIDI sequencer


A MIDI sequencer must be registered on the PC side to provide an output for MIDI playback software. It's extremely easy with MIDI::ALSA module and Perl:

MIDI::ALSA::client('Arduino MIDI Beeper', 1, 1, 0);

and that's all. I register a new sequencer with one input port and one output port. We can already see it in the system:

$ aconnect -oil
client 0: 'System' [type=kernel]    0 'Timer           '    1 'Announce        'client 14: 'Midi Through' [type=kernel]    0 'Midi Through Port-0'client 128: 'Arduino MIDI Beeper' [type=user]    0 'Input port      '    1 'Output port     '

Ok, we are almost done, all that's left is to take the MIDI data and send it to Arduino. Let's analyze the main program loop, again there will be some Perl trickery but I'll try to de-crypt everything which is not obvious to everyone not very familiar with Perl.

The call to MIDI::ALSA::input() is blocking, it means that we will wait on the first line for the MIDI event before we'll send anything to the Arduino - that's a good thing, that way we do not have to worry about the timing, the notes will be played as soon as they arrive. Next I detect if the incoming event does not define the MIDI file end - I exit the loop if that's the case. 

Next thing is to convert the MIDI event to a "score event". Next two lines simply skip to another loop roll if the event does not contain any useful information (it's either empty or it's not a 'note' event). Just for the debugging purposes I dump the event to the console. Going further I filter the events (by skipping them) if in the command line channels to be player are specified, if not then data from every channel will be sent. Later on I simply convert the MIDI note to frequency and because of the problems with getting the reliable event 'duration' information from MIDI::ALSA I simply define a fixed duration for every note in milliseconds (50 in this example). Next the data is being placed in the SLIP frame and sent to Arduino to be played. Arduino should return the data back (again just for debugging purposes) - and that's all.

Arduino Side


Basically all the hard work has been already done and is hidden in libpca. Please refer to my previous posts for more details, since every aspect and function used for this mini project has been already thoroughly discussed. 

Arduino firmware will be very simple, all that needs to be done is to simply receive the SLIP frame, verify it and play the note inside. Let's have a look on the message format first.

Each note is defined as a pair of numbers - frequency and the duration. Each packet coming from the host contains the CRC16 checksum, a byte denoting how many notes have been sent and the notes themselves. I decided that in a single packet it will be possible to send up to four notes - that would require some timing information for each note itself so even though it's possible to sent up to 4 notes in one shot in the final version of this project I'm always sending only one.

The listing bellow present a complete source for the Arduino firmware.


In the while loop it wait's for a new SLIP frame, verify the data integrity and play the notes from the packet. After flashing the Arduino we can finally do some tests.

How to use it ?


The whole project snapshot can be obtained here. One can fetch the newest version of libpca and the project itself from my public GitHub repository as well. The snapshot and the GitHub version have a slightly different Makefile, paths to the libpca are a little bit different, everything else is exactly the same. 

GitHub


git clone git@github.com:dagon666/avr_Libpca pca
git clone git@github.com:dagon666/avr_ArduinoProjects projects

Snapshot


Download and unpack the snapshot from here


... the rest is common:

Build and flash the Arduino firmware:

cd projects/musicbox
make
make install

Launch the mid_converter.pl script (optionally provide channel numbers in the command line which will be played), get the sequencer details:

$ aconnect -oil

play your midi file using your player of choice (I'll use aplaymidi):

aplaymidi -p 128:0 <midi_file>

Musicbox in action 


Now that everything has been said it's time to see the thing in action. I'm using Timer 0 and OC0A compare output (which is located on PORTD6 - Pin6 on Arduino) to which the buzzer is connected.





1 comment: