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.
Files changed (35) hide show
  1. data/ChangeLog.rdoc +5 -1
  2. data/lib/spcore.rb +9 -6
  3. data/lib/spcore/analysis/calculus.rb +38 -0
  4. data/lib/spcore/analysis/features.rb +186 -0
  5. data/lib/spcore/analysis/frequency_domain.rb +191 -0
  6. data/lib/spcore/analysis/{correlation.rb → statistics.rb} +41 -18
  7. data/lib/spcore/core/delay_line.rb +1 -1
  8. data/lib/spcore/filters/fir/dual_sinc_filter.rb +1 -1
  9. data/lib/spcore/filters/fir/sinc_filter.rb +1 -1
  10. data/lib/spcore/generation/comb_filter.rb +65 -0
  11. data/lib/spcore/{core → generation}/oscillator.rb +1 -1
  12. data/lib/spcore/{util → generation}/signal_generator.rb +1 -1
  13. data/lib/spcore/util/envelope_detector.rb +1 -1
  14. data/lib/spcore/util/gain.rb +9 -167
  15. data/lib/spcore/util/plotter.rb +1 -1
  16. data/lib/spcore/{analysis → util}/signal.rb +116 -127
  17. data/lib/spcore/version.rb +1 -1
  18. data/spcore.gemspec +2 -0
  19. data/spec/analysis/calculus_spec.rb +54 -0
  20. data/spec/analysis/features_spec.rb +106 -0
  21. data/spec/analysis/frequency_domain_spec.rb +147 -0
  22. data/spec/analysis/piano_C4.wav +0 -0
  23. data/spec/analysis/statistics_spec.rb +61 -0
  24. data/spec/analysis/trumpet_B4.wav +0 -0
  25. data/spec/generation/comb_filter_spec.rb +37 -0
  26. data/spec/{core → generation}/oscillator_spec.rb +0 -0
  27. data/spec/{util → generation}/signal_generator_spec.rb +0 -0
  28. data/spec/interpolation/interpolation_spec.rb +0 -2
  29. data/spec/{analysis → util}/signal_spec.rb +1 -35
  30. metadata +64 -22
  31. data/lib/spcore/analysis/envelope.rb +0 -76
  32. data/lib/spcore/analysis/extrema.rb +0 -55
  33. data/spec/analysis/correlation_spec.rb +0 -28
  34. data/spec/analysis/envelope_spec.rb +0 -50
  35. data/spec/analysis/extrema_spec.rb +0 -42
@@ -1,5 +1,5 @@
1
1
  # A library of signal processing methods and classes.
2
2
  module SPCore
3
3
  # spcore version
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -27,4 +27,6 @@ DESCRIPTION
27
27
  gem.add_development_dependency 'rspec', '~> 2.4'
28
28
  gem.add_development_dependency 'yard', '~> 0.8'
29
29
  gem.add_development_dependency 'pry'
30
+ gem.add_development_dependency 'pry-nav'
31
+ gem.add_development_dependency 'wavefile'
30
32
  end
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,3 @@
1
- require 'pry'
2
- require 'benchmark'
3
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
2
 
5
3
  describe SPCore::Interpolation do
@@ -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