Files
uopi/internal/datasource/synthetic/dsp_bridge.go
T
Martino Ferrari f7f297c3df Add synthetic array (waveform) DSP support + UX improvements
Adds full array/waveform support through the synthetic DSP engine: a
dsp.Sample value model (scalar or []float64), array ops (index, slice,
sum, mean, min, max, length, fft) with an in-tree radix-2 FFT, and static
type propagation (OpOutputType) that the editor mirrors to colour wires by
data type and flag invalid wirings. Stateful filters and lua stay
scalar-only. Adds a waveform plot mode (x-vs-index trace).

Also: errored-node hover reasons, S/N add-signal/add-node HUD shortcuts in
the synthetic editor, and view-mode widgets that blend with the canvas
background (chrome kept in edit mode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-20 17:06:55 +02:00

157 lines
3.2 KiB
Go

package synthetic
import (
"fmt"
"github.com/uopi/uopi/internal/dsp"
)
// floatParam extracts a float64 from params; returns 0 if missing or wrong type.
func floatParam(params map[string]any, key string) float64 {
if params == nil {
return 0
}
v, ok := params[key]
if !ok {
return 0
}
f, ok := v.(float64)
if !ok {
return 0
}
return f
}
// stringParam extracts a string from params; returns "" if missing or wrong type.
func stringParam(params map[string]any, key string) string {
if params == nil {
return ""
}
v, ok := params[key]
if !ok {
return ""
}
s, ok := v.(string)
if !ok {
return ""
}
return s
}
// stringSliceParam extracts a []string from params; JSON arrays decode to []any,
// so each element is coerced via its string value. Returns nil if missing.
func stringSliceParam(params map[string]any, key string) []string {
if params == nil {
return nil
}
v, ok := params[key]
if !ok {
return nil
}
arr, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, e := range arr {
if s, ok := e.(string); ok && s != "" {
out = append(out, s)
}
}
return out
}
// buildNode converts a single NodeDef (from JSON) into a dsp.Node ready for
// execution. JSON numbers are float64, so all numeric params are handled as
// float64 regardless of the final type needed.
func buildNode(d NodeDef) (dsp.Node, error) {
p := d.Params
switch d.Type {
case "gain":
return &dsp.GainNode{Gain: floatParam(p, "gain")}, nil
case "offset":
return &dsp.OffsetNode{Offset: floatParam(p, "offset")}, nil
case "add":
return &dsp.AddNode{}, nil
case "subtract":
return &dsp.SubtractNode{}, nil
case "multiply":
return &dsp.MultiplyNode{}, nil
case "divide":
return &dsp.DivideNode{}, nil
case "moving_average":
return &dsp.MovingAverageNode{Window: max(1, int(floatParam(p, "window")))}, nil
case "rms":
return &dsp.RMSNode{Window: max(1, int(floatParam(p, "window")))}, nil
case "derivative":
return &dsp.DerivativeNode{}, nil
case "integrate":
return &dsp.IntegrateNode{}, nil
case "clamp":
return &dsp.ClampNode{
Min: floatParam(p, "min"),
Max: floatParam(p, "max"),
}, nil
case "threshold":
return &dsp.ThresholdNode{
Threshold: floatParam(p, "threshold"),
High: floatParam(p, "high"),
Low: floatParam(p, "low"),
}, nil
case "expr":
return &dsp.ExprNode{Expr: stringParam(p, "expr"), Vars: stringSliceParam(p, "vars")}, nil
case "lowpass":
order := int(floatParam(p, "order"))
if order < 1 {
order = 1
}
return &dsp.LowPassNode{
Freq: floatParam(p, "freq"),
Order: order,
}, nil
case "lua":
return &dsp.LuaNode{Script: stringParam(p, "script"), Vars: stringSliceParam(p, "vars")}, nil
case "index":
return &dsp.IndexNode{I: int(floatParam(p, "i"))}, nil
case "slice":
return &dsp.SliceNode{Start: int(floatParam(p, "start")), End: int(floatParam(p, "end"))}, nil
case "sum":
return &dsp.SumNode{}, nil
case "mean":
return &dsp.MeanNode{}, nil
case "min":
return &dsp.MinNode{}, nil
case "max":
return &dsp.MaxNode{}, nil
case "length":
return &dsp.LengthNode{}, nil
case "fft":
return &dsp.FFTNode{}, nil
default:
return nil, fmt.Errorf("unknown node type %q", d.Type)
}
}