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,18 +1,38 @@
1
1
  module SPCore
2
- # Determines the normalized cross-correlation of a feature with an image.
3
- # Normalization is from -1 to +1, where +1 is high correlation, -1 is high
4
- # correlation (of inverse), and 0 is no correlation.
5
- #
6
- # For autocorrelation, just cross-correlate a signal with itself.
7
- #
8
- # @author James Tunnell
9
- class Correlation
10
- attr_reader :data
11
-
2
+ # Statistical analysis methods.
3
+ class Statistics
4
+ # Compute the mean of a value series.
5
+ def self.sum values
6
+ return values.inject(0){ |s, x| s + x }
7
+ end
8
+
9
+ # Compute the mean of a value series.
10
+ def self.mean values
11
+ return Statistics.sum(values) / values.size
12
+ end
13
+
14
+ # Compute the standard deviation of a value series.
15
+ def self.std_dev values
16
+ size = values.size
17
+ raise ArgumentError, "size is <= 1" if size <= 1
18
+
19
+ mean = Statistics.mean values
20
+ total_dist_from_mean = values.inject(0){|sum,x| sum + (x - mean)**2}
21
+ return Math.sqrt(total_dist_from_mean / (size - 1))
22
+ end
23
+
24
+ # Determines the normalized cross-correlation of a feature with an image.
25
+ #
26
+ # Normalization is from -1 to +1, where +1 is high correlation, -1 is high
27
+ # correlation (of inverse), and 0 is no correlation.
28
+ #
29
+ # For autocorrelation, just cross-correlate a signal with itself.
30
+ #
12
31
  # @param [Array] image The values which are actually recieved/measured.
13
- # @param [Array] feature The values to be searched for in the image. Size must not be greater than size of image.
32
+ # @param [Array] feature The values to be searched for in the image. Size must
33
+ # not be greater than size of image.
14
34
  # @param [Fixnum] zero_padding Number of zeros to surround the image with.
15
- def initialize image, feature, zero_padding = 0
35
+ def self.correlation image, feature, zero_padding = 0
16
36
  raise ArgumentError, "feature size is > image size" if feature.size > image.size
17
37
 
18
38
  unless zero_padding == 0
@@ -23,7 +43,7 @@ class Correlation
23
43
  feature_diff = feature.map {|x| x - feature_mean }
24
44
  sx = feature_diff.inject(0){ |s, x| s + x**2 }
25
45
 
26
- @data = []
46
+ data = []
27
47
  for i in 0...(1 + image.size - feature.size)
28
48
  region = image[i...(i + feature.size)]
29
49
  region_mean = region.inject(0){|s,x| s + x } / feature.size.to_f
@@ -32,9 +52,9 @@ class Correlation
32
52
 
33
53
  if sx == 0 || sy == 0
34
54
  if sx == 0 && sy == 0
35
- @data.push 1.0
55
+ data.push 1.0
36
56
  else
37
- @data.push 0.0
57
+ data.push 0.0
38
58
  end
39
59
 
40
60
  next
@@ -48,8 +68,11 @@ class Correlation
48
68
  end
49
69
 
50
70
  r = sum / denom
51
- @data.push(r)
52
- end
71
+ data.push(r)
72
+ end
73
+
74
+ return data
53
75
  end
76
+
77
+ end
54
78
  end
55
- end
@@ -20,7 +20,7 @@ class DelayLine
20
20
  # :max_delay_seconds (reqd) and :delay_seconds (not reqd).
21
21
  # See ARG_SPECS for more details.
22
22
  def initialize args
23
- hash_make DelayLine::ARG_SPECS, args
23
+ hash_make args, DelayLine::ARG_SPECS
24
24
  raise ArgumentError, "delay_seconds #{delay_seconds} is greater than max_delay_seconds #{max_delay_seconds}" if @delay_seconds > @max_delay_seconds
25
25
  @buffer = CircularBuffer.new((@sample_rate * @max_delay_seconds) + 1, :override_when_full => true)
26
26
  @buffer.push_ary Array.new(@buffer.size, 0.0)
@@ -24,7 +24,7 @@ class DualSincFilter
24
24
  # Given a filter order, 2 cutoff frequencies, sample rate, and window class,
25
25
  # develop a FIR filter kernel that can be used for lowpass filtering.
26
26
  def initialize args
