By design the monome grid does nothing on it's own. You the user assign it purpose and meaning: instrument, experiment, tool, toy... choose your own adventure. This grid is intended to be reimagined. Here we set forth to impart some introductory knowledge: potential energy for radical creative freedom.
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.
Node.js is generally used to build web servers but can also be used to interface with monome devices, send and receive MIDI messages, or work with raw audio stream using the many modules available on NPM.
If you're very new to node.js (or JavaScript) a few tutorials and introduction videos have been provided below:
Download Node.js: nodejs.org
Download the monome installer: monome.org/docs/begin
Download the code examples here: github.com/monome/grid-studies-nodejs/releases/latest
First install node.js. This guide may be helpful. You may need to look into node-gyp if you're on Windows. This tutorial isn't recommended for new node.js users on Windows.
Once installed, open a terminal window and create a new folder:
$ mkdir grid-studies
$ cd grid-studies
Next run npm init
to create a package.json
file. Just push enter on each of the prompts after running npm init
:
$ npm init
Next install the monome-grid
library:
$ npm install --save monome-grid
Now install the easymidi
library:
$ npm install --save easymidi
You can now copy the examples to this folder and run them using the node
command:
$ node grid_studies_2.js
The monome-grid library facilitates easy connection and communication with grids. Connect to a grid with the following code:
grid = require('monome-grid')();
Here the first monome device found is attached. If you want to connect to a specific grid (if you have more than one connected) you can specify a serial number, which can be found using serialosc-monitor.
grid = require('monome-grid')('m1000011');
The library communicates with serialosc to discover attached devices using OSC. For a detailed description of how the mechanism and protocol work, see monome.org/docs/tech:osc.
See grid_studies_2.js for this section.
The monome-grid library calls the function passed to grid.key()
upon receiving input from the grid. It has three parameters.
x : horizontal position (0-15)
y : vertical position (0-7)
s : state (1 = key down, 0 = key up)
Below we define the key function and simply print out incoming data.
grid.key(function(x, y, s) {
console.log('key received: ' + x + ', ' + y + ', ' + s);
});
We will, of course, do more interesting things with this function in the future.
Use the grid.refresh()
function to update the state of the LEDs on the grid. This function accepts an array which represents the entire grid, hence the full frame is updated on each call.
First create the 2-dimensional array:
var led = [];
for (var y = 0; y < 8; y++) {
led[y] = [];
}
This array has 8 rows which each contain an empty array representing the columns. Each LED on the grid can have a brightness range of 0-15, where 0 is off and 15 is maximum brightness. The values in the array are initially undefined, but we can set them all to 0 as follows:
var led = [];
for (var y = 0; y < 8; y++) {
led[y] = [];
for (var x = 0; x < 16; x++) {
led[y][x] = 0;
}
}
We can draw a small pattern by setting individual elements of the array:
led[0][0] = 15;
led[2][0] = 5;
led[0][2] = 5;
And finally, to copy this entire array to the grid:
grid.refresh(led);
As seen in grid_studies_2.js we place this code inside the refresh()
function. Upon running the sketch you will see this:
See grid_studies_2_3.js for this section.
The previous code refreshes the grid constantly with every call of refresh()
, which we typically do not want to do-- it doesn't make sense to have the computer spend time redrawing the same thing constantly.
Next we'll change the LED state to show which keys are being held, and only redraw when something has changed.
We add a boolean variable dirty
to indicate if the grid needs to be refreshed. We set this to true
initially so the grid is immediately cleared upon start.
Now we change the grid display upon incoming key data:
grid.key(function (x, y, s) {
led[y][x] = s * 15;
dirty = true;
});
Since s
is either 0 or 1, when we multiply it by 15 we get off or full brightness. We set the LED location according to the position of the incoming key press, x and y.
We changed the led
array, so we specify that the grid need refreshing:
dirty = true;
Once this flag is set, the grid will be updated on the next iteration of refresh()
:
function refresh() {
if(dirty) {
grid.refresh(led);
dirty = false;
}
}
Once we've refreshed the grid, we set the dirty
flag to false
so we're not needlessly refreshing.
The refresh()
function is called at 60fps unless you specify a different rate in the setInterval(refresh, 1000 / 60)
such as setInterval(refresh, 1000 / 10)
for 10fps.
The most basic decoupled interaction is a toggle. Turn the grid into a huge bank of toggles simply by changing line 27 (which is in the grid.key
callback function):
if(s == 1) led[y][x] ^= 15;
Now only key downs (s = 1) do something. They use an xor operator to toggle the LED value between 0 and 15, depending on the previous state of the LED.
Now we'll show how basic grid applications are developed by creating a step sequencer. We will add features incrementally:
See grid_studies_3_1.js for this step.
First we'll create a new array called step
that can hold 6 rows worth of step data. Note that we've created a new function called create2DArray
to help us create new arrays. On key input we'll look for key-down events in the top six rows:
// toggle steps
if(s == 1 && y < 6) {
step[y][x] ^= 1;
dirty = true;
}
If this condition is true, we toggle the corresponding position in the step
data and set the dirty flag so the grid will refresh.
We will "build" the LED display from scratch each time we need to refresh. This will be done inside of refresh()
so we no longer need the led
array to be global. Below we simply copy the step
data to the led
array, doing the proper multiplication by 15 in order to get full brightness.
if(dirty) {
led = create2DArray(8, 16);
// display steps
for(var x=0;x<16;x++)
for(var y=0;y<6;y++)
led[y][x] = step[y][x] * 15;
// update grid
grid.refresh(led);
dirty = false;
}
That'll get us started.
See grid_studies_3_2.js for this step.
For simplicity we're going to make a not-very-smart timer to drive our sequencer. Basically we'll count refresh()
cycles and upon matching a specified interval, we'll take a step forward in the sequence.
var timer = 0;
var play_position = 0;
var STEP_TIME = 10;
// ...
public void refresh() {
if(timer == STEP_TIME) {
if(play_position == 15)
play_position = 0;
else
play_position++;
timer = 0;
dirty = true;
}
else timer++;
// ...
In refresh()
we check timer
against STEP_TIME
. If they are equal, we process the next step, which in this case simply means incrementing play_position
, which must be wrapped to 0 if it's at the end. We reset timer
so it can count back up, and set the dirty flag so the grid redraws.
You can change the speed by altering STEP_TIME
.
For the redraw we add highlighting for the play position. Note how the multiply by 15 has been decreased to 11 to provide another mid-level brightness. We now have a series of brightness levels helping to indicate playback, lit keys, and currently active keys:
var highlight = 0;
// display steps
for(var x=0;x<16;x++) {
// highlight the play position
if(x == play_position)
highlight = 4;
else
highlight = 0;
for(var y=0;y<6;y++)
led[y][x] = step[y][x] * 11 + highlight;
}
During this loop which copies steps to the grid, we check if we're updating a column that is the play position. If so, we increase the highlight value. By adding this value during the copy we'll get a nice effect of an overlaid translucent bar.
See grid_studies_3_3.js for this step.
When the playhead advances to a new row we want something to happen which corresponds to the toggled-on rows. We'll do two things: we'll show separate visual feedback on the grid in the second-to-last (trigger) row, and we'll print something to the console.
Drawing the trigger row happens entirely in the refresh()
:
// draw trigger bar and on-states
for(int x=0;x<16;x++)
led[6][x] = 4;
for(int y=0;y<6;y++)
if(step[y][play_position] == 1)
led[6][y] = 15;
First we create a dim row (level 4 is fairly dim). Then we search through the step
array at the current play position, showing a bright indicator for each on state. This displays a sort of horizontal correlation of rows (or "channels") 1-6 current state.
For the screen drawing, we create a function trigger()
which gets passed values of activated steps. This is what we do, inside refresh()
right after we change `play_position':
// TRIGGER SOMETHING
for(int y=0;y<6;y++)
if(step[y][play_position] == 1)
trigger(y);
And then trigger()
itself:
function trigger(i) {
console.log('trigger at ' + i)
}
This simply prints on the console. We could have this trigger a MIDI note for example.
See grid_studies_3_4.js for this step.
We will now use the bottom row to dynamically cut the playback position. First let's add a position display to the last row, which will be inside refresh()
:
led[7][play_position] = 15;
Now we look for key presses in the last row, in the key
function:
grid.key(function (x, y, s) {
// toggle steps
if(s == 1 && y < 6) {
step[y][x] ^= 1;
dirty = true;
}
// cut
else if(y == 7) {
if(s == 1)
cutting = true;
next_position = x;
}
});
We've added two variables, cutting
and next_position
. Check out the changed code where we check the timer:
if(timer == STEP_TIME) {
if(cutting)
play_position = next_position;
else if(play_position == 15)
play_position = 0;
else
play_position++;
cutting = false;
// ...
Now, when pressing keys on the bottom row it will cue the next position to be played. Note that we set cutting = false
after each cycle so that each press only affects the timer once.
Lastly, we'll implement setting the loop start and end points with a two-press gesture: pressing and holding the start point, and pressing an end point while still holding the first key. We'll need to add a variable to count keys held, one to track the last key pressed, and variables to store the loop positions.
var keys_held = 0
var key_last = 0;
var loop_start = 0
var loop_end = 15;
We set loop_end to 15 to begin with the full range. We count keys held on the bottom row thusly:
keys_held = keys_held + (s*2) - 1;
By multiplying s
by 2 and then subtracting one, we add one on a key down and subtract one on a key up.
We'll then use the keys_held
counter to do different actions:
// cut and loop
else if(y == 7) {
// track number of keys held
keys_held = keys_held + (s*2) - 1;
// cut
if(s == 1 && keys_held == 1) {
cutting = true;
next_position = x;
key_last = x;
}
// set loop points
else if(s == 1 && keys_held == 2) {
loop_start = key_last;
loop_end = x;
}
}
We then modify the position change code:
if(timer == STEP_TIME) {
if(cutting)
play_position = next_position;
else if(play_position == 15)
play_position = 0;
else if(play_position == loop_end)
play_position = loop_start;
else
play_position++;
Done!
Let's make it actually send some MIDI notes. The easymidi
module can be used to create and listen to all types of MIDI events. First we'll get some basic initialization out of the way by loading the easymidi
module and creating a virtual MIDI input and output:
var easymidi = require('easymidi');
var output = new easymidi.Output('grid out', true);
var input = new easymidi.Input('grid in', true);
Next let's implement the trigger method and make it actually do something. We're going to add a new variable called type
that will take the values noteon
or noteoff
:
function trigger(type, i) {
output.send(type, {
note: 36 + i,
velocity: 127,
channel: 0
});
}
Next we need to modify the code that calls trigger to pass it the type
argument. We'll need to send both "noteon" and "noteoff" messages to avoid leaving hanging notes. To do this we'll calculate the last_play_position
and use it to trigger "noteoff" messages while sending "noteon" messages to the current play_position:
// TRIGGER SOMETHING
var last_play_position = play_position - 1;
if(last_play_position == -1)
last_play_position = 15;
for(var y=0;y<6;y++) {
if(step[y][last_play_position] == 1)
trigger('noteoff', y);
if(step[y][play_position] == 1)
trigger('noteon', y);
}
Now if you open Ableton Live for example, you should see a "grid out" device that you can enable and route to an instrument. You can also route the notes to a real MIDI device by changing this line:
var output = new easymidi.Output('grid out', true);
To something like this:
var output = new easymidi.Output('Real Device Name');
The true
argument means create a virtual device so don't use that when interfacing with a real MIDI output. If you aren't sure what the names of your devices are you can create a small script to check:
var easymidi = require('easymidi');
console.log(easymidi.getOutputs());
Save this as listmidi.js
and run it on the command line with node listmidi.js
. You should see an array of device names.
Now we'll make the sequencer respond to MIDI clock messages. First, we can delete the STEP_TIME and timer variables as they are no longer needed. We'll also want to move the code that handles timing out of the refresh()
function and into a few event handlers. Here's the main event handler to listen for midi clock messages:
var ticks = 0;
input.on('clock', function () {
ticks++;
if(ticks % 12 != 0)
return;
if(cutting)
play_position = next_position;
else if(play_position == 15)
play_position = 0;
else if(play_position == loop_end)
play_position = loop_start;
else
play_position++;
// TRIGGER SOMETHING
var last_play_position = play_position - 1;
if(last_play_position == -1)
last_play_position = 15;
for(var y=0;y<6;y++) {
if(step[y][last_play_position] == 1)
trigger('noteoff', y);
if(step[y][play_position] == 1)
trigger('noteon', y);
}
cutting = false;
dirty = true;
});
This is mostly a re-arrangement of existing code but there is a new variable called ticks
that we'll use to keep track of the number of MIDI clock messages we've received. This function will get called every time a 'clock' message is received on the input device (our virtual MIDI device named "grid in").
MIDI clock messages come in at a rate of 96 per measure, so on each tick we'll check if it's divisible evenly by 12 to provide 8th note resolution:
ticks++;
if(ticks % 12 != 0)
return;
First we increment ticks
and then do the divisibility check. If it's not divisible by 12 we'll return out of the function and wait for the next tick. If it is divisible we'll advance the play_position and trigger noteoff/noteon messages as needed.
This mostly works but we need to account for a few other MIDI messages. For example, this won't trigger notes in the first play_position
. To trigger these notes we'll listen for the 'start' MIDI message:
input.on('start', function () {
for(var y=0;y<6;y++)
if(step[y][play_position] == 1)
trigger('noteon', y);
});
Another issue we have is that if we reset the play position to 0 in our DAW we we still might be halfway through playing the segment on the sequencer. We should reset the play_position
and ticks
if this occurs:
input.on('position', function (data) { if(data.value != 0) return; ticks = 0; playposition = 0; if(loopstart) playposition = loopstart; dirty = true; });
And that's it! We have a fully functioning MIDI sequencer that can sync to MIDI clock and trigger notes. To try it out in Ableton, simply turn on the "sync" option for the "grid in" MIDI device and turn on the "track" option for the "grid out" device (and route it to an instrument). See grid_studies_3_5.js
for the completed MIDI implementation.
Node.js is maintained by the Joyent.
The monome-grid library was written and is maintained by Tom Dinchak.
This tutorial was created by Tom Dinchak for monome.org.
Contributions welcome. Submit a pull request to github.com/monome/grid-studies-nodejs or e-mail info@monome.org.