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,54 @@
1
+ require 'synth_blocks/core/sound'
2
+ require 'synth_blocks/core/oscillator'
3
+ require 'synth_blocks/mod/envelope'
4
+
5
+ module SynthBlocks
6
+ module Drum
7
+ ##
8
+ # A simple kick drum generator
9
+ class KickDrum < SynthBlocks::Core::Sound
10
+
11
+ ##
12
+ # === Structure
13
+ # Pitch Env > Sine wave OSC >
14
+ #
15
+ # === parameter:
16
+ # - pitch_attack, pitch_decay - Pitch envelope params in s
17
+ # - amp_attack, amp_decay - Amp envelope params in s
18
+ # - base_frequency - base frequency in Hz
19
+ # - pitch_mod - frequency modulation amount in Hz
20
+ def initialize(sfreq, preset = {})
21
+ super(sfreq, mode: :polyphonic)
22
+ @preset = {
23
+ pitch_attack: 0.001,
24
+ pitch_decay: 0.05,
25
+ amp_attack: 0.001,
26
+ amp_decay: 0.1,
27
+ base_frequency: 50,
28
+ pitch_mod: 200
29
+ }.merge(preset)
30
+ @oscillator = SynthBlocks::Core::Oscillator.new(@sampling_frequency)
31
+ @pitch_env = SynthBlocks::Mod::Envelope.new(@preset[:pitch_attack], @preset[:pitch_decay])
32
+ @amp_env = SynthBlocks::Mod::Envelope.new(@preset[:amp_attack], @preset[:amp_decay])
33
+ end
34
+
35
+ def duration(_) # :nodoc:
36
+ @preset[:amp_attack] + @preset[:amp_decay]
37
+ end
38
+ ##
39
+ # Run generator
40
+ def run(offset)
41
+ t = time(offset)
42
+ events = active_events(t)
43
+ if events.empty?
44
+ 0.0
45
+ else
46
+ event = events[events.keys.last]
47
+ local_started = t - event[:started]
48
+ osc_out = @oscillator.run(@preset[:base_frequency].to_f + @pitch_env.run(local_started) * @preset[:pitch_mod].to_f, waveform: :sine)
49
+ osc_out = osc_out * 1.0 * @amp_env.run(local_started)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ require 'synth_blocks/core/sound'
2
+ require 'synth_blocks/core/state_variable_filter'
3
+ require 'synth_blocks/drum/kick_drum'
4
+ require 'synth_blocks/mod/envelope'
5
+
6
+ module SynthBlocks
7
+ module Drum
8
+ ##
9
+ # Simple snare drum generator
10
+ class SnareDrum < SynthBlocks::Core::Sound
11
+ ##
12
+ # === Parameters
13
+ # [flt_frequency] Noise filter frequency
14
+ # [flt_envmod] Noise filter frequency modulation by envelope
15
+ # [flt_attack, flt_decay] Noise filter envelope params
16
+ # [flt_Q] Noise filter Q/resonance
17
+ # [noise_amp_attack, noise_amp_decay] Noise amp envelope params
18
+ # [noise_vol, drum_vol] Noise and Drum body volumes
19
+ # [base_frequency] Drum body base frequency (see KickDrum#initialize)
20
+ # [pitch_mod] Drum body pitch mod (see KickDrum#initialize)
21
+ # [pitch_attack, pitch_decay] Drum body pitch env params
22
+ #
23
+ def initialize(sfreq, preset = {})
24
+ super(sfreq, mode: :polyphonic)
25
+ @preset = {
26
+ flt_frequency: 4000,
27
+ flt_envmod: 6000,
28
+ flt_attack: 0.001,
29
+ flt_decay: 0.1,
30
+ flt_Q: 2,
31
+ noise_amp_attack: 0.001,
32
+ noise_amp_decay: 0.15,
33
+ noise_vol: 0.5,
34
+ drum_vol: 0.3,
35
+ base_frequency: 200,
36
+ pitch_mod: 200,
37
+ pitch_decay: 0.07
38
+
39
+ }.merge(preset)
40
+ @drum = SynthBlocks::Drum::KickDrum.new(sfreq, @preset)
41
+ @filter = SynthBlocks::Core::StateVariableFilter.new(sfreq)
42
+ @flt_env = SynthBlocks::Mod::Envelope.new(@preset[:flt_attack], @preset[:flt_decay])
43
+ @amp_env = SynthBlocks::Mod::Envelope.new(@preset[:noise_amp_attack], @preset[:noise_amp_decay])
44
+ end
45
+
46
+ # create a note on event
47
+ # [t] time in seconds since song start
48
+ # [note] MIDI note number
49
+ # [velocity] velocity (currently unused)
50
+ def start(t, note = 36, velocity = 1.0)
51
+ super(t, note, velocity)
52
+ @drum.start(t, note, velocity)
53
+ end
54
+
55
+ # create a note off event
56
+ # [t] time in seconds since song start
57
+ # [note] MIDI note number
58
+ def stop(t, note = 36)
59
+ super(t, note)
60
+ @drum.stop(t, note)
61
+ end
62
+
63
+ def duration(t) # :nodoc:
64
+ [@preset[:noise_amp_attack] + @preset[:noise_amp_decay], @drum.duration(t)].max
65
+ end
66
+
67
+ # run the generator
68
+ def run(offset)
69
+ drum_out = @drum.run(offset)
70
+ # time in seconds
71
+ t = time(offset)
72
+ events = active_events(t)
73
+ if events.empty?
74
+ 0.0
75
+ else
76
+ event = events[events.keys.last]
77
+ # lfo_out = (@lfo.run(@preset[:lfo_frequency], waveform: @preset[:lfo_waveform]) + 1) / 8 + 0.5
78
+ noise_out = rand * 2.0 - 1.0
79
+ local_started = t - event[:started]
80
+ noise_out = @filter.run(noise_out, @preset[:flt_frequency] + @flt_env.run(local_started) * @preset[:flt_envmod], @preset[:flt_Q])
81
+ noise_out = 0.3 * noise_out * @amp_env.run(local_started)
82
+ noise_out * @preset[:noise_vol] + drum_out * @preset[:drum_vol]
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,25 @@
1
+ require 'synth_blocks/drum/kick_drum'
2
+
3
+ module SynthBlocks
4
+ module Drum
5
+ ##
6
+ # Special case of the kick drum that allows to run it from a note pattern to
7
+ # create percussive sounds
8
+ class TunedDrum < KickDrum
9
+ def run(offset)
10
+ t = time(offset)
11
+ events = active_events(t)
12
+ if events.empty?
13
+ 0.0
14
+ else
15
+ event = events[events.keys.last]
16
+ note = events.keys.last
17
+ base_freq = frequency(note)
18
+ local_started = t - event[:started]
19
+ osc_out = @oscillator.run(base_freq + @pitch_env.run(local_started) * @preset[:pitch_mod].to_f, waveform: :sine)
20
+ osc_out = osc_out * 1.0 * @amp_env.run(local_started)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,85 @@
1
+ require 'synth_blocks/core/one_pole_lowpass'
2
+
3
+ module SynthBlocks
4
+ module Fx
5
+ ##
6
+ # A simple chorus
7
+ class Chorus
8
+ ##
9
+ attr_writer :phase, :rate, :delay_time, :mix # :nodoc:
10
+ ##
11
+ # Create new Chorus instance
12
+ #
13
+ # phase allows you to shift the phase of the delayed signal additionally
14
+ #
15
+ # rate is the LFO rate in Hz
16
+ #
17
+ # delay_time is the maximum delay time in ms
18
+ #
19
+ # mix is the ratio between original and delayed signal. 1.0 would mean only
20
+ # delayed signal (which wouldn't make any sense)
21
+ def initialize(sample_rate, phase: 0.0, rate: 0.5, delay_time: 7.0, mix: 0.5)
22
+ @sample_rate = sample_rate
23
+ @rate = rate
24
+ @delay_time = delay_time
25
+ @mix = mix
26
+
27
+ @z1 = 0.0
28
+ @sign = 0
29
+ @lfo_phase = phase * 2.0 - 1.0
30
+ @lfo_step_size = (4.0 * @rate / @sample_rate)
31
+ @lfo_sign = 1.0
32
+
33
+ # Compute required buffer size for desired delay and allocate it
34
+ # Add extra point to aid in interpolation later
35
+ @delay_line_length = ((@delay_time * @sample_rate * 0.001).floor * 2).to_i
36
+ @delay_line = [0.0] * @delay_line_length
37
+ @write_ptr = @delay_line_length - 1
38
+ @lp = SynthBlocks::Core::OnePoleLP.new
39
+ @output = 0.0
40
+ end
41
+
42
+ ##
43
+ # run the chorus
44
+ def run(input)
45
+ # Get delay time
46
+ offset = (next_lfo() * 0.3 + 0.4) * @delay_time * @sample_rate * 0.001
47
+
48
+ # Compute the largest read pointer based on the offset. If ptr
49
+ # is before the first delayline location, wrap around end point
50
+ ptr = @write_ptr - offset.floor;
51
+ ptr += @delay_line_length - 1 if ptr < 0
52
+
53
+
54
+ ptr2 = ptr - 1
55
+ ptr2 += @delay_line_length - 1 if ptr2 < 0
56
+
57
+ frac = offset - offset.floor.to_f
58
+ @output = @delay_line[ptr2] + @delay_line[ptr] * (1.0 - frac) - (1.0 - frac) * @z1
59
+ @z1 = @output
60
+
61
+ # Low pass
62
+ @lp.run(@output, 0.95)
63
+
64
+ # Write the input sample and any feedback to delayline
65
+ @delay_line[@write_ptr] = input
66
+
67
+ # Increment buffer index and wrap if necesary
68
+ @write_ptr += 1
69
+ @write_ptr = 0 if @write_ptr >= @delay_line_length
70
+ return (@output * @mix) + (input * (1.0-@mix))
71
+ end
72
+
73
+ private
74
+
75
+ def next_lfo()
76
+ if @lfo_phase >= 1.0
77
+ @lfo_sign = -1.0
78
+ elsif @lfo_phase <= -1.0
79
+ @lfo_sign = 1.0
80
+ end
81
+ @lfo_phase += @lfo_step_size * @lfo_sign
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,121 @@
1
+ module SynthBlocks
2
+ module Fx
3
+
4
+ class EnvelopeDetector # :nodoc:
5
+ def initialize(srate, tc: ms)
6
+ @sample_rate = srate
7
+ @ms = tc
8
+ set_coef()
9
+ end
10
+
11
+ def set_coef
12
+ @coef = Math.exp( -1000.0 / ( @ms * @sample_rate) )
13
+ end
14
+
15
+ def tc=(tc)
16
+ @ms = tc
17
+ set_coef()
18
+ end
19
+
20
+ def run(input, state)
21
+ input + @coef * ( state - input )
22
+ end
23
+ end
24
+
25
+
26
+ class AttRelEnvelope # :nodoc:
27
+ def initialize(srate, attack:, release:)
28
+ @attack = EnvelopeDetector.new(srate, tc: attack)
29
+ @release = EnvelopeDetector.new(srate, tc: release)
30
+ end
31
+
32
+ def attack=(attack)
33
+ @attack.tc = attack
34
+ end
35
+
36
+ def release=(decay)
37
+ @release.tc = decay
38
+ end
39
+
40
+ def run(input, state)
41
+ if input > state
42
+ @attack.run( input, state )
43
+ else
44
+ @release.run( input, state )
45
+ end
46
+ end
47
+ end
48
+
49
+ ##
50
+ # simple compresor
51
+ # taken from http://www.musicdsp.org/en/latest/Effects/204-simple-compressor-class-c.html
52
+
53
+ class Compressor
54
+ DC_OFFSET = 1.0E-25 # :nodoc:
55
+ LOG_2_DB = 8.6858896380650365530225783783321 # :nodoc:
56
+ DB_2_LOG = 0.11512925464970228420089957273422 # :nodoc:
57
+ attr_writer :ratio, :threshold, :window # :nodoc:
58
+ ##
59
+ # Create compressor instance
60
+ #
61
+ # attack is the attack time in ms
62
+ #
63
+ # release is the release time in ms
64
+ #
65
+ # ratio is the compresor ratio
66
+ #
67
+ # threshold is the knee threshold
68
+ def initialize(srate, attack: 10.0, release: 100.0, ratio: 1.0, threshold: 0.0)
69
+ @sample_rate = srate
70
+ @envelope = AttRelEnvelope.new(srate, attack: attack, release: release)
71
+ @env_db = DC_OFFSET
72
+ @ratio = ratio
73
+ @threshold = threshold
74
+ end
75
+
76
+ ##
77
+ # set attack
78
+ def attack=(attack)
79
+ @envelope.attack = attack
80
+ end
81
+
82
+ ##
83
+ # set release
84
+ def release=(release)
85
+ @envelope.release = release
86
+ end
87
+
88
+
89
+ ##
90
+ # run compressor
91
+ def run(input)
92
+ rect = input.abs
93
+ rect += DC_OFFSET
94
+ key_db = lin2db(rect)
95
+
96
+ over_db = key_db - @threshold
97
+ over_db = 0.0 if over_db < 0.0
98
+
99
+ # attack/release
100
+ over_db += DC_OFFSET
101
+ @env_db = @envelope.run(over_db, @env_db)
102
+ over_db = @env_db - DC_OFFSET
103
+
104
+ gr = over_db * @ratio - 1.0
105
+ gr = db2lin(gr)
106
+ input * gr
107
+ end
108
+
109
+ private
110
+
111
+ def lin2db(lin)
112
+ return Math.log( lin ) * LOG_2_DB
113
+ end
114
+
115
+ def db2lin(db)
116
+ return Math.exp( db * DB_2_LOG )
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,42 @@
1
+ module SynthBlocks
2
+ module Fx
3
+ ##
4
+ # Simple delay with mix and feedback parameters
5
+ # Currently doesn't really have a variable delay time, I'll tackle that when I
6
+ # need it. It uses a simple ring buffer implementation and delay time is only
7
+ # exact down to the sample.
8
+ class Delay
9
+ attr_reader :mix, :feedback # :nodoc:
10
+
11
+ ##
12
+ # time is given in seconds
13
+ #
14
+ # mix (0 = no delay, 1 = only delay)
15
+ #
16
+ # feedback (0 = zero feedback, 1 = full feedback (not advised))
17
+ #
18
+ # if given a block it will call the block from the run method to process the
19
+ # feedback signal
20
+ def initialize(sample_rate, time: 0.2, mix: 0.5, feedback: 0.4, &block)
21
+ @buffer = Array.new((sample_rate.to_f * time).floor)
22
+ @block = block
23
+ @pointer = 0
24
+ @mix = mix
25
+ @feedback = feedback
26
+ end
27
+
28
+ ##
29
+ # run delay
30
+ def run(input)
31
+ old_pointer = @pointer
32
+ @pointer = (@pointer + 1) % @buffer.length
33
+ delayed = (@buffer[@pointer] || 0.0)
34
+ if @block
35
+ delayed = @block.call(delayed)
36
+ end
37
+ @buffer[old_pointer] = input + (feedback * delayed)
38
+ input * (1.0 - mix) + delayed * mix
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ module SynthBlocks
2
+ module Fx
3
+ ##
4
+ # Simple 3 band Equalizer with variable shelving frequencies
5
+ #
6
+ # Source: http://www.musicdsp.org/en/latest/Filters/236-3-band-equaliser.html
7
+ class Eq
8
+ VSA = 1.0 / 4294967295.0 # very small amount (Denormal fix) :nodoc:
9
+
10
+ attr_accessor :low_gain, :mid_gain, :high_gain # :nodoc: Low Gain, Mid Gain, High Gain
11
+
12
+ ##
13
+ # Create new Equalizer instance
14
+ #
15
+ # lowfreq and highfreq are the shelving frequencies in Hz
16
+ def initialize(sample_rate=44100, lowfreq: 880, highfreq: 5000)
17
+
18
+ # Poles Lowpass
19
+ @f1p0 = 0.0
20
+ @f1p1 = 0.0
21
+ @f1p2 = 0.0
22
+ @f1p3 = 0.0
23
+
24
+ # Poles Highpass
25
+ @f2p0 = 0.0
26
+ @f2p1 = 0.0
27
+ @f2p2 = 0.0
28
+ @f2p3 = 0.0
29
+
30
+ # Sample history buffer
31
+
32
+ @sdm1 = 0.0 # Sample data minus 1
33
+ @sdm2 = 0.0 # 2
34
+ @sdm3 = 0.0 # 3
35
+
36
+ # Gain Controls
37
+ # Set Low/Mid/High gains to unity
38
+
39
+ @low_gain = 1.0 # low gain
40
+ @mid_gain = 1.0 # mid gain
41
+ @high_gain = 1.0 # high gain
42
+
43
+ # Calculate filter cutoff frequencies
44
+
45
+ @lf = 2 * Math.sin(Math::PI * (lowfreq.to_f / sample_rate.to_f))
46
+ @hf = 2 * Math.sin(Math::PI * (highfreq.to_f / sample_rate.to_f))
47
+ end
48
+
49
+ ##
50
+ # run equalizer
51
+ def run(sample)
52
+ # Filter #1 (lowpass)
53
+
54
+ @f1p0 += (@lf * (sample - @f1p0)) + VSA;
55
+ @f1p1 += (@lf * (@f1p0 - @f1p1));
56
+ @f1p2 += (@lf * (@f1p1 - @f1p2));
57
+ @f1p3 += (@lf * (@f1p2 - @f1p3));
58
+
59
+ l = @f1p3;
60
+
61
+ # Filter #2 (highpass)
62
+
63
+ @f2p0 += (@hf * (sample - @f2p0)) + VSA;
64
+ @f2p1 += (@hf * (@f2p0 - @f2p1));
65
+ @f2p2 += (@hf * (@f2p1 - @f2p2));
66
+ @f2p3 += (@hf * (@f2p2 - @f2p3));
67
+
68
+ h = @sdm3 - @f2p3;
69
+
70
+ # Calculate midrange (signal - (low + high))
71
+
72
+ m = @sdm3 - (h + l);
73
+
74
+ # Scale, Combine and store
75
+
76
+ l *= @low_gain
77
+ m *= @mid_gain
78
+ h *= @high_gain
79
+
80
+ # Shuffle history buffer
81
+
82
+ @sdm3 = @sdm2;
83
+ @sdm2 = @sdm1;
84
+ @sdm1 = sample;
85
+
86
+ # Return result
87
+
88
+ l + m + h
89
+ end
90
+ end
91
+ end
92
+ end