ui: the node editor offers the de-noise node and persists its window

The form gains a `median samples` row, shown only for `DeNoise`; the corner row widens to
anything that reads `fc` (`math_op_uses_fc()`) while `q` stays band-pass-only, and the wiring
row keeps its place at the end so the form's order is unchanged for every existing op.

The session format gains a thirteenth field, `med_n`, after `y`. A twelve-field node -- a file
written before this node existed -- still loads, with `med_n` left at the struct's default,
which is what such a file meant; the reader's `return number(...) && ...` chain had to become a
guarded sequence so the thirteenth field could be read conditionally.

The math probe enables the channel it builds its graphs from, the same self-sufficiency rule
the noise and measurement probes already follow: `enable_all_probe` ends by switching one
signal off, and with `sine` gone the graphs landed on `square`, changing a documented line for
reasons unrelated to the math.

Readings: `[math-probe] 2 math channels on 'sine' and 'noise'`, `pane add: 'math3' sources
'sine'`, `graph edit: stored_slots 31000 -> 32000`, `dialog build: nodes=2, output after add=1`
and the rest of the suite unchanged; no `Gtk-CRITICAL`.
This commit is contained in:
Martino Ferrari committed 2026-10-13 15:50:58 +02:00
1 parent 1bde8809dd
commit 5c2c221777
5 files changed
+41 -7

No files matched your search

