new mini, standalone MIDI beatbox/live looper
authorPaul Davis <paul@linuxaudiosystems.com>
Fri, 4 Aug 2017 21:19:36 +0000 (17:19 -0400)
committerPaul Davis <paul@linuxaudiosystems.com>
Fri, 4 Aug 2017 21:19:46 +0000 (17:19 -0400)
This is for experiments with loop sequencing, MIDI region generation and superclock stuff

tools/bb/bb.cc [new file with mode: 0644]
tools/bb/bb.h [new file with mode: 0644]
tools/bb/gui.cc [new file with mode: 0644]
tools/bb/gui.h [new file with mode: 0644]
tools/bb/makefile [new file with mode: 0644]

diff --git a/tools/bb/bb.cc b/tools/bb/bb.cc
new file mode 100644 (file)
index 0000000..fb6a89c
--- /dev/null
@@ -0,0 +1,404 @@
+#include <iostream>
+#include <cstdio>
+#include <cmath>
+#include <cstring>
+
+#include <unistd.h>
+#include <stdint.h>
+
+#include <jack/jack.h>
+#include <jack/midiport.h>
+
+#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<BeatBox*> (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 (file)
index 0000000..e9d89dd
--- /dev/null
@@ -0,0 +1,79 @@
+#ifndef __bb_h__
+#define __bb_h__
+
+#include <algorithm>
+#include <vector>
+#include <set>
+#include <cstring>
+
+#include <stdint.h>
+
+#include <jack/jack.h>
+
+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<Event*,EventComparator> Events;
+       Events _current_events;
+
+       typedef std::vector<Event*> EventPool;
+       EventPool  event_pool;
+};
+
+
+#endif /* __bb_h__ */
diff --git a/tools/bb/gui.cc b/tools/bb/gui.cc
new file mode 100644 (file)
index 0000000..cdb34eb
--- /dev/null
@@ -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 (file)
index 0000000..d3d9dc5
--- /dev/null
@@ -0,0 +1,43 @@
+#ifndef __bb_gui_h__
+#define __bb_gui_h__
+
+#include <gtkmm.h>
+#include <jack/jack.h>
+
+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 (file)
index 0000000..4e89022
--- /dev/null
@@ -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