shithub: orca

Download patch

ref: 804f8c3dd3961180c8975dba651f9a5f64342a3e
parent: 735491fe026b57af7f3ade3c1e2144c68308b3db
author: cancel <cancel@cancel.fm>
date: Fri Jan 24 20:40:51 EST 2020

Add MIDI beat clock output, with conf and menu toggles

Also includes MIDI start/stop support.

Fixes #40

--- a/tui_main.c
+++ b/tui_main.c
@@ -898,9 +898,11 @@
   int softmargin_y, softmargin_x;
   int grid_h;
   int grid_scroll_y, grid_scroll_x; // not sure if i like this being int
+  U8 midi_bclock_sixths;
   bool needs_remarking : 1;
   bool is_draw_dirty : 1;
   bool is_playing : 1;
+  bool midi_bclock : 1;
   bool draw_event_list : 1;
   bool is_mouse_down : 1;
   bool is_mouse_dragging : 1;
@@ -933,9 +935,11 @@
   a->softmargin_y = a->softmargin_x = 0;
   a->grid_h = 0;
   a->grid_scroll_y = a->grid_scroll_x = 0;
+  a->midi_bclock_sixths = 0;
   a->needs_remarking = true;
   a->is_draw_dirty = false;
-  a->is_playing = true;
+  a->is_playing = false;
+  a->midi_bclock = true;
   a->draw_event_list = false;
   a->is_mouse_down = false;
   a->is_mouse_dragging = false;
@@ -1001,6 +1005,31 @@
   }
 }
 
+staticni void send_midi_byte(Oosc_dev *oosc_dev, Midi_mode const *midi_mode,
+                             int x) {
+#ifdef FEAT_PORTMIDI
+  PmTimestamp pm_timestamp = portmidi_timestamp_now();
+#endif
+  switch (midi_mode->any.type) {
+  case Midi_mode_type_null:
+    break;
+  case Midi_mode_type_osc_bidule: {
+    if (!oosc_dev)
+      break;
+    oosc_send_int32s(oosc_dev, midi_mode->osc_bidule.path, (int[]){x, 0, 0}, 3);
+    break;
+  }
+#ifdef FEAT_PORTMIDI
+  case Midi_mode_type_portmidi: {
+    PmError pme = Pm_WriteShort(midi_mode->portmidi.stream, pm_timestamp,
+                                Pm_Message(x, 0, 0));
+    (void)pme;
+    break;
+  }
+#endif
+  }
+}
+
 staticni void //
 send_midi_note_offs(Oosc_dev *oosc_dev, Midi_mode const *midi_mode,
                     Susnote const *start, Susnote const *end) {
@@ -1230,19 +1259,23 @@
 
 static double ms_to_sec(double ms) { return ms / 1000.0; }
 
-double ged_secs_to_deadline(Ged const *a) {
-  if (a->is_playing) {
-    double secs_span = 60.0 / (double)a->bpm / 4.0;
-    double rem = secs_span - (stm_sec(stm_since(a->clock)) + a->accum_secs);
-    double next_note_off = a->time_to_next_note_off;
-    if (rem < 0.0)
-      rem = 0.0;
-    else if (next_note_off < rem)
-      rem = next_note_off;
-    return rem;
-  } else {
+static double ged_secs_to_deadline(Ged const *a) {
+  if (!a->is_playing)
     return 1.0;
-  }
+  double secs_span = 60.0 / (double)a->bpm / 4.0;
+  // If MIDI beat clock output is enabled, we need to send an event every 24
+  // parts per quarter note. Since we've already divided quarter notes into 4
+  // for ORCA's timing semantics, divide it by a further 6. This same logic is
+  // mirrored in ged_do_stuff().
+  if (a->midi_bclock)
+    secs_span /= 6.0;
+  double rem = secs_span - (stm_sec(stm_since(a->clock)) + a->accum_secs);
+  double next_note_off = a->time_to_next_note_off;
+  if (next_note_off < rem)
+    rem = next_note_off;
+  if (rem < 0.0)
+    rem = 0.0;
+  return rem;
 }
 
 void ged_reset_clock(Ged *a) { a->clock = stm_now(); }
@@ -1256,14 +1289,14 @@
 }
 
 void ged_do_stuff(Ged *a) {
+  if (!a->is_playing)
+    return;
   double secs_span = 60.0 / (double)a->bpm / 4.0;
+  if (a->midi_bclock) // see also ged_secs_to_deadline()
+    secs_span /= 6.0;
   Oosc_dev *oosc_dev = a->oosc_dev;
   Midi_mode const *midi_mode = a->midi_mode;
-  double secs = stm_sec(stm_since(a->clock));
-  (void)secs; // unused, was previously used for activity meter decay
-  if (!a->is_playing)
-    return;
-  bool do_play = false;
+  bool crossed_deadline = false;
 #if TIME_DEBUG
   Usz spins = 0;
   U64 spin_start = stm_now();
@@ -1284,7 +1317,7 @@
         }
       }
 #endif