27
- hash_make DualSincFilter::ARG_SPECS, args
27
+ hash_make args, DualSincFilter::ARG_SPECS
28
28
 
29
29
  raise ArgumentError, "left_cutoff_freq is greater than 0.5 * sample_rate" if @left_cutoff_freq > (@sample_rate * 0.5)
30
30
  raise ArgumentError, "right_cutoff_freq is greater than 0.5 * sample_rate" if @right_cutoff_freq > (@sample_rate * 0.5)
@@ -24,7 +24,7 @@ class SincFilter
24
24
  # Given a filter order, cutoff frequency, sample rate, and window class,
25
25
  # develop a FIR filter kernel that can be used for lowpass filtering.
26
26
  def initialize args
27
- hash_make SincFilter::ARG_SPECS, args
27
+ hash_make args, SincFilter::ARG_SPECS
28
28
 
29
29
  raise ArgumentError, "cutoff_freq is greater than 0.5 * sample_rate" if @cutoff_freq > (@sample_rate / 2.0)
30
30
 
@@ -0,0 +1,65 @@
1
+ module SPCore
2
+ class CombFilter
3
+ include Hashmake::HashMakeable
4
+
5
+ FEED_FORWARD = :feedForward
6
+ FEED_BACK = :feedBack
7
+
8
+ TYPES = [ FEED_FORWARD, FEED_BACK ]
9
+
10
+ ARG_SPECS = {
11
+ :type => arg_spec(:reqd => true, :type => Symbol, :validator => ->(a){ TYPES.include?(a)}),
12
+ :frequency => arg_spec(:reqd => true, :type => Numeric, :validator => ->(a){ a > 0}),
13
+ :alpha => arg_spec(:reqd => true, :type => Float, :validator => ->(a){ a.between?(0.0,1.0) })
14
+ }
15
+
16
+ attr_reader :type, :frequency, :alpha
17
+
18
+ def initialize args
19
+ hash_make args, CombFilter::ARG_SPECS
20
+ calculate_params
21
+ end
22
+
23
+ def type= type
24
+ ARG_SPECS[:type].validate_value type
25
+ @type = type
26
+ end
27
+
28
+ def frequency= frequency
29
+ ARG_SPECS[:frequency].validate_value frequency
30
+ @frequency = frequency
31
+ calculate_params
32
+ end
33
+
34
+ def alpha= alpha
35
+ ARG_SPECS[:alpha].validate_value alpha
36
+ @alpha = alpha
37
+ end
38
+
39
+ def frequency_response_at x
40
+ output = 0
41
+ if @type == FEED_FORWARD
42
+ output = Math.sqrt((1.0 + @alpha**2) + 2.0 * @alpha * Math.cos(@k * x))
43
+ elsif @type == FEED_BACK
44
+ output = 1.0 / Math.sqrt((1.0 + alpha**2) - 2.0 * @alpha * Math.cos(@k * x))
45
+ end
46
+ return output
47
+ end
48
+
49
+ def frequency_response sample_rate, sample_count
50
+ output = []
51
+ sample_period = 1.0 / sample_rate
52
+ sample_count.times do |n|
53
+ x = sample_period * n
54
+ output.push frequency_response_at(x)
55
+ end
56
+ return output
57
+ end
58
+
59
+ private
60
+
61
+ def calculate_params
62
+ @k = (Math::PI * 2) * @frequency
63
+ end
64
+ end
65
+ end
@@ -38,7 +38,7 @@ class Oscillator
38
38
  # :wave_type, :frequency, :amplitude, :phase_offset, and :dc_offset.
39
39
  # See ARG_SPECS for more details.
40
40
  def initialize args
41
- hash_make Oscillator::ARG_SPECS, args
41
+ hash_make args, Oscillator::ARG_SPECS
42
42
 
43
43
  @phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
44
44
  @current_phase_angle = @phase_offset
@@ -14,7 +14,7 @@ class SignalGenerator
14
14
  # A new instance of SignalGenerator.
15
15
  # @param [Hash] args Required keys are :sample_rate and :size.
16
16
  def initialize args
17
- hash_make ARG_SPECS, args
17
+ hash_make args, ARG_SPECS
18
18
  end
19
19
 
20
20
  # Generate a Signal object with noise data.
@@ -20,7 +20,7 @@ class EnvelopeDetector
20
20
  # :attack_time (in seconds) (reqd) and :release_time
