synth_blocks 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|