Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sequence module #77

Merged
merged 11 commits into from
Jan 18, 2025
Merged
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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
go-version: '1.23'

- name: Test
run: make test
48 changes: 31 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ Run `synth -h` to see all configuration options.

### Data types

| Synth | | |
| ----------- | ----------------- | -------------------------------------------------- |
| **Field** | **Type** | **Description** |
| vol | Float | main volume in range [0,2] |
| out | String [0..*] | names of all modules whose outputs will be audible |
| time | Float | initial time shift in seconds [0,7200] |
| oscillators | Oscillator [0..*] | all oscillators |
| noises | Noise [0..*] | all noise generators |
| wavetables | Wavetables [0..*] | all wavetables |
| samplers | Sampler[0..*] | all samplers |
| Synth | | |
| ----------- | ---------------- | -------------------------------------------------- |
| **Field** | **Type** | **Description** |
| vol | Float | main volume in range [0,2] |
| out | String[0..*] | names of all modules whose outputs will be audible |
| time | Float | initial time shift in seconds [0,7200] |
| oscillators | Oscillator[0..*] | all oscillators |
| noises | Noise[0..*] | all noise generators |
| wavetables | Wavetables[0..*] | all wavetables |
| samplers | Sampler[0..*] | all samplers |

| Oscillator | | |
| ---------- | -------------- | --------------------------------------------------------------- |
Expand Down Expand Up @@ -97,7 +97,7 @@ Run `synth -h` to see all configuration options.
| amp | Input | amplitude in range [0,2] |
| pan | Input | stereo balance in range [-1,1] |
| freq | Input | periods per second [0,20000] |
| table | Float [0..*] | output values |
| table | Float[0..*] | output values |
| filters | String[0..*] | names of the filters to apply |
| envelope | Envelope | envelope to apply; if omitted, wavetable will constantly sound |

Expand All @@ -114,6 +114,20 @@ Run `synth -h` to see all configuration options.

A sampler periodically samples the output values of the given inputs and outputs their sum.

| Sequence | | |
| --------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Field** | **Type** | **Description** |
| name | String | should be unique in the scope of the file |
| amp | Input | amplitude in range [0,2] |
| pan | Input | stereo balance in range [-1,1] |
| type | OscillatorType | wave form |
| sequence | String[0..*] | a sequence of notes written in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation) |
| randomize | Boolean | if true, the notes of the sequence will be played in random order |
| pitch | Float | standard pitch in hz [400,500] |
| transpose | Input | transposition in semitones [-24,24] |
| filters | String[0..*] | names of the filters to apply |
| envelope | Envelope | envelope to apply; if omitted, first note of sequence will constantly sound |

| Filter | | |
| ----------- | -------- | ---------------------------------------------------------- |
| **Field** | **Type** | **Description** |
Expand All @@ -137,12 +151,12 @@ transitioning at the `low-cutoff` frequency. If both cutoff frequencies are defi
| bpm | Input | triggers per minute [0,600000] |
| time-shift | Float | initial time shift |

| Input | | |
| --------- | ------------- | ----------------------------------------------------------------------- |
| **Field** | **Type** | **Description** |
| val | Float | initial value of the respective parameter |
| mod | String [0..*] | names of modulating modules (oscillators, samplers, wavetables, noises) |
| mod-amp | Float | amplitude of the modulation in range [0,1] |
| Input | | |
| --------- | ------------ | ----------------------------------------------------------------------- |
| **Field** | **Type** | **Description** |
| val | Float | initial value of the respective parameter |
| mod | String[0..*] | names of modulating modules (oscillators, samplers, wavetables, noises) |
| mod-amp | Float | amplitude of the modulation in range [0,1] |

## Example patch file

Expand Down
75 changes: 75 additions & 0 deletions examples/sequence.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
vol: 1
out: [s1, s2, s3]

