spcore 0.1.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.
- 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
|