package dsp import "fmt" // ValType is the data type of a Sample: a scalar float, a float array // (waveform), or — at graph-compile time, before a source's real type is // known — unknown. type ValType uint8 const ( ValUnknown ValType = iota ValScalar ValArray ) func (t ValType) String() string { switch t { case ValScalar: return "scalar" case ValArray: return "array" default: return "unknown" } } // Sample is a value flowing through the synthetic DSP graph: either a scalar // float64 or a float64 array (waveform). It is the array-aware counterpart of // the bare float64 the legacy scalar Node interface uses. type Sample struct { F float64 Arr []float64 IsArray bool } // Scalar wraps a float64 as a scalar Sample. func Scalar(f float64) Sample { return Sample{F: f} } // Array wraps a []float64 as an array Sample. func Array(a []float64) Sample { return Sample{Arr: a, IsArray: true} } // Type reports whether the sample is a scalar or an array. func (s Sample) Type() ValType { if s.IsArray { return ValArray } return ValScalar } // AsAny returns the value in the form datasource.Value.Data expects: a // []float64 for arrays, a float64 for scalars. func (s Sample) AsAny() any { if s.IsArray { return s.Arr } return s.F } // AsArray returns the sample's data as a slice: the array itself, or a // single-element slice for a scalar. Used by reductions that accept either. func (s Sample) AsArray() []float64 { if s.IsArray { return s.Arr } return []float64{s.F} } // ArrayNode is an optional extension of Node implemented by ops that operate // natively on Samples (reductions array→scalar, producers array→array, etc.). // eval prefers ProcessSample when a node implements it. type ArrayNode interface { Node ProcessSample(inputs []Sample, state map[string]any) (Sample, error) } // statelessElementwise lists scalar ops that are safe to broadcast element-wise // over array inputs: they hold no per-evaluation state, so running the legacy // Process once per array lane is well-defined. Stateful ops (moving_average, // rms, lowpass, derivative, integrate) and lua are excluded — a single shared // state map cannot be meaningfully split across lanes. var statelessElementwise = map[string]bool{ "gain": true, "offset": true, "add": true, "subtract": true, "multiply": true, "divide": true, "clamp": true, "threshold": true, "expr": true, } // StatelessElementwise reports whether a scalar op type may be broadcast over // array inputs via ApplyElementwise. func StatelessElementwise(nodeType string) bool { return statelessElementwise[nodeType] } // scalarInputs wraps a legacy float64 input slice as scalar Samples. func scalarInputs(in []float64) []Sample { out := make([]Sample, len(in)) for i, v := range in { out[i] = Scalar(v) } return out } // ApplyElementwise runs a stateless scalar Node over Sample inputs. If every // input is scalar it calls Process once and wraps the result. If any input is // an array it broadcasts: scalar inputs act as constants, all array inputs must // share a common length (else an error), and Process is invoked once per index. // // The node MUST be stateless (see StatelessElementwise) — a shared state map // cannot be split across array lanes. func ApplyElementwise(n Node, inputs []Sample, state map[string]any) (Sample, error) { // Determine the array length, if any input is an array. length := -1 for _, s := range inputs { if !s.IsArray { continue } if length == -1 { length = len(s.Arr) } else if len(s.Arr) != length { return Sample{}, fmt.Errorf("%s: array length mismatch (%d vs %d)", n.Type(), length, len(s.Arr)) } } if length == -1 { // All scalar — single legacy call. row := make([]float64, len(inputs)) for i, s := range inputs { row[i] = s.F } r, err := n.Process(row, state) if err != nil { return Sample{}, err } return Scalar(r), nil } out := make([]float64, length) row := make([]float64, len(inputs)) for i := 0; i < length; i++ { for j, s := range inputs { if s.IsArray { row[j] = s.Arr[i] } else { row[j] = s.F } } r, err := n.Process(row, state) if err != nil { return Sample{}, err } out[i] = r } return Array(out), nil }