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
@@ -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