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