21
21
  # (in seconds) (reqd). See ARG_SPECS for more details.
22
22
  def initialize args
23
- hash_make EnvelopeDetector::ARG_SPECS, args
23
+ hash_make args, EnvelopeDetector::ARG_SPECS
24
24
 
25
25
  @g_attack = Math.exp(-1.0 / (sample_rate * attack_time))
26
26
  @g_release = Math.exp(-1.0 / (sample_rate * release_time))
@@ -1,184 +1,26 @@
1
+
1
2
  module SPCore
2
3
  # Provide utility functions to convert between a linear and decibel (logarithm) unit.
3
4
  class Gain
4
-
5
- #MAX_DB = 72
6
- #NUM_DB_HALF_STEPS=146
7
- #DB_TO_SCALAR_TABLE = [
8
- # 1.0, # 0 dB
9
- # 1.059253725, # 0.5
10
- # 1.122018454, # 1
11
- # 1.188502227, # 1.5
12
- # 1.258925412, # 2
13
- # 1.333521432, # 2.5
14
- # 1.412537545, # 3
15
- # 1.496235656, # 3.5
16
- # 1.584893192, # 4
17
- # 1.678804018, # 4.5
18
- # 1.77827941, # 5
19
- # 1.883649089, # 5.5
20
- # 1.995262315, # 6
21
- # 2.11348904, # 6.5
22
- # 2.238721139, # 7
23
- # 2.371373706, # 7.5
24
- # 2.511886432, # 8
25
- # 2.66072506, # 8.5
26
- # 2.818382931, # 9
27
- # 2.985382619, # 9.5
28
- # 3.16227766, # 10 dB
29
- # 3.349654392, # 10.5
30
- # 3.548133892, # 11
31
- # 3.758374043, # 11.5
32
- # 3.981071706, # 12
33
- # 4.216965034, # 12.5
34
- # 4.466835922, # 13
35
- # 4.73151259, # /13.5
36
- # 5.011872336, # 14
37
- # 5.308844442, # 14.5
38
- # 5.623413252, # 15
39
- # 5.956621435, # 15.5
40
- # 6.309573445, # 16
41
- # 6.683439176, # 16.5
42
- # 7.079457844, # 17
43
- # 7.498942093, # 17.5
44
- # 7.943282347, # 18
45
- # 8.413951416, # 18.5
46
- # 8.912509381, # 19
47
- # 9.440608763, # 19.5
48
- # 10.0, # 20 dB
49
- # 10.59253725, # 20.5
50
- # 11.22018454, # 21
51
- # 11.88502227, # 21.5
52
- # 12.58925412, # 22
53
- # 13.33521432, # 22.5
54
- # 14.12537545, # 23
55
- # 14.96235656, # 23.5
56
- # 15.84893192, # 24
57
- # 16.78804018, # 24.5
58
- # 17.7827941, # 25
59
- # 18.83649089, # 25.5
60
- # 19.95262315, # 26
61
- # 21.1348904, # 26.5
62
- # 22.38721139, # 27
63
- # 23.71373706, # 27.5
64
- # 25.11886432, # 28
65
- # 26.6072506, # 28.5
66
- # 28.18382931, # 29
67
- # 29.85382619, # 29.5
68
- # 31.6227766, # 30 dB
69
- # 33.49654392, # 30.5
70
- # 35.48133892, # 31
71
- # 37.58374043, # 31.5
72
- # 39.81071706, # 32
73
- # 42.16965034, # 32.5
74
- # 44.66835922, # 33
75
- # 47.3151259, # 33.5
76
- # 50.11872336, # 34
77
- # 53.08844442, # 34.5
78
- # 56.23413252, # 35
79
- # 59.56621435, # 35.5
80
- # 63.09573445, # 36
81
- # 66.83439176, # 36.5
82
- # 70.79457844, # 37
83
- # 74.98942093, # 37.5
84
- # 79.43282347, # 38
85
- # 84.13951416, # 38.5
86
- # 89.12509381, # 39
87
- # 94.40608763, # 39.5
88
- # 100.0, # 40 dB
89
- # 105.9253725, # 40.5
90
- # 112.2018454, # 41
91
- # 118.8502227, # 41.5
92
- # 125.8925412, # 42
93
- # 133.3521432, # 42.5
94
- # 141.2537545, # 43
95
- # 149.6235656, # 43.5
96
- # 158.4893192, # 44
97
- # 167.8804018, # 44.5
98
- # 177.827941, # 45
99
- # 188.3649089, # 45.5
100
- # 199.5262315, # 46
101
- # 211.348904, # 46.5
102
- # 223.8721139, # 47
103
- # 237.1373706, # 47.5
104
- # 251.1886432, # 48
105
- # 266.072506, # 48.5
106
- # 281.8382931, # 49
107
- # 298.5382619, # 49.5
108
- # 316.227766, # 50 dB
109
- # 334.9654392, # 50.5
110
- # 354.8133892, # 51
111
- # 375.8374043, # 51.5
112
- # 398.1071706, # 52
113
- # 421.6965034, # 52.5
114
- # 446.6835922, # 53
115
- # 473.151259, # 53.5
116
- # 501.1872336, # 54
117
- # 530.8844442, # 54.5
118
- # 562.3413252, # 55
119
- # 595.6621435, # 55.5
120
- # 630.9573445, # 56
121
- # 668.3439176, # 56.5
122
- # 707.9457844, # 57
123
- # 749.8942093, # 57.5
124
- # 794.3282347, # 58
125
- # 841.3951416, # 58.5
126
- # 891.2509381, # 59
127
- # 944.0608763, # 59.5
128
- # 1000.0, # 60 dB
129
- # 1059.253725, # 60.5
130
- # 1122.018454, # 61
131
- # 1188.502227, # 61.5
132
- # 1258.925412, # 62
133
- # 1333.521432, # 62.5
134
- # 1412.537545, # 63
135
- # 1496.235656, # 63.5
136
- # 1584.893192, # 64
137
- # 1678.804018, # 64.5
138
- # 1778.27941, # 65
139
- # 1883.649089, # 65.5
140
- # 1995.262315, # 66
141
- # 2113.48904, # 66.5
142
- # 2238.721139, # 67
143
- # 2371.373706, # 67.5
144
- # 2511.886432, # 68
145
- # 2660.72506, # 68.5
146
- # 2818.382931, # 69
147
- # 2985.382619, # 69.5
148
- # 3162.27766, # 70 dB
149
- # 3349.654392, # 70.5
150
- # 3548.133892, # 71
151
- # 3758.374043, # 71.5
152
- # 3981.071706, # 72
153
- #]
154
- #
155
- #def self.db_to_linear db
156
- # raise ArgumentError, "#{db} db is not between -#{MAX_DB} and #{MAX_DB}" unless db.between(-MAX_DB, MAX_DB)
157
- # db_half_step = (dB * 2.0).to_i
158
- #
159
- # if(db_half_step >= 0)
160
- # linear = DB_TO_SCALAR_TABLE[db_half_step]
161
- # elsif
162
- # linear = DB_TO_SCALAR_TABLE[-db_half_step]
163
- # scalar = 1.0 / scalar
164
- # end
165
- #
166
- # return scalar
167
- #end
168
5
 