oscillators:
- name: lfo1
type: Sine
freq: {val: 2.5}
amp: {val: 1}
envelope:
attack: {val: 0.1}
decay: {val: 0.1}
sustain: {val: 1}
release: {val: 1.6}
peak: {val: 1}
sustain-level: {val: 0.6}
bpm: {val: 20}
time-shift:

sequences:
- name: s1
amp: {val: 0.2}
type: Sine
sequence: [a_4, c_5, b_4, f_4, d_4]
pan: {}
filters: []
pitch: 440
transpose: {mod: [lfo1], mod-amp: 0.1}
randomize:
envelope:
attack: {val: 0.1}
decay: {val: 0.1}
sustain: {val: 1}
release: {val: 1.6}
peak: {val: 1}
sustain-level: {val: 0.6}
bpm: {val: 20}
time-shift:

- name: s2
amp: {val: 0.2}
type: Sine
sequence: [a_3, c_4, b_3, f_3, d_3, e_3, g#_3]
pan: {}
filters: []
pitch: 440
transpose: {mod: [lfo1], mod-amp: 0.15}
randomize: true
envelope:
attack: {val: 0.1}
decay: {val: 0.1}
sustain: {val: 1}
release: {val: 1.6}
peak: {val: 1}
sustain-level: {val: 0.6}
bpm: {val: 20}
time-shift: -0.03

- name: s3
amp: {val: 0.2}
type: Sine
sequence: [a_2, c_3, b_2, f_2, d_2, e_2]
pan: {}
filters: []
pitch: 440
transpose: {mod: [lfo1], mod-amp: 0.15}
randomize: true
envelope:
attack: {val: 0.1}
decay: {val: 0.1}
sustain: {val: 1}
release: {val: 1.6}
peak: {val: 1}
sustain-level: {val: 0.6}
bpm: {val: 20}
time-shift: 0.05
19 changes: 13 additions & 6 deletions module/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ type Envelope struct {
TimeShift float64 `yaml:"time-shift"`
BPM Input `yaml:"bpm"`
current float64
lastTriggeredAt *float64
currentBPM float64
currentConfig envelopeConfig
lastTriggeredAt *float64
triggered bool
}

type envelopeConfig struct {
Expand All @@ -34,8 +36,8 @@ func (e *Envelope) Initialize() {
}

func (e *Envelope) Next(t float64, modMap ModulesMap) {
bpm := modulate(e.BPM, bpmLimits, modMap)
e.trigger(t, bpm, modMap)
e.currentBPM = modulate(e.BPM, bpmLimits, modMap)
e.trigger(t, modMap)
y := e.getCurrentValue(t)
e.current = y
}
Expand All @@ -60,11 +62,14 @@ func (e *Envelope) getCurrentConfig(t float64, modMap ModulesMap) {
e.currentConfig = config
}

func (e *Envelope) trigger(t, bpm float64, modMap ModulesMap) {
if bpm == 0 {
func (e *Envelope) trigger(t float64, modMap ModulesMap) {
e.triggered = false

if e.currentBPM == 0 {
return
}
secondsBetweenTwoBeats := 60 / bpm

secondsBetweenTwoBeats := 60 / e.currentBPM
var triggerAt float64
if t >= e.TimeShift {
numberOfTriggersMinusOne := math.Floor((t - e.TimeShift) / secondsBetweenTwoBeats)
Expand All @@ -76,12 +81,14 @@ func (e *Envelope) trigger(t, bpm float64, modMap ModulesMap) {

oldLastTriggeredAt := e.lastTriggeredAt
if oldLastTriggeredAt == nil {
e.triggered = true
e.lastTriggeredAt = &triggerAt
e.getCurrentConfig(t, modMap)
return
}

if t-*e.lastTriggeredAt >= secondsBetweenTwoBeats {
e.triggered = true
newLastTriggeredAt := t
e.lastTriggeredAt = &newLastTriggeredAt
e.getCurrentConfig(t, modMap)
Expand Down
91 changes: 55 additions & 36 deletions module/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,77 @@ func TestEnvelope_trigger(t *testing.T) {
want float64
}{
{
name: "no time shift must trigger at 0",
envelope: Envelope{},
t: 0,
bpm: 10,
want: 0,
name: "no time shift must trigger at 0",
envelope: Envelope{
currentBPM: 10,
},
t: 0,
bpm: 10,
want: 0,
},
{
name: "no time shift must trigger at multiples of seconds between two beats",
envelope: Envelope{},
t: 13,
bpm: 10,
want: 12,
name: "no time shift must trigger at multiples of seconds between two beats",
envelope: Envelope{
currentBPM: 10,
},
t: 13,
bpm: 10,
want: 12,
},
{
name: "with time shift could first trigger at negative time",
envelope: Envelope{TimeShift: 10},
t: 0,
bpm: 10,
want: -2,
name: "with time shift could first trigger at negative time",
envelope: Envelope{
currentBPM: 10,
TimeShift: 10,
},
t: 0,
bpm: 10,
want: -2,
},
{
name: "with time shift must trigger at multiples of seconds between two beats plus time shift",
envelope: Envelope{TimeShift: 10},
t: 18,
bpm: 10,
want: 16,
name: "with time shift must trigger at multiples of seconds between two beats plus time shift",
envelope: Envelope{
currentBPM: 10,
TimeShift: 10,
},
t: 18,
bpm: 10,
want: 16,
},
{
name: "negative time shift also works",
envelope: Envelope{TimeShift: -3},
t: 4,
bpm: 10,
want: 3,
name: "negative time shift also works",
envelope: Envelope{
currentBPM: 10,
TimeShift: -3,
},
t: 4,
bpm: 10,
want: 3,
},
{
name: "change from negative triggered time to positive",
envelope: Envelope{lastTriggeredAt: pointer(-1.0), TimeShift: 5},
t: 5,
bpm: 10,
want: 5,
name: "change from negative triggered time to positive",
envelope: Envelope{
lastTriggeredAt: pointer(-1.0),
currentBPM: 10,
TimeShift: 5,
},
t: 5,
bpm: 10,
want: 5,
},
{
name: "no trigger if t is closer to last trigger than seconds between two beats",
envelope: Envelope{lastTriggeredAt: pointer(12.0)},
t: 17,
bpm: 10,
want: 12,
name: "no trigger if t is closer to last trigger than seconds between two beats",
envelope: Envelope{
lastTriggeredAt: pointer(12.0),
currentBPM: 10,
},
t: 17,
want: 12,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.envelope.trigger(tt.t, tt.bpm, make(ModulesMap))
tt.envelope.trigger(tt.t, make(ModulesMap))
if got := tt.envelope.lastTriggeredAt; *got != tt.want {
t.Errorf("Envelope.trigger() got %f, want %f", *got, tt.want)
}
Expand Down
16 changes: 9 additions & 7 deletions module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ type output struct {
}

var (
ampLimits limits = limits{min: 0, max: 2}
panLimits limits = limits{min: -1, max: 1}
phaseLimits limits = limits{min: -1, max: 1}
freqLimits limits = limits{min: 0, max: 20000}
cutoffLimits limits = limits{min: 1, max: 20000}
bpmLimits limits = limits{min: 0, max: 600000}
envelopeLimits limits = limits{min: 0, max: 10000}
ampLimits limits = limits{min: 0, max: 2}
bpmLimits limits = limits{min: 0, max: 600000}
cutoffLimits limits = limits{min: 1, max: 20000}
envelopeLimits limits = limits{min: 0, max: 10000}
freqLimits limits = limits{min: 0, max: 20000}
panLimits limits = limits{min: -1, max: 1}
phaseLimits limits = limits{min: -1, max: 1}
pitchLimits limits = limits{min: 400, max: 500}
transposeLimits limits = limits{min: -24, max: 24}
)

func modulateValue(modulators []string, modMap ModulesMap) float64 {
Expand Down
Loading
Loading