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,45 @@
|
|
1
|
+
module SPCore
|
2
|
+
class Saturation
|
3
|
+
# Sigmoid-based saturation when input is above threshold.
|
4
|
+
# From musicdsp.org, posted by Bram.
|
5
|
+
def self.sigmoid input, threshold
|
6
|
+
input_abs = input.abs
|
7
|
+
if input_abs < threshold
|
8
|
+
return input
|
9
|
+
else
|
10
|
+
#y = threshold + (1.0 - threshold) * mock_sigmoid((input_abs - threshold) / ((1.0 - threshold) * 1.5))
|
11
|
+
y = threshold + (1.0 - threshold) * Math::tanh((input_abs - threshold)/(1-threshold))
|
12
|
+
|
13
|
+
if input > 0.0
|
14
|
+
return y
|
15
|
+
else
|
16
|
+
return -y
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.gompertz input, threshold
|
22
|
+
a = threshold
|
23
|
+
b = -4
|
24
|
+
c = -2
|
25
|
+
x = input.abs
|
26
|
+
y = 2 * a * Math::exp(b * Math::exp(c * x))
|
27
|
+
|
28
|
+
if input > 0.0
|
29
|
+
return y
|
30
|
+
else
|
31
|
+
return -y
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
#def self.mock_sigmoid x
|
38
|
+
# if(x.abs < 1.0)
|
39
|
+
# return x * (1.5 - 0.5 * x * x)
|
40
|
+
# else
|
41
|
+
# return x > 0.0 ? 1.0 : -1.0
|
42
|
+
# end
|
43
|
+
#end
|
44
|
+
end
|
45
|
+
end
|
data/spcore.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path('../lib/spcore/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "spcore"
|
7
|
+
gem.version = SPCore::VERSION
|
8
|
+
gem.summary = %q{Perform basic signal processing functions (delay line, filters, envelope detection, etc...).}
|
9
|
+
gem.description = <<DESCRIPTION
|
10
|
+
Contains core signal processing functions, including:
|
11
|
+
Delay line
|
12
|
+
Biquad filters
|
13
|
+
Envelope detector
|
14
|
+
Conversion from dB-linear and linear-dB
|
15
|
+
Linear interpolation
|
16
|
+
Oscillator with selectable wave type (sine, square, triangle, sawtooth)
|
17
|
+
|
18
|
+
DESCRIPTION
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.authors = ["James Tunnell"]
|
21
|
+
gem.email = "jamestunnell@lavabit.com"
|
22
|
+
gem.homepage = "https://rubygems.org/gems/spcore"
|
23
|
+
|
24
|
+
gem.files = `git ls-files`.split($/)
|
25
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
26
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
27
|
+
gem.require_paths = ['lib']
|
28
|
+
|
29
|
+
gem.add_dependency 'hashmake'
|
30
|
+
|
31
|
+
gem.add_development_dependency 'bundler', '~> 1.0'
|
32
|
+
gem.add_development_dependency 'rake', '~> 0.8'
|
33
|
+
gem.add_development_dependency 'rspec', '~> 2.4'
|
34
|
+
gem.add_development_dependency 'yard', '~> 0.8'
|
35
|
+
gem.add_development_dependency 'pry'
|
36
|
+
gem.add_development_dependency 'gnuplot'
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::Limiters do
|
4
|
+
describe '.make_no_limiter' do
|
5
|
+
it 'should make a lambda that does not limit values' do
|
6
|
+
limiter = SPCore::Limiters.make_no_limiter
|
7
|
+
limiter.call(Float::MAX).should eq(Float::MAX)
|
8
|
+
limiter.call(-Float::MAX).should eq(-Float::MAX)
|
9
|
+
limiter.call(Float::MIN).should eq(Float::MIN)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '.make_lower_limiter' do
|
14
|
+
it 'should make a lambda that limits values to be above the limit value' do
|
15
|
+
limiter = SPCore::Limiters.make_lower_limiter 5.0
|
16
|
+
limiter.call(4.5).should eq(5.0)
|
17
|
+
limiter.call(5.5).should eq(5.5)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'make_upper_limiter' do
|
22
|
+
it 'should make a lambda that limits values to be above the limit value' do
|
23
|
+
limiter = SPCore::Limiters.make_upper_limiter 5.0
|
24
|
+
limiter.call(5.5).should eq(5.0)
|
25
|
+
limiter.call(4.5).should eq(4.5)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.make_range_limiter' do
|
30
|
+
it 'should make a lambda that limits values to be between the limit range' do
|
31
|
+
limiter = SPCore::Limiters.make_range_limiter(2.5..5.0)
|
32
|
+
limiter.call(1.5).should eq(2.5)
|
33
|
+
limiter.call(5.5).should eq(5.0)
|
34
|
+
limiter.call(3.0).should eq(3.0)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::CircularBuffer do
|
4
|
+
context '.new' do
|
5
|
+
it 'should set buffer size but fill count should be zero' do
|
6
|
+
[0,20,100].each do |size|
|
7
|
+
buffer = SPCore::CircularBuffer.new size
|
8
|
+
buffer.empty?.should be_true
|
9
|
+
buffer.size.should eq(size)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#push' do
|
15
|
+
it 'should report full after buffer.size calls to #push' do
|
16
|
+
[0,20,100].each do |size|
|
17
|
+
buffer = SPCore::CircularBuffer.new size
|
18
|
+
|
19
|
+
size.times do
|
20
|
+
buffer.push rand
|
21
|
+
end
|
22
|
+
|
23
|
+
buffer.full?.should be_true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#push_ary' do
|
29
|
+
it 'should add the given array' do
|
30
|
+
elements = [1,2,3,4,5,6]
|
31
|
+
buffer = SPCore::CircularBuffer.new elements.count
|
32
|
+
buffer.push_ary elements
|
33
|
+
buffer.to_ary.should eq(elements)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#pop' do
|
38
|
+
it 'should, with fifo set to true, after a series of pushes, report the first element pushed' do
|
39
|
+
elements = [1,2,3,4,5]
|
40
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => true
|
41
|
+
elements.each do |element|
|
42
|
+
buffer.push element
|
43
|
+
end
|
44
|
+
buffer.pop.should eq(elements.first)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should, with fifo set to false, after a series of pushes, report the last element pushed' do
|
48
|
+
elements = [1,2,3,4,5]
|
49
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => false
|
50
|
+
elements.each do |element|
|
51
|
+
buffer.push element
|
52
|
+
end
|
53
|
+
buffer.pop.should eq(elements.last)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#newest' do
|
58
|
+
it 'should, after a series of pushes, report the last element pushed' do
|
59
|
+
elements = [1,2,3,4,5]
|
60
|
+
buffer = SPCore::CircularBuffer.new elements.count
|
61
|
+
elements.each do |element|
|
62
|
+
buffer.push element
|
63
|
+
end
|
64
|
+
buffer.newest.should eq(elements.last)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should, with fifo set to true, after a series of pushes and then a pop, report the last element pushed' do
|
68
|
+
elements = [1,2,3,4,5]
|
69
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => true
|
70
|
+
elements.each do |element|
|
71
|
+
buffer.push element
|
72
|
+
end
|
73
|
+
buffer.pop
|
74
|
+
buffer.newest.should eq(elements.last)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should, with fifo set to false, after a series of pushes and then a pop, report the second to last element pushed' do
|
78
|
+
elements = [1,2,3,4,5]
|
79
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => false
|
80
|
+
elements.each do |element|
|
81
|
+
buffer.push element
|
82
|
+
end
|
83
|
+
buffer.pop
|
84
|
+
buffer.newest.should eq(elements[-2])
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should, when given a relative index, report the element reverse-indexed from the newest' do
|
88
|
+
elements = [1,2,3,4,5]
|
89
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => false
|
90
|
+
elements.each do |element|
|
91
|
+
buffer.push element
|
92
|
+
end
|
93
|
+
|
94
|
+
for i in 0...elements.count do
|
95
|
+
buffer.newest(i).should eq(elements[elements.count - 1 - i])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#oldest' do
|
101
|
+
it 'should, after a series of pushes, report the first element pushed' do
|
102
|
+
elements = [1,2,3,4,5]
|
103
|
+
buffer = SPCore::CircularBuffer.new elements.count
|
104
|
+
elements.each do |element|
|
105
|
+
buffer.push element
|
106
|
+
end
|
107
|
+
buffer.oldest.should eq(elements.first)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should, with fifo set to true, after a series of pushes and then a pop, report the second element pushed' do
|
111
|
+
elements = [1,2,3,4,5]
|
112
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => true
|
113
|
+
elements.each do |element|
|
114
|
+
buffer.push element
|
115
|
+
end
|
116
|
+
buffer.pop
|
117
|
+
buffer.oldest.should eq(elements[1])
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should, with fifo set to false, after a series of pushes and then a pop, report the first element pushed' do
|
121
|
+
elements = [1,2,3,4,5]
|
122
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => false
|
123
|
+
elements.each do |element|
|
124
|
+
buffer.push element
|
125
|
+
end
|
126
|
+
buffer.pop
|
127
|
+
buffer.oldest.should eq(elements.first)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should, when given a relative index, report the element reverse-indexed from the newest' do
|
131
|
+
elements = [1,2,3,4,5]
|
132
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => false
|
133
|
+
elements.each do |element|
|
134
|
+
buffer.push element
|
135
|
+
end
|
136
|
+
|
137
|
+
for i in 0...elements.count do
|
138
|
+
buffer.oldest(i).should eq(elements[i])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#to_ary' do
|
144
|
+
it 'should produce an empty array for an empty buffer' do
|
145
|
+
buffer = SPCore::CircularBuffer.new 10
|
146
|
+
buffer.to_ary.should be_empty
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should, after pushing an array of elements, produce an array of the same elements' do
|
150
|
+
elements = [1,2,3,4,5]
|
151
|
+
buffer = SPCore::CircularBuffer.new elements.count, :fifo => true
|
152
|
+
elements.each do |element|
|
153
|
+
buffer.push element
|
154
|
+
end
|
155
|
+
buffer.to_ary.should eq(elements)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'should, after pushing and popping an array of elements several times and then pushing the array one last time, produce an array of the same elements' do
|
159
|
+
elements = [1,2,3,4,5]
|
160
|
+
buffer = SPCore::CircularBuffer.new(3 * elements.count, :fifo => true)
|
161
|
+
|
162
|
+
5.times do
|
163
|
+
elements.each do |element|
|
164
|
+
buffer.push element
|
165
|
+
end
|
166
|
+
elements.count.times do
|
167
|
+
buffer.pop
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
elements.each do |element|
|
172
|
+
buffer.push element
|
173
|
+
end
|
174
|
+
buffer.to_ary.should eq(elements)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'gnuplot'
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
describe 'cookbook filter' do
|
6
|
+
#it 'should produce a nice frequency response graph' do
|
7
|
+
# sample_rate = 44000.0
|
8
|
+
# crit_freq = 1000.0
|
9
|
+
# max_test_freq = 10000.0
|
10
|
+
# bw = 2
|
11
|
+
# filter = SPCore::CookbookNotchFilter.new sample_rate
|
12
|
+
# filter.set_critical_freq_and_bw crit_freq, bw
|
13
|
+
#
|
14
|
+
# freqs = []
|
15
|
+
# dbs = []
|
16
|
+
#
|
17
|
+
# start_freq = 10.0
|
18
|
+
# test_freq = start_freq
|
19
|
+
#
|
20
|
+
# 200.times do
|
21
|
+
# mag = filter.get_freq_magnitude_response test_freq
|
22
|
+
#
|
23
|
+
# dbs << SPCore::Gain.linear_to_db(mag)
|
24
|
+
# freqs << test_freq
|
25
|
+
#
|
26
|
+
# test_freq *= 1.035
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Gnuplot.open do |gp|
|
30
|
+
# Gnuplot::Plot.new(gp) do |plot|
|
31
|
+
# plot.title "Frequency Magnitude Response for Lowpass Filter with Critical Freq of #{crit_freq} and BW of #{bw}"
|
32
|
+
# plot.xlabel "Frequency (f)"
|
33
|
+
# plot.ylabel "Frequency Magnitude Response (dB) at f"
|
34
|
+
# plot.logscale 'x'
|
35
|
+
#
|
36
|
+
# plot.data << Gnuplot::DataSet.new( [freqs, dbs] ) do |ds|
|
37
|
+
# ds.with = "linespoints"
|
38
|
+
# #ds.linewidth = 4
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
#end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::DelayLine do
|
4
|
+
it 'should' do
|
5
|
+
SAMPLE_RATE = 44100.0
|
6
|
+
MAX_DELAY_SEC = 0.1
|
7
|
+
|
8
|
+
5.times do
|
9
|
+
delay_line = SPCore::DelayLine.new(
|
10
|
+
:sample_rate => SAMPLE_RATE,
|
11
|
+
:max_delay_seconds => MAX_DELAY_SEC,
|
12
|
+
:delay_seconds => (rand * MAX_DELAY_SEC)
|
13
|
+
)
|
14
|
+
|
15
|
+
rand_sample = rand
|
16
|
+
delay_line.push_sample rand_sample
|
17
|
+
delay_line.delay_samples.times do
|
18
|
+
delay_line.push_sample 0.0
|
19
|
+
end
|
20
|
+
|
21
|
+
delay_line.delayed_sample.should eq(rand_sample)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::EnvelopeDetector do
|
4
|
+
describe '#process_sample' do
|
5
|
+
it 'should produce an output that follows the amplitude of the input' do
|
6
|
+
sample_rate = 10000.0
|
7
|
+
freqs = [20.0, 200.0, 2000.0]
|
8
|
+
|
9
|
+
envelope_start = 1.0
|
10
|
+
envelope_end = 0.0
|
11
|
+
|
12
|
+
freqs.each do |freq|
|
13
|
+
osc = SPCore::Oscillator.new :sample_rate => sample_rate, :frequency => freq, :amplitude => envelope_start
|
14
|
+
detector = SPCore::EnvelopeDetector.new :sample_rate => sample_rate, :attack_time => (1e-2 / freq), :release_time => (1.0 / freq)
|
15
|
+
|
16
|
+
# 1 full period to acclimate the detector to the starting envelope
|
17
|
+
(sample_rate / freq).to_i .times do
|
18
|
+
detector.process_sample osc.sample
|
19
|
+
end
|
20
|
+
|
21
|
+
input = []
|
22
|
+
output = []
|
23
|
+
envelope = []
|
24
|
+
|
25
|
+
# 5 full periods to track the envelope as it changes
|
26
|
+
sample_count = (5.0 * sample_rate / freq).to_i
|
27
|
+
sample_count.times do |n|
|
28
|
+
percent = n.to_f / sample_count
|
29
|
+
amplitude = SPCore::Interpolation.linear 0.0, envelope_start, 1.0, envelope_end, percent
|
30
|
+
osc.amplitude = amplitude
|
31
|
+
|
32
|
+
sample = osc.sample
|
33
|
+
env = detector.process_sample sample
|
34
|
+
|
35
|
+
input << n
|
36
|
+
output << sample
|
37
|
+
envelope << env
|
38
|
+
|
39
|
+
env.should be_within(0.25).of(amplitude)
|
40
|
+
end
|
41
|
+
|
42
|
+
## plot the data
|
43
|
+
#
|
44
|
+
#Gnuplot.open do |gp|
|
45
|
+
# Gnuplot::Plot.new( gp ) do |plot|
|
46
|
+
#
|
47
|
+
# plot.title "Signal and Envelope"
|
48
|
+
# plot.ylabel "sample n"
|
49
|
+
# plot.xlabel "y[n]"
|
50
|
+
#
|
51
|
+
# plot.data = [
|
52
|
+
# Gnuplot::DataSet.new( [input, output] ) { |ds|
|
53
|
+
# ds.with = "lines"
|
54
|
+
# ds.title = "Signal"
|
55
|
+
# ds.linewidth = 2
|
56
|
+
# },
|
57
|
+
#
|
58
|
+
# Gnuplot::DataSet.new( [ input, envelope ] ) { |ds|
|
59
|
+
# ds.with = "lines"
|
60
|
+
# ds.title = "Envelope"
|
61
|
+
# ds.linewidth = 2
|
62
|
+
# }
|
63
|
+
# ]
|
64
|
+
#
|
65
|
+
# end
|
66
|
+
#end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|