From ec896f9f8a000506ca0d2df9c475290eb861546e Mon Sep 17 00:00:00 2001 From: Richard Gruet Date: Sun, 5 Nov 2023 18:51:10 +0100 Subject: [PATCH] Release Reduce interactively the number of events in Midi CC lanes v1.0.1 Doc changes --- MIDI Editor/rig-trimCCs.eel | 532 ++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 MIDI Editor/rig-trimCCs.eel diff --git a/MIDI Editor/rig-trimCCs.eel b/MIDI Editor/rig-trimCCs.eel new file mode 100644 index 000000000..050690247 --- /dev/null +++ b/MIDI Editor/rig-trimCCs.eel @@ -0,0 +1,532 @@ +// @description Reduce interactively the number of events in Midi CC lanes +// @author rig +// @version 1.0.1 +// @changelog Doc changes +// @provides [main=main] . +// @about +// # rig-trimCCs +// +// Reduce iteratively the number of CC events (points) in the selected MIDI editor lane. +// +// Only CC lanes are supported (e.g. velocity or Channel pressure/aftertouch won't work). +// +// ## Usage +// +// - Define a MIDI Editor *action* for the script (actions > New action... > Load ReaScript...). +// For convenience you can associate it to a shortcut or a button in a toolbar. +// - **Run the action** from within Reaper's Midi Editor -> the **script window** is displayed. +// - **Select a CC lane** by clicking on it. The script window shows the number of events (points) present and +// how many points can be trimmed in this iteration. If a **selection** of points exists, the script will +// only process them (in case of multiple disjoint selection ranges, only the first selection range will be processed). +// - **Click on the "Trim events" button** and see directly the effect on the curve (be patient if the number of points +// is large). The script windows displays the new number of events after the trim and the number of possible +// trims on the next iteration. +// +// **TIP**: if you press the SHIFT key while clicking on the Trim button (recommended), the SHAPE of all remaining CC +// points after the trim will be set to BEZIER (which will smooth the curve). Likewise, pressing CTRL +// while clicking on the Trim button will set the shape of the remaining points to SLOW START/END. +// - Keep clicking until you are satisfied or no further trimming is possible ("Trimmable events: 0"). +// - Use Ctrl (or Cmd)-Z to **undo** the last change. Do **not** directly use the Undo/Redo options in Reaper's Edit menu. +// - Exit the program by closing the script window, or by hitting ESC. +// +// ## How it works +// +// This script simplifies the CC event curve using the +// ([ Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)). +// +// The amount of points (CC events) removed ("trimmed") depends on the value of epsilon, the distance dimension (>0). +// In order to trim progressively the curve, the script starts with a low value of epsilon (EPSILON_INIT). +// After each trimming it increases the value of epsilon by EPSILON_INCR, which potentially adds new candidate points +// for trimming. Conversely when UNDOing operations (Ctrl/Cmd+Z), the previous value of epsilon is restored. +// +// ### Notes +// +// - This script can process a maximum of POINTS_SIZE (200000 by default) CC events. Extra events are ignored. +// - It attaches a "pin on top" button to the script window if Reaper 6.24+ and js_reascriptAPI extension installed. +// - To smooth the effects of trimming points I suggest you assign "CC curve shape = bezier (or slow start/end )" +// to your selection (or press SHIFT when clicking on the "Trim events" button, see above). +// - Based loosely on spk77's script: spk77_Remove_redundant_CCs.eel, itself adapted from JSFX script by DarkStar. +// - See also the [thin MIDI CC Events](https://forums.cockos.com/showthread.php?t=272820) lua scripts by sockmonkey72 +// which offers a series of individual actions rather than an interactive process. +// - Licence: GPL v3 + +SCRIPT_NAME = "Trim CCs"; +true = 1; false = 0; + +///// USER TWEEKABLE SETTINGS: //////////////////////////////////////////////////////////////////////////////////// +EPSILON_INIT = 0.02; // Initial value of epsilon +EPSILON_INCR = 0.03; // added to epsilon after each trim. Substracted on Ctrl/Cmd-Z (Undo). +////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +epsilon = EPSILON_INIT; // distance dimension (>0) for Ramer-Douglas-Peucker algorithm +last_unselected_epsilon = epsilon; // epsilon value for the last selected set of points. +points_checksum = +last_after_trim_checksum = -1; // (undefined) +has_selection = false; // Whether there are points currently selected in the target CC lane +undo_cnt = 0; + +CC_SHAPE_LINEAR = 1; // Reaper API CC shape values +CC_SHAPE_SLOW_START_END = 2; +CC_SHAPE_FAST_START = 3; +CC_SHAPE_FAST_END = 4; +CC_SHAPE_BEZIER = 5; + +take; // current take + +points = 0; // Table of points (CC events), starting at offset +0 in memory. +POINT_SIZE = 4; // Each point has 4 fields: +OFF_INDEX = +0; // Index in Reaper's CC events +OFF_VALUE = Y_OFFSET = +1; // CC value (0-127) - Used as point Y coordinate +OFF_TIME = X_OFFSET = +2; // Time (either PPQ or project time) - Used as point X coordinate +OFF_KEPT = +3; // true if the point must be kept +POINTS_SIZE = 200000; // table dimension in points + +point_cnt = 0; // current length of table points + +// Table points helpers (thanks EEL2 ;-): + +function clear_points() ( + point_cnt = 0; +); + +function append_point(index, value, time, kept) local(p) ( + + point_cnt < POINTS_SIZE ? ( + p = point_cnt * POINT_SIZE; + p[OFF_INDEX] = index; + p[OFF_VALUE] = value; + p[OFF_TIME] = time; + p[OFF_KEPT] = kept; + point_cnt += 1; + true; + ) : ( + false; + ); +); + +function set_point_kept(i, kept) ( + points[i*POINT_SIZE + OFF_KEPT] = kept; +); + +// Our stack for emulation of recursive calls (since EEL2 doesn't support them): + +stack = points + (POINTS_SIZE * POINT_SIZE); // start address in memory +push_cnt = 0; + +function push(v) ( + stack[push_cnt] = v; + push_cnt += 1; +); + +function pop() ( + push_cnt > 0 ? ( + push_cnt -= 1; + stack[push_cnt]; + ) : 0; +); + + +function set_default_colors() +( + gfx_r = 0.5; + gfx_g = 0.8; + gfx_b = 0.5; +); + + +// Copies CC events for the target lane (either all events or only selected ones if there is a selection) +// to our table points[]. All events/points are marked as "not kept" by default. +// The table points[] will be empty if there is no MIDI editor window active or no active lane. +// On return: +// has_selection: true if there is a selection. +// points_checksum: checksum of all points to take into account (regardless of has_selection) + +function copy_ccs_to_points() local(first_selected_idx, stop, cc_idx, selectedOut, mutedOut, startppqpos, + chanmsgOut, chanOut, msg2Out, event_value, midiEditor, selection_checksum, + project_time, shape, bezier_tension) ( + + clear_points(); + has_selection = false; + points_checksum = 0; + + midiEditor = MIDIEditor_GetActive(); + (take = MIDIEditor_GetTake(midiEditor)) ? ( + + last_clicked_cc_lane = MIDIEditor_GetSetting_int(midiEditor, "last_clicked_cc_lane"); + MIDIEditor_GetSetting_str(midiEditor, "last_clicked_cc_lane", #lane_name) == 0 ? #lane_name = ""; + + last_clicked_cc_lane >= 0 && last_clicked_cc_lane <= 127 ? ( + + // if a selection exists, trim it only (and start looking up from 1st selected): + first_selected_idx = MIDI_EnumSelCC(take, -1); + has_selection = (first_selected_idx != -1); + cc_idx = (has_selection ? first_selected_idx : 0); + stop = false; + + while (!stop && MIDI_GetCC(take, cc_idx, selectedOut, mutedOut, startppqpos, chanmsgOut, chanOut, msg2Out, event_value)) ( + + msg2Out == last_clicked_cc_lane ? ( // target CC + (has_selection && !selectedOut) ? ( // end of first selection area + stop = true; + ) : ( + project_time = MIDI_GetProjTimeFromPPQPos(take, startppqpos); + append_point(cc_idx, event_value, project_time, false); + points_checksum += (project_time + event_value); // Dumb checksum calculation! + ); + ); + cc_idx += 1; + ); + + (!has_selection || points_checksum != last_after_trim_checksum) ? + epsilon = last_unselected_epsilon; + ); + ); +); + + +// Returns the distance from point p to the line between p1 and p2. +// p, p1, p2 are (start) indexes in points[]. +function perpendicular_distance(p, p1, p2) local(dx, dy, d) ( + + dx = p2[X_OFFSET] - p1[X_OFFSET]; + dy = p2[Y_OFFSET] - p1[Y_OFFSET]; + d = (p[X_OFFSET] * dy) - (p[Y_OFFSET] * dx) + (p2[X_OFFSET] * p1[Y_OFFSET]) - (p2[Y_OFFSET] * p1[X_OFFSET]); + abs(d) / sqrt(dx*dx + dy*dy); +); + + +// Simplifies the target CC lane event curve using Ramer-Douglas-Peucker algorithm. +// On return CCs are in points[] (point_cnt x points), with those to be kept having their 'kept' flag true. + +function ramer_douglas_peucker() local(max_dist, idx_max_dist, i, dist, begin, end) ( + + // Copy target CC events from reaper to points[]. + copy_ccs_to_points(); + + // Process points: + + point_cnt > 0 ? set_point_kept(0, true); // 1st point always kept + point_cnt >= 2 ? ( + + set_point_kept(point_cnt - 1, true); //last point always kept + + point_cnt > 2 && epsilon > 0 ? ( + + // <=> ramer_douglas_peucker(0, point_cnt - 1); + push(0); + push(point_cnt - 1); + + // One iteration is equivalent to one recursive call to ramer_douglas_peucker(): + + while (push_cnt > 0) ( + + end = pop(); + begin = pop(); + + // Finds the point with max perpendicular distance to a line [begin..end]: + + max_dist = 0; + idx_max_dist = -1; + i = begin + 1; + while(i < end) ( + dist = perpendicular_distance(points + (i * POINT_SIZE), points + (begin * POINT_SIZE), points + (end * POINT_SIZE)); + dist > max_dist ? ( + max_dist = dist; + idx_max_dist = i; + ); + i += 1; + ); + + // If the max distance is over epsilon, simplify the two half segments: + max_dist > epsilon ? ( + + set_point_kept(idx_max_dist, true); + + (end - idx_max_dist) >= 2 ? ( + // <=> ramer_douglas_peucker(idx_max_dist, end); + push(idx_max_dist); + push(end); + ); + + (idx_max_dist - begin) >= 2 ? ( + // <=> ramer_douglas_peucker(begin, idx_max_dist); + push(begin); + push(idx_max_dist); + ); + ) ; + // Otherwise (max_dist <= epsilon) all examined points can potentially be + // erased unless marked as to be kept otherwise + + ); // while + ); + ); +); + + +function push_state() ( + + stack_push(epsilon); + stack_push(last_unselected_epsilon); + stack_push(last_after_trim_checksum); + +); + +function pop_state() ( + + last_after_trim_checksum = stack_pop(); + last_unselected_epsilon = stack_pop(); + epsilon = stack_pop(); +); + + +// Trims events (if do_trim >0), or just counts trimmable events and updates display. +// If do_trim == 3 or 5 also sets the shape of remaining points to Bezier or Slow start/end. +// Returns the number of events trimmed. + +function trim(do_trim) local(i, p, new_checksum, trimmed_cnt, shape, new_shape, bezier_tension) ( + + // Get a list of CC events in points[], with those to keep specially marked: + + ramer_douglas_peucker(); + + // Count and delete (if requested) the trimmable events: + + trimmed_cnt = 0; + + do_trim ? ( + // Save current state for future UNDO: + push_state(); + MIDI_DisableSort(take); // speed up deletions! + ); + + trimmable_event_cnt = new_checksum = 0; + i = point_cnt - 1; // proceed backwards because deletion affects reaper indexes + while (i >= 0) ( + p = points + (i * POINT_SIZE); // ptr + !p[OFF_KEPT] ? ( + trimmable_event_cnt += 1; + do_trim ? ( + MIDI_DeleteCC(take, p[OFF_INDEX]); + trimmed_cnt += 1; + ); + ) : ( + new_checksum += (p[OFF_TIME] + p[OFF_VALUE]); // Dumb checksum calculation! + ); + + // Set the shape of kept point if requested: + do_trim > 1 && p[OFF_KEPT] && MIDI_GetCCShape(take, p[OFF_INDEX], shape, bezier_tension) ? ( + new_shape = (do_trim == 3 ? CC_SHAPE_BEZIER : CC_SHAPE_SLOW_START_END); // +SHIFT -> Bezier; +CTRL -> Slow start/end + new_shape != shape ? + MIDI_SetCCShape(take, p[OFF_INDEX], new_shape, bezier_tension, true); + ); + + i -= 1; + ); + + do_trim ? ( + MIDI_Sort(take); + + // Increment epsilon so to trim more points (if possible) next time: + (has_selection ? epsilon : last_unselected_epsilon) += EPSILON_INCR; + + // Calculate new checksum after trimming: + last_after_trim_checksum = new_checksum; + ) : ( + // [No trim] Just update the UI: + + gfx_x = draw_start_x; + gfx_y = draw_start_y + gfx_texth; + set_default_colors(); + gfx_a = 1; + gfx_drawstr("Events: "); + gfx_r = 0.8; + gfx_b = 1; + gfx_g = 0.8; + gfx_printf("%d", point_cnt); + set_default_colors(); + gfx_y += gfx_texth; + gfx_x = draw_start_x; + + gfx_drawstr("Trimmable events: "); + gfx_r = 0.8; + gfx_b = 1; + gfx_g = 0.8; + gfx_printf("%d", trimmable_event_cnt); + ); + + trimmed_cnt; +); + + + function check_cc_lane() local (cc_info_w, cc_info_h, lane_name_w, lane_name_h) +( + gfx_a = 1; + gfx_r = 0.8; + gfx_g = 1; + gfx_b = 0.8; + #cc_info = ""; + last_clicked_cc_lane == -1 || last_clicked_cc_lane > 287 ? ( + #cc_info = "Select a CC lane"; + ) : last_clicked_cc_lane > 127 && last_clicked_cc_lane <= 287 ? ( + #cc_info = "14-bit values not supported"; + ) : ( + #cc_info = "CC"; + #cc_info += sprintf(#, "%d", last_clicked_cc_lane); + #cc_info += " "; + #cc_info += #lane_name; + gfx_measurestr(#cc_info, cc_info_w, cc_info_h); + ); + gfx_x = draw_start_x; + gfx_y = draw_start_y; + gfx_measurestr(#cc_info, cc_info_w, cc_info_h); + gfx_drawstr(#cc_info); +); + + +// Checks whether the Trim button is depressed. +// return: 0: not depressed +// 1: depressed +// 3: depressed and SHIFT key depressed +// 5: depressed and CTRL key depressed. + +function check_trim_btn(x, y, w, h, r) local(trim_requested) +( + gfx_x = x; gfx_y = y; + trimmable_event_cnt == 0 ? gfx_a = 0.4; + set_default_colors(); + gfx_roundrect(gfx_x - 6, gfx_y - 3, w + 12, h + 6, r); + gfx_printf("Trim events"); + + trim_requested = 0; + + (mouse_x >= x) && (mouse_x <= x + w) && (mouse_y >= y) && (mouse_y <= y + h) && + !lmb_click_outside_window && (trimmable_event_cnt > 0) ? ( // click on button + gfx_r += 0.2; + gfx_g += 0.2; + gfx_b += 0.2; + mouse_cap & 0x01 && !lmb_down ? ( + lmb_down = true; + trim_requested = 1; + mouse_cap & 0x08 ? ( // SHIFT key depressed + trim_requested = 3; + ) : mouse_cap & 0x04 ? ( // CTRL key depressed + trim_requested = 5; + ); + ); + ); + + trim_requested; +); + + +function run() local(trim_requested, trimmed_cnt, c) +( + set_default_colors(); + gfx_a = 1; + + draw_end_x = gfx_w - 22; + draw_end_y = gfx_h - 80; + gfx_x = draw_start_x; + + gfx_y = draw_start_y; + + center_x = floor(draw_start_x + (draw_end_x - draw_start_x) / 2 + 0.5); + center_y = floor(draw_start_y + (draw_end_y - draw_start_y) / 2 + 0.5); + + gfx_w != last_w ? ( + center_x = floor(draw_start_x + (draw_end_x - draw_start_x) / 2 + 0.5); + slider_last_x = center_x; + last_w = gfx_w; + ); + + // Check if lmb down and mouse cursor outside window + mouse_cap >= 1 && (mouse_x <= 0 || mouse_x >= gfx_w || mouse_y < 2 || mouse_y >= gfx_h) ? ( + lmb_click_outside_window = true; + ) : mouse_cap == 0 ? ( + lmb_click_outside_window = false; + ); + + check_cc_lane(); + + gfx_x = center_x - floor(s_w_trim / 2 + 0.5); // centered + //gfx_x = draw_start_x; // left aligned + trim_requested = check_trim_btn(gfx_x, draw_start_y + (6 * gfx_texth), s_w_trim, s_h_trim, 12); + + trimmed_cnt = trim(trim_requested); // Do it! + + // add "undo point" if necessary: + trimmed_cnt > 0 ? ( + Undo_OnStateChange(sprintf(#, "Trim CC%d events (x%d)", last_clicked_cc_lane, trimmed_cnt)); + undo_cnt += 1; + ); + + c = gfx_getchar(); + + // ESC: exit + c == 27 ? gfx_quit(); + + // ctrl+Z: Undo + c == 26 && ((mouse_cap & 0x38)==0) ? ( + + #undo_desc = ""; + Undo_CanUndo2(#undo_desc, 0) ? ( + Undo_DoUndo2(0); // -> non zero if done + //Main_OnCommand(40029, 0); // undo + + // If this was one of OUR undos then restore our state as well. + (strncmp(#undo_desc, "Trim CC", 7) == 0) && (undo_cnt > 0) ? ( + last_after_trim_checksum = stack_pop(); + last_unselected_epsilon = stack_pop(); + epsilon = stack_pop(); + undo_cnt -= 1; + ); + ); + ); + + mouse_cap == 0 ? ( + lmb_down = false; + ); + + last_h = gfx_h; + last_w = gfx_w; + gfx_update(); + + // Loop unless graphics windows closed: + c != -1 ? ( + defer("run();"); + ); +); + + +function init() +( + gfx_init(SCRIPT_NAME, 230, 170); // show script window + + // Attach "pin on top" button (if Reaper 6.24+ with JS extension installed): + // FIXME: works randomly when script launched from toolbar!! + //GetAppVersion(#reaper_version); + //strnicmp(#reaper_version, "6.24", 4) >=0 && APIExists("JS_Window_Find") ? ( + (APIExists("JS_Window_Find") && APIExists("JS_Window_AttachTopmostPin")) ? ( + JS_Window_AttachTopmostPin(JS_Window_Find(SCRIPT_NAME, true)); + ); + + last_w = gfx_w; + last_h = gfx_h; + + draw_start_x = 22; + draw_end_x = gfx_w - 22; + draw_start_y = 30; + draw_end_y = gfx_h - 80; + center_x = floor(draw_start_x + (draw_end_x - draw_start_x) / 2 + 0.5); + center_y = floor(draw_start_y + (draw_end_y - draw_start_y) / 2 + 0.5); + + gfx_setfont(1, "Verdana", 14, ''); + gfx_measurestr("Trim events", s_w_trim, s_h_trim); + slider_last_x = draw_start_x; + lmb_click_outside_window = 0; + + last_clicked_cc_lane == -1; +); + +init(); +run();