169
6
  MAX_DB_ABS = 6000.0
170
7
 
171
8
  # Convert a decibel value to a linear value.
172
9
  def self.db_to_linear db
173
10
  db_abs = db.abs
174
- raise ArgumentError, "|db| is #{db_abs}, which is greater than the allowed #{MAX_DB_ABS}" if db_abs > MAX_DB_ABS
11
+ raise ArgumentError, "decibel value #{db} is more than allowed maximum #{MAX_DB_ABS}" if db_abs > MAX_DB_ABS
12
+ raise ArgumentError, "decibel value #{db} is less than allowed minimum #{-MAX_DB_ABS}" if db_abs < -MAX_DB_ABS
175
13
  return 10.0**(db / 20.0)
176
14
  end
177
15
 
178
16
  # Convert a linear value to a decibel value.
179
17
  def self.linear_to_db linear
180
- raise ArgumentError, "linear value #{linear} is less than or equal to 0.0" if linear <= 0.0
181
- return 20.0 * Math::log10(linear)
18
+ raise ArgumentError, "linear value #{linear} is less than 0.0" if linear < 0.0
19
+ if linear == 0.0
20
+ return -MAX_DB_ABS
21
+ else
22
+ return 20.0 * Math::log10(linear)
23
+ end
182
24
  end
183
25
 
184
26
  end
@@ -21,7 +21,7 @@ class Plotter
21
21
  # All params are optional. See ARG_SPECS for
22
22
  # parameter details.
23
23
  def initialize hashed_args = {}
24
- hash_make Plotter::ARG_SPECS, hashed_args
24
+ hash_make hashed_args, Plotter::ARG_SPECS
25
25
  end
26
26
 
27
27
  # Plot XY datapoints.
@@ -9,7 +9,8 @@ class Signal
9
9
  # Used to process hashed arguments in #initialize.
10
10
  ARG_SPECS = {
11
11
  :data => arg_spec(:reqd => true, :type => Array, :validator => ->(a){ a.any? }),
12
- :sample_rate => arg_spec(:reqd => true, :type => Fixnum, :validator => ->(a){ a > 0.0 })
12
+ :sample_rate => arg_spec(:reqd => true, :type => Numeric, :validator => ->(a){ a > 0.0 }),
13
+ #:fft_format => arg_spec(:reqd => false, :type => Symbol, :default => FFT_MAGNITUDE_DECIBEL, :validator => ->(a){FFT_FORMATS.include?(a)})
13
14
  }
14
15
 
15
16
  attr_reader :data, :sample_rate
@@ -19,18 +20,22 @@ class Signal
19
20
  # @param [Hash] hashed_args Hashed arguments. Required keys are :data and
20
21
  # :sample_rate. See ARG_SPECS for more details.
21
22
  def initialize hashed_args
22
- hash_make Signal::ARG_SPECS, hashed_args
23
+ hash_make hashed_args, Signal::ARG_SPECS
23
24
  end
24
25
 
25
26
  # Produce a new Signal object with the same data.
26
27
  def clone
27
- Signal.new(:data => @data.clone, :sample_rate => @sample_rate)
28
+ new = Signal.new(:data => @data.clone, :sample_rate => @sample_rate)
29
+ new.instance_variable_set(:@frequency_domain, @frequency_domain)
30
+ return new
28
31
  end
29
32
 
30
33
  # Produce a new Signal object with a subset of the current signal data.
31
34
  # @param [Range] range Used to pick the data range.
32
35
  def subset range
33
- Signal.new(:data => @data[range], :sample_rate => @sample_rate)
36
+ new = Signal.new(:data => @data[range], :sample_rate => @sample_rate)
37
+ new.instance_variable_set(:@frequency_domain, @frequency_domain)
38
+ return new
34
39
  end
35
40
 
36
41
  # Size of the signal data.
@@ -232,29 +237,56 @@ class Signal
232
237
  return self.clone.resample_hybrid!(upsample_factor, downsample_factor, filter_order)
233
238
  end
234
239
 
240
+ def frequency_domain fft_format = FrequencyDomain::FFT_MAGNITUDE_DECIBEL
241
+ if @frequency_domain.nil?
242
+ @frequency_domain = FrequencyDomain.new(
243
+ :time_data => @data,
244
+ :sample_rate => @sample_rate,
245
+ :fft_format => fft_format
246
+ )
247
+ end
248
+ return @frequency_domain
249
+ end
250
+
251
+ # Return the full output of forward FFT on the signal data.
252
+ def fft_full
253
+ frequency_domain.fft_full
254
+ end
255
+
256
+ # Return the first half of the the forward FFT on the signal data.
257
+ def fft_half
258
+ frequency_domain.fft_half
259
+ end
260
+
235
261
  # Run FFT on signal data to find magnitude of frequency components.
236
- # @param convert_to_db If true, magnitudes are converted to dB values.
237
262
  # @return [Hash] contains frequencies mapped to magnitudes.
238
- def freq_magnitudes convert_to_db = false
239
- fft_output = FFT.forward @data
240
-
241
- fft_output = fft_output[0...(fft_output.size / 2)] # ignore second half
242
- fft_output = fft_output.map {|x| x.magnitude } # map complex value to magnitude
243
-
244
- if convert_to_db
245
- fft_output = fft_output.map {|x| Gain.linear_to_db x}
246
- end
247
-
263
+ def freq_magnitudes
248
264
  freq_magnitudes = {}
249
- fft_output.each_index do |i|
250
- size = fft_output.size * 2 # mul by 2 because the second half of original fft_output was removed
251
- freq = (@sample_rate * i) / size
252
- freq_magnitudes[freq] = fft_output[i]
265
+
266
+ fft_half.each_index do |i|
267
+ freq = frequency_domain.idx_to_freq(i)
268
+ freq_magnitudes[freq] = fft_half[i]
253
269
  end
