synth_blocks 1.0.0

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