/* 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; }