Added velocity to tracks in the sequencer.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
A promotional composite of the arrangement, piano roll, mixer and track insert views.
|
||||
|
||||
---
|
||||
|
||||
## A compact workspace for composition
|
||||
|
||||

|
||||
|
||||
The sequencer brings track creation, arrangement tools and transport controls into one view.
|
||||
|
||||
---
|
||||
|
||||
## Arrange clips around a loop
|
||||
|
||||

|
||||
|
||||
MIDI clips sit beneath section markers and a highlighted loop range.
|
||||
|
||||
---
|
||||
|
||||
## Edit velocity beneath each track
|
||||
|
||||

|
||||
|
||||
Keys and Bass expand downward to reveal MIDI velocity bars while following tracks move down.
|
||||
|
||||
---
|
||||
|
||||
## Start a new track
|
||||
|
||||

|
||||
|
||||
The Create dropdown opens over the arrangement with track creation choices.
|
||||
|
||||
---
|
||||
|
||||
## Give the song a structure
|
||||
|
||||

|
||||
|
||||
Named Intro, Verse, Chorus and Outro markers identify sections along the ruler.
|
||||
|
||||
---
|
||||
|
||||
## Name an arrangement marker
|
||||
|
||||

|
||||
|
||||
The marker editor provides a name for a position in the song.
|
||||
|
||||
---
|
||||
|
||||
## Navigate song sections
|
||||
|
||||

|
||||
|
||||
The arrangement shows a loop around the current song section.
|
||||
|
||||
---
|
||||
|
||||
## Set the loop boundaries
|
||||
|
||||

|
||||
|
||||
The loop range dialog controls the start and end of repeated playback.
|
||||
|
||||
---
|
||||
|
||||
## Make room in the arrangement
|
||||
|
||||

|
||||
|
||||
The ripple time dialog requests a duration to insert or remove at the cursor.
|
||||
|
||||
---
|
||||
|
||||
## See the inserted space
|
||||
|
||||

|
||||
|
||||
The arrangement shows the gap created by inserting time.
|
||||
|
||||
---
|
||||
|
||||
## Build an independent track layer
|
||||
|
||||

|
||||
|
||||
A duplicated track appears alongside the original arrangement.
|
||||
|
||||
---
|
||||
|
||||
## Split a MIDI clip
|
||||
|
||||

|
||||
|
||||
The arrangement displays separate MIDI regions after a nondestructive split.
|
||||
|
||||
---
|
||||
|
||||
## Bring MIDI regions together
|
||||
|
||||

|
||||
|
||||
Separate MIDI material is consolidated into a single clip.
|
||||
|
||||
---
|
||||
|
||||
## Shape notes and their dynamics
|
||||
|
||||

|
||||
|
||||
The piano roll combines note editing with a dedicated velocity lane.
|
||||
|
||||
---
|
||||
|
||||
## Add timing and velocity variation
|
||||
|
||||

|
||||
|
||||
The humanization dialog exposes timing variation, velocity variation and a repeatable seed.
|
||||
|
||||
---
|
||||
|
||||
## Build chords from intervals
|
||||
|
||||

|
||||
|
||||
The chord dialog accepts intervals for constructing notes around a root.
|
||||
|
||||
---
|
||||
|
||||
## Review the generated chords
|
||||
|
||||

|
||||
|
||||
The piano roll displays the notes written by the chord builder.
|
||||
|
||||
---
|
||||
|
||||
## Turn chords into patterns
|
||||
|
||||

|
||||
|
||||
The arpeggio dialog controls step duration, gate and note direction.
|
||||
|
||||
---
|
||||
|
||||
## Review an arpeggiated pattern
|
||||
|
||||

|
||||
|
||||
The piano roll shows the sequence of notes produced by arpeggiation.
|
||||
|
||||
---
|
||||
|
||||
## Spread chord notes in time
|
||||
|
||||

|
||||
|
||||
The strumming dialog configures spacing and direction for chord notes.
|
||||
|
||||
---
|
||||
|
||||
## Review a strummed chord
|
||||
|
||||

|
||||
|
||||
The piano roll shows staggered note starts after applying chord strumming.
|
||||
|
||||
---
|
||||
|
||||
## Exchange standard MIDI files
|
||||
|
||||

|
||||
|
||||
The File menu exposes MIDI import and selected-clip export commands.
|
||||
|
||||
---
|
||||
|
||||
## Bring MIDI into the song
|
||||
|
||||

|
||||
|
||||
An exported MIDI clip has been imported into the arrangement.
|
||||
|
||||
---
|
||||
|
||||
## Inspect imported MIDI notes
|
||||
|
||||

|
||||
|
||||
The piano roll displays imported notes and their velocities.
|
||||
|
||||
---
|
||||
|
||||
## Arrange audio alongside MIDI
|
||||
|
||||

