spcore 0.2.0 → 0.2.1
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/ChangeLog.rdoc +5 -1
- data/lib/spcore.rb +9 -6
- data/lib/spcore/analysis/calculus.rb +38 -0
- data/lib/spcore/analysis/features.rb +186 -0
- data/lib/spcore/analysis/frequency_domain.rb +191 -0
- data/lib/spcore/analysis/{correlation.rb → statistics.rb} +41 -18
- data/lib/spcore/core/delay_line.rb +1 -1
- data/lib/spcore/filters/fir/dual_sinc_filter.rb +1 -1
- data/lib/spcore/filters/fir/sinc_filter.rb +1 -1
- data/lib/spcore/generation/comb_filter.rb +65 -0
- data/lib/spcore/{core → generation}/oscillator.rb +1 -1
- data/lib/spcore/{util → generation}/signal_generator.rb +1 -1
- data/lib/spcore/util/envelope_detector.rb +1 -1
- data/lib/spcore/util/gain.rb +9 -167
- data/lib/spcore/util/plotter.rb +1 -1
- data/lib/spcore/{analysis → util}/signal.rb +116 -127
- data/lib/spcore/version.rb +1 -1
- data/spcore.gemspec +2 -0
- data/spec/analysis/calculus_spec.rb +54 -0
- data/spec/analysis/features_spec.rb +106 -0
- data/spec/analysis/frequency_domain_spec.rb +147 -0
- data/spec/analysis/piano_C4.wav +0 -0
- data/spec/analysis/statistics_spec.rb +61 -0
- data/spec/analysis/trumpet_B4.wav +0 -0
- data/spec/generation/comb_filter_spec.rb +37 -0
- data/spec/{core → generation}/oscillator_spec.rb +0 -0
- data/spec/{util → generation}/signal_generator_spec.rb +0 -0
- data/spec/interpolation/interpolation_spec.rb +0 -2
- data/spec/{analysis → util}/signal_spec.rb +1 -35
- metadata +64 -22
- data/lib/spcore/analysis/envelope.rb +0 -76
- data/lib/spcore/analysis/extrema.rb +0 -55
- data/spec/analysis/correlation_spec.rb +0 -28
- data/spec/analysis/envelope_spec.rb +0 -50
- data/spec/analysis/extrema_spec.rb +0 -42
data/lib/spcore/version.rb
CHANGED
data/spcore.gemspec
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::Calculus do
|
4
|
+
before :all do
|
5
|
+
sample_rate = 200
|
6
|
+
@sample_period = 1.0 / sample_rate
|
7
|
+
sample_count = sample_rate
|
8
|
+
|
9
|
+
range = -Math::PI..Math::PI
|
10
|
+
delta = (range.max - range.min) / sample_count
|
11
|
+
|
12
|
+
@sin = []
|
13
|
+
@cos = []
|
14
|
+
|
15
|
+
range.step(delta) do |x|
|
16
|
+
@sin << Math::sin(x)
|
17
|
+
@cos << (Math::PI * 2 * Math::cos(x))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '.derivative' do
|
22
|
+
before :all do
|
23
|
+
@expected = @cos
|
24
|
+
@actual = Calculus.derivative @sin, @sample_period
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should produce a signal of same size' do
|
28
|
+
@actual.size.should eq @expected.size
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should produce a signal matching the 1st derivative' do
|
32
|
+
@actual.each_index do |i|
|
33
|
+
@actual[i].should be_within(0.1).of(@expected[i])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '.integral' do
|
39
|
+
before :all do
|
40
|
+
@expected = @sin
|
41
|
+
@actual = Calculus.integral @cos, @sample_period
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should produce a signal of same size' do
|
45
|
+
@actual.size.should eq @expected.size
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should produce a signal matching the 1st derivative' do
|
49
|
+
@actual.each_index do |i|
|
50
|
+
@actual[i].should be_within(0.1).of(@expected[i])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::Features do
|
4
|
+
context '.minima' do
|
5
|
+
it 'should return points where local and global minima occur' do
|
6
|
+
cases = {
|
7
|
+
[3.8, 3.0, 2.9, 2.95, 3.6, 3.4, 2.8, 2.3, 2.1, 2.0, 2.5] => { 2 => 2.9, 9 => 2.0 },
|
8
|
+
[3.2, 3.5, 2.9, 2.7, 2.8, 2.7, 2.5, 2.2, 2.4, 2.3, 2.0] => { 3 => 2.7, 7 => 2.2, 10 => 2.0 },
|
9
|
+
}
|
10
|
+
|
11
|
+
cases.each do |samples, minima|
|
12
|
+
Features.minima(samples).should eq minima
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context '.maxima' do
|
18
|
+
it 'should return points where local and global maxima occur' do
|
19
|
+
cases = {
|
20
|
+
[3.8, 3.0, 2.9, 2.95, 3.6, 3.4, 2.8, 2.3, 2.1, 2.0, 2.5] => { 0 => 3.8, 4 => 3.6 },
|
21
|
+
[3.2, 3.5, 2.9, 2.7, 2.8, 2.7, 2.5, 2.2, 2.4, 2.3, 2.0] => { 1 => 3.5, 4 => 2.8, 8 => 2.4},
|
22
|
+
}
|
23
|
+
|
24
|
+
cases.each do |samples, maxima|
|
25
|
+
Features.maxima(samples).should eq maxima
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context '.extrema' do
|
31
|
+
it 'should return points where local and global extrema occur' do
|
32
|
+
cases = {
|
33
|
+
[3.8, 3.0, 2.9, 2.95, 3.6, 3.4, 2.8, 2.3, 2.1, 2.0, 2.5] => { 0 => 3.8, 2 => 2.9, 4 => 3.6, 9 => 2.0},
|
34
|
+
[3.2, 3.5, 2.9, 2.7, 2.8, 2.7, 2.5, 2.2, 2.4, 2.3, 2.0] => { 1 => 3.5, 3 => 2.7, 4 => 2.8, 7 => 2.2, 8 => 2.4, 10 => 2.0},
|
35
|
+
}
|
36
|
+
|
37
|
+
cases.each do |samples, extrema|
|
38
|
+
Features.extrema(samples).should eq extrema
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.top_n' do
|
44
|
+
it 'should return the n greatest of the given values' do
|
45
|
+
cases = {
|
46
|
+
[[1,2,3,4,5,6],2] => [5,6],
|
47
|
+
[[13,2,32,42,75,6],4] => [13,32,42,75],
|
48
|
+
[[-11,21,-4],1] => [21],
|
49
|
+
[[-11,21,-4, -14],2] => [-4,21],
|
50
|
+
}
|
51
|
+
|
52
|
+
cases.each do |inputs, expected_output|
|
53
|
+
Features.top_n(inputs[0], inputs[1]).should eq expected_output
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '.envelope' do
|
59
|
+
before :all do
|
60
|
+
sample_rate = 1000
|
61
|
+
sample_count = 512 * 2
|
62
|
+
generator = SignalGenerator.new(:size => sample_count, :sample_rate => sample_rate)
|
63
|
+
|
64
|
+
@modulation_signal = generator.make_signal [4.0], :amplitude => 0.1
|
65
|
+
@modulation_signal.multiply! BlackmanWindow.new(sample_count).data
|
66
|
+
|
67
|
+
@base_signal = generator.make_signal [64.0]
|
68
|
+
@base_signal.multiply! @modulation_signal
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should produce an output that follows the amplitude of the input' do
|
72
|
+
envelope = @base_signal.envelope
|
73
|
+
check_envelope(envelope)
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_envelope envelope
|
77
|
+
#signals = {
|
78
|
+
# "signal" => @base_signal,
|
79
|
+
# "modulation (abs)" => @modulation_signal.abs,
|
80
|
+
# "envelope" => envelope,
|
81
|
+
#}
|
82
|
+
#
|
83
|
+
#Plotter.new(
|
84
|
+
# :title => "signal and envelope",
|
85
|
+
# :xlabel => "sample",
|
86
|
+
# :ylabel => "values",
|
87
|
+
#).plot_signals(signals)
|
88
|
+
|
89
|
+
#Plotter.new().plot_2d("envelop freq magnitudes" => envelope.freq_magnitudes)
|
90
|
+
|
91
|
+
begin
|
92
|
+
ideal = @modulation_signal.energy
|
93
|
+
actual = envelope.energy
|
94
|
+
error = (ideal - actual).abs / ideal
|
95
|
+
error.should be_within(0.1).of(0.0)
|
96
|
+
end
|
97
|
+
|
98
|
+
begin
|
99
|
+
ideal = @modulation_signal.rms
|
100
|
+
actual = envelope.rms
|
101
|
+
error = (ideal - actual).abs / ideal
|
102
|
+
error.should be_within(0.1).of(0.0)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'wavefile'
|
3
|
+
|
4
|
+
describe SPCore::FrequencyDomain do
|
5
|
+
before :all do
|
6
|
+
@freq_tolerance = 0.1
|
7
|
+
end
|
8
|
+
|
9
|
+
def check_tolerance ideal, actual, tolerance_percent
|
10
|
+
margin = ideal * tolerance_percent
|
11
|
+
actual.should be_between(ideal - margin, ideal + margin)
|
12
|
+
end
|
13
|
+
|
14
|
+
def verify_harmonic_series series, tolerance_percent
|
15
|
+
fund = series.first
|
16
|
+
for i in 1...series.count
|
17
|
+
freq = series[i]
|
18
|
+
ratio = freq / fund
|
19
|
+
target = ratio * fund
|
20
|
+
|
21
|
+
check_tolerance target, freq, tolerance_percent
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '.peaks' do
|
26
|
+
it 'should find the peak frequency components' do
|
27
|
+
cases = [
|
28
|
+
[20.0, 80.0, 125.0],
|
29
|
+
]
|
30
|
+
|
31
|
+
cases.each do |ideal_peaks|
|
32
|
+
generator = SignalGenerator.new(:sample_rate => 1000, :size => 256)
|
33
|
+
signal = generator.make_signal(ideal_peaks)
|
34
|
+
actual_peaks = signal.freq_peaks
|
35
|
+
|
36
|
+
ideal_peaks.each do |ideal_freq|
|
37
|
+
found = false
|
38
|
+
actual_peaks.keys.each do |actual_freq|
|
39
|
+
percent_error = (ideal_freq - actual_freq).abs / ideal_freq
|
40
|
+
if percent_error < @freq_tolerance
|
41
|
+
found = true
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
found.should be_true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '.harmonic_series' do
|
52
|
+
context 'harmonic series present' do
|
53
|
+
it 'should find the fundamental component when a harmonic series is present along with other non-series components' do
|
54
|
+
cases = {
|
55
|
+
[30.0, 50.0, 100.0, 125.0, 150.0, 200.0, 275.0] => 50.0,
|
56
|
+
[15.0, 20.0, 40.0, 50.0, 60.0, 80.0] => 20.0,
|
57
|
+
[25.0, 50.0, 85.0] => 25.0,
|
58
|
+
}
|
59
|
+
|
60
|
+
cases.each do |freqs, fundamental|
|
61
|
+
generator = SignalGenerator.new(:sample_rate => 600, :size => 1024)
|
62
|
+
signal = generator.make_signal(freqs)
|
63
|
+
|
64
|
+
series = signal.harmonic_series(:min_freq => 12.0)
|
65
|
+
check_tolerance fundamental, series.first, @freq_tolerance
|
66
|
+
verify_harmonic_series series, @freq_tolerance
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should find all the components when only a single harmonic series is present' do
|
71
|
+
cases = {
|
72
|
+
[30.0, 60.0, 90.0, 120.0] => 30.0,
|
73
|
+
[25.0, 50.0] => 25.0,
|
74
|
+
[50.0, 100.0, 150.0, 200.0] => 50.0,
|
75
|
+
}
|
76
|
+
|
77
|
+
cases.each do |ideal_freqs, fundamental|
|
78
|
+
generator = SignalGenerator.new(:sample_rate => 1000, :size => 512)
|
79
|
+
signal = generator.make_signal(ideal_freqs)
|
80
|
+
|
81
|
+
series = signal.harmonic_series(:min_freq => 16.0)
|
82
|
+
check_tolerance fundamental, series.first, @freq_tolerance
|
83
|
+
verify_harmonic_series series, @freq_tolerance
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should find the fundamental component of the strongest harmonic series present when multiple series are present' do
|
88
|
+
cases = {
|
89
|
+
[33.0, 66.0, 99.0, 132.0, 150.0, 300.0, 450.0] => 33.0,
|
90
|
+
}
|
91
|
+
|
92
|
+
cases.each do |freqs, fundamental|
|
93
|
+
generator = SignalGenerator.new(:sample_rate => 1000, :size => 1024)
|
94
|
+
signal = generator.make_signal(freqs)
|
95
|
+
|
96
|
+
series = signal.harmonic_series(:min_freq => 16.0)
|
97
|
+
check_tolerance fundamental, series.first, @freq_tolerance
|
98
|
+
verify_harmonic_series series, @freq_tolerance
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'no harmonic series preset' do
|
104
|
+
it 'should return the strongest peak frequency' do
|
105
|
+
cases = [
|
106
|
+
{:strongest_peak => 37.0, :other_peaks => [80.0, 125.0]},
|
107
|
+
{:strongest_peak => 44.0, :other_peaks => [99.0, 155.0]}
|
108
|
+
]
|
109
|
+
|
110
|
+
generator = SignalGenerator.new(:sample_rate => 1000, :size => 512)
|
111
|
+
cases.each do |hash|
|
112
|
+
signal = generator.make_signal([hash[:strongest_peak]], :amplitude => 1.5)
|
113
|
+
signal.add!(generator.make_signal(hash[:other_peaks]))
|
114
|
+
signal.fundamental(:min_freq => 20.0).should be_within(5.0).of(hash[:strongest_peak])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'real-world sound files' do
|
120
|
+
it 'should produce a series with the expected fundamental freq' do
|
121
|
+
cases = {
|
122
|
+
"trumpet_B4.wav" => 493.883,
|
123
|
+
"piano_C4.wav" => 261.626,
|
124
|
+
}
|
125
|
+
|
126
|
+
cases.each do |filename,ideal_fundamental|
|
127
|
+
file_path = File.dirname(__FILE__) + "/#{filename}"
|
128
|
+
WaveFile::Reader.new(file_path) do |reader|
|
129
|
+
read_size = reader.total_sample_frames
|
130
|
+
sample_frames = reader.read(read_size).samples
|
131
|
+
|
132
|
+
# if multiple channels are in the file, only use the 1st channel
|
133
|
+
if reader.format.channels == 1
|
134
|
+
data = sample_frames
|
135
|
+
else
|
136
|
+
data = sample_frames.map { |sample_frame| sample_frame[0] }
|
137
|
+
end
|
138
|
+
|
139
|
+
signal = SPCore::Signal.new(:data => data, :sample_rate => reader.format.sample_rate)
|
140
|
+
series = signal.harmonic_series(:min_freq => 40.0)
|
141
|
+
check_tolerance ideal_fundamental, series.min, @freq_tolerance
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
Binary file
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::Statistics do
|
4
|
+
describe '.mean' do
|
5
|
+
it 'should calculate the proper mean' do
|
6
|
+
cases = {
|
7
|
+
[1,2,3,4,5] => 3,
|
8
|
+
[11,25,60,4,22,48,40] => 30,
|
9
|
+
[100,200] => 150,
|
10
|
+
}
|
11
|
+
|
12
|
+
cases.each do |values,ideal_mean|
|
13
|
+
actual_mean = Statistics.mean values
|
14
|
+
actual_mean.should eq(ideal_mean)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.std_dev' do
|
20
|
+
it 'should determine the proper standard deviation' do
|
21
|
+
cases = {
|
22
|
+
[4,2,5,8,6] => 2.24,
|
23
|
+
[2,4,4,4,5,5,7,9] => 2,
|
24
|
+
[1,2,3,4,5] => 1.414
|
25
|
+
}
|
26
|
+
|
27
|
+
cases.each do |inputs,expected_output|
|
28
|
+
actual_output = Statistics.std_dev inputs
|
29
|
+
actual_output.should be_within(0.01).of(expected_output)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '.correlation' do
|
35
|
+
context 'image => triangular window' do
|
36
|
+
before :all do
|
37
|
+
@size = size = 48
|
38
|
+
@triangle = TriangularWindow.new(size * 2).data
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'feature => rising ramp (half size of triangular window)' do
|
42
|
+
it 'should have maximum correlation at beginning' do
|
43
|
+
rising_ramp = Array.new(@size){|i| i / @size.to_f }
|
44
|
+
correlation = Statistics.correlation(@triangle, rising_ramp)
|
45
|
+
correlation.first.should eq(correlation.max)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'feature => falling ramp (half size of triangular window)' do
|
50
|
+
it 'should have maximum correlation at end' do
|
51
|
+
falling_ramp = Array.new(@size){|i| (@size - i) / @size.to_f }
|
52
|
+
correlation = Statistics.correlation(@triangle, falling_ramp)
|
53
|
+
correlation.last.should eq(correlation.max)
|
54
|
+
|
55
|
+
#Plotter.plot_1d "correlate falling ramp" => correlation
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
Binary file
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
#
|
3
|
+
#describe SPCore::CombFilter do
|
4
|
+
# describe '#frequency_response' do
|
5
|
+
# before :all do
|
6
|
+
# @filters = []
|
7
|
+
# [100].each do |frequency|
|
8
|
+
# [0.15,0.25,0.5,0.75,0.85].each do |alpha|
|
9
|
+
# @filters.push(
|
10
|
+
# CombFilter.new(
|
11
|
+
# :type => CombFilter::FEED_BACK,
|
12
|
+
# :alpha => alpha,
|
13
|
+
# :frequency => frequency
|
14
|
+
# )
|
15
|
+
# )
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# it 'should produce the number of samples given by sample_count' do
|
21
|
+
# [5,20,50].each do |sample_count|
|
22
|
+
# @filters.each do |filter|
|
23
|
+
# samples = filter.frequency_response(6000, sample_count)
|
24
|
+
# samples.count.should eq(sample_count)
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# it 'should...' do
|
30
|
+
# outputs = {}
|
31
|
+
# @filters.each do |filter|
|
32
|
+
# outputs["comb filter: alpha = #{filter.alpha}, freq = #{filter.frequency}"] = filter.frequency_response 6000, 512
|
33
|
+
# end
|
34
|
+
# Plotter.plot_1d outputs
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#end
|
File without changes
|
File without changes
|
@@ -12,40 +12,7 @@ describe SPCore::Signal do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
15
|
-
|
16
|
-
describe '#derivative' do
|
17
|
-
before :all do
|
18
|
-
sample_rate = 200
|
19
|
-
sample_period = 1.0 / sample_rate
|
20
|
-
sample_count = sample_rate
|
21
|
-
|
22
|
-
range = -Math::PI..Math::PI
|
23
|
-
delta = (range.max - range.min) / sample_count
|
24
|
-
|
25
|
-
sin = []
|
26
|
-
cos = []
|
27
|
-
|
28
|
-
range.step(delta) do |x|
|
29
|
-
sin << Math::sin(x)
|
30
|
-
cos << (Math::PI * 2 * Math::cos(x))
|
31
|
-
end
|
32
|
-
|
33
|
-
@sin = SPCore::Signal.new(:sample_rate => sample_count, :data => sin)
|
34
|
-
@expected = SPCore::Signal.new(:sample_rate => sample_count, :data => cos)
|
35
|
-
@actual = @sin.derivative
|
36
|
-
end
|
37
15
|
|
38
|
-
it 'should produce a signal of same size' do
|
39
|
-
@actual.size.should eq @expected.size
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'should produce a signal matching the 1st derivative' do
|
43
|
-
@actual.data.each_index do |i|
|
44
|
-
@actual[i].should be_within(0.1).of(@expected[i])
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
16
|
describe '#normalize' do
|
50
17
|
before :all do
|
51
18
|
generator = SignalGenerator.new(:sample_rate => 2000, :size => 256)
|
@@ -132,5 +99,4 @@ describe SPCore::Signal do
|
|
132
99
|
#)
|
133
100
|
end
|
134
101
|
end
|
135
|
-
|
136
|
-
end
|
102
|
+
end
|