From 0dca76375928422d2c2aedae4adb521577f1e997 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 21 Jan 2026 18:33:06 +0100 Subject: [PATCH 1/9] use mutexed map for synth modules --- module/envelope.go | 4 +- module/envelope_test.go | 42 ++++++++----- module/filter.go | 8 +-- module/gate.go | 6 +- module/gate_test.go | 24 ++++--- module/mixer.go | 15 ++--- module/mixer_test.go | 96 +++++++++++++++------------- module/module.go | 47 ++++++++++++-- module/oscillator.go | 6 +- module/oscillator_test.go | 28 +++++---- module/pan.go | 6 +- module/pan_test.go | 30 +++++---- module/sampler.go | 6 +- module/sampler_test.go | 56 +++++++++-------- module/sequencer.go | 4 +- module/sequencer_test.go | 23 ++++--- module/wavetable.go | 6 +- module/wavetable_test.go | 24 ++++--- synth/synth.go | 128 +++++++++++++++++++++----------------- synth/synth_test.go | 86 +++++++++---------------- 20 files changed, 359 insertions(+), 286 deletions(-) diff --git a/module/envelope.go b/module/envelope.go index b01afdc..af9227e 100644 --- a/module/envelope.go +++ b/module/envelope.go @@ -98,8 +98,8 @@ func (e *Envelope) Update(new *Envelope) { e.initializeFaders() } -func (e *Envelope) Step(t float64, modules ModuleMap) { - gateValue := getMono(modules[e.Gate]) +func (e *Envelope) Step(t float64, modules *ModuleMap) { + gateValue := getMono(modules, e.Gate) switch { case e.gateValue <= 0 && gateValue > 0: diff --git a/module/envelope_test.go b/module/envelope_test.go index 01cd02e..b7b3b54 100644 --- a/module/envelope_test.go +++ b/module/envelope_test.go @@ -233,7 +233,7 @@ func TestEnvelope_Step(t *testing.T) { name string e *Envelope t float64 - modules ModuleMap + modules *ModuleMap want float64 wantTriggeredAt float64 wantReleasedAt float64 @@ -250,10 +250,12 @@ func TestEnvelope_Step(t *testing.T) { Level: 0.5, }, t: 2, - modules: ModuleMap{ - "gate": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, @@ -277,10 +279,12 @@ func TestEnvelope_Step(t *testing.T) { gateValue: 1, }, t: 5, - modules: ModuleMap{ - "gate": &Module{ - current: Output{ - Mono: -1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: -1, + }, }, }, }, @@ -303,10 +307,12 @@ func TestEnvelope_Step(t *testing.T) { level: 0.25, }, t: 8, - modules: ModuleMap{ - "gate": &Module{ - current: Output{ - Mono: -1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: -1, + }, }, }, }, @@ -329,10 +335,12 @@ func TestEnvelope_Step(t *testing.T) { Level: 0.75, }, t: 8, - modules: ModuleMap{ - "gate": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, diff --git a/module/filter.go b/module/filter.go index 84ebce1..c3c85a7 100644 --- a/module/filter.go +++ b/module/filter.go @@ -110,15 +110,15 @@ func (f *Filter) Update(new *Filter) { f.initializeFaders() } -func (f *Filter) Step(modules ModuleMap) { +func (f *Filter) Step(modules *ModuleMap) { freq := f.Freq if f.CV != "" { - freq = cv(freqRange, getMono(modules[f.CV])) + freq = cv(freqRange, getMono(modules, f.CV)) } - freq = modulate(freq, freqRange, getMono(modules[f.Mod])) + freq = modulate(freq, freqRange, getMono(modules, f.Mod)) f.calculateCoeffs(freq) - x := getMono(modules[f.In]) + x := getMono(modules, f.In) y := calc.Limit(f.tap(x, freq), outputRange) f.current = Output{ diff --git a/module/gate.go b/module/gate.go index 98924dd..0984faa 100644 --- a/module/gate.go +++ b/module/gate.go @@ -72,7 +72,7 @@ func (g *Gate) Update(new *Gate) { } } -func (g *Gate) Step(modules ModuleMap) { +func (g *Gate) Step(modules *ModuleMap) { if len(g.Signal) < 1 { return } @@ -86,10 +86,10 @@ func (g *Gate) Step(modules ModuleMap) { bpm := g.BPM if g.CV != "" { - bpm = cv(bpmRange, getMono(modules[g.CV])) + bpm = cv(bpmRange, getMono(modules, g.CV)) } - bpm = modulate(bpm, bpmRange, getMono(modules[g.Mod])) + bpm = modulate(bpm, bpmRange, getMono(modules, g.Mod)) spb := samplesPerBeat(g.sampleRate, bpm) if spb == 0 { return diff --git a/module/gate_test.go b/module/gate_test.go index 789e585..ba66172 100644 --- a/module/gate_test.go +++ b/module/gate_test.go @@ -39,7 +39,7 @@ func TestGate_Step(t *testing.T) { tests := []struct { name string g *Gate - modules ModuleMap + modules *ModuleMap want float64 wantIdx float64 }{ @@ -52,6 +52,7 @@ func TestGate_Step(t *testing.T) { sampleRate: sampleRate, idx: 0, }, + modules: &ModuleMap{}, want: 1, wantIdx: 0, }, @@ -63,6 +64,7 @@ func TestGate_Step(t *testing.T) { sampleRate: sampleRate, idx: 0, }, + modules: &ModuleMap{}, want: -1, wantIdx: 1 / sampleRate, }, @@ -75,10 +77,12 @@ func TestGate_Step(t *testing.T) { idx: 1, CV: "cv", }, - modules: ModuleMap{ - "cv": &Module{ - current: Output{ - Mono: calc.Transpose(120, bpmRange, outputRange), + modules: &ModuleMap{ + modules: map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: calc.Transpose(120, bpmRange, outputRange), + }, }, }, }, @@ -94,10 +98,12 @@ func TestGate_Step(t *testing.T) { idx: 2, Mod: "mod", }, - modules: ModuleMap{ - "mod": &Module{ - current: Output{ - Mono: -0.03, + modules: &ModuleMap{ + modules: map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: -0.03, + }, }, }, }, diff --git a/module/mixer.go b/module/mixer.go index 16691d5..9112e7c 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -9,9 +9,10 @@ import ( type ( Mixer struct { Module - Gain float64 `yaml:"gain"` - CV string `yaml:"cv"` - Mod string `yaml:"mod"` + Gain float64 `yaml:"gain"` + CV string `yaml:"cv"` + Mod string `yaml:"mod"` + // TODO: make `In` a mutexed map, too In map[string]float64 `yaml:"in"` Fade float64 `yaml:"fade"` @@ -71,13 +72,13 @@ func (m *Mixer) Update(new *Mixer) { m.updateGains(new) } -func (m *Mixer) Step(modules ModuleMap) { +func (m *Mixer) Step(modules *ModuleMap) { var ( left, right, mono float64 ) for name, gain := range m.In { - if mod, ok := modules[name]; ok { + if mod, ok := modules.Get(name); ok { left += mod.Current().Left * gain right += mod.Current().Right * gain mono += mod.Current().Mono * gain @@ -86,9 +87,9 @@ func (m *Mixer) Step(modules ModuleMap) { gain := m.Gain if m.CV != "" { - gain = cv(gainRange, getMono(modules[m.CV])) + gain = cv(gainRange, getMono(modules, m.CV)) } - gain = modulate(gain, gainRange, getMono(modules[m.Mod])) + gain = modulate(gain, gainRange, getMono(modules, m.Mod)) left = calc.Limit(left*gain, outputRange) right = calc.Limit(right*gain, outputRange) diff --git a/module/mixer_test.go b/module/mixer_test.go index 4fafbcc..16a6535 100644 --- a/module/mixer_test.go +++ b/module/mixer_test.go @@ -10,7 +10,7 @@ func TestMixer_Step(t *testing.T) { tests := []struct { name string m *Mixer - modules ModuleMap + modules *ModuleMap want Output }{ { @@ -20,8 +20,10 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{}, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Module{}, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{}, + }, }, want: Output{}, }, @@ -34,9 +36,11 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Oscillator{ - Module: Module{}, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Oscillator{ + Module: Module{}, + }, }, }, want: Output{}, @@ -50,12 +54,14 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, + }, }, }, }, @@ -70,12 +76,14 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, + }, }, }, }, @@ -95,19 +103,21 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, + }, }, - }, - "lfo": &Module{ - current: Output{ - Mono: 0.5, - Left: 0.25, - Right: 0.25, + "lfo": &Module{ + current: Output{ + Mono: 0.5, + Left: 0.25, + Right: 0.25, + }, }, }, }, @@ -127,19 +137,21 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, + }, }, - }, - "cv": &Module{ - current: Output{ - Mono: 0.5, - Left: 0.25, - Right: 0.25, + "cv": &Module{ + current: Output{ + Mono: 0.5, + Left: 0.25, + Right: 0.25, + }, }, }, }, diff --git a/module/module.go b/module/module.go index 41c0bda..40b5c66 100644 --- a/module/module.go +++ b/module/module.go @@ -1,13 +1,20 @@ package module -import "github.com/iljarotar/synth/calc" +import ( + "sync" + + "github.com/iljarotar/synth/calc" +) type ( IModule interface { Current() Output } - ModuleMap map[string]IModule + ModuleMap struct { + mu sync.Mutex + modules map[string]IModule + } Module struct { current Output @@ -61,6 +68,37 @@ var ( } ) +func NewModuleMap() *ModuleMap { + return &ModuleMap{ + modules: map[string]IModule{}, + } +} + +func (m *ModuleMap) Get(name string) (IModule, bool) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + mod, found := m.modules[name] + return mod, found +} + +func (m *ModuleMap) Set(name string, mod IModule) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + m.modules[name] = mod +} + +func (m *ModuleMap) Delete(name string) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + delete(m.modules, name) +} + func (m *Module) Current() Output { return m.current } @@ -81,8 +119,9 @@ func cv(rng calc.Range, val float64) float64 { return calc.Transpose(val, outputRange, rng) } -func getMono(mod IModule) float64 { - if mod == nil { +func getMono(modules *ModuleMap, name string) float64 { + mod, found := modules.Get(name) + if !found || mod == nil { return 0 } return mod.Current().Mono diff --git a/module/oscillator.go b/module/oscillator.go index 0741665..fa82388 100644 --- a/module/oscillator.go +++ b/module/oscillator.go @@ -93,15 +93,15 @@ func (o *Oscillator) Update(new *Oscillator) { o.initializeFaders() } -func (o *Oscillator) Step(modules ModuleMap) { +func (o *Oscillator) Step(modules *ModuleMap) { twoPi := 2 * math.Pi freq := o.Freq if o.CV != "" { - freq = cv(freqRange, getMono(modules[o.CV])) + freq = cv(freqRange, getMono(modules, o.CV)) } c := twoPi * o.Phase - mod := math.Pow(2, getMono(modules[o.Mod])) + mod := math.Pow(2, getMono(modules, o.Mod)) val := o.signal(o.arg + c) o.current = Output{ diff --git a/module/oscillator_test.go b/module/oscillator_test.go index 8c44db5..8135f92 100644 --- a/module/oscillator_test.go +++ b/module/oscillator_test.go @@ -13,14 +13,14 @@ func TestOscillator_Step(t *testing.T) { tests := []struct { name string - modules ModuleMap + modules *ModuleMap o *Oscillator want float64 wantArg float64 }{ { name: "all zero", - modules: ModuleMap{}, + modules: &ModuleMap{}, o: &Oscillator{ Freq: 200, signal: SineSignalFunc(), @@ -31,7 +31,7 @@ func TestOscillator_Step(t *testing.T) { }, { name: "after one second", - modules: ModuleMap{}, + modules: &ModuleMap{}, o: &Oscillator{ Freq: 200, signal: SineSignalFunc(), @@ -44,7 +44,7 @@ func TestOscillator_Step(t *testing.T) { }, { name: "phase shift", - modules: ModuleMap{}, + modules: &ModuleMap{}, o: &Oscillator{ Freq: 200, signal: SineSignalFunc(), @@ -57,10 +57,12 @@ func TestOscillator_Step(t *testing.T) { }, { name: "modulation", - modules: ModuleMap{ - "mod": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, @@ -77,10 +79,12 @@ func TestOscillator_Step(t *testing.T) { }, { name: "cv", - modules: ModuleMap{ - "cv": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, diff --git a/module/pan.go b/module/pan.go index 4fdcb92..c1364ae 100644 --- a/module/pan.go +++ b/module/pan.go @@ -54,10 +54,10 @@ func (p *Pan) Update(new *Pan) { } } -func (p *Pan) Step(modules ModuleMap) { - pan := modulate(p.Pan, panRange, getMono(modules[p.Mod])) +func (p *Pan) Step(modules *ModuleMap) { + pan := modulate(p.Pan, panRange, getMono(modules, p.Mod)) percent := calc.Percentage(pan, panRange) - in := getMono(modules[p.In]) + in := getMono(modules, p.In) p.current = Output{ Mono: in, diff --git a/module/pan_test.go b/module/pan_test.go index 7e235b2..fb59a60 100644 --- a/module/pan_test.go +++ b/module/pan_test.go @@ -10,7 +10,7 @@ func TestPan_Step(t *testing.T) { tests := []struct { name string p *Pan - modules ModuleMap + modules *ModuleMap want Output }{ { @@ -19,10 +19,12 @@ func TestPan_Step(t *testing.T) { Pan: 0.5, In: "in", }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, @@ -39,15 +41,17 @@ func TestPan_Step(t *testing.T) { Mod: "mod", In: "in", }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + }, }, - }, - "mod": &Module{ - current: Output{ - Mono: 0.25, + "mod": &Module{ + current: Output{ + Mono: 0.25, + }, }, }, }, diff --git a/module/sampler.go b/module/sampler.go index 6d91e5d..2963226 100644 --- a/module/sampler.go +++ b/module/sampler.go @@ -20,11 +20,11 @@ func (s *Sampler) Update(new *Sampler) { s.Trigger = new.Trigger } -func (s *Sampler) Step(modules ModuleMap) { - triggerValue := getMono(modules[s.Trigger]) +func (s *Sampler) Step(modules *ModuleMap) { + triggerValue := getMono(modules, s.Trigger) if triggerValue > 0 && s.triggerValue <= 0 { - val := getMono(modules[s.In]) + val := getMono(modules, s.In) s.current = Output{ Mono: val, diff --git a/module/sampler_test.go b/module/sampler_test.go index 8ae2c6e..3833c02 100644 --- a/module/sampler_test.go +++ b/module/sampler_test.go @@ -10,7 +10,7 @@ func TestSampler_Step(t *testing.T) { tests := []struct { name string s *Sampler - modules ModuleMap + modules *ModuleMap want float64 wantTrigger float64 }{ @@ -26,15 +26,17 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + }, }, - }, - "trigger": &Module{ - current: Output{ - Mono: 0, + "trigger": &Module{ + current: Output{ + Mono: 0, + }, }, }, }, @@ -53,15 +55,17 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: -1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + }, }, - }, - "trigger": &Module{ - current: Output{ - Mono: 1, + "trigger": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, @@ -80,15 +84,17 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: 1, }, - modules: ModuleMap{ - "in": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + }, }, - }, - "trigger": &Module{ - current: Output{ - Mono: 0.5, + "trigger": &Module{ + current: Output{ + Mono: 0.5, + }, }, }, }, diff --git a/module/sequencer.go b/module/sequencer.go index 8a48408..5017ad1 100644 --- a/module/sequencer.go +++ b/module/sequencer.go @@ -72,12 +72,12 @@ func (s *Sequencer) Update(new *Sequencer) { } } -func (s *Sequencer) Step(modules ModuleMap) { +func (s *Sequencer) Step(modules *ModuleMap) { if len(s.sequence) < 1 { return } - triggerValue := getMono(modules[s.Trigger]) + triggerValue := getMono(modules, s.Trigger) if triggerValue > 0 && s.triggerValue <= 0 { if s.Randomize { s.idx = rand.Intn(len(s.sequence)) diff --git a/module/sequencer_test.go b/module/sequencer_test.go index 181fbd5..99b9867 100644 --- a/module/sequencer_test.go +++ b/module/sequencer_test.go @@ -170,7 +170,7 @@ func TestSequencer_Step(t *testing.T) { tests := []struct { name string s *Sequencer - modules ModuleMap + modules *ModuleMap want float64 wantTrigger float64 }{ @@ -182,6 +182,7 @@ func TestSequencer_Step(t *testing.T) { idx: -1, triggerValue: 0, }, + modules: &ModuleMap{}, want: -1, wantTrigger: 0, }, @@ -194,10 +195,12 @@ func TestSequencer_Step(t *testing.T) { idx: 1, triggerValue: 0, }, - modules: ModuleMap{ - "trigger": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "trigger": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, @@ -213,10 +216,12 @@ func TestSequencer_Step(t *testing.T) { idx: 2, triggerValue: 0, }, - modules: ModuleMap{ - "trigger": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "trigger": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, diff --git a/module/wavetable.go b/module/wavetable.go index cef672f..e6039cd 100644 --- a/module/wavetable.go +++ b/module/wavetable.go @@ -67,7 +67,7 @@ func (w *Wavetable) Update(new *Wavetable) { } } -func (w *Wavetable) Step(modules ModuleMap) { +func (w *Wavetable) Step(modules *ModuleMap) { if len(w.Signal) < 1 { return } @@ -80,10 +80,10 @@ func (w *Wavetable) Step(modules ModuleMap) { freq := w.Freq if w.CV != "" { - freq = cv(freqRange, getMono(modules[w.CV])) + freq = cv(freqRange, getMono(modules, w.CV)) } - mod := math.Pow(2, getMono(modules[w.Mod])) + mod := math.Pow(2, getMono(modules, w.Mod)) w.idx += freq * mod * float64(len(w.Signal)) / w.sampleRate w.fade() diff --git a/module/wavetable_test.go b/module/wavetable_test.go index 791c749..4d2388f 100644 --- a/module/wavetable_test.go +++ b/module/wavetable_test.go @@ -37,7 +37,7 @@ func TestWavetable_Step(t *testing.T) { tests := []struct { name string w *Wavetable - modules ModuleMap + modules *ModuleMap want float64 wantIdx float64 }{ @@ -49,7 +49,7 @@ func TestWavetable_Step(t *testing.T) { sampleRate: sampleRate, idx: 44100.0 / 8, }, - modules: ModuleMap{}, + modules: &ModuleMap{}, want: 0, wantIdx: 44100.0/8 + 8/44100.0, }, @@ -62,10 +62,12 @@ func TestWavetable_Step(t *testing.T) { sampleRate: sampleRate, idx: 0, }, - modules: ModuleMap{ - "cv": &Module{ - current: Output{ - Mono: 0, + modules: &ModuleMap{ + modules: map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: 0, + }, }, }, }, @@ -81,10 +83,12 @@ func TestWavetable_Step(t *testing.T) { sampleRate: sampleRate, idx: 2.5, }, - modules: ModuleMap{ - "mod": &Module{ - current: Output{ - Mono: 1, + modules: &ModuleMap{ + modules: map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: 1, + }, }, }, }, diff --git a/synth/synth.go b/synth/synth.go index 64666d7..eb38b7a 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -38,7 +38,7 @@ type Synth struct { volumeStep float64 notifyFadeoutChan chan<- bool logger *log.Logger - modules module.ModuleMap + modules *module.ModuleMap envelopes []*module.Envelope filters []*module.Filter @@ -98,11 +98,15 @@ func (s *Synth) Update(from *Synth) error { } func (s *Synth) GetOutput() Output { + if s.modules == nil { + return Output{} + } + s.step() s.adjustVolume() out := Output{Time: s.Time} - if mod, ok := s.modules[s.Out]; ok { + if mod, ok := s.modules.Get(s.Out); ok { out.Left = mod.Current().Left * s.Volume out.Right = mod.Current().Right * s.Volume out.Mono = mod.Current().Mono * s.Volume @@ -225,67 +229,69 @@ func secondsToStep(seconds, delta, sampleRate float64) float64 { } func (s *Synth) makeModulesMap() { - s.modules = module.ModuleMap{} + if s.modules == nil { + s.modules = module.NewModuleMap() + } for name, e := range s.Envelopes { if e == nil { continue } - s.modules[name] = e + s.modules.Set(name, e) } for name, f := range s.Filters { if f == nil { continue } - s.modules[name] = f + s.modules.Set(name, f) } for name, g := range s.Gates { if g == nil { continue } - s.modules[name] = g + s.modules.Set(name, g) } for name, m := range s.Mixers { if m == nil { continue } - s.modules[name] = m + s.modules.Set(name, m) } for name, n := range s.Noises { if n == nil { continue } - s.modules[name] = n + s.modules.Set(name, n) } for name, osc := range s.Oscillators { if osc == nil { continue } - s.modules[name] = osc + s.modules.Set(name, osc) } for name, p := range s.Pans { if p == nil { continue } - s.modules[name] = p + s.modules.Set(name, p) } for name, smplr := range s.Samplers { if smplr == nil { continue } - s.modules[name] = smplr + s.modules.Set(name, smplr) } for name, seq := range s.Sequencers { if seq == nil { continue } - s.modules[name] = seq + s.modules.Set(name, seq) } for name, w := range s.Wavetables { if w == nil { continue } - s.modules[name] = w + s.modules.Set(name, w) } } @@ -303,10 +309,14 @@ func (s *Synth) flattenModules() { } func (s *Synth) deleteOldModules(new *Synth) { + if s.modules == nil { + s.modules = module.NewModuleMap() + } + for name, env := range s.Envelopes { if _, ok := new.Envelopes[name]; !ok { delete(s.Envelopes, name) - delete(s.modules, name) + s.modules.Delete(name) s.envelopes = slices.DeleteFunc(s.envelopes, func(e *module.Envelope) bool { return env == e }) @@ -315,7 +325,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, filter := range s.Filters { if _, ok := new.Filters[name]; !ok { delete(s.Filters, name) - delete(s.modules, name) + s.modules.Delete(name) s.filters = slices.DeleteFunc(s.filters, func(f *module.Filter) bool { return filter == f }) @@ -324,7 +334,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, gate := range s.Gates { if _, ok := new.Gates[name]; !ok { delete(s.Gates, name) - delete(s.modules, name) + s.modules.Delete(name) s.gates = slices.DeleteFunc(s.gates, func(g *module.Gate) bool { return gate == g }) @@ -333,7 +343,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, mixer := range s.Mixers { if _, ok := new.Mixers[name]; !ok { delete(s.Mixers, name) - delete(s.modules, name) + s.modules.Delete(name) s.mixers = slices.DeleteFunc(s.mixers, func(m *module.Mixer) bool { return mixer == m }) @@ -342,7 +352,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, noise := range s.Noises { if _, ok := new.Noises[name]; !ok { delete(s.Noises, name) - delete(s.modules, name) + s.modules.Delete(name) s.noises = slices.DeleteFunc(s.noises, func(n *module.Noise) bool { return noise == n }) @@ -351,7 +361,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, osc := range s.Oscillators { if _, ok := new.Oscillators[name]; !ok { delete(s.Oscillators, name) - delete(s.modules, name) + s.modules.Delete(name) s.oscillators = slices.DeleteFunc(s.oscillators, func(o *module.Oscillator) bool { return osc == o }) @@ -360,7 +370,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, pan := range s.Pans { if _, ok := new.Pans[name]; !ok { delete(s.Pans, name) - delete(s.modules, name) + s.modules.Delete(name) s.pans = slices.DeleteFunc(s.pans, func(p *module.Pan) bool { return pan == p }) @@ -369,7 +379,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, sampler := range s.Samplers { if _, ok := new.Samplers[name]; !ok { delete(s.Samplers, name) - delete(s.modules, name) + s.modules.Delete(name) s.samplers = slices.DeleteFunc(s.samplers, func(smplr *module.Sampler) bool { return sampler == smplr }) @@ -378,7 +388,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, seq := range s.Sequencers { if _, ok := new.Sequencers[name]; !ok { delete(s.Sequencers, name) - delete(s.modules, name) + s.modules.Delete(name) s.sequencers = slices.DeleteFunc(s.sequencers, func(sq *module.Sequencer) bool { return seq == sq }) @@ -387,7 +397,7 @@ func (s *Synth) deleteOldModules(new *Synth) { for name, wt := range s.Wavetables { if _, ok := new.Wavetables[name]; !ok { delete(s.Wavetables, name) - delete(s.modules, name) + s.modules.Delete(name) s.wavetables = slices.DeleteFunc(s.wavetables, func(w *module.Wavetable) bool { return wt == w }) @@ -396,74 +406,78 @@ func (s *Synth) deleteOldModules(new *Synth) { } func (s *Synth) addNewModules(new *Synth) { - for name, env := range new.Envelopes { + if s.modules == nil { + s.modules = module.NewModuleMap() + } + + for name, e := range new.Envelopes { if _, ok := s.Envelopes[name]; !ok { - s.Envelopes[name] = env - s.envelopes = append(s.envelopes, env) - s.modules[name] = env + s.Envelopes[name] = e + s.envelopes = append(s.envelopes, e) + s.modules.Set(name, e) } } - for name, filter := range new.Filters { + for name, f := range new.Filters { if _, ok := s.Filters[name]; !ok { - s.Filters[name] = filter - s.filters = append(s.filters, filter) - s.modules[name] = filter + s.Filters[name] = f + s.filters = append(s.filters, f) + s.modules.Set(name, f) } } - for name, gate := range new.Gates { + for name, g := range new.Gates { if _, ok := s.Gates[name]; !ok { - s.Gates[name] = gate - s.gates = append(s.gates, gate) - s.modules[name] = gate + s.Gates[name] = g + s.gates = append(s.gates, g) + s.modules.Set(name, g) } } - for name, mixer := range new.Mixers { + for name, m := range new.Mixers { if _, ok := s.Mixers[name]; !ok { - s.Mixers[name] = mixer - s.mixers = append(s.mixers, mixer) - s.modules[name] = mixer + s.Mixers[name] = m + s.mixers = append(s.mixers, m) + s.modules.Set(name, m) } } - for name, noise := range new.Noises { + for name, n := range new.Noises { if _, ok := s.Noises[name]; !ok { - s.Noises[name] = noise - s.noises = append(s.noises, noise) - s.modules[name] = noise + s.Noises[name] = n + s.noises = append(s.noises, n) + s.modules.Set(name, n) } } for name, osc := range new.Oscillators { if _, ok := s.Oscillators[name]; !ok { s.Oscillators[name] = osc s.oscillators = append(s.oscillators, osc) - s.modules[name] = osc + s.modules.Set(name, osc) } } - for name, pan := range new.Pans { + for name, p := range new.Pans { if _, ok := s.Pans[name]; !ok { - s.Pans[name] = pan - s.pans = append(s.pans, pan) - s.modules[name] = pan + s.Pans[name] = p + s.pans = append(s.pans, p) + s.modules.Set(name, p) } } - for name, sampler := range new.Samplers { + for name, smplr := range new.Samplers { if _, ok := s.Samplers[name]; !ok { - s.Samplers[name] = sampler - s.samplers = append(s.samplers, sampler) - s.modules[name] = sampler + s.Samplers[name] = smplr + s.samplers = append(s.samplers, smplr) + s.modules.Set(name, smplr) } } for name, seq := range new.Sequencers { if _, ok := s.Sequencers[name]; !ok { s.Sequencers[name] = seq s.sequencers = append(s.sequencers, seq) - s.modules[name] = seq + s.modules.Set(name, seq) } } - for name, wt := range new.Wavetables { + for name, w := range new.Wavetables { if _, ok := s.Wavetables[name]; !ok { - s.Wavetables[name] = wt - s.wavetables = append(s.wavetables, wt) - s.modules[name] = wt + s.Wavetables[name] = w + s.wavetables = append(s.wavetables, w) + s.modules.Set(name, w) } } } diff --git a/synth/synth_test.go b/synth/synth_test.go index 55bcb78..8964a08 100644 --- a/synth/synth_test.go +++ b/synth/synth_test.go @@ -71,10 +71,12 @@ func TestSynth_Update(t *testing.T) { w2 = &module.Wavetable{} ) tests := []struct { - name string - s *Synth - new *Synth - want *Synth + name string + s *Synth + new *Synth + want *Synth + currentModules map[string]module.IModule + wantModules map[string]module.IModule }{ { name: "update all modules", @@ -125,38 +127,16 @@ func TestSynth_Update(t *testing.T) { VolumeMemory: 1, sampleRate: 44100, volumeStep: 0.1, - modules: module.ModuleMap{ - "env1": env1, - "env2": env2, - "f1": f1, - "f2": f2, - "g1": g1, - "g2": g2, - "m1": m1, - "m2": m2, - "n1": n1, - "n2": n2, - "o1": o1, - "o2": o2, - "p1": p1, - "p2": p2, - "s1": s1, - "s2": s2, - "seq1": seq1, - "seq2": seq2, - "w1": w1, - "w2": w2, - }, - envelopes: []*module.Envelope{env1, env2}, - filters: []*module.Filter{f1, f2}, - gates: []*module.Gate{g1, g2}, - mixers: []*module.Mixer{m1, m2}, - noises: []*module.Noise{n1, n2}, - oscillators: []*module.Oscillator{o1, o2}, - pans: []*module.Pan{p1, p2}, - samplers: []*module.Sampler{s1, s2}, - sequencers: []*module.Sequencer{seq1, seq2}, - wavetables: []*module.Wavetable{w1, w2}, + envelopes: []*module.Envelope{env1, env2}, + filters: []*module.Filter{f1, f2}, + gates: []*module.Gate{g1, g2}, + mixers: []*module.Mixer{m1, m2}, + noises: []*module.Noise{n1, n2}, + oscillators: []*module.Oscillator{o1, o2}, + pans: []*module.Pan{p1, p2}, + samplers: []*module.Sampler{s1, s2}, + sequencers: []*module.Sequencer{seq1, seq2}, + wavetables: []*module.Wavetable{w1, w2}, }, new: &Synth{ Envelopes: module.EnvelopeMap{ @@ -314,28 +294,17 @@ func TestSynth_Update(t *testing.T) { VolumeMemory: 1, sampleRate: 44100, volumeStep: 0.1, - modules: module.ModuleMap{ - "env2": env2, - "f2": f2, - "g2": g2, - "m2": m2, - "n2": n2, - "o2": o2, - "p2": p2, - "s2": s2, - "seq2": seq2, - "w2": w2, - }, - envelopes: []*module.Envelope{env2}, - filters: []*module.Filter{f2}, - gates: []*module.Gate{g2}, - mixers: []*module.Mixer{m2}, - noises: []*module.Noise{n2}, - oscillators: []*module.Oscillator{o2}, - pans: []*module.Pan{p2}, - samplers: []*module.Sampler{s2}, - sequencers: []*module.Sequencer{seq2}, - wavetables: []*module.Wavetable{w2}, + modules: &module.ModuleMap{}, + envelopes: []*module.Envelope{env2}, + filters: []*module.Filter{f2}, + gates: []*module.Gate{g2}, + mixers: []*module.Mixer{m2}, + noises: []*module.Noise{n2}, + oscillators: []*module.Oscillator{o2}, + pans: []*module.Pan{p2}, + samplers: []*module.Sampler{s2}, + sequencers: []*module.Sequencer{seq2}, + wavetables: []*module.Wavetable{w2}, }, }, } @@ -357,6 +326,7 @@ func TestSynth_Update(t *testing.T) { module.Wavetable{}, ), cmp.AllowUnexported(Synth{}), + cmpopts.IgnoreUnexported(module.ModuleMap{}), ); diff != "" { t.Errorf("Synth.Update() diff = %s", diff) } From 2ee63ff527048f2a0f60209cac47bbe13457f1c7 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Thu, 22 Jan 2026 20:51:44 +0100 Subject: [PATCH 2/9] make concurrency safe map generic --- concurrency/map.go | 41 +++++++++++++++ module/envelope_test.go | 48 +++++++---------- module/gate_test.go | 24 ++++----- module/mixer_test.go | 106 +++++++++++++++++--------------------- module/module.go | 39 ++------------ module/oscillator_test.go | 24 ++++----- module/pan_test.go | 32 +++++------- module/sampler_test.go | 60 ++++++++++----------- module/sequencer_test.go | 24 ++++----- module/wavetable_test.go | 24 ++++----- synth/synth.go | 6 +-- synth/synth_test.go | 80 +++++++++++++++++++--------- 12 files changed, 253 insertions(+), 255 deletions(-) create mode 100644 concurrency/map.go diff --git a/concurrency/map.go b/concurrency/map.go new file mode 100644 index 0000000..2eb871b --- /dev/null +++ b/concurrency/map.go @@ -0,0 +1,41 @@ +package concurrency + +import "sync" + +type ( + SyncMap[T comparable, E any] struct { + mu sync.Mutex + m map[T]E + } +) + +func NewSyncMap[T comparable, E any](m map[T]E) *SyncMap[T, E] { + return &SyncMap[T, E]{ + m: m, + } +} + +func (m *SyncMap[T, E]) Get(idx T) (E, bool) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + e, found := m.m[idx] + return e, found +} + +func (m *SyncMap[T, E]) Set(idx T, e E) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + m.m[idx] = e +} + +func (m *SyncMap[T, E]) Delete(idx T) { + m.mu.Lock() + defer func() { + m.mu.Unlock() + }() + delete(m.m, idx) +} diff --git a/module/envelope_test.go b/module/envelope_test.go index b7b3b54..497a0c2 100644 --- a/module/envelope_test.go +++ b/module/envelope_test.go @@ -250,15 +250,13 @@ func TestEnvelope_Step(t *testing.T) { Level: 0.5, }, t: 2, - modules: &ModuleMap{ - modules: map[string]IModule{ - "gate": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: -1, wantTriggeredAt: 2, wantGateValue: 1, @@ -279,15 +277,13 @@ func TestEnvelope_Step(t *testing.T) { gateValue: 1, }, t: 5, - modules: &ModuleMap{ - modules: map[string]IModule{ - "gate": &Module{ - current: Output{ - Mono: -1, - }, + modules: NewModuleMap(map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: -1, }, }, - }, + }), want: 0.5, wantTriggeredAt: 2, wantReleasedAt: 5, @@ -307,15 +303,13 @@ func TestEnvelope_Step(t *testing.T) { level: 0.25, }, t: 8, - modules: &ModuleMap{ - modules: map[string]IModule{ - "gate": &Module{ - current: Output{ - Mono: -1, - }, + modules: NewModuleMap(map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: -1, }, }, - }, + }), want: -1, wantTriggeredAt: 2, wantReleasedAt: 5, @@ -335,15 +329,13 @@ func TestEnvelope_Step(t *testing.T) { Level: 0.75, }, t: 8, - modules: &ModuleMap{ - modules: map[string]IModule{ - "gate": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "gate": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: 0.5, wantTriggeredAt: 2, wantReleasedAt: 0, diff --git a/module/gate_test.go b/module/gate_test.go index ba66172..c2dbfa4 100644 --- a/module/gate_test.go +++ b/module/gate_test.go @@ -77,15 +77,13 @@ func TestGate_Step(t *testing.T) { idx: 1, CV: "cv", }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "cv": &Module{ - current: Output{ - Mono: calc.Transpose(120, bpmRange, outputRange), - }, + modules: NewModuleMap(map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: calc.Transpose(120, bpmRange, outputRange), }, }, - }, + }), want: 1, wantIdx: 1 + 2/sampleRate, }, @@ -98,15 +96,13 @@ func TestGate_Step(t *testing.T) { idx: 2, Mod: "mod", }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "mod": &Module{ - current: Output{ - Mono: -0.03, - }, + modules: NewModuleMap(map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: -0.03, }, }, - }, + }), want: -1, wantIdx: 2 + 0.5/sampleRate, }, diff --git a/module/mixer_test.go b/module/mixer_test.go index 16a6535..f3fd45d 100644 --- a/module/mixer_test.go +++ b/module/mixer_test.go @@ -20,11 +20,9 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{}, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{}, - }, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{}, + }), want: Output{}, }, { @@ -36,13 +34,11 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Oscillator{ - Module: Module{}, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Oscillator{ + Module: Module{}, }, - }, + }), want: Output{}, }, { @@ -54,17 +50,15 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, }, }, - }, + }), want: Output{}, }, { @@ -76,17 +70,15 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, }, }, - }, + }), want: Output{ Mono: 1, Left: 0.5, @@ -103,24 +95,22 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, }, - "lfo": &Module{ - current: Output{ - Mono: 0.5, - Left: 0.25, - Right: 0.25, - }, + }, + "lfo": &Module{ + current: Output{ + Mono: 0.5, + Left: 0.25, + Right: 0.25, }, }, - }, + }), want: Output{ Mono: 1 * 0.75, Left: 0.5 * 0.75, @@ -137,24 +127,22 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - Left: 0.5, - Right: 0.5, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, + Left: 0.5, + Right: 0.5, }, - "cv": &Module{ - current: Output{ - Mono: 0.5, - Left: 0.25, - Right: 0.25, - }, + }, + "cv": &Module{ + current: Output{ + Mono: 0.5, + Left: 0.25, + Right: 0.25, }, }, - }, + }), want: Output{ Mono: 1 * 0.75, Left: 0.5 * 0.75, diff --git a/module/module.go b/module/module.go index 40b5c66..042e7d2 100644 --- a/module/module.go +++ b/module/module.go @@ -1,9 +1,8 @@ package module import ( - "sync" - "github.com/iljarotar/synth/calc" + "github.com/iljarotar/synth/concurrency" ) type ( @@ -11,10 +10,7 @@ type ( Current() Output } - ModuleMap struct { - mu sync.Mutex - modules map[string]IModule - } + ModuleMap = concurrency.SyncMap[string, IModule] Module struct { current Output @@ -68,35 +64,8 @@ var ( } ) -func NewModuleMap() *ModuleMap { - return &ModuleMap{ - modules: map[string]IModule{}, - } -} - -func (m *ModuleMap) Get(name string) (IModule, bool) { - m.mu.Lock() - defer func() { - m.mu.Unlock() - }() - mod, found := m.modules[name] - return mod, found -} - -func (m *ModuleMap) Set(name string, mod IModule) { - m.mu.Lock() - defer func() { - m.mu.Unlock() - }() - m.modules[name] = mod -} - -func (m *ModuleMap) Delete(name string) { - m.mu.Lock() - defer func() { - m.mu.Unlock() - }() - delete(m.modules, name) +func NewModuleMap(m map[string]IModule) *ModuleMap { + return concurrency.NewSyncMap(m) } func (m *Module) Current() Output { diff --git a/module/oscillator_test.go b/module/oscillator_test.go index 8135f92..c12ff09 100644 --- a/module/oscillator_test.go +++ b/module/oscillator_test.go @@ -57,15 +57,13 @@ func TestOscillator_Step(t *testing.T) { }, { name: "modulation", - modules: &ModuleMap{ - modules: map[string]IModule{ - "mod": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), o: &Oscillator{ Freq: 200, signal: SineSignalFunc(), @@ -79,15 +77,13 @@ func TestOscillator_Step(t *testing.T) { }, { name: "cv", - modules: &ModuleMap{ - modules: map[string]IModule{ - "cv": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), o: &Oscillator{ Freq: 200, signal: SineSignalFunc(), diff --git a/module/pan_test.go b/module/pan_test.go index fb59a60..dc31c14 100644 --- a/module/pan_test.go +++ b/module/pan_test.go @@ -19,15 +19,13 @@ func TestPan_Step(t *testing.T) { Pan: 0.5, In: "in", }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: Output{ Mono: 1, Left: 0.25, @@ -41,20 +39,18 @@ func TestPan_Step(t *testing.T) { Mod: "mod", In: "in", }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, }, - "mod": &Module{ - current: Output{ - Mono: 0.25, - }, + }, + "mod": &Module{ + current: Output{ + Mono: 0.25, }, }, - }, + }), want: Output{ Mono: 1, Left: 0.25, diff --git a/module/sampler_test.go b/module/sampler_test.go index 3833c02..fa1d261 100644 --- a/module/sampler_test.go +++ b/module/sampler_test.go @@ -26,20 +26,18 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, }, - "trigger": &Module{ - current: Output{ - Mono: 0, - }, + }, + "trigger": &Module{ + current: Output{ + Mono: 0, }, }, - }, + }), want: 0.5, wantTrigger: 0, }, @@ -55,20 +53,18 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: -1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, }, - "trigger": &Module{ - current: Output{ - Mono: 1, - }, + }, + "trigger": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: 1, wantTrigger: 1, }, @@ -84,20 +80,18 @@ func TestSampler_Step(t *testing.T) { Trigger: "trigger", triggerValue: 1, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "in": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "in": &Module{ + current: Output{ + Mono: 1, }, - "trigger": &Module{ - current: Output{ - Mono: 0.5, - }, + }, + "trigger": &Module{ + current: Output{ + Mono: 0.5, }, }, - }, + }), want: 0.5, wantTrigger: 0.5, }, diff --git a/module/sequencer_test.go b/module/sequencer_test.go index 99b9867..b8f6a45 100644 --- a/module/sequencer_test.go +++ b/module/sequencer_test.go @@ -195,15 +195,13 @@ func TestSequencer_Step(t *testing.T) { idx: 1, triggerValue: 0, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "trigger": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "trigger": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: calc.Transpose(110, freqRange, outputRange), wantTrigger: 1, }, @@ -216,15 +214,13 @@ func TestSequencer_Step(t *testing.T) { idx: 2, triggerValue: 0, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "trigger": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "trigger": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: calc.Transpose(440, freqRange, outputRange), wantTrigger: 1, }, diff --git a/module/wavetable_test.go b/module/wavetable_test.go index 4d2388f..0e6a1b2 100644 --- a/module/wavetable_test.go +++ b/module/wavetable_test.go @@ -62,15 +62,13 @@ func TestWavetable_Step(t *testing.T) { sampleRate: sampleRate, idx: 0, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "cv": &Module{ - current: Output{ - Mono: 0, - }, + modules: NewModuleMap(map[string]IModule{ + "cv": &Module{ + current: Output{ + Mono: 0, }, }, - }, + }), want: 1, wantIdx: 4 * freqRange.Max / (2 * sampleRate), }, @@ -83,15 +81,13 @@ func TestWavetable_Step(t *testing.T) { sampleRate: sampleRate, idx: 2.5, }, - modules: &ModuleMap{ - modules: map[string]IModule{ - "mod": &Module{ - current: Output{ - Mono: 1, - }, + modules: NewModuleMap(map[string]IModule{ + "mod": &Module{ + current: Output{ + Mono: 1, }, }, - }, + }), want: -1, wantIdx: 2.5 + 16/sampleRate, }, diff --git a/synth/synth.go b/synth/synth.go index eb38b7a..16f6b7e 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -230,7 +230,7 @@ func secondsToStep(seconds, delta, sampleRate float64) float64 { func (s *Synth) makeModulesMap() { if s.modules == nil { - s.modules = module.NewModuleMap() + s.modules = module.NewModuleMap(map[string]module.IModule{}) } for name, e := range s.Envelopes { @@ -310,7 +310,7 @@ func (s *Synth) flattenModules() { func (s *Synth) deleteOldModules(new *Synth) { if s.modules == nil { - s.modules = module.NewModuleMap() + s.modules = module.NewModuleMap(map[string]module.IModule{}) } for name, env := range s.Envelopes { @@ -407,7 +407,7 @@ func (s *Synth) deleteOldModules(new *Synth) { func (s *Synth) addNewModules(new *Synth) { if s.modules == nil { - s.modules = module.NewModuleMap() + s.modules = module.NewModuleMap(map[string]module.IModule{}) } for name, e := range new.Envelopes { diff --git a/synth/synth_test.go b/synth/synth_test.go index 8964a08..b643f63 100644 --- a/synth/synth_test.go +++ b/synth/synth_test.go @@ -1,6 +1,7 @@ package synth import ( + "sync" "testing" "github.com/google/go-cmp/cmp" @@ -127,16 +128,38 @@ func TestSynth_Update(t *testing.T) { VolumeMemory: 1, sampleRate: 44100, volumeStep: 0.1, - envelopes: []*module.Envelope{env1, env2}, - filters: []*module.Filter{f1, f2}, - gates: []*module.Gate{g1, g2}, - mixers: []*module.Mixer{m1, m2}, - noises: []*module.Noise{n1, n2}, - oscillators: []*module.Oscillator{o1, o2}, - pans: []*module.Pan{p1, p2}, - samplers: []*module.Sampler{s1, s2}, - sequencers: []*module.Sequencer{seq1, seq2}, - wavetables: []*module.Wavetable{w1, w2}, + modules: module.NewModuleMap(map[string]module.IModule{ + "env1": env1, + "env2": env2, + "f1": f1, + "f2": f2, + "g1": g1, + "g2": g2, + "m1": m1, + "m2": m2, + "n1": n1, + "n2": n2, + "o1": o1, + "o2": o2, + "p1": p1, + "p2": p2, + "s1": s1, + "s2": s2, + "seq1": seq1, + "seq2": seq2, + "w1": w1, + "w2": w2, + }), + envelopes: []*module.Envelope{env1, env2}, + filters: []*module.Filter{f1, f2}, + gates: []*module.Gate{g1, g2}, + mixers: []*module.Mixer{m1, m2}, + noises: []*module.Noise{n1, n2}, + oscillators: []*module.Oscillator{o1, o2}, + pans: []*module.Pan{p1, p2}, + samplers: []*module.Sampler{s1, s2}, + sequencers: []*module.Sequencer{seq1, seq2}, + wavetables: []*module.Wavetable{w1, w2}, }, new: &Synth{ Envelopes: module.EnvelopeMap{ @@ -294,17 +317,28 @@ func TestSynth_Update(t *testing.T) { VolumeMemory: 1, sampleRate: 44100, volumeStep: 0.1, - modules: &module.ModuleMap{}, - envelopes: []*module.Envelope{env2}, - filters: []*module.Filter{f2}, - gates: []*module.Gate{g2}, - mixers: []*module.Mixer{m2}, - noises: []*module.Noise{n2}, - oscillators: []*module.Oscillator{o2}, - pans: []*module.Pan{p2}, - samplers: []*module.Sampler{s2}, - sequencers: []*module.Sequencer{seq2}, - wavetables: []*module.Wavetable{w2}, + modules: module.NewModuleMap(map[string]module.IModule{ + "env2": env2, + "f2": f2, + "g2": g2, + "m2": m2, + "n2": n2, + "o2": o2, + "p2": p2, + "s2": s2, + "seq2": seq2, + "w2": w2, + }), + envelopes: []*module.Envelope{env2}, + filters: []*module.Filter{f2}, + gates: []*module.Gate{g2}, + mixers: []*module.Mixer{m2}, + noises: []*module.Noise{n2}, + oscillators: []*module.Oscillator{o2}, + pans: []*module.Pan{p2}, + samplers: []*module.Sampler{s2}, + sequencers: []*module.Sequencer{seq2}, + wavetables: []*module.Wavetable{w2}, }, }, } @@ -325,8 +359,8 @@ func TestSynth_Update(t *testing.T) { module.Sequencer{}, module.Wavetable{}, ), - cmp.AllowUnexported(Synth{}), - cmpopts.IgnoreUnexported(module.ModuleMap{}), + cmp.AllowUnexported(Synth{}, module.ModuleMap{}), + cmpopts.IgnoreUnexported(sync.Mutex{}), ); diff != "" { t.Errorf("Synth.Update() diff = %s", diff) } From e818c0658f1ce092f8dafeefeafa995b9ee1331f Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Thu, 22 Jan 2026 21:15:10 +0100 Subject: [PATCH 3/9] mixer inputfaders syncmap --- concurrency/map.go | 15 ++++++--- module/mixer.go | 50 ++++++++++++++-------------- module/mixer_test.go | 78 +++++++++++++++++++++++++++++--------------- module/module.go | 4 +-- synth/synth.go | 2 +- 5 files changed, 92 insertions(+), 57 deletions(-) diff --git a/concurrency/map.go b/concurrency/map.go index 2eb871b..ba2aa04 100644 --- a/concurrency/map.go +++ b/concurrency/map.go @@ -1,6 +1,10 @@ package concurrency -import "sync" +import ( + "sync" + + "github.com/samber/lo" +) type ( SyncMap[T comparable, E any] struct { @@ -15,13 +19,12 @@ func NewSyncMap[T comparable, E any](m map[T]E) *SyncMap[T, E] { } } -func (m *SyncMap[T, E]) Get(idx T) (E, bool) { +func (m *SyncMap[T, E]) Get(idx T) E { m.mu.Lock() defer func() { m.mu.Unlock() }() - e, found := m.m[idx] - return e, found + return m.m[idx] } func (m *SyncMap[T, E]) Set(idx T, e E) { @@ -39,3 +42,7 @@ func (m *SyncMap[T, E]) Delete(idx T) { }() delete(m.m, idx) } + +func (m *SyncMap[T, E]) Keys() []T { + return lo.Keys(m.m) +} diff --git a/module/mixer.go b/module/mixer.go index 9112e7c..f708698 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -4,22 +4,23 @@ import ( "fmt" "github.com/iljarotar/synth/calc" + "github.com/iljarotar/synth/concurrency" ) type ( Mixer struct { Module - Gain float64 `yaml:"gain"` - CV string `yaml:"cv"` - Mod string `yaml:"mod"` - // TODO: make `In` a mutexed map, too + Gain float64 `yaml:"gain"` + CV string `yaml:"cv"` + Mod string `yaml:"mod"` In map[string]float64 `yaml:"in"` Fade float64 `yaml:"fade"` + in *concurrency.SyncMap[string, float64] sampleRate float64 gainFader *fader - inputFaders map[string]*fader + inputFaders *concurrency.SyncMap[string, *fader] } MixerMap map[string]*Mixer @@ -47,14 +48,14 @@ func (m *Mixer) initialize(sampleRate float64) error { target: m.Gain, } - m.inputFaders = map[string]*fader{} + m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) for mod, gain := range m.In { m.In[mod] = calc.Limit(gain, inputGainRange) - m.inputFaders[mod] = &fader{ + m.inputFaders.Set(mod, &fader{ current: gain, target: gain, - } + }) } m.initializeFaders() @@ -78,7 +79,7 @@ func (m *Mixer) Step(modules *ModuleMap) { ) for name, gain := range m.In { - if mod, ok := modules.Get(name); ok { + if mod := modules.Get(name); mod != nil { left += mod.Current().Left * gain right += mod.Current().Right * gain mono += mod.Current().Mono * gain @@ -109,15 +110,16 @@ func (m *Mixer) fade() { m.Gain = m.gainFader.fade() } - for mod, f := range m.inputFaders { + for _, name := range m.inputFaders.Keys() { + f := m.inputFaders.Get(name) if f == nil { continue } - m.In[mod] = f.fade() + m.In[name] = f.fade() if f.current == 0 && f.target == 0 { - delete(m.inputFaders, mod) - delete(m.In, mod) + m.inputFaders.Delete(name) + delete(m.In, name) } } } @@ -127,9 +129,9 @@ func (m *Mixer) initializeFaders() { m.gainFader.initialize(m.Fade, m.sampleRate) } - for _, fader := range m.inputFaders { - if fader != nil { - fader.initialize(m.Fade, m.sampleRate) + for _, name := range m.inputFaders.Keys() { + if f := m.inputFaders.Get(name); f != nil { + f.initialize(m.Fade, m.sampleRate) } } } @@ -140,33 +142,33 @@ func (m *Mixer) updateGains(new *Mixer) { } if m.inputFaders == nil { - m.inputFaders = map[string]*fader{} + m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) } if m.In == nil { m.In = map[string]float64{} } for mod, gain := range new.In { - f, ok := m.inputFaders[mod] - - if ok && f != nil { + f := m.inputFaders.Get(mod) + if f != nil { f.target = gain continue } - m.inputFaders[mod] = &fader{ + m.inputFaders.Set(mod, &fader{ current: 0, target: gain, - } + }) m.In[mod] = 0 } - for mod, f := range m.inputFaders { + for _, name := range m.inputFaders.Keys() { + f := m.inputFaders.Get(name) if f == nil { continue } - if _, ok := new.In[mod]; !ok { + if _, ok := new.In[name]; !ok { f.target = 0 } } diff --git a/module/mixer_test.go b/module/mixer_test.go index f3fd45d..a5f59f2 100644 --- a/module/mixer_test.go +++ b/module/mixer_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/iljarotar/synth/concurrency" ) func TestMixer_Step(t *testing.T) { @@ -16,9 +18,10 @@ func TestMixer_Step(t *testing.T) { { name: "no input", m: &Mixer{ - Gain: 0, - In: map[string]float64{}, - sampleRate: 1, + Gain: 0, + In: map[string]float64{}, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{}, @@ -32,7 +35,8 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{ "sine": 1, }, - sampleRate: 1, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Oscillator{ @@ -48,7 +52,8 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{ "in": 0, }, - sampleRate: 1, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -68,7 +73,8 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{ "in": 1, }, - sampleRate: 1, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -93,7 +99,8 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{ "in": 1, }, - sampleRate: 1, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -125,7 +132,8 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{ "in": 1, }, - sampleRate: 1, + sampleRate: 1, + inputFaders: concurrency.NewSyncMap(map[string]*fader{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -191,13 +199,13 @@ func TestMixer_Update(t *testing.T) { target: 1, step: 0.5, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 1, step: 0.5, }, - }, + }), }, new: nil, want: &Mixer{ @@ -219,13 +227,13 @@ func TestMixer_Update(t *testing.T) { target: 1, step: 0.5, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 1, step: 0.5, }, - }, + }), }, }, { @@ -250,7 +258,7 @@ func TestMixer_Update(t *testing.T) { target: 1, step: 0.5, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 1, @@ -261,7 +269,7 @@ func TestMixer_Update(t *testing.T) { target: 1, step: 0.5, }, - }, + }), }, new: &Mixer{ Gain: 0.5, @@ -294,7 +302,7 @@ func TestMixer_Update(t *testing.T) { target: 0.5, step: -0.25 / sampleRate, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 0.5, @@ -310,14 +318,23 @@ func TestMixer_Update(t *testing.T) { target: 0.5, step: 0.25 / sampleRate, }, - }, + }), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.m.Update(tt.new) - if diff := cmp.Diff(tt.want, tt.m, cmp.AllowUnexported(Module{}, Mixer{}, fader{})); diff != "" { + if diff := cmp.Diff( + tt.want, + tt.m, + cmp.AllowUnexported( + Module{}, + Mixer{}, + fader{}, + ), + cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}), + ); diff != "" { t.Errorf("Mixer.Update() diff = %s", diff) } }) @@ -343,7 +360,7 @@ func TestMixer_fade(t *testing.T) { target: 1, step: 0.5, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 1, @@ -354,7 +371,7 @@ func TestMixer_fade(t *testing.T) { target: 1, step: 0.5, }, - }, + }), }, want: &Mixer{ Gain: 1, @@ -366,7 +383,7 @@ func TestMixer_fade(t *testing.T) { current: 1, target: 1, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 1, @@ -375,7 +392,7 @@ func TestMixer_fade(t *testing.T) { current: 1, target: 1, }, - }, + }), }, }, { @@ -391,7 +408,7 @@ func TestMixer_fade(t *testing.T) { target: 0.5, step: -0.1, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 1, target: 0.5, @@ -402,7 +419,7 @@ func TestMixer_fade(t *testing.T) { target: 0, step: -0.2, }, - }, + }), }, want: &Mixer{ Gain: 0.9, @@ -414,20 +431,29 @@ func TestMixer_fade(t *testing.T) { target: 0.5, step: -0.1, }, - inputFaders: map[string]*fader{ + inputFaders: concurrency.NewSyncMap(map[string]*fader{ "in1": { current: 0.8, target: 0.5, step: -0.2, }, - }, + }), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.m.fade() - if diff := cmp.Diff(tt.want, tt.m, cmp.AllowUnexported(Module{}, Mixer{}, fader{})); diff != "" { + if diff := cmp.Diff( + tt.want, + tt.m, + cmp.AllowUnexported( + Module{}, + Mixer{}, + fader{}, + ), + cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}), + ); diff != "" { t.Errorf("Mixer.fade() diff = %s", diff) } }) diff --git a/module/module.go b/module/module.go index 042e7d2..d94d9e1 100644 --- a/module/module.go +++ b/module/module.go @@ -89,8 +89,8 @@ func cv(rng calc.Range, val float64) float64 { } func getMono(modules *ModuleMap, name string) float64 { - mod, found := modules.Get(name) - if !found || mod == nil { + mod := modules.Get(name) + if mod == nil { return 0 } return mod.Current().Mono diff --git a/synth/synth.go b/synth/synth.go index 16f6b7e..bcbad4a 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -106,7 +106,7 @@ func (s *Synth) GetOutput() Output { s.adjustVolume() out := Output{Time: s.Time} - if mod, ok := s.modules.Get(s.Out); ok { + if mod := s.modules.Get(s.Out); mod != nil { out.Left = mod.Current().Left * s.Volume out.Right = mod.Current().Right * s.Volume out.Mono = mod.Current().Mono * s.Volume From 0469a80baec38c75d74d7aebd4c9d5f5cc7baa6c Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Fri, 23 Jan 2026 22:21:04 +0100 Subject: [PATCH 4/9] mixer in sync map --- concurrency/map.go | 5 ++-- module/filter.go | 8 +++--- module/mixer.go | 42 +++++++++++++++++--------------- module/mixer_test.go | 58 +++++++++++++++++++++++++++++++++++++++----- module/module.go | 2 +- synth/synth.go | 2 +- synth/synth_test.go | 9 +++++-- 7 files changed, 91 insertions(+), 35 deletions(-) diff --git a/concurrency/map.go b/concurrency/map.go index ba2aa04..b99073b 100644 --- a/concurrency/map.go +++ b/concurrency/map.go @@ -19,12 +19,13 @@ func NewSyncMap[T comparable, E any](m map[T]E) *SyncMap[T, E] { } } -func (m *SyncMap[T, E]) Get(idx T) E { +func (m *SyncMap[T, E]) Get(idx T) (E, bool) { m.mu.Lock() defer func() { m.mu.Unlock() }() - return m.m[idx] + e, found := m.m[idx] + return e, found } func (m *SyncMap[T, E]) Set(idx T, e E) { diff --git a/module/filter.go b/module/filter.go index c3c85a7..632132a 100644 --- a/module/filter.go +++ b/module/filter.go @@ -39,12 +39,12 @@ const ( filterTypeHighPass filterType = "HighPass" filterTypeBandPass filterType = "BandPass" - gain = -50 - slope = 0.99 + filterGain = -50 + filterSlope = 0.99 ) var ( - amp = math.Pow(10, gain/40) + amp = math.Pow(10, filterGain/40) ) func (m FilterMap) Initialize(sampleRate float64) error { @@ -232,7 +232,7 @@ func getOmega(freq float64, sampleRate float64) float64 { } func getAlphaLPHP(omega float64) float64 { - rootArg := (amp+1/amp)*(1/slope-slope) + 2 + rootArg := (amp+1/amp)*(1/filterSlope-filterSlope) + 2 root := math.Sqrt(rootArg) factor := math.Sin(omega) / 2 return factor * root diff --git a/module/mixer.go b/module/mixer.go index f708698..522f1be 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -48,11 +48,13 @@ func (m *Mixer) initialize(sampleRate float64) error { target: m.Gain, } + m.in = concurrency.NewSyncMap(m.In) m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) - for mod, gain := range m.In { - m.In[mod] = calc.Limit(gain, inputGainRange) + for _, name := range m.in.Keys() { + gain, _ := m.in.Get(name) + m.in.Set(name, calc.Limit(gain, inputGainRange)) - m.inputFaders.Set(mod, &fader{ + m.inputFaders.Set(name, &fader{ current: gain, target: gain, }) @@ -70,6 +72,7 @@ func (m *Mixer) Update(new *Mixer) { m.CV = new.CV m.Mod = new.Mod m.Fade = new.Fade + m.In = new.In m.updateGains(new) } @@ -78,8 +81,9 @@ func (m *Mixer) Step(modules *ModuleMap) { left, right, mono float64 ) - for name, gain := range m.In { - if mod := modules.Get(name); mod != nil { + for _, name := range m.in.Keys() { + gain, _ := m.in.Get(name) + if mod, _ := modules.Get(name); mod != nil { left += mod.Current().Left * gain right += mod.Current().Right * gain mono += mod.Current().Mono * gain @@ -111,15 +115,15 @@ func (m *Mixer) fade() { } for _, name := range m.inputFaders.Keys() { - f := m.inputFaders.Get(name) + f, _ := m.inputFaders.Get(name) if f == nil { continue } - m.In[name] = f.fade() + m.in.Set(name, f.fade()) if f.current == 0 && f.target == 0 { m.inputFaders.Delete(name) - delete(m.In, name) + m.in.Delete(name) } } } @@ -130,7 +134,7 @@ func (m *Mixer) initializeFaders() { } for _, name := range m.inputFaders.Keys() { - if f := m.inputFaders.Get(name); f != nil { + if f, _ := m.inputFaders.Get(name); f != nil { f.initialize(m.Fade, m.sampleRate) } } @@ -144,31 +148,31 @@ func (m *Mixer) updateGains(new *Mixer) { if m.inputFaders == nil { m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) } - if m.In == nil { - m.In = map[string]float64{} + if m.in == nil { + m.in = concurrency.NewSyncMap(map[string]float64{}) } - for mod, gain := range new.In { - f := m.inputFaders.Get(mod) + for _, name := range new.in.Keys() { + f, _ := m.inputFaders.Get(name) if f != nil { - f.target = gain + f.target = filterGain continue } - m.inputFaders.Set(mod, &fader{ + m.inputFaders.Set(name, &fader{ current: 0, - target: gain, + target: filterGain, }) - m.In[mod] = 0 + m.in.Set(name, 0) } for _, name := range m.inputFaders.Keys() { - f := m.inputFaders.Get(name) + f, _ := m.inputFaders.Get(name) if f == nil { continue } - if _, ok := new.In[name]; !ok { + if _, ok := new.in.Get(name); !ok { f.target = 0 } } diff --git a/module/mixer_test.go b/module/mixer_test.go index a5f59f2..e34d507 100644 --- a/module/mixer_test.go +++ b/module/mixer_test.go @@ -22,6 +22,7 @@ func TestMixer_Step(t *testing.T) { In: map[string]float64{}, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{}, @@ -37,6 +38,7 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Oscillator{ @@ -54,6 +56,7 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{}), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -75,6 +78,9 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{ + "in": 1, + }), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -101,6 +107,9 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{ + "in": 1, + }), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -134,6 +143,9 @@ func TestMixer_Step(t *testing.T) { }, sampleRate: 1, inputFaders: concurrency.NewSyncMap(map[string]*fader{}), + in: concurrency.NewSyncMap(map[string]float64{ + "in": 1, + }), }, modules: NewModuleMap(map[string]IModule{ "in": &Module{ @@ -206,6 +218,9 @@ func TestMixer_Update(t *testing.T) { step: 0.5, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in": 1, + }), }, new: nil, want: &Mixer{ @@ -234,6 +249,9 @@ func TestMixer_Update(t *testing.T) { step: 0.5, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in": 1, + }), }, }, { @@ -270,6 +288,10 @@ func TestMixer_Update(t *testing.T) { step: 0.5, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 1, + "in2": 1, + }), }, new: &Mixer{ Gain: 0.5, @@ -280,6 +302,10 @@ func TestMixer_Update(t *testing.T) { "in3": 0.5, }, Fade: 2, + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 0.5, + "in3": 0.5, + }), }, want: &Mixer{ Module: Module{ @@ -291,9 +317,8 @@ func TestMixer_Update(t *testing.T) { CV: "new-cv", Mod: "new-mod", In: map[string]float64{ - "in1": 1, - "in2": 1, - "in3": 0, + "in1": 0.5, + "in3": 0.5, }, Fade: 2, sampleRate: sampleRate, @@ -319,6 +344,11 @@ func TestMixer_Update(t *testing.T) { step: 0.25 / sampleRate, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 1, + "in2": 1, + "in3": 0, + }), }, }, } @@ -333,7 +363,7 @@ func TestMixer_Update(t *testing.T) { Mixer{}, fader{}, ), - cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}), + cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}, concurrency.SyncMap[string, float64]{}), ); diff != "" { t.Errorf("Mixer.Update() diff = %s", diff) } @@ -372,6 +402,10 @@ func TestMixer_fade(t *testing.T) { step: 0.5, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 1, + "in2": 1, + }), }, want: &Mixer{ Gain: 1, @@ -393,6 +427,10 @@ func TestMixer_fade(t *testing.T) { target: 1, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 1, + "in2": 1, + }), }, }, { @@ -420,11 +458,16 @@ func TestMixer_fade(t *testing.T) { step: -0.2, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 1, + "in2": 0.1, + }), }, want: &Mixer{ Gain: 0.9, In: map[string]float64{ - "in1": 0.8, + "in1": 1, + "in2": 0.1, }, gainFader: &fader{ current: 0.9, @@ -438,6 +481,9 @@ func TestMixer_fade(t *testing.T) { step: -0.2, }, }), + in: concurrency.NewSyncMap(map[string]float64{ + "in1": 0.8, + }), }, }, } @@ -452,7 +498,7 @@ func TestMixer_fade(t *testing.T) { Mixer{}, fader{}, ), - cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}), + cmpopts.IgnoreUnexported(concurrency.SyncMap[string, *fader]{}, concurrency.SyncMap[string, float64]{}), ); diff != "" { t.Errorf("Mixer.fade() diff = %s", diff) } diff --git a/module/module.go b/module/module.go index d94d9e1..1ad0cad 100644 --- a/module/module.go +++ b/module/module.go @@ -89,7 +89,7 @@ func cv(rng calc.Range, val float64) float64 { } func getMono(modules *ModuleMap, name string) float64 { - mod := modules.Get(name) + mod, _ := modules.Get(name) if mod == nil { return 0 } diff --git a/synth/synth.go b/synth/synth.go index bcbad4a..1074bd3 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -106,7 +106,7 @@ func (s *Synth) GetOutput() Output { s.adjustVolume() out := Output{Time: s.Time} - if mod := s.modules.Get(s.Out); mod != nil { + if mod, _ := s.modules.Get(s.Out); mod != nil { out.Left = mod.Current().Left * s.Volume out.Right = mod.Current().Right * s.Volume out.Mono = mod.Current().Mono * s.Volume diff --git a/synth/synth_test.go b/synth/synth_test.go index b643f63..cac8da0 100644 --- a/synth/synth_test.go +++ b/synth/synth_test.go @@ -270,7 +270,7 @@ func TestSynth_Update(t *testing.T) { CV: "new-mod", Mod: "new-mod", In: map[string]float64{ - "new-in": 0, + "new-in": 1, }, }, }, @@ -310,7 +310,7 @@ func TestSynth_Update(t *testing.T) { "w2": { CV: "new-cv", Mod: "new-mod", - Signal: []float64{2}, + Signal: []float64{1}, }, }, Time: 5, @@ -344,6 +344,11 @@ func TestSynth_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + err := tt.new.Initialize(tt.s.sampleRate) + if err != nil { + t.Errorf("Synth.Update() new.Initialize() error %v", err) + } + tt.s.Update(tt.new) if diff := cmp.Diff(tt.want, tt.s, cmpopts.IgnoreUnexported( From 8f4286ce60500081b1403e5068deb79c67600a7b Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 26 Jan 2026 21:14:29 +0100 Subject: [PATCH 5/9] fix mixer faders --- module/mixer.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/mixer.go b/module/mixer.go index 522f1be..de65060 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -153,15 +153,17 @@ func (m *Mixer) updateGains(new *Mixer) { } for _, name := range new.in.Keys() { + gain, _ := new.in.Get(name) + f, _ := m.inputFaders.Get(name) if f != nil { - f.target = filterGain + f.target = gain continue } m.inputFaders.Set(name, &fader{ current: 0, - target: filterGain, + target: gain, }) m.in.Set(name, 0) } From 62f1aeacaebce5eaaa34605c6178f914eb3cea9d Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 26 Jan 2026 21:26:16 +0100 Subject: [PATCH 6/9] remove useless filter vars --- module/filter.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/module/filter.go b/module/filter.go index 632132a..261a3d0 100644 --- a/module/filter.go +++ b/module/filter.go @@ -38,13 +38,6 @@ const ( filterTypeLowPass filterType = "LowPass" filterTypeHighPass filterType = "HighPass" filterTypeBandPass filterType = "BandPass" - - filterGain = -50 - filterSlope = 0.99 -) - -var ( - amp = math.Pow(10, filterGain/40) ) func (m FilterMap) Initialize(sampleRate float64) error { @@ -232,8 +225,7 @@ func getOmega(freq float64, sampleRate float64) float64 { } func getAlphaLPHP(omega float64) float64 { - rootArg := (amp+1/amp)*(1/filterSlope-filterSlope) + 2 - root := math.Sqrt(rootArg) + root := math.Sqrt(2) factor := math.Sin(omega) / 2 return factor * root } From b065323fdd69f88fccb8ace3f725e2c4e23ce744 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 26 Jan 2026 21:34:01 +0100 Subject: [PATCH 7/9] fix nil map --- module/mixer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/mixer.go b/module/mixer.go index de65060..8c5512e 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -148,7 +148,7 @@ func (m *Mixer) updateGains(new *Mixer) { if m.inputFaders == nil { m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) } - if m.in == nil { + if len(m.in.Keys()) == 0 { m.in = concurrency.NewSyncMap(map[string]float64{}) } From 6adc9a1e11deaf1f89388b51806aa672d59c03d7 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 26 Jan 2026 21:36:17 +0100 Subject: [PATCH 8/9] fix nil pointer --- module/mixer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/mixer.go b/module/mixer.go index 8c5512e..eb58c63 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -148,7 +148,7 @@ func (m *Mixer) updateGains(new *Mixer) { if m.inputFaders == nil { m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) } - if len(m.in.Keys()) == 0 { + if m.in == nil || len(m.in.Keys()) == 0 { m.in = concurrency.NewSyncMap(map[string]float64{}) } From 021d6b40ab7462377d912cc8dfaab23b4db973b4 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 26 Jan 2026 21:44:31 +0100 Subject: [PATCH 9/9] prevent nil map in concurrency map --- concurrency/map.go | 3 +++ module/mixer.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/concurrency/map.go b/concurrency/map.go index b99073b..4a33cd4 100644 --- a/concurrency/map.go +++ b/concurrency/map.go @@ -33,6 +33,9 @@ func (m *SyncMap[T, E]) Set(idx T, e E) { defer func() { m.mu.Unlock() }() + if m.m == nil { + m.m = make(map[T]E) + } m.m[idx] = e } diff --git a/module/mixer.go b/module/mixer.go index eb58c63..de65060 100644 --- a/module/mixer.go +++ b/module/mixer.go @@ -148,7 +148,7 @@ func (m *Mixer) updateGains(new *Mixer) { if m.inputFaders == nil { m.inputFaders = concurrency.NewSyncMap(map[string]*fader{}) } - if m.in == nil || len(m.in.Keys()) == 0 { + if m.in == nil { m.in = concurrency.NewSyncMap(map[string]float64{}) }