Charlie Harvey

Seven More Languages: Lua — Day 3

The third and final day with Lua was a fun day. We consolidated what we had learned so far by building a multi channel MIDI player using the RtMIDI library and Lua’s ability to play well with others. In this case C++.

My experiences

Once again, I needed to make a fair number of tweaks to get my dev environment to play properly. First of all there were some tweaks to the libraries that were needed. Some of them have different names on Debian Jessie. $ sudo aptitude install build-essential liblua5.2-dev cmake librtmidi-dev

Getting MIDI working properly was also little bit challenging. I gave up on ZynAddSubFX and went back to using Timidity, which installed easily from apt. $ sudo aptitude install timidity

I needed to run timidity from the command line like this. Adding the vs just gives more debgging info, you can remove them if this is too noisy! $ timidity -iAvvvv -Os

Then I had MIDI and was ready to start hacking. Almost. I had to do some tweaking of the CMakeLists.txt file. As it turns out, Jessie calls the Lua library lua5.2 and the RtMidi library gets turned into lowercase. And you need to tell it where to find them. My final file read like this. cmake_minimum_required (VERSION 2.8) project (play) add_executable (play play.cpp) target_link_libraries (play lua5.2 rtmidi) include_directories(/usr/include/lua5.2 /usr/include/)

Now, I just had the programming to do. Much easier. Sort of.

Exercises

Easy exercises

Find the music for your favourite adventure movie's theme song and translate it to Lua. Play it with the music player you wrote.

Note that this is in the easy exercises. Also not that I do not read music. So the exercise ought to have read Learn to read and understand music and then find the music for your favourite …. I had a crack at the Imperial March from Star Wars. As I say in the video, it is recognizable but not too recognizable!

Still a pretty fun exercise. It was just a case of watching some youtube videos of kids playing it with the notes written on their keyboards and I was away. song.set_tempo(120)
song.part{ G1q, G1q, G1q, Ds1q, As1e, G1q, Ds1q, As1e, G1h, D2q, D2q, D2q, Ds2q, As1e, Fs1q, Ds1q, As1e, G1h, G2q, G1q, G1e, G2q, Fs2q, F2e, E2e, Ds2e, E2e, Gs1q, Cs2q, C2q, B1e, As1e, A1e, As1q, Ds1q, Fs1q, Ds1q, As1e, G1q, Ds1q, As1e, G1h } song.part{ G4q, G4q, G4q, Ds4q, As4e, G4q, Ds4q, As4e, G4h, D5q, D5q, D5q, Ds5q, As4e, Fs4q, Ds4q, As4e, G4h, G5q, G4q, G4e, G5q, Fs5q, F5e, E5e, Ds5e, E5e, Gs4q, Cs5q, C5q, B4e, As4e, A4e, As4q, Ds4q, Fs4q, Ds4q, As4e, G4q, Ds4q, As4e, G4h }

The way it stands, we have to put require 'notation' at the beginning of every song and song.go() at the end. Modify play.cpp to do this for you so that songs can just containn the tempo and parts.

This was dead easy, I just added a couple of calls to luaL_dostring to my play.cpp. luaL_dostring(L,"song = require 'notation'"); luaL_dofile(L, argv[1]); luaL_dostring(L,"song.go()");

Medium exercises

We’ve always played notes at one constant volume. Design a notation for louder or quieter notes, and modify your music player to support it.

My notation is just a number from 0 to 9 appended to the end of the note. This gets multiplied by 10 to set the volume. If it is not present, volume gets set at 5.

I realize that this means it won’t go up to 11. Life is hard.

The code change in play.cpp meant adding another parameter to the midi_send function. I found a good reference to the MIDI control codes and came up with the byte sequence 176, 7, new_volume. double volume = lua_tonumber(L, -4); double status = lua_tonumber(L, -3); double data1 = lua_tonumber(L, -2); double data2 = lua_tonumber(L, -1);
int channel_offset = channel - 1;
std::vector<unsigned char> volmsg(3); volmsg[0] = static_cast<unsigned char>(176); // cf: http://www.midi.org/techspecs/midimessages.php volmsg[1] = static_cast<unsigned char>(7); volmsg[2] = static_cast<unsigned char>(volume);
// Here we get the required port from our midi_ports array midi_ports[port].sendMessage(&volmsg);

This also meant changing the notation.lua file to parse the new volume part of our note description and pass it to the c function. Lua made this almost trivial by the way. local function parse_note(s) local letter, octave, value, vol = string.match(s, "([A-Gs]+)(%d+)(%a+)(%d?)") if not (letter and octave and value) then return end
volume = 5;
if(vol and tonumber(vol)) then volume = 1 + vol; if volume < 1 then volume = 1 end if volume > 10 then volume = 10 end end
return { note = note(letter, octave) , duration = duration(value) , letter = letter , octave = octave , volume = (volume*10) } end
local function play(note, duration, volume) midi_send(channel, volume, NOTE_DOWN, note, VELOCITY) scheduler.wait(duration) midi_send(channel, volume, NOTE_UP, note, VELOCITY) end

