/*
Matriseq, a simple step sequencer for the Launchpad Pro.
Copyright 2007-2015 David Robillard
Matriseq is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or any later version.
Matriseq is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for details.
You should have received a copy of the GNU Affero General Public License
along with Matriseq. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include "lv2/lv2plug.in/ns/ext/atom/forge.h"
#include "lv2/lv2plug.in/ns/ext/log/log.h"
#include "lv2/lv2plug.in/ns/ext/log/logger.h"
#include "lv2/lv2plug.in/ns/ext/midi/midi.h"
#include "lv2/lv2plug.in/ns/ext/time/time.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"
#define MATRISEQ_URI "http://drobilla.net/plugins/matriseq"
#define GRID_H 8
#define GRID_W 8
#define SEQ_H (8 * 8)
#define SEQ_W 16
#define NOTE_MIN 28
#define STEP_TYPE 16
#define RING_SIZE 4096
static const uint8_t msg_set_col[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xC };
static const uint8_t msg_set_row[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xD };
static const uint8_t msg_show_text[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0x14 };
static const uint8_t msg_light_led[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xA };
typedef enum {
BLACK = 0,
DARK_GREY = 1,
LIGHT_GREY = 2,
WHITE = 3,
RED = 5,
YELLOW = 13,
DARK_GREEN = 15,
GREEN = 21
} Color;
typedef enum {
MATRISEQ_CONTROL_IN = 0,
MATRISEQ_CONTROL_OUT = 1,
MATRISEQ_PLAYBACK = 2
} PortIndex;
typedef enum {
BOTTOM_1 = 1,
BOTTOM_2 = 2,
BOTTOM_3 = 3,
BOTTOM_4 = 4,
BOTTOM_5 = 5,
BOTTOM_6 = 6,
BOTTOM_7 = 7,
BOTTOM_8 = 8,
LEFT_1 = 10,
LEFT_2 = 20,
LEFT_3 = 30,
LEFT_4 = 40,
LEFT_5 = 50,
LEFT_6 = 60,
LEFT_7 = 70,
LEFT_8 = 80,
RIGHT_1 = 19,
RIGHT_2 = 29,
RIGHT_3 = 39,
RIGHT_4 = 49,
RIGHT_5 = 59,
RIGHT_6 = 69,
RIGHT_7 = 79,
RIGHT_8 = 89,
TOP_1 = 91,
TOP_2 = 92,
TOP_3 = 93,
TOP_4 = 94,
TOP_5 = 95,
TOP_6 = 96,
TOP_7 = 97,
TOP_8 = 98,
TOP_9 = 99
} ButtonID;
// URIDs used by this plugin
typedef struct {
LV2_URID atom_Blank;
LV2_URID atom_Float;
LV2_URID log_Error;
LV2_URID midi_MidiEvent;
LV2_URID time_Position;
LV2_URID time_barBeat;
LV2_URID time_beatsPerMinute;
LV2_URID time_speed;
} MatriseqURIs;
typedef struct {
// Port buffers
LV2_Atom_Sequence* control_in;
LV2_Atom_Sequence* control_out;
LV2_Atom_Sequence* playback;
// Features
LV2_URID_Map* map;
LV2_Log_Log* log;
LV2_Log_Logger logger;
LV2_Atom_Forge control_forge;
LV2_Atom_Forge playback_forge;
MatriseqURIs uris;
// State
double rate;
float bpm;
float speed;
uint32_t beats_per_bar;
uint32_t time_frames;
uint32_t step;
uint8_t page_x;
uint8_t page_y;
uint32_t seq[SEQ_H][SEQ_W];
uint32_t last_inquiry;
bool refresh;
bool attached;
} Matriseq;
static LV2_Handle
instantiate(const LV2_Descriptor* descriptor,
double rate,
const char* bundle_path,
const LV2_Feature* const* features)
{
Matriseq* self = (Matriseq*)calloc(1, sizeof(Matriseq));
if (!self) {
return NULL;
}
// Get features
for (int i = 0; features[i]; ++i) {
if (!strcmp(features[i]->URI, LV2_URID__map)) {
self->map = (LV2_URID_Map*)features[i]->data;
} else if (!strcmp(features[i]->URI, LV2_LOG__log)) {
self->log = (LV2_Log_Log*)features[i]->data;
}
}
if (!self->map) {
lv2_log_error(&self->logger, "Missing feature urid:map\n");
free(self);
return NULL;
}
// Initialise LV2 stuff
LV2_URID_Map* map = self->map;
self->uris.atom_Blank = map->map(map->handle, LV2_ATOM__Blank);
self->uris.atom_Float = map->map(map->handle, LV2_ATOM__Float);
self->uris.log_Error = map->map(map->handle, LV2_LOG__Error);
self->uris.midi_MidiEvent = map->map(map->handle, LV2_MIDI__MidiEvent);
self->uris.time_Position = map->map(map->handle, LV2_TIME__Position);
self->uris.time_barBeat = map->map(map->handle, LV2_TIME__barBeat);
self->uris.time_beatsPerMinute = map->map(map->handle, LV2_TIME__beatsPerMinute);
self->uris.time_speed = map->map(map->handle, LV2_TIME__speed);
lv2_atom_forge_init(&self->control_forge, self->map);
lv2_atom_forge_init(&self->playback_forge, self->map);
lv2_log_logger_init(&self->logger, self->map, self->log);
// Initialise state
self->rate = rate;
self->bpm = 140.0f;
//self->speed = 0.0f;
self->speed = 1.0f;
self->beats_per_bar = 4;
self->page_y = 1; // Start at note 36 (kick)
return (LV2_Handle)self;
}
static void
connect_port(LV2_Handle instance,
uint32_t port,
void* data)
{
Matriseq* self = (Matriseq*)instance;
switch ((PortIndex)port) {
case MATRISEQ_CONTROL_IN:
self->control_in = (LV2_Atom_Sequence*)data;
break;
case MATRISEQ_CONTROL_OUT:
self->control_out = (LV2_Atom_Sequence*)data;
break;
case MATRISEQ_PLAYBACK:
self->playback = (LV2_Atom_Sequence*)data;
break;
}
}
static uint32_t*
get_cell(Matriseq* self, uint8_t pad_x, uint8_t pad_y)
{
const uint32_t seq_x = (self->page_x * GRID_W) + pad_x;
const uint32_t seq_y = (self->page_y * GRID_H) + pad_y;
return &self->seq[seq_y][seq_x];
}
static void
on_button(Matriseq* self, uint8_t num, uint8_t value)
{
if (!value) {
return; // Ignore button release
}
if (num == TOP_1 && self->page_y < 7) { // Page up
++self->page_y;
self->refresh = true;
} else if (num == TOP_2 && self->page_y > 0) { // Page down
--self->page_y;
self->refresh = true;
} else if (num == TOP_3 && self->page_x > 0) { // Page left
--self->page_x;
self->refresh = true;
} else if (num == TOP_4 && self->page_x < 1) { // Page right
++self->page_x;
self->refresh = true;
} else if (num % 10 == 9) { // Right button column
self->page_y = num / 10 - 1;
self->refresh = true;
} else if (num <= BOTTOM_8) { // Bottom button row
if ((num - 1) * GRID_W < SEQ_W) {
self->page_x = num - 1;
self->refresh = true;
}
}
}
static void
write_control(Matriseq* self, uint32_t t, const uint8_t* msg, uint32_t n)
{
lv2_atom_forge_frame_time(&self->control_forge, t);
lv2_atom_forge_atom(&self->control_forge, n, self->uris.midi_MidiEvent);
lv2_atom_forge_write(&self->control_forge, msg, n);
}
/** Pad on the central 8x8 grid pressed. */
static void
on_pad(Matriseq* self, uint32_t t, uint8_t x, uint8_t y, uint8_t vel)
{
if (vel > 0) {
// Flip sequence cell
uint32_t* cell = get_cell(self, x, y);
*cell = !*cell;
// Illuminate pad
const uint8_t msg[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xA,
(y + 1) * 10 + x + 1,
*cell ? GREEN : BLACK,
0xF7 };
write_control(self, t, msg, sizeof(msg));
}
}
static uint8_t
step_button_color(Matriseq* self, uint8_t step, uint8_t col)
{
const uint32_t page_begin = self->page_x * 8;
const uint32_t page_end = (self->page_x + 1) * 8;
if (step >= page_begin && step < page_end && step % 8 == col) {
return GREEN;
} else if (self->page_x == col) {
return DARK_GREY;
}
return BLACK;
}
/** Display scrolling text banner. */
static void
pad_show_text(Matriseq* self, uint32_t t, const char* text)
{
const size_t len = strlen(text);
uint8_t msg[sizeof(msg_show_text) + 3 + len + 1];
memcpy(msg, msg_show_text, sizeof(msg_show_text));
uint8_t* ptr = msg + sizeof(msg_show_text);
*ptr++ = DARK_GREY; // Text color
*ptr++ = 0; // Loop
*ptr++ = 7; // Speed (1-7)
memcpy((char*)ptr, text, len);
ptr += len;
*ptr++ = 0xF7;
write_control(self, t, msg, sizeof(msg));
}
/** Update step indicators when the page has not changed. */
static void
pad_update_step(Matriseq* self, uint32_t t, uint32_t old_step, uint32_t new_step)
{
// Turn off old step indicator
const uint8_t off_msg[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xA,
old_step % 8 + 1,
step_button_color(self, new_step, old_step % 8),
0xF7 };
write_control(self, t, off_msg, sizeof(off_msg));
// Turn on new step indicator
const uint8_t on_msg[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xA,
new_step % 8 + 1,
step_button_color(self, new_step, new_step % 8),
0xF7 };
write_control(self, t, on_msg, sizeof(on_msg));
}
/** Refresh page and step indicators. */
static void
pad_refresh_position(Matriseq* self, uint32_t t)
{
// Set bottom horizontal tick/page indicators
uint8_t xmsg[sizeof(msg_set_row) + 11];
memcpy(xmsg, msg_set_row, sizeof(msg_set_row));
uint8_t* ptr = xmsg + sizeof(msg_set_row);
*ptr++ = 0; // Row number 0 (bottom)
*ptr++ = 0; // Non-existent bottom left button color
for (int i = 0; i < 8; ++i) {
*ptr++ = step_button_color(self, self->step, i);
}
*ptr++ = 0xF7;
write_control(self, t, xmsg, sizeof(xmsg));
// Set right vertical page indicators
uint8_t ymsg[sizeof(msg_set_col) + 11];
memcpy(ymsg, msg_set_col, sizeof(msg_set_col));
ptr = ymsg + sizeof(msg_set_col);
*ptr++ = 9; // Column number 9 (right)
*ptr++ = 0; // Non-existent bottom right button color
for (int i = 0; i < 8; ++i) {
*ptr++ = self->page_y == i ? DARK_GREY : BLACK;
}
*ptr++ = 0xF7;
write_control(self, t, ymsg, sizeof(ymsg));
}
static void
pad_clear(Matriseq* self, uint32_t t)
{
const uint8_t msg[] = { 0xF0, 0x00, 0x20, 0x29, 0x02, 0x10, 0xE, 0, 0xF7 };
write_control(self, t, msg, sizeof(msg));
}
static void
pad_refresh_grid(Matriseq* self, uint32_t t)
{
uint8_t msg[136];
memcpy(msg, msg_light_led, sizeof(msg_light_led));
uint8_t* ptr = msg + sizeof(msg_light_led);
for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) {
const uint32_t gx = (self->page_x * GRID_W) + x;
const uint32_t gy = (self->page_y * GRID_H) + y;
*ptr++ = (y + 1) * 10 + x + 1;
*ptr++ = self->seq[gy][gx] ? GREEN : BLACK;
}
}
*ptr++ = 0xF7;
write_control(self, t, msg, sizeof(msg));
}
static void
send_device_inquiry(Matriseq* self, uint32_t t)
{
const uint8_t msg[] = { 0xF0, 0x7E, 0x7F, 0x06, 0x01, 0xF7 };
write_control(self, t, msg, sizeof(msg));
self->last_inquiry = self->time_frames;
}
static void
run(LV2_Handle instance, uint32_t n_frames)
{
Matriseq* self = (Matriseq*)instance;
const MatriseqURIs* uris = &self->uris;
const float s_per_beat = 60.0f / self->bpm;
const float s_per_step = s_per_beat * self->beats_per_bar / STEP_TYPE;
// Set output buffers for forges
lv2_atom_forge_set_buffer(
&self->control_forge, (uint8_t*)self->control_out, self->control_out->atom.size);
lv2_atom_forge_set_buffer(
&self->playback_forge, (uint8_t*)self->playback, self->playback->atom.size);
// Initialise outputs to empty sequence
LV2_Atom_Forge_Frame control_frame;
LV2_Atom_Forge_Frame playback_frame;
lv2_atom_forge_sequence_head(&self->control_forge, &control_frame, 0);
lv2_atom_forge_sequence_head(&self->playback_forge, &playback_frame, 0);
if (!self->attached && self->time_frames - self->last_inquiry > self->rate) {
// Haven't heard from Launchpad, send device inquiry once a second
send_device_inquiry(self, 0);
}
// Work forwards in time frame by frame, handling events as we go
const LV2_Atom_Sequence* in = self->control_in;
const LV2_Atom_Event* ev = lv2_atom_sequence_begin(&in->body);
for (uint32_t t = 0; t < n_frames; ++t) {
while (!lv2_atom_sequence_is_end(&in->body, in->atom.size, ev) &&
ev->time.frames == t) {
if (ev->body.type == uris->atom_Blank) {
const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body;
if (obj->body.otype == uris->time_Position) {
// Update transport position and speed
LV2_Atom *beat = NULL, *bpm = NULL, *speed = NULL;
lv2_atom_object_get(obj,
uris->time_barBeat, &beat,
uris->time_beatsPerMinute, &bpm,
uris->time_speed, &speed,
NULL);
if (bpm && bpm->type == uris->atom_Float) {
self->bpm = ((LV2_Atom_Float*)bpm)->body;
}
if (beat && beat->type == uris->atom_Float) {
self->time_frames = (((LV2_Atom_Float*)beat)->body
* s_per_beat
* self->rate);
}
if (speed && speed->type == uris->atom_Float) {
self->speed = ((LV2_Atom_Float*)speed)->body;
}
}
} else if (ev->body.type == uris->midi_MidiEvent) {
const uint8_t* const msg = (const uint8_t*)(ev + 1);
switch (lv2_midi_message_type(msg)) {
case LV2_MIDI_MSG_NOTE_ON:
on_pad(self, t, msg[1] % 10 - 1, msg[1] / 10 - 1, msg[2]);
break;
case LV2_MIDI_MSG_CONTROLLER:
on_button(self, msg[1], msg[2]);
break;
case LV2_MIDI_MSG_SYSTEM_EXCLUSIVE:
if (msg[1] == 0x7E) {
// Launchpad responded to device inquiry, initialise
pad_clear(self, t);
pad_refresh_position(self, t);
pad_refresh_grid(self, t);
pad_show_text(self, t, "Matriseq");
self->attached = true;
}
default:
break;
}
}
ev = lv2_atom_sequence_next(ev);
}
const double time_s = self->time_frames / self->rate;
const uint32_t step = (uint32_t)(time_s / s_per_step) % STEP_TYPE;
if (step != self->step) {
// Update step
const uint32_t last_step = self->step;
pad_update_step(self, t, self->step, step);
self->step = step;
if (step == 0) {
self->time_frames = 0;
}
// Send note offs for enabled notes last step
for (uint32_t y = 0; y < SEQ_H; ++y) {
if (self->seq[y][last_step]) {
const uint8_t off[] = { 0x80, NOTE_MIN + y, 0x40 };
lv2_atom_forge_frame_time(&self->playback_forge, t);
lv2_atom_forge_atom(&self->playback_forge, 3, self->uris.midi_MidiEvent);
lv2_atom_forge_write(&self->playback_forge, off, 3);
}
}
// Send note ons for enabled notes this step
for (uint32_t y = 0; y < SEQ_H; ++y) {
if (self->seq[y][step]) {
const uint8_t on[] = { 0x90, NOTE_MIN + y, 0x40 };
lv2_atom_forge_frame_time(&self->playback_forge, t);
lv2_atom_forge_atom(&self->playback_forge, 3, self->uris.midi_MidiEvent);
lv2_atom_forge_write(&self->playback_forge, on, 3);
}
}
}
if (self->speed) {
++self->time_frames;
}
}
if (self->refresh) {
pad_refresh_position(self, n_frames - 1);
pad_refresh_grid(self, n_frames - 1);
self->refresh = false;
}
}
static void
cleanup(LV2_Handle instance)
{
free(instance);
}
static const void*
extension_data(const char* uri)
{
return NULL;
}
static const LV2_Descriptor descriptor = {
MATRISEQ_URI,
instantiate,
connect_port,
NULL,
run,
NULL,
cleanup,
extension_data
};
LV2_SYMBOL_EXPORT const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
return (index == 0) ? &descriptor : NULL;
}