Phase 33: add a flat modern DAW workspace and track creation workflow

This commit is contained in:
Johannes Findeisen 2026-10-27 18:04:39 +01:00
commit d800a6392e
19 changed files with 711 additions and 115 deletions

View file

@ -279,3 +279,8 @@ VST2 compatibility uses original ABI declarations. `make test-modern-plugins`
runs original shared-library fixtures without a server; `make test-gui-modern-plugins`
adds GUI/browser/undo/reopen tests under Xvfb. The fixtures also run in `make test`
and therefore in sanitizer/release builds. Notices are installed with LICENSE.
Phase 33 uses a header-only FLTK theme and existing drawing primitives; no new
GUI dependency or generated asset is required. `make test-gui-instruments` also
exercises sidebar track creation, rollback, undo/reopen and minimum/large layouts.
See WORKSPACE.md for the default size and manual visual verification.

View file

@ -61,3 +61,10 @@ layouts remain unsupported. No third-party modern plugin has been qualified.
Phase 32 adds CLAP/VST2/VST3 mono instruments bound to MIDI tracks, sample-offset
notes, a saved live target, track mixing, GUI assignment/undo and v12 persistence.
See PLUGIN_HOSTING.md for compatibility and controller/state limits.
Phase 33 adds a flat DAW workspace, shared dark theme, track-creation sidebar,
read-only musical transport/track inspector, colored track clips and real MIDI
previews. New MIDI/audio track commands are undoable and prepare candidates
before adoption. The default window is 1280 × 800, minimum 1220 × 720.
No dependency or project-format change. See WORKSPACE.md and the new
`modern-session.xtc` example; existing engine/plugin limitations still apply.

View file

@ -931,6 +931,35 @@ installation, reproducible source releases and relocatable binary packages pass.
---
## Phase 33 — Flat Modern DAW Workspace
### Goal
Give xtc a clear, modern DAW presentation for creating tracks and arranging
music, with simple flat controls and no 3D styling or proprietary assets.
### Tasks and acceptance criteria
- [x] Introduce one shared dark palette, typography, flat controls and focus/selection accents.
- [x] Make track creation, import, instrument loading and MIDI editing visible in a persistent sidebar.
- [x] Create/select MIDI tracks with clips and audio tracks transactionally, with undo and failure rollback.
- [x] Organize transport, musical position/BPM, main editors, inspector and status in a resizable workspace.
- [x] Draw colored track headers, clear clip selection, bar/beat grids and actual MIDI note previews.
- [x] Preserve existing editing, mixer/automation, graph, plugin, recording and keyboard behavior.
- [x] Add an original multi-track example and document the workflow and retained limitations.
- [x] Verify GUI actions, minimum/large layouts, core regressions and release packaging.
### Implementation status
Implemented in the FLTK GUI without new dependencies, engine changes or a project
format change. Default workspace 1280 × 800; supported minimum 1220 × 720.
Track creation is a single undoable prepared-model edit. The transport and
sidebar inspector are read-only displays; existing editors retain their controls.
Original four-track example visually checked; GUI tests cover creation,
selection, rollback, undo/reopen and minimum/large window sizes. This phase
modernizes presentation and workflow; it does not claim commercial-DAW feature
parity or add docking, scanning or mixer channel-strip functionality.
See WORKSPACE.md for the workflow, tests and limits.
---
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification.

View file

@ -199,3 +199,10 @@ Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.
## Phase 33 workspace
The flat workspace puts New MIDI/audio track, WAV import and instrument loading
in a persistent sidebar, with a musical transport display and selected-track
overview. See [WORKSPACE.md](WORKSPACE.md) for shortcuts and workflow. Open
`modern-session.xtc` in the installed examples for an original four-track arrangement.

57
docs/WORKSPACE.md Normal file
View file

@ -0,0 +1,57 @@
# Flat DAW workspace
Phase 33 presents the existing xtc editors in a consistent flat workspace. It uses
FLTK vector drawing and system fonts: no images, shadows, bevels, 3D rendering,
external theme packages or new runtime dependencies.
The default window is 1280 × 800; the supported minimum is 1220 × 720. The main
editor grows with the window. Use the scrollbars for longer arrangements and
larger track counts. The sidebar stays available across Sequencer, Mixer and
Modular Graph views. Ctrl+1/2/3 switches those views; Ctrl+4 opens the piano roll.
## Create and arrange
- **New MIDI track** (Ctrl+Shift+M) creates a named track and a one-bar MIDI clip
at the cursor. The new clip and mixer track are selected together. **Edit MIDI
notes** opens its piano roll. Creation is one undo step.
- **New audio track** (Ctrl+Shift+A) creates an empty named track. **Import WAV**
imports into the selected track, or creates one when none is selected.
- **Load instrument plugin** opens the instrument browser for the selected
sequencer track. Unassigned MIDI tracks retain the built-in sine instrument.
- Existing Add track/region/MIDI clip, duplication, trimming, split, rename and
delete commands remain available. F2 renames the selection. Drag clips to move
them and their edges to trim them; click the ruler to seek while stopped.
Creating a track uses candidate preparation, so module, capacity or allocation
failures retain the current project. As with other project replacements, the
engine stops and the transport returns to the beginning. Track creation is
unavailable while recording. No track type restriction is added to the model:
ordinary tracks still accept the existing audio and MIDI workflows.
Track headers show a stable color, name, order and mute/solo state. Clip colors
follow track identity through reordering. MIDI previews show real note starts,
durations and relative pitches; waveforms continue to show loaded audio data.
The selected clip has a light outline. A teal playhead and bar grid make musical
time visible. The transport display reads **bar : beat : tick** (4/4, 960 ticks
per beat); BPM follows the project tempo. These displays are read-only.
The sidebar inspector follows the sequencer selection, showing track name,
gain/pan, mute/solo and assigned instrument. Use the Mixer for controls, routing,
meters and automation; the sidebar is an overview, not another mixer. Status and
errors remain visible below the editor. Existing engine connection, transport,
recording and keyboard safety behavior is retained.
## Example and verification
Open `examples/projects/modern-session.xtc` for an original four-track, four-bar
MIDI arrangement with 16 clips and 128 notes. It needs no media or plugins. It is
also installed with the project examples. Connect JACK playback ports before
listening. Work on a copy if the installed examples are read-only.
GUI tests exercise sidebar creation, selected clip/editor access, undo/redo,
failed preparation, save/reopen and layout at 1220 × 720 and 1600 × 1000. The
running example was also inspected at 1280 × 800. Existing GUI plugin, live JACK
and workflow tests cover the retained commands. A modern presentation does not
add commercial-DAW features: detachable/resizable docks, mixer channel strips,
automatic plugin scanning, custom plugin editors and new audio/MIDI features
remain outside this phase. Existing plugin and engine limits still apply.

View file

@ -5,3 +5,8 @@ repository v10 fixture. It contains one A4 MIDI note, a gain node and an output.
Open it in xtc and Play after connecting JACK output ports to hear it. It needs
no media or plugins. `xtc-check --validate midi-demo.xtc` works without JACK.
Save a working copy in your own folder; installed examples may be read-only.
`modern-session.xtc` is an original MIT-licensed four-track, four-bar arrangement
for the Phase 33 workspace. Its 16 MIDI clips contain 128 notes for built-in sine
voices. It needs no plugins or media. Open it to explore colored tracks, MIDI
previews, clip selection and the piano roll. Save a copy before editing.

