Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ build: test

PHONY: test
test:
go test ./...
go test -cover ./...
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- TODO: don't forget to add new `fade` field-->

# Synth

A modular synthesizer for the command line written in [golang](https://go.dev/).
Expand Down
1 change: 1 addition & 0 deletions examples/mixer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ out: main

mixers:
main:
fade: 3
gain: 1
in:
sine1: 0.25
Expand Down
1 change: 1 addition & 0 deletions examples/noise.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ out: lp
filters:
lp:
type: BandPass
fade: 1
freq: 800
width: 20
in: noise
Expand Down
1 change: 1 addition & 0 deletions examples/sine-440.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ out: sine

oscillators:
sine:
fade: 1
type: Sine
freq: 440
108 changes: 94 additions & 14 deletions module/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,95 @@ import (
type (
Envelope struct {
Module
Attack float64 `yaml:"attack"`
Decay float64 `yaml:"decay"`
Release float64 `yaml:"release"`
Peak float64 `yaml:"peak"`
Level float64 `yaml:"level"`
Gate string `yaml:"gate"`
Attack float64 `yaml:"attack"`
Decay float64 `yaml:"decay"`
Release float64 `yaml:"release"`
Peak float64 `yaml:"peak"`
Level float64 `yaml:"level"`
Gate string `yaml:"gate"`
Fade float64 `yaml:"fade"`

triggeredAt float64
releasedAt float64
gateValue float64
level float64
sampleRate float64

attackFader *fader
decayFader *fader
releaseFader *fader
peakFader *fader
levelFader *fader
}

EnvelopeMap map[string]*Envelope
)

func (m EnvelopeMap) Initialize() {
func (m EnvelopeMap) Initialize(sampleRate float64) {
for _, e := range m {
if e == nil {
continue
}
e.initialize()
e.initialize(sampleRate)
}
}

func (e *Envelope) initialize() {
func (e *Envelope) initialize(sampleRate float64) {
e.sampleRate = sampleRate
e.Attack = calc.Limit(e.Attack, envelopeRange)
e.Decay = calc.Limit(e.Decay, envelopeRange)
e.Release = calc.Limit(e.Release, envelopeRange)
e.Peak = calc.Limit(e.Peak, gainRange)
e.Level = calc.Limit(e.Level, gainRange)
e.Fade = calc.Limit(e.Fade, fadeRange)

e.attackFader = &fader{
current: e.Attack,
target: e.Attack,
}
e.decayFader = &fader{
current: e.Decay,
target: e.Decay,
}
e.releaseFader = &fader{
current: e.Release,
target: e.Release,
}
e.peakFader = &fader{
current: e.Peak,
target: e.Peak,
}
e.levelFader = &fader{
current: e.Level,
target: e.Level,
}
e.initializeFaders()
}

func (e *Envelope) Update(new *Envelope) {
if new == nil {
return
}

e.Attack = new.Attack
e.Decay = new.Decay
e.Release = new.Release
e.Peak = new.Peak
e.Level = new.Level
e.Gate = new.Gate
e.Fade = new.Fade

if e.attackFader != nil {
e.attackFader.target = new.Attack
}
if e.decayFader != nil {
e.decayFader.target = new.Decay
}
if e.releaseFader != nil {
e.releaseFader.target = new.Release
}
if e.peakFader != nil {
e.peakFader.target = new.Peak
}
if e.levelFader != nil {
e.levelFader.target = new.Level
}
e.initializeFaders()
}

func (e *Envelope) Step(t float64, modules ModuleMap) {
Expand All @@ -73,6 +119,40 @@ func (e *Envelope) Step(t float64, modules ModuleMap) {
}

e.gateValue = gateValue

if e.attackFader != nil {
e.Attack = e.attackFader.fade()
}
if e.decayFader != nil {
e.Decay = e.decayFader.fade()
}
if e.releaseFader != nil {
e.Release = e.releaseFader.fade()
}
if e.peakFader != nil {
e.Peak = e.peakFader.fade()
}
if e.levelFader != nil {
e.Level = e.levelFader.fade()
}
}

func (e *Envelope) initializeFaders() {
if e.attackFader != nil {
e.attackFader.initialize(e.Fade, e.sampleRate)
}
if e.decayFader != nil {
e.decayFader.initialize(e.Fade, e.sampleRate)
}
if e.releaseFader != nil {
e.releaseFader.initialize(e.Fade, e.sampleRate)
}
if e.peakFader != nil {
e.peakFader.initialize(e.Peak, e.sampleRate)
}
if e.levelFader != nil {
e.levelFader.initialize(e.Fade, e.sampleRate)
}
}

func (e *Envelope) getValue(t float64) float64 {
Expand Down
32 changes: 32 additions & 0 deletions module/fader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package module

type (
fader struct {
current float64
target float64
step float64
}
)

func (f *fader) initialize(duration, sampleRate float64) {
delta := f.target - f.current
if duration == 0 || sampleRate == 0 {
f.step = delta
return
}
f.step = delta / (duration * sampleRate)
}

func (f *fader) fade() float64 {
// technically, this case is covered below, but for efficiency we make an early return here
if f.current == f.target {
return f.current
}

f.current += f.step
if (f.current > f.target) == (f.step > 0) {
f.current = f.target
}

return f.current
}
133 changes: 133 additions & 0 deletions module/fader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package module

import (
"testing"
)

func Test_fader_initialize(t *testing.T) {
tests := []struct {
name string
duration float64
sampleRate float64
f *fader
wantStep float64
}{
{
name: "duration is zero",
duration: 0,
sampleRate: 44100,
f: &fader{
current: 440,
target: 330,
},
wantStep: -110,
},
{
name: "sample rate is zero",
duration: 1,
sampleRate: 0,
f: &fader{
current: 440,
target: 330,
},
wantStep: -110,
},
{
name: "both non-zero",
duration: 5,
sampleRate: 2000,
f: &fader{
current: 400,
target: 300,
},
wantStep: -0.01,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.f.initialize(tt.duration, tt.sampleRate)
if tt.f.step != tt.wantStep {
t.Errorf("fader.initialize() step = %v, want %v", tt.f.step, tt.wantStep)
}
})
}
}

func Test_fader_fade(t *testing.T) {
tests := []struct {
name string
f *fader
want float64
}{
{
name: "target equals current",
f: &fader{
current: 400,
target: 400,
step: 10,
},
want: 400,
},
{
name: "current lower than target",
f: &fader{
current: 400,
target: 500,
step: 10,
},
want: 410,
},
{
name: "current higher than target",
f: &fader{
current: 400,
target: 300,
step: -10,
},
want: 390,
},
{
name: "current lower than target but exceeds it after update",
f: &fader{
current: 395,
target: 400,
step: 10,
},
want: 400,
},
{
name: "current higher than target but lower after update",
f: &fader{
current: 305,
target: 300,
step: -10,
},
want: 300,
},
{
name: "current higher than target but step is positive",
f: &fader{
current: 305,
target: 300,
step: 10,
},
want: 300,
},
{
name: "current lower than target but step is negative",
f: &fader{
current: 295,
target: 300,
step: -10,
},
want: 300,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.f.fade(); got != tt.want {
t.Errorf("fader.fade() = %v, want %v", got, tt.want)
}
})
}
}
Loading