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
@@ -54,4 +54,8 @@ Add Signal#keep_frequences, Signal#remove_frequencies, and Plotter#plot_signals.
54
54
 
55
55
  Add instance methods to Signal class: #duration, #normalize, #derivative, #lowpass, #highpass, #bandpass, #bandstop, #plot_1d, and #plot_2d.
56
56
  Add Correlation class to find similarity between signals (used to find a feature in an image).
57
- Make envelopes smoother with polynomial upsampling. In Signal#envelope and Signal, always return a Signal object (so also remove make_signal flag).
57
+ Make envelopes smoother with polynomial upsampling. In Signal#envelope and Signal, always return a Signal object (so also remove make_signal flag).
58
+
59
+ === 0.2.1 / 2013-07-03
60
+
61
+ Update for compatibility with hashmake-0.2.0.
@@ -4,7 +4,6 @@ require 'spcore/version'
4
4
  require 'spcore/core/circular_buffer'
5
5
  require 'spcore/core/constants'
6
6
  require 'spcore/core/delay_line'
7
- require 'spcore/core/oscillator'
8
7
 
9
8
  require 'spcore/windows/bartlett_hann_window'
10
9
  require 'spcore/windows/bartlett_window'
@@ -41,15 +40,19 @@ require 'spcore/resampling/discrete_resampling'
41
40
  require 'spcore/resampling/polynomial_resampling'
42
41
  require 'spcore/resampling/hybrid_resampling'
43
42
 
44
- require 'spcore/analysis/correlation'
45
- require 'spcore/analysis/extrema'
46
- require 'spcore/analysis/envelope'
47
- require 'spcore/analysis/signal'
43
+ require 'spcore/analysis/calculus'
44
+ require 'spcore/analysis/features'
45
+ require 'spcore/analysis/statistics'
46
+ require 'spcore/analysis/frequency_domain'
48
47
 
49
48
  require 'spcore/util/gain'
50
49
  require 'spcore/util/limiters'
51
50
  require 'spcore/util/plotter'
52
51
  require 'spcore/util/saturation'
53
52
  require 'spcore/util/scale'
54
- require 'spcore/util/signal_generator'
53
+ require 'spcore/util/signal'
55
54
  require 'spcore/util/envelope_detector'
55
+
56
+ require 'spcore/generation/oscillator'
57
+ require 'spcore/generation/signal_generator'
58
+ require 'spcore/generation/comb_filter'
@@ -0,0 +1,38 @@
1
+ module SPCore
2
+ # Calculus analysis methods.
3
+ class Calculus
4
+ # Differentiates the given values.
5
+ #
6
+ # @param [Array] values The input value series.
7
+ # @param [Numeric] dt The time differential (i.e. the sample period)
8
+ def self.derivative values, dt
9
+ raise "values.size is <= 2" if values.size <= 2
10
+
11
+ derivative = Array.new(values.size)
12
+
13
+ for i in 1...values.count
14
+ derivative[i] = (values[i] - values[i-1]) / dt
15
+ end
16
+
17
+ derivative[0] = derivative[1]
18
+ return derivative
19
+ end
20
+
21
+ # Integrates the given values.
22
+ #
23
+ # @param [Array] values The input value series.
24
+ # @param [Numeric] dt The time differential (i.e. the sample period)
25
+ def self.integral values, dt
26
+ raise "values.size is <= 2" if values.size <= 2
27
+
28
+ integral = Array.new(values.size)
29
+
30
+ integral[0] = values[0] * dt
31
+ for i in 1...values.count
32
+ integral[i] = (values[i] * dt) + integral[i-1]
33
+ end
34
+
35
+ return integral
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,186 @@
1
+ module SPCore
2
+ # Features analysis methods.
3
+ class Features
4
+ # Returns all minima and maxima (including positive minima and negative maxima).
5
+ def self.extrema samples
6
+ remove_inner = false
7
+ self.extrema_hash(samples, remove_inner)[:extrema]
8
+ end
9
+
10
+ # Returns outer minima and maxima (excludes positive minima and negative maxima).
11
+ def self.outer_extrema samples
12
+ remove_inner = true
13
+ self.extrema_hash(samples, remove_inner)[:extrema]
14
+ end
15
+
16
+ # Returns all minima (including positive minima).
17
+ def self.minima samples
18
+ remove_inner = false
19
+ self.extrema_hash(samples, remove_inner)[:minima]
20
+ end
21
+
22
+ # Returns all minima (excludes positive minima).
23
+ def self.negative_minima samples
24
+ remove_inner = true
25
+ self.extrema_hash(samples, remove_inner)[:minima]
26
+ end
27
+
28
+ # Returns maxima (includes negative maxima).
29
+ def self.maxima samples
30
+ remove_inner = false
31
+ self.extrema_hash(samples, remove_inner)[:maxima]
32
+ end
33
+
34
+ # Returns maxima (excludes negative maxima).
35
+ def self.positive_maxima samples
36
+ remove_inner = true
37
+ self.extrema_hash(samples, remove_inner)[:maxima]
38
+ end
39
+
40
+ # return the n greatest values of the given array of values.
41
+ def self.top_n values, n
42
+ top_n = []
43
+ values.each do |value|
44
+ if top_n.count < n
45
+ top_n.push value
46
+ else
47
+ smaller = top_n.select {|x| x < value}
48
+ if smaller.any?
49
+ top_n.delete smaller.min
50
+ top_n.push value
51
+ end
52
+ end
53
+ end
54
+ return top_n.sort
55
+ end
56
+
57
+ def self.outline samples
58
+ starting_outline = make_starting_outline samples
59
+ filled_in_outline = fill_in_starting_outline starting_outline, samples.count
60
+ dropsample_factor = (samples.count / starting_outline.count).to_i
61
+ return dropsample_filled_in_outline filled_in_outline, dropsample_factor
62
+ end
63
+
64
+ def self.envelope samples
65
+ outline = self.outline samples
66
+ upsample_factor = samples.count / outline.count.to_f
67
+ return PolynomialResampling.upsample(outline, upsample_factor)
68
+ end
69
+
70
+ private
71
+
72
+ def self.make_starting_outline samples
73
+ starting_outline = self.outer_extrema(samples)
74
+
75
+ # add in first and last samples so the envelope follows entire signal
76
+ starting_outline[0] = samples[0].abs
77
+ starting_outline[samples.count - 1] = samples[samples.count - 1].abs
78
+
79
+ # the envelope is only concerned with absolute values
80
+ starting_outline.keys.each do |key|
81
+ starting_outline[key] = starting_outline[key].abs
82
+ end
83
+
84
+ return starting_outline
85
+ end
86
+
87
+ def self.fill_in_starting_outline starting_outline, tgt_count
88
+ # the extrema we have now are probably not spaced evenly. Upsampling at
89
+ # this point would lead to a time-distorted signal. So the next step is to
90
+ # interpolate between all the extrema to make a crude but properly sampled
91
+ # envelope.
92
+
93
+ proper_outline = Array.new(tgt_count, 0)
94
+ indices = starting_outline.keys.sort
95
+
96
+ for i in 1...indices.count
97
+ l_idx = indices[i-1]
98
+ r_idx = indices[i]
99
+
100
+ l_val = starting_outline[l_idx]
101
+ r_val = starting_outline[r_idx]
102
+
103
+ proper_outline[l_idx] = l_val
104
+ proper_outline[r_idx] = r_val
105
+
106
+ idx_span = r_idx - l_idx
107
+
108
+ for j in (l_idx + 1)...(r_idx)
109
+ x = (j - l_idx).to_f / idx_span
110
+ y = Interpolation.linear l_val, r_val, x
111
+ proper_outline[j] = y
112
+ end
113
+ end
114
+
115
+ return proper_outline
116
+ end
117
+
118
+ # This properly spaces samples in the filled in outline, so as to avoid time
119
+ # distortion after upsampling.
120
+ def self.dropsample_filled_in_outline filled_in_outline, dropsample_factor
121
+ dropsampled_outline = []
122
+
123
+ (0...filled_in_outline.count).step(dropsample_factor) do |n|
124
+ dropsampled_outline.push filled_in_outline[n]
125
+ end
126
+
127
+ return dropsampled_outline
128
+ end
129
+
130
+ # Finds extrema (minima and maxima), return :extrema, :minima, and :maxima
131
+ # seperately in a hash.
132
+ def self.extrema_hash samples, remove_inner = false
133
+ minima = {}
134
+ maxima = {}
135
+
136
+ global_min_idx = 0
137
+ global_min_val = samples[0]
138
+ global_max_idx = 0
139
+ global_max_val = samples[0]
140
+
141
+ diffs = []
142
+ for i in (1...samples.count)
143
+ diffs.push(samples[i] - samples[i-1])
144
+
145
+ if samples[i] < global_min_val
146
+ global_min_idx = i
147
+ global_min_val = samples[i]
148
+ end
149
+
150
+ if samples[i] > global_max_val
151
+ global_max_idx = i
152
+ global_max_val = samples[i]
153
+ end
154
+ end
155
+ minima[global_min_idx] = global_min_val
156
+ maxima[global_max_idx] = global_max_val
157
+
158
+ is_positive = diffs.first > 0.0 # starting off with positive difference?
159
+
160
+ # at zero crossings there is a local maxima/minima
161
+ for i in (1...diffs.count)
162
+ if is_positive
163
+ # at positive-to-negative transition there is a local maxima
164
+ if diffs[i] <= 0.0
165
+ maxima[i] = samples[i]
166
+ is_positive = false
167
+ end
168
+ else
169
+ # at negative-to-positive transition there is a local minima
170
+ if diffs[i] > 0.0
171
+ minima[i] = samples[i]
172
+ is_positive = true
173
+ end
174
+ end
175
+ end
176
+
177
+ if remove_inner
178
+ minima.keep_if {|idx,val| val <= 0 }
179
+ maxima.keep_if {|idx,val| val >= 0 }
180
+ end
181
+
182
+ return :minima => minima, :maxima => maxima, :extrema => minima.merge(maxima)
183
+ end
184
+
185
+ end
186
+ end
@@ -0,0 +1,191 @@
1
+ module SPCore
2
+ # Frequency domain analysis class. On instantiation a forware FFT is performed
3
+ # on the given time series data, and the results are stored in full and half form.
4
+ # The half-FFT form cuts out the latter half of the FFT results. Also, for the
5
+ # half-FFT the complex values will be converted to magnitude (linear or decibel)
6
+ # if specified in :fft_format (see FFT_FORMATS for valid values).
7
+ class FrequencyDomain
8
+ include Hashmake::HashMakeable
9
+
10
+ FFT_COMPLEX_VALUED = :complexValued
11
+ FFT_MAGNITUDE_LINEAR = :magnitudeLinear
12
+ FFT_MAGNITUDE_DECIBEL = :magnitudeDecibel
13
+
14
+ # valid values to give for the :fft_format key.
15
+ FFT_FORMATS = [
16
+ FFT_COMPLEX_VALUED,
17
+ FFT_MAGNITUDE_LINEAR,
18
+ FFT_MAGNITUDE_DECIBEL
19
+ ]
20
+
21
+ # define how the class is to be instantiated by hash.
22
+ ARG_SPECS = {
23
+ :time_data => arg_spec_array(:reqd => true, :type => Numeric),
24
+ :sample_rate => arg_spec(:reqd => true, :type => Numeric, :validator => ->(a){ a > 0 }),
25
+ :fft_format => arg_spec(:reqd => false, :type => Symbol, :default => FFT_MAGNITUDE_DECIBEL, :validator => ->(a){FFT_FORMATS.include?(a)})
26
+ }
27
+
28
+ attr_reader :time_data, :sample_rate, :fft_format, :fft_full, :fft_half
29
+
30
+ def initialize args
31
+ hash_make args, FrequencyDomain::ARG_SPECS
32
+ @fft_full = FFT.forward @time_data
33
+ @fft_half = @fft_full[0...(@fft_full.size / 2)]
34
+
35
+ case(@fft_format)
36
+ when FFT_MAGNITUDE_LINEAR
37
+ @fft_half = @fft_half.map {|x| x.magnitude }
38
+ when FFT_MAGNITUDE_DECIBEL
39
+ @fft_half = @fft_half.map {|x| Gain.linear_to_db x.magnitude } # in decibels
40
+ end
41
+ end
42
+
43
+ # Convert an FFT output index to the corresponding frequency bin
44
+ def idx_to_freq(idx)
45
+ return (idx * @sample_rate.to_f) / @fft_full.size
46
+ end
47
+
48
+ # Convert an FFT frequency bin to the corresponding FFT output index
49
+ def freq_to_idx(freq)
50
+ return (freq * @fft_full.size) / @sample_rate.to_f
51
+ end
52
+
53
+ # Find frequency peak values.
54
+ def peaks
55
+ # map positive maxima to indices
56
+ positive_maxima = Features.positive_maxima(@fft_half)
57
+
58
+ freq_peaks = {}
59
+ positive_maxima.keys.sort.each do |idx|
60
+ freq = idx_to_freq(idx)
61
+ freq_peaks[freq] = positive_maxima[idx]
62
+ end
63
+
64
+ return freq_peaks
65
+ end
66
+
67
+ GCD = :gcd
68
+ WINDOW = :window
69
+ HARMONIC_SERIES_APPROACHES = [ GCD, WINDOW ]
70
+
71
+ # Find the strongest harmonic series among the given peak data.
72
+ def harmonic_series opts = {}
73
+ defaults = { :n_peaks => 8, :min_freq => 40.0, :approach => WINDOW }
74
+ opts = defaults.merge(opts)
75
+
76
+ n_peaks = opts[:n_peaks]
77
+ min_freq = opts[:min_freq]
78
+ approach = opts[:approach]
79
+
80
+ raise ArgumentError, "n_peaks is < 1" if n_peaks < 1
81
+ peaks = self.peaks
82
+
83
+ if peaks.empty?
84
+ return []
85
+ end
86
+
87
+ max_freq = peaks.keys.max
88
+ max_idx = freq_to_idx(max_freq)
89
+
90
+ sorted_pairs = peaks.sort_by {|f,m| m}
91
+ top_n_pairs = sorted_pairs.reverse[0, n_peaks]
92
+
93
+ candidate_series = []
94
+
95
+ case approach
96
+ when GCD
97
+ for n in 1..n_peaks
98
+ combinations = top_n_pairs.combination(n).to_a
99
+ combinations.each do |combination|
100
+ freq_indices = combination.map {|pair| freq_to_idx(pair[0]) }
101
+ fund_idx = multi_gcd freq_indices
102
+
103
+ if fund_idx >= freq_to_idx(min_freq)
104
+ series = []
105
+ idx = fund_idx
106
+ while idx <= max_idx
107
+ freq = idx_to_freq(idx)
108
+ #if peaks.has_key? freq
109
+ series.push freq
110
+ #end
111
+ idx += fund_idx
112
+ end
113
+ candidate_series.push series
114
+ end
115
+ end
116
+ end
117
+ when WINDOW
118
+ # look for a harmonic series
119
+ top_n_pairs.each do |pair|
120
+ f_base = pair[0]
121
+
122
+ min_idx_base = freq_to_idx(f_base) - 0.5
123
+ max_idx_base = min_idx_base + 1.0
124
+
125
+ harmonic_series = [ f_base ]
126
+ target = 2 * f_base
127
+ min_idx = 2 * min_idx_base
128
+ max_idx = 2 * max_idx_base
129
+
130
+ while target < max_freq
131
+ f_l = idx_to_freq(min_idx.floor)
132
+ f_h = idx_to_freq(max_idx.ceil)
133
+ window = f_l..f_h
134
+ candidates = peaks.select {|actual,magn| window.include?(actual) }
135
+
136
+ if candidates.any?
137
+ min = candidates.min_by {|actual,magn| (actual - target).abs }
138
+ harmonic_series.push min[0]
139
+ else
140
+ break
141
+ end
142
+
143
+ target += f_base
144
+ min_idx += min_idx_base
145
+ max_idx += max_idx_base
146
+ end
147
+
148
+ candidate_series.push harmonic_series
149
+ end
150
+ else
151
+ raise ArgumentError, "#{approach} approach is not supported"
152
+ end
153
+
154
+ strongest_series = candidate_series.max_by do |harmonic_series|
155
+ sum = 0
156
+ harmonic_series.each do |f|
157
+ if peaks.has_key?(f)
158
+ sum += peaks[f]**2
159
+ else
160
+ m = @fft_half[freq_to_idx(f)].magnitude
161
+ sum += m**2
162
+ end
163
+ end
164
+ sum
165
+ end
166
+
167
+ return strongest_series
168
+ end
169
+
170
+ private
171
+
172
+ def gcd a,b
173
+ if b == 0
174
+ return a
175
+ else
176
+ return gcd(b, a % b)
177
+ end
178
+ end
179
+
180
+ def multi_gcd nums
181
+ if nums.count == 1
182
+ return nums[0]
183
+ elsif nums.count == 2
184
+ return gcd nums[0], nums[1]
185
+ else
186
+ return multi_gcd [gcd(nums[0], nums[1])] + nums[2..-1]
187
+ end
188
+ end
189
+
190
+ end
191
+ end