synth_blocks 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +40 -0
- data/lib/synth_blocks.rb +24 -0
- data/lib/synth_blocks/core/moog_filter.rb +35 -0
- data/lib/synth_blocks/core/one_pole_lowpass.rb +14 -0
- data/lib/synth_blocks/core/oscillator.rb +36 -0
- data/lib/synth_blocks/core/sound.rb +187 -0
- data/lib/synth_blocks/core/state_variable_filter.rb +41 -0
- data/lib/synth_blocks/core/wave_writer.rb +28 -0
- data/lib/synth_blocks/drum/hihat.rb +54 -0
- data/lib/synth_blocks/drum/kick_drum.rb +54 -0
- data/lib/synth_blocks/drum/snare_drum.rb +87 -0
- data/lib/synth_blocks/drum/tuned_drum.rb +25 -0
- data/lib/synth_blocks/fx/chorus.rb +85 -0
- data/lib/synth_blocks/fx/compressor.rb +121 -0
- data/lib/synth_blocks/fx/delay.rb +42 -0
- data/lib/synth_blocks/fx/eq.rb +92 -0
- data/lib/synth_blocks/fx/g_verb.rb +275 -0
- data/lib/synth_blocks/fx/limiter.rb +15 -0
- data/lib/synth_blocks/fx/waveshaper.rb +24 -0
- data/lib/synth_blocks/mixer/mixer_channel.rb +106 -0
- data/lib/synth_blocks/mixer/send_channel.rb +25 -0
- data/lib/synth_blocks/mod/adsr.rb +83 -0
- data/lib/synth_blocks/mod/envelope.rb +35 -0
- data/lib/synth_blocks/sequencer/sequencer_dsl.rb +219 -0
- data/lib/synth_blocks/synth/monosynth.rb +77 -0
- data/lib/synth_blocks/synth/polysynth.rb +89 -0
- data/lib/synth_blocks/utils.rb +17 -0
- metadata +113 -0
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Synth Blocks
|
2
|
+
|
3
|
+
Synth blocks is a proper extraction of the synthesizer/sequncer I built for my [Ruby Synth](https://rubysynth.fun) talk an Euruko and RubyConf.by
|
4
|
+
|
5
|
+
It is a gem and can be used for writing electronic music in ruby. See [examples](examples/) for more info on how to use this.
|
6
|
+
|
7
|
+
## Blocks
|
8
|
+
|
9
|
+
The code is divided into 7 sections:
|
10
|
+
|
11
|
+
- Core contains things like Oscillators and filters, a base class for all generators as well as a class that uses the wavefile gem to write out sound files
|
12
|
+
- Mod contains modulators, currently two Envelopes
|
13
|
+
- Drum and Synth contains Drum and Synthesizer generators respectively
|
14
|
+
- Fx contains Audio effects such as Reverb, Chorus, Delay, Waveshaper and more
|
15
|
+
- Mixer contains utilities to build a mixing system
|
16
|
+
- Sequencer contains a DSL for building songs
|
17
|
+
|
18
|
+
The code is super unoptimised, as it is written for learning purposes. This means, for example, that the full song contained in [examples/a_song.rb](examples/a_song.rb)
|
19
|
+
takes a couple of hours to render. I'm relatively sure that there are some low hanging fruits for optimisation, especially in the sequencer code that does a lot of
|
20
|
+
useless lookups on quite large data structures that hold the automation data, but I haven't yet gotten around to take a look at it.
|
21
|
+
|
22
|
+
## Examples
|
23
|
+
|
24
|
+
The easiest way to test the example code is to check out the repo and then execute the code directly.
|
25
|
+
|
26
|
+
Each example has a line on the bottom:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
SynthBlocks::Core::WaveWriter.write_if_name_given(out)
|
30
|
+
```
|
31
|
+
|
32
|
+
This will write out a wave file if you run the file with a wav-file as the last argument like so:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
ruby -Ilib examples/waveshaper_demo.rb test.wav
|
36
|
+
```
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
All code here is licensed under the AGPL 3.0 license as documented at [LICENSE](LICENSE) unless stated otherwise.
|
data/lib/synth_blocks.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'synth_blocks/core/sound'
|
2
|
+
require 'synth_blocks/mod/adsr'
|
3
|
+
require 'synth_blocks/fx/chorus'
|
4
|
+
require 'synth_blocks/fx/compressor'
|
5
|
+
require 'synth_blocks/fx/delay'
|
6
|
+
require 'synth_blocks/mod/envelope'
|
7
|
+
require 'synth_blocks/fx/eq'
|
8
|
+
require 'synth_blocks/fx/g_verb'
|
9
|
+
require 'synth_blocks/drum/hihat'
|
10
|
+
require 'synth_blocks/drum/kick_drum'
|
11
|
+
require 'synth_blocks/drum/tuned_drum'
|
12
|
+
require 'synth_blocks/mixer/mixer_channel'
|
13
|
+
require 'synth_blocks/synth/monosynth'
|
14
|
+
require 'synth_blocks/core/moog_filter'
|
15
|
+
require 'synth_blocks/core/oscillator'
|
16
|
+
require 'synth_blocks/synth/polysynth'
|
17
|
+
require 'synth_blocks/mixer/send_channel'
|
18
|
+
require 'synth_blocks/sequencer/sequencer_dsl'
|
19
|
+
require 'synth_blocks/drum/snare_drum'
|
20
|
+
require 'synth_blocks/core/state_variable_filter'
|
21
|
+
require 'synth_blocks/utils'
|
22
|
+
require 'synth_blocks/fx/waveshaper'
|
23
|
+
require 'synth_blocks/fx/limiter'
|
24
|
+
require 'synth_blocks/core/wave_writer'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# A better sounding lowpass (for some applications that is)
|
2
|
+
# Source http://www.musicdsp.org/en/latest/Filters/26-moog-vcf-variation-2.html
|
3
|
+
# Straight port from c++
|
4
|
+
module SynthBlocks
|
5
|
+
module Core
|
6
|
+
class MoogFilter
|
7
|
+
##
|
8
|
+
# create new instance
|
9
|
+
def initialize
|
10
|
+
@in1 = @in2 = @in3 = @in4 = 0
|
11
|
+
@out1 = @out2 = @out3 = @out4 = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# runs the filter on the input value
|
16
|
+
# fc is the cutoff frequency (not in Hz but from 0..1)
|
17
|
+
# res is the resonance from 0..4
|
18
|
+
def run(input, fc, res)
|
19
|
+
f = fc * 1.16;
|
20
|
+
fb = res * (1.0 - 0.15 * f * f);
|
21
|
+
input -= @out4 * fb;
|
22
|
+
input *= 0.35013 * (f*f)*(f*f);
|
23
|
+
@out1 = input + 0.3 * @in1 + (1 - f) * @out1; # Pole 1
|
24
|
+
@in1 = input;
|
25
|
+
@out2 = @out1 + 0.3 * @in2 + (1 - f) * @out2; # Pole 2
|
26
|
+
@in2 = @out1;
|
27
|
+
@out3 = @out2 + 0.3 * @in3 + (1 - f) * @out3; # Pole 3
|
28
|
+
@in3 = @out2;
|
29
|
+
@out4 = @out3 + 0.3 * @in4 + (1 - f) * @out4; # Pole 4
|
30
|
+
@in4 = @out3;
|
31
|
+
return @out4;
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Core
|
3
|
+
class OnePoleLP # :nodoc:
|
4
|
+
def initialize
|
5
|
+
@outputs = 0.0
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(input, cutoff)
|
9
|
+
p = (cutoff * 0.98) * (cutoff * 0.98) * (cutoff * 0.98) * (cutoff * 0.98);
|
10
|
+
@outputs = (1.0 - p) * input + p * @outputs
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Core
|
3
|
+
##
|
4
|
+
# simple oscillator
|
5
|
+
# can currently do squarewave, sawtooth and sine
|
6
|
+
# this oscillator is not bandwidth limited and will thus alias like there's no tomorrow
|
7
|
+
class Oscillator
|
8
|
+
##
|
9
|
+
# Create new oscillator
|
10
|
+
def initialize(sampling_frequency)
|
11
|
+
@sampling_frequency = sampling_frequency.to_f
|
12
|
+
@in_cycle = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# [frequency] Oscillator frequency in Hz (can be altered at any time)
|
16
|
+
# [pulse_width] pulse width, only in effect when creating a square wave
|
17
|
+
# [waveform] can be: :square (default), :sawtooth, :sine
|
18
|
+
def run(frequency, pulse_width: 0.5, waveform: :square)
|
19
|
+
period = @sampling_frequency / frequency.to_f
|
20
|
+
output = 0
|
21
|
+
if waveform == :square
|
22
|
+
output = @in_cycle > pulse_width ? -1.0 : 1.0
|
23
|
+
end
|
24
|
+
if waveform == :sawtooth
|
25
|
+
output = (@in_cycle * 2) - 1.0
|
26
|
+
end
|
27
|
+
if waveform == :sine
|
28
|
+
phase = @in_cycle * 2 * Math::PI
|
29
|
+
output = Math.sin(phase)
|
30
|
+
end
|
31
|
+
@in_cycle = (@in_cycle + (1.0 / period)) % 1.0
|
32
|
+
output
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Core
|
3
|
+
##
|
4
|
+
# Base class for all sound generators and the mixer channels
|
5
|
+
# Handles events (note on / off) and automation
|
6
|
+
# Has two modes, polyphonic and monophonic
|
7
|
+
class Sound
|
8
|
+
# Mode, either :monophonic or :polyphonic
|
9
|
+
attr_accessor :mode
|
10
|
+
|
11
|
+
# run the generator at offset (samples from song start)
|
12
|
+
def run(offset)
|
13
|
+
raise "Base Class, should not be called"
|
14
|
+
end
|
15
|
+
|
16
|
+
def live_params # :nodoc:
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
# this + start time makes it possible to delete events from list
|
21
|
+
#
|
22
|
+
# define this in your generator implementation if your generator has fixed
|
23
|
+
# note lengths not dependent on the note off event
|
24
|
+
# (for example one shot drum hits)
|
25
|
+
def duration(t=0)
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# this + end time makes it possible to delete events from list
|
30
|
+
#
|
31
|
+
# define this in your generator implementation if your generator is dependent
|
32
|
+
# on note off events
|
33
|
+
def release(t=0)
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# create new sound generator instance
|
39
|
+
#
|
40
|
+
# <tt>super(sfreq, mode: $mode)</tt> should be called from the sound generator
|
41
|
+
# implementation initializer.
|
42
|
+
|
43
|
+
def initialize(sfreq, mode: :polyphonic)
|
44
|
+
@mode = mode
|
45
|
+
@sampling_frequency = sfreq.to_f
|
46
|
+
@parameters = {}
|
47
|
+
@events = []
|
48
|
+
@active_events = {}
|
49
|
+
initialize_live_params
|
50
|
+
@prepared = false
|
51
|
+
@sample_duration = 1.0 / @sampling_frequency
|
52
|
+
end
|
53
|
+
|
54
|
+
# create a note on event at time t with note and velocity
|
55
|
+
# [t] time in seconds from song start
|
56
|
+
# [note] MIDI note
|
57
|
+
# [velocity] velocity of note from 0 to 1.0
|
58
|
+
def start(t, note = 36, velocity = 1.0)
|
59
|
+
@events << [t.to_f, :start, note, velocity]
|
60
|
+
end
|
61
|
+
|
62
|
+
# create a note off event at time t with note
|
63
|
+
# [t] time in seconds from song start
|
64
|
+
# [note] MIDI note
|
65
|
+
def stop(t, note = 36)
|
66
|
+
@events << [t.to_f, :stop, note, 0]
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# returns active events at time t
|
71
|
+
def active_events(t)
|
72
|
+
if mode == :polyphonic
|
73
|
+
active_polyphonic_events(t)
|
74
|
+
else
|
75
|
+
active_monophonic_events(t)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# sets a parameter to a specific value at a given time.
|
81
|
+
# you can interpolate linearly between two points by setting to value A
|
82
|
+
# then setting value B at a later point in time with type: linear
|
83
|
+
# TODO: implement quadratic interpolation
|
84
|
+
#
|
85
|
+
# Note: this does no sanity checking, so please make sure you set events
|
86
|
+
# in the correct order etc.
|
87
|
+
# [parameter] parameter name
|
88
|
+
# [time] time in seconds from song start
|
89
|
+
# [value] value of the parameter you want to get to
|
90
|
+
# [type] either :set or :linear
|
91
|
+
|
92
|
+
def set(parameter, time, value, type: :set)
|
93
|
+
@parameters[parameter] ||= []
|
94
|
+
@parameters[parameter] << [time, value, type]
|
95
|
+
@parameters[parameter].sort_by! { |item| item.first }
|
96
|
+
end
|
97
|
+
|
98
|
+
# get the exact parameter value including interpolation
|
99
|
+
# [parameter] parameter name
|
100
|
+
# [time] time of from where you want the value
|
101
|
+
def get(parameter, time)
|
102
|
+
return nil if @parameters[parameter].nil?
|
103
|
+
return nil if @parameters[parameter].first.first > time
|
104
|
+
reverse_list = @parameters[parameter].reverse
|
105
|
+
reverse_list.each_with_index do |entry, index|
|
106
|
+
return entry[1] if entry.first <= time
|
107
|
+
if entry.first >= time && entry[2] == :linear
|
108
|
+
if reverse_list[index + 1].nil?
|
109
|
+
return nil
|
110
|
+
end
|
111
|
+
lin_time_start = reverse_list[index + 1][0]
|
112
|
+
lin_value_start = reverse_list[index + 1][1]
|
113
|
+
value_diff = entry[1] - lin_value_start
|
114
|
+
time_diff = entry[0] - lin_time_start
|
115
|
+
return value_diff / time_diff * (time - lin_time_start)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def initialize_live_params
|
123
|
+
live_params.each do |p|
|
124
|
+
set(p, 0, @preset[p], type: :set)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def time(offset)
|
129
|
+
offset.to_f / @sampling_frequency
|
130
|
+
end
|
131
|
+
|
132
|
+
def frequency(note)
|
133
|
+
(2.0 ** ((note.to_f - 69.0) / 12.0)) * 440.0
|
134
|
+
end
|
135
|
+
|
136
|
+
def prepare
|
137
|
+
return if @prepared
|
138
|
+
@events.sort_by! { |item| item.first }
|
139
|
+
@prepared = true
|
140
|
+
end
|
141
|
+
|
142
|
+
def filter_done_events(t)
|
143
|
+
return if duration(t).nil? && release(t).nil? # sound subclass needs to implement this to work
|
144
|
+
@active_events.reject! do |note, event|
|
145
|
+
# one shots with duration
|
146
|
+
duration(t) && event[:started] + duration(t) <= t ||
|
147
|
+
# stopped with release time
|
148
|
+
release(t) && event[:stopped] && event[:stopped] + release(t) <= t
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# returns correct events for a monophonic synth with proper not priority.
|
153
|
+
|
154
|
+
def active_monophonic_events(t)
|
155
|
+
active_polyphonic_events(t)
|
156
|
+
non_stopped = @active_events.select { |note, event| event[:stopped].nil? }
|
157
|
+
unless non_stopped.empty?
|
158
|
+
return Hash[[non_stopped.sort_by{|note, event| event[:started] }.last]]
|
159
|
+
end
|
160
|
+
stopped = @active_events.sort_by{|note, event| event[:stopped]}.last
|
161
|
+
if stopped
|
162
|
+
Hash[[stopped]]
|
163
|
+
else
|
164
|
+
{}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def active_polyphonic_events(t)
|
169
|
+
t = t.to_f
|
170
|
+
prepare
|
171
|
+
@events.each_with_index do |event|
|
172
|
+
# let's look at the smallest interval possible
|
173
|
+
# pp [t.to_f, t.to_f + (@sample_duration * 2)]
|
174
|
+
next if event.first < t
|
175
|
+
break if event.first > t + (@sample_duration * 2)
|
176
|
+
if event[1] == :start
|
177
|
+
@active_events[event[2]] = {started: event[0], velocity: event[3]}
|
178
|
+
elsif event[1] == :stop
|
179
|
+
@active_events[event[2]][:stopped] = event[0] if @active_events[event[2]]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
filter_done_events(t)
|
183
|
+
@active_events
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Core
|
3
|
+
# Simple State Variable filter
|
4
|
+
#
|
5
|
+
# source: http://www.musicdsp.org/en/latest/Filters/23-state-variable.html
|
6
|
+
# More info: https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/
|
7
|
+
class StateVariableFilter
|
8
|
+
|
9
|
+
##
|
10
|
+
# Create new filter instance
|
11
|
+
def initialize(sfreq)
|
12
|
+
@sampling_frequency = sfreq.to_f
|
13
|
+
@delay_1 = 0.0
|
14
|
+
@delay_2 = 0.0
|
15
|
+
end
|
16
|
+
|
17
|
+
# run the filter from input value
|
18
|
+
# [frequency] cutoff freq in Hz
|
19
|
+
# [q] resonance, from 0 to ...
|
20
|
+
# [type] can be :lowpass, :highpass, :bandpass and :notch
|
21
|
+
def run(input, frequency, q, type: :lowpass)
|
22
|
+
# derived parameters
|
23
|
+
q1 = 1.0 / q.to_f
|
24
|
+
f1 = 2.0 * Math::PI * frequency.to_f / @sampling_frequency
|
25
|
+
|
26
|
+
# calculate filters
|
27
|
+
lowpass = @delay_2 + f1 * @delay_1
|
28
|
+
highpass = input - lowpass - q1 * @delay_1
|
29
|
+
bandpass = f1 * highpass + @delay_1
|
30
|
+
notch = highpass + lowpass
|
31
|
+
|
32
|
+
# store delays
|
33
|
+
@delay_1 = bandpass
|
34
|
+
@delay_2 = lowpass
|
35
|
+
|
36
|
+
results = { lowpass: lowpass, highpass: highpass, bandpass: bandpass, notch: notch }
|
37
|
+
results[type]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'wavefile'
|
2
|
+
|
3
|
+
module SynthBlocks
|
4
|
+
module Core
|
5
|
+
##
|
6
|
+
# Writes Floating point data to a wavefile using the wave file gem
|
7
|
+
class WaveWriter
|
8
|
+
##
|
9
|
+
# Static method to write to file given as first argument IF given
|
10
|
+
def self.write_if_name_given(samples)
|
11
|
+
if (ARGV[0])
|
12
|
+
WaveWriter.new(ARGV[0]).write(samples)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(filename)
|
17
|
+
@filename = filename
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(float_data)
|
21
|
+
buffer = WaveFile::Buffer.new(float_data, WaveFile::Format.new(:mono, :float, 44100))
|
22
|
+
WaveFile::Writer.new(@filename, WaveFile::Format.new(:mono, :pcm_16, 44100)) do |writer|
|
23
|
+
writer.write(buffer)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'synth_blocks/core/state_variable_filter'
|
2
|
+
require 'synth_blocks/mod/envelope'
|
3
|
+
|
4
|
+
module SynthBlocks
|
5
|
+
module Drum
|
6
|
+
##
|
7
|
+
# Simple Hihat generator
|
8
|
+
# Nois > Filter > Amp
|
9
|
+
class Hihat < SynthBlocks::Core::Sound
|
10
|
+
##
|
11
|
+
# === parameters
|
12
|
+
# flt_frequency - center frequency of the bandpass filter
|
13
|
+
#
|
14
|
+
# flt_Q - Q (resonance) value of the bandpass filter
|
15
|
+
#
|
16
|
+
# amp_attack - attack time in seconds
|
17
|
+
#
|
18
|
+
# amp_decay - decay time in seconds
|
19
|
+
#
|
20
|
+
def initialize(sfreq, preset = {})
|
21
|
+
super(sfreq, mode: :polyphonic)
|
22
|
+
@filter = SynthBlocks::Core::StateVariableFilter.new(sfreq)
|
23
|
+
@preset = {
|
24
|
+
flt_frequency: 10000,
|
25
|
+
flt_Q: 2,
|
26
|
+
amp_attack: 0.001,
|
27
|
+
amp_decay: 0.1,
|
28
|
+
}.merge(preset)
|
29
|
+
@amp_env = SynthBlocks::Mod::Envelope.new(@preset[:amp_attack], @preset[:amp_decay])
|
30
|
+
end
|
31
|
+
|
32
|
+
def duration(_) # :nodoc:
|
33
|
+
@preset[:amp_attack] + @preset[:amp_decay]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Run the generator (offset is given in samples)
|
37
|
+
def run(offset)
|
38
|
+
# time in seconds
|
39
|
+
t = time(offset)
|
40
|
+
events = active_events(t)
|
41
|
+
if events.empty?
|
42
|
+
0.0
|
43
|
+
else
|
44
|
+
event = events[events.keys.last]
|
45
|
+
# lfo_out = (@lfo.run(@preset[:lfo_frequency], waveform: @preset[:lfo_waveform]) + 1) / 8 + 0.5
|
46
|
+
local_started = t - event[:started]
|
47
|
+
noise_out = rand * 2.0 - 1.0
|
48
|
+
noise_out = @filter.run(noise_out, @preset[:flt_frequency], @preset[:flt_Q], type: :bandpass)
|
49
|
+
0.3 * noise_out * @amp_env.run(local_started)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|