Phase 34: add snap grids, MIDI velocity editing and track instrument menus
This commit is contained in:
parent
d800a6392e
commit
4873b70672
14 changed files with 481 additions and 29 deletions
12
docs/MIDI.md
12
docs/MIDI.md
|
|
@ -167,3 +167,15 @@ Do not unload/replace modules during playback; candidate plans are prepared off
|
|||
the callback and old instances are reclaimed on the control thread. Missing
|
||||
references remain inspectable without loading; failed GUI preparation retains
|
||||
the current project. Save a backup for applications limited to v11 or earlier.
|
||||
|
||||
## Phase 34 note dynamics and clip editing
|
||||
|
||||
Double-click a MIDI clip to open its piano roll. The Note velocity slider and
|
||||
fixed Velocity lane edit individual notes from 1 to 127, preserving timing,
|
||||
pitch and channel. Chord notes at the same start can be selected individually in
|
||||
the keys before editing their highlighted bar. Completed gestures are undoable
|
||||
and save in the existing note velocity field. Velocity is not a separate dB
|
||||
volume stage: the built-in sine scales amplitude, while plugins choose their
|
||||
own patch-dependent response. The track header instrument dropdown reuses saved
|
||||
project instruments or opens explicit loading; bus tracks cannot host instruments.
|
||||
See WORKSPACE.md for grid/snap divisions, filters and control behavior.
|
||||
|
|
|
|||
|
|
@ -68,3 +68,9 @@ previews. New MIDI/audio track commands are undoable and prepare candidates
|
|||
before adoption. The default window is 1280 × 800, minimum 1220 × 720.
|
||||
No dependency or project-format change. See WORKSPACE.md and the new
|
||||
`modern-session.xtc` example; existing engine/plugin limitations still apply.
|
||||
|
||||
Phase 34 adds a sequencer grid/Snap dropdown, absolute grid snapping for moves
|
||||
and trims, MIDI double-click editing, a piano velocity lane/slider and per-track
|
||||
instrument dropdowns. Snap preferences are UI state; velocity and instrument
|
||||
bindings use the existing v12 format. No new dependency or engine feature is
|
||||
required. See WORKSPACE.md for supported divisions, controller semantics and limits.
|
||||
|
|
|
|||
|
|
@ -960,6 +960,35 @@ See WORKSPACE.md for the workflow, tests and limits.
|
|||
|
||||
---
|
||||
|
||||
## Phase 34 — Grid, Clip Editing and Track Instrument Controls
|
||||
|
||||
### Goal
|
||||
Expose common DAW editing actions directly in the flat workspace: selectable
|
||||
snap, MIDI clip double-click, individual note velocity and track instruments.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [x] Draw bar/beat/subdivision grid lines and expose a sequencer Snap dropdown.
|
||||
- [x] Offer Off, straight, triplet, half-note and bar divisions; synchronize the existing snap menu.
|
||||
- [x] Snap clip moves and both trim edges to absolute grid positions, including off-grid clips.
|
||||
- [x] Open the selected MIDI clip's piano roll on left-button double-click without moving it.
|
||||
- [x] Add a fixed velocity lane and selected-note slider with 1..127 validation and one undo step per edit.
|
||||
- [x] Keep coincident chord notes individually editable and preserve timing/pitch/channel.
|
||||
- [x] Add a dropdown to every non-bus track for built-in sine, project instruments and explicit plugin loading.
|
||||
- [x] Preserve track identity through reorder, undo, clear and project reload; retain candidate failure rollback.
|
||||
- [x] Keep controls flat, retain current project format and verify builds, GUI regressions and releases.
|
||||
|
||||
### Implementation status
|
||||
Implemented in the C++ GUI over existing C model APIs. Snap preferences are
|
||||
session UI state; note velocity and track instruments already persist in v12.
|
||||
The project instrument list is built from existing assignments, with Load plugin
|
||||
for other trusted binaries; automatic disk scanning is not introduced.
|
||||
At low zoom the visual grid coarsens for readability while snap remains exact to
|
||||
the selected tick division. Off uses pointer/model tick precision. MIDI velocity
|
||||
is a performance value, not a per-note dB gain stage; third-party instrument
|
||||
patches control its audible response. See WORKSPACE.md and MIDI.md.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification.
|
||||
|
|
|
|||
|
|
@ -206,3 +206,8 @@ The flat workspace puts New MIDI/audio track, WAV import and instrument loading
|
|||
in a persistent sidebar, with a musical transport display and selected-track
|
||||
overview. See [WORKSPACE.md](WORKSPACE.md) for shortcuts and workflow. Open
|
||||
`modern-session.xtc` in the installed examples for an original four-track arrangement.
|
||||
|
||||
Phase 34: use the sequencer Snap dropdown for straight/triplet/bar grids or Off.
|
||||
Double-click MIDI clips to edit notes, then use the Velocity lane or selected-note
|
||||
slider for dynamics. Each non-bus track header has an instrument dropdown offering
|
||||
built-in sine, project instruments and explicit plugin loading. See WORKSPACE.md.
|
||||
|
|
|
|||
|
|
@ -55,3 +55,47 @@ and workflow tests cover the retained commands. A modern presentation does not
|
|||
add commercial-DAW features: detachable/resizable docks, mixer channel strips,
|
||||
automatic plugin scanning, custom plugin editors and new audio/MIDI features
|
||||
remain outside this phase. Existing plugin and engine limits still apply.
|
||||
|
||||
## Phase 34 editing controls
|
||||
|
||||
The **Snap** dropdown above the sequencer selects Off, 1/32, 1/16 triplet,
|
||||
1/16, 1/8 triplet, 1/8, 1/4, 1/2 or one bar. Existing View/Snap commands update
|
||||
the same preference. Bar lines are strongest, beats medium, subdivisions subtle.
|
||||
At low zoom closely spaced lines are coarsened to at least six pixels; snapping
|
||||
still uses the selected division. Clip moves and trim boundaries snap to the
|
||||
nearest absolute grid position (halfway rounds forward), including clips that
|
||||
start off the grid. Off removes musical quantization; pointer resolution and
|
||||
integer model ticks still apply. Invalid trims retain the original clip.
|
||||
Snap is a UI preference, not a project edit or a saved project setting.
|
||||
|
||||
Double-click a MIDI clip with the left mouse button to open its piano roll.
|
||||
The clip is selected and the click does not move/trim it. Audio/silent clips
|
||||
retain their existing selection/drag behavior. The piano roll also has a Snap
|
||||
dropdown for its existing note grid; sequencer and piano snap remain independent.
|
||||
|
||||
The piano roll has a fixed **Velocity** lane beneath the keys and a **Note
|
||||
velocity** slider. Select a note, then drag its bar vertically or release the
|
||||
slider at a value from 1 to 127. Only that note changes; pitch, timing, length,
|
||||
channel and other notes stay intact. One completed drag/slider edit is one undo
|
||||
step; Escape cancels a lane drag. For chord notes sharing a start time, select
|
||||
the desired note in the keys first; its highlighted velocity bar and slider
|
||||
remain individually editable. Horizontal scrolling aligns the lane with note
|
||||
starts; vertical scrolling moves the keys while the lane stays available.
|
||||
Filters apply to lane visibility and selection. Values save with existing note
|
||||
metadata and affect built-in synthesis; plugin patches determine velocity response.
|
||||
This is MIDI velocity, not a per-note audio gain or loudness meter.
|
||||
|
||||
Every ordinary track header has an instrument dropdown. Choose **Built-in sine**,
|
||||
an instrument already assigned elsewhere in the project, or **Load plugin...**
|
||||
for a trusted CLAP/VST2/VST3 binary. Entries show the format and plugin label.
|
||||
Each assignment creates an independent track instance, selects that track for
|
||||
live MIDI and remains undoable; clear/reorder/reopen use stable track IDs. Bus
|
||||
tracks show their bus label instead. Cancel or failed preparation restores the
|
||||
model's selection in the dropdown. The list is project-local, not a scanner or
|
||||
installed-plugin database. Existing mono/default-configuration host limits apply.
|
||||
Instrument changes stop/reprepare playback and are refused while recording.
|
||||
|
||||
Verification includes off-grid move/trim, snap Off, dropdown labels, double-click,
|
||||
coincident notes, per-note slider/lane edits, invalid velocities, undo/reload and
|
||||
instrument reuse/clear/reorder for all three fixture formats. Manual checks use
|
||||
the original modern-session example and the flat sequencer/piano windows.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
#include "mixer_widget.hpp"
|
||||
#include "plugin_dialog.hpp"
|
||||
#include "theme.hpp"
|
||||
#include "snap.hpp"
|
||||
#include "velocity_lane.hpp"
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <FL/Fl_Native_File_Chooser.H>
|
||||
#include "xtc/xtc_version.h"
|
||||
|
||||
|
|
@ -169,13 +173,32 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this);
|
||||
timeline_scroll->end();
|
||||
sequencer_->before_edit = before_arrangement_edit;
|
||||
sequencer_->open_clip = [](void *c) { piano_callback(nullptr,c); };
|
||||
sequencer_->instrument_select = [](void *c, uint64_t track, const xtc_plugin_spec *spec, bool browse) {
|
||||
auto *w = static_cast<MainWindow *>(c);
|
||||
w->sequencer_->select(track);
|
||||
if (browse) { instrument_callback(nullptr,c); return; }
|
||||
xtc_result r = w->assign_instrument(track,spec ? spec->path : nullptr,spec ? spec->index : 0);
|
||||
if (r != XTC_OK) w->report_error("Track instrument unchanged",r);
|
||||
};
|
||||
// Keep this in the editor group rather than the scrollable timeline.
|
||||
auto *parent = Fl_Group::current(); Fl_Group::current(sequencer);
|
||||
snap_ = new Fl_Choice(1040,168,210,26,"Snap");
|
||||
for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = snap_->add("Snap"); snap_->replace(item,xtc_ui::snap_labels[i]); }
|
||||
snap_->value(xtc_ui::snap_index(sequencer_->grid()));
|
||||
snap_->callback([](Fl_Widget *widget, void *c) {
|
||||
auto *choice = static_cast<Fl_Choice *>(widget); int value = choice->value();
|
||||
if (value >= 0 && static_cast<size_t>(value) < xtc_ui::snap_count)
|
||||
(void)static_cast<MainWindow *>(c)->sequencer_->grid(xtc_ui::snap_ticks[value]);
|
||||
},this);
|
||||
Fl_Group::current(parent);
|
||||
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
|
||||
menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);
|
||||
menu->add("Edit/Audio clip gain and fades", 0, [](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->audio_properties_dialog(); }, sequencer_);
|
||||
menu->add("Edit/Rename clip or track",FL_F+2,[](Fl_Widget *, void *c) {
|
||||
const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast<SequencerWidget *>(c)->rename_selection(name);
|
||||
},sequencer_);
|
||||
for (const char *label : {"View/Snap/1 tick","View/Snap/120 ticks","View/Snap/240 ticks","View/Snap/480 ticks","View/Snap/960 ticks"})
|
||||
for (const char *label : {"View/Snap/1 tick","View/Snap/120 ticks","View/Snap/160 ticks","View/Snap/240 ticks","View/Snap/320 ticks","View/Snap/480 ticks","View/Snap/960 ticks","View/Snap/1920 ticks","View/Snap/3840 ticks"})
|
||||
menu->add(label,0,[](Fl_Widget *widget,void *c) {
|
||||
auto *m = static_cast<Fl_Menu_Bar *>(widget);
|
||||
(void)static_cast<SequencerWidget *>(c)->grid(std::strtoull(m->mvalue()->label(),nullptr,10));
|
||||
|
|
@ -1354,20 +1377,38 @@ xtc_result MainWindow::open_piano_roll()
|
|||
menu->add("Edit/Undo",FL_CTRL+'z',history_callback,this);
|
||||
menu->add("Edit/Redo",FL_CTRL+FL_SHIFT+'z',history_callback,this);
|
||||
menu->add("Edit/Redo alternate",FL_CTRL+'y',history_callback,this);
|
||||
auto *scroll = new Fl_Scroll(12, 48, 876, 560);
|
||||
piano_ = new PianoRollWidget(12, 48, project_.get(), clip.id, before_arrangement_edit, this);
|
||||
piano_snap_ = new Fl_Choice(60,46,150,26,"Snap");
|
||||
for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = piano_snap_->add("Snap"); piano_snap_->replace(item,xtc_ui::snap_labels[i]); }
|
||||
piano_snap_->callback([](Fl_Widget *widget, void *c) {
|
||||
auto *w = static_cast<MainWindow *>(c); int value = static_cast<Fl_Choice *>(widget)->value();
|
||||
if (value >= 0 && static_cast<size_t>(value) < xtc_ui::snap_count) (void)w->piano_->set_grid(xtc_ui::snap_ticks[value]);
|
||||
},this);
|
||||
note_velocity_ = new Fl_Value_Slider(340,46,300,26,"Note velocity");
|
||||
note_velocity_->align(FL_ALIGN_LEFT); note_velocity_->value(100);
|
||||
note_velocity_->tooltip("Selected note MIDI velocity, 1..127. Instrument patches determine the response.");
|
||||
note_velocity_->type(FL_HOR_FILL_SLIDER); note_velocity_->bounds(1,127); note_velocity_->step(1);
|
||||
note_velocity_->when(FL_WHEN_RELEASE);
|
||||
note_velocity_->callback([](Fl_Widget *widget, void *c) {
|
||||
auto *w = static_cast<MainWindow *>(c);
|
||||
(void)w->piano_->set_velocity(w->piano_->selected_note(),static_cast<unsigned int>(static_cast<Fl_Value_Slider *>(widget)->value()));
|
||||
},this);
|
||||
auto *scroll = new Fl_Scroll(12,84,876,404);
|
||||
piano_ = new PianoRollWidget(12,84, project_.get(), clip.id, before_arrangement_edit, this);
|
||||
piano_->callback(
|
||||
[](Fl_Widget *, void *c) {
|
||||
static_cast<MainWindow *>(c)->mark_dirty();
|
||||
},
|
||||
this);
|
||||
scroll->end();
|
||||
velocity_lane_ = new VelocityLane(12,500,876,108,*piano_);
|
||||
piano_window_->end();
|
||||
piano_window_->size_range(800,520);
|
||||
xtc_ui::style(piano_window_.get());
|
||||
piano_window_->resizable(scroll);
|
||||
scroll->scroll_to(0, 60 * 16);
|
||||
} else
|
||||
piano_->set_project(project_.get(), clip.id);
|
||||
refresh_workspace();
|
||||
piano_window_->show();
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -1496,6 +1537,19 @@ xtc_result MainWindow::create_track(bool midi)
|
|||
}
|
||||
void MainWindow::refresh_workspace()
|
||||
{
|
||||
if (snap_) snap_->value(xtc_ui::snap_index(sequencer_->grid()));
|
||||
if (piano_) {
|
||||
piano_snap_->value(xtc_ui::snap_index(piano_->grid()));
|
||||
size_t count = 0; bool selected = false; xtc_project_note_count(project_.get(),&count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{}; xtc_project_get_note(project_.get(),i,¬e);
|
||||
if (note.id == piano_->selected_note() && piano_->note_visible(note)) {
|
||||
selected = true; if (Fl::pushed() != note_velocity_) note_velocity_->value(note.velocity); break;
|
||||
}
|
||||
}
|
||||
if (selected) note_velocity_->activate(); else note_velocity_->deactivate();
|
||||
velocity_lane_->redraw();
|
||||
}
|
||||
char position[64], tempo[40], info[512];
|
||||
uint64_t tick = sequencer_->position();
|
||||
std::snprintf(position,sizeof(position),"%03llu : %u : %03u",
|
||||
|
|
@ -1521,6 +1575,7 @@ void MainWindow::refresh_workspace()
|
|||
|
||||
xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index)
|
||||
{
|
||||
uint64_t clip = sequencer_->selected_track() == track ? sequencer_->selected_clip() : UINT64_MAX;
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
xtc_project *candidate = nullptr;
|
||||
xtc_plugin_spec spec{};
|
||||
|
|
@ -1530,7 +1585,7 @@ xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsig
|
|||
if (r == XTC_OK) r = xtc_project_set_instrument(candidate,track,path ? &spec : nullptr);
|
||||
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
|
||||
if (r == XTC_OK) r = install_project(candidate,false);
|
||||
if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty();
|
||||
if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); }
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::select_live_instrument(uint64_t track)
|
||||
|
|
@ -1547,6 +1602,7 @@ xtc_result MainWindow::select_live_instrument(uint64_t track)
|
|||
void MainWindow::instrument_callback(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *w = static_cast<MainWindow *>(context);
|
||||
if (w->recorder_) { w->report_error("Stop recording before instrument changes",XTC_ERR_INVALID_STATE); return; }
|
||||
auto *menu = dynamic_cast<Fl_Menu_Bar *>(widget);
|
||||
const char *label = menu ? menu->mvalue()->label() : "Load instrument plugin";
|
||||
uint64_t track = w->sequencer_->selected_track();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Fl_Box;
|
|||
class Fl_Button;
|
||||
class Fl_Tabs;
|
||||
class Fl_Menu_Bar;
|
||||
class Fl_Choice;
|
||||
class Fl_Value_Slider;
|
||||
|
||||
class MainWindow : public Fl_Double_Window {
|
||||
public:
|
||||
|
|
@ -25,6 +27,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
* Preparation stops/resets transport; recording and failed candidates retain
|
||||
* the current document. No engine/model changes happen in audio callbacks. */
|
||||
xtc_result create_track(bool midi);
|
||||
Fl_Widget *velocity_lane() const { return velocity_lane_; }
|
||||
/* Control-thread plugin commands, transactional model adoption. */
|
||||
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
|
||||
xtc_result select_live_instrument(uint64_t track);
|
||||
|
|
@ -177,6 +180,9 @@ class MainWindow : public Fl_Double_Window {
|
|||
private:
|
||||
void draw() override;
|
||||
void refresh_workspace();
|
||||
Fl_Choice *snap_ = nullptr, *piano_snap_ = nullptr;
|
||||
Fl_Value_Slider *note_velocity_ = nullptr;
|
||||
Fl_Widget *velocity_lane_ = nullptr;
|
||||
Fl_Box *transport_position_ = nullptr;
|
||||
Fl_Box *tempo_display_ = nullptr;
|
||||
Fl_Box *selection_info_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "theme.hpp"
|
||||
#include "snap.hpp"
|
||||
#include "piano_roll.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
|
|
@ -352,7 +353,7 @@ bool PianoRollWidget::visible(const xtc_note_info &n) const
|
|||
|
||||
xtc_result PianoRollWidget::set_grid(uint64_t ticks)
|
||||
{
|
||||
if (ticks != 1 && ticks != 120 && ticks != 160 && ticks != 240 && ticks != 320 && ticks != 480 && ticks != 960)
|
||||
if (xtc_ui::snap_index(ticks) < 0)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
grid_ = ticks;
|
||||
redraw();
|
||||
|
|
@ -400,7 +401,7 @@ void PianoRollWidget::command(int action)
|
|||
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");
|
||||
text = fl_input("Snap ticks: 1, 120, 160 (1/16 triplet), 240, 320 (1/8 triplet), 480, 960, 1920, 3840", "240");
|
||||
unsigned int value;
|
||||
if (text && (!parse(text, value) || set_grid(value) != XTC_OK))
|
||||
fl_alert("Choose one of the listed grids.");
|
||||
|
|
@ -440,3 +441,18 @@ void PianoRollWidget::command(int action)
|
|||
} else return;
|
||||
apply(edit);
|
||||
}
|
||||
|
||||
xtc_result PianoRollWidget::set_velocity(uint64_t id, unsigned int velocity)
|
||||
{
|
||||
if (velocity < 1 || velocity > 127) return XTC_ERR_INVALID_ARGUMENT;
|
||||
size_t count = 0; xtc_project_note_count(project_,&count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{}; xtc_project_get_note(project_,i,¬e);
|
||||
if (note.id != id || !visible(note)) continue;
|
||||
if (note.velocity == velocity) return XTC_OK;
|
||||
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||||
if (r == XTC_OK) r = xtc_project_edit_note(project_,id,note.start_tick,note.length_tick,note.pitch,velocity,note.channel);
|
||||
return change(r);
|
||||
}
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@ class PianoRollWidget : public Fl_Widget {
|
|||
*
|
||||
* @return XTC_OK on success, the guard's error, or the project error.
|
||||
*/
|
||||
xtc_project *project() const { return project_; }
|
||||
bool note_visible(const xtc_note_info ¬e) const { return visible(note); }
|
||||
int note_x(uint64_t tick) const { return x()+64+static_cast<int>(tick*96/960); }
|
||||
uint64_t grid() const { return grid_; }
|
||||
xtc_result set_velocity(uint64_t note, unsigned int velocity);
|
||||
uint64_t selected_note() const { return selected_; }
|
||||
bool select_note(uint64_t id);
|
||||
xtc_result delete_selected();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "sequencer_widget.hpp"
|
||||
#include "theme.hpp"
|
||||
#include "snap.hpp"
|
||||
#include <cstring>
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Scroll.H>
|
||||
|
|
@ -21,9 +23,11 @@
|
|||
* @param context Opaque callback context.
|
||||
*/
|
||||
SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context)
|
||||
: Fl_Widget(left, top, 6300, 3700), project_(project), seek_(seek), context_(context)
|
||||
: Fl_Group(left, top, 6300, 3700), project_(project), seek_(seek), context_(context)
|
||||
{
|
||||
resizable(nullptr);
|
||||
extent();
|
||||
end();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,15 +69,15 @@ void SequencerWidget::set_project(xtc_project *project, bool reset)
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a screen x coordinate to a bounded timeline tick snapped to a sixteenth note.
|
||||
* @brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid.
|
||||
*
|
||||
* @param px Screen x coordinate in pixels.
|
||||
* @return Snapped tick within the supported project range.
|
||||
*/
|
||||
uint64_t SequencerWidget::tick(int px) const
|
||||
{
|
||||
return std::min(XTC_PROJECT_MAX_TICK, static_cast<uint64_t>(std::max(0, px - x() - 180)) * 960U /
|
||||
static_cast<unsigned int>(scale_) / grid_ * grid_);
|
||||
int64_t raw = static_cast<int64_t>(std::max(0,px-x()-180))*960/scale_;
|
||||
return std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(xtc_ui::snap_position(raw,grid_)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,6 +95,7 @@ void SequencerWidget::extent()
|
|||
}
|
||||
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + 200U),
|
||||
std::max(400, static_cast<int>(tracks) * 56 + 100));
|
||||
refresh_instruments();
|
||||
if (parent())
|
||||
parent()->redraw();
|
||||
}
|
||||
|
|
@ -237,16 +242,22 @@ void SequencerWidget::draw()
|
|||
fl_rectf(x()+180,py,w()-180,55);
|
||||
}
|
||||
fl_font(FL_HELVETICA,11);
|
||||
int first = std::max(0,(cx-x()-180)/scale_);
|
||||
int last = (cx+cw-x()-180)/scale_+1;
|
||||
for (int beat = first; beat <= last; ++beat) {
|
||||
int px = x()+180+beat*scale_;
|
||||
fl_color(beat%4 ? fl_rgb_color(43,48,56) : fl_rgb_color(65,72,83));
|
||||
uint64_t visual = grid_ == 1 ? 960 : grid_;
|
||||
while (visual*static_cast<unsigned int>(scale_)/960 < 6) visual *= 2;
|
||||
int spacing = static_cast<int>(visual*static_cast<unsigned int>(scale_)/960);
|
||||
int first = std::max(0,(cx-x()-180)/spacing);
|
||||
int last = (cx+cw-x()-180)/spacing+1;
|
||||
for (int step = first; step <= last; ++step) {
|
||||
uint64_t tick = static_cast<uint64_t>(step)*visual;
|
||||
int px = x()+180+static_cast<int>(tick*static_cast<unsigned int>(scale_)/960);
|
||||
fl_color(tick%3840 == 0 ? fl_rgb_color(76,84,96) : tick%960 == 0 ? fl_rgb_color(56,63,73) : fl_rgb_color(42,48,56));
|
||||
fl_line(px,y()+28,px,y()+h());
|
||||
if (beat%4 == 0) {
|
||||
char label[32]; std::snprintf(label,sizeof(label),"%d",beat/4+1);
|
||||
fl_color(xtc_ui::muted()); fl_draw(label,px+6,y()+19);
|
||||
}
|
||||
}
|
||||
first = std::max(0,(cx-x()-180)/(scale_*4));
|
||||
last = (cx+cw-x()-180)/(scale_*4)+1;
|
||||
for (int bar = first; bar <= last; ++bar) {
|
||||
char label[32]; std::snprintf(label,sizeof(label),"%d",bar+1);
|
||||
fl_color(xtc_ui::muted()); fl_draw(label,x()+180+bar*scale_*4+6,y()+19);
|
||||
}
|
||||
fl_color(xtc_ui::muted()); fl_draw("TRACKS / 4:4",x()+12,y()+19);
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
|
|
@ -259,11 +270,12 @@ void SequencerWidget::draw()
|
|||
fl_color(color); fl_rectf(x(),py,3,55);
|
||||
fl_push_clip(x()+9,py,162,55);
|
||||
fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12);
|
||||
fl_draw(t.name,x()+12,py+23);
|
||||
fl_draw(t.name,x()+12,py+18);
|
||||
char detail[64];
|
||||
std::snprintf(detail,sizeof(detail),"%02zu %s%s%s",i+1,t.is_bus ? "BUS" : "TRACK",
|
||||
t.mute ? " MUTE" : "",t.solo ? " SOLO" : "");
|
||||
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,py+42);
|
||||
if (t.is_bus) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,py+42); }
|
||||
if (t.mute || t.solo) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",x()+164,py+18); }
|
||||
fl_pop_clip();
|
||||
for (size_t j = 0; j < clips; ++j) {
|
||||
xtc_clip_info c{}; xtc_project_get_clip(project_,j,&c);
|
||||
|
|
@ -314,6 +326,7 @@ void SequencerWidget::draw()
|
|||
fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h());
|
||||
fl_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6);
|
||||
}
|
||||
draw_children();
|
||||
fl_pop_clip();
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +338,13 @@ void SequencerWidget::draw()
|
|||
*/
|
||||
int SequencerWidget::handle(int event)
|
||||
{
|
||||
for (const auto &choice : track_choices_) {
|
||||
if (!choice.widget || !choice.widget->visible()) continue;
|
||||
if ((event == FL_PUSH && Fl::event_inside(choice.widget)) ||
|
||||
((event == FL_DRAG || event == FL_RELEASE) && Fl::pushed() == choice.widget) ||
|
||||
(event == FL_KEYDOWN && Fl::focus() == choice.widget))
|
||||
return Fl_Group::handle(event);
|
||||
}
|
||||
if (event == FL_PUSH && (Fl::event_button() == FL_LEFT_MOUSE || Fl::event_button() == FL_RIGHT_MOUSE)) {
|
||||
take_focus();
|
||||
mode_ = 0;
|
||||
|
|
@ -360,6 +380,11 @@ int SequencerWidget::handle(int event)
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks() && clip_ != UINT64_MAX && original_.midi) {
|
||||
mode_ = 0;
|
||||
if (open_clip) open_clip(context_);
|
||||
return 1;
|
||||
}
|
||||
if (Fl::event_button() == FL_RIGHT_MOUSE) {
|
||||
mode_ = 0;
|
||||
Fl_Menu_Item items[6]{};
|
||||
|
|
@ -380,23 +405,24 @@ int SequencerWidget::handle(int event)
|
|||
if (event == FL_RELEASE && mode_) {
|
||||
int mode = mode_;
|
||||
mode_ = 0;
|
||||
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale_ / static_cast<int64_t>(grid_) * static_cast<int64_t>(grid_);
|
||||
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale_;
|
||||
int64_t start = static_cast<int64_t>(original_.start_tick);
|
||||
int64_t length = static_cast<int64_t>(original_.length_tick);
|
||||
int64_t offset = static_cast<int64_t>(original_.offset_tick);
|
||||
uint64_t target = original_.track_id;
|
||||
if (mode == 1) {
|
||||
start = std::max<int64_t>(0, start + delta);
|
||||
start = xtc_ui::snap_position(start+delta,grid_);
|
||||
int row = (Fl::event_y() - y() - 28) / 56;
|
||||
xtc_track_info t{};
|
||||
if (row >= 0 && xtc_project_get_track(project_, static_cast<size_t>(row), &t) == XTC_OK)
|
||||
target = t.id;
|
||||
} else if (mode == 2) {
|
||||
delta = xtc_ui::snap_position(start+delta,grid_)-start;
|
||||
start += delta;
|
||||
offset += delta;
|
||||
length -= delta;
|
||||
} else
|
||||
length += delta;
|
||||
length = xtc_ui::snap_position(start+length+delta,grid_)-start;
|
||||
if (start < 0 || offset < 0 || length <= 0)
|
||||
return 1;
|
||||
if (before_edit) {
|
||||
|
|
@ -430,12 +456,12 @@ int SequencerWidget::handle(int event)
|
|||
}
|
||||
if (event == FL_FOCUS || event == FL_UNFOCUS)
|
||||
return 1;
|
||||
return Fl_Widget::handle(event);
|
||||
return Fl_Group::handle(event);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::grid(uint64_t ticks)
|
||||
{
|
||||
if (ticks != 1 && ticks != 120 && ticks != 240 && ticks != 480 && ticks != 960) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
|
||||
grid_ = ticks; redraw(); return XTC_OK;
|
||||
}
|
||||
xtc_result SequencerWidget::duplicate_selection()
|
||||
|
|
@ -557,3 +583,73 @@ void SequencerWidget::draw_waveform(const xtc_clip_info &c, int left, int top)
|
|||
fl_rectf(px, y1, width, std::max(1, y2-y1+1));
|
||||
}
|
||||
}
|
||||
|
||||
Fl_Choice *SequencerWidget::instrument_choice(uint64_t track) const
|
||||
{
|
||||
for (const auto &entry : track_choices_) if (entry.widget && entry.track == track) return entry.widget;
|
||||
return nullptr;
|
||||
}
|
||||
void SequencerWidget::refresh_instruments() try
|
||||
{
|
||||
size_t tracks = 0, clips = 0;
|
||||
xtc_project_get_counts(project_,&tracks,&clips);
|
||||
catalog_.clear();
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
xtc_track_info t{}; xtc_plugin_spec spec{};
|
||||
xtc_project_get_track(project_,i,&t);
|
||||
if (xtc_project_get_instrument(project_,t.id,&spec) != XTC_OK) continue;
|
||||
bool present = false;
|
||||
for (const auto &item : catalog_) if (!std::strcmp(item.path,spec.path) && item.index == spec.index) present = true;
|
||||
if (!present) catalog_.push_back(spec);
|
||||
}
|
||||
for (size_t i = 0; i < track_choices_.size(); ++i) {
|
||||
auto &entry = track_choices_[i];
|
||||
xtc_track_info t{};
|
||||
if (i >= tracks || xtc_project_get_track(project_,i,&t) != XTC_OK || t.is_bus) {
|
||||
if (entry.widget) entry.widget->hide();
|
||||
entry.track = UINT64_MAX; continue;
|
||||
}
|
||||
if (!entry.widget) {
|
||||
struct GroupScope {
|
||||
Fl_Group *previous;
|
||||
explicit GroupScope(Fl_Group *group) : previous(Fl_Group::current()) { Fl_Group::current(group); }
|
||||
~GroupScope() { Fl_Group::current(previous); }
|
||||
} scope(this);
|
||||
entry.widget = new Fl_Choice(x()+10,y()+58+static_cast<int>(i)*56,158,20);
|
||||
xtc_ui::style(entry.widget); entry.widget->textsize(10); entry.widget->textcolor(xtc_ui::text());
|
||||
entry.widget->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin...");
|
||||
entry.widget->callback(instrument_callback,this);
|
||||
}
|
||||
entry.track = t.id;
|
||||
entry.widget->resize(x()+10,y()+58+static_cast<int>(i)*56,158,20);
|
||||
entry.widget->clear(); entry.widget->add("Built-in sine");
|
||||
xtc_plugin_spec current{}; int selected = 0;
|
||||
bool assigned = xtc_project_get_instrument(project_,t.id,¤t) == XTC_OK;
|
||||
for (size_t j = 0; j < catalog_.size(); ++j) {
|
||||
std::string name = std::string(xtc_plugin_format_name(catalog_[j].format))+": "+catalog_[j].label;
|
||||
int item = entry.widget->add("Instrument"); entry.widget->replace(item,name.c_str());
|
||||
if (assigned && !std::strcmp(current.path,catalog_[j].path) && current.index == catalog_[j].index) selected = static_cast<int>(j)+1;
|
||||
}
|
||||
entry.widget->add("Load plugin..."); entry.widget->value(selected); entry.widget->show();
|
||||
}
|
||||
} catch (const std::bad_alloc &) {
|
||||
catalog_.clear();
|
||||
for (auto &entry : track_choices_) if (entry.widget) entry.widget->hide();
|
||||
}
|
||||
void SequencerWidget::instrument_callback(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *owner = static_cast<SequencerWidget *>(context);
|
||||
auto *choice = static_cast<Fl_Choice *>(widget);
|
||||
for (const auto &entry : owner->track_choices_) if (entry.widget == choice) {
|
||||
uint64_t track = entry.track;
|
||||
int index = choice->value();
|
||||
bool browse = index == static_cast<int>(owner->catalog_.size())+1;
|
||||
xtc_plugin_spec spec{};
|
||||
if (index > 0 && !browse) {
|
||||
if (static_cast<size_t>(index) > owner->catalog_.size()) return;
|
||||
spec = owner->catalog_[static_cast<size_t>(index)-1];
|
||||
}
|
||||
if (owner->instrument_select) owner->instrument_select(owner->context_,track,index > 0 && !browse ? &spec : nullptr,browse);
|
||||
owner->refresh_instruments(); owner->redraw(); return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
#ifndef XTC_SEQUENCER_WIDGET_HPP
|
||||
#define XTC_SEQUENCER_WIDGET_HPP
|
||||
#include <FL/Fl_Widget.H>
|
||||
#include <FL/Fl_Group.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include "xtc/xtc_plugin.h"
|
||||
#include <array>
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class SequencerWidget : public Fl_Widget {
|
||||
class SequencerWidget : public Fl_Group {
|
||||
public:
|
||||
/**
|
||||
* @brief Prepares a model edit using the widget's opaque callback context.
|
||||
*
|
||||
* @return XTC_OK to allow the edit, or an error to cancel it.
|
||||
*/
|
||||
using OpenClip = void (*)(void *);
|
||||
using InstrumentSelect = void (*)(void *, uint64_t, const xtc_plugin_spec *, bool);
|
||||
OpenClip open_clip = nullptr;
|
||||
InstrumentSelect instrument_select = nullptr;
|
||||
Fl_Choice *instrument_choice(uint64_t track) const;
|
||||
void refresh_instruments();
|
||||
bool select(uint64_t track, uint64_t clip = UINT64_MAX);
|
||||
xtc_result duplicate_selection();
|
||||
xtc_result split_selection();
|
||||
|
|
@ -141,6 +150,10 @@ class SequencerWidget : public Fl_Widget {
|
|||
void draw() override;
|
||||
|
||||
private:
|
||||
struct TrackChoice { Fl_Choice *widget = nullptr; uint64_t track = UINT64_MAX; };
|
||||
std::array<TrackChoice,XTC_PROJECT_MAX_TRACKS> track_choices_{};
|
||||
std::vector<xtc_plugin_spec> catalog_;
|
||||
static void instrument_callback(Fl_Widget *, void *);
|
||||
struct Wave {
|
||||
std::string path;
|
||||
std::unique_ptr<xtc_waveform, decltype(&xtc_waveform_destroy)> data{nullptr, xtc_waveform_destroy};
|
||||
|
|
@ -154,7 +167,7 @@ class SequencerWidget : public Fl_Widget {
|
|||
int scale_ = 48, drag_x_ = 0, mode_ = 0;
|
||||
xtc_clip_info original_{};
|
||||
/**
|
||||
* @brief Converts a screen x coordinate to a bounded timeline tick snapped to a sixteenth note.
|
||||
* @brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid.
|
||||
*
|
||||
* @param px Screen x coordinate in pixels.
|
||||
* @return Snapped tick within the supported project range.
|
||||
|
|
|
|||
19
src/gui/snap.hpp
Normal file
19
src/gui/snap.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef XTC_GUI_SNAP_HPP
|
||||
#define XTC_GUI_SNAP_HPP
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
namespace xtc_ui {
|
||||
constexpr uint64_t snap_ticks[] = {1,120,160,240,320,480,960,1920,3840};
|
||||
constexpr const char *snap_labels[] = {"Off","1/32","1/16 triplet","1/16","1/8 triplet","1/8","1/4","1/2","1 bar"};
|
||||
constexpr size_t snap_count = sizeof(snap_ticks)/sizeof(snap_ticks[0]);
|
||||
inline int snap_index(uint64_t ticks) {
|
||||
for (size_t i = 0; i < snap_count; ++i) if (snap_ticks[i] == ticks) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
inline int64_t snap_position(int64_t ticks, uint64_t grid) {
|
||||
if (ticks <= 0) return 0;
|
||||
int64_t step = static_cast<int64_t>(grid);
|
||||
return (ticks+step/2)/step*step;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
73
src/gui/velocity_lane.hpp
Normal file
73
src/gui/velocity_lane.hpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#ifndef XTC_GUI_VELOCITY_LANE_HPP
|
||||
#define XTC_GUI_VELOCITY_LANE_HPP
|
||||
#include "piano_roll.hpp"
|
||||
#include "theme.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
/* Fixed-height controller lane, separate from the vertically scrolled keys.
|
||||
* X coordinates follow the piano widget, including its horizontal scroll. */
|
||||
class VelocityLane : public Fl_Widget {
|
||||
PianoRollWidget &piano_;
|
||||
uint64_t editing_ = UINT64_MAX;
|
||||
unsigned int preview_ = 100;
|
||||
const xtc_project *editing_project_ = nullptr;
|
||||
uint64_t editing_clip_ = UINT64_MAX;
|
||||
unsigned int value(int py) const {
|
||||
return static_cast<unsigned int>(std::clamp((y()+h()-8-py)*127/std::max(1,h()-34),1,127));
|
||||
}
|
||||
public:
|
||||
VelocityLane(int x, int y, int w, int h, PianoRollWidget &piano)
|
||||
: Fl_Widget(x,y,w,h,"Note velocity"), piano_(piano) {}
|
||||
void draw() override {
|
||||
fl_push_clip(x(),y(),w(),h());
|
||||
fl_color(xtc_ui::background()); fl_rectf(x(),y(),w(),h());
|
||||
fl_color(xtc_ui::border()); fl_line(x(),y(),x()+w(),y());
|
||||
fl_font(FL_HELVETICA,11); fl_color(xtc_ui::muted());
|
||||
fl_draw("VELOCITY 1..127 / Select a note, then drag its bar or use the velocity slider",x()+8,y()+17);
|
||||
size_t count = 0; xtc_project_note_count(piano_.project(),&count);
|
||||
for (int pass = 0; pass < 2; ++pass) for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e);
|
||||
if (!piano_.note_visible(note) || (note.id == piano_.selected_note()) != (pass == 1)) continue;
|
||||
int px = piano_.note_x(note.start_tick);
|
||||
unsigned int velocity = editing_ == note.id ? preview_ : note.velocity;
|
||||
int top = y()+h()-8-static_cast<int>(velocity)*(h()-34)/127;
|
||||
fl_color(note.id == piano_.selected_note() ? xtc_ui::accent() : fl_rgb_color(95,137,172));
|
||||
fl_rectf(px-2,top,4,y()+h()-8-top); fl_rectf(px-4,top-2,8,4);
|
||||
}
|
||||
fl_pop_clip();
|
||||
}
|
||||
int handle(int event) override {
|
||||
if (event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE && Fl::event_y() >= y()+24) {
|
||||
take_focus(); editing_ = UINT64_MAX;
|
||||
size_t count = 0; int distance = 9;
|
||||
xtc_project_note_count(piano_.project(),&count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e);
|
||||
if (!piano_.note_visible(note)) continue;
|
||||
int d = std::abs(Fl::event_x()-piano_.note_x(note.start_tick));
|
||||
if (d < distance || (d == distance && note.id == piano_.selected_note())) {
|
||||
distance = d; editing_ = note.id;
|
||||
}
|
||||
}
|
||||
if (editing_ != UINT64_MAX) {
|
||||
editing_project_ = piano_.project(); editing_clip_ = piano_.clip();
|
||||
piano_.select_note(editing_); preview_ = value(Fl::event_y());
|
||||
}
|
||||
redraw(); piano_.redraw(); return 1;
|
||||
}
|
||||
if (event == FL_DRAG && editing_ != UINT64_MAX) {
|
||||
preview_ = value(Fl::event_y()); redraw(); return 1;
|
||||
}
|
||||
if (event == FL_RELEASE && editing_ != UINT64_MAX) {
|
||||
uint64_t id = editing_; editing_ = UINT64_MAX;
|
||||
if (editing_project_ == piano_.project() && editing_clip_ == piano_.clip())
|
||||
(void)piano_.set_velocity(id,value(Fl::event_y()));
|
||||
redraw(); return 1;
|
||||
}
|
||||
if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { editing_ = UINT64_MAX; redraw(); return 1; }
|
||||
if (event == FL_FOCUS || event == FL_UNFOCUS) return 1;
|
||||
return Fl_Widget::handle(event);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include "../../src/gui/plugin_dialog.hpp"
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <unistd.h>
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
|
|
@ -845,6 +846,19 @@ int main(int argc, char **argv)
|
|||
CHECK(window.save_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
|
||||
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK && spec.instrument);
|
||||
CHECK(window.create_track(true) == XTC_OK);
|
||||
uint64_t second = sequence->selected_track();
|
||||
auto *selector = sequence->instrument_choice(second); CHECK(selector && selector->value() == 0);
|
||||
CHECK(selector->size() == 4); // Sine, current project instrument, load, terminator.
|
||||
selector->value(1); selector->do_callback();
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_OK && spec.instrument);
|
||||
CHECK(sequence->selected_track() == second);
|
||||
CHECK(sequence->move_track(-1) == XTC_OK);
|
||||
selector = sequence->instrument_choice(second); selector->value(0); selector->do_callback();
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_ERR_NOT_FOUND);
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_OK);
|
||||
CHECK(window.assign_instrument(track,nullptr,0) == XTC_OK);
|
||||
unlink("/tmp/xtc-gui-instrument.xtc"); window.do_callback();
|
||||
}
|
||||
|
|
@ -892,6 +906,64 @@ int main(int argc, char **argv)
|
|||
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
|
||||
unlink("/tmp/xtc-workspace.xtc"); window.do_callback();
|
||||
}
|
||||
reset();
|
||||
{
|
||||
MainWindow window(false); window.show(); auto *sequence = timeline(&window);
|
||||
CHECK(window.create_track(true) == XTC_OK);
|
||||
uint64_t clip = sequence->selected_clip(), track = sequence->selected_track();
|
||||
auto *snap = dynamic_cast<Fl_Choice *>(find(&window,"Snap")); CHECK(snap && snap->value() == 3);
|
||||
CHECK(!std::strcmp(snap->text(3),"1/16") && !std::strcmp(snap->text(8),"1 bar"));
|
||||
snap->value(5); snap->do_callback(); CHECK(sequence->grid() == 480);
|
||||
CHECK(xtc_project_edit_clip(sequence->project(),clip,track,100,3840,0) == XTC_OK);
|
||||
CHECK(sequence->rename_selection("Off-grid") == XTC_OK);
|
||||
gesture(sequence,205,50,100,0);
|
||||
xtc_clip_info info{}; xtc_project_find_clip(sequence->project(),clip,&info);
|
||||
CHECK(info.start_tick == 1920); // Snap absolute position, including an off-grid source.
|
||||
snap->value(0); snap->do_callback(); CHECK(sequence->grid() == 1);
|
||||
gesture(sequence,300,50,1,0); xtc_project_find_clip(sequence->project(),clip,&info);
|
||||
CHECK(info.start_tick == 1940); // Off retains pointer precision rather than a musical grid.
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(sequence->select(track,clip));
|
||||
snap->value(5); snap->do_callback();
|
||||
gesture(sequence,466,50,-13,0); xtc_project_find_clip(sequence->project(),clip,&info);
|
||||
CHECK(info.start_tick+info.length_tick == 5280);
|
||||
gesture(sequence,277,50,13,0); xtc_project_find_clip(sequence->project(),clip,&info);
|
||||
CHECK(info.start_tick == 2400 && info.offset_tick == 480 && info.length_tick == 2880);
|
||||
int old_x = Fl::e_x, old_y = Fl::e_y, old_key = Fl::e_keysym, old_clicks = Fl::e_clicks;
|
||||
Fl::e_x = sequence->x()+320; Fl::e_y = sequence->y()+50;
|
||||
Fl::e_keysym = FL_Button+FL_LEFT_MOUSE; Fl::e_clicks = 1;
|
||||
CHECK(sequence->handle(FL_PUSH) == 1); CHECK(window.piano_roll() && window.piano_roll()->clip() == clip);
|
||||
Fl::e_x = old_x; Fl::e_y = old_y; Fl::e_keysym = old_key; Fl::e_clicks = old_clicks;
|
||||
auto *piano = window.piano_roll();
|
||||
CHECK(piano->add_note(0,960,60,100,2) == XTC_OK);
|
||||
uint64_t note_id = piano->selected_note();
|
||||
CHECK(piano->add_note(0,960,64,90,2) == XTC_OK); // Coincident notes remain individually editable.
|
||||
uint64_t chord_id = piano->selected_note();
|
||||
CHECK(piano->select_note(note_id)); pump();
|
||||
auto *velocity = dynamic_cast<Fl_Value_Slider *>(find(piano->window(),"Note velocity"));
|
||||
CHECK(velocity && velocity->active()); velocity->value(29); velocity->do_callback();
|
||||
xtc_note_info note{}, chord{};
|
||||
for (size_t i = 0; i < 2; ++i) { xtc_note_info n{}; xtc_project_get_note(sequence->project(),i,&n);
|
||||
if (n.id == note_id) note = n;
|
||||
if (n.id == chord_id) chord = n;
|
||||
}
|
||||
CHECK(note.velocity == 29 && note.start_tick == 0 && note.length_tick == 960 && note.pitch == 60 && note.channel == 2);
|
||||
CHECK(chord.velocity == 90);
|
||||
CHECK(piano->set_velocity(note_id,0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(piano->set_velocity(note_id,128) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(piano->set_velocity(UINT64_MAX,40) == XTC_ERR_NOT_FOUND);
|
||||
auto *lane = window.velocity_lane(); CHECK(lane);
|
||||
gesture(lane,piano->note_x(0)-lane->x(),70,0,-30);
|
||||
xtc_project_get_note(sequence->project(),0,¬e); CHECK(note.id == note_id && note.velocity > 29);
|
||||
xtc_project_get_note(sequence->project(),1,&chord); CHECK(chord.id == chord_id && chord.velocity == 90);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
xtc_project_get_note(sequence->project(),0,¬e); CHECK(note.velocity == 29);
|
||||
CHECK(window.undo(true) == XTC_OK);
|
||||
CHECK(window.save_project("/tmp/xtc-grid-velocity.xtc") == XTC_OK);
|
||||
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-grid-velocity.xtc") == XTC_OK);
|
||||
xtc_project_get_note(sequence->project(),0,¬e); CHECK(note.velocity > 29 && note.pitch == 60 && note.channel == 2);
|
||||
unlink("/tmp/xtc-grid-velocity.xtc"); window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
|
||||
return 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue