diff --git a/docs/MIXER.md b/docs/MIXER.md index dad75b1..0b3e4ea 100644 --- a/docs/MIXER.md +++ b/docs/MIXER.md @@ -1,10 +1,53 @@ -# Mixer, automation and undo +# Studio mixer -Phase 8 provides one selectable channel strip per project track. Open Mixer -(Ctrl+2), select a track, and set Gain, Pan, Mute and Solo. Changes stop transport; -Play prepares independent audio/MIDI snapshots with the accepted settings. -No edit changes a plan that the callback is using. Editing is blocked while -recording. Graph edits retain their existing live publication behavior. +Open **View / Mixer** (Ctrl+2) to see a channel strip for every track and bus. +The horizontal scrollbar reveals additional channels; the master output monitor +stays pinned on the right. The bank supports the full 64-track/bus project limit. +Channel order follows the sequencer, and colors match the track headers. + +Each strip offers: + +- A name button that selects its track for automation below the bank. +- **Mute** and **Solo** switches using the existing inclusive track/bus solo rules. +- A flat **Pan** knob: left -1, center 0, right +1. Drag to turn it; + double-click to center. The text shows direction and percentage. +- An output dropdown for **Master** or a project bus. Bus outputs stay at Master; + nested bus routing remains unsupported. **Add bus** creates a new bus strip. +- A vertical level fader, displayed in dB. Zero dB is unity gain. The upper limit + is approximately +12.04 dB (linear gain 4); the -60 dB bottom position is silence. +- Independent stereo sample-peak meters, numeric dBFS readouts and sticky clip + indications. **Reset peaks** clears all strip and master clip latches. + +Changing a fader or knob commits on release, so a completed gesture is one undo +step. The existing preparation guard stops playback before model edits and +rejects changes while recording. Failed edits restore the displayed model values. +Selections are UI state and create no undo step. Track IDs keep controls attached +to the correct channel after reordering, undo, deletion or project replacement. + +The top Track selector provides another way to select automation targets; the +bank scrolls to reveal the selection. M/S toggle the selected track's mute/solo, +Left/Right adjust its linear gain, Shift+Left/Right adjust pan, and Up/Down select +adjacent tracks. The existing automation controls below the bank retain Gain/Pan, +First tick and Linear/Hold interpolation. Click to add a point and right-click +near a point to remove it. + +Faders and knobs display **static** mix values. Automation replaces those values +during playback; the controls are not motorized automation displays. Automation +values retain their existing linear gain and pan units. Existing project files +store mix values and automation without a format change. Very small nonzero +gains display at the fader floor; pan/mute/solo edits preserve their actual value. +Moving the fader to its bottom explicitly sets silence. + +Meters use the backend's consumed peak snapshot for every track and bus, +including offscreen channels. Track taps are after track controls and before the +bus, bus taps follow bus controls, and the master follows the modular graph and +transport envelope. Missing taps decay to silence. Displays use instant attack, +20 dB/s release and sticky clipping at a linear sample peak of 1 or more. +Numeric displays have a -90 dBFS floor, channel bars a -60 dBFS floor; master +bars cover -90 to +6 dBFS. These are sample-peak meters, not loudness or true-peak +meters. The master strip is an output monitor, not a new gain control. + +## Engine mixing behavior Gain is a linear multiplier from 0 to 4. Pan ranges from -1 (left) to +1 (right): center has unity on both sides and moving away attenuates the opposite side @@ -62,11 +105,11 @@ The backend retains interval sample-peak maxima until one serialized control reader consumes them with `xtc_backend_read_meters`. The GUI polls about every 100 ms. Atomic channels are independent (not a simultaneous waveform snapshot); peaks are quantized to 0.000001 linear and capped at 1000. Legacy backend status -peaks remain last-block linear values. GUI bars display 20*log10(peak) dBFS, floor --90, instantaneous attack and 20 dB/second release, with a +6 display ceiling. +peaks remain last-block linear values. GUI readouts display 20*log10(peak) dBFS, floor -90, with instantaneous +attack and 20 dB/second release. Channel bars cover -60 to +6 dBFS; master +bars cover -90 to +6 dBFS. Each channel latches red CLIP at linear peak >=1 (0 dBFS) until Reset peaks. -Reset is session state, not an audio edit. Selection changes reset selected-strip -history. These are sample-peak meters, not RMS, LUFS or true-peak measurements. +Reset is session state, not an audio edit. Selection changes preserve every strip's meter history. These are sample-peak meters, not RMS, LUFS or true-peak measurements. There is no limiter; summed levels can exceed unity. ## Mix buses and gain staging (Phase 24) @@ -134,6 +177,11 @@ events, use mixer keys and exercise undo/redo, failed installation rollback, branching, save/reopen and history reset. Live JACK MIDI acceptance verifies saved/reopened hard-right playback and stereo peaks using only software clients. A native Xvfb mouse smoke check covers sliders and automation drawing. +Phase 36 adds checks for independent strip callbacks/meters, fader gestures, pan +centering, failed-edit restoration, routing, undo/reload, duplicate names and +stable IDs across reorder/removal. A 64-channel GUI session verifies scrolling +and minimum/large layouts. Screenshots include live JACK meters and a bus strip. + Physical stereo devices, listening quality, realtime endurance and high-density project performance remain unverified; no new dependency was introduced. diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index e7ea962..d59bc69 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -79,3 +79,9 @@ Phase 35 adds Edit / Preferences with an Apply button and `~/.xtc.conf` INI persistence, configurable plugin search paths and a file library, plus sequencer ruler loop selection and sample-boundary playback wrapping. See PREFERENCES.md for settings, reconnect behavior and looping limits. + +Phase 36 replaces the single-track mixer panel with a scrolling studio mixer: +vertical dB faders, flat pan knobs, mute/solo, routing and stereo meters on every +track and bus, plus a pinned master monitor. Selected-track automation remains +below the strips. No project format or engine changes. Screenshots are refreshed; +see MIXER.md and screenshots/README.md. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4a4bf00..5f33fac 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1013,6 +1013,31 @@ See [PREFERENCES.md](PREFERENCES.md) for configuration, workflow and limitations --- +## Phase 36 — Studio Mixer Overview + +### Goal +Show every track and bus as a studio-style channel strip with direct level, +pan, routing and metering controls in the flat workspace. + +### Tasks and acceptance criteria +- [x] Replace the single-track control panel with a horizontally scrolling bank of all tracks and buses. +- [x] Provide vertical dB faders, flat pan knobs, mute/solo switches and output routing per strip. +- [x] Show simultaneous stereo track/bus meters and keep the master output monitor visible while scrolling. +- [x] Retain selected-track gain/pan automation, interpolation, keyboard control and Add bus. +- [x] Preserve stable track identity, undo/redo, persistence and failure rollback across strip edits. +- [x] Verify empty/full sessions, reorder/remove, minimum/large layouts and independent meters. +- [x] Refresh the view screenshots and document mixer controls and metering limits. +- [x] Build and run GUI/core, live JACK, sanitizer and release checks. + +### Implementation status +Implemented in the C++ FLTK GUI using the existing C mixer and backend snapshot +APIs. No audio callback, project format or dependency changes. The fixed master +strip monitors graph output; it does not introduce a separate master gain stage. +Faders edit static gain and do not chase automation. See [MIXER.md](MIXER.md) +and the updated [screenshots](../screenshots/README.md). + +--- + ## 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. diff --git a/docs/WORKSPACE.md b/docs/WORKSPACE.md index 17ed4b7..139c5f0 100644 --- a/docs/WORKSPACE.md +++ b/docs/WORKSPACE.md @@ -99,3 +99,12 @@ Verification includes off-grid move/trim, snap Off, dropdown labels, double-clic coincident notes, per-note slider/lane edits, invalid velocities, undo/reload and instrument reuse/clear/reorder for all three fixture formats. Manual checks use the original modern-session example and the flat sequencer/piano windows. + +## Phase 36 studio mixer + +The Mixer tab now presents all tracks and buses as channel strips with vertical +dB faders, flat pan knobs, mute/solo, output routing and stereo meters. Scroll +horizontally for larger projects; the master output monitor stays visible. +Select a channel name to edit its gain/pan automation below the bank. Existing +undo, keyboard controls and project persistence remain available. See +[MIXER.md](MIXER.md) for fader units, metering, automation and limits. diff --git a/screenshots/02-mixer-automation.png b/screenshots/02-mixer-automation.png index b795471..0956a3d 100644 Binary files a/screenshots/02-mixer-automation.png and b/screenshots/02-mixer-automation.png differ diff --git a/screenshots/03-modular-graph.png b/screenshots/03-modular-graph.png index 27a30bc..5ec5a61 100644 Binary files a/screenshots/03-modular-graph.png and b/screenshots/03-modular-graph.png differ diff --git a/screenshots/05-sequencer-audio-waveform.png b/screenshots/05-sequencer-audio-waveform.png index cc8f5c4..16dbee0 100644 Binary files a/screenshots/05-sequencer-audio-waveform.png and b/screenshots/05-sequencer-audio-waveform.png differ diff --git a/screenshots/12-plugin-library.png b/screenshots/12-plugin-library.png index 777cce7..96ea645 100644 Binary files a/screenshots/12-plugin-library.png and b/screenshots/12-plugin-library.png differ diff --git a/screenshots/13-plugin-descriptors.png b/screenshots/13-plugin-descriptors.png index df16307..bfa617d 100644 Binary files a/screenshots/13-plugin-descriptors.png and b/screenshots/13-plugin-descriptors.png differ diff --git a/screenshots/14-plugin-controls.png b/screenshots/14-plugin-controls.png index ab5ea48..4e1aad3 100644 Binary files a/screenshots/14-plugin-controls.png and b/screenshots/14-plugin-controls.png differ diff --git a/screenshots/19-clap-instrument-descriptors.png b/screenshots/19-clap-instrument-descriptors.png index a4a5c6b..54ab718 100644 Binary files a/screenshots/19-clap-instrument-descriptors.png and b/screenshots/19-clap-instrument-descriptors.png differ diff --git a/screenshots/20-vst2-instrument-descriptors.png b/screenshots/20-vst2-instrument-descriptors.png index e4fa344..e52bc59 100644 Binary files a/screenshots/20-vst2-instrument-descriptors.png and b/screenshots/20-vst2-instrument-descriptors.png differ diff --git a/screenshots/21-vst3-instrument-descriptors.png b/screenshots/21-vst3-instrument-descriptors.png index d2c5d17..59590b2 100644 Binary files a/screenshots/21-vst3-instrument-descriptors.png and b/screenshots/21-vst3-instrument-descriptors.png differ diff --git a/screenshots/22-mixer-playing.png b/screenshots/22-mixer-playing.png new file mode 100644 index 0000000..b0a3f35 Binary files /dev/null and b/screenshots/22-mixer-playing.png differ diff --git a/screenshots/23-mixer-minimum-size.png b/screenshots/23-mixer-minimum-size.png new file mode 100644 index 0000000..f547e4d Binary files /dev/null and b/screenshots/23-mixer-minimum-size.png differ diff --git a/screenshots/24-mixer-scrolled-session.png b/screenshots/24-mixer-scrolled-session.png new file mode 100644 index 0000000..a155a5e Binary files /dev/null and b/screenshots/24-mixer-scrolled-session.png differ diff --git a/screenshots/README.md b/screenshots/README.md index 1527ffc..f50c207 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -1,16 +1,18 @@ # xtc screenshots -Captured from the Phase 35 FLTK application on 2026-10-28, using a virtual X11 +Captured from the Phase 36 FLTK application on 2026-10-28, using a virtual X11 screen. Images show actual rendered widgets, without mockups or image edits. [View the contact sheet](overview.png). -The arrangement uses `examples/projects/modern-session.xtc`. The audio view adds -a temporary generated waveform. Mixer automation and loop selections are staged -for these captures. Plugin views use the repository's LADSPA, CLAP, VST2 and -VST3 test fixtures; these are demonstration plugins, not commercial instruments. -Preferences show application defaults and the staged loop. No user configuration -or example project was overwritten. +The arrangement uses `examples/projects/modern-session.xtc`. Mixer captures add +a bus, routing, pan/level settings and automation. The playing view shows actual +JACK callback meters; additional captures show the minimum 1220 × 720 layout and +a larger session scrolled to its final channels, with the master monitor pinned. +The audio view adds a temporary generated waveform. Plugin views use the +repository's LADSPA, CLAP, VST2 and VST3 test fixtures; these are demonstration +plugins, not commercial instruments. Preferences show application defaults and +the staged loop. No user configuration or example project was overwritten. - [Sequencer loop](01-sequencer-loop.png) - [Mixer automation](02-mixer-automation.png) @@ -33,3 +35,6 @@ or example project was overwritten. - [Clap instrument descriptors](19-clap-instrument-descriptors.png) - [Vst2 instrument descriptors](20-vst2-instrument-descriptors.png) - [Vst3 instrument descriptors](21-vst3-instrument-descriptors.png) +- [Mixer playing](22-mixer-playing.png) +- [Mixer minimum size](23-mixer-minimum-size.png) +- [Mixer scrolled session](24-mixer-scrolled-session.png) diff --git a/screenshots/overview.png b/screenshots/overview.png index e85625b..8049a8f 100644 Binary files a/screenshots/overview.png and b/screenshots/overview.png differ diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index e2d8895..8e29943 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -652,7 +652,7 @@ void MainWindow::refresh_status() { refresh_workspace(); update_history_controls(); - if (mixer_ && !ready_) { mixer_->peaks(0, 0); mixer_->track_peaks(0, 0); } + if (mixer_ && !ready_) mixer_->meters(xtc_meter_snapshot{}); bool can_play = false, can_record = false; if (!ready_) { set_label(indicator_, "Audio: disconnected"); @@ -689,14 +689,8 @@ void MainWindow::refresh_status() : running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215)); xtc_meter_snapshot meters{}; - float track_left = 0, track_right = 0; - if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) { - mixer_->peaks(meters.master_left, meters.master_right); - for (size_t i = 0; i < meters.count; ++i) if (meters.tracks[i].id == mixer_->selected_track()) { - track_left = meters.tracks[i].left; track_right = meters.tracks[i].right; break; - } - } else mixer_->peaks(status.peak_left, status.peak_right); - mixer_->track_peaks(track_left, track_right); + if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) mixer_->meters(meters); + else { meters.master_left=status.peak_left;meters.master_right=status.peak_right;mixer_->meters(meters); } char text[256]; std::snprintf( text, sizeof(text), diff --git a/src/gui/mixer_widget.cpp b/src/gui/mixer_widget.cpp index f391d8a..cfc31a4 100644 --- a/src/gui/mixer_widget.cpp +++ b/src/gui/mixer_widget.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -162,10 +164,289 @@ class AutomationLane : public Fl_Widget { } }; +/* Fl_Menu_::add merges duplicate labels and interprets menu paths. Append a + * unique temporary entry, then replace its text literally so IDs stay dense. */ +static int literal_item(Fl_Choice *choice, const char *label) +{ + std::string key="#"; + while (choice->find_item(key.c_str())) key+="#"; + int index=choice->add(key.c_str()); + choice->replace(index,label); + return index; +} + +class VerticalPeak : public Fl_Progress +{ + public: + VerticalPeak(int x, int y, int w, int h, const char *label) : Fl_Progress(x, y, w, h, label) + { + labeltype(FL_NO_LABEL); + } + void draw() override + { + fl_color(xtc_ui::background()); + fl_rectf(x(), y(), w(), h()); + int height = static_cast((std::clamp(value(), -90.0f, 6.0f) + 90) / 96 * (h() - 2)); + fl_color(selection_color()); + fl_rectf(x() + 1, y() + h() - 1 - height, w() - 2, height); + fl_color(xtc_ui::border()); + fl_rect(x(), y(), w(), h()); + } +}; + +/* A flat pan dial: no embossed face, bevels or simulated lighting. */ +class PanKnob : public Fl_Dial +{ + bool centering_ = false; + + public: + PanKnob(int x, int y, int w, int h) : Fl_Dial(x, y, w, h, "Pan") + { + bounds(-1, 1); + step(.01); + type(FL_LINE_DIAL); + when(FL_WHEN_RELEASE); + tooltip("Pan: -1 left, 0 center, +1 right. Double-click to center."); + } + void draw() override + { + fl_color(xtc_ui::background()); + fl_rectf(x(), y(), w(), h()); + int size = std::min(w(), h()) - 8, cx = x() + w() / 2, cy = y() + h() / 2; + fl_color(xtc_ui::border()); + fl_pie(cx - size / 2, cy - size / 2, size, size, 0, 360); + fl_color(active_r() ? xtc_ui::accent() : xtc_ui::muted()); + double angle = (value() * 140 - 90) * 3.141592653589793 / 180; + fl_line_style(FL_SOLID, 2); + fl_line(cx, cy, cx + static_cast(std::cos(angle) * (size / 2 - 4)), + cy + static_cast(std::sin(angle) * (size / 2 - 4))); + fl_line_style(0); + if (Fl::focus() == this) { + fl_color(xtc_ui::text()); + fl_rect(x(), y(), w(), h()); + } + } + int handle(int event) override + { + if (event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks()) { + centering_ = true; + value(0); + redraw(); + do_callback(); + return 1; + } + if (centering_ && (event == FL_DRAG || event == FL_RELEASE)) { + if (event == FL_RELEASE) + centering_ = false; + return 1; + } + return Fl_Dial::handle(event); + } +}; + +/* FLTK owns these children. Identity is a model ID, never a dense track index. */ +class MixerStrip : public Fl_Group +{ + MixerWidget &owner_; + Fl_Button *name_; + Fl_Check_Button *mute_, *solo_; + PanKnob *pan_; + Fl_Value_Slider *gain_; + Fl_Choice *route_; + Fl_Box *pan_text_, *readout_; + std::vector destinations_; + static void control(Fl_Widget *widget, void *context) + { + auto *s = static_cast(context); + // Copy values before the owner refreshes dependent widgets. + const auto id = s->id; + xtc_track_info track{}; + size_t count = 0, clips = 0; + xtc_project_get_counts(s->owner_.project(), &count, &clips); + bool found = false; + for (size_t i = 0; i < count; ++i) { + xtc_project_get_track(s->owner_.project(), i, &track); + if (track.id == id) { + found = true; + break; + } + } + if (!found) + return; + const float gain = + widget == s->gain_ + ? (s->gain_->value() <= -60 ? 0.0f + : std::min(4.0f, static_cast(std::pow(10, s->gain_->value() / 20)))) + : track.gain; + const float pan = widget == s->pan_ ? static_cast(s->pan_->value()) : track.pan; + const int mute = widget == s->mute_ ? s->mute_->value() : track.mute; + const int solo = widget == s->solo_ ? s->solo_->value() : track.solo; + auto &owner = s->owner_; + if (owner.select_track(id)) + owner.mix(gain, pan, mute, solo); + } + + public: + uint64_t id; + xtc_peak_meter meter[2]{}; + MixerStrip(MixerWidget &owner, uint64_t track) : Fl_Group(0, 0, 148, 350), owner_(owner), id(track) + { + box(xtc_ui::flat); + color(xtc_ui::panel()); + begin(); + name_ = new Fl_Button(0, 0, 120, 26); + name_->callback( + [](Fl_Widget *, void *c) { + auto *s = static_cast(c); + s->owner_.select_track(s->id); + }, + this); + mute_ = new Fl_Check_Button(0, 0, 64, 24, "Mute"); + solo_ = new Fl_Check_Button(0, 0, 64, 24, "Solo"); + pan_ = new PanKnob(0, 0, 48, 48); + pan_text_ = new Fl_Box(0, 0, 124, 18); + route_ = new Fl_Choice(0, 0, 124, 24); + route_->tooltip("Output route: Master or project bus"); + route_->callback( + [](Fl_Widget *, void *c) { + auto *s = static_cast(c); + int i = s->route_->value(); + if (i < 0 || static_cast(i) >= s->destinations_.size()) + return; + auto bus = s->destinations_[static_cast(i)]; + auto &owner = s->owner_; + auto id = s->id; + if (owner.select_track(id)) + owner.route(bus); + }, + this); + gain_ = new Fl_Value_Slider(0, 0, 42, 150, "dB"); + gain_->align(FL_ALIGN_TOP); + gain_->labelsize(10); + gain_->type(FL_VERT_SLIDER); + gain_->slider(FL_FLAT_BOX); + gain_->slider_size(.10); + gain_->bounds(20 * std::log10(4.0), -60); + gain_->step(.1); + gain_->when(FL_WHEN_RELEASE); + gain_->tooltip( + "Level in dB: bottom is silence, 0 dB is unity, top is +12.04 dB. Automation overrides this static value."); + for (auto *w : {static_cast(gain_), static_cast(pan_), + static_cast(mute_), static_cast(solo_)}) + w->callback(control, this); + readout_ = new Fl_Box(0, 0, 132, 24); + readout_->labelsize(10); + end(); + xtc_ui::style(this); + pan_->label(""); + reset(); + resize(0, 0, 148, 350); + } + void reset() + { + xtc_peak_meter_reset(meter); + xtc_peak_meter_reset(meter + 1); + } + void sync(const xtc_track_info &track) + { + name_->copy_label((std::string(track.is_bus ? "Bus: " : "") + track.name).c_str()); + name_->tooltip(track.is_bus ? "Bus — select for automation" : "Track — select for automation"); + name_->color(owner_.selected_track() == id ? xtc_ui::track(id) : xtc_ui::panel()); + gain_->value(track.gain > 0 ? std::max(-60.0, 20 * std::log10(static_cast(track.gain))) : -60); + pan_->value(track.pan); + mute_->value(track.mute); + solo_->value(track.solo); + char text[64]; + std::snprintf(text, sizeof(text), "Pan %s %d%%", + track.pan < 0 ? "L" + : track.pan > 0 ? "R" + : "C", + static_cast(std::fabs(track.pan) * 100)); + pan_text_->copy_label(text); + route_->clear(); + destinations_.clear(); + destinations_.push_back(UINT64_MAX); + literal_item(route_,"Master"); + route_->value(0); + size_t tracks = 0, clips = 0; + xtc_project_get_counts(owner_.project(), &tracks, &clips); + for (size_t i = 0; i < tracks; ++i) { + xtc_track_info bus{}; + xtc_project_get_track(owner_.project(), i, &bus); + if (!bus.is_bus) + continue; + // Literal labels, including FLTK menu metacharacters. + int index = literal_item(route_,(std::string("Bus: ")+bus.name).c_str()); + destinations_.push_back(bus.id); + if (bus.id == track.bus_id) + route_->value(index); + } + if (track.is_bus) + route_->deactivate(); + else + route_->activate(); + redraw(); + } + void update(float left, float right, double elapsed) + { + xtc_peak_meter_update(meter, left, elapsed); + xtc_peak_meter_update(meter + 1, right, elapsed); + char text[96]; + std::snprintf(text, sizeof(text), "L %.1f R %.1f dBFS%s", static_cast(meter[0].db), + static_cast(meter[1].db), meter[0].clipped || meter[1].clipped ? " CLIP" : ""); + readout_->copy_label(text); + readout_->labelcolor(meter[0].clipped || meter[1].clipped ? FL_RED : xtc_ui::muted()); + redraw(); + } + void resize(int x, int y, int w, int h) override + { + Fl_Widget::resize(x, y, w, h); + name_->resize(x + 8, y + 8, w - 16, 26); + mute_->resize(x + 6, y + 42, 50, 24); + solo_->resize(x + 56, y + 42, 50, 24); + pan_->resize(x + 105, y + 38, 38, 38); + pan_text_->resize(x + 8, y + 78, w - 16, 18); + route_->resize(x + 8, y + 104, w - 16, 24); + gain_->resize(x + 10, y + 150, 44, h - 184); + readout_->resize(x + 4, y + h - 30, w - 8, 24); + redraw(); + } + void draw() override + { + Fl_Group::draw(); + fl_color(xtc_ui::track(id)); + fl_rectf(x() + 1, y() + 1, w() - 2, 3); + fl_font(FL_HELVETICA, 10); + for (int db : {12, 0, -12, -24, -48, -60}) { + char text[12]; + std::snprintf(text, sizeof(text), "%d", db); + if (h() < 300 && db != 0 && db != -60) + continue; + int py = y() + 175 + static_cast((12.0412 - db) / 72.0412 * (h() - 212)); + fl_color(xtc_ui::muted()); + fl_draw(text, x() + 112, py); + } + int top = y() + 150, height = h() - 184; + for (int side = 0; side < 2; ++side) { + int px = x() + 70 + side * 15; + fl_color(xtc_ui::background()); + fl_rectf(px, top, 11, height); + const float db = std::clamp(meter[side].db, -60.0f, 6.0f); + int pixels = static_cast((db + 60) / 66 * height); + fl_color(meter[side].clipped ? FL_RED : xtc_ui::accent()); + fl_rectf(px, top + height - pixels, 11, pixels); + fl_color(xtc_ui::border()); + fl_rect(px, top, 11, height); + fl_color(meter[side].clipped ? FL_RED : xtc_ui::border()); + fl_rectf(px, top - 6, 11, 3); + } + } +}; + /** * @brief Creates the mixer panel and its child controls. * - * Builds the track chooser, gain/pan sliders, mute/solo buttons, output + * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output * meters, automation controls, and lane, then displays the given project. * * @param x Left edge in parent coordinates. @@ -182,41 +463,39 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit begin(); 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_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_SLIDER); - pan_->bounds(-1, 1); - pan_->step(0.01); - pan_->when(FL_WHEN_RELEASE); - pan_->callback(control, this); - mute_ = new Fl_Check_Button(x + 340, y + 8, 90, 26, "Mute"); - solo_ = new Fl_Check_Button(x + 440, y + 8, 90, 26, "Solo"); - mute_->callback(control, this); - solo_->callback(control, this); routes_ = new Fl_Choice(x + 600, y + 8, 180, 26, "Route"); - routes_->callback([](Fl_Widget *, void *c) { - auto *m = static_cast(c); - int index = m->routes_->value(); - if (index >= 0 && static_cast(index) < m->route_ids_.size()) m->route(m->route_ids_[static_cast(index)]); - }, this); - auto *bus = new Fl_Button(x + 820, y + 8, 110, 26, "Add bus"); - bus->callback([](Fl_Widget *, void *c) { static_cast(c)->add_bus(); }, this); - left_ = new Fl_Progress(x + 70, y + 90, 320, 20, "Output L"); - right_ = new Fl_Progress(x + 480, y + 90, 320, 20, "Output R"); - track_left_ = new Fl_Progress(x + 70, y + 116, 320, 20, "Track L"); - track_right_ = new Fl_Progress(x + 480, y + 116, 320, 20, "Track R"); - for (auto *meter : {left_, right_, track_left_, track_right_}) { + routes_->callback( + [](Fl_Widget *, void *c) { + auto *m = static_cast(c); + int index = m->routes_->value(); + if (index >= 0 && static_cast(index) < m->route_ids_.size()) + m->route(m->route_ids_[static_cast(index)]); + }, + this); + bus_ = new Fl_Button(x + 820, y + 8, 110, 26, "Add bus"); + bus_->callback([](Fl_Widget *, void *c) { static_cast(c)->add_bus(); }, this); + left_ = new VerticalPeak(x + 70, y + 90, 320, 20, "Output L"); + right_ = new VerticalPeak(x + 480, y + 90, 320, 20, "Output R"); + for (auto *meter : {left_, right_}) { meter->minimum(-90); meter->maximum(6); - meter->selection_color(fl_rgb_color(80, 180, 120)); } - auto *reset = new Fl_Button(x + 820, y + 90, 110, 46, "Reset peaks"); - reset->callback([](Fl_Widget *, void *c) { static_cast(c)->reset_meters(); }, this); + reset_ = new Fl_Button(x, y, 120, 26, "Reset peaks"); + reset_->callback([](Fl_Widget *, void *c) { static_cast(c)->reset_meters(); }, this); + bank_ = new Fl_Scroll(x, y + 40, w - 154, h - 172); + bank_->type(Fl_Scroll::HORIZONTAL_ALWAYS); + bank_->end(); + master_panel_ = new Fl_Group(x + w - 144, y + 40, 144, h - 172); + master_panel_->box(xtc_ui::flat); + auto *master_name = new Fl_Box(x + w - 140, y + 48, 136, 26, "MASTER"); + master_name->labelfont(FL_HELVETICA_BOLD); + auto *master_hint = new Fl_Box(x + w - 140, y + 80, 136, 24, "Output monitor"); + master_hint->labelsize(11); + master_readout_ = new Fl_Box(x + w - 140, y + 340, 136, 40); + master_readout_->labelsize(10); + master_panel_->add(left_); + master_panel_->add(right_); + master_panel_->end(); reset_meters(); parameter_ = new Fl_Choice(x + 110, y + 146, 150, 26, "Automation"); parameter_->add("Gain|Pan"); @@ -227,14 +506,16 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit start_->step(3840); start_->callback(select, this); mode_ = new Fl_Choice(x + 705, y + 146, 150, 26, "Interpolation"); - mode_->add("Linear|Hold (step)"); mode_->value(0); - auto *hint = new Fl_Box(x + 10, y + 180, w - 20, 24, - "Gain 0..4; pan -1 left / +1 right. M/S toggles; arrows change gain, " - "Shift+arrows pan; Up/Down selects track."); - hint->labelsize(12); - lane_ = new AutomationLane(x + 10, y + 212, w - 20, h - 242, *this); + mode_->add("Linear|Hold (step)"); + mode_->value(0); + hint_ = new Fl_Box(x, y, w, 18, + "Select a strip for automation. M/S toggles; arrows adjust gain, Shift+arrows pan. Faders show " + "static dB gain."); + hint_->labelsize(11); + lane_ = new AutomationLane(x + 10, y + h - 80, w - 20, 76, *this); end(); set_project(p); + layout(); } /** @@ -245,8 +526,7 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit */ uint64_t MixerWidget::selected_track() const { - xtc_track_info t{}; - return xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK ? t.id : UINT64_MAX; + return selected_id_; } /** @@ -284,6 +564,7 @@ uint64_t MixerWidget::lane_start() const */ void MixerWidget::set_project(xtc_project *p) { + bool reset = project_ != p; uint64_t selected = project_ == p ? selected_track() : UINT64_MAX; project_ = p; tracks_->clear(); @@ -295,18 +576,15 @@ void MixerWidget::set_project(xtc_project *p) for (size_t i = 0; i < n; ++i) { xtc_track_info t{}; xtc_project_get_track(p, i, &t); - std::string label; - for (char ch : std::string(t.name)) { - if (ch == '/' || ch == '\\' || ch == '_' || ch == '&') - label += '\\'; - label += ch; - } - tracks_->add((std::string(t.is_bus ? "Bus: " : "Track: ") + label).c_str()); - if (t.is_bus) { routes_->add(label.c_str()); route_ids_.push_back(t.id); } + literal_item(tracks_,(std::string(t.is_bus?"Bus: ":"Track: ")+t.name).c_str()); + if(t.is_bus){literal_item(routes_,(std::string("Bus: ")+t.name).c_str());route_ids_.push_back(t.id);} if (t.id == selected) index = static_cast(i); } tracks_->value(n ? index : -1); + selected_id_=UINT64_MAX; + if(n){xtc_track_info track{};xtc_project_get_track(p,static_cast(index),&track);selected_id_=track.id;} + sync_strips(reset); selection(); redraw(); } @@ -314,32 +592,44 @@ void MixerWidget::set_project(xtc_project *p) /** * @brief Synchronizes the child controls with the selected track. * - * Controls are deactivated when no valid track is selected. Gain falls - * back to 1, and the remaining values come from the track info or zero when - * unresolved. + * Reconcile strip values and routing with model IDs, highlight the selected + * track, and keep its automation controls in sync. */ void MixerWidget::selection() { xtc_track_info t{}; bool valid = xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK; - for (Fl_Widget *w : - {static_cast(gain_), static_cast(pan_), static_cast(mute_), - static_cast(solo_), static_cast(lane_)}) { - if (valid) - w->activate(); - else - w->deactivate(); + if (valid) + lane_->activate(); + else + lane_->deactivate(); + for (auto *s : strips_) { + xtc_track_info track{}; + size_t count = 0, clips = 0; + xtc_project_get_counts(project_, &count, &clips); + for (size_t i = 0; i < count; ++i) { + xtc_project_get_track(project_, i, &track); + if (track.id == s->id) { + s->sync(track); + break; + } + } } - gain_->value(valid ? t.gain : 1); - pan_->value(t.pan); - mute_->value(t.mute); - solo_->value(t.solo); - if (valid && !t.is_bus) routes_->activate(); else routes_->deactivate(); + if (valid && !t.is_bus) + routes_->activate(); + else + routes_->deactivate(); routes_->value(0); - for (size_t i = 0; i < route_ids_.size(); ++i) if (route_ids_[i] == t.bus_id) routes_->value(static_cast(i)); - if (meter_track_ != selected_track()) { - meter_track_ = selected_track(); - xtc_peak_meter_reset(track_meter_); xtc_peak_meter_reset(track_meter_+1); + for (size_t i = 0; i < route_ids_.size(); ++i) + if (route_ids_[i] == t.bus_id) + routes_->value(static_cast(i)); + if (auto *selected = strip(selected_track())) { + int target = bank_->xposition(); + if (selected->x() < bank_->x()) + target -= bank_->x() - selected->x(); + else if (selected->x() + selected->w() > bank_->x() + bank_->w() - 12) + target += selected->x() + selected->w() - (bank_->x() + bank_->w() - 12); + bank_->scroll_to(std::max(0, target), 0); } lane_->redraw(); } @@ -361,6 +651,8 @@ void MixerWidget::peaks(float left, float right) (void)xtc_peak_meter_update(master_, left, meter_elapsed_); (void)xtc_peak_meter_update(master_+1, right, meter_elapsed_); display_peaks(left_, right_, master_, "Master"); + char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); + master_readout_->copy_label(text);master_readout_->align(FL_ALIGN_WRAP|FL_ALIGN_INSIDE); } /** @@ -425,29 +717,18 @@ xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) return changed(r); } -/** - * @brief FLTK callback for the gain, pan, mute, and solo controls. - * - * Reads all four controls and applies them together to the selected track - * through mix(). - * - * @param c Opaque MixerWidget pointer supplied as callback context. - */ -void MixerWidget::control(Fl_Widget *, void *c) -{ - auto *m = static_cast(c); - m->mix(static_cast(m->gain_->value()), static_cast(m->pan_->value()), m->mute_->value(), - m->solo_->value()); -} - /** * @brief FLTK callback for the track, parameter, and first-tick controls. * * @param c Opaque MixerWidget pointer supplied as callback context. */ -void MixerWidget::select(Fl_Widget *, void *c) +void MixerWidget::select(Fl_Widget *widget, void *c) { - static_cast(c)->selection(); + auto *m=static_cast(c); + if(widget==m->tracks_){xtc_track_info track{}; + if(xtc_project_get_track(m->project_,static_cast(m->tracks_->value()),&track)==XTC_OK)m->selected_id_=track.id; + } + m->selection(); } /** @@ -481,7 +762,7 @@ int MixerWidget::handle(int e) size_t n = 0, c = 0; xtc_project_get_counts(project_, &n, &c); tracks_->value(std::clamp(tracks_->value() + (key == FL_Up ? -1 : 1), 0, static_cast(n) - 1)); - selection(); + select(tracks_,this); return 1; } } @@ -498,22 +779,20 @@ void MixerWidget::display_peaks(Fl_Progress *left, Fl_Progress *right, xtc_peak_ static_cast(meters[side].db), meters[side].clipped ? " CLIP" : ""); bar->copy_label(label); bar->value(std::clamp(meters[side].db, -90.0f, 6.0f)); - bar->selection_color(meters[side].clipped ? FL_RED : FL_GREEN); + bar->selection_color(meters[side].clipped ? FL_RED : xtc_ui::accent()); } } void MixerWidget::track_peaks(float left, float right) { - (void)xtc_peak_meter_update(track_meter_, left, meter_elapsed_); - (void)xtc_peak_meter_update(track_meter_+1, right, meter_elapsed_); - xtc_track_info track{}; - bool bus = xtc_project_get_track(project_, static_cast(tracks_->value()), &track) == XTC_OK && track.is_bus; - display_peaks(track_left_, track_right_, track_meter_, bus ? "Bus" : "Track"); + for(auto *s:strips_)if(s->id==selected_track())s->update(left,right,meter_elapsed_); } void MixerWidget::reset_meters() { - for (unsigned int i = 0; i < 2; ++i) { xtc_peak_meter_reset(master_+i); xtc_peak_meter_reset(track_meter_+i); } + for (unsigned int i = 0; i < 2; ++i) { xtc_peak_meter_reset(master_+i); } display_peaks(left_, right_, master_, "Master"); - display_peaks(track_left_, track_right_, track_meter_, "Track"); + char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); + master_readout_->copy_label(text);master_readout_->align(FL_ALIGN_WRAP|FL_ALIGN_INSIDE); + for(auto *s:strips_){s->reset();s->update(0,0,0);} } xtc_result MixerWidget::add_bus() { @@ -534,7 +813,89 @@ bool MixerWidget::select_track(uint64_t id) size_t n = 0, clips = 0; xtc_project_get_counts(project_, &n, &clips); for (size_t i = 0; i < n; ++i) { xtc_track_info track{}; xtc_project_get_track(project_, i, &track); - if (track.id == id) { tracks_->value(static_cast(i)); selection(); return true; } + if (track.id == id) { selected_id_=id;tracks_->value(static_cast(i)); selection(); return true; } } return false; } + +Fl_Group *MixerWidget::strip(uint64_t id) const +{ + for (auto *s : strips_) + if (s->id == id) + return s; + return nullptr; +} +void MixerWidget::meters(const xtc_meter_snapshot &snapshot) +{ + peaks(snapshot.master_left, snapshot.master_right); + for (auto *s : strips_) { + float left = 0, right = 0; + for (size_t i = 0; i < std::min(snapshot.count, static_cast(XTC_PROJECT_MAX_TRACKS)); ++i) + if (snapshot.tracks[i].id == s->id) { + left = snapshot.tracks[i].left; + right = snapshot.tracks[i].right; + break; + } + s->update(left, right, meter_elapsed_); + } +} +void MixerWidget::sync_strips(bool reset) +{ + size_t n = 0, clips = 0; + xtc_project_get_counts(project_, &n, &clips); + bool rebuild = n != strips_.size(); + for (size_t i = 0; i < n && !rebuild; ++i) { + xtc_track_info t{}; + xtc_project_get_track(project_, i, &t); + rebuild = t.id != strips_[i]->id; + } + if (rebuild) { + auto *previous = Fl_Group::current(); + bank_->clear(); + strips_.clear(); + bank_->begin(); + for (size_t i = 0; i < n; ++i) { + xtc_track_info t{}; + xtc_project_get_track(project_, i, &t); + strips_.push_back(new MixerStrip(*this, t.id)); + } + bank_->end(); + Fl_Group::current(previous); + layout(); + } + if (reset) + for (auto *s : strips_) + s->reset(); +} +void MixerWidget::layout() +{ + const int bank_height = std::max(250, h() - 172); + tracks_->resize(x() + 48, y() + 4, 210, 26); + routes_->resize(x() + 326, y() + 4, 160, 26); + bus_->resize(x() + 500, y() + 4, 100, 26); + reset_->resize(x() + 612, y() + 4, 112, 26); + bank_->resize(x(), y() + 40, w() - 154, bank_height); + int offset = bank_->xposition(); + for (size_t i = 0; i < strips_.size(); ++i) + strips_[i]->resize(bank_->x() + static_cast(i) * 156 - offset, bank_->y(), 148, bank_height - 16); + master_panel_->resize(x() + w() - 144, y() + 40, 144, bank_height); + // Master is pinned outside the horizontally scrolling bank. + left_->resize(x() + w() - 118, y() + 118, 32, bank_height - 140); + right_->resize(x() + w() - 66, y() + 118, 32, bank_height - 140); + master_readout_->resize(x() + w() - 140, y() + 40 + bank_height - 52, 136, 42); + left_->align(FL_ALIGN_TOP); + right_->align(FL_ALIGN_TOP); + left_->labelsize(10); + right_->labelsize(10); + parameter_->resize(x() + 84, y() + h() - 120, 100, 26); + start_->resize(x() + 264, y() + h() - 120, 116, 26); + mode_->resize(x() + 488, y() + h() - 120, 136, 26); + hint_->resize(x() + 8, y() + h() - 92, w() - 16, 18); + lane_->resize(x() + 8, y() + h() - 72, w() - 16, 68); + redraw(); +} +void MixerWidget::resize(int x, int y, int w, int h) +{ + Fl_Widget::resize(x, y, w, h); + layout(); +} diff --git a/src/gui/mixer_widget.hpp b/src/gui/mixer_widget.hpp index 5de73ea..98ad9da 100644 --- a/src/gui/mixer_widget.hpp +++ b/src/gui/mixer_widget.hpp @@ -1,16 +1,21 @@ #ifndef XTC_MIXER_WIDGET_HPP #define XTC_MIXER_WIDGET_HPP #include "xtc/xtc_mixer.h" +#include "xtc/xtc_backend.h" #include #include #include class Fl_Choice; +class Fl_Button; class Fl_Value_Slider; class Fl_Check_Button; class Fl_Progress; class Fl_Value_Input; class AutomationLane; +class MixerStrip; +class Fl_Scroll; +class Fl_Box; class MixerWidget : public Fl_Group { public: @@ -24,7 +29,7 @@ class MixerWidget : public Fl_Group { /** * @brief Creates the mixer panel and its child controls. * - * Builds the track chooser, gain/pan sliders, mute/solo buttons, output + * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output * meters, automation controls, and lane, then displays the given project. * * @param x Left edge in parent coordinates. @@ -124,6 +129,11 @@ class MixerWidget : public Fl_Group { xtc_result route(uint64_t bus); bool select_track(uint64_t id); void track_peaks(float left, float right); + /* One control-thread snapshot updates all strips; missing taps decay to silence. */ + void meters(const xtc_meter_snapshot &snapshot); + Fl_Group *strip(uint64_t id) const; + size_t strip_count() const { return strips_.size(); } + void resize(int x, int y, int w, int h) override; void reset_meters(); bool master_clipped() const { return master_[0].clipped || master_[1].clipped; } xtc_result mix(float gain, float pan, int mute, int solo); @@ -145,11 +155,16 @@ class MixerWidget : public Fl_Group { void *context_; Fl_Choice *tracks_, *parameter_, *routes_, *mode_; std::vector route_ids_; - Fl_Value_Slider *gain_, *pan_; - Fl_Check_Button *mute_, *solo_; - Fl_Progress *left_, *right_, *track_left_, *track_right_; - xtc_peak_meter master_[2]{}, track_meter_[2]{}; - uint64_t meter_track_ = UINT64_MAX; + Fl_Progress *left_, *right_; + Fl_Scroll *bank_; + Fl_Group *master_panel_; + Fl_Button *bus_, *reset_; + Fl_Box *hint_, *master_readout_; + std::vector strips_; + void sync_strips(bool reset = false); + void layout(); + xtc_peak_meter master_[2]{}; + uint64_t selected_id_ = UINT64_MAX; std::chrono::steady_clock::time_point peak_time_ = std::chrono::steady_clock::now(); double meter_elapsed_ = 0.1; void display_peaks(Fl_Progress *left, Fl_Progress *right, xtc_peak_meter *meters, const char *prefix); @@ -171,27 +186,15 @@ class MixerWidget : public Fl_Group { /** * @brief Synchronizes the child controls with the selected track. * - * Controls are deactivated when no valid track is selected. Gain falls - * back to 1, and the remaining values come from the track info or zero when - * unresolved. + * Reconcile strip controls with stable model IDs and the selected + * automation target. */ void selection(); - /** - * @brief FLTK callback for the gain, pan, mute, and solo controls. - * - * Reads all four controls and applies them together to the selected track - * through mix(). - * - * @param widget Widget that triggered the callback; unused. - * @param c Opaque MixerWidget pointer supplied as callback context. - */ - static void control(Fl_Widget * widget, void * c); - /** * @brief FLTK callback for the track, parameter, and first-tick controls. * - * @param widget Widget that triggered the callback; unused. + * @param widget Track chooser or automation control that changed. * @param c Opaque MixerWidget pointer supplied as callback context. */ static void select(Fl_Widget * widget, void * c); diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 186a39a..0090c35 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -1006,6 +1009,77 @@ int main(int argc, char **argv) } unlink(path.c_str());rmdir(dir); } + { + reset();MainWindow window(false,"");window.show();auto *m=window.mixer(); + CHECK(m->strip_count()==0 && m->strip(UINT64_MAX)==nullptr); + CHECK(window.create_track(true)==XTC_OK && window.create_track(true)==XTC_OK); + xtc_track_info a{},b{};xtc_project_get_track(m->project(),0,&a);xtc_project_get_track(m->project(),1,&b); + CHECK(m->strip_count()==2 && m->strip(a.id) && m->strip(b.id)); + auto *first=m->strip(a.id); + auto *fader=dynamic_cast(find(first,"dB"));CHECK(fader); + CHECK(fader && fader->type()==FL_VERT_SLIDER && fader->h()>fader->w()); + if(fader){fader->value(-6);fader->do_callback();} + xtc_project_get_track(m->project(),0,&a);xtc_project_get_track(m->project(),1,&b); + CHECK(std::fabs(a.gain-std::pow(10.f,-6.f/20))<.0001f && b.gain==1); + CHECK(first==m->strip(a.id)); // Editing must not delete its callback's widget. + if(fader){gesture(fader,20,40,0,30);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain<.5f);CHECK(window.undo()==XTC_OK);} + if(fader){ + reserve_result=XTC_ERR_OUT_OF_MEMORY; + Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();CHECK(dialog && contains(dialog,"Mixer edit failed"));if(dialog)dialog->hide();}); + fader->value(-12);fader->do_callback();reserve_result=XTC_OK; + xtc_project_get_track(m->project(),0,&a);CHECK(std::fabs(a.gain-std::pow(10.f,-6.f/20))<.0001f); + CHECK(std::fabs(fader->value()+6)<.001); + } + press(find(m->strip(b.id),b.name));CHECK(m->selected_track()==b.id); + CHECK(window.undo()==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain==1); + CHECK(window.undo(true)==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain<.51f); + Fl_Dial *pan=nullptr;for(int i=0;istrip(b.id)->children();++i)if(auto *d=dynamic_cast(m->strip(b.id)->child(i)))pan=d; + CHECK(pan);if(pan){pan->value(-.75);pan->do_callback();} + xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==-.75f && b.gain==1); + auto *mute=dynamic_cast(find(m->strip(b.id),"Mute"));CHECK(mute); + if(mute){mute->value(1);mute->do_callback();}xtc_project_get_track(m->project(),1,&b);CHECK(b.mute && b.pan==-.75f); + if(pan){int old=Fl::e_clicks;Fl::e_clicks=1;gesture(pan,20,20,0,0);Fl::e_clicks=old;} + xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==0 && b.mute); + CHECK(m->add_bus()==XTC_OK);uint64_t bus=m->selected_track();CHECK(m->strip_count()==3 && contains(m->strip(bus),"Bus:")); + Fl_Choice *route=nullptr;for(int i=0;istrip(a.id)->children();++i)if(auto *c=dynamic_cast(m->strip(a.id)->child(i)))route=c; + CHECK(route && route->size()==3); // Master, bus, terminator. + if(route){route->value(1);route->do_callback();}xtc_project_get_track(m->project(),0,&a);CHECK(a.bus_id==bus); + CHECK(window.undo()==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.bus_id==UINT64_MAX); + CHECK(m->add_bus()==XTC_OK);CHECK(m->strip_count()==4); + auto *routes=dynamic_cast(find(m,"Route"));CHECK(routes && routes->size()==4); + CHECK(window.undo()==XTC_OK);CHECK(m->strip_count()==3); + xtc_meter_snapshot snapshot{};snapshot.count=2;snapshot.tracks[0]={a.id,.5f,.25f};snapshot.tracks[1]={b.id,1.2f,.125f}; + m->meters(snapshot);CHECK(contains(m->strip(a.id),"L -6.0") && contains(m->strip(b.id),"CLIP")); + m->select_track(a.id);CHECK(contains(m->strip(b.id),"CLIP"));m->meters(xtc_meter_snapshot{});CHECK(contains(m->strip(b.id),"CLIP")); + m->reset_meters();CHECK(!contains(m->strip(b.id),"CLIP")); + CHECK(window.save_project("/tmp/xtc-mixer-strips.xtc")==XTC_OK && window.open_project("/tmp/xtc-mixer-strips.xtc")==XTC_OK); + CHECK(m->strip_count()==3);xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==0 && b.mute); + CHECK(m->select_track(b.id) && m->mix(.00001f,0,0,0)==XTC_OK); + mute=dynamic_cast(find(m->strip(b.id),"Mute"));CHECK(mute); + if(mute){mute->value(1);mute->do_callback();}xtc_project_get_track(m->project(),1,&b);CHECK(b.gain==.00001f && b.mute); + unlink("/tmp/xtc-mixer-strips.xtc");window.do_callback(); + } + { + xtc_project *project=nullptr;CHECK(xtc_project_create(&project)==XTC_OK); + uint64_t first=UINT64_MAX,last=UINT64_MAX; + for(size_t i=0;i(find(&m,"Track")); + CHECK(track_choice && track_choice->size()==static_cast(XTC_PROJECT_MAX_TRACKS)+1); + CHECK(track_choice && track_choice->value()==static_cast(XTC_PROJECT_MAX_TRACKS)-1); + CHECK(m.strip(last)->x()>=m.x() && m.strip(last)->x()+m.strip(last)->w()(find(m.strip(last),"dB"));CHECK(f && f->h()>40); + m.resize(0,0,1500,900);CHECK(f && f->h()>400); + window.hide(); + } + xtc_project_destroy(project); + } if (failures != 0) return 1; std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes); return 0;