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,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
|