If there’s an error in the Lua script, the whole C++ program just exits without a word. Modify play.cpp to report any error information returned from the Lua interpreter.

After a quick google about I came up with some error handling logic, which I broke out into a seperate function as the main function was beginning to look a bit long. SRP and all that.

In main I added this code int err = luaL_dofile(L, argv[1]); if(err){ std::cerr << "Error: Problem reading file\n\n"; err_handle(L); return -3; } else { luaL_dostring(L,"song.go()"); }

And before it I added this new function // Catches file errors and pulls the error off the stack void err_handle(lua_State *state) { if (!lua_isstring(state, lua_gettop(state))) std::cerr << "Error: Undefined error. What the actual fuck?";
std::string str = lua_tostring(state, lua_gettop(state)); lua_pop(state, 1);
std::cerr << str; std::cerr << "\n"; }

Hard exercise

The current implementation of play.cpp opens one global MIDI output port. Change it to allow the user to pass a port into midi_send so that you can control more than one device from the same script.

OK. I admit that at first I misread the question and added the ability to set a channel per part. Then I spent ages before realizing that I could just make an array of RtMidiOut objects, each one controlling a port. And then I was done.

My implementation lets you set the channel and the port on a per part basis. It is limited to 20 ports (I have 4 on my machine), but it’d be easy to tweak if required.

First I added my function to initialize the midi objects. static RtMidiOut midi_main; RtMidiOut midi_ports [20]; // create a new object for each port and keep in this array; 20 should be enough
// Opens up all available Midi ports and stashes them into midi_ports[] int initialize_midi_ports() { unsigned int ports = midi_main.getPortCount(); if (ports < 1) { std::cerr << "Error: No Midi Ports available.\n"; return 0; }
for(int i=0; i<ports && i<20; i++) { RtMidiOut m = new RtMidiOut(); m->openPort(i); midi_ports[i]=m; }
return ports; }

This gets called from the main function which I have added some extra error handling and stuff to by this point. int main(int argc, const char* argv[]) { if (argc < 1) { std::cerr << "Error: Need a filename to play\n"; return -1; }
if (! initialize_midi_ports()) { std::cerr << "Error: Can't initialize MIDI ports"; return -2; }; lua_State* L = luaL_newstate(); luaL_openlibs(L);
lua_pushcfunction(L, midi_send); lua_setglobal(L, "midi_send");
luaL_dostring(L,"song = require 'notation'"); int err = luaL_dofile(L, argv[1]); if(err){ std::cerr << "Error: Problem reading file\n\n"; err_handle(L); return -3; } else { luaL_dostring(L,"song.go()"); }
lua_close(L); return 0; }

I also added the channel support into the midi_send function. It looks like this now. int midi_send(lua_State* L) { int port = lua_tonumber(L, -6);
double channel = lua_tonumber(L, -5);
// Added support for MIDI channels. It just means adding an offset to // the first bit of the messages we send. if (channel<1 || channel>16) { std::cerr << "Warning: MIDI only has 16 channels. You tried to use channel " << channel << ". Will play on channel 1 instead.\n"; channel = 1; }
double volume = lua_tonumber(L, -4); double status = lua_tonumber(L, -3); double data1 = lua_tonumber(L, -2); double data2 = lua_tonumber(L, -1);
int channel_offset = channel - 1;
std::vector<unsigned char> volmsg(3); volmsg[0] = static_cast<unsigned char>(176 + channel_offset); // cf: http://www.midi.org/techspecs/midimessages.php volmsg[1] = static_cast<unsigned char>(7); volmsg[2] = static_cast<unsigned char>(volume);
// Here we get the required port from our midi_ports array midi_ports[port].sendMessage(&volmsg);
std::vector<unsigned char> message(3);
message[0] = static_cast<unsigned char>(status + channel_offset);
message[1] = static_cast<unsigned char>(data1);
message[2] = static_cast<unsigned char>(data2);
midi_ports[port].sendMessage(&message);
return 0; }

Now I can play on multiple channels on multiple ports at the same time. Course it sounds crap as I am not a musician, but I am hoping that others will share their tunes so I can play them!

Lua: Conclusions

Day 3 of Lua convinced me of its value, it interfaces really elegantly and straightforwardly with C, which is something that many languages are not strong on. I have enjoyed the simplicity of the language and the power of the table data structure.

Ian Dees correctly predicted one of my gripes which was the unusual syntax. For a person who counts from 0, 1-indexed arrays are anathema. But this is a pretty trivial gripe. The concurrency model is not straightforward if you want to play with muticores. Which I imagine most people do.

Ian also picks up on something I felt when he says

The great thing about Lua is that you can build everything yourself. The downside is that you often have to build everything yourself.

Amen to that.

Aside from talking about Lua, I spent quite a lot of the third day exercising my (rusty) C skills. I can see why that was necessary to illustrate the power of Lua as a glue language. But I wonder if I might have learned more if the balance had leaned more towards Lua rather than C in the exercises.

The next language is Factor. For now adieu.


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.