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.
@@ -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