View file

@ -0,0 +1,236 @@
XTC_PROJECT 12
APP 0 1 0
TEMPO 120000
IDS 4 16
GRAPH 1 2 2 1
NODE 0 6 00000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 4
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
TRACK 1 42617373
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
TRACK 2 417270656767696f
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
TRACK 3 4d656c6f6479
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 16
CLIP 0 0 0 3840 0 3840 4b657973
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 1 0 3840 3840 0 3840 4b657973
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 2 0 7680 3840 0 3840 4b657973
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 3 0 11520 3840 0 3840 4b657973
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 4 1 0 3840 0 3840 42617373
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 5 1 3840 3840 0 3840 42617373
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 6 1 7680 3840 0 3840 42617373
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 7 1 11520 3840 0 3840 42617373
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 8 2 0 3840 0 3840 417270656767696f
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 9 2 3840 3840 0 3840 417270656767696f
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 10 2 7680 3840 0 3840 417270656767696f
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 11 2 11520 3840 0 3840 417270656767696f
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 12 3 0 3840 0 3840 4d656c6f6479
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 13 3 3840 3840 0 3840 4d656c6f6479
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 14 3 7680 3840 0 3840 4d656c6f6479
ASSET -
MIDI 1
AUDIO 3f800000 0 0
CLIP 15 3 11520 3840 0 3840 4d656c6f6479
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 128 128
NOTE 0 0 0 360 60 72 0
NOTE 1 0 480 360 64 72 0
NOTE 2 0 960 360 67 72 0
NOTE 3 0 1440 360 64 72 0
NOTE 4 0 1920 360 62 72 0
NOTE 5 0 2400 360 65 72 0
NOTE 6 0 2880 360 69 72 0
NOTE 7 0 3360 360 65 72 0
NOTE 8 1 0 360 60 72 0
NOTE 9 1 480 360 64 72 0
NOTE 10 1 960 360 67 72 0
NOTE 11 1 1440 360 64 72 0
NOTE 12 1 1920 360 62 72 0
NOTE 13 1 2400 360 65 72 0
NOTE 14 1 2880 360 69 72 0
NOTE 15 1 3360 360 65 72 0
NOTE 16 2 0 360 60 72 0
NOTE 17 2 480 360 64 72 0
NOTE 18 2 960 360 67 72 0
NOTE 19 2 1440 360 64 72 0
NOTE 20 2 1920 360 62 72 0
NOTE 21 2 2400 360 65 72 0
NOTE 22 2 2880 360 69 72 0
NOTE 23 2 3360 360 65 72 0
NOTE 24 3 0 360 60 72 0
NOTE 25 3 480 360 64 72 0
NOTE 26 3 960 360 67 72 0
NOTE 27 3 1440 360 64 72 0
NOTE 28 3 1920 360 62 72 0
NOTE 29 3 2400 360 65 72 0
NOTE 30 3 2880 360 69 72 0
NOTE 31 3 3360 360 65 72 0
NOTE 32 4 0 420 36 88 0
NOTE 33 4 480 420 36 88 0
NOTE 34 4 960 420 43 88 0
NOTE 35 4 1440 420 43 88 0
NOTE 36 4 1920 420 38 88 0
NOTE 37 4 2400 420 38 88 0
NOTE 38 4 2880 420 45 88 0
NOTE 39 4 3360 420 45 88 0
NOTE 40 5 0 420 36 88 0
NOTE 41 5 480 420 36 88 0
NOTE 42 5 960 420 43 88 0
NOTE 43 5 1440 420 43 88 0
NOTE 44 5 1920 420 38 88 0
NOTE 45 5 2400 420 38 88 0
NOTE 46 5 2880 420 45 88 0
NOTE 47 5 3360 420 45 88 0
NOTE 48 6 0 420 36 88 0
NOTE 49 6 480 420 36 88 0
NOTE 50 6 960 420 43 88 0
NOTE 51 6 1440 420 43 88 0
NOTE 52 6 1920 420 38 88 0
NOTE 53 6 2400 420 38 88 0
NOTE 54 6 2880 420 45 88 0
NOTE 55 6 3360 420 45 88 0
NOTE 56 7 0 420 36 88 0
NOTE 57 7 480 420 36 88 0
NOTE 58 7 960 420 43 88 0
NOTE 59 7 1440 420 43 88 0
NOTE 60 7 1920 420 38 88 0
NOTE 61 7 2400 420 38 88 0
NOTE 62 7 2880 420 45 88 0
NOTE 63 7 3360 420 45 88 0
NOTE 64 8 0 360 72 72 0
NOTE 65 8 480 360 76 72 0
NOTE 66 8 960 360 79 72 0
NOTE 67 8 1440 360 76 72 0
NOTE 68 8 1920 360 74 72 0
NOTE 69 8 2400 360 77 72 0
NOTE 70 8 2880 360 81 72 0
NOTE 71 8 3360 360 77 72 0
NOTE 72 9 0 360 72 72 0
NOTE 73 9 480 360 76 72 0
NOTE 74 9 960 360 79 72 0
NOTE 75 9 1440 360 76 72 0
NOTE 76 9 1920 360 74 72 0
NOTE 77 9 2400 360 77 72 0
NOTE 78 9 2880 360 81 72 0
NOTE 79 9 3360 360 77 72 0
NOTE 80 10 0 360 72 72 0
NOTE 81 10 480 360 76 72 0
NOTE 82 10 960 360 79 72 0
NOTE 83 10 1440 360 76 72 0
NOTE 84 10 1920 360 74 72 0
NOTE 85 10 2400 360 77 72 0
NOTE 86 10 2880 360 81 72 0
NOTE 87 10 3360 360 77 72 0
NOTE 88 11 0 360 72 72 0
NOTE 89 11 480 360 76 72 0
NOTE 90 11 960 360 79 72 0
NOTE 91 11 1440 360 76 72 0
NOTE 92 11 1920 360 74 72 0
NOTE 93 11 2400 360 77 72 0
NOTE 94 11 2880 360 81 72 0
NOTE 95 11 3360 360 77 72 0
NOTE 96 12 0 360 79 72 0
NOTE 97 12 480 360 76 72 0
NOTE 98 12 960 360 74 72 0
NOTE 99 12 1440 360 72 72 0
NOTE 100 12 1920 360 77 72 0
NOTE 101 12 2400 360 74 72 0
NOTE 102 12 2880 360 72 72 0
NOTE 103 12 3360 360 69 72 0
NOTE 104 13 0 360 79 72 0
NOTE 105 13 480 360 76 72 0
NOTE 106 13 960 360 74 72 0
NOTE 107 13 1440 360 72 72 0
NOTE 108 13 1920 360 77 72 0
NOTE 109 13 2400 360 74 72 0
NOTE 110 13 2880 360 72 72 0
NOTE 111 13 3360 360 69 72 0
NOTE 112 14 0 360 79 72 0
NOTE 113 14 480 360 76 72 0
NOTE 114 14 960 360 74 72 0
NOTE 115 14 1440 360 72 72 0
NOTE 116 14 1920 360 77 72 0
NOTE 117 14 2400 360 74 72 0
NOTE 118 14 2880 360 72 72 0
NOTE 119 14 3360 360 69 72 0
NOTE 120 15 0 360 79 72 0
NOTE 121 15 480 360 76 72 0
NOTE 122 15 960 360 74 72 0
NOTE 123 15 1440 360 72 72 0
NOTE 124 15 1920 360 77 72 0
NOTE 125 15 2400 360 74 72 0
NOTE 126 15 2880 360 72 72 0
NOTE 127 15 3360 360 69 72 0
AUTOMATION 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
TRACK_INSTRUMENTS 0
LIVE_INSTRUMENT 18446744073709551615
END

View file

@ -1,3 +1,4 @@
#include "theme.hpp"
#include "graph_widget.hpp"
#include "xtc/xtc_plugin.h"
#include <FL/Fl.H>
@ -366,7 +367,7 @@ void GraphWidget::draw()
selected_node_ == edge.source_node || selected_node_ == edge.destination_node;
xtc_routing_info route{};
bool audible_path = routing(edge.destination_node, route) && route.reaches_output;
fl_color(selected ? FL_YELLOW : audible_path ? fl_rgb_color(100, 200, 190) : fl_rgb_color(110, 115, 125));
fl_color(selected ? xtc_ui::accent() : audible_path ? fl_rgb_color(100, 200, 190) : fl_rgb_color(110, 115, 125));
fl_line_style(FL_SOLID, selected ? 3 : 2);
fl_line(x() + sx, y() + sy, x() + dx, y() + dy);
double length = std::hypot(dx-sx, dy-sy);
@ -388,7 +389,7 @@ void GraphWidget::draw()
: node.type == XTC_NODE_OSCILLATOR || node.type == XTC_NODE_AUDIO || node.type == XTC_NODE_INSTRUMENT
? fl_rgb_color(35, 80, 75) : fl_rgb_color(49, 65, 85));
fl_rectf(left, top, node_width, node_height);
fl_color(selected_node_ == node.id ? FL_YELLOW : fl_rgb_color(115, 130, 145));
fl_color(selected_node_ == node.id ? xtc_ui::accent() : fl_rgb_color(115, 130, 145));
fl_rect(left, top, node_width, node_height);
fl_color(FL_WHITE);
fl_font(FL_HELVETICA_BOLD, 12);
@ -415,7 +416,7 @@ void GraphWidget::draw()
fl_pop_clip();
xtc_routing_info route{};
if (routing(node.id, route) && !route.reaches_output) {
fl_color(FL_YELLOW);
fl_color(xtc_ui::accent());
fl_draw("! no output path", left + 10, top + 72);
}
for (size_t port = 0U; port < 2U; ++port) {
@ -431,7 +432,7 @@ void GraphWidget::draw()
if (wiring_) {
int px, py;
if (port_position(wire_node_, wire_port_, px, py)) {
fl_color(FL_YELLOW);
fl_color(xtc_ui::accent());
fl_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_);
}
}

View file

@ -39,10 +39,6 @@ int main(int argc, char **argv)
argv[1] = nullptr;
}
try {
Fl::scheme("flat");
Fl::background(34, 36, 40);
Fl::background2(24, 26, 30);
Fl::foreground(230, 232, 236);
MainWindow window;
if (project_path) {
xtc_result result = window.open_project(project_path);

View file

@ -4,6 +4,7 @@
#include "piano_roll.hpp"
#include "mixer_widget.hpp"
#include "plugin_dialog.hpp"
#include "theme.hpp"
#include <FL/Fl_Native_File_Chooser.H>
#include "xtc/xtc_version.h"
@ -60,8 +61,9 @@ class BusyOperation {
* discarding unsaved changes.
*/
MainWindow::MainWindow(bool prompt_unsaved)
: Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved)
: Fl_Double_Window(1280, 800, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved)
{
xtc_ui::initialize();
xtc_project *initial = nullptr;
xtc_result result = xtc_project_create(&initial);
if (result != XTC_OK)
@ -73,17 +75,17 @@ MainWindow::MainWindow(bool prompt_unsaved)
throw std::runtime_error(xtc_result_string(result));
history_.reset(history);
begin();
auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
auto *menu = new Fl_Menu_Bar(0, 0, 1280, 28);
menu_ = menu;
menu->add("Edit/Undo", FL_CTRL + 'z', history_callback, this);
menu->add("Edit/Redo", FL_CTRL + FL_SHIFT + 'z', history_callback, this);
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
menu->add("File/New", FL_CTRL + 'n', file_callback, this);
menu->add("File/Open", FL_CTRL + 'o', file_callback, this);
menu->add("File/Save", FL_CTRL + 's', file_callback, this);
menu->add("File/Save As", FL_CTRL + FL_SHIFT + 's', file_callback, this);
menu->add("File/Import WAV", FL_CTRL + 'i', audio_file_callback, this);
menu->add("File/Relink selected audio", 0, audio_file_callback, this);
menu->add("Edit/Undo", FL_CTRL + 'z', history_callback, this);
menu->add("Edit/Redo", FL_CTRL + FL_SHIFT + 'z', history_callback, this);
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this);
menu->add("Audio/Panic", FL_F + 9, panic_callback, this);
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
@ -99,6 +101,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("Edit/Selected plugin automation", 0, plugin_callback, this);
menu->add("Edit/Selected plugin control", 0, plugin_callback, this);
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
menu->add("Audio/Start engine", FL_F + 5, play_callback, this);
menu->add("Audio/Stop engine", FL_F + 8, stop_callback, this);
menu->add("Audio/Reconnect", FL_F + 6, reconnect_callback, this);
@ -135,35 +138,35 @@ MainWindow::MainWindow(bool prompt_unsaved)
(void)static_cast<MainWindow *>(c)->return_to_start();
}, this);
home_->tooltip("Stop and return to the project beginning (Home). Unavailable while recording.");
indicator_ = new Fl_Box(668, 40, 320, 30, "Audio: disconnected");
indicator_ = new Fl_Box(1000, 40, 268, 30, "Audio: disconnected");
indicator_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
indicator_->labelfont(FL_BOLD);
indicator_->labelsize(12);
auto *hint = new Fl_Box(12, 76, 976, 34,
"Start: Add track, Add MIDI clip, then Piano roll to draw notes; or Import WAV (Ctrl+I).\n"
"Play with F5. Connect JACK output/output_right to playback ports to hear audio.");
transport_position_ = new Fl_Box(680,40,190,30,"001 : 1 : 000");
transport_position_->tooltip("Musical position: bar : beat : tick (4/4, 960 ticks per beat)");
tempo_display_ = new Fl_Box(880,40,110,30,"120.0 BPM");
auto *hint = new Fl_Box(12,82,1256,28,"xtc / ARRANGEMENT Create tracks, arrange clips and shape your sound.");
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
hint->labelsize(12);
tabs_ = new Fl_Tabs(8, 114, 984, 470);
tabs_ = new Fl_Tabs(216, 126, 1056, 578);
tabs_->begin();
auto *sequencer = new Fl_Group(8, 140, 984, 444, "Sequencer");
auto *sequencer = new Fl_Group(216, 158, 1056, 546, "Sequencer");
sequencer->begin();
auto *add_track = new Fl_Button(20, 150, 100, 26, "Add track");
auto *add_region = new Fl_Button(128, 150, 110, 26, "Add region");
auto *delete_region = new Fl_Button(246, 150, 130, 26, "Delete region/track");
auto *zoom_in = new Fl_Button(384, 150, 80, 26, "Zoom +");
auto *zoom_out = new Fl_Button(472, 150, 80, 26, "Zoom -");
auto *add_midi = new Fl_Button(560, 150, 110, 26, "Add MIDI clip");
auto *piano = new Fl_Button(678, 150, 100, 26, "Piano roll");
auto *add_track = new Fl_Button(228, 168, 100, 26, "Add track");
auto *add_region = new Fl_Button(336, 168, 110, 26, "Add region");
auto *delete_region = new Fl_Button(454, 168, 130, 26, "Delete region/track");
auto *zoom_in = new Fl_Button(592, 168, 80, 26, "Zoom +");
auto *zoom_out = new Fl_Button(680, 168, 80, 26, "Zoom -");
auto *add_midi = new Fl_Button(768, 168, 110, 26, "Add MIDI clip");
auto *piano = new Fl_Button(886, 168, 100, 26, "Piano roll");
piano->callback(piano_callback, this);
auto *timeline_hint =
new Fl_Box(20, 548, 956, 28,
new Fl_Box(228, 666, 1032, 28,
"Drag clips to move, edges to trim. Click the ruler while stopped to seek; Home returns to start.");
timeline_hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
timeline_hint->labelsize(12);
auto *timeline_scroll = new Fl_Scroll(20, 188, 956, 350);
sequencer_ = new SequencerWidget(20, 188, project_.get(), seek, this);
auto *timeline_scroll = new Fl_Scroll(228, 206, 1032, 452);
sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this);
timeline_scroll->end();
sequencer_->before_edit = before_arrangement_edit;
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
@ -214,9 +217,9 @@ MainWindow::MainWindow(bool prompt_unsaved)
sequencer_);
sequencer->resizable(timeline_scroll);
sequencer->end();
auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer");
auto *mixer = new Fl_Group(216, 158, 1056, 546, "Mixer");
mixer->begin();
mixer_ = new MixerWidget(20, 150, 956, 420, project_.get(), before_arrangement_edit, this);
mixer_ = new MixerWidget(228, 168, 1032, 522, project_.get(), before_arrangement_edit, this);
mixer_->callback(
[](Fl_Widget *, void *c) {
static_cast<MainWindow *>(c)->mark_dirty();
@ -224,58 +227,58 @@ MainWindow::MainWindow(bool prompt_unsaved)
this);
mixer->resizable(mixer_);
mixer->end();
auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph");
auto *graph = new Fl_Group(216, 158, 1056, 546, "Modular Graph");
graph->begin();
graph_state_ = new Fl_Box(20, 182, 956, 24, "Graph: offline");
graph_state_ = new Fl_Box(228, 200, 956, 24, "Graph: offline");
graph_state_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
graph_state_->labelsize(12);
auto *feedback =
new Fl_Box(20, 548, 956, 28,
new Fl_Box(228, 666, 1032, 28,
"Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan.");
feedback->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
feedback->labelsize(12);
auto *scroll = new Fl_Scroll(20, 212, 956, 328);
graph_widget_ = new GraphWidget(20, 212, 2000, 4000, xtc_project_graph(project_.get()), edit_graph, this, feedback);
auto *scroll = new Fl_Scroll(228, 230, 1032, 430);
graph_widget_ = new GraphWidget(228, 230, 2000, 4000, xtc_project_graph(project_.get()), edit_graph, this, feedback);
scroll->end();
menu->add("View/Arrange graph by signal flow", 0, [](Fl_Widget *, void *c) { (void)static_cast<GraphWidget *>(c)->auto_layout(); }, graph_widget_);
menu->add("Edit/Insert unity gain in selected wire", 0, [](Fl_Widget *, void *c) { (void)static_cast<GraphWidget *>(c)->insert_gain(); }, graph_widget_);
auto *oscillator = new Fl_Button(20, 150, 130, 26, "Add oscillator");
auto *oscillator = new Fl_Button(228, 168, 130, 26, "Add oscillator");
oscillator->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->add_node(XTC_NODE_OSCILLATOR);
},
graph_widget_);
auto *gain = new Fl_Button(158, 150, 110, 26, "Add gain");
auto *gain = new Fl_Button(366, 168, 110, 26, "Add gain");
gain->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->add_node(XTC_NODE_GAIN);
},
graph_widget_);
auto *meter = new Fl_Button(276, 150, 110, 26, "Add meter");
auto *meter = new Fl_Button(484, 168, 110, 26, "Add meter");
meter->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->add_node(XTC_NODE_METER);
},
graph_widget_);
auto *output = new Fl_Button(394, 150, 110, 26, "Add output");
auto *output = new Fl_Button(602, 168, 110, 26, "Add output");
output->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->add_node(XTC_NODE_OUTPUT);
},
graph_widget_);
auto *audio = new Fl_Button(650, 150, 130, 26, "Add audio source");
auto *audio = new Fl_Button(858, 168, 130, 26, "Add audio source");
audio->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->add_node(XTC_NODE_AUDIO);
},
graph_widget_);
auto *instrument = new Fl_Button(788, 150, 130, 26, "Add instrument");
auto *instrument = new Fl_Button(996, 168, 130, 26, "Add instrument");
instrument->callback(
[](Fl_Widget *, void *c) {
(void)static_cast<GraphWidget *>(c)->add_node(XTC_NODE_INSTRUMENT);
},
graph_widget_);
auto *remove = new Fl_Button(512, 150, 130, 26, "Delete selection");
auto *remove = new Fl_Button(720, 168, 130, 26, "Delete selection");
remove->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(context)->delete_selected();
@ -290,21 +293,63 @@ MainWindow::MainWindow(bool prompt_unsaved)
menu->add("View/Modular Graph", FL_CTRL + '3', view_callback, graph);
menu->add("Help/About", 0, about_callback);
status_ = new Fl_Box(12, 594, 976, 26, "JACK: not connected");
auto *sidebar = new Fl_Group(12,126,192,578);
sidebar->box(FL_BORDER_BOX);
auto *create_title = new Fl_Box(24,140,168,24,"CREATE TRACKS");
create_title->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
auto *midi_track = new Fl_Button(24,176,168,34,"New MIDI track");
midi_track->tooltip("Create a track and one-bar MIDI clip at the cursor; undo as one edit.");
midi_track->callback([](Fl_Widget *, void *c) { auto *w = static_cast<MainWindow *>(c);
xtc_result r = w->create_track(true); if (r != XTC_OK) w->report_error("Create MIDI track",r); },this);
auto *audio_track = new Fl_Button(24,218,168,34,"New audio track");
audio_track->tooltip("Create an empty audio track, ready for WAV import or recording.");
audio_track->callback([](Fl_Widget *, void *c) { auto *w = static_cast<MainWindow *>(c);
xtc_result r = w->create_track(false); if (r != XTC_OK) w->report_error("Create audio track",r); },this);
menu->add("Track/New MIDI track",FL_CTRL+FL_SHIFT+'m',midi_track->callback(),this);
menu->add("Track/New audio track",FL_CTRL+FL_SHIFT+'a',audio_track->callback(),this);
auto *import = new Fl_Button(24,260,168,34,"Import WAV");
import->callback(audio_file_callback,this);
auto *load = new Fl_Button(24,302,168,34,"Load instrument plugin");
load->callback(instrument_callback,this);
auto *editor = new Fl_Button(24,344,168,34,"Edit MIDI notes");
editor->callback(piano_callback,this);
auto *selection_title = new Fl_Box(24,404,168,24,"SELECTED TRACK");
selection_title->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
selection_info_ = new Fl_Box(24,438,168,170,"Select a track to inspect it.");
selection_info_->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
auto *tip = new Fl_Box(24,620,168,64,"Ctrl+1 Arrange\nCtrl+2 Mixer\nCtrl+3 Routing");
tip->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
sidebar->end();
status_ = new Fl_Box(12,714,1256,26,"JACK: not connected");
status_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
status_->labelsize(12);
error_ = new Fl_Box(12, 624, 976, 44, "");
error_ = new Fl_Box(12,744,1256,44,"");
error_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
error_->labelcolor(fl_rgb_color(255, 155, 120));
error_->labelsize(12);
xtc_ui::style(this);
color(xtc_ui::background());
create_title->labelcolor(xtc_ui::muted()); selection_title->labelcolor(xtc_ui::muted());
tip->labelcolor(xtc_ui::muted()); hint->labelcolor(xtc_ui::muted());
transport_position_->box(xtc_ui::flat); transport_position_->color(xtc_ui::background());
transport_position_->labelfont(FL_COURIER_BOLD); transport_position_->labelsize(18);
tempo_display_->labelcolor(xtc_ui::muted());
midi_track->color(fl_rgb_color(45,91,86));
play_->labelcolor(xtc_ui::accent()); record_->labelcolor(fl_rgb_color(245,132,142));
error_->labelcolor(fl_rgb_color(255,166,139));
end();
resizable(tabs_);
size_range(800, 540);
size_range(1220, 720);
callback(close_callback, this);
open_backend();
Fl::add_timeout(0.1, timer_callback, this);
}
void MainWindow::draw()
{
// FLTK may reload the platform scheme when a window first becomes visible.
// Keep built-in arrows, popups and choice fields flat as well as our boxes.
xtc_ui::configure_boxes();
Fl_Double_Window::draw();
}
/**
* @brief Removes the status timer and releases backend and recording state.
*
@ -554,6 +599,7 @@ void MainWindow::update_history_controls()
}
void MainWindow::refresh_status()
{
refresh_workspace();
update_history_controls();
if (mixer_ && !ready_) { mixer_->peaks(0, 0); mixer_->track_peaks(0, 0); }
bool can_play = false, can_record = false;
@ -1246,7 +1292,8 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context)
{
auto *window = static_cast<MainWindow *>(context);
auto *menu = dynamic_cast<Fl_Menu_Bar *>(widget);
const char *action = menu ? menu->mvalue()->label() : "Record WAV";
const char *action = menu ? menu->mvalue()->label() :
(widget && widget->label() && !std::strcmp(widget->label(),"Import WAV") ? "Import WAV" : "Record WAV");
bool recording = std::strcmp(action, "Record WAV") == 0;
bool relink = std::strcmp(action, "Relink selected audio") == 0;
if (window->recorder_) {
@ -1316,6 +1363,7 @@ xtc_result MainWindow::open_piano_roll()
this);
scroll->end();
piano_window_->end();
xtc_ui::style(piano_window_.get());
piano_window_->resizable(scroll);
scroll->scroll_to(0, 60 * 16);
} else
@ -1428,6 +1476,49 @@ void MainWindow::history_callback(Fl_Widget *widget, void *context)
window->report_error("Undo/redo failed", r);
}
xtc_result MainWindow::create_track(bool midi)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
const uint64_t position = sequencer_->position();
xtc_project *candidate = nullptr;
uint64_t track = UINT64_MAX, clip = UINT64_MAX;
xtc_result r = xtc_project_clone(project_.get(),&candidate);
if (r == XTC_OK) r = xtc_project_add_track(candidate,midi ? "MIDI track" : "Audio track",&track);
if (r == XTC_OK && midi) r = xtc_project_add_midi_clip(candidate,track,position,3840,&clip);
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
if (r == XTC_OK) r = install_project(candidate,false);
if (r != XTC_OK) xtc_project_destroy(candidate);
else {
sequencer_->select(track,clip);
mixer_->select_track(track); tabs_->value(tabs_->child(0)); mark_dirty();
}
return r;
}
void MainWindow::refresh_workspace()
{
char position[64], tempo[40], info[512];
uint64_t tick = sequencer_->position();
std::snprintf(position,sizeof(position),"%03llu : %u : %03u",
static_cast<unsigned long long>(tick/3840+1),static_cast<unsigned int>((tick/960)%4+1),
static_cast<unsigned int>(tick%960));
set_label(transport_position_,position);
std::snprintf(tempo,sizeof(tempo),"%.1f BPM",xtc_project_tempo(project_.get())/1000.0);
set_label(tempo_display_,tempo);
size_t tracks = 0, clips = 0; xtc_project_get_counts(project_.get(),&tracks,&clips);
for (size_t i = 0; i < tracks; ++i) {
xtc_track_info track{}; xtc_project_get_track(project_.get(),i,&track);
if (track.id != sequencer_->selected_track()) continue;
xtc_plugin_spec spec{};
bool plugin = xtc_project_get_instrument(project_.get(),track.id,&spec) == XTC_OK;
std::snprintf(info,sizeof(info),"%s\n\n%s\nGain %.2f | Pan %.2f\n%s%s\n\n%s",
track.name,track.is_bus ? "BUS" : "TRACK",static_cast<double>(track.gain),static_cast<double>(track.pan),
track.mute ? "Muted " : "",track.solo ? "Solo" : "",plugin ? spec.label : "Built-in MIDI sine");
set_label(selection_info_,info); return;
}
std::snprintf(info,sizeof(info),"No track selected\n\n%zu tracks / buses\n%zu clips\n\nCreate a MIDI track to start drawing notes, or import a WAV file.",tracks,clips);
set_label(selection_info_,info);
}
xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index)
{
if (recorder_) return XTC_ERR_INVALID_STATE;
@ -1456,8 +1547,8 @@ xtc_result MainWindow::select_live_instrument(uint64_t track)
void MainWindow::instrument_callback(Fl_Widget *widget, void *context)
{
auto *w = static_cast<MainWindow *>(context);
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
const char *label = menu->mvalue()->label();
auto *menu = dynamic_cast<Fl_Menu_Bar *>(widget);
const char *label = menu ? menu->mvalue()->label() : "Load instrument plugin";
uint64_t track = w->sequencer_->selected_track();
xtc_result r;
if (std::strcmp(label,"Use built-in instrument") == 0) r = w->assign_instrument(track,nullptr,0);

View file

@ -21,6 +21,10 @@ class Fl_Menu_Bar;
class MainWindow : public Fl_Double_Window {
public:
/* One undoable candidate edit. MIDI also adds a one-bar clip at the cursor.
* Preparation stops/resets transport; recording and failed candidates retain
* the current document. No engine/model changes happen in audio callbacks. */
xtc_result create_track(bool midi);
/* Control-thread plugin commands, transactional model adoption. */
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
xtc_result select_live_instrument(uint64_t track);
@ -171,6 +175,11 @@ class MainWindow : public Fl_Double_Window {
xtc_result return_to_start();
private:
void draw() override;
void refresh_workspace();
Fl_Box *transport_position_ = nullptr;
Fl_Box *tempo_display_ = nullptr;
Fl_Box *selection_info_ = nullptr;
MixerWidget *mixer_ = nullptr;
Fl_Menu_Bar *menu_ = nullptr;
void update_history_controls();

View file

@ -1,3 +1,4 @@
#include "theme.hpp"
#include "mixer_widget.hpp"
#include "xtc/xtc_control.h"
#include <FL/Fl.H>
@ -95,7 +96,7 @@ class AutomationLane : public Fl_Widget {
static_cast<float>(static_cast<double>(tick - a.tick) / static_cast<double>(it->tick - a.tick)) *
(it->value - a.value);
};
fl_color(fl_rgb_color(105, 200, 150));
fl_color(xtc_ui::accent());
if (!points.empty())
for (int px = 1; px < w(); ++px) {
uint64_t a =
@ -182,13 +183,13 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit
tracks_ = new Fl_Choice(x + 70, y + 8, 240, 26, "Track");
tracks_->callback(select, this);
gain_ = new Fl_Value_Slider(x + 70, y + 48, 320, 28, "Gain");
gain_->type(FL_HOR_NICE_SLIDER);
gain_->type(FL_HOR_FILL_SLIDER);
gain_->bounds(0, 4);
gain_->step(0.01);
gain_->when(FL_WHEN_RELEASE);
gain_->callback(control, this);
pan_ = new Fl_Value_Slider(x + 480, y + 48, 320, 28, "Pan");
pan_->type(FL_HOR_NICE_SLIDER);
pan_->type(FL_HOR_SLIDER);
pan_->bounds(-1, 1);
pan_->step(0.01);
pan_->when(FL_WHEN_RELEASE);

View file

@ -1,3 +1,4 @@
#include "theme.hpp"
#include "piano_roll.hpp"
#include <FL/Fl.H>
#include <FL/fl_draw.H>
@ -210,7 +211,7 @@ void PianoRollWidget::draw()
int a = std::max(cx, left), b = std::min(cx + cw, right);
if (a >= b || py + row_height < cy || py > cy + ch)
continue;
fl_color(all_ || n.id == selected_ ? fl_rgb_color(230, 190, 80)
fl_color(all_ || n.id == selected_ ? xtc_ui::accent()
: fl_rgb_color(50, static_cast<uchar>(70 + n.velocity), 110));
fl_rectf(a, py + 1, b - a, row_height - 3);
}

View file

@ -1,3 +1,4 @@
#include "theme.hpp"
#include "plugin_dialog.hpp"
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
@ -19,7 +20,7 @@ struct Dialog : Fl_Double_Window {
Dialog(int width, int height, const char *title) : Fl_Double_Window(width,height,title) { set_modal(); }
static void accept(Fl_Widget *, void *data) { auto *d = static_cast<Dialog *>(data); d->accepted = true; d->hide(); }
static void cancel(Fl_Widget *, void *data) { static_cast<Dialog *>(data)->hide(); }
bool run() { callback(cancel,this); end(); show(); while (shown()) Fl::wait(); return accepted; }
bool run() { callback(cancel,this); end(); xtc_ui::style(this); show(); while (shown()) Fl::wait(); return accepted; }
};
}
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument)

