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