From db14a8892c0a3d11e7911c6bb61611bff9e93b52 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 5 Aug 2024 20:58:25 +0200 Subject: [PATCH 01/13] remove global config --- cmd/root.go | 52 ++++++++++++++++++++++++------------------ config/config.go | 54 ++++++++++++++++++++++++++------------------ control/control.go | 14 +++++++----- module/filter.go | 15 ++++++------ module/noise.go | 17 +++++++------- module/oscillator.go | 25 ++++++++++---------- module/sampler.go | 9 ++++---- module/wavetable.go | 21 +++++++++-------- synth/synth.go | 25 ++++++++++---------- ui/ui.go | 10 ++++---- 10 files changed, 134 insertions(+), 108 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index fafea6d..cdd5b76 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,7 +8,7 @@ import ( "time" "github.com/iljarotar/synth/audio" - "github.com/iljarotar/synth/config" + c "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/control" f "github.com/iljarotar/synth/file" @@ -53,19 +53,19 @@ documentation and usage: https://github.com/iljarotar/synth`, if cfg == "" { cfg = defaultConfigPath } - err = c.LoadConfig(cfg) + config, err := c.LoadConfig(cfg) if err != nil { fmt.Printf("could not load config file: %v\n", err) return } - err = parseFlags(cmd) + err = parseFlags(cmd, config) if err != nil { fmt.Println(err) return } - err = start(file) + err = start(file, config) if err != nil { fmt.Println(err) } @@ -80,32 +80,40 @@ func Execute() { } func init() { - defaultConfigPath, err := config.GetDefaultConfigPath() + defaultConfigPath, err := c.GetDefaultConfigPath() if err != nil { os.Exit(1) } - rootCmd.Flags().Float64P("sample-rate", "s", config.Default.SampleRate, "sample rate") - rootCmd.Flags().Float64("fade-in", config.Default.FadeIn, "fade-in in seconds") - rootCmd.Flags().Float64("fade-out", config.Default.FadeOut, "fade-out in seconds") + rootCmd.Flags().Float64P("sample-rate", "s", c.DefaultSampleRate, "sample rate") + rootCmd.Flags().Float64("fade-in", c.DefaultFadeIn, "fade-in in seconds") + rootCmd.Flags().Float64("fade-out", c.DefaultFadeOut, "fade-out in seconds") rootCmd.Flags().StringP("config", "c", defaultConfigPath, "path to your config file") - rootCmd.Flags().Float64P("duration", "d", config.Default.Duration, "duration in seconds; if positive duration is specified, synth will stop playing after the defined time") + rootCmd.Flags().Float64P("duration", "d", c.DefaultDuration, "duration in seconds; if positive duration is specified, synth will stop playing after the defined time") } -func parseFlags(cmd *cobra.Command) error { +func parseFlags(cmd *cobra.Command, config *c.Config) error { s, _ := cmd.Flags().GetFloat64("sample-rate") in, _ := cmd.Flags().GetFloat64("fade-in") out, _ := cmd.Flags().GetFloat64("fade-out") duration, _ := cmd.Flags().GetFloat64("duration") - c.Config.SampleRate = s - c.Config.FadeIn = in - c.Config.FadeOut = out - c.Config.Duration = duration + if cmd.Flag("sample-rate").Changed { + config.SampleRate = s + } + if cmd.Flag("fade-in").Changed { + config.FadeIn = in + } + if cmd.Flag("fade-out").Changed { + config.FadeOut = out + } + if cmd.Flag("duration").Changed { + config.Duration = duration + } - return c.Config.Validate() + return config.Validate() } -func start(file string) error { +func start(file string, config *c.Config) error { err := audio.Init() if err != nil { return err @@ -114,11 +122,11 @@ func start(file string) error { quit := make(chan bool) autoStop := make(chan bool) - u := ui.NewUI(file, quit, autoStop) + u := ui.NewUI(file, quit, autoStop, config.Duration) go u.Enter() output := make(chan struct{ Left, Right float32 }) - ctx, err := audio.NewContext(output, c.Config.SampleRate) + ctx, err := audio.NewContext(output, config.SampleRate) if err != nil { return err } @@ -129,7 +137,7 @@ func start(file string) error { return err } - ctl := control.NewControl(output, autoStop) + ctl := control.NewControl(*config, output, autoStop) ctl.Start() defer ctl.StopSynth() @@ -150,7 +158,7 @@ func start(file string) error { interrupt := make(chan bool) go catchInterrupt(interrupt, sig) - ctl.FadeIn(c.Config.FadeIn) + ctl.FadeIn(config.FadeIn) var fadingOut bool Loop: @@ -162,8 +170,8 @@ Loop: continue } fadingOut = true - ui.Logger.Info(fmt.Sprintf("fading out in %fs", c.Config.FadeOut)) - ctl.Stop(c.Config.FadeOut) + ui.Logger.Info(fmt.Sprintf("fading out in %fs", config.FadeOut)) + ctl.Stop(config.FadeOut) case <-interrupt: ctl.Stop(0.05) case <-ctl.SynthDone: diff --git a/config/config.go b/config/config.go index 535c027..0755a4d 100644 --- a/config/config.go +++ b/config/config.go @@ -9,30 +9,26 @@ import ( ) const ( - minSampleRate = 8000 - maxSampleRate = 48000 - maxFadeDuration = 3600 - maxDuration = 7200 + minSampleRate = 8000 + maxSampleRate = 48000 + maxFadeDuration = 3600 + maxDuration = 7200 + defaultConfigFile = "config.yaml" defaultConfigDir = "synth" + DefaultSampleRate = 44100 + DefaultFadeIn = 1 + DefaultFadeOut = 1 + DefaultDuration = -1 ) -type config struct { +type Config struct { SampleRate float64 `yaml:"sample-rate"` FadeIn float64 `yaml:"fade-in"` FadeOut float64 `yaml:"fade-out"` Duration float64 `yaml:"duration"` } -var Default = config{ - SampleRate: 44100, - FadeIn: 1, - FadeOut: 1, - Duration: -1, -} - -var Config = config{} - func GetDefaultConfigPath() (string, error) { userConfigDir, err := os.UserConfigDir() if err != nil { @@ -55,7 +51,14 @@ func EnsureDefaultConfig() error { return fmt.Errorf("unable to open config file: %w", err) } - defaultConfig, err := yaml.Marshal(Default) + defaultConfig := Config{ + SampleRate: DefaultSampleRate, + FadeIn: DefaultFadeIn, + FadeOut: DefaultFadeOut, + Duration: DefaultDuration, + } + + defaultConfigBytes, err := yaml.Marshal(defaultConfig) if err != nil { return fmt.Errorf("unable to marshal default config: %w", err) } @@ -65,24 +68,31 @@ func EnsureDefaultConfig() error { return fmt.Errorf("unable to create config directory: %w", err) } - return os.WriteFile(configPath, defaultConfig, 0600) + return os.WriteFile(configPath, defaultConfigBytes, 0600) } -func LoadConfig(path string) error { +func LoadConfig(path string) (*Config, error) { raw, err := os.ReadFile(path) if err != nil { - return err + return nil, err } - err = yaml.Unmarshal(raw, &Config) + config := &Config{} + + err = yaml.Unmarshal(raw, &config) if err != nil { - return err + return nil, err + } + + err = config.Validate() + if err != nil { + return nil, err } - return Config.Validate() + return config, nil } -func (c *config) Validate() error { +func (c *Config) Validate() error { if c.SampleRate < minSampleRate { return fmt.Errorf("sample rate must be greater than or equal to %d", minSampleRate) } diff --git a/control/control.go b/control/control.go index 59c276e..a0c63ab 100644 --- a/control/control.go +++ b/control/control.go @@ -3,12 +3,13 @@ package control import ( "math" - "github.com/iljarotar/synth/config" + cfg "github.com/iljarotar/synth/config" s "github.com/iljarotar/synth/synth" "github.com/iljarotar/synth/ui" ) type Control struct { + config cfg.Config synth *s.Synth output chan struct{ Left, Right float32 } SynthDone chan bool @@ -17,12 +18,13 @@ type Control struct { currentTime float64 } -func NewControl(output chan struct{ Left, Right float32 }, autoStop chan bool) *Control { +func NewControl(config cfg.Config, output chan struct{ Left, Right float32 }, autoStop chan bool) *Control { var synth s.Synth - synth.Initialize() + synth.Initialize(config.SampleRate) reportTime := make(chan float64) ctl := &Control{ + config: config, synth: &synth, output: output, SynthDone: make(chan bool), @@ -39,7 +41,7 @@ func (c *Control) Start() { } func (c *Control) LoadSynth(synth s.Synth) { - synth.Initialize() + synth.Initialize(c.config.SampleRate) synth.Time += c.synth.Time *c.synth = synth @@ -71,10 +73,10 @@ func (c *Control) observeTime() { } func (c *Control) checkDuration() { - if config.Config.Duration < 0 { + if c.config.Duration < 0 { return } - duration := config.Config.Duration - config.Config.FadeOut + duration := c.config.Duration - c.config.FadeOut if c.currentTime < duration || ui.State.Closed { return } diff --git a/module/filter.go b/module/filter.go index 86e1e72..34e8aab 100644 --- a/module/filter.go +++ b/module/filter.go @@ -3,7 +3,6 @@ package module import ( "math" - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/utils" ) @@ -33,9 +32,11 @@ type Filter struct { a0, a1, a2, b0, b1, b2 float64 amp float64 bypass bool + sampleRate float64 } -func (f *Filter) Initialize() { +func (f *Filter) Initialize(sampleRate float64) { + f.sampleRate = sampleRate f.limitParams() f.amp = getAmp(dbGain) f.calculateCoeffs(f.LowCutoff.Val, f.HighCutoff.Val) @@ -67,7 +68,7 @@ func (f *Filter) calculateCoeffs(fl, fh float64) { } func (f *Filter) calculateLowPassCoeffs(fc float64) { - omega := getOmega(fc) + omega := getOmega(fc, f.sampleRate) alpha := getAlphaLPHP(omega, f.amp, slope) f.b1 = 1 - math.Cos(omega) f.b0 = f.b1 / 2 @@ -78,7 +79,7 @@ func (f *Filter) calculateLowPassCoeffs(fc float64) { } func (f *Filter) calculateHighPassCoeffs(fc float64) { - omega := getOmega(fc) + omega := getOmega(fc, f.sampleRate) alpha := getAlphaLPHP(omega, f.amp, slope) f.b0 = (1 + math.Cos(omega)) / 2 f.b1 = -(1 + math.Cos(omega)) @@ -94,7 +95,7 @@ func (f *Filter) calculateBandPassCoeffs(fl, fh float64) { } bw := math.Log2(fh / fl) fc := fl + (fh-fl)/2 - omega := getOmega(fc) + omega := getOmega(fc, f.sampleRate) alpha := getAlphaBP(omega, bw) f.b0 = alpha f.b1 = 0 @@ -108,8 +109,8 @@ func getAmp(dbGain float64) float64 { return math.Pow(10, dbGain/40) } -func getOmega(fc float64) float64 { - return 2 * math.Pi * (fc / config.Config.SampleRate) +func getOmega(fc float64, sampleRate float64) float64 { + return 2 * math.Pi * (fc / sampleRate) } func getAlphaLPHP(omega, amp, slope float64) float64 { diff --git a/module/noise.go b/module/noise.go index b1531e8..dead8bf 100644 --- a/module/noise.go +++ b/module/noise.go @@ -3,20 +3,21 @@ package module import ( "math/rand" - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/utils" ) type Noise struct { Module - Name string `yaml:"name"` - Amp Input `yaml:"amp"` - Pan Input `yaml:"pan"` - Filters []string `yaml:"filters"` - inputs []filterInputs + Name string `yaml:"name"` + Amp Input `yaml:"amp"` + Pan Input `yaml:"pan"` + Filters []string `yaml:"filters"` + inputs []filterInputs + sampleRate float64 } -func (n *Noise) Initialize() { +func (n *Noise) Initialize(sampleRate float64) { + n.sampleRate = sampleRate n.limitParams() n.inputs = make([]filterInputs, len(n.Filters)) n.current = stereo(noise()*n.Amp.Val, n.Pan.Val) @@ -33,7 +34,7 @@ func (n *Noise) Next(modMap ModulesMap, filtersMap FiltersMap) { } y, newInputs := cfg.applyFilters(noise()) - n.integral += y / config.Config.SampleRate + n.integral += y / n.sampleRate n.inputs = newInputs n.current = stereo(y*amp, pan) } diff --git a/module/oscillator.go b/module/oscillator.go index 0b8f452..ebf8fd4 100644 --- a/module/oscillator.go +++ b/module/oscillator.go @@ -3,7 +3,6 @@ package module import ( "math" - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/utils" ) @@ -23,18 +22,20 @@ const ( type Oscillator struct { Module - Name string `yaml:"name"` - Type OscillatorType `yaml:"type"` - Freq Input `yaml:"freq"` - Amp Input `yaml:"amp"` - Phase float64 `yaml:"phase"` - Pan Input `yaml:"pan"` - Filters []string `yaml:"filters"` - inputs []filterInputs - signal SignalFunc + Name string `yaml:"name"` + Type OscillatorType `yaml:"type"` + Freq Input `yaml:"freq"` + Amp Input `yaml:"amp"` + Phase float64 `yaml:"phase"` + Pan Input `yaml:"pan"` + Filters []string `yaml:"filters"` + inputs []filterInputs + signal SignalFunc + sampleRate float64 } -func (o *Oscillator) Initialize() { +func (o *Oscillator) Initialize(sampleRate float64) { + o.sampleRate = sampleRate o.signal = newSignalFunc(o.Type) o.limitParams() o.inputs = make([]filterInputs, len(o.Filters)) @@ -57,7 +58,7 @@ func (o *Oscillator) Next(t float64, modMap ModulesMap, filtersMap FiltersMap) { x := o.signalValue(t, amp, offset) y, newInputs := cfg.applyFilters(x) avg := (y + o.Current().Mono) / 2 - o.integral += avg / config.Config.SampleRate + o.integral += avg / o.sampleRate o.inputs = newInputs o.current = stereo(y, pan) } diff --git a/module/sampler.go b/module/sampler.go index 2dcfc08..934ee21 100644 --- a/module/sampler.go +++ b/module/sampler.go @@ -1,7 +1,6 @@ package module import ( - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/utils" ) @@ -16,10 +15,12 @@ type Sampler struct { inputs []filterInputs lastTriggeredAt float64 limits + sampleRate float64 } -func (s *Sampler) Initialize() { - s.limits = limits{min: 0, max: config.Config.SampleRate} +func (s *Sampler) Initialize(sampleRate float64) { + s.sampleRate = sampleRate + s.limits = limits{min: 0, max: sampleRate} s.limitParams() s.inputs = make([]filterInputs, len(s.Filters)) s.current = stereo(0, s.Pan.Val) @@ -38,7 +39,7 @@ func (s *Sampler) Next(t float64, modMap ModulesMap, filtersMap FiltersMap) { x := s.sample(t, freq, amp, modMap) y, newInputs := cfg.applyFilters(x) - s.integral += y / config.Config.SampleRate + s.integral += y / s.sampleRate s.inputs = newInputs s.current = stereo(y, pan) } diff --git a/module/wavetable.go b/module/wavetable.go index 8febdeb..3e2a05a 100644 --- a/module/wavetable.go +++ b/module/wavetable.go @@ -3,22 +3,23 @@ package module import ( "math" - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/utils" ) type Wavetable struct { Module - Name string `yaml:"name"` - Table []float64 `yaml:"table"` - Freq Input `yaml:"freq"` - Amp Input `yaml:"amp"` - Pan Input `yaml:"pan"` - Filters []string `yaml:"filters"` - inputs []filterInputs + Name string `yaml:"name"` + Table []float64 `yaml:"table"` + Freq Input `yaml:"freq"` + Amp Input `yaml:"amp"` + Pan Input `yaml:"pan"` + Filters []string `yaml:"filters"` + inputs []filterInputs + sampleRate float64 } -func (w *Wavetable) Initialize() { +func (w *Wavetable) Initialize(sampleRate float64) { + w.sampleRate = sampleRate w.limitParams() w.Table = utils.Normalize(w.Table, -1, 1) w.inputs = make([]filterInputs, len(w.Filters)) @@ -40,7 +41,7 @@ func (w *Wavetable) Next(t float64, modMap ModulesMap, filtersMap FiltersMap) { x := w.signalValue(t, amp, freq) y, newInputs := cfg.applyFilters(x) - w.integral += y / config.Config.SampleRate + w.integral += y / w.sampleRate w.inputs = newInputs w.current = stereo(y, pan) } diff --git a/synth/synth.go b/synth/synth.go index 57ebc5d..ffdb129 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -4,7 +4,6 @@ import ( "fmt" "math" - "github.com/iljarotar/synth/config" "github.com/iljarotar/synth/module" "github.com/iljarotar/synth/ui" "github.com/iljarotar/synth/utils" @@ -31,6 +30,7 @@ type Synth struct { Envelopes []*module.Envelope `yaml:"envelopes"` Filters []*module.Filter `yaml:"filters"` Time float64 `yaml:"time"` + sampleRate float64 modMap module.ModulesMap filtersMap module.FiltersMap step, volumeMemory float64 @@ -40,8 +40,9 @@ type Synth struct { playing bool } -func (s *Synth) Initialize() { - s.step = 1 / config.Config.SampleRate +func (s *Synth) Initialize(sampleRate float64) { + s.step = 1 / sampleRate + s.sampleRate = sampleRate s.Volume = utils.Limit(s.Volume, 0, 1) s.Time = utils.Limit(s.Time, 0, maxInitTime) s.volumeMemory = s.Volume @@ -49,19 +50,19 @@ func (s *Synth) Initialize() { s.playing = true for _, osc := range s.Oscillators { - osc.Initialize() + osc.Initialize(sampleRate) } for _, n := range s.Noises { - n.Initialize() + n.Initialize(sampleRate) } for _, c := range s.Wavetables { - c.Initialize() + c.Initialize(sampleRate) } for _, smplr := range s.Samplers { - smplr.Initialize() + smplr.Initialize(sampleRate) } for _, e := range s.Envelopes { @@ -69,7 +70,7 @@ func (s *Synth) Initialize() { } for _, f := range s.Filters { - f.Initialize() + f.Initialize(sampleRate) } s.makeMaps() @@ -125,9 +126,9 @@ func (s *Synth) fadeIn() { return } - step := secondsToStep(s.fadeDuration, s.volumeMemory-s.Volume, config.Config.SampleRate) + step := secondsToStep(s.fadeDuration, s.volumeMemory-s.Volume, s.sampleRate) s.Volume += step - s.fadeDuration -= 1 / config.Config.SampleRate + s.fadeDuration -= 1 / s.sampleRate if s.Volume > s.volumeMemory { s.Volume = s.volumeMemory @@ -144,9 +145,9 @@ func (s *Synth) fadeOut() { return } - step := secondsToStep(s.fadeDuration, s.Volume, config.Config.SampleRate) + step := secondsToStep(s.fadeDuration, s.Volume, s.sampleRate) s.Volume -= step - s.fadeDuration -= 1 / config.Config.SampleRate + s.fadeDuration -= 1 / s.sampleRate if s.Volume < 0 { s.Volume = 0 diff --git a/ui/ui.go b/ui/ui.go index 62cd122..e7de26d 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -6,8 +6,6 @@ import ( "os" "os/exec" "strings" - - "github.com/iljarotar/synth/config" ) type UI struct { @@ -17,15 +15,17 @@ type UI struct { file string logs []string time string + duration float64 } -func NewUI(file string, quit chan bool, autoStop chan bool) *UI { +func NewUI(file string, quit chan bool, autoStop chan bool, duration float64) *UI { return &UI{ quit: quit, autoStop: autoStop, input: make(chan string), file: file, time: "00:00:00", + duration: duration, } } @@ -96,8 +96,8 @@ func (ui *UI) resetScreen() { LineBreaks(2) } fmt.Printf("%s", ui.time) - if config.Config.Duration >= 0 { - fmt.Printf(" - automatically stopping after %fs", config.Config.Duration) + if ui.duration >= 0 { + fmt.Printf(" - automatically stopping after %fs", ui.duration) } LineBreaks(1) fmt.Printf("%s ", colored("Type 'q' to quit:", colorBlueStrong)) From 6a02857f49840427e01e1495928d004b9515eda7 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Sun, 11 Aug 2024 12:01:13 +0200 Subject: [PATCH 02/13] factor out ui specific things from synth --- .gitignore | 1 + audio/context.go | 12 ++++++----- cmd/root.go | 13 ++++++------ control/control.go | 53 ++++++++++++++++++++++++++++++---------------- file/loader.go | 21 ++++++++++++------ synth/synth.go | 36 ++++++++++++------------------- ui/log.go | 28 ++++++++++++------------ ui/ui.go | 10 +++++---- 8 files changed, 98 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index ba077a4..8f87429 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin +examples/test.yaml diff --git a/audio/context.go b/audio/context.go index a3e946f..8aa882a 100644 --- a/audio/context.go +++ b/audio/context.go @@ -4,14 +4,16 @@ import ( "github.com/gordonklaus/portaudio" ) -type ProcessCallback func([]float32) +type AudioOutput struct { + Left, Right float64 +} type Context struct { *portaudio.Stream - Input chan struct{ Left, Right float32 } + Input chan AudioOutput } -func NewContext(input chan struct{ Left, Right float32 }, sampleRate float64) (*Context, error) { +func NewContext(input chan AudioOutput, sampleRate float64) (*Context, error) { ctx := &Context{Input: input} var err error @@ -26,8 +28,8 @@ func NewContext(input chan struct{ Left, Right float32 }, sampleRate float64) (* func (c *Context) Process(out [][]float32) { for i := range out[0] { y := <-c.Input - out[0][i] = y.Left - out[1][i] = y.Right + out[0][i] = float32(y.Left) + out[1][i] = float32(y.Right) } } diff --git a/cmd/root.go b/cmd/root.go index cdd5b76..f0feeb6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -120,12 +120,13 @@ func start(file string, config *c.Config) error { } defer audio.Terminate() + logger := ui.NewLogger() quit := make(chan bool) autoStop := make(chan bool) - u := ui.NewUI(file, quit, autoStop, config.Duration) + u := ui.NewUI(logger, file, quit, autoStop, config.Duration) go u.Enter() - output := make(chan struct{ Left, Right float32 }) + output := make(chan audio.AudioOutput) ctx, err := audio.NewContext(output, config.SampleRate) if err != nil { return err @@ -137,11 +138,11 @@ func start(file string, config *c.Config) error { return err } - ctl := control.NewControl(*config, output, autoStop) + ctl := control.NewControl(logger, *config, output, autoStop) ctl.Start() defer ctl.StopSynth() - loader, err := f.NewLoader(ctl, file) + loader, err := f.NewLoader(logger, ctl, file) if err != nil { return err } @@ -166,11 +167,11 @@ Loop: select { case <-quit: if fadingOut { - ui.Logger.Info("already received quit signal") + logger.Info("already received quit signal") continue } fadingOut = true - ui.Logger.Info(fmt.Sprintf("fading out in %fs", config.FadeOut)) + logger.Info(fmt.Sprintf("fading out in %fs", config.FadeOut)) ctl.Stop(config.FadeOut) case <-interrupt: ctl.Stop(0.05) diff --git a/control/control.go b/control/control.go index a0c63ab..81f185f 100644 --- a/control/control.go +++ b/control/control.go @@ -1,43 +1,46 @@ package control import ( + "fmt" "math" + "github.com/iljarotar/synth/audio" cfg "github.com/iljarotar/synth/config" + "github.com/iljarotar/synth/synth" s "github.com/iljarotar/synth/synth" "github.com/iljarotar/synth/ui" ) type Control struct { + logger *ui.Logger config cfg.Config synth *s.Synth - output chan struct{ Left, Right float32 } + output chan audio.AudioOutput SynthDone chan bool autoStop chan bool - reportTime chan float64 currentTime float64 } -func NewControl(config cfg.Config, output chan struct{ Left, Right float32 }, autoStop chan bool) *Control { +func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOutput, autoStop chan bool) *Control { var synth s.Synth synth.Initialize(config.SampleRate) - reportTime := make(chan float64) ctl := &Control{ - config: config, - synth: &synth, - output: output, - SynthDone: make(chan bool), - reportTime: reportTime, - autoStop: autoStop, + logger: logger, + config: config, + synth: &synth, + output: output, + SynthDone: make(chan bool), + autoStop: autoStop, } return ctl } func (c *Control) Start() { - go c.synth.Play(c.output, c.reportTime) - go c.observeTime() + outputChan := make(chan synth.Output) + go c.synth.Play(outputChan) + go c.receiveOutput(outputChan) } func (c *Control) LoadSynth(synth s.Synth) { @@ -64,11 +67,25 @@ func (c *Control) FadeOut(fadeOut float64, notifyDone chan bool) { c.synth.Fade(s.FadeDirectionOut, fadeOut) } -func (c *Control) observeTime() { - for time := range c.reportTime { - c.currentTime = time - logTime(time) +func (c *Control) receiveOutput(outputChan <-chan synth.Output) { + for out := range outputChan { + c.currentTime = out.Time + c.logTime(out.Time) c.checkDuration() + + c.checkOverdrive(out.Mono) + + c.output <- audio.AudioOutput{ + Left: out.Left, + Right: out.Right, + } + } +} + +func (c *Control) checkOverdrive(output float64) { + if math.Abs(output) >= 1.00001 && !ui.State.ShowingOverdriveWarning { + c.logger.ShowOverdriveWarning(true) + c.logger.Warning(fmt.Sprintf("Output value %f", output)) } } @@ -83,9 +100,9 @@ func (c *Control) checkDuration() { c.autoStop <- true } -func logTime(time float64) { +func (c *Control) logTime(time float64) { if isNextSecond(time) { - ui.Logger.SendTime(int(time)) + c.logger.SendTime(int(time)) } } diff --git a/file/loader.go b/file/loader.go index 74c6bc2..740b72c 100644 --- a/file/loader.go +++ b/file/loader.go @@ -14,6 +14,7 @@ import ( ) type Loader struct { + logger *ui.Logger watcher *fsnotify.Watcher watch *bool lastLoaded time.Time @@ -21,17 +22,23 @@ type Loader struct { file string } -func NewLoader(ctl *control.Control, file string) (*Loader, error) { +func NewLoader(logger *ui.Logger, ctl *control.Control, file string) (*Loader, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } watch := true - l := Loader{watcher: watcher, watch: &watch, ctl: ctl, file: file} + l := &Loader{ + logger: logger, + watcher: watcher, + watch: &watch, + ctl: ctl, + file: file, + } go l.StartWatching() - return &l, nil + return l, nil } func (l *Loader) Close() error { @@ -97,10 +104,10 @@ func (l *Loader) StartWatching() { err := l.Load() if err != nil { - ui.Logger.Error("could not load file. error: " + err.Error()) + l.logger.Error("could not load file. error: " + err.Error()) } else { - ui.Logger.Info("reloaded patch file") - ui.Logger.ShowOverdriveWarning(false) + l.logger.Info("reloaded patch file") + l.logger.ShowOverdriveWarning(false) } l.ctl.FadeIn(0.01) @@ -109,7 +116,7 @@ func (l *Loader) StartWatching() { if !ok { return } - ui.Logger.Error("an error occurred. please restart synth. error: " + err.Error()) + l.logger.Error("an error occurred. please restart synth. error: " + err.Error()) } } } diff --git a/synth/synth.go b/synth/synth.go index ffdb129..d1e0c6b 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -1,25 +1,21 @@ package synth import ( - "fmt" - "math" - "github.com/iljarotar/synth/module" - "github.com/iljarotar/synth/ui" "github.com/iljarotar/synth/utils" ) const ( - maxInitTime = 7200 -) - -type FadeDirection string - -const ( + maxInitTime = 7200 FadeDirectionIn FadeDirection = "in" FadeDirectionOut FadeDirection = "out" ) +type FadeDirection string +type Output struct { + Left, Right, Mono, Time float64 +} + type Synth struct { Volume float64 `yaml:"vol"` Out []string `yaml:"out"` @@ -76,27 +72,21 @@ func (s *Synth) Initialize(sampleRate float64) { s.makeMaps() } -func (s *Synth) Play(output chan<- struct{ Left, Right float32 }, reportTime chan float64) { - defer close(output) - defer close(reportTime) +func (s *Synth) Play(outputChan chan<- Output) { + defer close(outputChan) for s.playing { - reportTime <- s.Time - left, right, mono := s.getCurrentValue() s.adjustVolume() left *= s.Volume right *= s.Volume - mono *= s.Volume - // ignore exceeding limit if the difference is sufficiently small - if math.Abs(mono) >= 1.00001 && !ui.State.ShowingOverdriveWarning { - ui.Logger.ShowOverdriveWarning(true) - ui.Logger.Warning(fmt.Sprintf("Output value %f", mono)) + outputChan <- Output{ + Left: left, + Right: right, + Mono: mono, + Time: s.Time, } - - y := struct{ Left, Right float32 }{Left: float32(left), Right: float32(right)} - output <- y } } diff --git a/ui/log.go b/ui/log.go index c4dd182..55062d5 100644 --- a/ui/log.go +++ b/ui/log.go @@ -10,35 +10,43 @@ const ( labelError = "[ERROR] " ) -type logger struct { +type Logger struct { log chan string overdriveWarning chan bool time chan string } -func (l *logger) SendTime(time int) { +func NewLogger() *Logger { + return &Logger{ + log: make(chan string), + overdriveWarning: make(chan bool), + time: make(chan string), + } +} + +func (l *Logger) SendTime(time int) { State.CurrentTime = time l.time <- formatTime(time) } -func (l *logger) Info(log string) { +func (l *Logger) Info(log string) { l.sendLog(log, labelInfo, colorGreenStrong) } -func (l *logger) Warning(log string) { +func (l *Logger) Warning(log string) { l.sendLog(log, labelWarning, colorOrangeStorng) } -func (l *logger) Error(log string) { +func (l *Logger) Error(log string) { l.sendLog(log, labelError, colorRedStrong) } -func (l *logger) ShowOverdriveWarning(limitExceeded bool) { +func (l *Logger) ShowOverdriveWarning(limitExceeded bool) { State.ShowingOverdriveWarning = limitExceeded l.overdriveWarning <- limitExceeded } -func (l *logger) sendLog(log, label string, labelColor color) { +func (l *Logger) sendLog(log, label string, labelColor color) { time := formatTime(State.CurrentTime) coloredLabel := fmt.Sprintf("%s", colored(label, labelColor)) l.log <- fmt.Sprintf("[%s] %s %s", time, coloredLabel, log) @@ -69,9 +77,3 @@ func formatTime(time int) string { return fmt.Sprintf("%s:%s:%s", hoursString, minutesString, secondsString) } - -var Logger = &logger{ - log: make(chan string), - overdriveWarning: make(chan bool), - time: make(chan string), -} diff --git a/ui/ui.go b/ui/ui.go index e7de26d..cfac46b 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -9,6 +9,7 @@ import ( ) type UI struct { + logger *Logger quit chan bool input chan string autoStop chan bool @@ -18,8 +19,9 @@ type UI struct { duration float64 } -func NewUI(file string, quit chan bool, autoStop chan bool, duration float64) *UI { +func NewUI(logger *Logger, file string, quit chan bool, autoStop chan bool, duration float64) *UI { return &UI{ + logger: logger, quit: quit, autoStop: autoStop, input: make(chan string), @@ -55,13 +57,13 @@ func (ui *UI) Enter() { } else { ui.resetScreen() } - case time := <-Logger.time: + case time := <-ui.logger.time: ui.time = time ui.updateTime() - case log := <-Logger.log: + case log := <-ui.logger.log: ui.appendLog(log) ui.resetScreen() - case <-Logger.overdriveWarning: + case <-ui.logger.overdriveWarning: ui.resetScreen() case <-ui.autoStop: State.Closed = true From 91f1cd9a69f0eb9125f42f21f18e2e9bdd346922 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Sun, 11 Aug 2024 12:34:07 +0200 Subject: [PATCH 03/13] remove current time from control --- control/control.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/control/control.go b/control/control.go index 81f185f..cd4c40e 100644 --- a/control/control.go +++ b/control/control.go @@ -12,13 +12,12 @@ import ( ) type Control struct { - logger *ui.Logger - config cfg.Config - synth *s.Synth - output chan audio.AudioOutput - SynthDone chan bool - autoStop chan bool - currentTime float64 + logger *ui.Logger + config cfg.Config + synth *s.Synth + output chan audio.AudioOutput + SynthDone chan bool + autoStop chan bool } func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOutput, autoStop chan bool) *Control { @@ -69,9 +68,8 @@ func (c *Control) FadeOut(fadeOut float64, notifyDone chan bool) { func (c *Control) receiveOutput(outputChan <-chan synth.Output) { for out := range outputChan { - c.currentTime = out.Time c.logTime(out.Time) - c.checkDuration() + c.checkDuration(out.Time) c.checkOverdrive(out.Mono) @@ -89,12 +87,12 @@ func (c *Control) checkOverdrive(output float64) { } } -func (c *Control) checkDuration() { +func (c *Control) checkDuration(time float64) { if c.config.Duration < 0 { return } duration := c.config.Duration - c.config.FadeOut - if c.currentTime < duration || ui.State.Closed { + if time < duration || ui.State.Closed { return } c.autoStop <- true From 15cd185002e81cff9d379218b9514ebd622d144a Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Sun, 11 Aug 2024 12:42:21 +0200 Subject: [PATCH 04/13] close audio output in the end --- control/control.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/control/control.go b/control/control.go index cd4c40e..e631d4f 100644 --- a/control/control.go +++ b/control/control.go @@ -67,6 +67,8 @@ func (c *Control) FadeOut(fadeOut float64, notifyDone chan bool) { } func (c *Control) receiveOutput(outputChan <-chan synth.Output) { + defer close(c.output) + for out := range outputChan { c.logTime(out.Time) c.checkDuration(out.Time) From c19b97fa93e3f7532c5e27729a936e9fc070c5be Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Sun, 11 Aug 2024 12:42:26 +0200 Subject: [PATCH 05/13] rename --- synth/synth.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synth/synth.go b/synth/synth.go index d1e0c6b..5a5a42d 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -33,7 +33,7 @@ type Synth struct { notifyFadeOutDone chan bool fadeDirection FadeDirection fadeDuration float64 - playing bool + active bool } func (s *Synth) Initialize(sampleRate float64) { @@ -43,7 +43,7 @@ func (s *Synth) Initialize(sampleRate float64) { s.Time = utils.Limit(s.Time, 0, maxInitTime) s.volumeMemory = s.Volume s.Volume = 0 // start muted - s.playing = true + s.active = true for _, osc := range s.Oscillators { osc.Initialize(sampleRate) @@ -75,7 +75,7 @@ func (s *Synth) Initialize(sampleRate float64) { func (s *Synth) Play(outputChan chan<- Output) { defer close(outputChan) - for s.playing { + for s.active { left, right, mono := s.getCurrentValue() s.adjustVolume() left *= s.Volume @@ -91,7 +91,7 @@ func (s *Synth) Play(outputChan chan<- Output) { } func (s *Synth) Stop() { - s.playing = false + s.active = false } func (s *Synth) Fade(direction FadeDirection, seconds float64) { From e740b043e2d907b07bf2acd5d4ad36d8f37b2d52 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Sun, 11 Aug 2024 13:31:05 +0200 Subject: [PATCH 06/13] notify max output --- control/control.go | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/control/control.go b/control/control.go index e631d4f..9b89407 100644 --- a/control/control.go +++ b/control/control.go @@ -12,12 +12,14 @@ import ( ) type Control struct { - logger *ui.Logger - config cfg.Config - synth *s.Synth - output chan audio.AudioOutput - SynthDone chan bool - autoStop chan bool + logger *ui.Logger + config cfg.Config + synth *s.Synth + output chan audio.AudioOutput + SynthDone chan bool + autoStop chan bool + maxOutput, lastNotifiedOutput float64 + overdriveWarningTriggeredAt float64 } func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOutput, autoStop chan bool) *Control { @@ -43,6 +45,8 @@ func (c *Control) Start() { } func (c *Control) LoadSynth(synth s.Synth) { + c.maxOutput = 0 + c.lastNotifiedOutput = 0 synth.Initialize(c.config.SampleRate) synth.Time += c.synth.Time @@ -73,7 +77,7 @@ func (c *Control) receiveOutput(outputChan <-chan synth.Output) { c.logTime(out.Time) c.checkDuration(out.Time) - c.checkOverdrive(out.Mono) + c.checkOverdrive(out.Mono, out.Time) c.output <- audio.AudioOutput{ Left: out.Left, @@ -82,10 +86,18 @@ func (c *Control) receiveOutput(outputChan <-chan synth.Output) { } } -func (c *Control) checkOverdrive(output float64) { - if math.Abs(output) >= 1.00001 && !ui.State.ShowingOverdriveWarning { +func (c *Control) checkOverdrive(output, time float64) { + // only consider up to three decimals + abs := math.Round(math.Abs(output)*1000) / 1000 + if abs > c.maxOutput { + c.maxOutput = abs + } + + if c.maxOutput >= 1 && c.maxOutput > c.lastNotifiedOutput && time-c.overdriveWarningTriggeredAt >= 0.5 { + c.lastNotifiedOutput = c.maxOutput c.logger.ShowOverdriveWarning(true) - c.logger.Warning(fmt.Sprintf("Output value %f", output)) + c.logger.Warning(fmt.Sprintf("Output value %f", c.maxOutput)) + c.overdriveWarningTriggeredAt = time } } From dfc2714e375b703e3472d7b1b1796a3dec9bf2f1 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Mon, 12 Aug 2024 20:56:54 +0200 Subject: [PATCH 07/13] remove state singleton --- cmd/root.go | 7 ++++--- control/control.go | 20 +++++--------------- file/loader.go | 8 ++++---- ui/log.go | 38 +++++++++++++++++++++++++------------- ui/state.go | 9 --------- ui/ui.go | 34 +++++++++++++++++++--------------- 6 files changed, 57 insertions(+), 59 deletions(-) delete mode 100644 ui/state.go diff --git a/cmd/root.go b/cmd/root.go index f0feeb6..bdfbddd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -123,7 +123,8 @@ func start(file string, config *c.Config) error { logger := ui.NewLogger() quit := make(chan bool) autoStop := make(chan bool) - u := ui.NewUI(logger, file, quit, autoStop, config.Duration) + var closing bool + u := ui.NewUI(logger, file, quit, autoStop, config.Duration, &closing) go u.Enter() output := make(chan audio.AudioOutput) @@ -138,11 +139,11 @@ func start(file string, config *c.Config) error { return err } - ctl := control.NewControl(logger, *config, output, autoStop) + ctl := control.NewControl(logger, *config, output, autoStop, &closing) ctl.Start() defer ctl.StopSynth() - loader, err := f.NewLoader(logger, ctl, file) + loader, err := f.NewLoader(logger, ctl, file, &closing) if err != nil { return err } diff --git a/control/control.go b/control/control.go index 9b89407..8b20367 100644 --- a/control/control.go +++ b/control/control.go @@ -20,9 +20,10 @@ type Control struct { autoStop chan bool maxOutput, lastNotifiedOutput float64 overdriveWarningTriggeredAt float64 + closing *bool } -func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOutput, autoStop chan bool) *Control { +func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOutput, autoStop chan bool, closing *bool) *Control { var synth s.Synth synth.Initialize(config.SampleRate) @@ -33,6 +34,7 @@ func NewControl(logger *ui.Logger, config cfg.Config, output chan audio.AudioOut output: output, SynthDone: make(chan bool), autoStop: autoStop, + closing: closing, } return ctl @@ -74,9 +76,8 @@ func (c *Control) receiveOutput(outputChan <-chan synth.Output) { defer close(c.output) for out := range outputChan { - c.logTime(out.Time) + c.logger.SendTime(out.Time) c.checkDuration(out.Time) - c.checkOverdrive(out.Mono, out.Time) c.output <- audio.AudioOutput{ @@ -106,19 +107,8 @@ func (c *Control) checkDuration(time float64) { return } duration := c.config.Duration - c.config.FadeOut - if time < duration || ui.State.Closed { + if time < duration || *c.closing { return } c.autoStop <- true } - -func (c *Control) logTime(time float64) { - if isNextSecond(time) { - c.logger.SendTime(int(time)) - } -} - -func isNextSecond(time float64) bool { - sec, _ := math.Modf(time) - return sec > float64(ui.State.CurrentTime) -} diff --git a/file/loader.go b/file/loader.go index 740b72c..0f842df 100644 --- a/file/loader.go +++ b/file/loader.go @@ -22,7 +22,7 @@ type Loader struct { file string } -func NewLoader(logger *ui.Logger, ctl *control.Control, file string) (*Loader, error) { +func NewLoader(logger *ui.Logger, ctl *control.Control, file string, closing *bool) (*Loader, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -36,7 +36,7 @@ func NewLoader(logger *ui.Logger, ctl *control.Control, file string) (*Loader, e ctl: ctl, file: file, } - go l.StartWatching() + go l.StartWatching(closing) return l, nil } @@ -82,14 +82,14 @@ func (l *Loader) Watch(file string) error { return l.watcher.Add(filePath) } -func (l *Loader) StartWatching() { +func (l *Loader) StartWatching(closed *bool) { for *l.watch { select { case event, ok := <-l.watcher.Events: if !ok { return } - if ui.State.Closed { + if *closed { return } diff --git a/ui/log.go b/ui/log.go index 55062d5..54dc3e6 100644 --- a/ui/log.go +++ b/ui/log.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "math" ) const ( @@ -11,22 +12,26 @@ const ( ) type Logger struct { - log chan string - overdriveWarning chan bool - time chan string + logChan chan string + overdriveWarningChan chan bool + timeChan chan string + currentTime int } func NewLogger() *Logger { return &Logger{ - log: make(chan string), - overdriveWarning: make(chan bool), - time: make(chan string), + logChan: make(chan string), + overdriveWarningChan: make(chan bool), + timeChan: make(chan string), } } -func (l *Logger) SendTime(time int) { - State.CurrentTime = time - l.time <- formatTime(time) +func (l *Logger) SendTime(time float64) { + if l.isNextSecond(time) { + seconds := int(time) + l.currentTime = seconds + l.timeChan <- formatTime(seconds) + } } func (l *Logger) Info(log string) { @@ -42,14 +47,18 @@ func (l *Logger) Error(log string) { } func (l *Logger) ShowOverdriveWarning(limitExceeded bool) { - State.ShowingOverdriveWarning = limitExceeded - l.overdriveWarning <- limitExceeded + l.overdriveWarningChan <- limitExceeded } func (l *Logger) sendLog(log, label string, labelColor color) { - time := formatTime(State.CurrentTime) + time := formatTime(l.currentTime) coloredLabel := fmt.Sprintf("%s", colored(label, labelColor)) - l.log <- fmt.Sprintf("[%s] %s %s", time, coloredLabel, log) + l.logChan <- fmt.Sprintf("[%s] %s %s", time, coloredLabel, log) +} + +func (l *Logger) isNextSecond(time float64) bool { + sec, _ := math.Modf(time) + return sec > float64(l.currentTime) } func colored(str string, col color) string { @@ -77,3 +86,6 @@ func formatTime(time int) string { return fmt.Sprintf("%s:%s:%s", hoursString, minutesString, secondsString) } + +// TODO: +// implement publish/subscribe mechanism diff --git a/ui/state.go b/ui/state.go deleted file mode 100644 index 08fcba0..0000000 --- a/ui/state.go +++ /dev/null @@ -1,9 +0,0 @@ -package ui - -type state struct { - Closed bool - ShowingOverdriveWarning bool - CurrentTime int -} - -var State = state{} diff --git a/ui/ui.go b/ui/ui.go index cfac46b..7ddb71b 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -9,17 +9,19 @@ import ( ) type UI struct { - logger *Logger - quit chan bool - input chan string - autoStop chan bool - file string - logs []string - time string - duration float64 + logger *Logger + quit chan bool + input chan string + autoStop chan bool + file string + logs []string + time string + duration float64 + showOverdriveWarning bool + closing *bool } -func NewUI(logger *Logger, file string, quit chan bool, autoStop chan bool, duration float64) *UI { +func NewUI(logger *Logger, file string, quit chan bool, autoStop chan bool, duration float64, closing *bool) *UI { return &UI{ logger: logger, quit: quit, @@ -28,6 +30,7 @@ func NewUI(logger *Logger, file string, quit chan bool, autoStop chan bool, dura file: file, time: "00:00:00", duration: duration, + closing: closing, } } @@ -51,22 +54,23 @@ func (ui *UI) Enter() { select { case input := <-ui.input: if input == "q" { - State.Closed = true + *ui.closing = true ui.resetScreen() ui.quit <- true } else { ui.resetScreen() } - case time := <-ui.logger.time: + case time := <-ui.logger.timeChan: ui.time = time ui.updateTime() - case log := <-ui.logger.log: + case log := <-ui.logger.logChan: ui.appendLog(log) ui.resetScreen() - case <-ui.logger.overdriveWarning: + case overdriveWarning := <-ui.logger.overdriveWarningChan: + ui.showOverdriveWarning = overdriveWarning ui.resetScreen() case <-ui.autoStop: - State.Closed = true + *ui.closing = true ui.quit <- true } } @@ -93,7 +97,7 @@ func (ui *UI) resetScreen() { if len(ui.logs) > 0 { LineBreaks(1) } - if State.ShowingOverdriveWarning { + if ui.showOverdriveWarning { fmt.Printf("%s", colored("[WARNING] Volume exceeded 100%%", colorOrangeStorng)) LineBreaks(2) } From a657d0ab3adec7893107a8fb5a8b13d944c45bdf Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 16:49:00 +0200 Subject: [PATCH 08/13] subscribe to logger --- cmd/root.go | 2 +- ui/log.go | 56 ++++++++++++++++++++++++++++++++++++++--------------- ui/ui.go | 17 ++++++++++++---- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index bdfbddd..3ad07d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -120,7 +120,7 @@ func start(file string, config *c.Config) error { } defer audio.Terminate() - logger := ui.NewLogger() + logger := ui.NewLogger(10) quit := make(chan bool) autoStop := make(chan bool) var closing bool diff --git a/ui/log.go b/ui/log.go index 54dc3e6..c5b8f6a 100644 --- a/ui/log.go +++ b/ui/log.go @@ -11,26 +11,44 @@ const ( labelError = "[ERROR] " ) +type State struct { + overdriveWarning bool +} + type Logger struct { - logChan chan string - overdriveWarningChan chan bool - timeChan chan string - currentTime int + logs []string + State + currentTime int + maxLogs uint + logSubscribers []chan<- string + stateSubscribers []chan<- State + timeSubscribers []chan<- string } -func NewLogger() *Logger { - return &Logger{ - logChan: make(chan string), - overdriveWarningChan: make(chan bool), - timeChan: make(chan string), - } +func NewLogger(maxLogs uint) *Logger { + return &Logger{maxLogs: maxLogs} +} + +func (l *Logger) SubscribeToLogs(subscriber chan<- string) { + l.logSubscribers = append(l.logSubscribers, subscriber) +} + +func (l *Logger) SubscribeToState(subscriber chan<- State) { + l.stateSubscribers = append(l.stateSubscribers, subscriber) +} + +func (l *Logger) SubscribeToTime(subscriber chan<- string) { + l.timeSubscribers = append(l.timeSubscribers, subscriber) } func (l *Logger) SendTime(time float64) { if l.isNextSecond(time) { seconds := int(time) l.currentTime = seconds - l.timeChan <- formatTime(seconds) + + for _, s := range l.timeSubscribers { + s <- formatTime(seconds) + } } } @@ -47,13 +65,22 @@ func (l *Logger) Error(log string) { } func (l *Logger) ShowOverdriveWarning(limitExceeded bool) { - l.overdriveWarningChan <- limitExceeded + newState := l.State + newState.overdriveWarning = limitExceeded + l.State = newState + + for _, s := range l.stateSubscribers { + s <- newState + } } func (l *Logger) sendLog(log, label string, labelColor color) { time := formatTime(l.currentTime) coloredLabel := fmt.Sprintf("%s", colored(label, labelColor)) - l.logChan <- fmt.Sprintf("[%s] %s %s", time, coloredLabel, log) + + for _, s := range l.logSubscribers { + s <- fmt.Sprintf("[%s] %s %s", time, coloredLabel, log) + } } func (l *Logger) isNextSecond(time float64) bool { @@ -86,6 +113,3 @@ func formatTime(time int) string { return fmt.Sprintf("%s:%s:%s", hoursString, minutesString, secondsString) } - -// TODO: -// implement publish/subscribe mechanism diff --git a/ui/ui.go b/ui/ui.go index 7ddb71b..2a93a63 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -50,6 +50,15 @@ func (ui *UI) Enter() { go ui.read() ui.resetScreen() + logChan := make(chan string) + ui.logger.SubscribeToLogs(logChan) + + timeChan := make(chan string) + ui.logger.SubscribeToTime(timeChan) + + stateChan := make(chan State) + ui.logger.SubscribeToState(stateChan) + for { select { case input := <-ui.input: @@ -60,14 +69,14 @@ func (ui *UI) Enter() { } else { ui.resetScreen() } - case time := <-ui.logger.timeChan: + case time := <-timeChan: ui.time = time ui.updateTime() - case log := <-ui.logger.logChan: + case log := <-logChan: ui.appendLog(log) ui.resetScreen() - case overdriveWarning := <-ui.logger.overdriveWarningChan: - ui.showOverdriveWarning = overdriveWarning + case state := <-stateChan: + ui.showOverdriveWarning = state.overdriveWarning ui.resetScreen() case <-ui.autoStop: *ui.closing = true From 22c052b4feb7386d3118743a64fc00436427b488 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 16:50:50 +0200 Subject: [PATCH 09/13] fix overdrive condition --- control/control.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/control.go b/control/control.go index 8b20367..d71b841 100644 --- a/control/control.go +++ b/control/control.go @@ -94,7 +94,7 @@ func (c *Control) checkOverdrive(output, time float64) { c.maxOutput = abs } - if c.maxOutput >= 1 && c.maxOutput > c.lastNotifiedOutput && time-c.overdriveWarningTriggeredAt >= 0.5 { + if c.maxOutput > 1 && c.maxOutput > c.lastNotifiedOutput && time-c.overdriveWarningTriggeredAt >= 0.5 { c.lastNotifiedOutput = c.maxOutput c.logger.ShowOverdriveWarning(true) c.logger.Warning(fmt.Sprintf("Output value %f", c.maxOutput)) From 4037a475e11bb19d16103a0c04d08ff3b4544f3e Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 16:56:23 +0200 Subject: [PATCH 10/13] make target test --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 7c0e28e..d1e9c50 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,7 @@ VERSION := $(shell git describe --tags --exact-match 2> /dev/null || git symboli PHONY: build build: go build -o bin/synth -ldflags="-X github.com/iljarotar/synth/cmd.version=$(VERSION)" + +PHONY: test +test: + go test ./... From d6ae16c00ff1d38bae96ca3da3027c97133ec570 Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 16:59:09 +0200 Subject: [PATCH 11/13] try arch for github action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0cee6ef..b0bcca5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: arch steps: - uses: actions/checkout@v4 From 72e93cf6314b53e10f826bb919c8029a7395d54b Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 17:01:13 +0200 Subject: [PATCH 12/13] install portaudio on runner --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0bcca5..c83c1e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,10 +9,13 @@ on: jobs: build: - runs-on: arch + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: install portaudio + run: sudo apt update && sudo apt -y install portaudio19-dev + - name: Set up Go uses: actions/setup-go@v4 with: From e77129b0eb636a30afccae985487278fce80b66a Mon Sep 17 00:00:00 2001 From: Ilja Rotar Date: Wed, 14 Aug 2024 17:05:11 +0200 Subject: [PATCH 13/13] rename job --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c83c1e6..8f5cc38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,8 +7,7 @@ on: branches: [ "main" ] jobs: - - build: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4