254
270
 
255
271
  return freq_magnitudes
256
272
  end
257
273
 
274
+ # Apply FrequencyDomain.peaks to the signal and return the result.
275
+ def freq_peaks
276
+ return frequency_domain.peaks
277
+ end
278
+
279
+ # Apply FrequencyDomain.harmonic_series to the signal frequency peaks and
280
+ # return the result.
281
+ def harmonic_series opts = {}
282
+ return frequency_domain.harmonic_series(opts)
283
+ end
284
+
285
+ # Return the lowest frequency of the signal harmonic series.
286
+ def fundamental opts
287
+ return harmonic_series(opts).min
288
+ end
289
+
258
290
  # Calculate the energy in current signal data.
259
291
  def energy
260
292
  return @data.inject(0.0){|sum,x| sum + (x * x)}
@@ -266,15 +298,49 @@ class Signal
266
298
  Math.sqrt(energy / size)
267
299
  end
268
300
 
269
- # Compute the mean of signal data.
301
+ # Apply Statistics.mean to the signal data.
270
302
  def mean
271
- sum = @data.inject(0){ |s, x| s + x }
272
- return sum.to_f / size
303
+ Statistics.mean @data
304
+ end
305
+
306
+ # Apply Statistics.std_dev to the signal data.
307
+ def std_dev
308
+ Statistics.std_dev @data
273
309
  end
274
310
 
275
- # Find extrema (maxima, minima) within signal data.
311
+ # Apply Statistics.correlation to the signal data (as the image).
312
+ def correlation feature, zero_padding = 0
313
+ Statistics.correlation @data, feature, zero_padding
314
+ end
315
+
316
+ # Apply Features.extrema to the signal data.
276
317
  def extrema
277
- return Extrema.new(@data)
318
+ return Features.extrema(@data)
319
+ end
320
+
321
+ # Apply Features.outer_extrema to the signal data.
322
+ def outer_extrema
323
+ return Features.outer_extrema(@data)
324
+ end
325
+
326
+ # Apply Features.minima to the signal data.
327
+ def minima
328
+ return Features.minima(@data)
329
+ end
330
+
331
+ # Apply Features.negative_minima to the signal data.
332
+ def negative_minima
333
+ return Features.negative_minima(@data)
334
+ end
335
+
336
+ # Apply Features.maxima to the signal data.
337
+ def maxima
338
+ return Features.maxima(@data)
339
+ end
340
+
341
+ # Apply Features.positive_maxima to the signal data.
342
+ def positive_maxima
343
+ return Features.positive_maxima(@data)
278
344
  end
279
345
 
280
346
  # Operate on the signal data (in place) with the absolute value function.
@@ -287,22 +353,30 @@ class Signal
287
353
  def abs
288
354
  self.clone.abs!
289
355
  end
290
-
356
+
357
+ # reduce all samples to the given level
291
358
  def normalize! level = 1.0
292
359
  self.divide!(@data.max / level)
293
360
  end
294
361
 
295
- # reduce all samples to
362
+ # reduce all samples to the given level
296
363
  def normalize level = 1.0
297
364
  self.clone.normalize! level
298
365
  end
299
366
 
300
- # Determine the envelope of the current Signal and return either a Envelope
301
- # or a new Signal object as a result.
302
- # @param [True/False] make_signal If true, return envelope data in a new
303
- # Otherwise, return an Envelope object.
304
- def envelope
305
- Signal.new(:sample_rate => @sample_rate, :data => Envelope.new(@data).data)
367
+ # Apply Features.envelope to the signal data, and return either a new Signal
368
+ # object or the raw envelope data.
369
+ # @param [True/False] as_signal If true, return envelope data in a new signal
370
+ # object. Otherwise, return raw envelope data.
371
+ # Set to true by default.
372
+ def envelope as_signal = true
373
+ env_data = Features.envelope @data
374
+
375
+ if as_signal
376
+ return Signal.new(:sample_rate => @sample_rate, :data => env_data)
377
+ else
378
+ return env_data
379
+ end
306
380
  end
307
381
 
308
382
  # Add data in array or other signal to the beginning of current data.