-      do_play = true;
+      crossed_deadline = true;
       break;
     }
     if (secs_span - sdiff > ms_to_sec(0.1))
@@ -1299,25 +1332,29 @@
             stm_us(stm_since(spin_start)), spin_track_timeout);
   }
 #endif
-  if (do_play) {
-    apply_time_to_sustained_notes(oosc_dev, midi_mode, secs_span,
-                                  &a->susnote_list, &a->time_to_next_note_off);
-    clear_and_run_vm(a->field.buffer, a->mbuf_r.buffer, a->field.height,
-                     a->field.width, a->tick_num, &a->oevent_list,
-                     a->random_seed);
-    ++a->tick_num;
-    a->needs_remarking = true;
-    a->is_draw_dirty = true;
+  if (!crossed_deadline)
+    return;
+  if (a->midi_bclock) {
+    send_midi_byte(oosc_dev, midi_mode, 0xF8); // MIDI beat clock
+    Usz sixths = a->midi_bclock_sixths;
+    a->midi_bclock_sixths = (U8)((sixths + 1) % 6);
+    if (sixths != 0)
+      return;
+  }
+  apply_time_to_sustained_notes(oosc_dev, midi_mode, secs_span,
+                                &a->susnote_list, &a->time_to_next_note_off);
+  clear_and_run_vm(a->field.buffer, a->mbuf_r.buffer, a->field.height,
+                   a->field.width, a->tick_num, &a->oevent_list,
+                   a->random_seed);
+  ++a->tick_num;
+  a->needs_remarking = true;
+  a->is_draw_dirty = true;
 
-    Usz count = a->oevent_list.count;
-    if (count > 0) {
-      send_output_events(oosc_dev, midi_mode, a->bpm, &a->susnote_list,
-                         a->oevent_list.buffer, count);
-      a->activity_counter += count;
-    }
-    // note for future: sustained note deadlines may have changed due to note
-    // on. will need to update stored deadline in memory if
-    // ged_apply_delta_secs isn't called again immediately after ged_do_stuff.
+  Usz count = a->oevent_list.count;
+  if (count > 0) {
+    send_output_events(oosc_dev, midi_mode, a->bpm, &a->susnote_list,
+                       a->oevent_list.buffer, count);
+    a->activity_counter += count;
   }
 }
 
@@ -1856,12 +1893,20 @@
       ged_stop_all_sustained_notes(a);
       a->is_playing = false;
       send_control_message(a->oosc_dev, "/orca/stopped");
+      if (a->midi_bclock)
+        send_midi_byte(a->oosc_dev, a->midi_mode, 0xFC); // "stop"
     } else {
       undo_history_push(&a->undo_hist, &a->field, a->tick_num);
       a->is_playing = true;
       a->clock = stm_now();
+      a->midi_bclock_sixths = 0;
       // dumb'n'dirty, get us close to the next step time, but not quite
-      a->accum_secs = 60.0 / (double)a->bpm / 4.0 - 0.02;
+      a->accum_secs = 60.0 / (double)a->bpm / 4.0;
+      if (a->midi_bclock) {
+        send_midi_byte(a->oosc_dev, a->midi_mode, 0xFA); // "start"
+        a->accum_secs /= 6.0;
+      }
+      a->accum_secs -= 0.0001;
       send_control_message(a->oosc_dev, "/orca/started");
     }
     a->is_draw_dirty = true;
@@ -1952,6 +1997,7 @@
   Osc_menu_id,
   Osc_output_address_form_id,
   Osc_output_port_form_id,
+  Playback_menu_id,
   Set_soft_margins_form_id,
   Set_fancy_grid_dots_menu_id,
   Set_fancy_grid_rulers_menu_id,
@@ -1983,6 +2029,7 @@
   Main_menu_autofit_grid,
   Main_menu_about,
   Main_menu_cosmetics,
+  Main_menu_playback,
   Main_menu_osc,
 #ifdef FEAT_PORTMIDI
   Main_menu_choose_portmidi_output,
@@ -2006,7 +2053,9 @@
   qmenu_add_choice(qm, Main_menu_choose_portmidi_output, "MIDI Output...");
 #endif
   qmenu_add_spacer(qm);
+  qmenu_add_choice(qm, Main_menu_playback, "Clock & Timing...");
   qmenu_add_choice(qm, Main_menu_cosmetics, "Appearance...");
+  qmenu_add_spacer(qm);
   qmenu_add_choice(qm, Main_menu_controls, "Controls...");
   qmenu_add_choice(qm, Main_menu_opers_guide, "Operators...");
   qmenu_add_choice(qm, Main_menu_about, "About ORCA...");
@@ -2096,6 +2145,16 @@
   qform_add_text_line(qf, Single_form_item_id, initial);
   qform_push_to_nav(qf);
 }
