Seperating the sequencer into multiple widgets.
This commit is contained in:
parent
896a7907d3
commit
e664414796
23 changed files with 1455 additions and 914 deletions
|
|
@ -57,7 +57,7 @@
|
|||
|
||||
- **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph.
|
||||
- **C core:** project model, graph, DSP and audio engine are implemented in C.
|
||||
- **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API.
|
||||
- **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API. The sequencer coordinates nested ruler, track, header, clip and velocity widgets; see [GUI ownership](docs/ARCHITECTURE.md#sequencer-widget-ownership).
|
||||
- **Linux first:** keep backend-specific details behind an interface.
|
||||
- **Small increments:** build and test each milestone before adding features.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ endif()
|
|||
enable_language(CXX)
|
||||
set(XTC_GUI_AVAILABLE TRUE)
|
||||
add_library(xtc-gui STATIC src/gui/main_window.cpp src/gui/graph_widget.cpp
|
||||
src/gui/sequencer_widget.cpp src/gui/piano_roll.cpp src/gui/mixer_widget.cpp
|
||||
src/gui/sequencer_widget.cpp src/gui/sequencer_commands.cpp
|
||||
src/gui/sequencer_track_widget.cpp src/gui/sequencer_clip_widget.cpp
|
||||
src/gui/sequencer_velocity_widget.cpp src/gui/sequencer_ruler_widget.cpp
|
||||
src/gui/sequencer_instruments.cpp src/gui/sequencer_waveforms.cpp
|
||||
src/gui/piano_roll.cpp src/gui/mixer_widget.cpp
|
||||
src/gui/plugin_dialog.cpp src/gui/sample_editor.cpp)
|
||||
xtc_warnings(xtc-gui)
|
||||
target_link_libraries(xtc-gui PUBLIC xtc-core ${xtc_fltk})
|
||||
|
|
@ -54,6 +58,7 @@ if(XTC_XVFB_RUN AND BUILD_TESTING)
|
|||
set_tests_properties(${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS gui ENVIRONMENT "${ARGN}")
|
||||
endfunction()
|
||||
xtc_gui_test(gui)
|
||||
xtc_gui_test(gui-sequencer "XTC_TEST_SEQUENCER_COMPONENTS_ONLY=1")
|
||||
if(XTC_LADSPA_AVAILABLE)
|
||||
xtc_gui_test(gui-plugins "XTC_TEST_PLUGIN=$<TARGET_FILE:plugin-fixture>")
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -65,6 +65,63 @@ commands and publish independent compiled snapshots. See GUI.md.
|
|||
|
||||
---
|
||||
|
||||
## Sequencer Widget Ownership
|
||||
|
||||
`SequencerWidget` is the master FLTK group. It owns the timeline coordinate
|
||||
system, grid/background, selection, zoom, scrolling extent, expansion state,
|
||||
shared edit hooks and gestures that cross tracks. Drawing and local input belong
|
||||
to child widgets:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S[SequencerWidget] --> R[SequencerRulerWidget]
|
||||
S --> T[SequencerTrackWidget per track]
|
||||
T --> H[SequencerTrackHeader]
|
||||
H --> I[Fl_Choice instrument selector]
|
||||
T --> C[SequencerClipWidget per clip]
|
||||
T --> V[SequencerVelocityWidget]
|
||||
```
|
||||
|
||||
FLTK groups own and destroy their children. The master's track index and each
|
||||
track's clip index contain borrowed pointers; they do not independently delete
|
||||
children. Views reconcile copied C metadata by stable IDs when edits, project
|
||||
installation or drawing refresh the timeline. Surviving track views retain their
|
||||
identity through reorder and undo; clips moved between tracks acquire a view
|
||||
under their destination group. No child keeps a borrowed pointer into a C
|
||||
project snapshot. Removed views are detached, hidden and scheduled with
|
||||
`Fl::delete_widget`, allowing the current event handler to return safely.
|
||||
A project replacement cancels captured gestures before refreshing the hierarchy.
|
||||
|
||||
The ruler handles seeking and loop selection. Track groups manage row layout;
|
||||
headers handle disclosure/selection and own instrument controls. Clip widgets
|
||||
draw notes or cached waveforms and start selection/move/trim gestures; the
|
||||
master completes cross-track edits. Velocity children own MIDI/audio previews
|
||||
and local point/bar hit testing. All mutations still use the master's
|
||||
before-edit and changed callbacks, preserving C history, transport stopping and
|
||||
plan publication. Instrument keyboard focus is handled before timeline shortcuts.
|
||||
|
||||
Implementation map:
|
||||
|
||||
| Files in `src/gui/` | Responsibility |
|
||||
| --- | --- |
|
||||
| `sequencer_widget.hpp/.cpp` | Master API, layout/reconciliation, shared selection, gestures and timeline overlays |
|
||||
| `sequencer_track_widget.hpp/.cpp` | Track groups, pinned header groups and nested instrument selectors |
|
||||
| `sequencer_clip_widget.hpp/.cpp` | Clip drawing and local selection/gesture initiation |
|
||||
| `sequencer_velocity_widget.hpp/.cpp` | MIDI velocity bars and continuous audio gain envelopes, including preview state |
|
||||
| `sequencer_ruler_widget.hpp/.cpp` | Bar/marker labels, loop display, seeking and loop gestures |
|
||||
| `sequencer_commands.cpp` | Editing commands and dialogs exposed by the master |
|
||||
| `sequencer_instruments.cpp` | Instrument catalog and selector callbacks |
|
||||
| `sequencer_waveforms.cpp` | Shared control-thread waveform cache and waveform drawing |
|
||||
| `sequencer_group_scope.hpp` | Restore FLTK's implicit parent around dynamic child construction |
|
||||
|
||||
`cmake/Gui.cmake` lists all implementation files explicitly in `xtc-gui`.
|
||||
The hierarchy adds no dependency, C API, project format or audio-thread work.
|
||||
GUI integration checks cover actual parent relationships, stable track views,
|
||||
FLTK window-dispatched clip/velocity drags, cross-track movement, deferred
|
||||
removal, project replacement during a captured gesture and undo/redo.
|
||||
|
||||
---
|
||||
|
||||
## Editable Graph and Canvas
|
||||
|
||||
Read-only C snapshots, monotonic node IDs, and a small set of C edit commands
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ cmake --build build --target analyze
|
|||
cmake --build build --target quality-preview builtin-preview
|
||||
```
|
||||
|
||||
The `xtc-gui` target explicitly includes the sequencer master, track/header,
|
||||
clip, velocity and ruler implementations, plus command, instrument and waveform
|
||||
support files. See the [component map](ARCHITECTURE.md#sequencer-widget-ownership)
|
||||
when adding GUI sources; update `cmake/Gui.cmake` alongside new implementation files.
|
||||
|
||||
Each registered root test also has a `test-NAME` build target (for example
|
||||
`test-core`, `test-gui`, `test-track-fx`). `test-builtins` aliases `test-builtin`.
|
||||
`gui` builds the optional main application. Preview targets write WAVs below the
|
||||
|
|
|
|||
19
docs/GUI.md
19
docs/GUI.md
|
|
@ -148,6 +148,25 @@ Tests cover C-model/persistence boundaries, GUI move/trim gestures and zoom,
|
|||
save/reopen, failed load/submission rollback, sample-derived cursor updates, and
|
||||
live JACK playhead advance/pause.
|
||||
|
||||
### Sequencer components
|
||||
|
||||
The sequencer is a master `Fl_Group` containing a ruler and one group per track.
|
||||
Each track contains clip widgets, a velocity lane, and a pinned header group
|
||||
with its instrument selector. Local drawing and input live with those children;
|
||||
the master coordinates selection, scrolling, zoom and cross-track edits.
|
||||
See [widget ownership and source files](ARCHITECTURE.md#sequencer-widget-ownership)
|
||||
for the implementation map and lifetime rules.
|
||||
|
||||
Click the small track-header arrow to expand a track downward. MIDI tracks
|
||||
expose note velocity bars (1–127); drag a cap to edit a note. Audio tracks expose
|
||||
the saved track-gain envelope (0–400%): click at any timeline position to add a
|
||||
point, drag vertically to adjust it, or right-click a cap to remove it. Audio
|
||||
positions ignore the snap grid. Escape cancels a preview; undo/redo restores
|
||||
committed edits. Instrument selectors retain their own keyboard focus.
|
||||
Expansion is temporary view state; audio gain points are shared with the mixer
|
||||
and saved with the project. This component refactor preserves the existing
|
||||
workflow and project format.
|
||||
|
||||
---
|
||||
|
||||
## Audio Workflow
|
||||
|
|
|
|||
|
|
@ -216,6 +216,9 @@ xtc/
|
|||
└── build/ # generated; do not commit
|
||||
```
|
||||
|
||||
The implemented sequencer now uses nested track, header, clip, velocity and
|
||||
ruler widgets; see the [current source and ownership map](ARCHITECTURE.md#sequencer-widget-ownership).
|
||||
|
||||
Keep C and C++ files clearly separated. C headers exposed to C++ must use
|
||||
`extern "C"` guards.
|
||||
|
||||
|
|
|
|||
|
|
@ -3656,6 +3656,55 @@ and inspection of the actual [audio envelope capture](../screenshots/audio-veloc
|
|||
|
||||
---
|
||||
|
||||
## Phase 111 — Sequencer Master and Child Widgets
|
||||
|
||||
### Goal
|
||||
|
||||
Split the sequencer GUI into focused components. Keep `SequencerWidget` as the
|
||||
master with real nested FLTK track, header, clip, velocity and ruler widgets,
|
||||
preserving current editing behavior, the C domain boundary and saved projects.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
|
||||
- [x] Make the master own ruler and track groups; each track owns its header,
|
||||
instrument selector, clip widgets and velocity lane through FLTK ownership.
|
||||
- [x] Move child drawing and local input into dedicated track/header, clip,
|
||||
velocity and ruler implementation files. Keep shared coordinates, selection,
|
||||
scrolling and cross-track gesture completion in the master.
|
||||
- [x] Extract editing commands/dialogs, instrument support and waveform caching
|
||||
into separate source files and register them explicitly in CMake.
|
||||
- [x] Reconcile views by stable C IDs, preserving surviving track identity through
|
||||
reorder/undo and moving clip views into the destination track hierarchy.
|
||||
- [x] Cancel gestures on project replacement and defer removed widget deletion
|
||||
until event handlers can return; preserve instrument focus and shortcuts.
|
||||
- [x] Exercise real parent/child membership, cross-track movement, velocity edits,
|
||||
undo/redo, removal and project replacement through GUI integration tests,
|
||||
including FLTK window-level event dispatch.
|
||||
- [x] Update the architecture/component map, GUI guide, build guidance, README
|
||||
and integration-test documentation.
|
||||
|
||||
### Implementation status
|
||||
|
||||
Implemented on 2026-11-06. The master now delegates track/header, clip, velocity
|
||||
and ruler drawing/input to child classes; commands and support code are separate
|
||||
translation units. See [the ownership and source map](ARCHITECTURE.md#sequencer-widget-ownership).
|
||||
FLTK owns the hierarchy; indexes and captured-gesture pointers are borrowed.
|
||||
Child views hold copied C metadata rather than pointers into project snapshots.
|
||||
Removed children are detached/hidden and deleted through FLTK's deferred queue.
|
||||
The master remains the single selection and edit/publication coordinator.
|
||||
|
||||
No dependency, core API, persistence or intended visual/workflow change is
|
||||
introduced. Existing screenshots continue to describe the same interface.
|
||||
Validation: the warning-enabled CMake build succeeds, all 55 existing CTests
|
||||
pass, and the added `gui-sequencer` hierarchy/dispatched-gesture check passes.
|
||||
The final build's main GUI and focused hierarchy tests were also rerun successfully.
|
||||
The focused hierarchy/lifetime test passes Clang ASan/UBSan with leak detection
|
||||
disabled for the unsanitized system FLTK library. Both audio-envelope captures
|
||||
remain pixel-identical to the existing gallery. Physical hardware qualification
|
||||
is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
Reorder the phases when the audit reveals a blocking dependency. Do not mark a
|
||||
|
|
|
|||
79
src/gui/sequencer_clip_widget.cpp
Normal file
79
src/gui/sequencer_clip_widget.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#include "sequencer_clip_widget.hpp"
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "theme.hpp"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
|
||||
SequencerClipWidget::SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip)
|
||||
: Fl_Widget(0,0,1,46), owner_(owner), info_(clip) {}
|
||||
void SequencerClipWidget::sync(const xtc_clip_info &clip, int top)
|
||||
{
|
||||
info_=clip;
|
||||
int left=owner_.x()+owner_.header_width_+static_cast<int>(clip.start_tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
int width=std::max(4,static_cast<int>(clip.length_tick*static_cast<unsigned int>(owner_.scale_)/960U));
|
||||
resize(left,top+5,width,46);
|
||||
}
|
||||
void SequencerClipWidget::draw()
|
||||
{
|
||||
const auto &c=info_;
|
||||
int left=x(),py=y()-5,cx,cy,cw,ch;
|
||||
fl_clip_box(x(),y(),w(),h(),cx,cy,cw,ch);
|
||||
int a=std::max(left,cx),b=std::min(x()+w(),cx+cw);
|
||||
if(b<=a) return;
|
||||
size_t notes=0; xtc_project_note_count(owner_.project_,¬es);
|
||||
Fl_Color color=xtc_ui::track(c.track_id);
|
||||
fl_color(fl_color_average(color,xtc_ui::background(),owner_.clip_selected(c.id) ? .75f : .45f));
|
||||
fl_rectf(a,py+5,b-a,46);
|
||||
fl_color(owner_.clip_selected(c.id) ? xtc_ui::text() : color); fl_rect(a,py+5,b-a,46);
|
||||
fl_push_clip(a+1,py+6,std::max(0,b-a-2),44);
|
||||
if (c.midi) {
|
||||
/* Real note positions, pitch and duration; no placeholder waveform. */
|
||||
unsigned int low = 127, high = 0;
|
||||
for (size_t n = 0; n < notes; ++n) {
|
||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e);
|
||||
if (note.clip_id == c.id) { low = std::min(low,note.pitch); high = std::max(high,note.pitch); }
|
||||
}
|
||||
unsigned int range = high >= low ? std::max(12U,high-low) : 12U;
|
||||
fl_color(fl_color_average(color,FL_WHITE,.55f));
|
||||
for (size_t n = 0; n < notes; ++n) {
|
||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e);
|
||||
if (note.clip_id != c.id) continue;
|
||||
uint64_t begin = std::max(note.start_tick,c.offset_tick);
|
||||
uint64_t end = std::min(note.start_tick+note.length_tick,c.offset_tick+c.length_tick);
|
||||
if (begin >= end) continue;
|
||||
int nx = left+static_cast<int>((begin-c.offset_tick)*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
int nw = std::max(2,static_cast<int>((end-begin)*static_cast<unsigned int>(owner_.scale_)/960U));
|
||||
fl_rectf(nx,py+26+static_cast<int>((high-note.pitch)*18/range),nw,2);
|
||||
}
|
||||
} else owner_.draw_waveform(c,left,py);
|
||||
fl_color(fl_color_average(color,xtc_ui::background(),.35f)); fl_rectf(a+1,py+6,std::max(0,b-a-2),15);
|
||||
fl_font(FL_HELVETICA,11); fl_color(xtc_ui::text());
|
||||
fl_draw(c.name,left+7,py+18);
|
||||
fl_pop_clip();
|
||||
}
|
||||
|
||||
int SequencerClipWidget::handle(int event)
|
||||
{
|
||||
if(event==FL_PUSH && (Fl::event_button()==FL_LEFT_MOUSE || Fl::event_button()==FL_RIGHT_MOUSE)) {
|
||||
owner_.take_focus();
|
||||
uint64_t previous=owner_.clip_;
|
||||
owner_.track_=info_.track_id; owner_.clip_=info_.id;
|
||||
owner_.original_=info_; owner_.drag_x_=Fl::event_x();
|
||||
owner_.mode_=Fl::event_x()-x()<7 ? 2 : x()+w()-Fl::event_x()<7 ? 3 : 1;
|
||||
if(Fl::event_state(FL_SHIFT)) {
|
||||
owner_.clip_=previous; owner_.toggle_clip(info_.id); owner_.mode_=0; return 1;
|
||||
}
|
||||
owner_.selection_.clear();
|
||||
if(Fl::event_button()==FL_LEFT_MOUSE && Fl::event_clicks()) {
|
||||
owner_.mode_=0;
|
||||
if(owner_.open_clip) owner_.open_clip(owner_.context_);
|
||||
return 1;
|
||||
}
|
||||
if(Fl::event_button()==FL_RIGHT_MOUSE) { owner_.mode_=0; owner_.context_menu(); }
|
||||
owner_.redraw(); return 1;
|
||||
}
|
||||
if(event==FL_DRAG || event==FL_RELEASE) return owner_.handle(event);
|
||||
return 0;
|
||||
}
|
||||
17
src/gui/sequencer_clip_widget.hpp
Normal file
17
src/gui/sequencer_clip_widget.hpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#ifndef XTC_SEQUENCER_CLIP_WIDGET_HPP
|
||||
#define XTC_SEQUENCER_CLIP_WIDGET_HPP
|
||||
#include <FL/Fl_Widget.H>
|
||||
#include "xtc/xtc_project.h"
|
||||
class SequencerWidget;
|
||||
class SequencerClipWidget : public Fl_Widget {
|
||||
SequencerWidget &owner_;
|
||||
xtc_clip_info info_{};
|
||||
public:
|
||||
SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip);
|
||||
uint64_t clip_id() const { return info_.id; }
|
||||
void sync(const xtc_clip_info &clip, int track_top);
|
||||
int handle(int event) override;
|
||||
protected:
|
||||
void draw() override;
|
||||
};
|
||||
#endif
|
||||
412
src/gui/sequencer_commands.cpp
Normal file
412
src/gui/sequencer_commands.cpp
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
#include "sequencer_widget.hpp"
|
||||
#include "snap.hpp"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include <FL/fl_ask.H>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <new>
|
||||
|
||||
// User-facing editing commands use the master's single history/publication path.
|
||||
|
||||
xtc_result SequencerWidget::add_track()
|
||||
{
|
||||
if (before_edit) {
|
||||
xtc_result r = before_edit(context_);
|
||||
if (r != XTC_OK) {
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
uint64_t id;
|
||||
xtc_result r = xtc_project_add_track(project_, "Track", &id);
|
||||
if (r == XTC_OK) {
|
||||
track_ = id;
|
||||
clip_ = UINT64_MAX;selection_.clear();
|
||||
}
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::add_region()
|
||||
{
|
||||
if (before_edit) {
|
||||
xtc_result r = before_edit(context_);
|
||||
if (r != XTC_OK) {
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
size_t tracks, clips;
|
||||
xtc_project_get_counts(project_, &tracks, &clips);
|
||||
if (track_ == UINT64_MAX && tracks) {
|
||||
xtc_track_info t{};
|
||||
xtc_project_get_track(project_, 0, &t);
|
||||
track_ = t.id;
|
||||
}
|
||||
uint64_t id;
|
||||
xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id);
|
||||
if (r == XTC_OK) {clip_ = id;selection_.clear();}
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::add_midi_clip()
|
||||
{
|
||||
xtc_result r = before_edit ? before_edit(context_) : XTC_OK;
|
||||
uint64_t id;
|
||||
if (r == XTC_OK) {
|
||||
if (track_ == UINT64_MAX) {
|
||||
xtc_track_info t{};
|
||||
r = xtc_project_get_track(project_, 0, &t);
|
||||
track_ = r == XTC_OK ? t.id : UINT64_MAX;
|
||||
}
|
||||
if (r == XTC_OK)
|
||||
r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id);
|
||||
if (r == XTC_OK) {clip_ = id;selection_.clear();}
|
||||
}
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::delete_selection()
|
||||
{
|
||||
if(!selected_clips().empty())return transform_selection(XTC_CLIPS_DELETE,0);
|
||||
xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_remove_track(project_,track_);
|
||||
if(r==XTC_OK)track_=clip_=UINT64_MAX;
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::merge_midi()
|
||||
{
|
||||
auto ids=selected_clips();uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_merge_midi(project_,ids.data(),ids.size(),&id);
|
||||
if(r==XTC_OK){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);select(c.track_id,id);}
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::duplicate_track(const char *name)
|
||||
{
|
||||
const uint64_t source=track_;uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_duplicate_track(project_,source,name,&id);
|
||||
if(r==XTC_OK)select(id);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::duplicate_track_dialog()
|
||||
{
|
||||
const char *name=fl_input("Name for complete track duplicate","Track copy");if(name)duplicate_track(name);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::ripple_time(uint64_t length,bool remove)
|
||||
{
|
||||
const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_ripple_time(project_,at,length,remove);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::ripple_dialog(bool remove)
|
||||
{
|
||||
const char *input=fl_input(remove ? "Remove time from cursor: ticks (split crossing clips first)" : "Insert time at cursor: ticks (split crossing clips first)","3840");if(!input)return;
|
||||
uint64_t length=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>length) || (values>>extra) || !length || length>XTC_PROJECT_MAX_TICK){fl_alert("Enter a positive bounded tick duration.");return;}
|
||||
ripple_time(length,remove);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::marker(const char *name)
|
||||
{
|
||||
const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=name ? xtc_project_set_marker(project_,at,name) : xtc_project_remove_marker(project_,at);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::marker_dialog()
|
||||
{
|
||||
const char *name=fl_input("Marker at cursor (1..63 UTF-8 bytes)","Section");if(name)marker(name);
|
||||
}
|
||||
|
||||
std::vector<uint64_t> SequencerWidget::selected_clips() const
|
||||
{
|
||||
std::vector<uint64_t> ids;xtc_clip_info c{};
|
||||
for(auto id:selection_)if(xtc_project_find_clip(project_,id,&c)==XTC_OK)ids.push_back(id);
|
||||
if(ids.empty() && clip_!=UINT64_MAX && xtc_project_find_clip(project_,clip_,&c)==XTC_OK)ids.push_back(clip_);
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool SequencerWidget::clip_selected(uint64_t id) const
|
||||
{
|
||||
return selection_.empty() ? id==clip_ : std::find(selection_.begin(),selection_.end(),id)!=selection_.end();
|
||||
}
|
||||
|
||||
bool SequencerWidget::toggle_clip(uint64_t id)
|
||||
{
|
||||
xtc_clip_info c{};if(xtc_project_find_clip(project_,id,&c)!=XTC_OK)return false;
|
||||
if(selection_.empty() && clip_!=UINT64_MAX)selection_.push_back(clip_);
|
||||
auto found=std::find(selection_.begin(),selection_.end(),id);
|
||||
if(found==selection_.end())selection_.push_back(id);else selection_.erase(found);
|
||||
clip_=selection_.empty() ? UINT64_MAX : selection_.back();
|
||||
if(clip_!=UINT64_MAX)xtc_project_find_clip(project_,clip_,&c);
|
||||
track_=c.track_id;redraw();return true;
|
||||
}
|
||||
|
||||
void SequencerWidget::select_all_clips()
|
||||
{
|
||||
selection_.clear();size_t tracks=0,clips=0;xtc_project_get_counts(project_,&tracks,&clips);
|
||||
for(size_t i=0;i<clips;++i){xtc_clip_info c{};xtc_project_get_clip(project_,i,&c);selection_.push_back(c.id);}
|
||||
if(!selection_.empty()){xtc_clip_info c{};clip_=selection_.back();xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;}
|
||||
redraw();
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::grid(uint64_t ticks)
|
||||
{
|
||||
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
|
||||
grid_ = ticks; redraw(); return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::transform_selection(xtc_clip_operation op,int64_t amount)
|
||||
{
|
||||
auto ids=selected_clips();std::vector<uint64_t> output(ids.size());
|
||||
xtc_result r=ids.empty() ? XTC_ERR_NOT_FOUND : XTC_OK;
|
||||
if(r==XTC_OK && before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_project_transform_clips(project_,ids.data(),ids.size(),op,amount,output.data());
|
||||
if(r==XTC_OK) {
|
||||
selection_=op==XTC_CLIPS_DELETE ? std::vector<uint64_t>{} : output;
|
||||
clip_=selection_.empty() ? UINT64_MAX : selection_.back();
|
||||
}
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::transform_midi(const xtc_midi_transform &edit)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::fades_dialog()
|
||||
{
|
||||
const char *input=fl_input("Symmetric fade length in ticks (0 removes; at most half each clip)","10");if(!input)return;
|
||||
int64_t ticks=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>ticks) || (values>>extra) || ticks<0 || ticks>static_cast<int64_t>(XTC_PROJECT_MAX_TICK)){fl_alert("Enter a valid nonnegative tick length.");return;}
|
||||
transform_selection(XTC_CLIPS_FADE,ticks);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::make_chord(const std::vector<int> &intervals)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_chordify(project_,ids.data(),ids.size(),intervals.data(),intervals.size());
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::arpeggiate(uint64_t step,unsigned int gate,bool descending)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_arpeggiate(project_,ids.data(),ids.size(),step,gate,descending);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::normalize_audio(float target)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_normalize_audio(project_,ids.data(),ids.size(),target);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::normalize_dialog()
|
||||
{
|
||||
const char *input=fl_input("Source sample peak target in dBFS (-60..0; excludes fades and mixer)","-1");if(!input)return;
|
||||
float db=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>db) || (values>>extra) || !std::isfinite(db) || db < -60 || db>0){fl_alert("Enter a finite sample peak target from -60 to 0 dBFS.");return;}
|
||||
normalize_audio(std::pow(10.0f,db/20.0f));
|
||||
}
|
||||
|
||||
void SequencerWidget::arpeggio_dialog()
|
||||
{
|
||||
const char *input=fl_input("Step ticks (1..3840), gate percent (1..100), direction (0=up, 1=down)","240 80 0");if(!input)return;
|
||||
uint64_t step=0;unsigned int gate=0;int down=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>step>>gate>>down) || (values>>extra) || !step || step>3840 || !gate || gate>100 || down<0 || down>1){fl_alert("Enter a valid step, gate and direction.");return;}
|
||||
arpeggiate(step,gate,down!=0);
|
||||
}
|
||||
|
||||
void SequencerWidget::chord_dialog()
|
||||
{
|
||||
const char *input=fl_input("Additional semitone intervals: 1..8 unique nonzero values (-127..127)","4 7");if(!input)return;
|
||||
std::istringstream values(input);std::vector<int> intervals;int interval;
|
||||
while(values>>interval){intervals.push_back(interval);if(intervals.size()>8)break;}
|
||||
if(!values.eof() || intervals.empty() || intervals.size()>8){fl_alert("Enter 1 to 8 semitone intervals.");return;}
|
||||
make_chord(intervals);
|
||||
}
|
||||
|
||||
void SequencerWidget::humanize_dialog()
|
||||
{
|
||||
const char *input=fl_input("Timing variation ticks (0..960), velocity variation (0..127), seed","12 8 1");if(!input)return;
|
||||
unsigned int timing=0,velocity=0,seed=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>timing>>velocity>>seed) || (values>>extra) || timing>960 || velocity>127){fl_alert("Enter bounded timing/velocity variation and a 32-bit seed.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_HUMANIZE;edit.timing_variation=timing;edit.velocity_variation=velocity;edit.seed=seed;transform_midi(edit);
|
||||
}
|
||||
|
||||
void SequencerWidget::velocity_dialog()
|
||||
{
|
||||
const char *input=fl_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return;
|
||||
unsigned int percent=0;int offset=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>percent>>offset) || (values>>extra) || percent>400 || offset < -127 || offset>127){fl_alert("Enter a valid percentage and velocity offset.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=percent;edit.velocity_offset=offset;transform_midi(edit);
|
||||
}
|
||||
|
||||
void SequencerWidget::scale_dialog()
|
||||
{
|
||||
const char *input=fl_input("Root (0=C..11=B), scale (0=chromatic, 1=major, 2=natural minor)","0 1");if(!input)return;
|
||||
unsigned int root=0,scale=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>root>>scale) || (values>>extra) || root>11 || scale>2){fl_alert("Enter a root from 0 to 11 and scale from 0 to 2.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=root;edit.scale=scale;transform_midi(edit);
|
||||
}
|
||||
|
||||
void SequencerWidget::gate_dialog()
|
||||
{
|
||||
const char *input=fl_input("Note length percentage (1..400)","50");if(!input)return;
|
||||
unsigned int percent=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>percent) || (values>>extra) || percent<1 || percent>400){fl_alert("Enter a whole percentage from 1 to 400.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=percent;transform_midi(edit);
|
||||
}
|
||||
|
||||
void SequencerWidget::swing_dialog()
|
||||
{
|
||||
const char *input=fl_input("Grid ticks (1..3840), swing (0..100), strength (0..100)","240 50 100");if(!input)return;
|
||||
int grid=0,swing=-1,strength=-1;char extra;
|
||||
std::istringstream values(input);
|
||||
if(!(values>>grid>>swing>>strength) || (values>>extra) || grid<1 || grid>3840 || swing<0 || swing>100 || strength<0 || strength>100){fl_alert("Enter a valid grid, swing and strength.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SWING;edit.grid=static_cast<uint64_t>(grid);edit.swing=static_cast<unsigned int>(swing);edit.strength=static_cast<unsigned int>(strength);transform_midi(edit);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::repeat_selection(unsigned int copies)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_repeat_clips(project_,ids.data(),ids.size(),copies);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::repeat_dialog()
|
||||
{
|
||||
const char *input=fl_input("Additional phrase copies (1..1024)","3");if(!input)return;
|
||||
char *end=nullptr;unsigned long copies=std::strtoul(input,&end,10);
|
||||
if(end==input || *end || copies<1 || copies>1024){fl_alert("Enter a whole number from 1 to 1024.");return;}
|
||||
repeat_selection(static_cast<unsigned int>(copies));
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::copy_selection()
|
||||
{
|
||||
auto ids=selected_clips();xtc_clipboard *copy=nullptr;
|
||||
xtc_result r=xtc_clipboard_create(project_,ids.data(),ids.size(),©);
|
||||
if(r==XTC_OK)clipboard_.reset(copy);
|
||||
else fl_alert("Copy unchanged: %s",xtc_result_string(r));
|
||||
return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::paste_selection()
|
||||
{
|
||||
const uint64_t start=cursor_,track=track_;
|
||||
std::vector<uint64_t> output(xtc_clipboard_count(clipboard_.get()));
|
||||
xtc_result r=output.empty() ? XTC_ERR_INVALID_STATE : XTC_OK;
|
||||
if(r==XTC_OK && before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_clipboard_paste(clipboard_.get(),project_,track,start,output.data());
|
||||
if(r==XTC_OK){selection_=output;clip_=selection_.back();xtc_clip_info c{};xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;}
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::nudge_selection(int direction)
|
||||
{
|
||||
return transform_selection(XTC_CLIPS_MOVE,direction<0 ? -static_cast<int64_t>(grid_) : static_cast<int64_t>(grid_));
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::duplicate_selection()
|
||||
{
|
||||
auto ids=selected_clips();uint64_t begin=XTC_PROJECT_MAX_TICK,end=0;
|
||||
for(auto id:ids){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);begin=std::min(begin,c.start_tick);end=std::max(end,c.start_tick+c.length_tick);}
|
||||
return transform_selection(XTC_CLIPS_DUPLICATE,static_cast<int64_t>(end)-static_cast<int64_t>(begin));
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::rename_selection(const char *name)
|
||||
{
|
||||
xtc_result r = before_edit ? before_edit(context_) : XTC_OK;
|
||||
if (r == XTC_OK) r = clip_ != UINT64_MAX ? xtc_project_rename_clip(project_,clip_,name) : xtc_project_rename_track(project_,track_,name);
|
||||
changed(r); return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::move_track(int direction)
|
||||
{
|
||||
size_t tracks, clips, i; xtc_project_get_counts(project_,&tracks,&clips);
|
||||
xtc_track_info track{};
|
||||
for (i = 0; i < tracks; ++i) { xtc_project_get_track(project_,i,&track); if (track.id == track_) break; }
|
||||
if (i == tracks || (direction < 0 ? i == 0 : i+1 == tracks)) return XTC_ERR_NOT_FOUND;
|
||||
xtc_result r = before_edit ? before_edit(context_) : XTC_OK;
|
||||
if (r == XTC_OK) r = xtc_project_move_track(project_,track_,direction < 0 ? i-1 : i+1);
|
||||
changed(r); return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::split_selection()
|
||||
{
|
||||
uint64_t right;
|
||||
const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead.
|
||||
xtc_result result = before_edit ? before_edit(context_) : XTC_OK;
|
||||
if (result == XTC_OK) {
|
||||
xtc_clip_info c{};result=xtc_project_find_clip(project_,clip_,&c);
|
||||
if(result==XTC_OK)result=c.midi ? xtc_project_split_midi(project_,clip_,split_tick,&right) : xtc_project_split_audio(project_,clip_,split_tick,&right);
|
||||
}
|
||||
if (result == XTC_OK) {clip_ = right;selection_.clear();}
|
||||
changed(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::audio_properties(float gain, uint64_t in, uint64_t out)
|
||||
{
|
||||
xtc_result result = before_edit ? before_edit(context_) : XTC_OK;
|
||||
if (result == XTC_OK) result = xtc_project_set_clip_audio(project_, clip_, gain, in, out);
|
||||
changed(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void SequencerWidget::audio_properties_dialog()
|
||||
{
|
||||
xtc_clip_info clip{};
|
||||
if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || clip.midi) {
|
||||
fl_alert("Select an audio clip or silent region first.");
|
||||
return;
|
||||
}
|
||||
char defaults[128];
|
||||
std::snprintf(defaults, sizeof(defaults), "%.9g %llu %llu", static_cast<double>(clip.gain),
|
||||
static_cast<unsigned long long>(clip.fade_in_tick), static_cast<unsigned long long>(clip.fade_out_tick));
|
||||
const char *text = fl_input("Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables)", defaults);
|
||||
if (!text) return;
|
||||
std::istringstream input(text);
|
||||
float gain; uint64_t in, out;
|
||||
if (!(input >> gain >> in >> out) || !(input >> std::ws).eof()) {
|
||||
fl_alert("Enter gain and two fade durations in ticks."); return;
|
||||
}
|
||||
audio_properties(gain, in, out);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::crossfade_selection()
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=ids.size()==2 ? XTC_OK : XTC_ERR_INVALID_ARGUMENT;
|
||||
if(r==XTC_OK && before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_project_crossfade(project_,ids[0],ids[1]);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::strum_chords(uint64_t spacing,bool descending)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=XTC_OK;
|
||||
if(before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_project_strum(project_,ids.data(),ids.size(),spacing,descending ? 1 : 0);
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
void SequencerWidget::strum_dialog()
|
||||
{
|
||||
const char *input=fl_input("Spacing ticks (1..960), direction (0=up, 1=down)","30 0");if(!input)return;
|
||||
uint64_t spacing=0;int down=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>spacing>>down) || (values>>extra) || !spacing || spacing>960 || down<0 || down>1){fl_alert("Enter a valid spacing and direction.");return;}
|
||||
strum_chords(spacing,down!=0);
|
||||
}
|
||||
12
src/gui/sequencer_group_scope.hpp
Normal file
12
src/gui/sequencer_group_scope.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef XTC_SEQUENCER_GROUP_SCOPE_HPP
|
||||
#define XTC_SEQUENCER_GROUP_SCOPE_HPP
|
||||
#include <FL/Fl_Group.H>
|
||||
class SequencerGroupScope {
|
||||
Fl_Group *previous_;
|
||||
public:
|
||||
explicit SequencerGroupScope(Fl_Group *group) : previous_(Fl_Group::current()) { Fl_Group::current(group); }
|
||||
~SequencerGroupScope() { Fl_Group::current(previous_); }
|
||||
SequencerGroupScope(const SequencerGroupScope &) = delete;
|
||||
SequencerGroupScope &operator=(const SequencerGroupScope &) = delete;
|
||||
};
|
||||
#endif
|
||||
63
src/gui/sequencer_instruments.cpp
Normal file
63
src/gui/sequencer_instruments.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include "sequencer_widget.hpp"
|
||||
#include "sequencer_track_widget.hpp"
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
Fl_Choice *SequencerWidget::instrument_choice(uint64_t track) const
|
||||
{
|
||||
auto *view=track_widget(track);
|
||||
return view && !view->is_bus() ? view->instrument_choice() : nullptr;
|
||||
}
|
||||
|
||||
void SequencerWidget::refresh_instruments() try
|
||||
{
|
||||
sync_views();
|
||||
size_t tracks = 0, clips = 0;
|
||||
xtc_project_get_counts(project_,&tracks,&clips);
|
||||
catalog_.clear();
|
||||
for (unsigned int i=0;i<2;++i) { xtc_plugin_spec s{}; if (xtc_plugin_probe(XTC_BUILTIN_PATH,i,48000,&s)==XTC_OK) catalog_.push_back(s); }
|
||||
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(auto *view:track_widgets_) {
|
||||
auto *choice=view->instrument_choice();
|
||||
xtc_track_info t{};
|
||||
for(size_t i=0;i<tracks;++i) {xtc_project_get_track(project_,i,&t); if(t.id==view->track_id()) break;}
|
||||
if(t.is_bus) {choice->hide(); continue;}
|
||||
choice->clear(); choice->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 = choice->add("Instrument"); choice->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;
|
||||
}
|
||||
choice->add("Load plugin..."); choice->value(selected); choice->show();
|
||||
}
|
||||
} catch (const std::bad_alloc &) {
|
||||
catalog_.clear();
|
||||
for(auto *view:track_widgets_) view->instrument_choice()->hide();
|
||||
}
|
||||
|
||||
void SequencerWidget::instrument_callback(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *owner = static_cast<SequencerWidget *>(context);
|
||||
auto *choice = static_cast<Fl_Choice *>(widget);
|
||||
for (auto *view : owner->track_widgets_) if (view->instrument_choice() == choice) {
|
||||
uint64_t track = view->track_id();
|
||||
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;
|
||||
}
|
||||
}
|
||||
68
src/gui/sequencer_ruler_widget.cpp
Normal file
68
src/gui/sequencer_ruler_widget.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#include "sequencer_ruler_widget.hpp"
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "theme.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
SequencerRulerWidget::SequencerRulerWidget(SequencerWidget &owner)
|
||||
: Fl_Widget(owner.x(),owner.y(),owner.w(),28), owner_(owner) {}
|
||||
void SequencerRulerWidget::draw()
|
||||
{
|
||||
int cx,cy,cw,ch; fl_clip_box(x(),y(),w(),h(),cx,cy,cw,ch);
|
||||
fl_push_clip(x(),y(),w(),h());
|
||||
fl_font(FL_HELVETICA,11);
|
||||
int first=std::max(0,(cx-owner_.x()-owner_.header_width_)/(owner_.scale_*4));
|
||||
int last=(cx+cw-owner_.x()-owner_.header_width_)/(owner_.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,owner_.x()+owner_.header_width_+bar*owner_.scale_*4+6,y()+19);
|
||||
}
|
||||
uint64_t begin=owner_.mode_==4 ? std::min(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_begin_;
|
||||
uint64_t end=owner_.mode_==4 ? std::max(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_end_;
|
||||
int left=owner_.x()+owner_.header_width_+static_cast<int>(begin*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
int right=owner_.x()+owner_.header_width_+static_cast<int>(end*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
fl_color(owner_.loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); fl_rectf(left,y()+23,std::max(0,right-left),4);
|
||||
size_t markers=0; xtc_project_marker_count(owner_.project_,&markers);
|
||||
for(size_t i=0;i<markers;++i) {
|
||||
xtc_marker marker{}; xtc_project_get_marker(owner_.project_,i,&marker);
|
||||
int px=owner_.x()+owner_.header_width_+static_cast<int>(marker.tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
if(px<cx || px>cx+cw) continue;
|
||||
fl_push_clip(px,y(),160,12); fl_font(FL_HELVETICA,10); fl_color(xtc_ui::accent()); fl_draw(marker.name,px+3,y()+10); fl_pop_clip();
|
||||
}
|
||||
int playhead=owner_.x()+owner_.header_width_+static_cast<int>(owner_.cursor_*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
fl_color(xtc_ui::accent()); fl_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6);
|
||||
int pinned=owner_.header_left();
|
||||
fl_color(xtc_ui::background()); fl_rectf(pinned,y(),owner_.header_width_,28);
|
||||
fl_color(xtc_ui::muted()); fl_font(FL_HELVETICA,10); fl_draw("TRACKS / drag edge to resize",pinned+10,y()+19);
|
||||
fl_pop_clip();
|
||||
}
|
||||
int SequencerRulerWidget::handle(int event)
|
||||
{
|
||||
if (event==FL_DRAG && owner_.mode_==4) {owner_.loop_preview_=owner_.tick(Fl::event_x());owner_.redraw();return 1;}
|
||||
if (event==FL_RELEASE && owner_.mode_==4) {
|
||||
owner_.loop_preview_=owner_.tick(Fl::event_x()); owner_.mode_=0;
|
||||
uint64_t begin=std::min(owner_.loop_anchor_,owner_.loop_preview_),end=std::max(owner_.loop_anchor_,owner_.loop_preview_);
|
||||
if(end>begin && owner_.loop_select) {
|
||||
xtc_result r=owner_.loop_select(owner_.context_,begin,end);
|
||||
if(r!=XTC_OK) fl_alert("Loop unchanged: %s",xtc_result_string(r));
|
||||
}
|
||||
owner_.redraw();return 1;
|
||||
}
|
||||
|
||||
if(event==FL_PUSH && (Fl::event_button()==FL_LEFT_MOUSE || Fl::event_button()==FL_RIGHT_MOUSE)) {
|
||||
owner_.take_focus(); owner_.mode_=0;
|
||||
if(Fl::event_x()<owner_.header_left()+owner_.header_width_) return 1;
|
||||
if(Fl::event_button()==FL_LEFT_MOUSE && Fl::event_state(FL_SHIFT)) {
|
||||
owner_.loop_anchor_=owner_.loop_preview_=owner_.tick(Fl::event_x()); owner_.mode_=4; owner_.redraw(); return 1;
|
||||
}
|
||||
uint64_t target=owner_.tick(Fl::event_x());
|
||||
xtc_result r=owner_.seek_(owner_.context_,target);
|
||||
if(r==XTC_OK) owner_.cursor_=target;
|
||||
else fl_alert("Stop playback before seeking: %s",xtc_result_string(r));
|
||||
owner_.redraw(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
13
src/gui/sequencer_ruler_widget.hpp
Normal file
13
src/gui/sequencer_ruler_widget.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef XTC_SEQUENCER_RULER_WIDGET_HPP
|
||||
#define XTC_SEQUENCER_RULER_WIDGET_HPP
|
||||
#include <FL/Fl_Widget.H>
|
||||
class SequencerWidget;
|
||||
class SequencerRulerWidget : public Fl_Widget {
|
||||
SequencerWidget &owner_;
|
||||
public:
|
||||
explicit SequencerRulerWidget(SequencerWidget &owner);
|
||||
int handle(int event) override;
|
||||
protected:
|
||||
void draw() override;
|
||||
};
|
||||
#endif
|
||||
147
src/gui/sequencer_track_widget.cpp
Normal file
147
src/gui/sequencer_track_widget.cpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#include "sequencer_track_widget.hpp"
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "sequencer_clip_widget.hpp"
|
||||
#include "sequencer_velocity_widget.hpp"
|
||||
#include "sequencer_group_scope.hpp"
|
||||
#include "theme.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
SequencerTrackWidget::SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track)
|
||||
: Fl_Group(owner.x(),owner.y()+28,owner.w(),56), owner_(owner), info_(track)
|
||||
{
|
||||
resizable(nullptr);
|
||||
clips_.reserve(XTC_PROJECT_MAX_CLIPS);
|
||||
velocity_=new SequencerVelocityWidget(owner,track.id);
|
||||
header_=new SequencerTrackHeader(*this);
|
||||
end();
|
||||
}
|
||||
|
||||
Fl_Choice *SequencerTrackWidget::instrument_choice() const { return header_->instrument_choice(); }
|
||||
Fl_Group *SequencerTrackWidget::header() const { return header_; }
|
||||
SequencerClipWidget *SequencerTrackWidget::clip_widget(uint64_t clip) const
|
||||
{
|
||||
for(auto *view:clips_) if(view->clip_id()==clip) return view;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SequencerTrackWidget::sync(size_t row, const xtc_track_info &track)
|
||||
{
|
||||
row_=row; info_=track;
|
||||
resize(owner_.x(),owner_.track_top(row),owner_.w(),owner_.track_height(row));
|
||||
header_->resize(owner_.header_left(),y(),owner_.header_width_,h());
|
||||
instrument_choice()->resize(header_->x()+10,y()+30,header_->w()-22,20);
|
||||
if(info_.is_bus) instrument_choice()->hide();
|
||||
velocity_->resize(owner_.x()+owner_.header_width_,y()+56,owner_.w()-owner_.header_width_,88);
|
||||
if(owner_.track_expanded(info_.id)) velocity_->show(); else velocity_->hide();
|
||||
for(auto it=clips_.begin();it!=clips_.end();) {
|
||||
xtc_clip_info c{};
|
||||
if(xtc_project_find_clip(owner_.project_,(*it)->clip_id(),&c)!=XTC_OK || c.track_id!=info_.id) {
|
||||
auto *removed=*it; it=clips_.erase(it);
|
||||
removed->hide(); remove(removed); Fl::delete_widget(removed);
|
||||
} else ++it;
|
||||
}
|
||||
size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips);
|
||||
SequencerGroupScope scope(this);
|
||||
for(size_t i=0;i<clips;++i) {
|
||||
xtc_clip_info c{}; xtc_project_get_clip(owner_.project_,i,&c);
|
||||
if(c.track_id!=info_.id) continue;
|
||||
auto *view=clip_widget(c.id);
|
||||
if(!view) {
|
||||
view=new SequencerClipWidget(owner_,c);
|
||||
clips_.push_back(view); // Capacity reserved before creating child widgets.
|
||||
}
|
||||
view->sync(c,y());
|
||||
// Match dense model order for overlapping clip drawing and hit testing.
|
||||
insert(*view,header_);
|
||||
}
|
||||
}
|
||||
|
||||
void SequencerTrackWidget::draw()
|
||||
{
|
||||
fl_push_clip(x(),y(),w(),h());
|
||||
draw_children();
|
||||
fl_pop_clip();
|
||||
}
|
||||
|
||||
int SequencerTrackWidget::handle(int event)
|
||||
{
|
||||
if(event==FL_PUSH) {
|
||||
if(Fl::event_inside(header_)) return header_->handle(event);
|
||||
if(velocity_->visible() && Fl::event_inside(velocity_)) return velocity_->handle(event);
|
||||
// Preserve the full-row clip hit area, including its small vertical margins.
|
||||
for(int i=children()-1;i>=0;--i) {
|
||||
auto *view=dynamic_cast<SequencerClipWidget *>(child(i));
|
||||
if(view && Fl::event_x()>=view->x() && Fl::event_x()<view->x()+view->w()) return view->handle(event);
|
||||
}
|
||||
owner_.take_focus(); owner_.track_=info_.id; owner_.clip_=UINT64_MAX;
|
||||
owner_.selection_.clear(); owner_.mode_=0;
|
||||
if(Fl::event_button()==FL_RIGHT_MOUSE) owner_.context_menu();
|
||||
owner_.redraw(); return 1;
|
||||
}
|
||||
if(event==FL_DRAG || event==FL_RELEASE) return owner_.handle(event);
|
||||
return Fl_Group::handle(event);
|
||||
}
|
||||
|
||||
SequencerTrackHeader::SequencerTrackHeader(SequencerTrackWidget &track)
|
||||
: Fl_Group(track.x(),track.y(),180,56), track_(track)
|
||||
{
|
||||
resizable(nullptr);
|
||||
instrument_=new Fl_Choice(x()+10,y()+30,w()-22,20);
|
||||
xtc_ui::style(instrument_); instrument_->textsize(10); instrument_->textcolor(xtc_ui::text());
|
||||
instrument_->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin...");
|
||||
instrument_->callback(SequencerWidget::instrument_callback,&track.owner_);
|
||||
end();
|
||||
}
|
||||
|
||||
void SequencerTrackHeader::draw()
|
||||
{
|
||||
auto &owner=track_.owner_; const auto &t=track_.info_;
|
||||
fl_push_clip(x(),y(),w(),h());
|
||||
fl_color(t.id==owner.track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel());
|
||||
fl_rectf(x(),y(),w()-2,h()-1);
|
||||
fl_color(xtc_ui::track(t.id)); fl_rectf(x(),y(),3,h()-1);
|
||||
fl_push_clip(x()+9,y(),w()-18,55);
|
||||
fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12);
|
||||
if(owner.track_expanded(t.id)) fl_polygon(x()+10,y()+10,x()+20,y()+10,x()+15,y()+16);
|
||||
else fl_polygon(x()+12,y()+8,x()+12,y()+18,x()+18,y()+13);
|
||||
fl_draw(t.name,x()+26,y()+18);
|
||||
if(t.is_bus) {
|
||||
char detail[64]; std::snprintf(detail,sizeof(detail),"%02zu BUS%s%s",track_.row_+1,t.mute ? " MUTE" : "",t.solo ? " SOLO" : "");
|
||||
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,y()+42);
|
||||
}
|
||||
if(t.mute || t.solo) {
|
||||
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",x()+w()-16,y()+18);
|
||||
}
|
||||
fl_pop_clip();
|
||||
if(owner.track_expanded(t.id)) {
|
||||
fl_color(xtc_ui::muted()); fl_font(FL_HELVETICA,11);
|
||||
bool audio=track_.velocity_->audio();
|
||||
fl_draw(audio ? "AUDIO VELOCITY" : "VELOCITY 1..127",x()+12,y()+76);
|
||||
fl_font(FL_HELVETICA,10);
|
||||
fl_draw(audio ? "Level envelope 0..400%" : "Drag bars to edit notes",x()+12,y()+94);
|
||||
char value[32]; if(track_.velocity_->preview_label(value,sizeof(value))) fl_draw(value,x()+12,y()+114);
|
||||
}
|
||||
draw_children();
|
||||
fl_pop_clip();
|
||||
}
|
||||
|
||||
int SequencerTrackHeader::handle(int event)
|
||||
{
|
||||
auto &owner=track_.owner_;
|
||||
if(event==FL_PUSH) {
|
||||
if(instrument_->visible() && Fl::event_inside(instrument_)) return Fl_Group::handle(event);
|
||||
owner.take_focus();
|
||||
if(Fl::event_button()==FL_LEFT_MOUSE && Fl::event_x()>=x()+7 && Fl::event_x()<x()+24 && Fl::event_y()<y()+26) {
|
||||
owner.expand_track(track_.info_.id,!owner.track_expanded(track_.info_.id)); return 1;
|
||||
}
|
||||
// The velocity label area does not change clip selection.
|
||||
if(Fl::event_y()>=y()+56) return 1;
|
||||
owner.track_=track_.info_.id; owner.clip_=UINT64_MAX; owner.selection_.clear(); owner.mode_=0;
|
||||
if(Fl::event_button()==FL_RIGHT_MOUSE) owner.context_menu();
|
||||
owner.redraw(); return 1;
|
||||
}
|
||||
return Fl_Group::handle(event);
|
||||
}
|
||||
42
src/gui/sequencer_track_widget.hpp
Normal file
42
src/gui/sequencer_track_widget.hpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#ifndef XTC_SEQUENCER_TRACK_WIDGET_HPP
|
||||
#define XTC_SEQUENCER_TRACK_WIDGET_HPP
|
||||
#include <FL/Fl_Group.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include "xtc/xtc_project.h"
|
||||
#include <vector>
|
||||
class SequencerWidget;
|
||||
class SequencerClipWidget;
|
||||
class SequencerVelocityWidget;
|
||||
class SequencerTrackHeader;
|
||||
class SequencerTrackWidget : public Fl_Group {
|
||||
SequencerWidget &owner_;
|
||||
xtc_track_info info_{};
|
||||
size_t row_ = 0;
|
||||
SequencerTrackHeader *header_;
|
||||
SequencerVelocityWidget *velocity_;
|
||||
std::vector<SequencerClipWidget *> clips_; // Borrowed; FLTK owns children.
|
||||
friend class SequencerTrackHeader;
|
||||
public:
|
||||
SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track);
|
||||
uint64_t track_id() const { return info_.id; }
|
||||
bool is_bus() const { return info_.is_bus != 0; }
|
||||
void sync(size_t row, const xtc_track_info &track);
|
||||
Fl_Choice *instrument_choice() const;
|
||||
SequencerClipWidget *clip_widget(uint64_t clip) const;
|
||||
SequencerVelocityWidget *velocity_lane() const { return velocity_; }
|
||||
Fl_Group *header() const;
|
||||
int handle(int event) override;
|
||||
protected:
|
||||
void draw() override;
|
||||
};
|
||||
class SequencerTrackHeader : public Fl_Group {
|
||||
SequencerTrackWidget &track_;
|
||||
Fl_Choice *instrument_;
|
||||
public:
|
||||
explicit SequencerTrackHeader(SequencerTrackWidget &track);
|
||||
Fl_Choice *instrument_choice() const { return instrument_; }
|
||||
int handle(int event) override;
|
||||
protected:
|
||||
void draw() override;
|
||||
};
|
||||
#endif
|
||||
189
src/gui/sequencer_velocity_widget.cpp
Normal file
189
src/gui/sequencer_velocity_widget.cpp
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#include "sequencer_velocity_widget.hpp"
|
||||
#include "sequencer_widget.hpp"
|
||||
#include "theme.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
SequencerVelocityWidget::SequencerVelocityWidget(SequencerWidget &owner, uint64_t track)
|
||||
: Fl_Widget(0,0,1,88), owner_(owner), track_(track) {}
|
||||
bool SequencerVelocityWidget::preview_label(char *text, size_t size) const
|
||||
{
|
||||
if(owner_.active_velocity_!=this) return false;
|
||||
if(owner_.mode_==7) std::snprintf(text,size,"Level: %.0f%%",audio_preview_.value*100);
|
||||
else if(owner_.mode_==6) std::snprintf(text,size,"Velocity: %u",preview_);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
bool SequencerVelocityWidget::note_position(const xtc_note_info ¬e, int &px) const
|
||||
{
|
||||
xtc_clip_info c{};
|
||||
if(xtc_project_find_clip(owner_.project_,note.clip_id,&c)!=XTC_OK || c.track_id!=track_ || !c.midi ||
|
||||
note.start_tick<c.offset_tick || note.start_tick-c.offset_tick>=c.length_tick) return false;
|
||||
px=owner_.x()+owner_.header_width_+static_cast<int>((c.start_tick+note.start_tick-c.offset_tick)*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SequencerVelocityWidget::audio() const
|
||||
{
|
||||
size_t tracks=0, clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips);
|
||||
bool midi=false;
|
||||
for(size_t i=0;i<clips;++i) {
|
||||
xtc_clip_info c{}; xtc_project_get_clip(owner_.project_,i,&c);
|
||||
if(c.track_id!=track_) continue;
|
||||
if(!c.midi) return true;
|
||||
midi=true;
|
||||
}
|
||||
return !midi;
|
||||
}
|
||||
|
||||
float SequencerVelocityWidget::audio_value(int py) const
|
||||
{
|
||||
return std::clamp(static_cast<float>(y()+h()-8-py)*4.0f/(h()-28),0.0f,4.0f);
|
||||
}
|
||||
|
||||
void SequencerVelocityWidget::draw_audio(const xtc_track_info &track, int left, int right)
|
||||
{
|
||||
std::array<xtc_automation_point,XTC_MAX_AUTOMATION_POINTS+1> points{};
|
||||
size_t count=0,total=0; xtc_project_automation_count(owner_.project_,&total);
|
||||
for(size_t i=0;i<total;++i) {
|
||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
||||
if(a.track_id!=track.id || a.parameter!=XTC_AUTOMATION_GAIN) continue;
|
||||
if(owner_.mode_==7 && owner_.active_velocity_==this && a.tick==audio_preview_.tick) continue;
|
||||
points[count++]=a;
|
||||
}
|
||||
if(owner_.mode_==7 && owner_.active_velocity_==this) points[count++]=audio_preview_;
|
||||
std::sort(points.begin(),points.begin()+count,[](const auto &a,const auto &b){return a.tick<b.tick;});
|
||||
auto px=[&](uint64_t t){return owner_.x()+owner_.header_width_+static_cast<int>(t*static_cast<unsigned int>(owner_.scale_)/960U);};
|
||||
auto py=[&](float v){return y()+h()-8-static_cast<int>(v*(h()-28)/4.0f);};
|
||||
fl_color(xtc_ui::track(track.id));
|
||||
if(!count) fl_line(left,py(track.gain),right,py(track.gain));
|
||||
else {
|
||||
fl_line(left,py(points[0].value),px(points[0].tick),py(points[0].value));
|
||||
for(size_t i=1;i<count;++i) {
|
||||
const auto &a=points[i-1]; const auto &b=points[i];
|
||||
if(a.step) {
|
||||
fl_line(px(a.tick),py(a.value),px(b.tick),py(a.value));
|
||||
fl_line(px(b.tick),py(a.value),px(b.tick),py(b.value));
|
||||
} else fl_line(px(a.tick),py(a.value),px(b.tick),py(b.value));
|
||||
}
|
||||
fl_line(px(points[count-1].tick),py(points[count-1].value),right,py(points[count-1].value));
|
||||
for(size_t i=0;i<count;++i) {
|
||||
int point_x=px(points[i].tick);
|
||||
if(point_x<left-4 || point_x>right+4) continue;
|
||||
fl_rectf(point_x-4,py(points[i].value)-3,8,6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int SequencerVelocityWidget::note_value(int py) const
|
||||
{
|
||||
return static_cast<unsigned int>(std::clamp((y()+h()-8-py)*127/(h()-28),1,127));
|
||||
}
|
||||
|
||||
void SequencerVelocityWidget::draw()
|
||||
{
|
||||
int cx,cy,cw,ch; fl_clip_box(x(),y(),w(),h(),cx,cy,cw,ch);
|
||||
int left=std::max(cx,owner_.header_left()+owner_.header_width_);
|
||||
fl_push_clip(left,y(),std::max(0,cx+cw-left),h());
|
||||
fl_color(xtc_ui::border()); fl_line(x(),y(),x()+w(),y());
|
||||
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted());
|
||||
bool is_audio=audio();
|
||||
fl_draw(is_audio ? "Click anywhere to add a level point; drag vertically; right-click a point to remove" : "Drag a note bar to change velocity",left+8,y()+14);
|
||||
xtc_track_info track{};
|
||||
size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips);
|
||||
for(size_t i=0;i<tracks;++i) {xtc_project_get_track(owner_.project_,i,&track); if(track.id==track_) break;}
|
||||
if(is_audio) draw_audio(track,left,cx+cw);
|
||||
size_t notes=0; xtc_project_note_count(owner_.project_,¬es);
|
||||
for(size_t n=0;!is_audio && n<notes;++n) {
|
||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); int px=0;
|
||||
if(!note_position(note,px)) continue;
|
||||
bool editing=owner_.active_velocity_==this && owner_.mode_==6 && original_.id==note.id;
|
||||
unsigned int value=editing ? preview_ : note.velocity;
|
||||
int bottom=y()+h()-8,ny=bottom-static_cast<int>(value)*(h()-28)/127;
|
||||
fl_color(editing ? xtc_ui::text() : xtc_ui::track(track_));
|
||||
fl_rectf(px-2,ny,4,bottom-ny); fl_rectf(px-4,ny-2,8,4);
|
||||
}
|
||||
fl_pop_clip();
|
||||
}
|
||||
|
||||
int SequencerVelocityWidget::handle(int event)
|
||||
{
|
||||
if(owner_.mode_==7) {
|
||||
if(event==FL_KEYDOWN && Fl::event_key()==FL_Escape) { owner_.mode_=0; owner_.redraw(); return 1; }
|
||||
if(event==FL_DRAG) { audio_preview_.value=audio_value(Fl::event_y()); owner_.redraw(); return 1; }
|
||||
if(event==FL_RELEASE) {
|
||||
owner_.mode_=0;
|
||||
auto point=audio_preview_;
|
||||
point.value=audio_value(Fl::event_y());
|
||||
if(audio_existing_) {
|
||||
size_t count=0; xtc_project_automation_count(owner_.project_,&count);
|
||||
for(size_t i=0;i<count;++i) {
|
||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
||||
if(a.track_id==point.track_id && a.parameter==point.parameter && a.tick==point.tick && a.value==point.value) { owner_.redraw(); return 1; }
|
||||
}
|
||||
}
|
||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
||||
if(r==XTC_OK) r=xtc_project_set_automation(owner_.project_,&point);
|
||||
owner_.changed(r); return 1;
|
||||
}
|
||||
}
|
||||
if(owner_.mode_==6) {
|
||||
if(event==FL_KEYDOWN && Fl::event_key()==FL_Escape) { owner_.mode_=0; owner_.redraw(); return 1; }
|
||||
if(event==FL_DRAG) { preview_=note_value(Fl::event_y()); owner_.redraw(); return 1; }
|
||||
if(event==FL_RELEASE) {
|
||||
owner_.mode_=0;
|
||||
unsigned int value=note_value(Fl::event_y());
|
||||
if(value==original_.velocity) { owner_.redraw(); return 1; }
|
||||
xtc_note_info note=original_;
|
||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
||||
if(r==XTC_OK) r=xtc_project_edit_note(owner_.project_,note.id,note.start_tick,note.length_tick,note.pitch,value,note.channel);
|
||||
owner_.changed(r); return 1;
|
||||
}
|
||||
}
|
||||
if(event==FL_PUSH && (Fl::event_button()==FL_LEFT_MOUSE || Fl::event_button()==FL_RIGHT_MOUSE)) {
|
||||
owner_.take_focus();
|
||||
owner_.active_velocity_=this;
|
||||
if(audio()) {
|
||||
owner_.track_=track_; owner_.clip_=UINT64_MAX; owner_.selection_.clear();
|
||||
uint64_t position=std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(std::max(0,Fl::event_x()-owner_.x()-owner_.header_width_))*960/static_cast<unsigned int>(owner_.scale_));
|
||||
audio_preview_={track_,position,XTC_AUTOMATION_GAIN,audio_value(Fl::event_y()),0};
|
||||
audio_existing_=false;
|
||||
size_t count=0; xtc_project_automation_count(owner_.project_,&count); int distance=12;
|
||||
for(size_t i=0;i<count;++i) {
|
||||
xtc_automation_point a{}; xtc_project_get_automation(owner_.project_,i,&a);
|
||||
if(a.track_id!=track_ || a.parameter!=XTC_AUTOMATION_GAIN) continue;
|
||||
int px=owner_.x()+owner_.header_width_+static_cast<int>(a.tick*static_cast<unsigned int>(owner_.scale_)/960U);
|
||||
int py=y()+h()-8-static_cast<int>(a.value*(h()-28)/4.0f);
|
||||
int d=std::abs(Fl::event_x()-px)+std::abs(Fl::event_y()-py);
|
||||
if(d<distance) { distance=d; audio_preview_=a; audio_existing_=true; }
|
||||
}
|
||||
if(Fl::event_button()==FL_RIGHT_MOUSE) {
|
||||
if(audio_existing_) {
|
||||
auto point=audio_preview_;
|
||||
xtc_result r=owner_.before_edit ? owner_.before_edit(owner_.context_) : XTC_OK;
|
||||
if(r==XTC_OK) r=xtc_project_remove_automation(owner_.project_,point.track_id,point.parameter,point.tick);
|
||||
owner_.changed(r);
|
||||
}
|
||||
} else if(Fl::event_button()==FL_LEFT_MOUSE) owner_.mode_=7;
|
||||
owner_.redraw(); return 1;
|
||||
}
|
||||
if(Fl::event_button()!=FL_LEFT_MOUSE) return 1;
|
||||
size_t notes=0; xtc_project_note_count(owner_.project_,¬es);
|
||||
int distance=10; bool hit=false;
|
||||
|
||||
for(size_t n=0;n<notes;++n) {
|
||||
xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); int px=0;
|
||||
if(!note_position(note,px)) continue;
|
||||
int ny=y()+h()-8-static_cast<int>(note.velocity)*(h()-28)/127;
|
||||
int d=std::abs(Fl::event_x()-px)+std::abs(Fl::event_y()-ny);
|
||||
if(d<distance) { distance=d; original_=note; hit=true; }
|
||||
}
|
||||
if(hit) { owner_.track_=track_; owner_.clip_=original_.clip_id; owner_.mode_=6; preview_=original_.velocity; }
|
||||
owner_.redraw(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
26
src/gui/sequencer_velocity_widget.hpp
Normal file
26
src/gui/sequencer_velocity_widget.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef XTC_SEQUENCER_VELOCITY_WIDGET_HPP
|
||||
#define XTC_SEQUENCER_VELOCITY_WIDGET_HPP
|
||||
#include <FL/Fl_Widget.H>
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
class SequencerWidget;
|
||||
class SequencerVelocityWidget : public Fl_Widget {
|
||||
SequencerWidget &owner_;
|
||||
uint64_t track_;
|
||||
xtc_note_info original_{};
|
||||
unsigned int preview_ = 100;
|
||||
xtc_automation_point audio_preview_{};
|
||||
bool audio_existing_ = false;
|
||||
bool note_position(const xtc_note_info ¬e, int &px) const;
|
||||
unsigned int note_value(int py) const;
|
||||
float audio_value(int py) const;
|
||||
void draw_audio(const xtc_track_info &track, int left, int right);
|
||||
public:
|
||||
SequencerVelocityWidget(SequencerWidget &owner, uint64_t track);
|
||||
bool audio() const;
|
||||
bool preview_label(char *text, size_t size) const;
|
||||
int handle(int event) override;
|
||||
protected:
|
||||
void draw() override;
|
||||
};
|
||||
#endif
|
||||
59
src/gui/sequencer_waveforms.cpp
Normal file
59
src/gui/sequencer_waveforms.cpp
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#include "sequencer_widget.hpp"
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
#include <new>
|
||||
|
||||
void SequencerWidget::refresh_waveforms() try
|
||||
{
|
||||
// Control-thread loading only. Keep cached failures as well as successes;
|
||||
// removed/relinked paths are evicted. Reopening rebuilds externally changed files.
|
||||
size_t tracks, clips;
|
||||
xtc_project_get_counts(project_, &tracks, &clips);
|
||||
std::vector<Wave> next;
|
||||
for (size_t i = 0; i < clips && next.size() < 64; ++i) {
|
||||
xtc_clip_info c{};
|
||||
xtc_project_get_clip(project_, i, &c);
|
||||
if (!c.asset_path[0]) continue;
|
||||
bool present = false;
|
||||
for (const auto &wave : next) if (wave.path == c.asset_path) { present = true; break; }
|
||||
if (present) continue;
|
||||
Wave entry;
|
||||
entry.path = c.asset_path;
|
||||
for (auto &wave : waves_) if (wave.path == entry.path) {
|
||||
entry.data = std::move(wave.data); present = true; break;
|
||||
}
|
||||
if (!present) {
|
||||
xtc_waveform *summary = nullptr;
|
||||
(void)xtc_waveform_create(c.asset_path, &summary);
|
||||
entry.data.reset(summary);
|
||||
}
|
||||
next.push_back(std::move(entry));
|
||||
}
|
||||
waves_ = std::move(next);
|
||||
} catch (const std::bad_alloc &) {
|
||||
waves_.clear(); // Preview failure must not interrupt the model/history edit.
|
||||
}
|
||||
|
||||
void SequencerWidget::draw_waveform(const xtc_clip_info &c, int left, int top)
|
||||
{
|
||||
const xtc_waveform *summary = nullptr;
|
||||
for (const auto &wave : waves_) if (wave.path == c.asset_path) { summary = wave.data.get(); break; }
|
||||
if (!summary) return;
|
||||
unsigned int rate; size_t frames, bins;
|
||||
if (xtc_waveform_info(summary, &rate, &frames, &bins) != XTC_OK) return;
|
||||
double ticks_per_frame = static_cast<double>(xtc_project_tempo(project_)) * 960 / (rate * 60000.0);
|
||||
fl_color(fl_rgb_color(30, 65, 80));
|
||||
for (size_t bin = 0; bin < bins; ++bin) {
|
||||
double start = static_cast<double>(bin * frames / bins) * ticks_per_frame;
|
||||
double end = static_cast<double>((bin+1) * frames / bins) * ticks_per_frame;
|
||||
double offset = static_cast<double>(c.offset_tick);
|
||||
if (end <= offset || start >= offset + static_cast<double>(c.length_tick)) continue;
|
||||
int px = left + static_cast<int>((std::max(start, offset) - offset) * scale_ / 960);
|
||||
int width = std::max(1, static_cast<int>((end - std::max(start, offset)) * scale_ / 960));
|
||||
float low, high;
|
||||
if (xtc_waveform_peak(summary, bin, &low, &high) != XTC_OK) continue;
|
||||
int y1 = top + 28 - static_cast<int>(std::clamp(high, -1.0f, 1.0f) * 19);
|
||||
int y2 = top + 28 - static_cast<int>(std::clamp(low, -1.0f, 1.0f) * 19);
|
||||
fl_rectf(px, y1, width, std::max(1, y2-y1+1));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,12 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class SequencerTrackWidget;
|
||||
class SequencerTrackHeader;
|
||||
class SequencerClipWidget;
|
||||
class SequencerVelocityWidget;
|
||||
class SequencerRulerWidget;
|
||||
|
||||
class SequencerWidget : public Fl_Group {
|
||||
public:
|
||||
/**
|
||||
|
|
@ -32,6 +38,7 @@ class SequencerWidget : public Fl_Group {
|
|||
OpenClip open_clip = nullptr;
|
||||
InstrumentSelect instrument_select = nullptr;
|
||||
Fl_Choice *instrument_choice(uint64_t track) const;
|
||||
SequencerTrackWidget *track_widget(uint64_t track) const;
|
||||
void refresh_instruments();
|
||||
bool track_expanded(uint64_t track) const;
|
||||
bool expand_track(uint64_t track, bool expanded);
|
||||
|
|
@ -202,18 +209,16 @@ class SequencerWidget : public Fl_Group {
|
|||
std::array<uint64_t,XTC_PROJECT_MAX_TRACKS> expanded_tracks_{};
|
||||
int track_height(size_t row) const;
|
||||
int track_row(int py) const;
|
||||
bool velocity_note(const xtc_note_info ¬e, uint64_t track, int &px) const;
|
||||
unsigned int velocity_value(int py) const;
|
||||
xtc_note_info velocity_original_{};
|
||||
unsigned int velocity_preview_ = 100;
|
||||
int velocity_top_ = 0;
|
||||
bool audio_velocity_lane(uint64_t track) const;
|
||||
void draw_audio_velocity(const xtc_track_info &track, int top, int left, int right);
|
||||
float audio_velocity_value(int py) const;
|
||||
xtc_automation_point audio_velocity_preview_{};
|
||||
bool audio_velocity_existing_ = false;
|
||||
struct TrackChoice { Fl_Choice *widget = nullptr; uint64_t track = UINT64_MAX; };
|
||||
std::array<TrackChoice,XTC_PROJECT_MAX_TRACKS> track_choices_{};
|
||||
friend class SequencerTrackWidget;
|
||||
friend class SequencerTrackHeader;
|
||||
friend class SequencerClipWidget;
|
||||
friend class SequencerVelocityWidget;
|
||||
friend class SequencerRulerWidget;
|
||||
std::vector<SequencerTrackWidget *> track_widgets_; // FLTK owns children.
|
||||
SequencerRulerWidget *ruler_ = nullptr;
|
||||
SequencerVelocityWidget *active_velocity_ = nullptr; // Captured gesture, not ownership.
|
||||
void sync_views();
|
||||
void context_menu();
|
||||
std::vector<xtc_plugin_spec> catalog_;
|
||||
static void instrument_callback(Fl_Widget *, void *);
|
||||
struct Wave {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,25 @@ port/callback setup failure, activation/deactivation/close failure, null buffers
|
|||
invalid configuration, oversize buffers, and reopen are tested with injected
|
||||
JACK callbacks/results in `tests/unit/test_backend.c`.
|
||||
|
||||
## Sequencer component regression checks
|
||||
|
||||
`test_gui` verifies the actual FLTK hierarchy: a master sequencer owns its ruler
|
||||
and track groups; tracks own clips, velocity lanes and header groups; headers
|
||||
own instrument selectors. It checks view identity through track reordering and
|
||||
undo, child membership after cross-track moves, deferred deletion and project
|
||||
replacement during an active gesture. Clip and MIDI velocity gestures also run
|
||||
through `Fl::handle` at the window boundary to exercise native event dispatch
|
||||
and capture, alongside the existing direct-widget gesture tests.
|
||||
|
||||
```sh
|
||||
cmake --build build --target test_gui
|
||||
ctest --test-dir build -R '^gui-sequencer$' --output-on-failure
|
||||
ctest --test-dir build -R '^gui$' --output-on-failure
|
||||
```
|
||||
|
||||
Run GUI variants sequentially (`ctest --test-dir build -L gui -j 1`) because
|
||||
several existing workflows use the same fixed temporary project filenames.
|
||||
|
||||
## FLTK shell tests
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
#include "../../src/gui/sample_editor.hpp"
|
||||
#include "../../src/gui/graph_widget.hpp"
|
||||
#include "../../src/gui/sequencer_widget.hpp"
|
||||
#include "../../src/gui/sequencer_track_widget.hpp"
|
||||
#include "../../src/gui/sequencer_clip_widget.hpp"
|
||||
#include "../../src/gui/sequencer_velocity_widget.hpp"
|
||||
#include "../../src/gui/sequencer_ruler_widget.hpp"
|
||||
#include "../../src/gui/piano_roll.hpp"
|
||||
#include "../../src/gui/mixer_widget.hpp"
|
||||
#include "../../src/gui/plugin_dialog.hpp"
|
||||
|
|
@ -428,8 +432,78 @@ int midi_device_label_tests()
|
|||
press(find(&dialog,"Apply"));CHECK(!applied.midi_input_port[0] && !applied.midi_output_port[0]);
|
||||
puts("MIDI device dropdown labels/selection/refresh checks passed.");return failures ? 1 : 0;
|
||||
}
|
||||
int sequencer_component_tests()
|
||||
{
|
||||
reset();
|
||||
{
|
||||
MainWindow window(false,""); window.show(); auto *s=timeline(&window);
|
||||
CHECK(window.create_track(true)==XTC_OK);
|
||||
uint64_t first=s->selected_track(),clip=s->selected_clip();
|
||||
CHECK(window.create_track(true)==XTC_OK);
|
||||
uint64_t second=s->selected_track();
|
||||
auto *first_view=s->track_widget(first),*second_view=s->track_widget(second);
|
||||
CHECK(first_view && second_view && first_view->parent()==s && second_view->parent()==s);
|
||||
CHECK(first_view->clip_widget(clip) && first_view->clip_widget(clip)->parent()==first_view);
|
||||
CHECK(first_view->velocity_lane()->parent()==first_view && !first_view->velocity_lane()->visible());
|
||||
CHECK(first_view->header()->parent()==first_view && s->instrument_choice(first)->parent()==first_view->header());
|
||||
int rulers=0;
|
||||
for(int i=0;i<s->children();++i) if(dynamic_cast<SequencerRulerWidget *>(s->child(i))) ++rulers;
|
||||
CHECK(rulers==1 && s->children()==3);
|
||||
CHECK(s->expand_track(first,true));
|
||||
CHECK(first_view->velocity_lane()->visible() && second_view->y()==first_view->y()+first_view->h());
|
||||
CHECK(s->select(first,clip));
|
||||
CHECK(s->move_track(1)==XTC_OK);
|
||||
CHECK(s->track_widget(first)==first_view && s->track_widget(second)==second_view);
|
||||
CHECK(s->move_track(-1)==XTC_OK);
|
||||
// Route through FLTK's window dispatch and mouse capture, across track boundaries.
|
||||
pump();
|
||||
int ox=Fl::e_x,oy=Fl::e_y,ok=Fl::e_keysym,os=Fl::e_state;
|
||||
Fl::e_x=s->x()+s->header_width()+20; Fl::e_y=s->track_top(0)+20;
|
||||
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; Fl::e_state=FL_BUTTON1;
|
||||
CHECK(Fl::handle(FL_PUSH,&window)==1 && Fl::pushed());
|
||||
Fl::e_x+=48; Fl::e_y=s->track_top(1)+20;
|
||||
CHECK(Fl::handle(FL_DRAG,&window)==1);
|
||||
Fl::e_state=0; CHECK(Fl::handle(FL_RELEASE,&window)==1);
|
||||
Fl::e_x=ox; Fl::e_y=oy; Fl::e_keysym=ok; Fl::e_state=os;
|
||||
xtc_clip_info moved{}; CHECK(xtc_project_find_clip(s->project(),clip,&moved)==XTC_OK);
|
||||
CHECK(moved.track_id==second && moved.start_tick==960);
|
||||
CHECK(!first_view->clip_widget(clip) && second_view->clip_widget(clip));
|
||||
CHECK(window.undo(false)==XTC_OK);
|
||||
CHECK(s->track_widget(first)==first_view && first_view->clip_widget(clip));
|
||||
CHECK(window.undo(true)==XTC_OK && second_view->clip_widget(clip));
|
||||
CHECK(s->expand_track(second,true) && s->select(second,clip));
|
||||
uint64_t note_id=0;
|
||||
CHECK(xtc_project_add_note(s->project(),clip,480,480,60,90,0,¬e_id)==XTC_OK);
|
||||
CHECK(s->rename_selection("Nested clip")==XTC_OK); pump();
|
||||
Fl::e_x=s->x()+s->header_width()+72;
|
||||
Fl::e_y=second_view->y()+56+80-90*60/127;
|
||||
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; Fl::e_state=FL_BUTTON1;
|
||||
CHECK(Fl::handle(FL_PUSH,&window)==1);
|
||||
Fl::e_y+=20; CHECK(Fl::handle(FL_DRAG,&window)==1);
|
||||
Fl::e_state=0; CHECK(Fl::handle(FL_RELEASE,&window)==1);
|
||||
xtc_note_info note{}; CHECK(xtc_project_get_note(s->project(),0,¬e)==XTC_OK && note.velocity<90);
|
||||
Fl::e_x=ox; Fl::e_y=oy; Fl::e_keysym=ok; Fl::e_state=os;
|
||||
CHECK(s->select(first) && s->delete_selection()==XTC_OK);
|
||||
CHECK(!s->track_widget(first) && s->track_widget(second)==second_view);
|
||||
pump(); // Deferred removal of a widget during its event is safe.
|
||||
CHECK(window.undo(false)==XTC_OK && s->track_widget(first));
|
||||
CHECK(s->expand_track(second,true)); pump();
|
||||
Fl::e_x=s->x()+s->header_width()+72;
|
||||
Fl::e_y=second_view->y()+56+80-static_cast<int>(note.velocity)*60/127;
|
||||
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; Fl::e_state=FL_BUTTON1;
|
||||
CHECK(Fl::handle(FL_PUSH,&window)==1);
|
||||
// Replacing a project also cancels a captured child gesture.
|
||||
CHECK(window.new_project()==XTC_OK && s->children()==1 && !s->track_widget(second));
|
||||
Fl::e_state=0; Fl::handle(FL_RELEASE,&window);
|
||||
Fl::e_x=ox; Fl::e_y=oy; Fl::e_keysym=ok; Fl::e_state=os;
|
||||
pump(); window.do_callback();
|
||||
}
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if(std::getenv("XTC_TEST_SEQUENCER_COMPONENTS_ONLY")) return sequencer_component_tests();
|
||||
if(std::getenv("XTC_TEST_MIDI_LABELS_ONLY"))return midi_device_label_tests();
|
||||
if(std::getenv("XTC_TEST_MIDI_WORKFLOWS_ONLY"))return midi_workflow_tests();
|
||||
if(std::getenv("XTC_TEST_CREATIVE_ONLY"))return creative_workflow_tests();
|
||||
|
|
@ -1213,6 +1287,7 @@ 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();
|
||||
}
|
||||
sequencer_component_tests();
|
||||
reset();
|
||||
{
|
||||
MainWindow window(false,""); window.show(); auto *s=timeline(&window);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue