spcore 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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