From 70def122decd4277ac81989ea57fd101ea9a1058 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Fri, 4 Aug 2017 17:19:36 -0400 Subject: new mini, standalone MIDI beatbox/live looper This is for experiments with loop sequencing, MIDI region generation and superclock stuff --- tools/bb/bb.cc | 404 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/bb/bb.h | 79 +++++++++++ tools/bb/gui.cc | 77 +++++++++++ tools/bb/gui.h | 43 ++++++ tools/bb/makefile | 14 ++ 5 files changed, 617 insertions(+) create mode 100644 tools/bb/bb.cc create mode 100644 tools/bb/bb.h create mode 100644 tools/bb/gui.cc create mode 100644 tools/bb/gui.h create mode 100644 tools/bb/makefile diff --git a/tools/bb/bb.cc b/tools/bb/bb.cc new file mode 100644 index 0000000000..fb6a89c81f --- /dev/null +++ b/tools/bb/bb.cc @@ -0,0 +1,404 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "evoral/midi_events.h" + +#include "bb.h" +#include "gui.h" + +using std::cerr; +using std::endl; + + +static bool +second_simultaneous_midi_byte_is_first (uint8_t a, uint8_t b) +{ + bool b_first = false; + + /* two events at identical times. we need to determine + the order in which they should occur. + + the rule is: + + Controller messages + Program Change + Note Off + Note On + Note Pressure + Channel Pressure + Pitch Bend + */ + + if ((a) >= 0xf0 || (b) >= 0xf0 || ((a & 0xf) != (b & 0xf))) { + + /* if either message is not a channel message, or if the channels are + * different, we don't care about the type. + */ + + b_first = true; + + } else { + + switch (b & 0xf0) { + case MIDI_CMD_CONTROL: + b_first = true; + break; + + case MIDI_CMD_PGM_CHANGE: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + break; + case MIDI_CMD_PGM_CHANGE: + case MIDI_CMD_NOTE_OFF: + case MIDI_CMD_NOTE_ON: + case MIDI_CMD_NOTE_PRESSURE: + case MIDI_CMD_CHANNEL_PRESSURE: + case MIDI_CMD_BENDER: + b_first = true; + } + break; + + case MIDI_CMD_NOTE_OFF: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + case MIDI_CMD_PGM_CHANGE: + break; + case MIDI_CMD_NOTE_OFF: + case MIDI_CMD_NOTE_ON: + case MIDI_CMD_NOTE_PRESSURE: + case MIDI_CMD_CHANNEL_PRESSURE: + case MIDI_CMD_BENDER: + b_first = true; + } + break; + + case MIDI_CMD_NOTE_ON: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + case MIDI_CMD_PGM_CHANGE: + case MIDI_CMD_NOTE_OFF: + break; + case MIDI_CMD_NOTE_ON: + case MIDI_CMD_NOTE_PRESSURE: + case MIDI_CMD_CHANNEL_PRESSURE: + case MIDI_CMD_BENDER: + b_first = true; + } + break; + case MIDI_CMD_NOTE_PRESSURE: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + case MIDI_CMD_PGM_CHANGE: + case MIDI_CMD_NOTE_OFF: + case MIDI_CMD_NOTE_ON: + break; + case MIDI_CMD_NOTE_PRESSURE: + case MIDI_CMD_CHANNEL_PRESSURE: + case MIDI_CMD_BENDER: + b_first = true; + } + break; + + case MIDI_CMD_CHANNEL_PRESSURE: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + case MIDI_CMD_PGM_CHANGE: + case MIDI_CMD_NOTE_OFF: + case MIDI_CMD_NOTE_ON: + case MIDI_CMD_NOTE_PRESSURE: + break; + case MIDI_CMD_CHANNEL_PRESSURE: + case MIDI_CMD_BENDER: + b_first = true; + } + break; + case MIDI_CMD_BENDER: + switch (a & 0xf0) { + case MIDI_CMD_CONTROL: + case MIDI_CMD_PGM_CHANGE: + case MIDI_CMD_NOTE_OFF: + case MIDI_CMD_NOTE_ON: + case MIDI_CMD_NOTE_PRESSURE: + case MIDI_CMD_CHANNEL_PRESSURE: + break; + case MIDI_CMD_BENDER: + b_first = true; + } + break; + } + } + + return b_first; +} + +BeatBox::BeatBox (int sr) + : _start_requested (false) + , _running (false) + , _measures (2) + , _tempo (120) + , _meter_beats (4) + , _meter_beat_type (4) + , _input (0) + , _output (0) + , superclock_cnt (0) + , last_start (0) + , _sample_rate (sr) + , whole_note_superclocks (0) + , beat_superclocks (0) + , measure_superclocks (0) + , _quantize_divisor (4) + , clear_pending (false) +{ + for (uint32_t n = 0; n < 1024; ++n) { + event_pool.push_back (new Event()); + } +} + +BeatBox::~BeatBox () +{ +} + +int +BeatBox::register_ports (jack_client_t* jack) +{ + if ((_input = jack_port_register (jack, "midi-in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0)) == 0) { + cerr << "no input port\n"; + return -1; + } + if ((_output = jack_port_register (jack, "midi-out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0)) == 0) { + cerr << "no output port\n"; + jack_port_unregister (jack, _input); + return -1; + } + + return 0; +} + +void +BeatBox::start () +{ + /* compute tempo, beat steps etc. */ + + /* + superclocks_per_minute = superclock_ticks_per_second * 60; + beats_per_minute = _tempo; + whole_notes_per_minute = beats_per_minute / _meter_beat_type; + */ + + whole_note_superclocks = (superclock_ticks_per_second * 60) / (_tempo / _meter_beat_type); + cerr << "there are " << _tempo / _meter_beat_type << " whole notes per minute, which is " << superclock_ticks_per_second * 60 << " sct, so wns = " << whole_note_superclocks << endl; + beat_superclocks = whole_note_superclocks / _meter_beat_type; + measure_superclocks = beat_superclocks * _meter_beats; + + /* we can start */ + + _start_requested = true; +} + +void +BeatBox::stop () +{ + _start_requested = false; +} + +int +BeatBox::process (int nsamples) +{ + if (!_running) { + if (_start_requested) { + _running = true; + last_start = superclock_cnt; + } + + } else { + if (!_start_requested) { + _running = false; + } + } + + superclock_t superclocks = samples_to_superclock (nsamples, _sample_rate); + + if (!_running) { + superclock_cnt += superclocks; + return 0; + } + + superclock_t process_start = superclock_cnt - last_start; + superclock_t process_end = process_start + superclocks; + const superclock_t loop_length = _measures * measure_superclocks; + const superclock_t orig_superclocks = superclocks; + + process_start %= loop_length; + process_end %= loop_length; + + bool two_pass_required; + superclock_t offset = 0; + + if (process_end < process_start) { + two_pass_required = true; + process_end = loop_length; + superclocks = process_end - process_start; + } else { + two_pass_required = false; + } + + unsigned char* buffer; + void* out_buf; + void* in_buf; + jack_midi_event_t in_event; + jack_nframes_t event_index; + jack_nframes_t event_count; + + /* do this on the first pass only */ + out_buf = jack_port_get_buffer (_output, nsamples); + jack_midi_clear_buffer (out_buf); + + second_pass: + + /* Output */ + + if (clear_pending) { + + for (Events::iterator ee = _current_events.begin(); ee != _current_events.end(); ++ee) { + event_pool.push_back (*ee); + } + _current_events.clear (); + clear_pending = false; + } + + for (Events::iterator ee = _current_events.begin(); ee != _current_events.end(); ++ee) { + Event* e = (*ee); + + if (e->size && (e->time >= process_start && e->time < process_end)) { + if ((buffer = jack_midi_event_reserve (out_buf, superclock_to_samples (offset + e->time - process_start, _sample_rate), e->size)) != 0) { + memcpy (buffer, e->buf, e->size); + } else { + cerr << "Could not reserve space for output event @ " << e << " of size " << e->size << " @ " << offset + e->time - process_start + << " (samples: " << superclock_to_samples (offset + e->time - process_start, _sample_rate) << ") offset is " << offset + << ")\n"; + } + } + + if (e->time >= process_end) { + break; + } + } + + /* input */ + + in_buf = jack_port_get_buffer (_input, nsamples); + event_index = 0; + + while (jack_midi_event_get (&in_event, in_buf, event_index++) == 0) { + + superclock_t event_time = superclock_cnt + samples_to_superclock (in_event.time, _sample_rate); + superclock_t elapsed_time = event_time - last_start; + superclock_t in_loop_time = elapsed_time % loop_length; + superclock_t quantized_time; + + if (_quantize_divisor != 0) { + const superclock_t time_per_beat = whole_note_superclocks / _quantize_divisor; + quantized_time = (in_loop_time / time_per_beat) * time_per_beat; + } else { + quantized_time = elapsed_time; + } + + if (in_event.size > 24) { + cerr << "Ignored large MIDI event\n"; + continue; + } + + if (event_pool.empty()) { + cerr << "No more events, grow pool\n"; + continue; + } + + Event* e = event_pool.back(); + event_pool.pop_back (); + + e->time = quantized_time; + e->size = in_event.size; + memcpy (e->buf, in_event.buffer, in_event.size); + + _current_events.insert (e); + } + + superclock_cnt += superclocks; + + if (two_pass_required) { + offset = superclocks; + superclocks = orig_superclocks - superclocks; + process_start = 0; + process_end = superclocks; + two_pass_required = false; + cerr << "2nd Pass for " << superclocks << " offset = " << offset << endl; + goto second_pass; + } + + return 0; +} + +void +BeatBox::set_quantize (int divisor) +{ + _quantize_divisor = divisor; +} + +void +BeatBox::clear () +{ + clear_pending = true; +} + +bool +BeatBox::EventComparator::operator() (Event const * a, Event const *b) const +{ + if (a->time == b->time) { + if (a->buf[0] == b->buf[0]) { + return a < b; + } + return !second_simultaneous_midi_byte_is_first (a->buf[0], b->buf[0]); + } + return a->time < b->time; +} + +static int +process (jack_nframes_t nsamples, void* arg) +{ + BeatBox* bbox = static_cast (arg); + return bbox->process (nsamples); +} + +int +main (int argc, char* argv[]) +{ + jack_client_t* jack; + const char *server_name = NULL; + jack_options_t options = JackNullOption; + jack_status_t status; + + if ((jack = jack_client_open ("beatbox", options, &status, server_name)) == 0) { + cerr << "Could not connect to JACK\n"; + return -1; + } + + BeatBox* bbox = new BeatBox (jack_get_sample_rate (jack)); + BBGUI* gui = new BBGUI (&argc, &argv, jack, bbox); + + bbox->register_ports (jack); + + jack_set_process_callback (jack, process, bbox); + jack_activate (jack); + + bbox->start (); + + gui->run (); +} diff --git a/tools/bb/bb.h b/tools/bb/bb.h new file mode 100644 index 0000000000..e9d89ddf0b --- /dev/null +++ b/tools/bb/bb.h @@ -0,0 +1,79 @@ +#ifndef __bb_h__ +#define __bb_h__ + +#include +#include +#include +#include + +#include + +#include + +typedef uint64_t superclock_t; + +static const superclock_t superclock_ticks_per_second = 1235025792000; // 2^10 * 3^4 * 5^3 * 7^2 * 11 * 13 * 17 +inline superclock_t superclock_to_samples (superclock_t s, int sr) { return (s * sr) / superclock_ticks_per_second; } +inline superclock_t samples_to_superclock (int samples, int sr) { return (samples * superclock_ticks_per_second) / sr; } + +class BeatBox { + public: + BeatBox (int sample_rate); + ~BeatBox (); + + int register_ports (jack_client_t*); + int process (int nframes); + + bool running() const { return _running || _start_requested; } + void start (); + void stop (); + void clear (); + + void set_measure_count (int measures); + void set_meter (int beats, int beat_type); + void set_tempo (float bpm); + + void set_quantize (int divisor); + + private: + bool _start_requested; + bool _running; + int _measures; + float _tempo; + int _meter_beats; + int _meter_beat_type; + jack_port_t* _input; + jack_port_t* _output; + superclock_t superclock_cnt; + superclock_t last_start; + + int _sample_rate; + superclock_t whole_note_superclocks; + superclock_t beat_superclocks; + superclock_t measure_superclocks; + int _quantize_divisor; + bool clear_pending; + + struct Event { + superclock_t time; + size_t size; + unsigned char buf[24]; + + Event () : time (0), size (0) {} + Event (jack_nframes_t t, size_t sz, unsigned char* b) : time (t), size (sz) { memcpy (buf, b, std::min (sizeof (buf), sz)); } + Event (Event const & other) : time (other.time), size (other.size) { memcpy (buf, other.buf, other.size); } + }; + + struct EventComparator { + bool operator () (Event const * a, Event const * b) const; + }; + + typedef std::set Events; + Events _current_events; + + typedef std::vector EventPool; + EventPool event_pool; +}; + + +#endif /* __bb_h__ */ diff --git a/tools/bb/gui.cc b/tools/bb/gui.cc new file mode 100644 index 0000000000..cdb34eb124 --- /dev/null +++ b/tools/bb/gui.cc @@ -0,0 +1,77 @@ +#include "bb.h" +#include "gui.h" + +BBGUI::BBGUI (int* argc, char** argv[], jack_client_t* j, BeatBox* bb) + : jack (j) + , bbox (bb) + , main (argc, argv) + , quantize_off (quantize_group, "None") + , quantize_32nd (quantize_group, "ThirtySecond") + , quantize_16th (quantize_group, "Sixteenth") + , quantize_8th (quantize_group, "Eighth") + , quantize_quarter (quantize_group, "Quarter") + , quantize_half (quantize_group, "Half") + , quantize_whole (quantize_group, "Whole") + , play_button ("Run") + , clear_button ("Clear") +{ + quantize_off.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 0)); + quantize_32nd.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 32)); + quantize_16th.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 16)); + quantize_8th.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 8)); + quantize_quarter.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 4)); + quantize_half.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 2)); + quantize_whole.signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &BBGUI::set_quantize), 1)); + + quantize_button_box.pack_start (quantize_off); + quantize_button_box.pack_start (quantize_32nd); + quantize_button_box.pack_start (quantize_16th); + quantize_button_box.pack_start (quantize_8th); + quantize_button_box.pack_start (quantize_quarter); + quantize_button_box.pack_start (quantize_half); + quantize_button_box.pack_start (quantize_whole); + + play_button.signal_toggled().connect (sigc::mem_fun (*this, &BBGUI::toggle_play)); + clear_button.signal_clicked().connect (sigc::mem_fun (*this, &BBGUI::clear)); + + misc_button_box.pack_start (play_button); + misc_button_box.pack_start (clear_button); + + global_vbox.pack_start (misc_button_box); + global_vbox.pack_start (quantize_button_box, true, true); + window.add (global_vbox); + window.show_all (); +} + +BBGUI::~BBGUI () +{ +} + +void +BBGUI::run () +{ + window.show (); + main.run (); +} + +void +BBGUI::set_quantize (int divisor) +{ + bbox->set_quantize (divisor); +} + +void +BBGUI::clear () +{ + bbox->clear (); +} + +void +BBGUI::toggle_play () +{ + if (bbox->running()) { + bbox->stop (); + } else { + bbox->start (); + } +} diff --git a/tools/bb/gui.h b/tools/bb/gui.h new file mode 100644 index 0000000000..d3d9dc5e00 --- /dev/null +++ b/tools/bb/gui.h @@ -0,0 +1,43 @@ +#ifndef __bb_gui_h__ +#define __bb_gui_h__ + +#include +#include + +class BeatBox; + +class BBGUI { + public: + BBGUI (int*, char** [], jack_client_t* jack, BeatBox* bb); + ~BBGUI (); + + void run (); + + private: + jack_client_t* jack; + BeatBox* bbox; + Gtk::Main main; + Gtk::Window window; + + Gtk::RadioButtonGroup quantize_group; + Gtk::RadioButton quantize_off; + Gtk::RadioButton quantize_32nd; + Gtk::RadioButton quantize_16th; + Gtk::RadioButton quantize_8th; + Gtk::RadioButton quantize_quarter; + Gtk::RadioButton quantize_half; + Gtk::RadioButton quantize_whole; + + Gtk::ToggleButton play_button; + Gtk::Button clear_button; + + Gtk::VBox global_vbox; + Gtk::VBox quantize_button_box; + Gtk::HBox misc_button_box; + + void set_quantize (int divisor); + void toggle_play (); + void clear (); +}; + +#endif /* __bb_gui_h__ */ diff --git a/tools/bb/makefile b/tools/bb/makefile new file mode 100644 index 0000000000..4e89022da3 --- /dev/null +++ b/tools/bb/makefile @@ -0,0 +1,14 @@ +all: bb + +CXXFLAGS=-I$(AD)/5.0/libs/evoral `pkg-config --cflags jack` `pkg-config --cflags gtkmm-2.4` +LDFLAGS=`pkg-config --libs jack` `pkg-config --libs gtkmm-2.4` + +bb: bb.o gui.o + $(CXX) -o bb bb.o gui.o $(LDFLAGS) + + +bb.o: bb.cc bb.h gui.h +gui.o: gui.cc gui.h bb.h + +clean: + rm -f bb bb.o gui.o -- cgit v1.2.3