spcore 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +3 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/ChangeLog.rdoc +10 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +34 -0
- data/Rakefile +33 -0
- data/lib/spcore.rb +19 -0
- data/lib/spcore/core/constants.rb +4 -0
- data/lib/spcore/core/limiters.rb +51 -0
- data/lib/spcore/lib/biquad_filter.rb +72 -0
- data/lib/spcore/lib/circular_buffer.rb +148 -0
- data/lib/spcore/lib/cookbook_allpass_filter.rb +38 -0
- data/lib/spcore/lib/cookbook_bandpass_filter.rb +38 -0
- data/lib/spcore/lib/cookbook_highpass_filter.rb +38 -0
- data/lib/spcore/lib/cookbook_lowpass_filter.rb +44 -0
- data/lib/spcore/lib/cookbook_notch_filter.rb +38 -0
- data/lib/spcore/lib/delay_line.rb +39 -0
- data/lib/spcore/lib/envelope_detector.rb +35 -0
- data/lib/spcore/lib/gain.rb +182 -0
- data/lib/spcore/lib/interpolation.rb +15 -0
- data/lib/spcore/lib/oscillator.rb +117 -0
- data/lib/spcore/lib/saturation.rb +45 -0
- data/lib/spcore/version.rb +4 -0
- data/spcore.gemspec +37 -0
- data/spec/core/limiters_spec.rb +38 -0
- data/spec/lib/circular_buffer_spec.rb +177 -0
- data/spec/lib/cookbook_filter_spec.rb +44 -0
- data/spec/lib/delay_line_spec.rb +24 -0
- data/spec/lib/envelope_detector_spec.rb +71 -0
- data/spec/lib/gain_spec.rb +48 -0
- data/spec/lib/interpolation_spec.rb +21 -0
- data/spec/lib/oscillator_spec.rb +146 -0
- data/spec/lib/saturate_spec.rb +100 -0
- data/spec/sigproc_spec.rb +8 -0
- data/spec/spec_helper.rb +4 -0
- metadata +217 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module SPCore
|
2
|
+
class CookbookBandpassFilter < BiquadFilter
|
3
|
+
def initialize sample_rate
|
4
|
+
super(sample_rate)
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_critical_freq_and_bw critical_freq, bandwidth
|
8
|
+
@critical_freq = critical_freq
|
9
|
+
@bandwidth = bandwidth
|
10
|
+
|
11
|
+
# setup variables
|
12
|
+
omega = 2.0 * Math::PI * @critical_freq / @sample_rate
|
13
|
+
sn = Math::sin(omega)
|
14
|
+
cs = Math::cos(omega)
|
15
|
+
alpha = sn * Math::sinh(BiquadFilter::LN_2 / 2.0 * @bandwidth * omega / sn)
|
16
|
+
|
17
|
+
b0 = alpha
|
18
|
+
b1 = 0.0
|
19
|
+
b2 = -alpha
|
20
|
+
a0 = 1.0 + alpha
|
21
|
+
a1 = -2.0 * cs
|
22
|
+
a2 = 1.0 - alpha
|
23
|
+
|
24
|
+
# precompute the coefficients
|
25
|
+
@biquad.b0 = b0 / a0
|
26
|
+
@biquad.b1 = b1 / a0
|
27
|
+
@biquad.b2 = b2 / a0
|
28
|
+
@biquad.a0 = a0 / a0
|
29
|
+
@biquad.a1 = a1 / a0
|
30
|
+
@biquad.a2 = a2 / a0
|
31
|
+
|
32
|
+
## zero initial samples
|
33
|
+
#@biquad.x1 = @biquad.x2 = 0
|
34
|
+
#@biquad.y1 = @biquad.y2 = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SPCore
|
2
|
+
class CookbookHighpassFilter < BiquadFilter
|
3
|
+
def initialize sample_rate
|
4
|
+
super(sample_rate)
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_critical_freq_and_bw critical_freq, bandwidth
|
8
|
+
@critical_freq = critical_freq
|
9
|
+
@bandwidth = bandwidth
|
10
|
+
|
11
|
+
# setup variables
|
12
|
+
omega = 2.0 * Math::PI * @critical_freq / @sample_rate
|
13
|
+
sn = Math::sin(omega)
|
14
|
+
cs = Math::cos(omega)
|
15
|
+
alpha = sn * Math::sinh(BiquadFilter::LN_2 / 2.0 * @bandwidth * omega / sn)
|
16
|
+
|
17
|
+
b0 = (1.0 + cs) / 2.0
|
18
|
+
b1 =-(1.0 + cs)
|
19
|
+
b2 = (1.0 + cs) / 2.0
|
20
|
+
a0 = 1.0 + alpha
|
21
|
+
a1 = -2.0 * cs
|
22
|
+
a2 = 1.0 - alpha
|
23
|
+
|
24
|
+
# precompute the coefficients
|
25
|
+
@biquad.b0 = b0 / a0
|
26
|
+
@biquad.b1 = b1 / a0
|
27
|
+
@biquad.b2 = b2 / a0
|
28
|
+
@biquad.a0 = a0 / a0
|
29
|
+
@biquad.a1 = a1 / a0
|
30
|
+
@biquad.a2 = a2 / a0
|
31
|
+
|
32
|
+
## zero initial samples
|
33
|
+
#@biquad.x1 = @biquad.x2 = 0
|
34
|
+
#@biquad.y1 = @biquad.y2 = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SPCore
|
2
|
+
class CookbookLowpassFilter < BiquadFilter
|
3
|
+
def initialize sample_rate
|
4
|
+
super(sample_rate)
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_critical_freq_and_bw critical_freq, bandwidth
|
8
|
+
@critical_freq = critical_freq
|
9
|
+
@bandwidth = bandwidth
|
10
|
+
|
11
|
+
# setup variables
|
12
|
+
omega = 2.0 * Math::PI * @critical_freq / @sample_rate
|
13
|
+
sn = Math::sin(omega)
|
14
|
+
cs = Math::cos(omega)
|
15
|
+
alpha = sn * Math::sinh(BiquadFilter::LN_2 / 2.0 * @bandwidth * omega / sn)
|
16
|
+
|
17
|
+
#if(q_is_bandwidth)
|
18
|
+
# alpha=tsin*sinh(log(2.0)/2.0*q*omega/tsin);
|
19
|
+
#else
|
20
|
+
# alpha=tsin/(2.0*q);
|
21
|
+
#end
|
22
|
+
|
23
|
+
b0 = (1.0 - cs) / 2.0
|
24
|
+
b1 = 1.0 - cs
|
25
|
+
b2 = (1.0 - cs) / 2.0
|
26
|
+
a0 = 1.0 + alpha
|
27
|
+
a1 = -2.0 * cs
|
28
|
+
a2 = 1.0 - alpha
|
29
|
+
|
30
|
+
# precompute the coefficients
|
31
|
+
@biquad.b0 = b0 / a0
|
32
|
+
@biquad.b1 = b1 / a0
|
33
|
+
@biquad.b2 = b2 / a0
|
34
|
+
@biquad.a0 = a0 / a0
|
35
|
+
@biquad.a1 = a1 / a0
|
36
|
+
@biquad.a2 = a2 / a0
|
37
|
+
|
38
|
+
## zero initial samples
|
39
|
+
#@biquad.x1 = @biquad.x2 = 0
|
40
|
+
#@biquad.y1 = @biquad.y2 = 0
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SPCore
|
2
|
+
class CookbookNotchFilter < BiquadFilter
|
3
|
+
def initialize sample_rate
|
4
|
+
super(sample_rate)
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_critical_freq_and_bw critical_freq, bandwidth
|
8
|
+
@critical_freq = critical_freq
|
9
|
+
@bandwidth = bandwidth
|
10
|
+
|
11
|
+
# setup variables
|
12
|
+
omega = 2.0 * Math::PI * @critical_freq / @sample_rate
|
13
|
+
sn = Math::sin(omega)
|
14
|
+
cs = Math::cos(omega)
|
15
|
+
alpha = sn * Math::sinh(BiquadFilter::LN_2 / 2.0 * @bandwidth * omega / sn)
|
16
|
+
|
17
|
+
b0 = 1.0
|
18
|
+
b1 = -2.0 * cs
|
19
|
+
b2 = 1.0
|
20
|
+
a0 = 1.0 + alpha
|
21
|
+
a1 = -2.0 * cs
|
22
|
+
a2 = 1.0 - alpha
|
23
|
+
|
24
|
+
# precompute the coefficients
|
25
|
+
@biquad.b0 = b0 / a0
|
26
|
+
@biquad.b1 = b1 / a0
|
27
|
+
@biquad.b2 = b2 / a0
|
28
|
+
@biquad.a0 = a0 / a0
|
29
|
+
@biquad.a1 = a1 / a0
|
30
|
+
@biquad.a2 = a2 / a0
|
31
|
+
|
32
|
+
## zero initial samples
|
33
|
+
#@biquad.x1 = @biquad.x2 = 0
|
34
|
+
#@biquad.y1 = @biquad.y2 = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SPCore
|
2
|
+
class DelayLine
|
3
|
+
include Hashmake::HashMakeable
|
4
|
+
|
5
|
+
attr_reader :sample_rate, :max_delay_seconds, :delay_seconds, :delay_samples
|
6
|
+
|
7
|
+
ARG_SPECS = [
|
8
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
9
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :max_delay_seconds, :type => Float, :validator => ->(a){ (a > 0.0) } ),
|
10
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :delay_seconds, :type => Float, :default => 0.0, :validator => ->(a){ a >= 0.0 } ),
|
11
|
+
]
|
12
|
+
|
13
|
+
def initialize args
|
14
|
+
hash_make DelayLine::ARG_SPECS, args
|
15
|
+
raise ArgumentError, "delay_seconds #{delay_seconds} is greater than max_delay_seconds #{max_delay_seconds}" if @delay_seconds > @max_delay_seconds
|
16
|
+
@buffer = CircularBuffer.new((@sample_rate * @max_delay_seconds) + 1, :override_when_full => true)
|
17
|
+
@buffer.push_ary Array.new(@buffer.size, 0.0)
|
18
|
+
self.delay_seconds=(@delay_seconds)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delay_seconds= delay_seconds
|
22
|
+
delay_samples_floor = (@sample_rate * delay_seconds).floor
|
23
|
+
@delay_samples = delay_samples_floor.to_i
|
24
|
+
@delay_seconds = delay_samples_floor / @sample_rate
|
25
|
+
|
26
|
+
#if @buffer.fill_count < @delay_samples
|
27
|
+
# @buffer.push_ary Array.new(@delay_samples - @buffer.fill_count, 0.0)
|
28
|
+
#end
|
29
|
+
end
|
30
|
+
|
31
|
+
def push_sample sample
|
32
|
+
@buffer.push sample
|
33
|
+
end
|
34
|
+
|
35
|
+
def delayed_sample
|
36
|
+
return @buffer.newest(@delay_samples)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SPCore
|
2
|
+
class EnvelopeDetector
|
3
|
+
include Hashmake::HashMakeable
|
4
|
+
|
5
|
+
ARG_SPECS = [
|
6
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
7
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :attack_time, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
8
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :release_time, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
9
|
+
]
|
10
|
+
|
11
|
+
attr_reader :envelope, :sample_rate, :attack_time, :release_time
|
12
|
+
|
13
|
+
def initialize args
|
14
|
+
hash_make EnvelopeDetector::ARG_SPECS, args
|
15
|
+
|
16
|
+
@g_attack = Math.exp(-1.0 / (sample_rate * attack_time))
|
17
|
+
@g_release = Math.exp(-1.0 / (sample_rate * release_time))
|
18
|
+
|
19
|
+
@envelope = 0.0
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_sample sample
|
23
|
+
input_abs = sample.abs
|
24
|
+
|
25
|
+
if @envelope < input_abs
|
26
|
+
@envelope = (@envelope * @g_attack) + ((1.0 - @g_attack) * input_abs)
|
27
|
+
else
|
28
|
+
@envelope = (@envelope * @g_release) + ((1.0 - @g_release) * input_abs)
|
29
|
+
end
|
30
|
+
|
31
|
+
return @envelope
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module SPCore
|
2
|
+
class Gain
|
3
|
+
|
4
|
+
#MAX_DB = 72
|
5
|
+
#NUM_DB_HALF_STEPS=146
|
6
|
+
#DB_TO_SCALAR_TABLE = [
|
7
|
+
# 1.0, # 0 dB
|
8
|
+
# 1.059253725, # 0.5
|
9
|
+
# 1.122018454, # 1
|
10
|
+
# 1.188502227, # 1.5
|
11
|
+
# 1.258925412, # 2
|
12
|
+
# 1.333521432, # 2.5
|
13
|
+
# 1.412537545, # 3
|
14
|
+
# 1.496235656, # 3.5
|
15
|
+
# 1.584893192, # 4
|
16
|
+
# 1.678804018, # 4.5
|
17
|
+
# 1.77827941, # 5
|
18
|
+
# 1.883649089, # 5.5
|
19
|
+
# 1.995262315, # 6
|
20
|
+
# 2.11348904, # 6.5
|
21
|
+
# 2.238721139, # 7
|
22
|
+
# 2.371373706, # 7.5
|
23
|
+
# 2.511886432, # 8
|
24
|
+
# 2.66072506, # 8.5
|
25
|
+
# 2.818382931, # 9
|
26
|
+
# 2.985382619, # 9.5
|
27
|
+
# 3.16227766, # 10 dB
|
28
|
+
# 3.349654392, # 10.5
|
29
|
+
# 3.548133892, # 11
|
30
|
+
# 3.758374043, # 11.5
|
31
|
+
# 3.981071706, # 12
|
32
|
+
# 4.216965034, # 12.5
|
33
|
+
# 4.466835922, # 13
|
34
|
+
# 4.73151259, # /13.5
|
35
|
+
# 5.011872336, # 14
|
36
|
+
# 5.308844442, # 14.5
|
37
|
+
# 5.623413252, # 15
|
38
|
+
# 5.956621435, # 15.5
|
39
|
+
# 6.309573445, # 16
|
40
|
+
# 6.683439176, # 16.5
|
41
|
+
# 7.079457844, # 17
|
42
|
+
# 7.498942093, # 17.5
|
43
|
+
# 7.943282347, # 18
|
44
|
+
# 8.413951416, # 18.5
|
45
|
+
# 8.912509381, # 19
|
46
|
+
# 9.440608763, # 19.5
|
47
|
+
# 10.0, # 20 dB
|
48
|
+
# 10.59253725, # 20.5
|
49
|
+
# 11.22018454, # 21
|
50
|
+
# 11.88502227, # 21.5
|
51
|
+
# 12.58925412, # 22
|
52
|
+
# 13.33521432, # 22.5
|
53
|
+
# 14.12537545, # 23
|
54
|
+
# 14.96235656, # 23.5
|
55
|
+
# 15.84893192, # 24
|
56
|
+
# 16.78804018, # 24.5
|
57
|
+
# 17.7827941, # 25
|
58
|
+
# 18.83649089, # 25.5
|
59
|
+
# 19.95262315, # 26
|
60
|
+
# 21.1348904, # 26.5
|
61
|
+
# 22.38721139, # 27
|
62
|
+
# 23.71373706, # 27.5
|
63
|
+
# 25.11886432, # 28
|
64
|
+
# 26.6072506, # 28.5
|
65
|
+
# 28.18382931, # 29
|
66
|
+
# 29.85382619, # 29.5
|
67
|
+
# 31.6227766, # 30 dB
|
68
|
+
# 33.49654392, # 30.5
|
69
|
+
# 35.48133892, # 31
|
70
|
+
# 37.58374043, # 31.5
|
71
|
+
# 39.81071706, # 32
|
72
|
+
# 42.16965034, # 32.5
|
73
|
+
# 44.66835922, # 33
|
74
|
+
# 47.3151259, # 33.5
|
75
|
+
# 50.11872336, # 34
|
76
|
+
# 53.08844442, # 34.5
|
77
|
+
# 56.23413252, # 35
|
78
|
+
# 59.56621435, # 35.5
|
79
|
+
# 63.09573445, # 36
|
80
|
+
# 66.83439176, # 36.5
|
81
|
+
# 70.79457844, # 37
|
82
|
+
# 74.98942093, # 37.5
|
83
|
+
# 79.43282347, # 38
|
84
|
+
# 84.13951416, # 38.5
|
85
|
+
# 89.12509381, # 39
|
86
|
+
# 94.40608763, # 39.5
|
87
|
+
# 100.0, # 40 dB
|
88
|
+
# 105.9253725, # 40.5
|
89
|
+
# 112.2018454, # 41
|
90
|
+
# 118.8502227, # 41.5
|
91
|
+
# 125.8925412, # 42
|
92
|
+
# 133.3521432, # 42.5
|
93
|
+
# 141.2537545, # 43
|
94
|
+
# 149.6235656, # 43.5
|
95
|
+
# 158.4893192, # 44
|
96
|
+
# 167.8804018, # 44.5
|
97
|
+
# 177.827941, # 45
|
98
|
+
# 188.3649089, # 45.5
|
99
|
+
# 199.5262315, # 46
|
100
|
+
# 211.348904, # 46.5
|
101
|
+
# 223.8721139, # 47
|
102
|
+
# 237.1373706, # 47.5
|
103
|
+
# 251.1886432, # 48
|
104
|
+
# 266.072506, # 48.5
|
105
|
+
# 281.8382931, # 49
|
106
|
+
# 298.5382619, # 49.5
|
107
|
+
# 316.227766, # 50 dB
|
108
|
+
# 334.9654392, # 50.5
|
109
|
+
# 354.8133892, # 51
|
110
|
+
# 375.8374043, # 51.5
|
111
|
+
# 398.1071706, # 52
|
112
|
+
# 421.6965034, # 52.5
|
113
|
+
# 446.6835922, # 53
|
114
|
+
# 473.151259, # 53.5
|
115
|
+
# 501.1872336, # 54
|
116
|
+
# 530.8844442, # 54.5
|
117
|
+
# 562.3413252, # 55
|
118
|
+
# 595.6621435, # 55.5
|
119
|
+
# 630.9573445, # 56
|
120
|
+
# 668.3439176, # 56.5
|
121
|
+
# 707.9457844, # 57
|
122
|
+
# 749.8942093, # 57.5
|
123
|
+
# 794.3282347, # 58
|
124
|
+
# 841.3951416, # 58.5
|
125
|
+
# 891.2509381, # 59
|
126
|
+
# 944.0608763, # 59.5
|
127
|
+
# 1000.0, # 60 dB
|
128
|
+
# 1059.253725, # 60.5
|
129
|
+
# 1122.018454, # 61
|
130
|
+
# 1188.502227, # 61.5
|
131
|
+
# 1258.925412, # 62
|
132
|
+
# 1333.521432, # 62.5
|
133
|
+
# 1412.537545, # 63
|
134
|
+
# 1496.235656, # 63.5
|
135
|
+
# 1584.893192, # 64
|
136
|
+
# 1678.804018, # 64.5
|
137
|
+
# 1778.27941, # 65
|
138
|
+
# 1883.649089, # 65.5
|
139
|
+
# 1995.262315, # 66
|
140
|
+
# 2113.48904, # 66.5
|
141
|
+
# 2238.721139, # 67
|
142
|
+
# 2371.373706, # 67.5
|
143
|
+
# 2511.886432, # 68
|
144
|
+
# 2660.72506, # 68.5
|
145
|
+
# 2818.382931, # 69
|
146
|
+
# 2985.382619, # 69.5
|
147
|
+
# 3162.27766, # 70 dB
|
148
|
+
# 3349.654392, # 70.5
|
149
|
+
# 3548.133892, # 71
|
150
|
+
# 3758.374043, # 71.5
|
151
|
+
# 3981.071706, # 72
|
152
|
+
#]
|
153
|
+
#
|
154
|
+
#def self.db_to_linear db
|
155
|
+
# raise ArgumentError, "#{db} db is not between -#{MAX_DB} and #{MAX_DB}" unless db.between(-MAX_DB, MAX_DB)
|
156
|
+
# db_half_step = (dB * 2.0).to_i
|
157
|
+
#
|
158
|
+
# if(db_half_step >= 0)
|
159
|
+
# linear = DB_TO_SCALAR_TABLE[db_half_step]
|
160
|
+
# elsif
|
161
|
+
# linear = DB_TO_SCALAR_TABLE[-db_half_step]
|
162
|
+
# scalar = 1.0 / scalar
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# return scalar
|
166
|
+
#end
|
167
|
+
|
168
|
+
MAX_DB_ABS = 6000.0
|
169
|
+
|
170
|
+
def self.db_to_linear db
|
171
|
+
db_abs = db.abs
|
172
|
+
raise ArgumentError, "|db| is #{db_abs}, which is greater than the allowed #{MAX_DB_ABS}" if db_abs > MAX_DB_ABS
|
173
|
+
return 10.0**(db / 20.0)
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.linear_to_db linear
|
177
|
+
raise ArgumentError, "linear value #{linear} is less than or equal to 0.0" if linear <= 0.0
|
178
|
+
return 20.0 * Math::log10(linear)
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SPCore
|
2
|
+
class Interpolation
|
3
|
+
# Linear Interpolation Equation:
|
4
|
+
#
|
5
|
+
# (x3 - x1)(y2 - y1)
|
6
|
+
# y3 = ------------------ + y1
|
7
|
+
# (x2 - x1)
|
8
|
+
#
|
9
|
+
def self.linear x1, y1, x2, y2, x3
|
10
|
+
y3 = ((x3 - x1) * (y2 - y1)) / (x2 - x1);
|
11
|
+
y3 += y1;
|
12
|
+
return y3;
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module SPCore
|
2
|
+
class Oscillator
|
3
|
+
include Hashmake::HashMakeable
|
4
|
+
attr_accessor :wave_type, :amplitude, :dc_offset
|
5
|
+
attr_reader :frequency, :sample_rate, :phase_offset
|
6
|
+
|
7
|
+
WAVE_SINE = :waveSine
|
8
|
+
WAVE_TRIANGLE = :waveTriangle
|
9
|
+
WAVE_SAWTOOTH = :waveSawtooth
|
10
|
+
WAVE_SQUARE = :waveSquare
|
11
|
+
|
12
|
+
WAVES = [WAVE_SINE, WAVE_TRIANGLE, WAVE_SAWTOOTH, WAVE_SQUARE]
|
13
|
+
|
14
|
+
ARG_SPECS = [
|
15
|
+
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
16
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :wave_type, :type => Symbol, :default => WAVE_SINE, :validator => ->(a){ WAVES.include? a } ),
|
17
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :frequency, :type => Float, :default => 1.0, :validator => ->(a){ a > 0.0 } ),
|
18
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :amplitude, :type => Float, :default => 1.0 ),
|
19
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :phase_offset, :type => Float, :default => 0.0 ),
|
20
|
+
Hashmake::ArgSpec.new(:reqd => false, :key => :dc_offset, :type => Float, :default => 0.0 ),
|
21
|
+
]
|
22
|
+
|
23
|
+
def initialize args
|
24
|
+
hash_make Oscillator::ARG_SPECS, args
|
25
|
+
|
26
|
+
@phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
|
27
|
+
@current_phase_angle = @phase_offset
|
28
|
+
end
|
29
|
+
|
30
|
+
def sample_rate= sample_rate
|
31
|
+
@sample_rate = sample_rate
|
32
|
+
self.frequency = @frequency
|
33
|
+
end
|
34
|
+
|
35
|
+
def frequency= frequency
|
36
|
+
@frequency = frequency
|
37
|
+
@phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
|
38
|
+
end
|
39
|
+
|
40
|
+
def phase_offset= phase_offset
|
41
|
+
@current_phase_angle += (phase_offset - @phase_offset);
|
42
|
+
@phase_offset = phase_offset
|
43
|
+
end
|
44
|
+
|
45
|
+
def sample
|
46
|
+
output = 0.0
|
47
|
+
|
48
|
+
while(@current_phase_angle < NEG_PI)
|
49
|
+
@current_phase_angle += TWO_PI
|
50
|
+
end
|
51
|
+
|
52
|
+
while(@current_phase_angle > Math::PI)
|
53
|
+
@current_phase_angle -= TWO_PI
|
54
|
+
end
|
55
|
+
|
56
|
+
case @wave_type
|
57
|
+
when WAVE_SINE
|
58
|
+
output = @amplitude * sine(@current_phase_angle) + @dc_offset
|
59
|
+
when WAVE_TRIANGLE
|
60
|
+
output = @amplitude * triangle(@current_phase_angle) + @dc_offset
|
61
|
+
when WAVE_SQUARE
|
62
|
+
output = @amplitude * square(@current_phase_angle) + @dc_offset
|
63
|
+
when WAVE_SAWTOOTH
|
64
|
+
output = @amplitude * sawtooth(@current_phase_angle) + @dc_offset
|
65
|
+
else
|
66
|
+
raise "Encountered unexpected wave type #{@wave_type}"
|
67
|
+
end
|
68
|
+
|
69
|
+
@current_phase_angle += @phase_angle_incr
|
70
|
+
return output
|
71
|
+
end
|
72
|
+
|
73
|
+
K_SINE_B = 4.0 / Math::PI
|
74
|
+
K_SINE_C = -4.0 / (Math::PI * Math::PI)
|
75
|
+
# Q = 0.775
|
76
|
+
K_SINE_P = 0.225
|
77
|
+
|
78
|
+
# generate a sine wave:
|
79
|
+
# input range: -PI to PI
|
80
|
+
# ouput range: -1 to 1
|
81
|
+
def sine x
|
82
|
+
y = K_SINE_B * x + K_SINE_C * x * x.abs
|
83
|
+
# for extra precision
|
84
|
+
y = K_SINE_P * (y * y.abs - y) + y # Q * y + P * y * y.abs
|
85
|
+
|
86
|
+
# sin normally output outputs -1 to 1, so to adjust
|
87
|
+
# it to output 0 to 1, return (y*0.5)+0.5
|
88
|
+
return y
|
89
|
+
end
|
90
|
+
|
91
|
+
K_TRIANGLE_A = 2.0 / Math::PI;
|
92
|
+
|
93
|
+
# generate a triangle wave:
|
94
|
+
# input range: -PI to PI
|
95
|
+
# ouput range: -1 to 1
|
96
|
+
def triangle x
|
97
|
+
(K_TRIANGLE_A * x).abs - 1.0
|
98
|
+
end
|
99
|
+
|
100
|
+
# generate a square wave (50% duty cycle):
|
101
|
+
# input range: -PI to PI
|
102
|
+
# ouput range: 0 to 1
|
103
|
+
def square x
|
104
|
+
(x >= 0.0) ? 1.0 : -1.0
|
105
|
+
end
|
106
|
+
|
107
|
+
K_SAWTOOTH_A = 1.0 / Math::PI
|
108
|
+
|
109
|
+
# generate a sawtooth wave:
|
110
|
+
# input range: -PI to PI
|
111
|
+
# ouput range: -1 to 1
|
112
|
+
def sawtooth x
|
113
|
+
K_SAWTOOTH_A * x
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|