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);