Added velocity to tracks in the sequencer.

This commit is contained in:
Johannes Findeisen 2026-11-06 21:00:20 +01:00
commit b46d4f09bb
13 changed files with 873 additions and 19 deletions

View file

@ -139,6 +139,7 @@ cmake --build build --target clean
- Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes up to 32 edits.
- The GUI now exposes left `output` and `output_right` JACK ports; route both explicitly for stereo. See [mixer workflow and limits](docs/MIXER.md).
- Use Add track/Add region, drag regions or their edges, and Zoom +/-; scrollbars pan.
- Click the small arrow beside a track name to expand its MIDI velocity lane downward. Drag a note bar’s cap vertically to set velocity (1–127); Escape cancels and Ctrl+Z undoes. Click the arrow again to collapse. See the [expanded-track screenshot](screenshots/track-velocity-expanded.png).
- Ctrl+N/O/S creates, opens or saves a project; Save As chooses another path.
- Arrangement edits stop playback; graph edits remain live.
- The ruler seeks while stopped. Stop pauses; Reconnect and project load reset position. F5 starts, F8 stops, Space toggles, F6 reconnects, and Ctrl+1/2/3 switches views. Ctrl+Q closes the client and window. No physical ports are connected automatically.

View file

@ -3575,6 +3575,43 @@ qualification.
---
## Phase 109 — Expandable Track Velocity Lanes
### Goal
Expand arrangement tracks downward with a small disclosure arrow and edit MIDI
note velocities directly below their clips. This user-requested GUI increment
uses the existing C note-editing, persistence and history contracts.
### Tasks and acceptance criteria
- [x] Add a small arrow beside each track name; independently expand/collapse
tracks downward and reposition following clips and instrument selectors.
- [x] Draw velocity bars for visible note starts in each track's MIDI clips,
respecting clip offsets, timeline zoom and scrolling.
- [x] Drag a bar's cap vertically to preview and commit velocity (1–127), preserving
note timing, pitch and channel; Escape cancels and undo/redo restores edits.
- [x] Preserve expansion by track ID through reordering and ordinary edits;
reset it on project replacement. Expansion is temporary GUI state.
- [x] Verify real FLTK arrow gestures, lane edits, coincident notes, cancellation,
undo/redo, shifted clip selection and track reordering in GUI integration tests.
- [x] Update README guidance and capture actual expanded and compact sequencer
screenshots; regenerate the promotional overview.
### Implementation status
Implemented on 2026-11-06 in the FLTK sequencer using the existing C API; no
engine or project-format changes. Audio-only tracks and buses can expand but
have no MIDI note bars. Notes beginning before a trimmed clip's visible source
range are omitted. Coincident notes with different velocities can be selected
by their caps; fully overlapping caps select one note, with individual editing
still available in the piano roll. Track expansion is not saved in projects.
Validation: warning-enabled CMake build, CTest GUI integration and offline
checks, plus rendered screenshot inspection. See [the gallery](../screenshots/README.md).
---
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a

588
screenshots/ALL.md Normal file
View file

@ -0,0 +1,588 @@
# xtc — the complete screenshot gallery
Browse every screenshot, from arranging and editing to mixing, plugins and workspace preferences.
See [capture notes and reproduction instructions](README.md) for image provenance and workflow limits.
---
## The xtc workspace at a glance
![The xtc workspace at a glance](overview.png)
A promotional composite of the arrangement, piano roll, mixer and track insert views.
---
## A compact workspace for composition
![A compact workspace for composition](compact-workspace.png)
The sequencer brings track creation, arrangement tools and transport controls into one view.
---
## Arrange clips around a loop
![Arrange clips around a loop](sequencer-loop.png)
MIDI clips sit beneath section markers and a highlighted loop range.
---
## Edit velocity beneath each track
![Edit velocity beneath each track](track-velocity-expanded.png)
Keys and Bass expand downward to reveal MIDI velocity bars while following tracks move down.
---
## Start a new track
![Start a new track](create-track-menu.png)
The Create dropdown opens over the arrangement with track creation choices.
---
## Give the song a structure
![Give the song a structure](arrangement-markers.png)
Named Intro, Verse, Chorus and Outro markers identify sections along the ruler.
---
## Name an arrangement marker
![Name an arrangement marker](marker-edit.png)
The marker editor provides a name for a position in the song.
---
## Navigate song sections
![Navigate song sections](section-navigation.png)
The arrangement shows a loop around the current song section.
---
## Set the loop boundaries
![Set the loop boundaries](loop-range.png)
The loop range dialog controls the start and end of repeated playback.
---
## Make room in the arrangement
![Make room in the arrangement](ripple-time.png)
The ripple time dialog requests a duration to insert or remove at the cursor.
---
## See the inserted space
![See the inserted space](ripple-result.png)
The arrangement shows the gap created by inserting time.
---
## Build an independent track layer
![Build an independent track layer](track-duplication.png)
A duplicated track appears alongside the original arrangement.
---
## Split a MIDI clip
![Split a MIDI clip](midi-split.png)
The arrangement displays separate MIDI regions after a nondestructive split.
---
## Bring MIDI regions together
![Bring MIDI regions together](midi-consolidation.png)
Separate MIDI material is consolidated into a single clip.
---
## Shape notes and their dynamics
![Shape notes and their dynamics](piano-roll-velocity.png)
The piano roll combines note editing with a dedicated velocity lane.
---
## Add timing and velocity variation
![Add timing and velocity variation](midi-humanize.png)
The humanization dialog exposes timing variation, velocity variation and a repeatable seed.
---
## Build chords from intervals
![Build chords from intervals](midi-chord-builder.png)
The chord dialog accepts intervals for constructing notes around a root.
---
## Review the generated chords
![Review the generated chords](midi-chords-result.png)
The piano roll displays the notes written by the chord builder.
---
## Turn chords into patterns
![Turn chords into patterns](midi-arpeggio.png)
The arpeggio dialog controls step duration, gate and note direction.
---
## Review an arpeggiated pattern
![Review an arpeggiated pattern](midi-arpeggio-result.png)
The piano roll shows the sequence of notes produced by arpeggiation.
---
## Spread chord notes in time
![Spread chord notes in time](creative-strum-dialog.png)
The strumming dialog configures spacing and direction for chord notes.
---
## Review a strummed chord
![Review a strummed chord](creative-strum-result.png)
The piano roll shows staggered note starts after applying chord strumming.
---
## Exchange standard MIDI files
![Exchange standard MIDI files](midi-file-menu.png)
The File menu exposes MIDI import and selected-clip export commands.
---
## Bring MIDI into the song
![Bring MIDI into the song](midi-import-arrangement.png)
An exported MIDI clip has been imported into the arrangement.
---
## Inspect imported MIDI notes
![Inspect imported MIDI notes](midi-import-piano-roll.png)
The piano roll displays imported notes and their velocities.
---
## Arrange audio alongside MIDI
![Arrange audio alongside MIDI](sequencer-audio-waveform.png)
An imported audio clip displays its waveform directly in the sequencer.
---
## Adjust clip gain and fades
![Adjust clip gain and fades](audio-clip-properties.png)
The audio clip properties dialog exposes nondestructive gain and fade settings.
---
## Choose a normalization target
![Choose a normalization target](audio-normalization.png)
The normalization dialog requests a target peak level in dBFS.
---
## Review normalized audio
![Review normalized audio](audio-normalization-result.png)
The arrangement shows the audio clip after normalization.
---
## Work directly with an audio source
![Work directly with an audio source](sample-editor.png)
The sample editor displays waveform selection, statistics and processing controls.
---
## Find attacks for slicing
![Find attacks for slicing](sample-transients.png)
Detected source attacks appear alongside the sample editor's slicing controls.
---
## Collect portable project media
![Collect portable project media](project-collection.png)
The collection prompt requests a destination folder for the project and its media.
---
## Monitor the mix during playback
![Monitor the mix during playback](mixer-playing.png)
The mixer shows track controls and meters driven by actual JACK playback callbacks.
---
## Shape the mix over time
![Shape the mix over time](mixer-automation.png)
The mixer presents track controls together with automation editing.
---
## Mix in a smaller window
![Mix in a smaller window](mixer-minimum-size.png)
The mixer is shown at the supported 1220 × 720 minimum main-window size.
---
## Navigate a larger session
![Navigate a larger session](mixer-scrolled-session.png)
A session with additional tracks demonstrates scrolling through the mixer.
---
## Explore the signal flow
![Explore the signal flow](modular-graph.png)
The modular graph displays connected processing nodes and their routing.
---
## Chain installed VST3 effects
![Chain installed VST3 effects](track-inserts-vst3.png)
The Keys track hosts ZamEQ2, ZamAutoSat and ZamDelay in its insert graph.
---
## Inspect a hosted VST3 effect
![Inspect a hosted VST3 effect](track-insert-vst3-identity.png)
The identity view reports the plugin and the current adapter capabilities.
---
## Create parallel effect branches
![Create parallel effect branches](track-inserts-parallel.png)
The insert graph shows parallel wet and dry signal paths.
---
## Start with a Dub Echo rack
![Start with a Dub Echo rack](creative-dub-rack.png)
The insert graph displays the internal Dub Echo template and its idle meters.
---
## Manage a complete effect rack
![Manage a complete effect rack](creative-rack-actions.png)
Rack actions expose bulk operations, comparison and chain clipboard controls.
---
## Browse the plugin library
![Browse the plugin library](plugin-library.png)
The library presents available plugins for selection.
---
## Inspect plugin descriptors
![Inspect plugin descriptors](plugin-descriptors.png)
The descriptor browser displays metadata from repository plugin fixtures.
---
## Adjust hosted plugin controls
![Adjust hosted plugin controls](plugin-controls.png)
The control editor exposes parameters provided by a repository plugin fixture.
---
## Choose an internal instrument
![Choose an internal instrument](internal-instruments.png)
The instrument menu presents the built-in devices available to a track.
---
## Configure an internal delay
![Configure an internal delay](internal-delay.png)
The delay editor presents its available device controls.
---
## Configure an internal reverb
![Configure an internal reverb](internal-reverb.png)
The reverb editor presents its available device controls.
---
## Inspect a CLAP instrument
![Inspect a CLAP instrument](clap-instrument-descriptors.png)
The host displays descriptor information for a CLAP instrument.
---
## Inspect a VST2 instrument
![Inspect a VST2 instrument](vst2-instrument-descriptors.png)
The host displays descriptor information for a VST2 instrument.
---
## Inspect a VST3 instrument
![Inspect a VST3 instrument](vst3-instrument-descriptors.png)
The host displays descriptor information for a VST3 instrument.
---
## Host the Steinberg SDK example effect
![Host the Steinberg SDK example effect](steinberg-sdk-track-insert.png)
The actual SDK-built Half Gain effect appears in xtc's track insert graph.
---
## Inspect the SDK effect identity
![Inspect the SDK effect identity](steinberg-sdk-identity.png)
The host reports the Steinberg SDK effect's identity and current adapter limits.
---
## Discover the SDK sine instrument
![Discover the SDK sine instrument](steinberg-sdk-instrument.png)
The plugin chooser displays the instrument from the Steinberg SDK example bundle.
---
## Discover the JUCE instrument
![Discover the JUCE instrument](juce-vst3-instrument.png)
The plugin chooser displays the actual JUCE VST3 instrument bundle.
---
## Play with the Soft Keys interface
![Play with the Soft Keys interface](juce-soft-keys-editor.png)
The actual JUCE editor is rendered directly, showing presets, parameter controls and an on-screen keyboard.
---
## Connect MIDI controllers to parameters
![Connect MIDI controllers to parameters](midi-controller-mapping.png)
The controller mapping interface exposes MIDI control assignments.
---
## Configure audio preferences
![Configure audio preferences](preferences-audio.png)
The audio preferences tab presents the application's audio settings.
---
## Choose MIDI sources and destinations
![Choose MIDI sources and destinations](preferences-midi.png)
MIDI preferences provide port choices, channel filtering and thru settings.
---
## Browse real MIDI input devices
![Browse real MIDI input devices](preferences-midi-input-devices.png)
The open input dropdown lists actual MIDI sources exposed by the host; no route is selected.
---
## Understand MIDI setup and defaults
![Understand MIDI setup and defaults](preferences-midi-help.png)
The preferences help page explains device routing and MIDI editing defaults.
---
## Configure plugin preferences
![Configure plugin preferences](preferences-plugins.png)
The plugin preferences tab presents plugin discovery settings.
---
## Adjust the workspace preferences
![Adjust the workspace preferences](preferences-interface.png)
The interface preferences tab presents appearance and layout settings.
---
## Set editing defaults
![Set editing defaults](preferences-editing.png)
The editing preferences tab includes defaults for new MIDI notes.
---
## Set project defaults
![Set project defaults](preferences-project_defaults.png)
The project defaults tab presents settings used when creating project content.
---
## Configure transport preferences
![Configure transport preferences](preferences-transport.png)
The transport preferences tab presents playback and navigation settings.
---
## Find an application command
![Find an application command](commands.png)
The command palette provides access to the current application action list.
---
## Keep transport controls within reach
![Keep transport controls within reach](transport-detached.png)
The transport floats in a separate window with BPM and loop settings.
---
## Give the mixer its own window
![Give the mixer its own window](mixer-detached.png)
The mixer is detached from the main workspace.
---
## Spread the workspace across windows
![Spread the workspace across windows](workspace-detached.png)
Transport and mixer panels float alongside the main application window.
---
## Arrange in a separate window
![Arrange in a separate window](sequencer-detached.png)
The sequencer is detached from the main workspace.
---
## Route signals in a separate window
![Route signals in a separate window](graph-detached.png)
The modular graph is detached from the main workspace.
---
## About xtc
![About xtc](about.png)
The About dialog presents application information.