View file

@ -1,4 +1,5 @@
#include "sequencer_widget.hpp"
#include "theme.hpp"
#include "xtc/xtc_midi.h"
#include <FL/Fl.H>
#include <FL/Fl_Scroll.H>
@ -71,7 +72,7 @@ void SequencerWidget::set_project(xtc_project *project, bool reset)
*/
uint64_t SequencerWidget::tick(int px) const
{
return std::min(XTC_PROJECT_MAX_TICK, static_cast<uint64_t>(std::max(0, px - x() - 120)) * 960U /
return std::min(XTC_PROJECT_MAX_TICK, static_cast<uint64_t>(std::max(0, px - x() - 180)) * 960U /
static_cast<unsigned int>(scale_) / grid_ * grid_);
}
@ -88,7 +89,7 @@ void SequencerWidget::extent()
xtc_project_get_clip(project_, i, &c);
end = std::max(end, c.start_tick + c.length_tick + 3840U);
}
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + 140U),
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + 200U),
std::max(400, static_cast<int>(tracks) * 56 + 100));
if (parent())
parent()->redraw();
@ -223,62 +224,95 @@ void SequencerWidget::zoom(int direction)
*/
void SequencerWidget::draw()
{
fl_push_clip(x(), y(), w(), h());
fl_color(fl_rgb_color(35, 38, 43));
fl_rectf(x(), y(), w(), h());
fl_push_clip(x(),y(),w(),h());
int cx, cy, cw, ch;
fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch);
fl_font(FL_HELVETICA, 12);
int first = std::max(0, (cx - x() - 120) / (scale_ * 4));
int last = (cx + cw - x() - 120) / (scale_ * 4) + 1;
for (int bar = first; bar <= last; ++bar) {
int px = x() + 120 + bar * scale_ * 4;
fl_color(fl_rgb_color(65, 68, 74));
fl_line(px, y(), px, y() + h());
char label[32];
std::snprintf(label, sizeof(label), "%d", bar + 1);
fl_color(FL_WHITE);
fl_draw(label, px + 4, y() + 19);
}
char tempo[32];
std::snprintf(tempo, sizeof(tempo), "%.3g BPM | 4/4", xtc_project_tempo(project_) / 1000.0);
fl_color(FL_WHITE);
fl_draw(tempo, x() + 4, y() + 19);
size_t tracks, clips;
xtc_project_get_counts(project_, &tracks, &clips);
fl_clip_box(x(),y(),w(),h(),cx,cy,cw,ch);
fl_color(xtc_ui::background()); fl_rectf(cx,cy,cw,ch);
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) {
xtc_track_info t{};
xtc_project_get_track(project_, i, &t);
int py = y() + 28 + static_cast<int>(i) * 56;
fl_color(t.id == track_ ? fl_rgb_color(70, 90, 110) : fl_rgb_color(48, 51, 57));
fl_rectf(x(), py, 118, 55);
fl_color(FL_WHITE);
fl_draw(t.name, x() + 6, py + 32);
int py = y()+28+static_cast<int>(i)*56;
if (py+56 < cy || py > cy+ch) continue;
fl_color(i%2 ? fl_rgb_color(30,34,40) : fl_rgb_color(33,37,43));
fl_rectf(x()+180,py,w()-180,55);
}
fl_font(FL_HELVETICA,11);
int first = std::max(0,(cx-x()-180)/scale_);
int last = (cx+cw-x()-180)/scale_+1;
for (int beat = first; beat <= last; ++beat) {
int px = x()+180+beat*scale_;
fl_color(beat%4 ? fl_rgb_color(43,48,56) : fl_rgb_color(65,72,83));
fl_line(px,y()+28,px,y()+h());
if (beat%4 == 0) {
char label[32]; std::snprintf(label,sizeof(label),"%d",beat/4+1);
fl_color(xtc_ui::muted()); fl_draw(label,px+6,y()+19);
}
}
fl_color(xtc_ui::muted()); fl_draw("TRACKS / 4:4",x()+12,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;
Fl_Color color = xtc_ui::track(t.id);
fl_color(t.id == track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel());
fl_rectf(x(),py,178,55);
fl_color(color); fl_rectf(x(),py,3,55);
fl_push_clip(x()+9,py,162,55);
fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12);
fl_draw(t.name,x()+12,py+23);
char detail[64];
std::snprintf(detail,sizeof(detail),"%02zu %s%s%s",i+1,t.is_bus ? "BUS" : "TRACK",
t.mute ? " MUTE" : "",t.solo ? " SOLO" : "");
fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,py+42);
fl_pop_clip();
for (size_t j = 0; j < clips; ++j) {
xtc_clip_info c{};
xtc_project_get_clip(project_, j, &c);
if (c.track_id != t.id)
continue;
int left = x() + 120 + static_cast<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int right = left + std::max(4, static_cast<int>(c.length_tick * static_cast<unsigned int>(scale_) / 960U));
int a = std::max(left, cx), b = std::min(right, cx + cw);
if (b <= a || py + 50 < cy || py > cy + ch)
continue;
fl_color(c.id == clip_ ? fl_rgb_color(100, 170, 205) : fl_rgb_color(65, 120, 150));
fl_rectf(a, py + 5, b - a, 44);
fl_push_clip(a, py + 5, b - a, 44);
draw_waveform(c, left, py);
fl_color(FL_WHITE);
char label[96];
std::snprintf(label,sizeof(label),"%s: %s",c.midi ? "MIDI" : c.asset_path[0] ? "Audio" : "Region",c.name);
fl_draw(label,std::max(a,left+6),py+31);
xtc_clip_info c{}; xtc_project_get_clip(project_,j,&c);
if (c.track_id != t.id) continue;
int left = x()+180+static_cast<int>(c.start_tick*static_cast<unsigned int>(scale_)/960U);
int right = left+std::max(4,static_cast<int>(c.length_tick*static_cast<unsigned int>(scale_)/960U));
int a = std::max(left,cx), b = std::min(right,cx+cw);
if (b <= a) continue;
fl_color(fl_color_average(color,xtc_ui::background(),c.id == clip_ ? .75f : .45f));
fl_rectf(a,py+5,b-a,46);
fl_color(c.id == clip_ ? 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(project_,n,&note);
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(project_,n,&note);
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>(scale_)/960U);
int nw = std::max(2,static_cast<int>((end-begin)*static_cast<unsigned int>(scale_)/960U));
fl_rectf(nx,py+26+static_cast<int>((high-note.pitch)*18/range),nw,2);
}
} else 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 playhead = x() + 120 + static_cast<int>(cursor_ * static_cast<unsigned int>(scale_) / 960U);
if (playhead >= cx && playhead <= cx + cw) {
fl_color(FL_RED);
fl_line(playhead, y(), playhead, y() + h());
if (!tracks) {
fl_font(FL_HELVETICA_BOLD,18); fl_color(xtc_ui::text());
fl_draw("Your next track starts here",x()+220,y()+105);
fl_font(FL_HELVETICA,13); fl_color(xtc_ui::muted());
fl_draw("Create a MIDI track or import audio from the panel on the left.",x()+220,y()+135);
fl_draw("Arrange clips here. Open the piano roll to draw and edit MIDI notes.",x()+220,y()+161);
}
int playhead = x()+180+static_cast<int>(cursor_*static_cast<unsigned int>(scale_)/960U);
if (playhead >= cx && playhead <= cx+cw) {
fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h());
fl_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6);
}
fl_pop_clip();
}
@ -316,7 +350,7 @@ int SequencerWidget::handle(int event)
for (size_t j = clips; j > 0; --j) {
xtc_clip_info c{};
xtc_project_get_clip(project_, j - 1, &c);
int left = x() + 120 + static_cast<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int left = x() + 180 + static_cast<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int right = left + std::max(4, static_cast<int>(c.length_tick * static_cast<unsigned int>(scale_) / 960U));
if (c.track_id == track_ && Fl::event_x() >= left && Fl::event_x() < right) {
clip_ = c.id;

70
src/gui/theme.hpp Normal file
View file

@ -0,0 +1,70 @@
#ifndef XTC_GUI_THEME_HPP
#define XTC_GUI_THEME_HPP
#include <cstdint>
#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Scroll.H>
#include <FL/Fl_Tabs.H>
#include <FL/fl_draw.H>
/* Shared flat palette. No gradients, shadows, textures or external assets. */
namespace xtc_ui {
constexpr Fl_Boxtype flat = static_cast<Fl_Boxtype>(FL_FREE_BOXTYPE);
constexpr Fl_Boxtype pressed = static_cast<Fl_Boxtype>(FL_FREE_BOXTYPE+1);
inline Fl_Color background() { return fl_rgb_color(25,28,33); }
inline Fl_Color panel() { return fl_rgb_color(34,38,44); }
inline Fl_Color border() { return fl_rgb_color(58,64,73); }
inline Fl_Color text() { return fl_rgb_color(231,235,240); }
inline Fl_Color muted() { return fl_rgb_color(163,174,188); }
inline Fl_Color accent() { return fl_rgb_color(92,211,184); }
inline Fl_Color track(uint64_t id) {
static const unsigned char colors[][3] = {{74,155,184},{111,143,206},{160,123,191},
{74,163,139},{191,145,78},{183,113,139}};
const auto &c = colors[id % 6]; return fl_rgb_color(c[0],c[1],c[2]);
}
inline void flat_box(int x, int y, int w, int h, Fl_Color color) {
fl_color(color); fl_rectf(x,y,w,h);
fl_color(border()); fl_rect(x,y,w,h);
}
inline void pressed_box(int x, int y, int w, int h, Fl_Color color) {
flat_box(x,y,w,h,fl_darker(color));
}
inline void configure_boxes() {
Fl::set_boxtype(flat,flat_box,1,1,2,2);
Fl::set_boxtype(pressed,pressed_box,1,1,2,2);
Fl::set_boxtype(FL_UP_BOX,flat_box,1,1,2,2);
Fl::set_boxtype(FL_DOWN_BOX,pressed_box,1,1,2,2);
Fl::set_boxtype(FL_THIN_UP_BOX,flat_box,1,1,2,2);
Fl::set_boxtype(FL_THIN_DOWN_BOX,pressed_box,1,1,2,2);
Fl::set_boxtype(FL_ENGRAVED_BOX,flat_box,1,1,2,2);
Fl::set_boxtype(FL_BORDER_BOX,flat_box,1,1,2,2);
}
inline void initialize() {
static bool initialized = false;
if (initialized) return;
initialized = true;
Fl::background(34,38,44); Fl::background2(25,28,33); Fl::foreground(231,235,240);
Fl::set_color(FL_SELECTION_COLOR,53,116,111);
configure_boxes();
Fl::scrollbar_size(12);
}
inline void style(Fl_Widget *widget) {
widget->labelfont(FL_HELVETICA); widget->labelsize(12); widget->labelcolor(text());
widget->color(panel()); widget->selection_color(fl_rgb_color(53,116,111));
if (widget->box() != FL_NO_BOX && widget->box() != FL_FLAT_BOX) widget->box(flat);
if (auto *button = dynamic_cast<Fl_Button *>(widget)) {
button->box(flat); button->down_box(pressed);
button->color(fl_rgb_color(45,51,59));
}
if (auto *group = dynamic_cast<Fl_Group *>(widget)) {
for (int i = 0; i < group->children(); ++i) style(group->child(i));
if (auto *scroll = dynamic_cast<Fl_Scroll *>(group)) {
scroll->scrollbar.box(flat); scroll->hscrollbar.box(flat);
scroll->scrollbar.slider(FL_FLAT_BOX); scroll->hscrollbar.slider(FL_FLAT_BOX);
}
}
}
}
#endif

View file

@ -18,6 +18,7 @@
#include <cstring>
#include <cmath>
#include <initializer_list>
#include <utility>
#include <cstdlib>
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
@ -315,8 +316,8 @@ int main(int argc, char **argv)
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && track_count == 1 && clip_count == 1);
xtc_clip_info region{};
CHECK(xtc_project_get_clip(sequence->project(), 0, &region) == XTC_OK);
gesture(sequence, 150, 50, 144, 0); // Move by three beats.
gesture(sequence, 265, 50, 48, 0); // Trim left edge by one beat.
gesture(sequence, 210, 50, 144, 0); // Move by three beats.
gesture(sequence, 325, 50, 48, 0); // Trim left edge by one beat.
CHECK(xtc_project_get_clip(sequence->project(), 0, &region) == XTC_OK &&
region.start_tick == 3840 && region.length_tick == 2880 && region.offset_tick == 960);
int width = sequence->w();
@ -373,7 +374,7 @@ int main(int argc, char **argv)
xtc_track_info t{}; xtc_project_get_track(sequence->project(), audio_row, &t);
if (t.id == audio_clip.track_id) break;
}
gesture(sequence, 121 + static_cast<int>(audio_clip.start_tick*48/960), 50 + static_cast<int>(audio_row)*56, 0, 0);
gesture(sequence, 181 + static_cast<int>(audio_clip.start_tick*48/960), 50 + static_cast<int>(audio_row)*56, 0, 0);
CHECK(sequence->selected_clip() == audio_clip.id);
CHECK(sequence->audio_properties(0.5f, 1, 1) == XTC_OK);
sequence->position(audio_clip.start_tick+1);
@ -848,6 +849,49 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-gui-instrument.xtc"); window.do_callback();
}
}
// Flat workspace creation is one history edit, and failed preparation keeps
// the current model. Exercise real sidebar buttons rather than mock actions.
reset();
{
MainWindow window(false); window.show();
auto *sequence = timeline(&window);
CHECK(sequence != nullptr);
auto *midi = find(&window,"New MIDI track");
auto *audio = find(&window,"New audio track");
CHECK(midi && audio && find(&window,"Import WAV") && find(&window,"Edit MIDI notes"));
press(midi);
size_t tracks = 0, clips = 0;
xtc_project_get_counts(sequence->project(),&tracks,&clips);
CHECK(tracks == 1 && clips == 1);
uint64_t track = sequence->selected_track(), clip = sequence->selected_clip();
xtc_clip_info info{}; CHECK(xtc_project_find_clip(sequence->project(),clip,&info) == XTC_OK && info.midi);
CHECK(window.mixer()->selected_track() == track);
CHECK(window.open_piano_roll() == XTC_OK);
CHECK(window.undo(false) == XTC_OK);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 0 && clips == 0);
CHECK(window.undo(true) == XTC_OK);
CHECK(sequence->select(track,clip)); pump();
CHECK(contains(&window,"MIDI track"));
press(audio);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
const xtc_project *before = sequence->project();
reserve_result = XTC_ERR_OUT_OF_MEMORY;
CHECK(window.create_track(true) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
reserve_result = XTC_OK; submit_result = XTC_ERR_INVALID_STATE;
CHECK(window.create_track(false) == XTC_ERR_INVALID_STATE && sequence->project() == before);
submit_result = XTC_OK;
// Exercise the supported minimum and a larger workspace with real layout.
for (auto size : {std::pair<int,int>{1220,720},{1600,1000}}) {
window.resize(window.x(),window.y(),size.first,size.second); pump();
CHECK(midi->x() >= 0 && midi->x()+midi->w() <= sequence->parent()->x());
CHECK(sequence->parent()->x()+sequence->parent()->w() <= window.w());
CHECK(find(&window,"Play")->visible_r() && midi->visible_r());
}
CHECK(window.save_project("/tmp/xtc-workspace.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-workspace.xtc") == XTC_OK);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
unlink("/tmp/xtc-workspace.xtc"); window.do_callback();
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -53,6 +53,8 @@ def main():
example = resources / 'xtc/examples/midi-demo.xtc'
run([str(binaries / 'xtc-check'), '--inspect', str(example)])
assert (resources / 'xtc/examples/README.md').is_file()
run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')])
assert (docs / 'WORKSPACE.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..12' in diagnose and 'JACK compiled:' in diagnose
# Replacing a file symlink must not overwrite an unrelated target.

View file

@ -110,7 +110,7 @@ int main(int argc, char **argv)
CHECK(window.piano_roll());
CHECK(window.piano_roll()->add_note(0, 960, 60) == XTC_OK);
CHECK(window.piano_roll()->add_note(960, 960, 64) == XTC_OK);
gesture(*timeline, 150, 50, 96); // Move the clip two beats using its body.
gesture(*timeline, 210, 50, 96); // Move the clip two beats using its body.
CHECK(xtc_project_get_clip(timeline->project(), 0, &arranged) == XTC_OK);
CHECK(arranged.start_tick == 1920 && arranged.length_tick == 3840);
CHECK(window.import_audio(wave.c_str()) == XTC_OK);
@ -137,7 +137,7 @@ int main(int argc, char **argv)
size_t actual_nodes, actual_edges;
CHECK(xtc_graph_get_counts(xtc_project_graph(timeline->project()), &actual_nodes, &actual_edges) == XTC_OK);
CHECK(actual_nodes == nodes && actual_edges == edges);
gesture(*timeline, 216, 10, 0); // Ruler seeks two beats while stopped.
gesture(*timeline, 276, 10, 0); // Ruler seeks two beats while stopped.
CHECK(timeline->position() == 1920);
press(window, "Play"); pump();
CHECK(!button(window, "Play")->active() && button(window, "Pause")->active());