Phase 36: add studio mixer channel strips and refresh screenshots
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
screenshots/22-mixer-playing.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
screenshots/23-mixer-minimum-size.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
screenshots/24-mixer-scrolled-session.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include <FL/Fl_Value_Input.H>
|
||||
#include <FL/Fl_Input_.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <FL/Fl_Dial.H>
|
||||
#include <FL/Fl_Scroll.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <algorithm>
|
||||
|
|
@ -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<int>((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<int>(std::cos(angle) * (size / 2 - 4)),
|
||||
cy + static_cast<int>(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<uint64_t> destinations_;
|
||||
static void control(Fl_Widget *widget, void *context)
|
||||
{
|
||||
auto *s = static_cast<MixerStrip *>(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<float>(std::pow(10, s->gain_->value() / 20))))
|
||||
: track.gain;
|
||||
const float pan = widget == s->pan_ ? static_cast<float>(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<MixerStrip *>(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<MixerStrip *>(c);
|
||||
int i = s->route_->value();
|
||||
if (i < 0 || static_cast<size_t>(i) >= s->destinations_.size())
|
||||
return;
|
||||
auto bus = s->destinations_[static_cast<size_t>(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<Fl_Widget *>(gain_), static_cast<Fl_Widget *>(pan_),
|
||||
static_cast<Fl_Widget *>(mute_), static_cast<Fl_Widget *>(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<double>(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<int>(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<double>(meter[0].db),
|
||||
static_cast<double>(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<int>((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<int>((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<MixerWidget *>(c);
|
||||
int index = m->routes_->value();
|
||||
if (index >= 0 && static_cast<size_t>(index) < m->route_ids_.size()) m->route(m->route_ids_[static_cast<size_t>(index)]);
|
||||
}, this);
|
||||
auto *bus = new Fl_Button(x + 820, y + 8, 110, 26, "Add bus");
|
||||
bus->callback([](Fl_Widget *, void *c) { static_cast<MixerWidget *>(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<MixerWidget *>(c);
|
||||
int index = m->routes_->value();
|
||||
if (index >= 0 && static_cast<size_t>(index) < m->route_ids_.size())
|
||||
m->route(m->route_ids_[static_cast<size_t>(index)]);
|
||||
},
|
||||
this);
|
||||
bus_ = new Fl_Button(x + 820, y + 8, 110, 26, "Add bus");
|
||||
bus_->callback([](Fl_Widget *, void *c) { static_cast<MixerWidget *>(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<MixerWidget *>(c)->reset_meters(); }, this);
|
||||
reset_ = new Fl_Button(x, y, 120, 26, "Reset peaks");
|
||||
reset_->callback([](Fl_Widget *, void *c) { static_cast<MixerWidget *>(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<size_t>(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<int>(i);
|
||||
}
|
||||
tracks_->value(n ? index : -1);
|
||||
selected_id_=UINT64_MAX;
|
||||
if(n){xtc_track_info track{};xtc_project_get_track(p,static_cast<size_t>(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<size_t>(tracks_->value()), &t) == XTC_OK;
|
||||
for (Fl_Widget *w :
|
||||
{static_cast<Fl_Widget *>(gain_), static_cast<Fl_Widget *>(pan_), static_cast<Fl_Widget *>(mute_),
|
||||
static_cast<Fl_Widget *>(solo_), static_cast<Fl_Widget *>(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<int>(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<int>(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<double>(master_[0].db),static_cast<double>(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<MixerWidget *>(c);
|
||||
m->mix(static_cast<float>(m->gain_->value()), static_cast<float>(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<MixerWidget *>(c)->selection();
|
||||
auto *m=static_cast<MixerWidget *>(c);
|
||||
if(widget==m->tracks_){xtc_track_info track{};
|
||||
if(xtc_project_get_track(m->project_,static_cast<size_t>(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<int>(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<double>(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<size_t>(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<double>(master_[0].db),static_cast<double>(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<int>(i)); selection(); return true; }
|
||||
if (track.id == id) { selected_id_=id;tracks_->value(static_cast<int>(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<size_t>(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<int>(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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <FL/Fl_Group.H>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
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<uint64_t> 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<MixerStrip *> 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);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <FL/Fl_Dial.H>
|
||||
#include <FL/Fl_Check_Button.H>
|
||||
#include <FL/Fl_Scroll.H>
|
||||
#include <unistd.h>
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
|
|
@ -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<Fl_Value_Slider *>(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;i<m->strip(b.id)->children();++i)if(auto *d=dynamic_cast<Fl_Dial *>(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<Fl_Check_Button *>(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;i<m->strip(a.id)->children();++i)if(auto *c=dynamic_cast<Fl_Choice *>(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<Fl_Choice *>(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<Fl_Check_Button *>(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<XTC_PROJECT_MAX_TRACKS;++i){uint64_t id;char name[40];std::snprintf(name,sizeof(name),"Duplicate/name & _track");
|
||||
CHECK(xtc_project_add_track(project,name,&id)==XTC_OK);if(i==0)first=id;last=id;}
|
||||
{
|
||||
Fl_Double_Window window(1100,600);window.begin();MixerWidget m(0,0,1032,522,project,nullptr,nullptr);window.end();window.show();
|
||||
CHECK(m.strip_count()==XTC_PROJECT_MAX_TRACKS && m.select_track(last));
|
||||
auto *track_choice=dynamic_cast<Fl_Choice *>(find(&m,"Track"));
|
||||
CHECK(track_choice && track_choice->size()==static_cast<int>(XTC_PROJECT_MAX_TRACKS)+1);
|
||||
CHECK(track_choice && track_choice->value()==static_cast<int>(XTC_PROJECT_MAX_TRACKS)-1);
|
||||
CHECK(m.strip(last)->x()>=m.x() && m.strip(last)->x()+m.strip(last)->w()<m.x()+m.w()-144);
|
||||
CHECK(xtc_project_move_track(project,last,0)==XTC_OK);m.set_project(project);
|
||||
CHECK(m.selected_track()==last && m.strip_count()==XTC_PROJECT_MAX_TRACKS);
|
||||
CHECK(xtc_project_remove_track(project,first)==XTC_OK);m.set_project(project);CHECK(!m.strip(first));
|
||||
m.resize(0,0,972,442);m.select_track(last);auto *f=dynamic_cast<Fl_Value_Slider *>(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;
|
||||
|
|
|
|||