View file

@ -1,5 +1,7 @@
# xtc screenshots
[Browse all screenshots with captions](ALL.md).
The complete gallery was refreshed after Phases 100–105 on 2026-11-06, including
MIDI preferences/import, the sample editor, creative workflows and real installed
VST3 insert chains.
@ -30,6 +32,24 @@ APIs and GUI history/preparation paths. Original projects, media and user
configuration are not overwritten. Black margins are the virtual desktop;
floating views show the actual detached windows together.
## Expandable track velocity lanes (Phase 109)
![Tracks expanded downward with editable MIDI velocity bars](track-velocity-expanded.png)
Captured from the real application on 2026-11-06. Keys and Bass are expanded;
subsequent tracks move downward. Click the arrow beside a track name, then drag
a note bar's cap to edit velocity. The compact workspace, sequencer loop and
promotional overview were also refreshed to show the disclosure arrows.
Reproduce these sequencer captures without replacing the rest of the gallery:
```sh
cmake --build build --target capture_screenshots
XTC_CAPTURE_VELOCITY_ONLY=1 xvfb-run -a -s '-screen 0 1440x960x24' \
./build/capture_screenshots "$PWD/screenshots"
python3 tools/create_promotional_overview.py
```
## Steinberg and JUCE SDK integration (Phases 106–107)
Captured on 2026-11-06 from the built SDK examples, with no fabricated UI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -1428,7 +1428,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
}
graph_widget_->reset_layout();
graph_widget_->set_graph(xtc_project_graph(candidate));
sequencer_->set_project(candidate);
sequencer_->set_project(candidate,true,!reset_history);
if (reset_history)
tabs_->value(tabs_->child(0));
dirty_ = false;

