spcore 0.1.9 → 0.2.0
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.
- data/ChangeLog.rdoc +7 -1
- data/README.rdoc +3 -0
- data/lib/spcore.rb +1 -0
- data/lib/spcore/analysis/correlation.rb +55 -0
- data/lib/spcore/analysis/envelope.rb +33 -15
- data/lib/spcore/analysis/signal.rb +141 -15
- data/lib/spcore/util/plotter.rb +50 -13
- data/lib/spcore/version.rb +1 -1
- data/spec/analysis/correlation_spec.rb +28 -0
- data/spec/analysis/envelope_spec.rb +20 -13
- data/spec/analysis/signal_spec.rb +68 -0
- metadata +7 -4
data/ChangeLog.rdoc
CHANGED
@@ -48,4 +48,10 @@ Add Envelope and Extrema classes, for signal analysis.
|
|
48
48
|
=== 0.1.9 / 2013-05-06
|
49
49
|
|
50
50
|
Add TukeyWindow.
|
51
|
-
Add Signal#keep_frequences, Signal#remove_frequencies, and Plotter#plot_signals.
|
51
|
+
Add Signal#keep_frequences, Signal#remove_frequencies, and Plotter#plot_signals.
|
52
|
+
|
53
|
+
=== 0.2.0 / 2013-05-21
|
54
|
+
|
55
|
+
Add instance methods to Signal class: #duration, #normalize, #derivative, #lowpass, #highpass, #bandpass, #bandstop, #plot_1d, and #plot_2d.
|
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).
|
data/README.rdoc
CHANGED
@@ -25,6 +25,9 @@ A library of signal processing methods and classes.
|
|
25
25
|
* Oscillator with selectable wave type (sine, square, triangle, sawtooth)
|
26
26
|
* Signal abstraction class
|
27
27
|
* Extrema & Envelope measurement
|
28
|
+
* Normalization
|
29
|
+
* Derivatives
|
30
|
+
* Correlation
|
28
31
|
|
29
32
|
== Examples
|
30
33
|
|
data/lib/spcore.rb
CHANGED
@@ -41,6 +41,7 @@ require 'spcore/resampling/discrete_resampling'
|
|
41
41
|
require 'spcore/resampling/polynomial_resampling'
|
42
42
|
require 'spcore/resampling/hybrid_resampling'
|
43
43
|
|
44
|
+
require 'spcore/analysis/correlation'
|
44
45
|
require 'spcore/analysis/extrema'
|
45
46
|
require 'spcore/analysis/envelope'
|
46
47
|
require 'spcore/analysis/signal'
|
@@ -0,0 +1,55 @@
|
|
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
|
+
|
12
|
+
# @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.
|
14
|
+
# @param [Fixnum] zero_padding Number of zeros to surround the image with.
|
15
|
+
def initialize image, feature, zero_padding = 0
|
16
|
+
raise ArgumentError, "feature size is > image size" if feature.size > image.size
|
17
|
+
|
18
|
+
unless zero_padding == 0
|
19
|
+
image = Array.new(zero_padding, 0) + image + Array.new(zero_padding, 0)
|
20
|
+
end
|
21
|
+
|
22
|
+
feature_mean = feature.inject(0){ |s, x| s + x } / feature.size.to_f
|
23
|
+
feature_diff = feature.map {|x| x - feature_mean }
|
24
|
+
sx = feature_diff.inject(0){ |s, x| s + x**2 }
|
25
|
+
|
26
|
+
@data = []
|
27
|
+
for i in 0...(1 + image.size - feature.size)
|
28
|
+
region = image[i...(i + feature.size)]
|
29
|
+
region_mean = region.inject(0){|s,x| s + x } / feature.size.to_f
|
30
|
+
region_diff = region.map {|x| x - region_mean }
|
31
|
+
sy = region_diff.inject(0){ |s, x| s + x**2 }
|
32
|
+
|
33
|
+
if sx == 0 || sy == 0
|
34
|
+
if sx == 0 && sy == 0
|
35
|
+
@data.push 1.0
|
36
|
+
else
|
37
|
+
@data.push 0.0
|
38
|
+
end
|
39
|
+
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
denom = Math.sqrt(sx*sy)
|
44
|
+
|
45
|
+
sum = 0
|
46
|
+
feature.size.times do |j|
|
47
|
+
sum += (region_diff[j] * feature_diff[j])
|
48
|
+
end
|
49
|
+
|
50
|
+
r = sum / denom
|
51
|
+
@data.push(r)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -11,47 +11,65 @@ class Envelope
|
|
11
11
|
def initialize samples
|
12
12
|
# combine absolute values of positive maxima and negative minima
|
13
13
|
extrema = Extrema.new(samples)
|
14
|
-
|
14
|
+
starting_outline = {}
|
15
15
|
extrema.minima.each do |idx,val|
|
16
16
|
if val <= 0.0
|
17
|
-
|
17
|
+
starting_outline[idx] =val.abs
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
extrema.maxima.each do |idx,val|
|
22
22
|
if val >= 0.0
|
23
|
-
|
23
|
+
starting_outline[idx] = val.abs
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
# add in first and last samples so the envelope follows entire signal
|
28
|
-
|
29
|
-
|
28
|
+
starting_outline[0] = samples[0].abs
|
29
|
+
starting_outline[samples.count - 1] = samples[samples.count - 1].abs
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
# the extrema we have now are probably not spaced evenly. Upsampling at
|
32
|
+
# this point would lead to a time-distorted signal. So the next step is to
|
33
|
+
# interpolate between all the extrema to make a crude but properly sampled
|
34
|
+
# envelope.
|
35
|
+
|
36
|
+
proper_outline = Array.new(samples.count, 0)
|
37
|
+
indices = starting_outline.keys.sort
|
33
38
|
|
34
|
-
# interpolate between all the extrema for the rest of the values. We
|
35
|
-
# manually added first/last index so the whole signal should be covered
|
36
|
-
# by this
|
37
39
|
for i in 1...indices.count
|
38
40
|
l_idx = indices[i-1]
|
39
41
|
r_idx = indices[i]
|
40
42
|
|
41
|
-
l_val =
|
42
|
-
r_val =
|
43
|
+
l_val = starting_outline[l_idx]
|
44
|
+
r_val = starting_outline[r_idx]
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
+
proper_outline[l_idx] = l_val
|
47
|
+
proper_outline[r_idx] = r_val
|
46
48
|
|
47
49
|
idx_span = r_idx - l_idx
|
48
50
|
|
49
51
|
for j in (l_idx + 1)...(r_idx)
|
50
52
|
x = (j - l_idx).to_f / idx_span
|
51
53
|
y = Interpolation.linear l_val, r_val, x
|
52
|
-
|
54
|
+
proper_outline[j] = y
|
53
55
|
end
|
54
56
|
end
|
57
|
+
|
58
|
+
# Now downsample by dropping samples, back to the number of starting_outline we had
|
59
|
+
# with just the extrema, but this time with samples properly spaced so as
|
60
|
+
# to avoid time distortion after upsampling.
|
61
|
+
|
62
|
+
downsample_factor = (samples.count / starting_outline.count).to_i
|
63
|
+
downsampled_outline = []
|
64
|
+
|
65
|
+
(0...proper_outline.count).step(downsample_factor) do |n|
|
66
|
+
downsampled_outline.push proper_outline[n]
|
67
|
+
end
|
68
|
+
|
69
|
+
# finally, use polynomial interpolation to upsample to the original sample rate.
|
70
|
+
|
71
|
+
upsample_factor = samples.count / downsampled_outline.count.to_f
|
72
|
+
@data = PolynomialResampling.upsample(downsampled_outline, upsample_factor)
|
55
73
|
end
|
56
74
|
|
57
75
|
end
|
@@ -37,22 +37,107 @@ class Signal
|
|
37
37
|
def size
|
38
38
|
@data.size
|
39
39
|
end
|
40
|
+
|
41
|
+
# Size of the signal data.
|
42
|
+
def count
|
43
|
+
@data.size
|
44
|
+
end
|
45
|
+
|
46
|
+
# Signal duration in seconds.
|
47
|
+
def duration
|
48
|
+
return @data.size.to_f / @sample_rate
|
49
|
+
end
|
40
50
|
|
41
51
|
# Access signal data.
|
42
52
|
def [](arg)
|
43
53
|
@data[arg]
|
44
54
|
end
|
55
|
+
|
56
|
+
# Plot the signal data against sample numbers.
|
57
|
+
def plot_1d
|
58
|
+
plotter = Plotter.new(:title => "Signal: values vs. sample number", :xtitle => "sample number", :ytitle => "sample value")
|
59
|
+
plotter.plot_1d "signal data" => @data
|
60
|
+
end
|
61
|
+
|
62
|
+
# Plot the signal data against time.
|
63
|
+
def plot_2d
|
64
|
+
plotter = Plotter.new(:title => "Signal: values vs. time", :xtitle => "time (sec)", :ytitle => "sample value")
|
65
|
+
|
66
|
+
data_vs_time = {}
|
67
|
+
sp = 1.0 / @sample_rate
|
68
|
+
@data.each_index do |i|
|
69
|
+
data_vs_time[i * sp] = @data[i]
|
70
|
+
end
|
71
|
+
|
72
|
+
plotter.plot_2d "signal data" => data_vs_time
|
73
|
+
end
|
74
|
+
|
75
|
+
# Run a discrete lowpass filter over the signal data (using SincFilter).
|
76
|
+
# Modifies current object.
|
77
|
+
def lowpass! cutoff_freq, order
|
78
|
+
filter = SincFilter.new(:sample_rate => @sample_rate, :order => order, :cutoff_freq => cutoff_freq)
|
79
|
+
@data = filter.lowpass(@data)
|
80
|
+
return self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Run a discrete lowpass filter over the signal data (using SincFilter).
|
84
|
+
# Return result in a new Signal object.
|
85
|
+
def lowpass cutoff_freq, order
|
86
|
+
self.clone.lowpass! cutoff_freq, order
|
87
|
+
end
|
88
|
+
|
89
|
+
# Run a discrete highpass filter over the signal data (using SincFilter).
|
90
|
+
# Modifies current object.
|
91
|
+
def highpass! cutoff_freq, order
|
92
|
+
filter = SincFilter.new(:sample_rate => @sample_rate, :order => order, :cutoff_freq => cutoff_freq)
|
93
|
+
@data = filter.highpass(@data)
|
94
|
+
return self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Run a discrete highpass filter over the signal data (using SincFilter).
|
98
|
+
# Return result in a new Signal object.
|
99
|
+
def highpass cutoff_freq, order
|
100
|
+
self.clone.highpass! cutoff_freq, order
|
101
|
+
end
|
102
|
+
|
103
|
+
# Run a discrete bandpass filter over the signal data (using DualSincFilter).
|
104
|
+
# Modifies current object.
|
105
|
+
def bandpass! left_cutoff, right_cutoff, order
|
106
|
+
filter = DualSincFilter.new(
|
107
|
+
:sample_rate => @sample_rate,
|
108
|
+
:order => order,
|
109
|
+
:left_cutoff_freq => left_cutoff,
|
110
|
+
:right_cutoff_freq => right_cutoff
|
111
|
+
)
|
112
|
+
@data = filter.bandpass(@data)
|
113
|
+
return self
|
114
|
+
end
|
45
115
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
116
|
+
# Run a discrete bandpass filter over the signal data (using DualSincFilter).
|
117
|
+
# Return result in a new Signal object.
|
118
|
+
def bandpass left_cutoff, right_cutoff, order
|
119
|
+
self.clone.bandpass! left_cutoff, right_cutoff, order
|
120
|
+
end
|
121
|
+
|
122
|
+
# Run a discrete bandstop filter over the signal data (using DualSincFilter).
|
123
|
+
# Modifies current object.
|
124
|
+
def bandstop! left_cutoff, right_cutoff, order
|
125
|
+
filter = DualSincFilter.new(
|
126
|
+
:sample_rate => @sample_rate,
|
127
|
+
:order => order,
|
128
|
+
:left_cutoff_freq => left_cutoff,
|
129
|
+
:right_cutoff_freq => right_cutoff
|
130
|
+
)
|
131
|
+
@data = filter.bandstop(@data)
|
132
|
+
return self
|
54
133
|
end
|
55
134
|
|
135
|
+
# Run a discrete bandstop filter over the signal data (using DualSincFilter).
|
136
|
+
# Return result in a new Signal object.
|
137
|
+
def bandstop left_cutoff, right_cutoff, order
|
138
|
+
self.clone.bandstop! left_cutoff, right_cutoff, order
|
139
|
+
end
|
140
|
+
|
56
141
|
# Increase the sample rate of signal data by the given factor using
|
57
142
|
# discrete upsampling method. Modifies current object.
|
58
143
|
# @param [Fixnum] upsample_factor Increase the sample rate by this factor.
|
@@ -181,21 +266,43 @@ class Signal
|
|
181
266
|
Math.sqrt(energy / size)
|
182
267
|
end
|
183
268
|
|
269
|
+
# Compute the mean of signal data.
|
270
|
+
def mean
|
271
|
+
sum = @data.inject(0){ |s, x| s + x }
|
272
|
+
return sum.to_f / size
|
273
|
+
end
|
274
|
+
|
184
275
|
# Find extrema (maxima, minima) within signal data.
|
185
276
|
def extrema
|
186
277
|
return Extrema.new(@data)
|
187
278
|
end
|
188
279
|
|
280
|
+
# Operate on the signal data (in place) with the absolute value function.
|
281
|
+
def abs!
|
282
|
+
@data = @data.map {|x| x.abs }
|
283
|
+
return self
|
284
|
+
end
|
285
|
+
|
286
|
+
# Operate on copy of the Signal object with the absolute value function.
|
287
|
+
def abs
|
288
|
+
self.clone.abs!
|
289
|
+
end
|
290
|
+
|
291
|
+
def normalize! level = 1.0
|
292
|
+
self.divide!(@data.max / level)
|
293
|
+
end
|
294
|
+
|
295
|
+
# reduce all samples to
|
296
|
+
def normalize level = 1.0
|
297
|
+
self.clone.normalize! level
|
298
|
+
end
|
299
|
+
|
189
300
|
# Determine the envelope of the current Signal and return either a Envelope
|
190
301
|
# or a new Signal object as a result.
|
191
302
|
# @param [True/False] make_signal If true, return envelope data in a new
|
192
303
|
# Otherwise, return an Envelope object.
|
193
|
-
def envelope
|
194
|
-
|
195
|
-
return Signal.new(:sample_rate => @sample_rate, :data => Envelope.new(@data).data)
|
196
|
-
else
|
197
|
-
return Envelope.new(@data)
|
198
|
-
end
|
304
|
+
def envelope
|
305
|
+
Signal.new(:sample_rate => @sample_rate, :data => Envelope.new(@data).data)
|
199
306
|
end
|
200
307
|
|
201
308
|
# Add data in array or other signal to the beginning of current data.
|
@@ -437,7 +544,26 @@ class Signal
|
|
437
544
|
end
|
438
545
|
|
439
546
|
return cross_correlation
|
440
|
-
end
|
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.
|
553
|
+
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)
|
566
|
+
end
|
441
567
|
|
442
568
|
# Removes all but the given range of frequencies from the signal, using
|
443
569
|
# frequency domain filtering. Modifes and returns the current object.
|
data/lib/spcore/util/plotter.rb
CHANGED
@@ -25,11 +25,12 @@ class Plotter
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# Plot XY datapoints.
|
28
|
-
# @param [Hash] titled_hashes A hash that maps
|
29
|
-
# is a hash also, that maps
|
28
|
+
# @param [Hash] titled_hashes A hash that maps title strings to 2d datasets.
|
29
|
+
# The dataset itself is a hash also, that maps
|
30
|
+
# x values to y values.
|
30
31
|
#
|
31
32
|
# @example
|
32
|
-
# Plotter.plot_2d "somedata" => {0.0 => 4.0, 1.0 => 2.0}
|
33
|
+
# Plotter.new.plot_2d "somedata" => {0.0 => 4.0, 1.0 => 2.0}
|
33
34
|
def plot_2d titled_hashes
|
34
35
|
datasets = []
|
35
36
|
titled_hashes.each do |title, hash|
|
@@ -51,24 +52,34 @@ class Plotter
|
|
51
52
|
plot_datasets datasets
|
52
53
|
end
|
53
54
|
|
55
|
+
# Plot XY datapoints.
|
56
|
+
# @param [Hash] titled_hashes A hash that maps title strings to 2d datasets.
|
57
|
+
# The dataset itself is a hash also, that maps
|
58
|
+
# x values to y values.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# Plotter.plot_2d "somedata" => {0.0 => 4.0, 1.0 => 2.0}
|
62
|
+
def self.plot_2d titled_hashes
|
63
|
+
return Plotter.new.plot_2d titled_hashes
|
64
|
+
end
|
65
|
+
|
54
66
|
# Plot a sequence of values.
|
55
|
-
# @param [Hash] titled_sequences A hash that maps
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# them against the fraction (sample_number / total_samples).
|
67
|
+
# @param [Hash] titled_sequences A hash that maps title strings to data
|
68
|
+
# sequences. The data itself is an array of
|
69
|
+
# values. In the plot, values will be mapped to
|
70
|
+
# their index in the sequence.
|
60
71
|
#
|
61
72
|
# @example
|
62
|
-
# Plotter.plot_1d "somedata" => [0,2,3,6,3,-1]
|
63
|
-
def plot_1d titled_sequences
|
73
|
+
# Plotter.new.plot_1d "somedata" => [0,2,3,6,3,-1]
|
74
|
+
def plot_1d titled_sequences
|
64
75
|
datasets = []
|
65
76
|
titled_sequences.each do |title, sequence|
|
66
77
|
indices = Array.new(sequence.size)
|
67
78
|
sequence.each_index do |i|
|
68
79
|
indices[i] = i
|
69
|
-
if plot_against_fraction
|
70
|
-
|
71
|
-
end
|
80
|
+
#if plot_against_fraction
|
81
|
+
# indices[i] /= sequence.size.to_f
|
82
|
+
#end
|
72
83
|
end
|
73
84
|
|
74
85
|
dataset = Gnuplot::DataSet.new( [indices, sequence] ){ |ds|
|
@@ -82,6 +93,18 @@ class Plotter
|
|
82
93
|
plot_datasets datasets
|
83
94
|
end
|
84
95
|
|
96
|
+
# Plot a sequence of values.
|
97
|
+
# @param [Hash] titled_sequences A hash that maps title strings to data
|
98
|
+
# sequences. The data itself is an array of
|
99
|
+
# values. In the plot, values will be mapped to
|
100
|
+
# their index in the sequence.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# Plotter.plot_1d "somedata" => [0,2,3,6,3,-1]
|
104
|
+
def self.plot_1d titled_sequences
|
105
|
+
return Plotter.new.plot_1d titled_sequences
|
106
|
+
end
|
107
|
+
|
85
108
|
# Plot Gnuplot::DataSet objects.
|
86
109
|
# @param [Array] datasets An array of Gnuplot::DataSet objects.
|
87
110
|
def plot_datasets datasets
|
@@ -98,7 +121,15 @@ class Plotter
|
|
98
121
|
end
|
99
122
|
end
|
100
123
|
end
|
124
|
+
|
125
|
+
# Plot Gnuplot::DataSet objects.
|
126
|
+
# @param [Array] datasets An array of Gnuplot::DataSet objects.
|
127
|
+
def self.plot_datasets datasets
|
128
|
+
Plotter.new.plot_datasets datasets
|
129
|
+
end
|
101
130
|
|
131
|
+
# Plot data from Signal objects.
|
132
|
+
# @param [Hash] signals_hash A hash that maps title strings Signal objects
|
102
133
|
def plot_signals signals_hash
|
103
134
|
data_hash = {}
|
104
135
|
signals_hash.each do |name, signal|
|
@@ -106,5 +137,11 @@ class Plotter
|
|
106
137
|
end
|
107
138
|
plot_1d data_hash
|
108
139
|
end
|
140
|
+
|
141
|
+
# Plot data from Signal objects.
|
142
|
+
# @param [Hash] signals_hash A hash that maps title strings Signal objects
|
143
|
+
def self.plot_signals signals_hash
|
144
|
+
Plotter.new.plot_signals signals_hash
|
145
|
+
end
|
109
146
|
end
|
110
147
|
end
|
data/lib/spcore/version.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe SPCore::Correlation do
|
4
|
+
context 'image => triangular window' do
|
5
|
+
before :all do
|
6
|
+
@size = size = 48
|
7
|
+
@triangle = TriangularWindow.new(size * 2).data
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'feature => rising ramp (half size of triangular window)' do
|
11
|
+
it 'should have maximum correlation at beginning' do
|
12
|
+
rising_ramp = Array.new(@size){|i| i / @size.to_f }
|
13
|
+
correlation = Correlation.new(@triangle, rising_ramp)
|
14
|
+
correlation.data.first.should eq(correlation.data.max)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'feature => falling ramp (half size of triangular window)' do
|
19
|
+
it 'should have maximum correlation at end' do
|
20
|
+
falling_ramp = Array.new(@size){|i| (@size - i) / @size.to_f }
|
21
|
+
correlation = Correlation.new(@triangle, falling_ramp)
|
22
|
+
correlation.data.last.should eq(correlation.data.max)
|
23
|
+
|
24
|
+
#Plotter.plot_1d "correlate falling ramp" => correlation.data
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,22 +1,27 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
3
|
describe SPCore::Envelope do
|
4
|
-
|
4
|
+
before :all do
|
5
5
|
sample_rate = 1000
|
6
|
-
sample_count = 512
|
6
|
+
sample_count = 512 * 2
|
7
7
|
generator = SignalGenerator.new(:size => sample_count, :sample_rate => sample_rate)
|
8
8
|
|
9
|
-
modulation_signal = generator.make_signal [4.0]
|
10
|
-
modulation_signal.multiply! BlackmanWindow.new(sample_count).data
|
9
|
+
@modulation_signal = generator.make_signal [4.0], :amplitude => 0.1
|
10
|
+
@modulation_signal.multiply! BlackmanWindow.new(sample_count).data
|
11
11
|
|
12
|
-
base_signal = generator.make_signal [64.0]
|
13
|
-
base_signal.multiply! modulation_signal
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
@base_signal = generator.make_signal [64.0]
|
13
|
+
@base_signal.multiply! @modulation_signal
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should produce an output that follows the amplitude of the input' do
|
17
|
+
envelope = @base_signal.envelope
|
18
|
+
check_envelope(envelope)
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_envelope envelope
|
17
22
|
#signals = {
|
18
|
-
# "signal" => base_signal,
|
19
|
-
# "modulation" => modulation_signal,
|
23
|
+
# "signal" => @base_signal,
|
24
|
+
# "modulation (abs)" => @modulation_signal.abs,
|
20
25
|
# "envelope" => envelope,
|
21
26
|
#}
|
22
27
|
#
|
@@ -26,15 +31,17 @@ describe SPCore::Envelope do
|
|
26
31
|
# :ylabel => "values",
|
27
32
|
#).plot_signals(signals)
|
28
33
|
|
34
|
+
#Plotter.new().plot_2d("envelop freq magnitudes" => envelope.freq_magnitudes)
|
35
|
+
|
29
36
|
begin
|
30
|
-
ideal = modulation_signal.energy
|
37
|
+
ideal = @modulation_signal.energy
|
31
38
|
actual = envelope.energy
|
32
39
|
error = (ideal - actual).abs / ideal
|
33
40
|
error.should be_within(0.1).of(0.0)
|
34
41
|
end
|
35
42
|
|
36
43
|
begin
|
37
|
-
ideal = modulation_signal.rms
|
44
|
+
ideal = @modulation_signal.rms
|
38
45
|
actual = envelope.rms
|
39
46
|
error = (ideal - actual).abs / ideal
|
40
47
|
error.should be_within(0.1).of(0.0)
|
@@ -1,6 +1,74 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
3
|
describe SPCore::Signal do
|
4
|
+
describe '#duration' do
|
5
|
+
it 'should produce duration in seconds, according to sample_rate' do
|
6
|
+
sample_rate = 2000
|
7
|
+
[5,50,100,1500].each do |count|
|
8
|
+
zeros = Array.new(count, 0)
|
9
|
+
signal = SPCore::Signal.new(:data => zeros, :sample_rate => sample_rate)
|
10
|
+
expected_duration = count.to_f / sample_rate
|
11
|
+
signal.duration.should eq expected_duration
|
12
|
+
end
|
13
|
+
end
|
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
|
+
|
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
|
+
describe '#normalize' do
|
50
|
+
before :all do
|
51
|
+
generator = SignalGenerator.new(:sample_rate => 2000, :size => 256)
|
52
|
+
@signal = generator.make_signal([40.0, 160.0]).multiply(HannWindow.new(256).data)
|
53
|
+
@max = @signal.data.max
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should be able to normalize to a level lower than peak value' do
|
57
|
+
normalized = @signal.normalize(@max * 0.75)
|
58
|
+
normalized.data.max.should eq(@max * 0.75)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should be able to normalize to a level higher than peak value' do
|
62
|
+
normalized = @signal.normalize(@max * 1.5)
|
63
|
+
normalized.data.max.should eq(@max * 1.5)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should be able to normalize to a level equal to peak value' do
|
67
|
+
normalized = @signal.normalize(@max)
|
68
|
+
normalized.data.max.should eq(@max)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
4
72
|
describe '#remove_frequencies' do
|
5
73
|
before :each do
|
6
74
|
generator = SignalGenerator.new(:sample_rate => 2000, :size => 256)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spcore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hashmake
|
@@ -144,6 +144,7 @@ files:
|
|
144
144
|
- README.rdoc
|
145
145
|
- Rakefile
|
146
146
|
- lib/spcore.rb
|
147
|
+
- lib/spcore/analysis/correlation.rb
|
147
148
|
- lib/spcore/analysis/envelope.rb
|
148
149
|
- lib/spcore/analysis/extrema.rb
|
149
150
|
- lib/spcore/analysis/signal.rb
|
@@ -190,6 +191,7 @@ files:
|
|
190
191
|
- lib/spcore/windows/triangular_window.rb
|
191
192
|
- lib/spcore/windows/tukey_window.rb
|
192
193
|
- spcore.gemspec
|
194
|
+
- spec/analysis/correlation_spec.rb
|
193
195
|
- spec/analysis/envelope_spec.rb
|
194
196
|
- spec/analysis/extrema_spec.rb
|
195
197
|
- spec/analysis/signal_spec.rb
|
@@ -228,7 +230,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
228
230
|
version: '0'
|
229
231
|
segments:
|
230
232
|
- 0
|
231
|
-
hash:
|
233
|
+
hash: 688071795
|
232
234
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
233
235
|
none: false
|
234
236
|
requirements:
|
@@ -237,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
237
239
|
version: '0'
|
238
240
|
segments:
|
239
241
|
- 0
|
240
|
-
hash:
|
242
|
+
hash: 688071795
|
241
243
|
requirements: []
|
242
244
|
rubyforge_project:
|
243
245
|
rubygems_version: 1.8.23
|
@@ -245,6 +247,7 @@ signing_key:
|
|
245
247
|
specification_version: 3
|
246
248
|
summary: A library of signal processing methods and classes.
|
247
249
|
test_files:
|
250
|
+
- spec/analysis/correlation_spec.rb
|
248
251
|
- spec/analysis/envelope_spec.rb
|
249
252
|
- spec/analysis/extrema_spec.rb
|
250
253
|
- spec/analysis/signal_spec.rb
|