+enum {
+  Playback_menu_midi_bclock = 1,
+};
+static void push_playback_menu(bool midi_bclock_enabled) {
+  Qmenu *qm = qmenu_create(Playback_menu_id);
+  qmenu_set_title(qm, "Clock & Timing");
+  qmenu_add_printf(qm, Playback_menu_midi_bclock, "[%c] Send MIDI Beat Clock",
+                   midi_bclock_enabled ? '*' : ' ');
+  qmenu_push_to_nav(qm);
+}
 static void push_about_msg(void) {
   // clang-format off
   static char const* logo[] = {
@@ -2465,14 +2524,17 @@
 }
 
 char const *const confopts[] = {
-    "portmidi_output_device",
-    "osc_output_address",
-    "osc_output_port",
-    "osc_output_enabled",
-    "margins",
-    "grid_dot_type",
-    "grid_ruler_type",
-};
+    // clang-format off
+  "portmidi_output_device",
+  "osc_output_address",
+  "osc_output_port",
+  "osc_output_enabled",
+  "midi_beat_clock",
+  "margins",
+  "grid_dot_type",
+  "grid_ruler_type",
+}; // clang-format on
+
 enum { Confoptslen = ORCA_ARRAY_COUNTOF(confopts) };
 enum {
   Confopt_portmidi_output_device = 0,
@@ -2479,6 +2541,7 @@
   Confopt_osc_output_address,
   Confopt_osc_output_port,
   Confopt_osc_output_enabled,
+  Confopt_midi_beat_clock,
   Confopt_margins,
   Confopt_grid_dot_type,
   Confopt_grid_ruler_type,
@@ -2584,6 +2647,14 @@
       }
       break;
     }
+    case Confopt_midi_beat_clock: {
+      bool enabled;
+      if (conf_read_boolish(ez.value, &enabled)) {
+        t->ged.midi_bclock = true;
+        touched |= TOUCHFLAG(Confopt_midi_beat_clock);
+      }
+      break;
+    }
     case Confopt_margins: {
       int softmargin_y, softmargin_x;
       if (read_nxn_or_n(ez.value, &softmargin_x, &softmargin_y) &&
@@ -2688,6 +2759,9 @@
   if (t->prefs_touched & TOUCHFLAG(Confopt_osc_output_enabled))
     ezconf_w_addopt(&ez, confopts[Confopt_osc_output_enabled],
                     Confopt_osc_output_enabled);
+  if (t->prefs_touched & TOUCHFLAG(Confopt_midi_beat_clock))
+    ezconf_w_addopt(&ez, confopts[Confopt_midi_beat_clock],
+                    Confopt_midi_beat_clock);
   if (t->prefs_touched & TOUCHFLAG(Confopt_margins))
     ezconf_w_addopt(&ez, confopts[Confopt_margins], Confopt_margins);
   if (t->prefs_touched & TOUCHFLAG(Confopt_grid_dot_type))
@@ -2715,6 +2789,9 @@
       fputs(osoc(midi_output_device_name), ez.file);
       break;
 #endif
+    case Confopt_midi_beat_clock:
+      fputc(t->ged.midi_bclock ? '1' : '0', ez.file);
+      break;
     case Confopt_margins:
       fprintf(ez.file, "%dx%d", t->softmargin_x, t->softmargin_y);
       break;
@@ -2902,6 +2979,9 @@
         switch (act.picked.id) {
         case Main_menu_quit:
           return Tui_menus_quit;
+        case Main_menu_playback:
+          push_playback_menu(t->ged.midi_bclock);
+          break;
         case Main_menu_cosmetics:
           push_cosmetics_menu();
           break;
@@ -3018,6 +3098,28 @@
           break;
         }
         break;
+      case Playback_menu_id:
+        switch (act.picked.id) {
+        case Playback_menu_midi_bclock: {
+          bool new_enabled = !t->ged.midi_bclock;
+          t->ged.midi_bclock = new_enabled;
+          if (t->ged.is_playing) {
+            int msgbyte = new_enabled ? 0xFA /* start */ : 0xFC /* stop */;
+            send_midi_byte(t->ged.oosc_dev, t->ged.midi_mode, msgbyte);
+            // TODO timing judder will be experienced here, because the
+            // deadline calculation conditions will have been changed by
+            // toggling the midi_bclock flag. We would have to transfer the
+            // current remaining time from the reference clock point into the
+            // accum time, and mutiply or divide it.
+          }
+          t->prefs_touched |= TOUCHFLAG(Confopt_midi_beat_clock);
+          tui_save_prefs(t);
+          qnav_stack_pop();
+          push_playback_menu(new_enabled);
+          break;
+        }
+        }
+        break;
       case Set_fancy_grid_dots_menu_id:
         plainorfancy_menu_was_picked(t, act.picked.id, &t->fancy_grid_dots,
                                      TOUCHFLAG(Confopt_grid_dot_type));
@@ -3490,6 +3592,7 @@
   ged_make_cursor_visible(&t.ged);
   // Send initial BPM
   send_num_message(t.ged.oosc_dev, "/orca/bpm", (I32)t.ged.bpm);
+  ungetch(' '); // queue up auto-play. cheesy.
   // Enter main loop. Process events as they arrive.
 event_loop:;
   int key = wgetch(stdscr);