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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module SPCore
2
+ # spcore version
3
+ VERSION = "0.1.0"
4
+ end
@@ -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