diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0cee6ef..8f5cc38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,14 @@ on: branches: [ "main" ] jobs: - - build: + test: 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: diff --git a/.gitignore b/.gitignore index ba077a4..8f87429 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin +examples/test.yaml 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 ./... 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 fafea6d..3ad07d3 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,45 +80,55 @@ 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 } defer audio.Terminate() + logger := ui.NewLogger(10) quit := make(chan bool) autoStop := make(chan bool) - u := ui.NewUI(file, quit, autoStop) + var closing bool + u := ui.NewUI(logger, file, quit, autoStop, config.Duration, &closing) go u.Enter() - output := make(chan struct{ Left, Right float32 }) - ctx, err := audio.NewContext(output, c.Config.SampleRate) + output := make(chan audio.AudioOutput) + ctx, err := audio.NewContext(output, config.SampleRate) if err != nil { return err } @@ -129,11 +139,11 @@ func start(file string) error { return err } - ctl := control.NewControl(output, autoStop) + ctl := control.NewControl(logger, *config, output, autoStop, &closing) ctl.Start() defer ctl.StopSynth() - loader, err := f.NewLoader(ctl, file) + loader, err := f.NewLoader(logger, ctl, file, &closing) if err != nil { return err } @@ -150,7 +160,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: @@ -158,12 +168,12 @@ 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", c.Config.FadeOut)) - ctl.Stop(c.Config.FadeOut) + 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..d71b841 100644 --- a/control/control.go +++ b/control/control.go @@ -1,45 +1,55 @@ package control import ( + "fmt" "math" - "github.com/iljarotar/synth/config" + "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 { - synth *s.Synth - output chan struct{ Left, Right float32 } - SynthDone chan bool - autoStop chan bool - reportTime chan float64 - currentTime float64 + logger *ui.Logger + config cfg.Config + synth *s.Synth + output chan audio.AudioOutput + SynthDone chan bool + autoStop chan bool + maxOutput, lastNotifiedOutput float64 + overdriveWarningTriggeredAt float64 + closing *bool } -func NewControl(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, closing *bool) *Control { var synth s.Synth - synth.Initialize() - reportTime := make(chan float64) + synth.Initialize(config.SampleRate) ctl := &Control{ - 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, + closing: closing, } 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) { - synth.Initialize() + c.maxOutput = 0 + c.lastNotifiedOutput = 0 + synth.Initialize(c.config.SampleRate) synth.Time += c.synth.Time *c.synth = synth @@ -62,32 +72,43 @@ 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) - c.checkDuration() +func (c *Control) receiveOutput(outputChan <-chan synth.Output) { + defer close(c.output) + + for out := range outputChan { + c.logger.SendTime(out.Time) + c.checkDuration(out.Time) + c.checkOverdrive(out.Mono, out.Time) + + c.output <- audio.AudioOutput{ + Left: out.Left, + Right: out.Right, + } } } -func (c *Control) checkDuration() { - if config.Config.Duration < 0 { - return +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 } - duration := config.Config.Duration - config.Config.FadeOut - if c.currentTime < duration || ui.State.Closed { - return - } - c.autoStop <- true -} -func logTime(time float64) { - if isNextSecond(time) { - ui.Logger.SendTime(int(time)) + 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)) + c.overdriveWarningTriggeredAt = time } } -func isNextSecond(time float64) bool { - sec, _ := math.Modf(time) - return sec > float64(ui.State.CurrentTime) +func (c *Control) checkDuration(time float64) { + if c.config.Duration < 0 { + return + } + duration := c.config.Duration - c.config.FadeOut + if time < duration || *c.closing { + return + } + c.autoStop <- true } diff --git a/file/loader.go b/file/loader.go index 74c6bc2..0f842df 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, closing *bool) (*Loader, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } watch := true - l := Loader{watcher: watcher, watch: &watch, ctl: ctl, file: file} - go l.StartWatching() + l := &Loader{ + logger: logger, + watcher: watcher, + watch: &watch, + ctl: ctl, + file: file, + } + go l.StartWatching(closing) - return &l, nil + return l, nil } func (l *Loader) Close() error { @@ -75,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 } @@ -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/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..5a5a42d 100644 --- a/synth/synth.go +++ b/synth/synth.go @@ -1,26 +1,21 @@ package synth import ( - "fmt" - "math" - - "github.com/iljarotar/synth/config" "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"` @@ -31,37 +26,39 @@ 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 notifyFadeOutDone chan bool fadeDirection FadeDirection fadeDuration float64 - playing bool + active 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 s.Volume = 0 // start muted - s.playing = true + s.active = 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,38 +66,32 @@ func (s *Synth) Initialize() { } for _, f := range s.Filters { - f.Initialize() + f.Initialize(sampleRate) } s.makeMaps() } -func (s *Synth) Play(output chan<- struct{ Left, Right float32 }, reportTime chan float64) { - defer close(output) - defer close(reportTime) - - for s.playing { - reportTime <- s.Time +func (s *Synth) Play(outputChan chan<- Output) { + defer close(outputChan) + for s.active { 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 } } func (s *Synth) Stop() { - s.playing = false + s.active = false } func (s *Synth) Fade(direction FadeDirection, seconds float64) { @@ -125,9 +116,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 +135,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/log.go b/ui/log.go index c4dd182..c5b8f6a 100644 --- a/ui/log.go +++ b/ui/log.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "math" ) const ( @@ -10,38 +11,81 @@ const ( labelError = "[ERROR] " ) -type logger struct { - log chan string - overdriveWarning chan bool - time chan string +type State struct { + overdriveWarning bool } -func (l *logger) SendTime(time int) { - State.CurrentTime = time - l.time <- formatTime(time) +type Logger struct { + logs []string + State + currentTime int + maxLogs uint + logSubscribers []chan<- string + stateSubscribers []chan<- State + timeSubscribers []chan<- string } -func (l *logger) Info(log 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 + + for _, s := range l.timeSubscribers { + s <- formatTime(seconds) + } + } +} + +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) { - State.ShowingOverdriveWarning = limitExceeded - l.overdriveWarning <- limitExceeded +func (l *Logger) ShowOverdriveWarning(limitExceeded bool) { + 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(State.CurrentTime) +func (l *Logger) sendLog(log, label string, labelColor color) { + time := formatTime(l.currentTime) coloredLabel := fmt.Sprintf("%s", colored(label, labelColor)) - l.log <- 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 { + sec, _ := math.Modf(time) + return sec > float64(l.currentTime) } func colored(str string, col color) string { @@ -69,9 +113,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/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 62cd122..2a93a63 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -6,26 +6,31 @@ import ( "os" "os/exec" "strings" - - "github.com/iljarotar/synth/config" ) type UI struct { - quit chan bool - input chan string - autoStop chan bool - file string - logs []string - time string + 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(file string, quit chan bool, autoStop chan bool) *UI { +func NewUI(logger *Logger, file string, quit chan bool, autoStop chan bool, duration float64, closing *bool) *UI { return &UI{ + logger: logger, quit: quit, autoStop: autoStop, input: make(chan string), file: file, time: "00:00:00", + duration: duration, + closing: closing, } } @@ -45,26 +50,36 @@ 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: if input == "q" { - State.Closed = true + *ui.closing = true ui.resetScreen() ui.quit <- true } else { ui.resetScreen() } - case time := <-Logger.time: + case time := <-timeChan: ui.time = time ui.updateTime() - case log := <-Logger.log: + case log := <-logChan: ui.appendLog(log) ui.resetScreen() - case <-Logger.overdriveWarning: + case state := <-stateChan: + ui.showOverdriveWarning = state.overdriveWarning ui.resetScreen() case <-ui.autoStop: - State.Closed = true + *ui.closing = true ui.quit <- true } } @@ -91,13 +106,13 @@ 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) } 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))