|
||||
|
||||
An imported audio clip displays its waveform directly in the sequencer.
|
||||
|
||||
---
|
||||
|
||||
## Adjust clip gain and fades
|
||||
|
||||

|
||||
|
||||
The audio clip properties dialog exposes nondestructive gain and fade settings.
|
||||
|
||||
---
|
||||
|
||||
## Choose a normalization target
|
||||
|
||||

|
||||
|
||||
The normalization dialog requests a target peak level in dBFS.
|
||||
|
||||
---
|
||||
|
||||
## Review normalized audio
|
||||
|
||||

|
||||
|
||||
The arrangement shows the audio clip after normalization.
|
||||
|
||||
---
|
||||
|
||||
## Work directly with an audio source
|
||||
|
||||

|
||||
|
||||
The sample editor displays waveform selection, statistics and processing controls.
|
||||
|
||||
---
|
||||
|
||||
## Find attacks for slicing
|
||||
|
||||

|
||||
|
||||
Detected source attacks appear alongside the sample editor's slicing controls.
|
||||
|
||||
---
|
||||
|
||||
## Collect portable project media
|
||||
|
||||

|
||||
|
||||
The collection prompt requests a destination folder for the project and its media.
|
||||
|
||||
---
|
||||
|
||||
## Monitor the mix during playback
|
||||
|
||||

|
||||
|
||||
The mixer shows track controls and meters driven by actual JACK playback callbacks.
|
||||
|
||||
---
|
||||
|
||||
## Shape the mix over time
|
||||
|
||||

|
||||
|
||||
The mixer presents track controls together with automation editing.
|
||||
|
||||
---
|
||||
|
||||
## Mix in a smaller window
|
||||
|
||||

|
||||
|
||||
The mixer is shown at the supported 1220 × 720 minimum main-window size.
|
||||
|
||||
---
|
||||
|
||||
## Navigate a larger session
|
||||
|
||||

|
||||
|
||||
A session with additional tracks demonstrates scrolling through the mixer.
|
||||
|
||||
---
|
||||
|
||||
## Explore the signal flow
|
||||
|
||||

|
||||
|
||||
The modular graph displays connected processing nodes and their routing.
|
||||
|
||||
---
|
||||
|
||||
## Chain installed VST3 effects
|
||||
|
||||

|
||||
|
||||
The Keys track hosts ZamEQ2, ZamAutoSat and ZamDelay in its insert graph.
|
||||
|
||||
---
|
||||
|
||||
## Inspect a hosted VST3 effect
|
||||
|
||||

|
||||
|
||||
The identity view reports the plugin and the current adapter capabilities.
|
||||
|
||||
---
|
||||
|
||||
## Create parallel effect branches
|
||||
|
||||

|
||||
|
||||
The insert graph shows parallel wet and dry signal paths.
|
||||
|
||||
---
|
||||
|
||||
## Start with a Dub Echo rack
|
||||
|
||||

|
||||
|
||||
The insert graph displays the internal Dub Echo template and its idle meters.
|
||||
|
||||
---
|
||||
|
||||
## Manage a complete effect rack
|
||||
|
||||

|
||||
|
||||
Rack actions expose bulk operations, comparison and chain clipboard controls.
|
||||
|
||||
---
|
||||
|
||||
## Browse the plugin library
|
||||
|
||||

|
||||
|
||||
The library presents available plugins for selection.
|
||||
|
||||
---
|
||||
|
||||
## Inspect plugin descriptors
|
||||
|
||||

|
||||
|
||||
The descriptor browser displays metadata from repository plugin fixtures.
|
||||
|
||||
---
|
||||
|
||||
## Adjust hosted plugin controls
|
||||
|
||||

|
||||
|
||||
The control editor exposes parameters provided by a repository plugin fixture.
|
||||
|
||||
---
|
||||
|
||||
## Choose an internal instrument
|
||||
|
||||

|
||||
|
||||
The instrument menu presents the built-in devices available to a track.
|
||||
|
||||
---
|
||||
|
||||
## Configure an internal delay
|
||||
|
||||

|
||||
|
||||
The delay editor presents its available device controls.
|
||||
|
||||
---
|
||||
|
||||
## Configure an internal reverb
|
||||
|
||||

|
||||
|
||||
The reverb editor presents its available device controls.
|
||||
|
||||
---
|
||||
|
||||
## Inspect a CLAP instrument
|
||||
|
||||

|
||||
|
||||
The host displays descriptor information for a CLAP instrument.
|
||||
|
||||
---
|
||||
|
||||
## Inspect a VST2 instrument
|
||||
|
||||

|
||||
|
||||
The host displays descriptor information for a VST2 instrument.
|
||||
|
||||
---
|
||||
|
||||
## Inspect a VST3 instrument
|
||||
|
||||