+14 -4
View File
@@ -366,7 +366,7 @@ std::string math_node_text(const MathNode& node) {
std::ostringstream o; std::ostringstream o;
o << kind << '|' << node.source << '|' << node.in_a << '|' << node.in_b << '|' << node.gain << '|' o << kind << '|' << node.source << '|' << node.in_a << '|' << node.in_b << '|' << node.gain << '|'
<< node.offset << '|' << node.fc << '|' << node.q << '|' << node.lo << '|' << node.hi << '|' << node.offset << '|' << node.fc << '|' << node.q << '|' << node.lo << '|' << node.hi << '|'
<< node.x << '|' << node.y; << node.x << '|' << node.y << '|' << node.med_n;
return o.str(); return o.str();
} }
@@ -382,7 +382,9 @@ bool math_node_from_text(const std::string& text, MathNode& out) {
} }
} }
f.push_back(cur); f.push_back(cur);
if (f.size() != 12u) return false; // Twelve fields is a session file written before the de-noise node existed; its `med_n` stays at the
// struct's default, which is what that file meant.
if (f.size() != 12u && f.size() != 13u) return false;
const auto number = [&](std::size_t i, double& v) { const auto number = [&](std::size_t i, double& v) {
try { try {
v = std::stod(f[i]); v = std::stod(f[i]);
@@ -402,8 +404,16 @@ bool math_node_from_text(const std::string& text, MathNode& out) {
out.source = f[1]; out.source = f[1];
out.in_a = static_cast<int>(in_a); out.in_a = static_cast<int>(in_a);
out.in_b = static_cast<int>(in_b); out.in_b = static_cast<int>(in_b);
return number(4, out.gain) && number(5, out.offset) && number(6, out.fc) && number(7, out.q) && if (!(number(4, out.gain) && number(5, out.offset) && number(6, out.fc) && number(7, out.q) &&
number(8, out.lo) && number(9, out.hi) && number(10, out.x) && number(11, out.y); number(8, out.lo) && number(9, out.hi) && number(10, out.x) && number(11, out.y))) {
return false;
}
if (f.size() == 13u) {
double med = 0.0;
if (!number(12, med)) return false;
out.med_n = static_cast<int>(med);
}
return true;
} }
bool load_layout(const std::string& path, LayoutState& out) { bool load_layout(const std::string& path, LayoutState& out) {
+9 -1
View File
@@ -2140,6 +2140,14 @@ void open_math_dialog(App& app, int edit) {
gtk_box_append(GTK_BOX(form), clamp_rows); gtk_box_append(GTK_BOX(form), clamp_rows);
} }
// Odd, 3..9. The engine rounds up to odd anyway, but a step of 2 means the widget never offers an even
// one.
d->med = gtk_spin_button_new_with_range(3.0, 9.0, 2.0);
gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->med), 0);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->med), 5.0);
g_signal_connect(d->med, "value-changed", G_CALLBACK(on_math_param_changed), d);
gtk_box_append(GTK_BOX(form), math_row("median samples", d->med, &d->rows[6]));
d->inputs = gtk_label_new(""); d->inputs = gtk_label_new("");
gtk_label_set_wrap(GTK_LABEL(d->inputs), true); gtk_label_set_wrap(GTK_LABEL(d->inputs), true);
gtk_widget_add_css_class(d->inputs, "dim-label"); gtk_widget_add_css_class(d->inputs, "dim-label");
@@ -2159,7 +2167,7 @@ void open_math_dialog(App& app, int edit) {
gtk_widget_set_visible(d->problem, FALSE); gtk_widget_set_visible(d->problem, FALSE);
gtk_box_append(GTK_BOX(form), d->problem); gtk_box_append(GTK_BOX(form), d->problem);
// A menu of the twelve ops rather than one button that always makes a gain: building anything // A menu of the thirteen ops rather than one button that always makes a gain: building anything
// else was add-then-change-the-dropdown, which is two steps for the common case. // else was add-then-change-the-dropdown, which is two steps for the common case.
GtkWidget* add = gtk_menu_button_new(); GtkWidget* add = gtk_menu_button_new();
gtk_menu_button_set_label(GTK_MENU_BUTTON(add), "add node"); gtk_menu_button_set_label(GTK_MENU_BUTTON(add), "add node");
+2 -1
View File
@@ -978,9 +978,10 @@ struct MathDialog {
GtkWidget* offset = nullptr; GtkWidget* offset = nullptr;
GtkWidget* fc = nullptr; GtkWidget* fc = nullptr;
GtkWidget* q = nullptr; GtkWidget* q = nullptr;
GtkWidget* med = nullptr; ///< MathNode::med_n
GtkWidget* lo = nullptr; GtkWidget* lo = nullptr;
GtkWidget* hi = nullptr; GtkWidget* hi = nullptr;
GtkWidget* rows[6] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; GtkWidget* rows[7] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
GtkWidget* inputs = nullptr; GtkWidget* inputs = nullptr;
/// The validation line and the button it governs. Both are set from `math_dialog_sync()` and from /// The validation line and the button it governs. Both are set from `math_dialog_sync()` and from
/// nowhere else, so what the line says and what the button allows cannot disagree. /// nowhere else, so what the line says and what the button allows cannot disagree.
+6 -1
View File
@@ -835,6 +835,7 @@ void on_math_param_changed(GtkWidget*, gpointer user) {
n.offset = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->offset)); n.offset = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->offset));
n.fc = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->fc)); n.fc = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->fc));
n.q = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->q)); n.q = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->q));
n.med_n = static_cast<int>(gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->med)));
n.lo = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->lo)); n.lo = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->lo));
n.hi = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->hi)); n.hi = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->hi));
if (d->source_drop != nullptr) { if (d->source_drop != nullptr) {
@@ -930,14 +931,18 @@ void math_dialog_sync_form(MathDialog* d) {
const bool source = op == MathOp::Source; const bool source = op == MathOp::Source;
gtk_widget_set_visible(d->rows[0], source); // the source channel gtk_widget_set_visible(d->rows[0], source); // the source channel
gtk_widget_set_visible(d->rows[1], gain); gtk_widget_set_visible(d->rows[1], gain);
gtk_widget_set_visible(d->rows[2], filter); // The corner row is shown for anything that reads `fc`, which is now the three biquads *and* the
// de-noise node; `q` stays a band-pass-only row.
gtk_widget_set_visible(d->rows[2], math_op_uses_fc(op));
gtk_widget_set_visible(d->rows[3], filter && op == MathOp::BandPass); gtk_widget_set_visible(d->rows[3], filter && op == MathOp::BandPass);
gtk_widget_set_visible(d->rows[4], clamp); gtk_widget_set_visible(d->rows[4], clamp);
gtk_widget_set_visible(d->rows[5], true); // the inputs, always: they say what is wired gtk_widget_set_visible(d->rows[5], true); // the inputs, always: they say what is wired
gtk_widget_set_visible(d->rows[6], math_op_uses_median(op));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->gain), n.gain); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->gain), n.gain);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->offset), n.offset); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->offset), n.offset);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->fc), n.fc); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->fc), n.fc);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->q), n.q); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->q), n.q);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->med), static_cast<double>(n.med_n));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->lo), n.lo); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->lo), n.lo);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->hi), n.hi); gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->hi), n.hi);
if (d->source_drop != nullptr) { if (d->source_drop != nullptr) {
+10
View File
@@ -281,6 +281,16 @@ gboolean eventzoom_probe(gpointer user) {
gboolean math_probe(gpointer user) { gboolean math_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
// The channel the graphs are built from is enabled *here*, for the reason the noise and measurement
// probes give: `enable_all_probe` finishes by switching one signal off, and with `sine` gone the graphs
// land on `square`, which turns the documented "2 math channels on 'sine' and 'noise'" into a different
// line for reasons that have nothing to do with the math.
{
const std::vector<SignalInfo>& signals = a->model->signals();
for (std::size_t i = 0; i < signals.size(); ++i) {
if (signals[i].name == "sine") a->model->set_signal_enabled(i, true);
}
}
std::string name; std::string name;
std::string late_name; std::string late_name;
{ {