From 804f8c3dd3961180c8975dba651f9a5f64342a3e Mon Sep 17 00:00:00 2001 From: cancel Date: Sat, 25 Jan 2020 01:40:51 +0900 Subject: [PATCH] Add MIDI beat clock output, with conf and menu toggles Also includes MIDI start/stop support. Fixes #40 --- tui_main.c | 195 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 149 insertions(+), 46 deletions(-) diff --git a/tui_main.c b/tui_main.c index 7109fdb..b2ec35f 100644 --- a/tui_main.c +++ b/tui_main.c @@ -898,9 +898,11 @@ typedef struct { 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 @@ static void ged_init(Ged *a, Usz undo_limit, Usz init_bpm, Usz init_seed) { 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_chan_msg(Oosc_dev *oosc_dev, Midi_mode const *midi_mode, } } +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 @@ bool ged_set_osc_udp(Ged *a, char const *dest_addr, char const *dest_port) { 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 clear_and_run_vm(Glyph *restrict gbuf, Mark *restrict mbuf, Usz height, } 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 @@ void ged_do_stuff(Ged *a) { } } #endif - do_play = true; + crossed_deadline = true; break; } if (secs_span - sdiff > ms_to_sec(0.1)) @@ -1299,25 +1332,29 @@ void ged_do_stuff(Ged *a) { 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 @@ staticni void ged_input_cmd(Ged *a, Ged_input_cmd ev) { 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 @@ enum { 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 @@ enum { 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 @@ static void push_main_menu(void) { 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 @@ static void push_osc_output_port_form(char const *initial) { 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,20 +2524,24 @@ staticni void try_send_to_gui_clipboard(Ged const *a, } 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, 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 @@ staticni void tui_load_prefs(Tui *t) { } 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 @@ staticni void tui_save_prefs(Tui *t) { 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 @@ staticni void tui_save_prefs(Tui *t) { 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 @@ staticni Tui_menus_result tui_drive_menus(Tui *t, int key) { 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 @@ staticni Tui_menus_result tui_drive_menus(Tui *t, int key) { 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 @@ int main(int argc, char **argv) { 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);