|
||||
|
||||
The host displays descriptor information for a VST3 instrument.
|
||||
|
||||
---
|
||||
|
||||
## Host the Steinberg SDK example effect
|
||||
|
||||

|
||||
|
||||
The actual SDK-built Half Gain effect appears in xtc's track insert graph.
|
||||
|
||||
---
|
||||
|
||||
## Inspect the SDK effect identity
|
||||
|
||||

|
||||
|
||||
The host reports the Steinberg SDK effect's identity and current adapter limits.
|
||||
|
||||
---
|
||||
|
||||
## Discover the SDK sine instrument
|
||||
|
||||

|
||||
|
||||
The plugin chooser displays the instrument from the Steinberg SDK example bundle.
|
||||
|
||||
---
|
||||
|
||||
## Discover the JUCE instrument
|
||||
|
||||

|
||||
|
||||
The plugin chooser displays the actual JUCE VST3 instrument bundle.
|
||||
|
||||
---
|
||||
|
||||
## Play with the Soft Keys interface
|
||||
|
||||

|
||||
|
||||
The actual JUCE editor is rendered directly, showing presets, parameter controls and an on-screen keyboard.
|
||||
|
||||
---
|
||||
|
||||
## Connect MIDI controllers to parameters
|
||||
|
||||

|
||||
|
||||
The controller mapping interface exposes MIDI control assignments.
|
||||
|
||||
---
|
||||
|
||||
## Configure audio preferences
|
||||
|
||||

|
||||
|
||||
The audio preferences tab presents the application's audio settings.
|
||||
|
||||
---
|
||||
|
||||
## Choose MIDI sources and destinations
|
||||
|
||||

|
||||
|
||||
MIDI preferences provide port choices, channel filtering and thru settings.
|
||||
|
||||
---
|
||||
|
||||
## Browse real MIDI input devices
|
||||
|
||||

|
||||
|
||||
The open input dropdown lists actual MIDI sources exposed by the host; no route is selected.
|
||||
|
||||
---
|
||||
|
||||
## Understand MIDI setup and defaults
|
||||
|
||||

|
||||
|
||||
The preferences help page explains device routing and MIDI editing defaults.
|
||||
|
||||
---
|
||||
|
||||
## Configure plugin preferences
|
||||
|
||||

|
||||
|
||||
The plugin preferences tab presents plugin discovery settings.
|
||||
|
||||
---
|
||||
|
||||
## Adjust the workspace preferences
|
||||
|
||||

|
||||
|
||||
The interface preferences tab presents appearance and layout settings.
|
||||
|
||||
---
|
||||
|
||||
## Set editing defaults
|
||||
|
||||

|
||||
|
||||
The editing preferences tab includes defaults for new MIDI notes.
|
||||
|
||||
---
|
||||
|
||||
## Set project defaults
|
||||
|
||||

|
||||
|
||||
The project defaults tab presents settings used when creating project content.
|
||||
|
||||
---
|
||||
|
||||
## Configure transport preferences
|
||||
|
||||

|
||||
|
||||
The transport preferences tab presents playback and navigation settings.
|
||||
|
||||
---
|
||||
|
||||
## Find an application command
|
||||
|
||||

|
||||
|
||||
The command palette provides access to the current application action list.
|
||||
|
||||
---
|
||||
|
||||
## Keep transport controls within reach
|
||||
|
||||

|
||||
|
||||
The transport floats in a separate window with BPM and loop settings.
|
||||
|
||||
---
|
||||
|
||||
## Give the mixer its own window
|
||||
|
||||

|
||||
|
||||
The mixer is detached from the main workspace.
|
||||
|
||||
---
|
||||
|
||||
## Spread the workspace across windows
|
||||
|
||||

|
||||
|
||||
Transport and mixer panels float alongside the main application window.
|
||||
|
||||
---
|
||||
|
||||
## Arrange in a separate window
|
||||
|
||||

|
||||
|
||||
The sequencer is detached from the main workspace.
|
||||
|
||||
---
|
||||
|
||||
## Route signals in a separate window
|
||||
|
||||

|
||||
|
||||
The modular graph is detached from the main workspace.
|
||||
|
||||
---
|
||||
|
||||
## About xtc
|
||||
|
||||

|
||||
|
||||
The About dialog presents application information.
|
||||
|
|
@ -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)
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 518 KiB After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
BIN
screenshots/track-velocity-expanded.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ¬e, 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_,¬es);
|
||||
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,¬e); 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_,¬es);
|
||||
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,¬e); 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,¤t) == XTC_OK;
|
||||
|
|
|
|||
|
|
@ -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 ¬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;
|
||||
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_;
|
||||
|
|
|
|||
|
|
@ -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,¬e_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,¬e); 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,¬e); 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,¬e);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);});
|
||||
|
|
|
|||