spcore 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog.rdoc +5 -1
- data/lib/spcore.rb +9 -6
- data/lib/spcore/analysis/calculus.rb +38 -0
- data/lib/spcore/analysis/features.rb +186 -0
- data/lib/spcore/analysis/frequency_domain.rb +191 -0
- data/lib/spcore/analysis/{correlation.rb → statistics.rb} +41 -18
- data/lib/spcore/core/delay_line.rb +1 -1
- data/lib/spcore/filters/fir/dual_sinc_filter.rb +1 -1
- data/lib/spcore/filters/fir/sinc_filter.rb +1 -1
- data/lib/spcore/generation/comb_filter.rb +65 -0
- data/lib/spcore/{core → generation}/oscillator.rb +1 -1
- data/lib/spcore/{util → generation}/signal_generator.rb +1 -1
- data/lib/spcore/util/envelope_detector.rb +1 -1
- data/lib/spcore/util/gain.rb +9 -167
- data/lib/spcore/util/plotter.rb +1 -1
- data/lib/spcore/{analysis → util}/signal.rb +116 -127
- data/lib/spcore/version.rb +1 -1
- data/spcore.gemspec +2 -0
- data/spec/analysis/calculus_spec.rb +54 -0
- data/spec/analysis/features_spec.rb +106 -0
- data/spec/analysis/frequency_domain_spec.rb +147 -0
- data/spec/analysis/piano_C4.wav +0 -0
- data/spec/analysis/statistics_spec.rb +61 -0
- data/spec/analysis/trumpet_B4.wav +0 -0
- data/spec/generation/comb_filter_spec.rb +37 -0
- data/spec/{core → generation}/oscillator_spec.rb +0 -0
- data/spec/{util → generation}/signal_generator_spec.rb +0 -0
- data/spec/interpolation/interpolation_spec.rb +0 -2
- data/spec/{analysis → util}/signal_spec.rb +1 -35
- metadata +64 -22
- data/lib/spcore/analysis/envelope.rb +0 -76
- data/lib/spcore/analysis/extrema.rb +0 -55
- data/spec/analysis/correlation_spec.rb +0 -28
- data/spec/analysis/envelope_spec.rb +0 -50
- data/spec/analysis/extrema_spec.rb +0 -42
@@ -1,18 +1,38 @@
|
|
1
1
|
module SPCore
|
2
|
-
#
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
55
|
+
data.push 1.0
|
36
56
|
else
|
37
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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))
|
data/lib/spcore/util/gain.rb
CHANGED
@@ -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, "
|
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
|
181
|
-
|
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
|
data/lib/spcore/util/plotter.rb
CHANGED
@@ -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 =>
|
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
|
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
|
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
|
-
|
250
|
-
|
251
|
-
freq = (
|
252
|
-
freq_magnitudes[freq] =
|
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
|
-
#
|
301
|
+
# Apply Statistics.mean to the signal data.
|
270
302
|
def mean
|
271
|
-
|
272
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
301
|
-
# or
|
302
|
-
# @param [True/False]
|
303
|
-
#
|
304
|
-
|
305
|
-
|
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
|
-
|
547
|
-
|
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
|
-
|
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
|