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,83 @@
1
+ module SynthBlocks
2
+ module Mod
3
+ ##
4
+ # Implementation of a linear ADSR envelope generator with a tracking
5
+ # value so that envelope restarts don't click
6
+ class Adsr
7
+ ##
8
+ # attack time in seconds
9
+ attr_accessor :attack
10
+
11
+ ##
12
+ # decay time in seconds
13
+ attr_accessor :decay
14
+ ##
15
+ # sustain level (0.0-1.0)
16
+ attr_accessor :sustain
17
+
18
+ ##
19
+ # release time in seconds
20
+ attr_accessor :release
21
+
22
+ ##
23
+ # Creates new ADSR envelope
24
+ #
25
+ # attack, decay and release are times in seconds (as float)
26
+ #
27
+ # sustain should be between 0 and 1
28
+ def initialize(attack, decay, sustain, release)
29
+ @value = 0
30
+ @start_value = 0
31
+ @attack = attack
32
+ @decay = decay
33
+ @sustain = sustain
34
+ @release = release
35
+ end
36
+
37
+ ##
38
+ # run the envelope.
39
+ #
40
+ # if released is given (should be <= t), the envelope will enter the release stage
41
+ # returns the current value between 0 and 1
42
+ def run(t, released)
43
+ attack_decay = attack + decay
44
+ if !released
45
+ if t < 0.0001 # initialize start value (slightly hacky, but works)
46
+ @start_value = @value
47
+ return @start_value
48
+ end
49
+ if t <= attack # attack
50
+ return @value = linear(@start_value, 1, attack, t)
51
+ end
52
+ if t > attack && t < attack_decay # decay
53
+ return @value = linear(1.0, sustain, decay, t - attack)
54
+ end
55
+ if t >= attack + decay # sustain
56
+ return @value = sustain
57
+ end
58
+ else # release
59
+ if t <= attack # when released in attack phase
60
+ attack_level = linear(@start_value, 1, attack, released)
61
+ return linear(attack_level, 0, release, t - released)
62
+ end
63
+ if t > attack && t < attack_decay # when released in decay phase
64
+ decay_level = linear(1.0, sustain, decay, released - attack)
65
+ return @value = linear(decay_level, 0, release, t - released)
66
+ end
67
+ if t >= attack_decay && t < released + release # normal release
68
+ return @value = linear(sustain, 0, release, t - released)
69
+ end
70
+ if t >= released + release # after release
71
+ return @value = 0.0
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def linear(start, target, length, time)
79
+ (target - start) / length * time + start
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,35 @@
1
+ module SynthBlocks
2
+ module Mod
3
+ ##
4
+ # Simple Attack / Release envelope
5
+ class Envelope
6
+ ##
7
+ # attack time in seconds
8
+ attr_accessor :attack
9
+
10
+ ##
11
+ # release time in seconds
12
+ attr_accessor :release
13
+ ##
14
+ # create new attack/release envelope
15
+ def initialize(attack,release)
16
+ @attack = attack
17
+ @release = release
18
+ end
19
+ ##
20
+ # run the attack/release envelope
21
+ # You can override attack and decay
22
+ def run(t, a=@attack, r=@release)
23
+ @a = a
24
+ @r = r
25
+ if t > @a + @r
26
+ return 0
27
+ elsif t > @a #release
28
+ return 1 - ((1 / @r) * (t - @a))
29
+ else # attack
30
+ return 1 / @a * t
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,219 @@
1
+
2
+ module SynthBlocks
3
+ module Sequencer
4
+ ##
5
+ # A module that implements a sequencer DSL
6
+ # === Usage
7
+ #
8
+ # include SequencerDSL
9
+ # def_pattern(:pattern_name, 16) do
10
+ # drum_pattern kickdrum, '*---*---*---*---'
11
+ # end
12
+ #
13
+ # my_song = song(bpm: 125) do
14
+ # pattern(:pattern_name, at: 0, repeat: 4)
15
+ # end
16
+ #
17
+ # output = my_song.render(44100) do |sample|
18
+ # kickdrum.run(sample)
19
+ # end
20
+ # print output.pack('e*')
21
+ #
22
+ module SequencerDSL
23
+
24
+ P = nil # :nodoc:
25
+
26
+ ##
27
+ # The Pattern class is instantiated by the def_pattern helper
28
+ class Pattern
29
+ NOTES=%w(C C# D D# E F F# G G# A A# B) # :nodoc:
30
+
31
+ attr_reader :sounds, :steps # :nodoc:
32
+ def initialize(steps) # :nodoc:
33
+ @steps = steps
34
+ @sounds = []
35
+ end
36
+
37
+ def run(block) # :nodoc:
38
+ instance_eval(&block)
39
+ end
40
+
41
+ ##
42
+ # Define a drum pattern
43
+ # - sound is the sound generator object
44
+ # - pattern is a pattern in the form of a string
45
+ # === Defining patterns
46
+ #
47
+ # drum_pattern bass_drum, '*---*---*---!---'
48
+ #
49
+ # - <tt>*</tt> represents a normal drum hit (velocity: 0.5)
50
+ # - <tt>!</tt> represents an accented drum hit (velocity 1.0)
51
+ # - <tt>-</tt> represents a pause (no hit)
52
+
53
+ def drum_pattern(sound, pattern)
54
+ events = []
55
+ @steps.times do |i|
56
+ if pattern.chars[i] == '*'
57
+ events << [i, [:start, 36, 0.5]]
58
+ elsif pattern.chars[i] == '!'
59
+ events << [i, [:start, 36, 1.0]]
60
+ end
61
+ end
62
+ @sounds.push([sound, events])
63
+ end
64
+
65
+ def str2note(str) # :nodoc:
66
+ match = str.upcase.strip.match(/([ABCDEFGH]#?)(-?\d)/)
67
+ return nil unless match
68
+ octave = match[2].to_i + 2
69
+ note = NOTES.index(match[1])
70
+ if note >= 0 && octave > 0 && octave < 10
71
+ return 12 * octave + note
72
+ end
73
+ end
74
+
75
+ ##
76
+ # Define a note pattern
77
+ # [sound] sound generator base class
78
+ # [pattern] a note pattern
79
+ #
80
+ # === Defining a note pattern
81
+ #
82
+ # note_pattern monosynth, [
83
+ # ['C4, D#4, G4', 2], P, P, P,
84
+ # P, P, P, P,
85
+ # P, P, P, P,
86
+ # P, P, P, P
87
+ # ]
88
+ #
89
+ # - <tt>P</tt> is a pause
90
+ # - a note step in the pattern is an array containing the note and the
91
+ # length of the note in steps
92
+ # - a note is a note name as a string, which consists of the note and the
93
+ # octave. To play chords, concatenate notes with commas
94
+ def note_pattern(sound, pattern)
95
+ events = []
96
+ @steps.times do |i|
97
+ if pattern[i]
98
+ notes, len = pattern[i]
99
+ notes.split(',').each do |note|
100
+ note_num = str2note(note)
101
+ events << [i, [:start, note_num, 1.0]]
102
+ events << [i + len, [:stop, note_num]]
103
+ end
104
+ end
105
+ end
106
+ @sounds.push([sound, events])
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Define a note pattern
112
+ def def_pattern(name, steps, &block)
113
+ @patterns ||= {}
114
+ p = Pattern.new(steps)
115
+ p.run(block)
116
+ @patterns[name] = p
117
+ end
118
+
119
+ ##
120
+ # A
121
+ class Song
122
+ attr_reader :events, :per_bar, :per_beat # :nodoc:
123
+ def initialize(bpm, patterns) # :nodoc:
124
+ @tempo = bpm
125
+ @events = []
126
+ @per_beat = 60.0 / @tempo.to_f
127
+ @per_bar = @per_beat * 4.0
128
+ @per_step = @per_beat / 4.0
129
+ @patterns = patterns
130
+ @latest_time = 0
131
+ end
132
+
133
+ def run(block) # :nodoc:
134
+ instance_eval(&block)
135
+ end
136
+
137
+ ##
138
+ # inserts a pattern into the song
139
+ # [name] pattern needs to be defined by <tt>def_pattern</tt>
140
+ # [at] Position in bars to insert the pattern to
141
+ # [repeat] number of times the pattern should repeat
142
+ # [length] if you want to only use part of the pattern
143
+ #
144
+
145
+ def pattern(name, at: 0, repeat: 1, length: nil)
146
+ p = @patterns[name]
147
+ pattern_length = length || p.steps
148
+ start = at.to_f * @per_bar
149
+
150
+ p.sounds.each do |sound, events|
151
+ repeat.times do |rep|
152
+
153
+ events.each do |event|
154
+ step, data = event
155
+ next if step > pattern_length
156
+
157
+ time = start + (rep.to_f * pattern_length.to_f * @per_step.to_f) + step.to_f * @per_step
158
+ @latest_time = time if time > @latest_time
159
+ type, *rest = data
160
+ @events << [sound, [type, time, *rest]]
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ ##
167
+ # Returns the length of the song in seconds plus 2 seconds to allow for
168
+ # reverb tails etc.
169
+ def length
170
+ (@latest_time + 2.0).ceil
171
+ end
172
+
173
+ ##
174
+ # Sends all scheduled events to the instruments
175
+ def play
176
+ @events.each do |event|
177
+ instrument, data = event
178
+ instrument.send(*data)
179
+ end
180
+ end
181
+ end
182
+
183
+ ##
184
+ # Define a song in the given tempo (in BPM)
185
+ # using the Song#pattern method
186
+
187
+ def song(bpm: 120, &block)
188
+ song = Song.new(bpm, @patterns)
189
+ song.run(block)
190
+ song.play
191
+ # File.open("DEBUG.txt", 'wb') do |f|
192
+ # f.print song.events.inspect
193
+ # end
194
+ song
195
+ end
196
+ ##
197
+ # render the song
198
+ # the actual rendering needs to be done
199
+ # manually in the block passed
200
+ # start & length in bars
201
+ # block gets an offset in samples it should render
202
+ def render(sfreq, start=0, len=nil)
203
+ start_time = start * @per_bar
204
+ end_time = len ? start_time + len * @per_bar : length
205
+ start_sample = (sfreq * start_time).floor
206
+ end_sample = (sfreq * end_time).ceil
207
+ sample = start_sample
208
+ sample_len = end_sample - start_sample
209
+ output = Array.new(sample_len)
210
+ loop do
211
+ output[sample - start_sample] = yield sample
212
+ break if sample > end_sample
213
+ sample += 1
214
+ end
215
+ output
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,77 @@
1
+ require 'synth_blocks/core/sound'
2
+ require 'synth_blocks/core/state_variable_filter'
3
+ require 'synth_blocks/core/oscillator'
4
+ require 'synth_blocks/mod/adsr'
5
+
6
+ module SynthBlocks
7
+ module Synth
8
+ ##
9
+ # A monosynth sound generator
10
+ class Monosynth < SynthBlocks::Core::Sound
11
+ ##
12
+ # === Parameters
13
+ # - amp_attack, _decay, _sustain, _release - Amp Envelope params
14
+ # - flt_attack, _decay, _sustain, _release - Filter Envelope params
15
+ # - flt_envmod - filter envelope modulation amount in Hz
16
+ # - flt_frequency, flt_Q - filter params
17
+ # - osc_waveform - waveform to generate (see Oscillator class)
18
+ def initialize(sfreq, preset={})
19
+ super(sfreq, mode: :monophonic)
20
+ @preset = {
21
+ amp_attack: 0.001,
22
+ amp_decay: 0.2,
23
+ amp_sustain: 0.8,
24
+ amp_release: 0.2,
25
+ flt_attack: 0.001,
26
+ flt_decay: 0.05,
27
+ flt_sustain: 0.0,
28
+ flt_release: 0.2,
29
+ flt_envmod: 1000,
30
+ flt_frequency: 2000,
31
+ flt_Q: 2,
32
+ osc_waveform: :square,
33
+ lfo_waveform: :sine,
34
+ lfo_frequency: 2
35
+ }.merge(preset)
36
+ @oscillator = SynthBlocks::Core::Oscillator.new(@sampling_frequency)
37
+ @filter = SynthBlocks::Core::StateVariableFilter.new(@sampling_frequency)
38
+ @filter2 = SynthBlocks::Core::StateVariableFilter.new(@sampling_frequency)
39
+ @lfo = SynthBlocks::Core::Oscillator.new(@sampling_frequency)
40
+ @amp_env = SynthBlocks::Mod::Adsr.new(
41
+ @preset[:amp_attack],
42
+ @preset[:amp_decay],
43
+ @preset[:amp_sustain],
44
+ @preset[:amp_release]
45
+ )
46
+ @flt_env = SynthBlocks::Mod::Adsr.new(
47
+ @preset[:flt_attack],
48
+ @preset[:flt_decay],
49
+ @preset[:flt_sustain],
50
+ @preset[:flt_release]
51
+ )
52
+ end
53
+
54
+
55
+ ##
56
+ # run generator
57
+ def run(offset)
58
+ # time in seconds
59
+ t = time(offset)
60
+ events = active_events(t)
61
+ if events.empty?
62
+ 0.0
63
+ else
64
+ note = events.keys.last
65
+ event = events[note]
66
+ # lfo_out = (@lfo.run(@preset[:lfo_frequency], waveform: @preset[:lfo_waveform]) + 1) / 8 + 0.5
67
+ osc_out = @oscillator.run(frequency(note), waveform: @preset[:osc_waveform])
68
+ local_started = t - event[:started]
69
+ local_stopped = event[:stopped] && event[:stopped] - event[:started]
70
+ osc_out = @filter.run(osc_out, @preset[:flt_frequency] + @flt_env.run(local_started, local_stopped) * @preset[:flt_envmod], @preset[:flt_Q])
71
+ # osc_out = @filter2.run(osc_out, @preset[:flt_frequency] + @flt_env.run(local_started, local_stopped) * @preset[:flt_envmod], @preset[:flt_Q])
72
+ 0.3 * osc_out * @amp_env.run(local_started, local_stopped)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,89 @@
1
+ require 'synth_blocks/core/sound'
2
+ require 'synth_blocks/core/state_variable_filter'
3
+ require 'synth_blocks/core/oscillator'
4
+ require 'synth_blocks/mod/adsr'
5
+
6
+ module SynthBlocks
7
+ module Synth
8
+ class PolyVoice # :nodoc:
9
+ def initialize(sfreq, parent, preset)
10
+ @sampling_frequency = sfreq
11
+ @parent = parent
12
+ @preset = preset
13
+ @oscillator = SynthBlocks::Core::Oscillator.new(sfreq)
14
+ @filter = SynthBlocks::Core::StateVariableFilter.new(sfreq)
15
+ @amp_env = SynthBlocks::Mod::Adsr.new(@preset[:amp_env_attack], @preset[:amp_env_decay], @preset[:amp_env_sustain], @preset[:amp_env_release])
16
+ @flt_env = SynthBlocks::Mod::Adsr.new(@preset[:flt_env_attack], @preset[:flt_env_decay], @preset[:flt_env_sustain], @preset[:flt_env_release])
17
+ end
18
+
19
+ def run(started, stopped, frequency, velocity)
20
+ osc_out = @oscillator.run(frequency, waveform: @preset[:osc_waveform])
21
+ osc_out = @filter.run(osc_out, @parent.get(:flt_frequency, started) + @flt_env.run(started, stopped) * @parent.get(:flt_envmod, started), @preset[:flt_Q])
22
+ osc_out = osc_out * @amp_env.run(started, stopped) * velocity
23
+ end
24
+
25
+ end
26
+
27
+ ##
28
+ # A simple polyphonic synthesizer
29
+ #
30
+ # OSC > Filter > Amp
31
+ #
32
+
33
+ class Polysynth < SynthBlocks::Core::Sound
34
+ # === Parameters
35
+ # - amp_attack, _decay, _sustain, _release - Amp Envelope params
36
+ # - flt_attack, _decay, _sustain, _release - Filter Envelope params
37
+ # - flt_envmod - filter envelope modulation amount in Hz
38
+ # - flt_frequency, flt_Q - filter params
39
+ # - osc_waveform - waveform to generate (see Oscillator class)
40
+ def initialize(sfreq, preset = {})
41
+ @preset = {
42
+ osc_waveform: :sawtooth,
43
+ amp_env_attack: 0.2,
44
+ amp_env_decay: 0.2,
45
+ amp_env_sustain: 0.8,
46
+ amp_env_release: 0.5,
47
+ flt_env_attack: 0.5,
48
+ flt_env_decay: 0.7,
49
+ flt_env_sustain: 0.4,
50
+ flt_env_release: 0.5,
51
+ flt_frequency: 1000,
52
+ flt_envmod: 2000,
53
+ flt_Q: 3
54
+ }.merge(preset)
55
+ super(sfreq, mode: :polyphonic)
56
+ @active_voices = {}
57
+ end
58
+
59
+ def live_params # :nodoc:
60
+ [:flt_frequency, :flt_envmod]
61
+ end
62
+
63
+ def release(t) # :nodoc:
64
+ get(:flt_env_release, t)
65
+ end
66
+
67
+ ##
68
+ # run sound generator
69
+ def run(offset)
70
+ t = time(offset)
71
+ events = active_events(t)
72
+ voice_results = []
73
+ events.each do |note, event|
74
+ local_started = t - event[:started]
75
+ next if local_started < 0
76
+ local_stopped = event[:stopped] && event[:stopped] - event[:started]
77
+ note_key = "#{note}:#{event[:started]}"
78
+ if @active_voices[note_key].nil?
79
+ @active_voices[note_key] = PolyVoice.new(@sampling_frequency, self, @preset)
80
+ end
81
+ if @active_voices[note_key]
82
+ voice_results << @active_voices[note_key].run(local_started, local_stopped, frequency(note), event[:velocity])
83
+ end
84
+ end
85
+ 0.3 * voice_results.inject(0) {|sum, result| sum + result}
86
+ end
87
+ end
88
+ end
89
+ end