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
@@ -0,0 +1,275 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Fx
|
3
|
+
# Direct port from GVerb
|
4
|
+
# Source: https://github.com/swh/lv2/blob/master/gverb/gverbdsp.c
|
5
|
+
# (and other files from that repo)
|
6
|
+
#
|
7
|
+
# Here's the original (c) notice from https://github.com/swh/lv2/blob/master/gverb/gverbdsp.c
|
8
|
+
#
|
9
|
+
# Copyright (C) 1999 Juhana Sadeharju
|
10
|
+
# kouhia at nic.funet.fi
|
11
|
+
# This program is free software; you can redistribute it and/or modify
|
12
|
+
# it under the terms of the GNU General Public License as published by
|
13
|
+
# the Free Software Foundation; either version 2 of the License, or
|
14
|
+
# (at your option) any later version.
|
15
|
+
# This program is distributed in the hope that it will be useful,
|
16
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
+
# GNU General Public License for more details.
|
19
|
+
# You should have received a copy of the GNU General Public License
|
20
|
+
# along with this program; if not, write to the Free Software
|
21
|
+
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
22
|
+
#
|
23
|
+
|
24
|
+
require 'prime'
|
25
|
+
|
26
|
+
class FixedDelay # :nodoc:
|
27
|
+
def initialize(size)
|
28
|
+
@size = size
|
29
|
+
@buf = Array.new(size)
|
30
|
+
@idx = 0
|
31
|
+
@buf = @buf.map { |e| 0.0 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def read(n)
|
35
|
+
i = (@idx - n + @size) % @size;
|
36
|
+
@buf[i]
|
37
|
+
end
|
38
|
+
|
39
|
+
def write(x)
|
40
|
+
@buf[@idx] = x
|
41
|
+
@idx = (@idx + 1) % @size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Damper # :nodoc:
|
46
|
+
def initialize(damping)
|
47
|
+
@damping = damping
|
48
|
+
@delay = 0.0
|
49
|
+
end
|
50
|
+
|
51
|
+
def run(x)
|
52
|
+
y = x * (1.0-@damping) + @delay * @damping;
|
53
|
+
@delay = y
|
54
|
+
y
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Diffuser # :nodoc:
|
59
|
+
def initialize(size, coeff)
|
60
|
+
@size = size.floor
|
61
|
+
@coeff = coeff
|
62
|
+
@idx = 0
|
63
|
+
@buf = Array.new(@size)
|
64
|
+
@buf = @buf.map { |e| 0.0 }
|
65
|
+
end
|
66
|
+
|
67
|
+
def run(x)
|
68
|
+
w = x - @buf[@idx] * @coeff;
|
69
|
+
y = @buf[@idx] + w * @coeff;
|
70
|
+
@buf[@idx] = w
|
71
|
+
@idx = (@idx + 1) % @size;
|
72
|
+
y
|
73
|
+
end
|
74
|
+
end
|
75
|
+
##
|
76
|
+
# GVerb is a relatively simple reverb implementation
|
77
|
+
class GVerb
|
78
|
+
FDNORDER = 4 # :nodoc:
|
79
|
+
|
80
|
+
|
81
|
+
##
|
82
|
+
# Create new GVerb instance
|
83
|
+
#
|
84
|
+
# max_room_size is the maximum room size you'll use
|
85
|
+
#
|
86
|
+
# room_size is the current room size
|
87
|
+
#
|
88
|
+
def initialize(srate, max_room_size: 120.0, room_size: 50.0, rev_time: 2.0, damping: 0.3, spread: 15.0, input_bandwidth: 1.5, early_level: 0.8, tail_level: 0.5, mix: 0.2)
|
89
|
+
@rate = srate
|
90
|
+
@damping = damping
|
91
|
+
@max_room_size = max_room_size
|
92
|
+
@room_size = room_size
|
93
|
+
@rev_time = rev_time
|
94
|
+
@early_level = early_level
|
95
|
+
@tail_level = tail_level
|
96
|
+
@mix = mix
|
97
|
+
@max_delay = @rate * @max_room_size / 340.0
|
98
|
+
@largest_delay = @rate * @room_size / 340.0
|
99
|
+
@input_bandwidth = input_bandwidth;
|
100
|
+
@input_damper = Damper.new(1.0 - @input_bandwidth)
|
101
|
+
|
102
|
+
|
103
|
+
@fdndels = FDNORDER.times.map do |i|
|
104
|
+
FixedDelay.new(@max_delay + 1000)
|
105
|
+
end
|
106
|
+
@fdngains = Array.new(FDNORDER)
|
107
|
+
@fdnlens = Array.new(FDNORDER)
|
108
|
+
|
109
|
+
@fdndamps = FDNORDER.times.map do |i|
|
110
|
+
Damper.new(@damping)
|
111
|
+
end
|
112
|
+
|
113
|
+
ga = 60.0;
|
114
|
+
gt = @rev_time;
|
115
|
+
ga = 10.0 ** (-ga / 20.0)
|
116
|
+
n = @rate * gt
|
117
|
+
@alpha = ga ** (1.0 / n)
|
118
|
+
gb = 0.0;
|
119
|
+
FDNORDER.times do |i|
|
120
|
+
gb = 1.000000*@largest_delay if (i == 0)
|
121
|
+
gb = 0.816490*@largest_delay if (i == 1)
|
122
|
+
gb = 0.707100*@largest_delay if (i == 2)
|
123
|
+
gb = 0.632450*@largest_delay if (i == 3)
|
124
|
+
|
125
|
+
@fdnlens[i] = nearest_prime(gb, 0.5);
|
126
|
+
@fdnlens[i] = gb.round;
|
127
|
+
@fdngains[i] = -(@alpha ** @fdnlens[i])
|
128
|
+
end
|
129
|
+
|
130
|
+
@d = Array.new(FDNORDER)
|
131
|
+
@u = Array.new(FDNORDER)
|
132
|
+
@f = Array.new(FDNORDER)
|
133
|
+
|
134
|
+
# DIFFUSER SECTION
|
135
|
+
|
136
|
+
diffscale = @fdnlens[3].to_f/(210+159+562+410);
|
137
|
+
spread1 = spread.to_f
|
138
|
+
spread2 = 3.0*spread
|
139
|
+
|
140
|
+
b = 210
|
141
|
+
r = 0.125541
|
142
|
+
a = spread1*r
|
143
|
+
c = 210+159+a
|
144
|
+
cc = c-b
|
145
|
+
r = 0.854046
|
146
|
+
a = spread2*r
|
147
|
+
d = 210+159+562+a
|
148
|
+
dd = d-c
|
149
|
+
e = 1341-d
|
150
|
+
|
151
|
+
@ldifs = [
|
152
|
+
Diffuser.new((diffscale*b),0.75),
|
153
|
+
Diffuser.new((diffscale*cc),0.75),
|
154
|
+
Diffuser.new((diffscale*dd),0.625),
|
155
|
+
Diffuser.new((diffscale*e),0.625)
|
156
|
+
]
|
157
|
+
|
158
|
+
b = 210
|
159
|
+
r = -0.568366
|
160
|
+
a = spread1*r
|
161
|
+
c = 210+159+a
|
162
|
+
cc = c-b
|
163
|
+
r = -0.126815;
|
164
|
+
a = spread2*r
|
165
|
+
d = 210+159+562+a
|
166
|
+
dd = d-c
|
167
|
+
e = 1341-d
|
168
|
+
|
169
|
+
@rdifs = [
|
170
|
+
Diffuser.new((diffscale*b),0.75),
|
171
|
+
Diffuser.new((diffscale*cc),0.75),
|
172
|
+
Diffuser.new((diffscale*dd),0.625),
|
173
|
+
Diffuser.new((diffscale*e),0.625)
|
174
|
+
]
|
175
|
+
|
176
|
+
|
177
|
+
# Tapped delay section */
|
178
|
+
|
179
|
+
@tapdelay = FixedDelay.new(44000)
|
180
|
+
@taps = Array.new(FDNORDER)
|
181
|
+
@tapgains = Array.new(FDNORDER)
|
182
|
+
|
183
|
+
@taps[0] = 5+0.410*@largest_delay
|
184
|
+
@taps[1] = 5+0.300*@largest_delay
|
185
|
+
@taps[2] = 5+0.155*@largest_delay
|
186
|
+
@taps[3] = 5+0.000*@largest_delay
|
187
|
+
|
188
|
+
FDNORDER.times do |i|
|
189
|
+
@tapgains[i] = @alpha ** @taps[i]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
##
|
195
|
+
# runs a value through the reverb, returns the reverberated signal
|
196
|
+
# mixed with the original.
|
197
|
+
def run(x)
|
198
|
+
if x.nan? || x.abs > 100000.0
|
199
|
+
x = 0.0
|
200
|
+
end
|
201
|
+
|
202
|
+
z = @input_damper.run(x)
|
203
|
+
z = @ldifs[0].run(z)
|
204
|
+
FDNORDER.times do |i|
|
205
|
+
@u[i] = @tapgains[i] * @tapdelay.read(@taps[i])
|
206
|
+
end
|
207
|
+
|
208
|
+
@tapdelay.write(z)
|
209
|
+
|
210
|
+
FDNORDER.times do |i|
|
211
|
+
@d[i] = @fdndamps[i].run(@fdngains[i] * @fdndels[i].read(@fdnlens[i]))
|
212
|
+
end
|
213
|
+
|
214
|
+
sum = 0.0
|
215
|
+
sign = 1.0
|
216
|
+
FDNORDER.times do |i|
|
217
|
+
sum += sign * (@tail_level * @d[i] + @early_level * @u[i])
|
218
|
+
sign = -sign
|
219
|
+
end
|
220
|
+
|
221
|
+
sum += x* @early_level
|
222
|
+
|
223
|
+
lsum = sum
|
224
|
+
# rsum = sum
|
225
|
+
|
226
|
+
@f = fdn_matrix(@d)
|
227
|
+
|
228
|
+
FDNORDER.times do |i|
|
229
|
+
@fdndels[i].write(@u[i] + @f[i])
|
230
|
+
end
|
231
|
+
|
232
|
+
lsum = @ldifs[1].run(lsum)
|
233
|
+
lsum = @ldifs[2].run(lsum)
|
234
|
+
lsum = @ldifs[3].run(lsum)
|
235
|
+
|
236
|
+
# rsum = @rdifs[1].run(rsum)
|
237
|
+
# rsum = @rdifs[2].run(rsum)
|
238
|
+
# rsum = @rdifs[3].run(rsum)
|
239
|
+
|
240
|
+
lsum = x * (1.0 - @mix) + lsum * @mix
|
241
|
+
# rsum = x * (1.0 - mix) + rsum * mix
|
242
|
+
return lsum
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def nearest_prime(n_f, rerror)
|
249
|
+
n = n_f.to_i
|
250
|
+
return n if Prime.prime?(n)
|
251
|
+
# assume n is large enough and n*rerror enough smaller than n */
|
252
|
+
bound = n*rerror;
|
253
|
+
1.upto(bound) do |k|
|
254
|
+
return n+k if Prime.prime?(n+k)
|
255
|
+
return n-k if Prime.prime?(n-k)
|
256
|
+
end
|
257
|
+
return -1
|
258
|
+
end
|
259
|
+
|
260
|
+
def fdn_matrix(a)
|
261
|
+
b = Array.new(FDNORDER)
|
262
|
+
dl0 = a[0]
|
263
|
+
dl1 = a[1]
|
264
|
+
dl2 = a[2]
|
265
|
+
dl3 = a[3]
|
266
|
+
|
267
|
+
b[0] = 0.5*(dl0 + dl1 - dl2 - dl3);
|
268
|
+
b[1] = 0.5*(dl0 - dl1 - dl2 + dl3);
|
269
|
+
b[2] = 0.5*(-dl0 + dl1 - dl2 + dl3);
|
270
|
+
b[3] = 0.5*(dl0 + dl1 + dl2 + dl3);
|
271
|
+
b
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Fx
|
3
|
+
##
|
4
|
+
# Simple soft limiter
|
5
|
+
# Taken from https://github.com/pichenettes/stmlib/blob/448babb082dfe7b0a1ffbf0b349eefde64691b49/dsp/dsp.h#L97
|
6
|
+
class Limiter
|
7
|
+
|
8
|
+
##
|
9
|
+
# run limiter
|
10
|
+
def run(x)
|
11
|
+
x * (27.0 + x * x) / (27.0 + 9.0 * x * x)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SynthBlocks
|
2
|
+
module Fx
|
3
|
+
##
|
4
|
+
# waveshaper, source http://www.musicdsp.org/en/latest/Effects/41-waveshaper.html
|
5
|
+
# amount can go from 1 to ... oo
|
6
|
+
# the higher a the stronger is the distortion
|
7
|
+
class Waveshaper
|
8
|
+
##
|
9
|
+
# Waveshaper amount
|
10
|
+
attr_reader :amount
|
11
|
+
##
|
12
|
+
# Create waveshaper instance
|
13
|
+
# [amount] Amount can be from 0 to oo
|
14
|
+
def initialize(amount)
|
15
|
+
@amount = amount
|
16
|
+
end
|
17
|
+
##
|
18
|
+
# run waveshaper
|
19
|
+
def run(input)
|
20
|
+
input * (input.abs + amount) / (input ** 2 + (amount - 1) * input.abs + 1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'synth_blocks/core/sound'
|
2
|
+
|
3
|
+
module SynthBlocks
|
4
|
+
module Mixer
|
5
|
+
##
|
6
|
+
# Emulation of a mixer channel on a mixing desk
|
7
|
+
# has a built in EQ, compressor and ducker, can run an arbitrary
|
8
|
+
# number of insert effects and send channels
|
9
|
+
class MixerChannel < SynthBlocks::Core::Sound
|
10
|
+
##
|
11
|
+
# These params can be automated
|
12
|
+
LIVE_PARAMS = [:volume, :eq_low_gain, :eq_high_gain, :eq_mid_gain]
|
13
|
+
attr_accessor :preset # :nodoc:
|
14
|
+
|
15
|
+
def live_params # :nodoc:
|
16
|
+
LIVE_PARAMS
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# - source - Source sound generator
|
21
|
+
# - insert_effects - Array of effects instances
|
22
|
+
# - sends - Array of send values
|
23
|
+
# === Parameters
|
24
|
+
# - volume - channel volume
|
25
|
+
# - eq_low_freq, eq_high_freq - shelving frequencies for equalizer
|
26
|
+
# - eq_low_gain, eq_mid_gain, eq_high_gain - Equalizer gains per band
|
27
|
+
# - comp_threshold, comp_ratio, comp_attack, comp_release - Compressor params
|
28
|
+
# - duck - Duck amount (0-1)
|
29
|
+
# - duck attack, duck_release - Ducker envelope params in s
|
30
|
+
def initialize(srate, source, insert_effects: [], sends: [], preset: {})
|
31
|
+
@source = source
|
32
|
+
@insert_effects = insert_effects
|
33
|
+
@sends = sends
|
34
|
+
@preset = {
|
35
|
+
volume: 0.2,
|
36
|
+
eq_low_freq: 880,
|
37
|
+
eq_high_freq: 5000,
|
38
|
+
eq_low_gain: 1.0,
|
39
|
+
eq_mid_gain: 1.0,
|
40
|
+
eq_high_gain: 1.0,
|
41
|
+
comp_threshold: -50.0,
|
42
|
+
comp_ratio: 0.4,
|
43
|
+
comp_attack: 80.0,
|
44
|
+
comp_release: 200.0,
|
45
|
+
duck: 0.0,
|
46
|
+
duck_attack: 0.01,
|
47
|
+
duck_release: 0.5
|
48
|
+
}.merge(preset)
|
49
|
+
|
50
|
+
super(srate)
|
51
|
+
@ducks = []
|
52
|
+
@duck_env = SynthBlocks::Mod::Envelope.new(@preset[:duck_attack], @preset[:duck_release])
|
53
|
+
@eq = SynthBlocks::Fx::Eq.new(srate, lowfreq: @preset[:eq_low_freq], highfreq: @preset[:eq_high_freq])
|
54
|
+
@compressor = SynthBlocks::Fx::Compressor.new(srate, attack: @preset[:comp_attack], release: @preset[:comp_release], ratio: @preset[:comp_ratio], threshold: @preset[:comp_threshold])
|
55
|
+
update_live_params(0)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Schedule ducking at time t (in seconds)
|
60
|
+
def duck(t)
|
61
|
+
@ducks << t
|
62
|
+
@ducks.sort
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# returns send portion of output signal for send index
|
67
|
+
def send(index)
|
68
|
+
@output * (@sends[index] || 0.0)
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# runs channel
|
73
|
+
def run(offset)
|
74
|
+
t = time(offset)
|
75
|
+
update_live_params(t)
|
76
|
+
out = @eq.run(@source.run(offset))
|
77
|
+
@insert_effects.each do |effect|
|
78
|
+
out = effect.run(out)
|
79
|
+
end
|
80
|
+
if @preset[:duck] != 0.0
|
81
|
+
duck = current_duck(t)
|
82
|
+
if duck
|
83
|
+
local_duck = t - duck
|
84
|
+
out = out * (1.0 - @preset[:duck] * @duck_env.run(local_duck))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
out = @compressor.run(out)
|
88
|
+
@output = out * @preset[:volume]
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def update_live_params(t)
|
94
|
+
@eq.low_gain = get(:eq_low_gain, t)
|
95
|
+
@eq.mid_gain = get(:eq_low_gain, t)
|
96
|
+
@eq.high_gain = get(:eq_low_gain, t)
|
97
|
+
end
|
98
|
+
|
99
|
+
def current_duck(t)
|
100
|
+
past = @ducks.select {|duck| duck < t}
|
101
|
+
return past.last unless past.empty?
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'synth_blocks/mixer/mixer_channel'
|
2
|
+
|
3
|
+
module SynthBlocks
|
4
|
+
module Mixer
|
5
|
+
##t
|
6
|
+
# Channel subclass specifically for SendChannels
|
7
|
+
class SendChannel < MixerChannel
|
8
|
+
##
|
9
|
+
# creates new send channel. See MixerChannel#new for parameters
|
10
|
+
def initialize(srate, insert_effects: [], sends: [], preset: {})
|
11
|
+
super(srate, nil, insert_effects: insert_effects, sends: sends, preset: preset)
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# run the send channel
|
16
|
+
def run(offset, input)
|
17
|
+
out = @eq.run(input)
|
18
|
+
@insert_effects.each do |effect|
|
19
|
+
out = effect.run(out)
|
20
|
+
end
|
21
|
+
@output = out * @preset[:volume]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|