314 lines
12 KiB
C++
314 lines
12 KiB
C++
#include "mixer_widget.hpp"
|
|
#include <FL/Fl.H>
|
|
#include <FL/Fl_Box.H>
|
|
#include <FL/Fl_Check_Button.H>
|
|
#include <FL/Fl_Choice.H>
|
|
#include <FL/Fl_Progress.H>
|
|
#include <FL/Fl_Value_Input.H>
|
|
#include <FL/Fl_Value_Slider.H>
|
|
#include <FL/fl_ask.H>
|
|
#include <FL/fl_draw.H>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
namespace
|
|
{
|
|
constexpr uint64_t lane_ticks = 15360;
|
|
}
|
|
class AutomationLane : public Fl_Widget
|
|
{
|
|
MixerWidget &mixer_;
|
|
|
|
public:
|
|
AutomationLane(int x, int y, int w, int h, MixerWidget &m) : Fl_Widget(x, y, w, h), mixer_(m) {}
|
|
void draw() override
|
|
{
|
|
fl_push_clip(x(), y(), w(), h());
|
|
fl_color(fl_rgb_color(32, 35, 40));
|
|
fl_rectf(x(), y(), w(), h());
|
|
fl_font(FL_HELVETICA, 11);
|
|
for (int i = 0; i <= 16; ++i) {
|
|
int px = x() + i * (w() - 1) / 16;
|
|
fl_color(fl_rgb_color(i % 4 ? 55 : 95, 60, 65));
|
|
fl_line(px, y(), px, y() + h());
|
|
if (i % 4 == 0) {
|
|
char label[32];
|
|
std::snprintf(label, sizeof(label), "%llu",
|
|
static_cast<unsigned long long>(mixer_.lane_start() +
|
|
static_cast<uint64_t>(i) * 960));
|
|
fl_color(FL_WHITE);
|
|
fl_draw(label, px + 3, y() + 14);
|
|
}
|
|
}
|
|
size_t count = 0;
|
|
xtc_project_automation_count(mixer_.project(), &count);
|
|
std::vector<xtc_automation_point> points;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
xtc_automation_point a{};
|
|
xtc_project_get_automation(mixer_.project(), i, &a);
|
|
if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter())
|
|
points.push_back(a);
|
|
}
|
|
std::sort(points.begin(), points.end(),
|
|
[](const auto &a, const auto &b) { return a.tick < b.tick; });
|
|
float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f;
|
|
float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f;
|
|
auto py = [&](float v) {
|
|
return y() + h() - 6 - static_cast<int>((v - min) / range * (h() - 28));
|
|
};
|
|
auto value = [&](uint64_t tick) {
|
|
if (points.empty())
|
|
return min;
|
|
auto it = std::upper_bound(points.begin(), points.end(), tick,
|
|
[](uint64_t t, const auto &a) { return t < a.tick; });
|
|
if (it == points.begin())
|
|
return it->value;
|
|
if (it == points.end())
|
|
return points.back().value;
|
|
const auto &a = *(it - 1);
|
|
return a.value + static_cast<float>(static_cast<double>(tick - a.tick) /
|
|
static_cast<double>(it->tick - a.tick)) *
|
|
(it->value - a.value);
|
|
};
|
|
fl_color(fl_rgb_color(105, 200, 150));
|
|
if (!points.empty())
|
|
for (int px = 1; px < w(); ++px) {
|
|
uint64_t a = mixer_.lane_start() + static_cast<uint64_t>(px - 1) * lane_ticks /
|
|
static_cast<uint64_t>(w() - 1);
|
|
uint64_t b = mixer_.lane_start() + static_cast<uint64_t>(px) * lane_ticks /
|
|
static_cast<uint64_t>(w() - 1);
|
|
fl_line(x() + px - 1, py(value(a)), x() + px, py(value(b)));
|
|
}
|
|
for (const auto &a : points)
|
|
if (a.tick >= mixer_.lane_start() && a.tick <= mixer_.lane_start() + lane_ticks) {
|
|
int px = x() + static_cast<int>((a.tick - mixer_.lane_start()) *
|
|
static_cast<uint64_t>(w() - 1) / lane_ticks);
|
|
fl_rectf(px - 4, py(a.value) - 4, 8, 8);
|
|
}
|
|
if (points.empty()) {
|
|
fl_color(FL_WHITE);
|
|
fl_draw("Click to add automation; right-click a point to remove. Values replace the "
|
|
"static control.",
|
|
x() + 12, y() + h() / 2);
|
|
}
|
|
fl_pop_clip();
|
|
}
|
|
int handle(int event) override
|
|
{
|
|
if (event != FL_PUSH)
|
|
return Fl_Widget::handle(event);
|
|
if (Fl::event_button() != FL_LEFT_MOUSE && Fl::event_button() != FL_RIGHT_MOUSE)
|
|
return 0;
|
|
uint64_t tick = mixer_.lane_start() +
|
|
static_cast<uint64_t>(std::clamp(Fl::event_x() - x(), 0, w() - 1)) *
|
|
lane_ticks / static_cast<uint64_t>(w() - 1);
|
|
tick = (tick + 120) / 240 * 240;
|
|
tick = std::min(tick, XTC_PROJECT_MAX_TICK);
|
|
float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f;
|
|
float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f;
|
|
float value =
|
|
min + static_cast<float>(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) /
|
|
static_cast<float>(h() - 28) * range;
|
|
if (Fl::event_button() == FL_RIGHT_MOUSE) {
|
|
size_t count = 0;
|
|
xtc_project_automation_count(mixer_.project(), &count);
|
|
uint64_t distance =
|
|
static_cast<uint64_t>(8) * lane_ticks / static_cast<uint64_t>(w() - 1) + 1,
|
|
closest = UINT64_MAX;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
xtc_automation_point a{};
|
|
xtc_project_get_automation(mixer_.project(), i, &a);
|
|
uint64_t d = a.tick > tick ? a.tick - tick : tick - a.tick;
|
|
if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() &&
|
|
d < distance) {
|
|
distance = d;
|
|
closest = a.tick;
|
|
}
|
|
}
|
|
if (closest != UINT64_MAX)
|
|
mixer_.point(closest, 0, true);
|
|
} else
|
|
mixer_.point(tick, value);
|
|
return 1;
|
|
}
|
|
};
|
|
MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before,
|
|
void *context)
|
|
: Fl_Group(x, y, w, h), project_(p), before_(before), context_(context)
|
|
{
|
|
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_NICE_SLIDER);
|
|
gain_->bounds(0, 4);
|
|
gain_->step(0.01);
|
|
gain_->when(FL_WHEN_RELEASE);
|
|
gain_->callback(control, this);
|
|
pan_ = new Fl_Value_Slider(x + 480, y + 48, 320, 28, "Pan");
|
|
pan_->type(FL_HOR_NICE_SLIDER);
|
|
pan_->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);
|
|
left_ = new Fl_Progress(x + 70, y + 90, 320, 20, "Output L");
|
|
right_ = new Fl_Progress(x + 480, y + 90, 320, 20, "Output R");
|
|
for (auto *meter : {left_, right_}) {
|
|
meter->minimum(0);
|
|
meter->maximum(1);
|
|
meter->selection_color(fl_rgb_color(80, 180, 120));
|
|
}
|
|
parameter_ = new Fl_Choice(x + 110, y + 126, 150, 26, "Automation");
|
|
parameter_->add("Gain|Pan");
|
|
parameter_->value(0);
|
|
parameter_->callback(select, this);
|
|
start_ = new Fl_Value_Input(x + 420, y + 126, 140, 26, "First tick");
|
|
start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks);
|
|
start_->step(3840);
|
|
start_->callback(select, this);
|
|
auto *hint = new Fl_Box(x + 10, y + 160, 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 + 192, w - 20, h - 222, *this);
|
|
end();
|
|
set_project(p);
|
|
}
|
|
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;
|
|
}
|
|
xtc_automation_parameter MixerWidget::parameter() const
|
|
{
|
|
return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN;
|
|
}
|
|
uint64_t MixerWidget::lane_start() const
|
|
{
|
|
if (!std::isfinite(start_->value())) return 0;
|
|
return static_cast<uint64_t>(
|
|
std::clamp(start_->value(), 0.0, static_cast<double>(XTC_PROJECT_MAX_TICK - lane_ticks)));
|
|
}
|
|
void MixerWidget::set_project(xtc_project *p)
|
|
{
|
|
uint64_t selected = project_ == p ? selected_track() : UINT64_MAX;
|
|
project_ = p;
|
|
tracks_->clear();
|
|
size_t n = 0, c = 0;
|
|
xtc_project_get_counts(p, &n, &c);
|
|
int index = 0;
|
|
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(label.c_str());
|
|
if (t.id == selected)
|
|
index = static_cast<int>(i);
|
|
}
|
|
tracks_->value(n ? index : -1);
|
|
selection();
|
|
redraw();
|
|
}
|
|
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();
|
|
}
|
|
gain_->value(valid ? t.gain : 1);
|
|
pan_->value(t.pan);
|
|
mute_->value(t.mute);
|
|
solo_->value(t.solo);
|
|
lane_->redraw();
|
|
}
|
|
void MixerWidget::peaks(float left, float right)
|
|
{
|
|
left_->value(std::min(left, 1.0f));
|
|
right_->value(std::min(right, 1.0f));
|
|
left_->selection_color(left > 1 ? FL_RED : FL_GREEN);
|
|
right_->selection_color(right > 1 ? FL_RED : FL_GREEN);
|
|
}
|
|
xtc_result MixerWidget::changed(xtc_result r)
|
|
{
|
|
if (r == XTC_OK)
|
|
do_callback();
|
|
else
|
|
fl_alert("Mixer edit failed: %s", xtc_result_string(r));
|
|
selection();
|
|
return r;
|
|
}
|
|
xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo)
|
|
{
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
|
if (r == XTC_OK)
|
|
r = xtc_project_set_mix(project_, selected_track(), gain, pan, mute, solo);
|
|
return changed(r);
|
|
}
|
|
xtc_result MixerWidget::point(uint64_t tick, float value, bool remove)
|
|
{
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
|
xtc_automation_point a{selected_track(), tick, parameter(), value};
|
|
if (r == XTC_OK)
|
|
r = remove ? xtc_project_remove_automation(project_, a.track_id, a.parameter, tick)
|
|
: xtc_project_set_automation(project_, &a);
|
|
return changed(r);
|
|
}
|
|
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());
|
|
}
|
|
void MixerWidget::select(Fl_Widget *, void *c)
|
|
{
|
|
static_cast<MixerWidget *>(c)->selection();
|
|
}
|
|
int MixerWidget::handle(int e)
|
|
{
|
|
if (e == FL_KEYDOWN && !(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META))) {
|
|
int key = Fl::event_key();
|
|
xtc_track_info t{};
|
|
if (xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &t) == XTC_OK) {
|
|
if (key == 'm' || key == 's') {
|
|
mix(t.gain, t.pan, key == 'm' ? !t.mute : t.mute, key == 's' ? !t.solo : t.solo);
|
|
return 1;
|
|
}
|
|
if (key == FL_Left || key == FL_Right) {
|
|
float d = key == FL_Left ? -0.05f : 0.05f;
|
|
bool pan = (Fl::event_state() & FL_SHIFT) != 0;
|
|
mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f),
|
|
pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, t.mute, t.solo);
|
|
return 1;
|
|
}
|
|
if (key == FL_Up || key == FL_Down) {
|
|
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();
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
return Fl_Group::handle(e);
|
|
}
|