Phase 33: add a flat modern DAW workspace and track creation workflow
This commit is contained in:
parent
fe484537f9
commit
d800a6392e
19 changed files with 711 additions and 115 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
57
docs/WORKSPACE.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
236
examples/projects/modern-session.xtc
Normal file
236
examples/projects/modern-session.xtc
Normal 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
|
||||
|
|
@ -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_);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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_,¬es);
|
||||
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,¬e);
|
||||
if (note.clip_id == c.id) { low = std::min(low,note.pitch); high = std::max(high,note.pitch); }
|
||||
}
|
||||
unsigned int range = high >= low ? std::max(12U,high-low) : 12U;
|
||||
fl_color(fl_color_average(color,FL_WHITE,.55f));
|
||||
for (size_t n = 0; n < notes; ++n) {
|
||||
xtc_note_info note{}; xtc_project_get_note(project_,n,¬e);
|
||||
if (note.clip_id != c.id) continue;
|
||||
uint64_t begin = std::max(note.start_tick,c.offset_tick);
|
||||
uint64_t end = std::min(note.start_tick+note.length_tick,c.offset_tick+c.length_tick);
|
||||
if (begin >= end) continue;
|
||||
int nx = left+static_cast<int>((begin-c.offset_tick)*static_cast<unsigned int>(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
70
src/gui/theme.hpp
Normal 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
|
||||
|
|
@ -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, ®ion) == 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, ®ion) == 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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue