2026-10-27 00:53:41 +01:00
|
|
|
#include "piano_roll.hpp"
|
|
|
|
|
#include <FL/Fl.H>
|
|
|
|
|
#include <FL/fl_draw.H>
|
|
|
|
|
#include <FL/fl_ask.H>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdio>
|
2026-10-27 15:34:54 +01:00
|
|
|
#include <sstream>
|
2026-10-27 01:37:21 +01:00
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
constexpr int key_width = 64, row_height = 16, scale = 96, header = 28;
|
2026-10-27 15:34:54 +01:00
|
|
|
template <typename... Values> bool parse(const char *text, Values &...values)
|
|
|
|
|
{
|
|
|
|
|
std::istringstream input(text);
|
|
|
|
|
(input >> ... >> values);
|
|
|
|
|
return !input.fail() && (input >> std::ws).eof();
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Creates a piano-roll editor for a MIDI clip.
|
|
|
|
|
*
|
|
|
|
|
* The pre-edit guard, when non-NULL, is called before every project mutation
|
|
|
|
|
* and can veto the edit by returning an error.
|
|
|
|
|
*
|
|
|
|
|
* @param left Left edge of the widget in the parent's coordinate system.
|
|
|
|
|
* @param top Top edge of the widget in the parent's coordinate system.
|
|
|
|
|
* @param project Borrowed project that must outlive the widget.
|
|
|
|
|
* @param clip ID of the MIDI clip to edit.
|
|
|
|
|
* @param before Optional pre-edit guard; NULL means no guard is used.
|
|
|
|
|
* @param context Opaque pointer passed to @p before on every edit.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64_t clip, BeforeEdit before,
|
|
|
|
|
void *context)
|
|
|
|
|
: Fl_Widget(left, top, 900, header + 128 * row_height), project_(project), clip_(clip), before_(before),
|
|
|
|
|
context_(context)
|
|
|
|
|
{
|
|
|
|
|
set_project(project, clip);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Points the editor at a different project or clip.
|
|
|
|
|
*
|
|
|
|
|
* Clears any in-progress drag when the project or clip changes and resizes the
|
2026-10-27 15:34:54 +01:00
|
|
|
* widget to fit the clip's source length. Selection is cleared on a new model or clip.
|
2026-10-27 01:58:27 +01:00
|
|
|
*
|
|
|
|
|
* @param project Borrowed project that must outlive the widget.
|
|
|
|
|
* @param clip ID of the MIDI clip to display.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
void PianoRollWidget::set_project(xtc_project *project, uint64_t clip)
|
|
|
|
|
{
|
2026-10-27 15:34:54 +01:00
|
|
|
if (project != project_ || clip != clip_) {
|
2026-10-27 01:37:21 +01:00
|
|
|
mode_ = 0;
|
2026-10-27 15:34:54 +01:00
|
|
|
selected_ = UINT64_MAX;
|
|
|
|
|
all_ = false;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
project_ = project;
|
|
|
|
|
clip_ = clip;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_clip_info c{};
|
2026-10-27 01:37:21 +01:00
|
|
|
int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK
|
|
|
|
|
? static_cast<int>(c.source_length_tick * scale / 960) + key_width + 20
|
|
|
|
|
: 900;
|
2026-10-27 00:53:41 +01:00
|
|
|
size(std::max(900, width), header + 128 * row_height);
|
|
|
|
|
redraw();
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Converts a horizontal pixel position to a snapped tick.
|
|
|
|
|
*
|
|
|
|
|
* The position is measured in absolute window coordinates and snapped down to
|
2026-10-27 15:34:54 +01:00
|
|
|
* the current configured grid.
|
2026-10-27 01:58:27 +01:00
|
|
|
*
|
|
|
|
|
* @param px Horizontal position in window coordinates.
|
|
|
|
|
* @return Tick offset within the clip source, or 0 for positions on or left of
|
|
|
|
|
* the keyboard.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
uint64_t PianoRollWidget::tick(int px) const
|
2026-10-27 01:37:21 +01:00
|
|
|
{
|
2026-10-27 15:34:54 +01:00
|
|
|
return static_cast<uint64_t>(std::max(0, px - x() - key_width)) * 960U / scale / grid_ * grid_;
|
2026-10-27 01:37:21 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Reports an edit result and notifies listeners on success.
|
|
|
|
|
*
|
|
|
|
|
* On success the widget callback runs and the editor is redrawn; on failure an
|
|
|
|
|
* alert shows the error and the project is left unchanged.
|
|
|
|
|
*
|
|
|
|
|
* @param result Result of the attempted project mutation.
|
|
|
|
|
* @return The same result code.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result PianoRollWidget::change(xtc_result result)
|
|
|
|
|
{
|
2026-10-27 01:37:21 +01:00
|
|
|
if (result == XTC_OK) {
|
|
|
|
|
do_callback();
|
|
|
|
|
redraw();
|
|
|
|
|
} else
|
|
|
|
|
fl_alert("Note unchanged: %s", xtc_result_string(result));
|
2026-10-27 00:53:41 +01:00
|
|
|
return result;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Adds a note to the current clip and selects the new note.
|
|
|
|
|
*
|
|
|
|
|
* Invokes the pre-edit guard, if any, before mutating the project.
|
|
|
|
|
*
|
|
|
|
|
* @param start Note start in clip source ticks.
|
|
|
|
|
* @param length Note length in ticks.
|
|
|
|
|
* @param pitch MIDI pitch from 0 to 127.
|
|
|
|
|
* @param velocity MIDI velocity from 1 to 127.
|
|
|
|
|
* @param channel MIDI channel from 0 to 15.
|
|
|
|
|
* @return XTC_OK on success, the guard's error, or the project error.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity,
|
|
|
|
|
unsigned int channel)
|
2026-10-27 00:53:41 +01:00
|
|
|
{
|
|
|
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
|
|
|
|
uint64_t id;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (r == XTC_OK)
|
|
|
|
|
r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id);
|
2026-10-27 15:34:54 +01:00
|
|
|
if (r == XTC_OK) {
|
2026-10-27 01:37:21 +01:00
|
|
|
selected_ = id;
|
2026-10-27 15:34:54 +01:00
|
|
|
all_ = false;
|
|
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
return change(r);
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Removes the selected note from the current clip.
|
|
|
|
|
*
|
|
|
|
|
* Invokes the pre-edit guard, if any, before mutating the project. The
|
|
|
|
|
* selection is cleared only when the removal succeeds.
|
|
|
|
|
*
|
|
|
|
|
* @return XTC_OK on success, the guard's error, or the project error.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result PianoRollWidget::delete_selected()
|
|
|
|
|
{
|
2026-10-27 15:34:54 +01:00
|
|
|
xtc_note_edit edit{};
|
|
|
|
|
edit.action = XTC_NOTES_DELETE;
|
|
|
|
|
return apply(edit);
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Draws the keyboard, beat grid, and notes of the current clip.
|
|
|
|
|
*
|
|
|
|
|
* Paints only the background when the clip is missing or is not a MIDI clip.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
void PianoRollWidget::draw()
|
|
|
|
|
{
|
|
|
|
|
fl_push_clip(x(), y(), w(), h());
|
2026-10-27 01:37:21 +01:00
|
|
|
fl_color(fl_rgb_color(30, 33, 38));
|
|
|
|
|
fl_rectf(x(), y(), w(), h());
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_clip_info clip{};
|
2026-10-27 01:37:21 +01:00
|
|
|
if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) {
|
|
|
|
|
fl_pop_clip();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int cx, cy, cw, ch;
|
|
|
|
|
fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch);
|
2026-10-27 00:53:41 +01:00
|
|
|
fl_font(FL_HELVETICA, 11);
|
|
|
|
|
for (int pitch = 0; pitch < 128; ++pitch) {
|
|
|
|
|
int py = y() + header + (127 - pitch) * row_height;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (py + row_height < cy || py > cy + ch)
|
|
|
|
|
continue;
|
2026-10-27 00:53:41 +01:00
|
|
|
int pc = pitch % 12;
|
|
|
|
|
bool black = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10;
|
|
|
|
|
fl_color(black ? fl_rgb_color(29, 31, 35) : fl_rgb_color(43, 46, 52));
|
|
|
|
|
fl_rectf(cx, py, cw, row_height - 1);
|
|
|
|
|
fl_color(black ? fl_rgb_color(60, 63, 70) : fl_rgb_color(130, 135, 140));
|
|
|
|
|
fl_rectf(x(), py, key_width - 2, row_height - 1);
|
|
|
|
|
if (!pc) {
|
2026-10-27 01:37:21 +01:00
|
|
|
char text[20];
|
|
|
|
|
std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1);
|
|
|
|
|
fl_color(FL_WHITE);
|
|
|
|
|
fl_draw(text, x() + 6, py + 12);
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-27 15:34:54 +01:00
|
|
|
// At 1-tick snap, show at least a 120-tick visual grid to avoid pixel overdraw.
|
|
|
|
|
uint64_t visual = std::max<uint64_t>(120, grid_);
|
|
|
|
|
int spacing = static_cast<int>(visual * scale / 960);
|
|
|
|
|
int first = std::max(0, (cx - x() - key_width) / spacing);
|
|
|
|
|
int last = (cx + cw - x() - key_width) / spacing + 1;
|
2026-10-27 00:53:41 +01:00
|
|
|
for (int step = first; step <= last; ++step) {
|
2026-10-27 15:34:54 +01:00
|
|
|
int px = x() + key_width + static_cast<int>(static_cast<uint64_t>(step) * visual * scale / 960);
|
|
|
|
|
fl_color((static_cast<uint64_t>(step) * visual) % 3840 == 0 ? fl_rgb_color(95, 98, 105) : fl_rgb_color(52, 55, 62));
|
2026-10-27 00:53:41 +01:00
|
|
|
fl_line(px, cy, px, cy + ch);
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
size_t count = 0;
|
|
|
|
|
xtc_project_note_count(project_, &count);
|
2026-10-27 00:53:41 +01:00
|
|
|
for (size_t i = 0; i < count; ++i) {
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_note_info n{};
|
|
|
|
|
xtc_project_get_note(project_, i, &n);
|
2026-10-27 15:34:54 +01:00
|
|
|
if (!visible(n))
|
2026-10-27 01:37:21 +01:00
|
|
|
continue;
|
2026-10-27 00:53:41 +01:00
|
|
|
int left = x() + key_width + static_cast<int>(n.start_tick * scale / 960);
|
|
|
|
|
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
|
|
|
|
int py = y() + header + (127 - static_cast<int>(n.pitch)) * row_height;
|
|
|
|
|
int a = std::max(cx, left), b = std::min(cx + cw, right);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (a >= b || py + row_height < cy || py > cy + ch)
|
|
|
|
|
continue;
|
2026-10-27 15:34:54 +01:00
|
|
|
fl_color(all_ || n.id == selected_ ? fl_rgb_color(230, 190, 80)
|
|
|
|
|
: fl_rgb_color(50, static_cast<uchar>(70 + n.velocity), 110));
|
2026-10-27 00:53:41 +01:00
|
|
|
fl_rectf(a, py + 1, b - a, row_height - 3);
|
|
|
|
|
}
|
2026-10-27 15:34:54 +01:00
|
|
|
fl_color(fl_rgb_color(30, 33, 38));
|
|
|
|
|
fl_rectf(x(), y(), w(), header);
|
|
|
|
|
char status[160];
|
|
|
|
|
std::snprintf(status, sizeof(status), "Snap %llu ticks | Channel %d (-1 all) | Pitches %u..%u | %s",
|
|
|
|
|
static_cast<unsigned long long>(grid_), channel_filter_, low_, high_,
|
|
|
|
|
all_ ? "All filtered notes selected" : "Click selects; Ctrl+A selects filtered notes");
|
|
|
|
|
fl_color(FL_WHITE);
|
|
|
|
|
fl_draw(status, x() + 6, y() + 18);
|
2026-10-27 00:53:41 +01:00
|
|
|
fl_pop_clip();
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @brief Handles mouse, keyboard, and focus events for the editor.
|
|
|
|
|
*
|
|
|
|
|
* Left-click on empty space adds a note, clicking a note selects it, and
|
|
|
|
|
* dragging moves or resizes it. Right-click and Delete remove the selected
|
|
|
|
|
* note; Escape cancels an in-progress drag.
|
|
|
|
|
*
|
|
|
|
|
* @param event FLTK event code.
|
|
|
|
|
* @return 1 when the event was handled, otherwise the result of the base
|
|
|
|
|
* class handler.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
int PianoRollWidget::handle(int event)
|
|
|
|
|
{
|
|
|
|
|
if (event == FL_PUSH) {
|
2026-10-27 01:37:21 +01:00
|
|
|
take_focus();
|
|
|
|
|
mode_ = 0;
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
xtc_project_note_count(project_, &count);
|
2026-10-27 00:53:41 +01:00
|
|
|
selected_ = UINT64_MAX;
|
2026-10-27 15:34:54 +01:00
|
|
|
all_ = false;
|
2026-10-27 00:53:41 +01:00
|
|
|
for (size_t i = count; i > 0; --i) {
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_note_info n{};
|
|
|
|
|
xtc_project_get_note(project_, i - 1, &n);
|
2026-10-27 00:53:41 +01:00
|
|
|
int left = x() + key_width + static_cast<int>(n.start_tick * scale / 960);
|
|
|
|
|
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
|
|
|
|
int top = y() + header + (127 - static_cast<int>(n.pitch)) * row_height;
|
2026-10-27 15:34:54 +01:00
|
|
|
if (visible(n) && Fl::event_x() >= left && Fl::event_x() < right && Fl::event_y() >= top &&
|
2026-10-27 01:37:21 +01:00
|
|
|
Fl::event_y() < top + row_height) {
|
|
|
|
|
selected_ = n.id;
|
|
|
|
|
original_ = n;
|
|
|
|
|
drag_x_ = Fl::event_x();
|
|
|
|
|
drag_y_ = Fl::event_y();
|
|
|
|
|
mode_ = right - Fl::event_x() <= 6 ? 2 : 1;
|
|
|
|
|
break;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
if (selected_ != UINT64_MAX && Fl::event_button() == FL_RIGHT_MOUSE) {
|
|
|
|
|
delete_selected();
|
|
|
|
|
mode_ = 0;
|
|
|
|
|
} else if (Fl::event_button() == FL_LEFT_MOUSE && selected_ == UINT64_MAX && Fl::event_x() >= x() + key_width &&
|
|
|
|
|
Fl::event_y() >= y() + header) {
|
2026-10-27 00:53:41 +01:00
|
|
|
int pitch = 127 - (Fl::event_y() - y() - header) / row_height;
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_clip_info c{};
|
|
|
|
|
uint64_t start = tick(Fl::event_x());
|
2026-10-27 15:34:54 +01:00
|
|
|
if (pitch >= static_cast<int>(low_) && pitch <= static_cast<int>(high_) && xtc_project_find_clip(project_, clip_, &c) == XTC_OK &&
|
2026-10-27 01:37:21 +01:00
|
|
|
start < c.source_length_tick)
|
|
|
|
|
add_note(start, std::min<uint64_t>(960, c.source_length_tick - start),
|
2026-10-27 15:34:54 +01:00
|
|
|
static_cast<unsigned int>(pitch), 100, channel_filter_ < 0 ? 0U : static_cast<unsigned int>(channel_filter_));
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
redraw();
|
|
|
|
|
return 1;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
if (event == FL_DRAG)
|
|
|
|
|
return mode_ != 0;
|
2026-10-27 00:53:41 +01:00
|
|
|
if (event == FL_RELEASE && mode_) {
|
2026-10-27 01:37:21 +01:00
|
|
|
int mode = mode_;
|
|
|
|
|
mode_ = 0;
|
2026-10-27 15:34:54 +01:00
|
|
|
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale / static_cast<int64_t>(grid_) * static_cast<int64_t>(grid_);
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_clip_info c{};
|
|
|
|
|
if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK)
|
|
|
|
|
return 1;
|
|
|
|
|
int64_t start = static_cast<int64_t>(original_.start_tick),
|
|
|
|
|
length = static_cast<int64_t>(original_.length_tick);
|
2026-10-27 00:53:41 +01:00
|
|
|
unsigned int pitch = original_.pitch;
|
|
|
|
|
if (mode == 1) {
|
|
|
|
|
start = std::clamp<int64_t>(start + delta, 0, static_cast<int64_t>(c.source_length_tick) - length);
|
2026-10-27 01:37:21 +01:00
|
|
|
pitch = static_cast<unsigned int>(
|
|
|
|
|
std::clamp(static_cast<int>(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127));
|
2026-10-27 00:53:41 +01:00
|
|
|
} else {
|
|
|
|
|
int64_t maximum = static_cast<int64_t>(c.source_length_tick) - start;
|
2026-10-27 15:34:54 +01:00
|
|
|
length = std::clamp<int64_t>(length + delta, std::min<int64_t>(static_cast<int64_t>(grid_), maximum), maximum);
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (r == XTC_OK)
|
|
|
|
|
r = xtc_project_edit_note(project_, selected_, static_cast<uint64_t>(start), static_cast<uint64_t>(length),
|
|
|
|
|
pitch, original_.velocity, original_.channel);
|
|
|
|
|
change(r);
|
|
|
|
|
return 1;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 15:34:54 +01:00
|
|
|
if (event == FL_KEYDOWN) {
|
|
|
|
|
int key = Fl::event_key();
|
|
|
|
|
if (key == 'a' && (Fl::event_state() & FL_CTRL)) { select_filtered(); return 1; }
|
|
|
|
|
if (key == 'q') { command(3); return 1; }
|
|
|
|
|
if (key == 'v') { command(2); return 1; }
|
|
|
|
|
if (key == 'g') { command(0); return 1; }
|
|
|
|
|
if (key == 'f') { command(1); return 1; }
|
|
|
|
|
if (key == 't') { command(4); return 1; }
|
|
|
|
|
if (key == 'd' && (Fl::event_state() & FL_CTRL)) { command(5); return 1; }
|
|
|
|
|
if (key == FL_Up || key == FL_Down) {
|
|
|
|
|
xtc_note_edit edit{};
|
|
|
|
|
edit.velocity = edit.channel = -1;
|
|
|
|
|
edit.transpose = key == FL_Up ? 1 : -1;
|
|
|
|
|
apply(edit);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) {
|
|
|
|
|
delete_selected();
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) {
|
|
|
|
|
mode_ = 0;
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (event == FL_FOCUS || event == FL_UNFOCUS)
|
|
|
|
|
return 1;
|
2026-10-27 00:53:41 +01:00
|
|
|
return Fl_Widget::handle(event);
|
|
|
|
|
}
|
2026-10-27 15:34:54 +01:00
|
|
|
|
|
|
|
|
bool PianoRollWidget::visible(const xtc_note_info &n) const
|
|
|
|
|
{
|
|
|
|
|
return n.clip_id == clip_ && (channel_filter_ < 0 || n.channel == static_cast<unsigned int>(channel_filter_)) &&
|
|
|
|
|
n.pitch >= low_ && n.pitch <= high_;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result PianoRollWidget::set_grid(uint64_t ticks)
|
|
|
|
|
{
|
|
|
|
|
if (ticks != 1 && ticks != 120 && ticks != 160 && ticks != 240 && ticks != 320 && ticks != 480 && ticks != 960)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
grid_ = ticks;
|
|
|
|
|
redraw();
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result PianoRollWidget::set_filter(int channel, unsigned int low, unsigned int high)
|
|
|
|
|
{
|
|
|
|
|
if (channel < -1 || channel > 15 || low > high || high > 127) return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
channel_filter_ = channel;
|
|
|
|
|
low_ = low;
|
|
|
|
|
high_ = high;
|
|
|
|
|
selected_ = UINT64_MAX;
|
|
|
|
|
all_ = false;
|
|
|
|
|
mode_ = 0;
|
|
|
|
|
redraw();
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PianoRollWidget::select_filtered()
|
|
|
|
|
{
|
|
|
|
|
selected_ = UINT64_MAX;
|
|
|
|
|
all_ = true;
|
|
|
|
|
mode_ = 0;
|
|
|
|
|
redraw();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xtc_result PianoRollWidget::apply(const xtc_note_edit &edit)
|
|
|
|
|
{
|
|
|
|
|
if (!all_ && selected_ == UINT64_MAX) return XTC_ERR_NOT_FOUND;
|
|
|
|
|
xtc_note_filter filter{clip_, all_ ? UINT64_MAX : selected_, channel_filter_, low_, high_};
|
|
|
|
|
size_t count;
|
|
|
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
|
|
|
|
if (r == XTC_OK) r = xtc_project_edit_notes(project_, &filter, &edit, &count);
|
|
|
|
|
if (r == XTC_OK && edit.action == XTC_NOTES_DELETE) {
|
|
|
|
|
selected_ = UINT64_MAX;
|
|
|
|
|
all_ = false;
|
|
|
|
|
}
|
|
|
|
|
return change(r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PianoRollWidget::command(int action)
|
|
|
|
|
{
|
|
|
|
|
const char *text;
|
|
|
|
|
xtc_note_edit edit{};
|
|
|
|
|
edit.velocity = edit.channel = -1;
|
|
|
|
|
if (action == 0) {
|
|
|
|
|
text = fl_input("Snap ticks: 1, 120, 160 (1/16 triplet), 240, 320 (1/8 triplet), 480, 960", "240");
|
|
|
|
|
unsigned int value;
|
|
|
|
|
if (text && (!parse(text, value) || set_grid(value) != XTC_OK))
|
|
|
|
|
fl_alert("Choose one of the listed grids.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action == 1) {
|
|
|
|
|
text = fl_input("Channel (-1 all, 0..15), lowest pitch, highest pitch", "-1 0 127");
|
|
|
|
|
int channel; unsigned int low, high;
|
|
|
|
|
if (text && (!parse(text, channel, low, high) ||
|
|
|
|
|
set_filter(channel, low, high) != XTC_OK)) fl_alert("Invalid note filter.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action == 6) { select_filtered(); return; }
|
|
|
|
|
if (action == 7) { delete_selected(); return; }
|
|
|
|
|
if (action == 2) {
|
|
|
|
|
text = fl_input("Velocity (1..127, -1 preserve), channel (0..15, -1 preserve)", "100 -1");
|
|
|
|
|
if (!text) return;
|
|
|
|
|
if (!parse(text, edit.velocity, edit.channel)) {
|
|
|
|
|
fl_alert("Enter velocity and channel."); return;
|
|
|
|
|
}
|
|
|
|
|
} else if (action == 3) edit.grid_tick = grid_;
|
|
|
|
|
else if (action == 4) {
|
|
|
|
|
text = fl_input("Transpose semitones (-127..127)", "12");
|
|
|
|
|
if (!text) return;
|
|
|
|
|
if (!parse(text, edit.transpose)) {
|
|
|
|
|
fl_alert("Enter a semitone count."); return;
|
|
|
|
|
}
|
|
|
|
|
} else if (action == 5) {
|
|
|
|
|
text = fl_input("Additional copies (1..4096), spacing in source ticks (clip must have room)", "1 960");
|
|
|
|
|
unsigned long long interval;
|
|
|
|
|
if (!text) return;
|
|
|
|
|
if (!parse(text, edit.copies, interval)) {
|
|
|
|
|
fl_alert("Enter copy count and spacing."); return;
|
|
|
|
|
}
|
|
|
|
|
edit.action = XTC_NOTES_REPEAT;
|
|
|
|
|
edit.interval_tick = static_cast<uint64_t>(interval);
|
|
|
|
|
} else return;
|
|
|
|
|
apply(edit);
|
|
|
|
|
}
|