@@ -467,104 +541,19 @@ class Signal
467
541
  alias_method :-, :subtract
468
542
  alias_method :*, :multiply
469
543
  alias_method :/, :divide
470
-
471
- # Determine how well the another signal (g) correlates to the current signal (f).
472
- # Correlation is determined at every point in f. The signal g must not be
473
- # longer than f. Correlation involves moving g along f and performing
474
- # convolution. Starting a the beginning of f, it continues until the end
475
- # of g hits the end of f. Doesn't actually convolve, though. Instead, it
476
- # adds
477
- #
478
- # @param [Array] other_signal The signal to look for in the current signal.
479
- # @param [true/false] normalize Flag to indicate if normalization should be
480
- # performed on input signals (performed on a copy
481
- # of the original data).
482
- # @raise [ArgumentError] if other_signal is not a Signal or Array.
483
- # @raise [ArgumentError] if other_signal is longer than the current signal data.
484
- def cross_correlation other_signal, normalize = true
485
- if other_signal.is_a? Signal
486
- other_data = other_signal.data
487
- elsif other_signal.is_a? Array
488
- other_data = other_signal
489
- else
490
- raise ArgumentError, "other_signal is not a Signal or Array"
491
- end
492
-
493
- f = @data
494
- g = other_data
495
-
496
- raise ArgumentError, "g.count #{g.count} is greater than f.count #{f.count}" if g.count > f.count
497
-
498
- g_size = g.count
499
- f_size = f.count
500
- f_g_diff = f_size - g_size
501
-
502
- cross_correlation = []
503
-
504
- if normalize
505
- max = (f.max_by {|x| x.abs }).abs.to_f
506
-
507
- f = f.clone
508
- g = g.clone
509
- f.each_index {|i| f[i] = f[i] / max }
510
- g.each_index {|i| g[i] = g[i] / max }
511
- end
512
-
513
- #puts "f: #{f.inspect}"
514
- #puts "g: #{g.inspect}"
515
-
516
- for n in 0..f_g_diff do
517
- f_window = (n...(n + g_size)).entries
518
- g_window = (0...g_size).entries
519
-
520
- sample = 0.0
521
- for i in 0...f_window.count do
522
- i_f = f_window[i]
523
- i_g = g_window[i]
524
-
525
- #if use_relative_error
526
- target = g[i_g].to_f
527
- actual = f[i_f]
528
-
529
- #if target == 0.0 && actual != 0.0 && normalize
530
- # puts "target is #{target} and actual is #{actual}"
531
- # error = 1.0
532
- #else
533
- error = (target - actual).abs# / target
534
- #end
535
-
536
- sample += error
537
-
538
- #else
539
- # sample += (f[i_f] * g[i_g])
540
- #end
541
- end
542
-
543
- cross_correlation << (sample)# / g_size.to_f)
544
- end
545
544
 
546
- return cross_correlation
547
- end
548
-
549
- # Differentiates the signal data.
550
- # @param [true/false] make_signal If true, return the result as a new
551
- # Signal object. Otherwise, return result
552
- # as an array.
545
+ # Applies Calculus.derivative on the signal data, and returns a new Signal
546
+ # object with the result.
553
547
  def derivative
554
- raise "Signal does not have at least 2 samples" unless @data.size > 2
555
-
556
- derivative = Array.new(@data.size)
557
- sample_period = 1.0 / @sample_rate
558
-
559
- for i in 1...@data.count
560
- derivative[i] = (@data[i] - @data[i-1]) / sample_period
561
- end
562
-
563
- derivative[0] = derivative[1]
564
-
565
- return Signal.new(:sample_rate => @sample_rate, :data => derivative)
548
+ return Signal.new(:sample_rate => @sample_rate, :data => Calculus.derivative(@data))
566
549
  end
567
550
 
551
+ # Applies Calculus.integral on the signal data, and returns a new Signal
552
+ # object with the result.
553
+ def integral
554
+ return Signal.new(:sample_rate => @sample_rate, :data => Calculus.integral(@data))
555
+ end
556
+
568
557
  # Removes all but the given range of frequencies from the signal, using
569
558
  # frequency domain filtering. Modifes and returns the current object.
570
559
  def remove_frequencies! freq_range
@@ -657,4 +646,4 @@ class Signal
657
646
  return self
658
647
  end
659
648
  end
660
- end
649
+ end