View file

@ -26,6 +26,7 @@
SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context)
: Fl_Group(left, top, 6300, 3700), project_(project), seek_(seek), context_(context)
{
expanded_tracks_.fill(UINT64_MAX);
resizable(nullptr);
extent();
end();
@ -56,10 +57,12 @@ bool SequencerWidget::select(uint64_t track, uint64_t clip)
return true;
}
void SequencerWidget::set_project(xtc_project *project, bool reset)
void SequencerWidget::set_project(xtc_project *project, bool reset, bool preserve_expansion)
{
if(project_ != project) mode_=0;
project_ = project;
if (reset) {
if(!preserve_expansion) expanded_tracks_.fill(UINT64_MAX);
waves_.clear();
selection_.clear();
track_ = clip_ = UINT64_MAX;
@ -93,6 +96,68 @@ uint64_t SequencerWidget::tick(int px) const
return std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(xtc_ui::snap_position(raw,grid_)));
}
bool SequencerWidget::track_expanded(uint64_t track) const
{
return track != UINT64_MAX && std::find(expanded_tracks_.begin(),expanded_tracks_.end(),track) != expanded_tracks_.end();
}
bool SequencerWidget::expand_track(uint64_t track, bool expanded)
{
size_t tracks=0, clips=0;
xtc_project_get_counts(project_,&tracks,&clips);
bool found=false;
for(size_t i=0;i<tracks;++i) {
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
if(t.id==track) found=true;
}
if(!found) return false;
if(track_expanded(track)==expanded) return true;
for(auto &id:expanded_tracks_) if(id==(expanded ? UINT64_MAX : track)) {
id=expanded ? track : UINT64_MAX;
mode_=0; extent(); redraw(); return true;
}
return false;
}
int SequencerWidget::track_height(size_t row) const
{
xtc_track_info t{};
return 56+(xtc_project_get_track(project_,row,&t)==XTC_OK && track_expanded(t.id) ? velocity_height_ : 0);
}
int SequencerWidget::track_top(size_t row) const
{
int top=y()+28;
for(size_t i=0;i<row;++i) top+=track_height(i);
return top;
}
int SequencerWidget::track_row(int py) const
{
size_t tracks=0, clips=0; xtc_project_get_counts(project_,&tracks,&clips);
int top=y()+28;
for(size_t i=0;i<tracks;++i) {
int bottom=top+track_height(i);
if(py>=top && py<bottom) return static_cast<int>(i);
top=bottom;
}
return -1;
}
bool SequencerWidget::velocity_note(const xtc_note_info &note, uint64_t track, int &px) const
{
xtc_clip_info c{};
if(xtc_project_find_clip(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=x()+header_width_+static_cast<int>((c.start_tick+note.start_tick-c.offset_tick)*static_cast<unsigned int>(scale_)/960U);
return true;
}
unsigned int SequencerWidget::velocity_value(int py) const
{
return static_cast<unsigned int>(std::clamp((velocity_top_+velocity_height_-8-py)*127/(velocity_height_-28),1,127));
}
/**
* @brief Resizes the timeline's scrollable area to fit its tracks and clip ends.
*/
@ -100,6 +165,15 @@ void SequencerWidget::extent()
{
size_t tracks, clips;
xtc_project_get_counts(project_, &tracks, &clips);
for(auto &id:expanded_tracks_) {
if(id==UINT64_MAX) continue;
bool found=false;
for(size_t i=0;i<tracks;++i) {
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
if(t.id==id) found=true;
}
if(!found) id=UINT64_MAX;
}
uint64_t end = 32U * 3840U;
for (size_t i = 0; i < clips; ++i) {
xtc_clip_info c{};
@ -113,7 +187,7 @@ void SequencerWidget::extent()
end=std::max(end,std::min(XTC_PROJECT_MAX_TICK,marker.tick)+UINT64_C(3840));
}
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + static_cast<unsigned int>(header_width_+20)),
std::max(400, static_cast<int>(tracks) * 56 + 100));
std::max(400, track_top(tracks)-y()+72));
refresh_instruments();
if (parent())
parent()->redraw();
@ -245,10 +319,10 @@ void SequencerWidget::draw()
size_t tracks = 0, clips = 0, notes = 0;
xtc_project_get_counts(project_,&tracks,&clips); xtc_project_note_count(project_,&notes);
for (size_t i = 0; i < tracks; ++i) {
int py = y()+28+static_cast<int>(i)*56;
if (py+56 < cy || py > cy+ch) continue;
int py = track_top(i);
if (py+track_height(i) < cy || py > cy+ch) continue;
fl_color(i%2 ? fl_rgb_color(30,34,40) : fl_rgb_color(33,37,43));
fl_rectf(x()+header_width_,py,w()-header_width_,55);
fl_rectf(x()+header_width_,py,w()-header_width_,track_height(i)-1);
}
fl_font(FL_HELVETICA,11);
uint64_t visual = grid_ == 1 ? 960 : grid_;
@ -270,8 +344,8 @@ void SequencerWidget::draw()
}
for (size_t i = 0; i < tracks; ++i) {
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
int py = y()+28+static_cast<int>(i)*56;
if (py+56 < cy || py > cy+ch) continue;
int py = track_top(i);
if (py+track_height(i) < cy || py > cy+ch) continue;
Fl_Color color = xtc_ui::track(t.id);
for (size_t j = 0; j < clips; ++j) {
xtc_clip_info c{}; xtc_project_get_clip(project_,j,&c);
@ -310,6 +384,24 @@ void SequencerWidget::draw()
fl_pop_clip();
}
}
for(size_t i=0;i<tracks;++i) {
xtc_track_info t{}; xtc_project_get_track(project_,i,&t);
if(!track_expanded(t.id)) continue;
int top=track_top(i)+56, bottom=top+velocity_height_-8;
fl_push_clip(std::max(cx,header_left()+header_width_),top,cw,velocity_height_);
fl_color(xtc_ui::border()); fl_line(x()+header_width_,top,x()+w(),top);
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted());
fl_draw("Drag a note bar to change velocity",std::max(x()+header_width_,header_left()+header_width_)+8,top+14);
for(size_t n=0;n<notes;++n) {
xtc_note_info note{}; xtc_project_get_note(project_,n,&note); int px=0;
if(!velocity_note(note,t.id,px)) continue;
unsigned int value=mode_==6 && velocity_original_.id==note.id ? velocity_preview_ : note.velocity;
int ny=bottom-static_cast<int>(value)*(velocity_height_-28)/127;
fl_color(mode_==6 && velocity_original_.id==note.id ? xtc_ui::text() : xtc_ui::track(t.id));
fl_rectf(px-2,ny,4,bottom-ny); fl_rectf(px-4,ny-2,8,4);
}
fl_pop_clip();
}
if (!tracks) {
fl_font(FL_HELVETICA_BOLD,18); fl_color(xtc_ui::text());
fl_draw("Your next track starts here",x()+220,y()+105);
@ -342,25 +434,37 @@ void SequencerWidget::draw()
fl_color(xtc_ui::background());fl_rectf(pinned,y(),header_width_,28);
fl_color(xtc_ui::muted());fl_draw("TRACKS / drag edge to resize",pinned+10,y()+19);
for(size_t i=0;i<tracks;++i) {
xtc_track_info t{};xtc_project_get_track(project_,i,&t);int py=y()+28+static_cast<int>(i)*56;
if(py+56<cy || py>cy+ch)continue;
xtc_track_info t{};xtc_project_get_track(project_,i,&t);int py=track_top(i);
if(py+track_height(i)<cy || py>cy+ch)continue;
Fl_Color color = xtc_ui::track(t.id);
fl_color(t.id == track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel());
fl_rectf(pinned,py,header_width_-2,55);
fl_color(color); fl_rectf(pinned,py,3,55);
fl_rectf(pinned,py,header_width_-2,track_height(i)-1);
fl_color(color); fl_rectf(pinned,py,3,track_height(i)-1);
fl_push_clip(pinned+9,py,header_width_-18,55);
fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12);
fl_draw(t.name,pinned+12,py+18);
if(track_expanded(t.id)) fl_polygon(pinned+10,py+10,pinned+20,py+10,pinned+15,py+16);
else fl_polygon(pinned+12,py+8,pinned+12,py+18,pinned+18,py+13);
fl_draw(t.name,pinned+26,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" : "");
if (t.is_bus) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,pinned+12,py+42); }
if (t.mute || t.solo) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",pinned+header_width_-16,py+18); }
fl_pop_clip();
if(track_expanded(t.id)) {
fl_color(xtc_ui::muted()); fl_font(FL_HELVETICA,11);
fl_draw("VELOCITY 1..127",pinned+12,py+76);
fl_font(FL_HELVETICA,10);
fl_draw(t.is_bus ? "No MIDI notes on buses" : "Drag bars to edit notes",pinned+12,py+94);
if(mode_==6 && t.id==track_) {
char value[24]; std::snprintf(value,sizeof(value),"Velocity: %u",velocity_preview_);
fl_draw(value,pinned+12,py+114);
}
}
}
fl_color(xtc_ui::muted());fl_rectf(pinned+header_width_-3,cy,3,ch);
for(size_t i=0;i<track_choices_.size();++i)if(track_choices_[i].widget)
track_choices_[i].widget->resize(pinned+10,y()+58+static_cast<int>(i)*56,header_width_-22,20);
track_choices_[i].widget->resize(pinned+10,track_top(i)+30,header_width_-22,20);
draw_children();
fl_pop_clip();
}
@ -373,6 +477,19 @@ void SequencerWidget::draw()
*/
int SequencerWidget::handle(int event)
{
if(mode_==6) {
if(event==FL_KEYDOWN && Fl::event_key()==FL_Escape) { mode_=0; redraw(); return 1; }
if(event==FL_DRAG) { velocity_preview_=velocity_value(Fl::event_y()); redraw(); return 1; }
if(event==FL_RELEASE) {
mode_=0;
unsigned int value=velocity_value(Fl::event_y());
if(value==velocity_original_.velocity) { redraw(); return 1; }
xtc_note_info note=velocity_original_;
xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
if(r==XTC_OK) r=xtc_project_edit_note(project_,note.id,note.start_tick,note.length_tick,note.pitch,value,note.channel);
changed(r); return 1;
}
}
if(event==FL_PUSH && Fl::event_button()==FL_LEFT_MOUSE && Fl::event_x()>=header_left()+header_width_-5 && Fl::event_x()<header_left()+header_width_) {
mode_=5;header_drag_x_=Fl::event_x();header_drag_original_=header_width_;return 1;
}
@ -407,11 +524,30 @@ int SequencerWidget::handle(int event)
}
size_t tracks, clips;
xtc_project_get_counts(project_, &tracks, &clips);
int row = (Fl::event_y() - y() - 28) / 56;
int row = track_row(Fl::event_y());
if (row < 0 || static_cast<size_t>(row) >= tracks)
return 1;
xtc_track_info t{};
xtc_project_get_track(project_, static_cast<size_t>(row), &t);
if(Fl::event_button()==FL_LEFT_MOUSE && Fl::event_x()>=header_left()+7 &&
Fl::event_x()<header_left()+24 && Fl::event_y()<track_top(static_cast<size_t>(row))+26) {
expand_track(t.id,!track_expanded(t.id)); return 1;
}
if(Fl::event_y()>=track_top(static_cast<size_t>(row))+56) {
if(Fl::event_button()!=FL_LEFT_MOUSE || Fl::event_x()<header_left()+header_width_) return 1;
size_t notes=0; xtc_project_note_count(project_,&notes);
int distance=10; bool hit=false;
velocity_top_=track_top(static_cast<size_t>(row))+56;
for(size_t n=0;n<notes;++n) {
xtc_note_info note{}; xtc_project_get_note(project_,n,&note); int px=0;
if(!velocity_note(note,t.id,px)) continue;
int ny=velocity_top_+velocity_height_-8-static_cast<int>(note.velocity)*(velocity_height_-28)/127;
int d=std::abs(Fl::event_x()-px)+std::abs(Fl::event_y()-ny);
if(d<distance) { distance=d; velocity_original_=note; hit=true; }
}
if(hit) { track_=t.id; clip_=velocity_original_.clip_id; mode_=6; velocity_preview_=velocity_original_.velocity; }
redraw(); return 1;
}
uint64_t previous_clip=clip_;
track_ = t.id;
clip_ = UINT64_MAX;
@ -474,7 +610,7 @@ int SequencerWidget::handle(int event)
uint64_t target = original_.track_id;
if (mode == 1) {
start = xtc_ui::snap_position(start+delta,grid_);
int row = (Fl::event_y() - y() - 28) / 56;
int row = track_row(Fl::event_y());
xtc_track_info t{};
if (row >= 0 && xtc_project_get_track(project_, static_cast<size_t>(row), &t) == XTC_OK)
target = t.id;
@ -887,13 +1023,13 @@ void SequencerWidget::refresh_instruments() try
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(header_left()+10,y()+58+static_cast<int>(i)*56,header_width_-22,20);
entry.widget = new Fl_Choice(header_left()+10,track_top(i)+30,header_width_-22,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(header_left()+10,y()+58+static_cast<int>(i)*56,header_width_-22,20);
entry.widget->resize(header_left()+10,track_top(i)+30,header_width_-22,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,&current) == XTC_OK;

View file

@ -32,6 +32,9 @@ class SequencerWidget : public Fl_Group {
InstrumentSelect instrument_select = nullptr;
Fl_Choice *instrument_choice(uint64_t track) const;
void refresh_instruments();
bool track_expanded(uint64_t track) const;
bool expand_track(uint64_t track, bool expanded);
int track_top(size_t row) const;
bool select(uint64_t track, uint64_t clip = UINT64_MAX);
xtc_result merge_midi();
xtc_result duplicate_track(const char *name);
@ -119,8 +122,9 @@ class SequencerWidget : public Fl_Group {
*
* @param project Borrowed project; must outlive the widget's use of it.
* @param reset Whether to clear selection, cursor, and drag state.
* @param preserve_expansion Keep disclosure state when installing history snapshots.
*/
void set_project(xtc_project *project, bool reset = true);
void set_project(xtc_project *project, bool reset = true, bool preserve_expansion = false);
/**
* @brief Returns the timeline cursor position in musical ticks.
@ -193,6 +197,15 @@ class SequencerWidget : public Fl_Group {
void draw() override;
private:
static constexpr int velocity_height_ = 88;
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 &note, 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;
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_;

View file

@ -1214,6 +1214,56 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-workspace.xtc"); window.do_callback();
}
reset();
{
MainWindow window(false,""); window.show(); auto *s=timeline(&window);
CHECK(window.create_track(true)==XTC_OK);
uint64_t track=s->selected_track(), clip=s->selected_clip(), note_id=0, chord_id=0;
CHECK(xtc_project_add_note(s->project(),clip,960,480,60,100,2,&note_id)==XTC_OK);
CHECK(xtc_project_add_note(s->project(),clip,960,480,64,40,2,&chord_id)==XTC_OK);
CHECK(window.create_track(true)==XTC_OK); // Publish notes and add a second row.
uint64_t second=s->selected_track(), second_clip=s->selected_clip();
int base=s->track_top(1)-s->y();
auto arrow=[&]() {
int ox=Fl::e_x,oy=Fl::e_y,ok=Fl::e_keysym;
Fl::e_x=s->header_left()+15; Fl::e_y=s->track_top(0)+13;
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; CHECK(s->handle(FL_PUSH)==1);
s->handle(FL_RELEASE);
Fl::e_x=ox; Fl::e_y=oy; Fl::e_keysym=ok;
};
arrow(); // Actual disclosure arrow.
CHECK(s->track_expanded(track) && s->track_top(1)-s->y()==base+88);
CHECK(s->instrument_choice(second)->y()==s->track_top(1)+30);
int px=s->header_width()+s->zoom_level();
int top=28+56, ny=top+88-8-100*60/127;
gesture(s,px,ny,0,30);
xtc_note_info note{},chord{};
xtc_project_get_note(s->project(),0,&note); xtc_project_get_note(s->project(),1,&chord);
CHECK(note.id==note_id && note.velocity<100 && note.pitch==60 && note.channel==2 && note.start_tick==960);
CHECK(chord.id==chord_id && chord.velocity==40);
CHECK(window.undo(false)==XTC_OK);
xtc_project_get_note(s->project(),0,&note); CHECK(note.velocity==100 && s->track_expanded(track));
CHECK(window.undo(true)==XTC_OK);
// Escape cancels a preview without changing the model.
xtc_project_get_note(s->project(),0,&note);
int old_x=Fl::e_x,old_y=Fl::e_y,old_key=Fl::e_keysym;
Fl::e_x=s->x()+px; Fl::e_y=s->y()+top+80-static_cast<int>(note.velocity)*60/127;
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; CHECK(s->handle(FL_PUSH)==1);
Fl::e_y-=20; CHECK(s->handle(FL_DRAG)==1);
Fl::e_keysym=FL_Escape; CHECK(s->handle(FL_KEYDOWN)==1);
xtc_note_info unchanged{}; xtc_project_get_note(s->project(),0,&unchanged); CHECK(unchanged.velocity==note.velocity);
Fl::e_x=old_x; Fl::e_y=old_y; Fl::e_keysym=old_key;
gesture(s,s->header_width()+20,s->track_top(1)-s->y()+15,0,0);
CHECK(s->selected_track()==second && s->selected_clip()==second_clip);
CHECK(s->move_track(-1)==XTC_OK && s->track_expanded(track));
CHECK(s->track_top(1)-s->y()==base); // Expansion follows the stable track ID.
CHECK(s->move_track(1)==XTC_OK);
arrow();
CHECK(!s->track_expanded(track) && s->track_top(1)-s->y()==base);
CHECK(!s->expand_track(UINT64_MAX,true));
CHECK(window.new_project()==XTC_OK && !s->track_expanded(track));
window.do_callback();
}
reset();
{
MainWindow window(false,""); window.show(); auto *sequence = timeline(&window);
CHECK(window.create_track(true) == XTC_OK);

View file

@ -64,6 +64,13 @@ int main(int argc,char **argv) {
MainWindow window(false,"");main_window=&window;window.resize(0,0,1400,900);window.show();check(window.open_project(project.c_str()));pump();
auto *s=first<SequencerWidget>(window);if(!s)throw std::runtime_error("Sequencer missing");
auto reset=[&](){window.reset_layout();check(window.open_project(project.c_str()));window.resize(0,20,1400,900);tab("Sequencer");s->select(0,0);s->position(0);check(window.set_loop(3840,7680,true));window.fit_song();pump();};
if(std::getenv("XTC_CAPTURE_VELOCITY_ONLY")) {
reset(); capture("compact-workspace"); capture("sequencer-loop");
check(s->set_zoom(96));
if(!s->expand_track(0,true) || !s->expand_track(1,true)) throw std::runtime_error("Expand track failed");
pump(); capture("track-velocity-expanded");
window.do_callback(); return 0;
}
if(std::getenv("XTC_CAPTURE_MIDI_INPUT_ONLY")) {
reset();
if(window.midi_devices(true).empty())throw std::runtime_error("No real MIDI input sources exposed by the host");
@ -137,6 +144,8 @@ int main(int argc,char **argv) {
window.do_callback();std::puts("Sample-editor captures completed.");return 0;
}
reset();capture("overview");capture("sequencer-loop");capture("arrangement-markers");
check(s->set_zoom(96)); s->expand_track(0,true); s->expand_track(1,true);
pump(); capture("track-velocity-expanded"); reset();
s->position(4800);check(window.loop_section());capture("section-navigation");
dialog("marker-edit",[&]{s->marker_dialog();});
dialog("ripple-time",[&]{s->ripple_dialog(false);});