spcore 0.1.2 → 0.1.3
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 +14 -1
- data/README.rdoc +10 -2
- data/lib/spcore.rb +45 -14
- data/lib/spcore/{lib → core}/circular_buffer.rb +28 -1
- data/lib/spcore/core/constants.rb +7 -1
- data/lib/spcore/{lib → core}/delay_line.rb +15 -5
- data/lib/spcore/{lib → core}/envelope_detector.rb +13 -1
- data/lib/spcore/{lib → core}/oscillator.rb +33 -3
- data/lib/spcore/core/signal.rb +350 -0
- data/lib/spcore/filters/fir/dual_sinc_filter.rb +84 -0
- data/lib/spcore/filters/fir/fir.rb +87 -0
- data/lib/spcore/filters/fir/sinc_filter.rb +68 -0
- data/lib/spcore/{lib → filters/iir}/biquad_filter.rb +7 -0
- data/lib/spcore/{lib → filters/iir}/cookbook_allpass_filter.rb +2 -0
- data/lib/spcore/{lib → filters/iir}/cookbook_bandpass_filter.rb +2 -0
- data/lib/spcore/{lib → filters/iir}/cookbook_highpass_filter.rb +2 -0
- data/lib/spcore/{lib → filters/iir}/cookbook_lowpass_filter.rb +2 -0
- data/lib/spcore/{lib → filters/iir}/cookbook_notch_filter.rb +2 -0
- data/lib/spcore/interpolation/interpolation.rb +64 -0
- data/lib/spcore/resampling/discrete_resampling.rb +78 -0
- data/lib/spcore/resampling/hybrid_resampling.rb +30 -0
- data/lib/spcore/resampling/polynomial_resampling.rb +56 -0
- data/lib/spcore/transforms/dft.rb +47 -0
- data/lib/spcore/transforms/fft.rb +125 -0
- data/lib/spcore/{lib → util}/gain.rb +3 -0
- data/lib/spcore/{core → util}/limiters.rb +10 -1
- data/lib/spcore/util/plotter.rb +91 -0
- data/lib/spcore/{lib → util}/saturation.rb +2 -9
- data/lib/spcore/util/scale.rb +41 -0
- data/lib/spcore/util/signal_generator.rb +48 -0
- data/lib/spcore/version.rb +2 -1
- data/lib/spcore/windows/bartlett_hann_window.rb +15 -0
- data/lib/spcore/windows/bartlett_window.rb +13 -0
- data/lib/spcore/windows/blackman_harris_window.rb +14 -0
- data/lib/spcore/windows/blackman_nuttall_window.rb +14 -0
- data/lib/spcore/windows/blackman_window.rb +18 -0
- data/lib/spcore/windows/cosine_window.rb +13 -0
- data/lib/spcore/windows/flat_top_window.rb +27 -0
- data/lib/spcore/windows/gaussian_window.rb +15 -0
- data/lib/spcore/windows/hamming_window.rb +16 -0
- data/lib/spcore/windows/hann_window.rb +13 -0
- data/lib/spcore/windows/lanczos_window.rb +15 -0
- data/lib/spcore/windows/nuttall_window.rb +14 -0
- data/lib/spcore/windows/rectangular_window.rb +10 -0
- data/lib/spcore/windows/triangular_window.rb +14 -0
- data/spcore.gemspec +11 -3
- data/spec/{lib → core}/circular_buffer_spec.rb +0 -0
- data/spec/{lib → core}/delay_line_spec.rb +1 -1
- data/spec/{lib → core}/envelope_detector_spec.rb +3 -3
- data/spec/{lib → core}/oscillator_spec.rb +0 -0
- data/spec/filters/fir/dual_sinc_filter_spec.rb +64 -0
- data/spec/filters/fir/sinc_filter_spec.rb +57 -0
- data/spec/filters/iir/cookbook_filter_spec.rb +30 -0
- data/spec/interpolation/interpolation_spec.rb +49 -0
- data/spec/resampling/discrete_resampling_spec.rb +81 -0
- data/spec/resampling/hybrid_resampling_spec.rb +31 -0
- data/spec/resampling/polynomial_resampling_spec.rb +30 -0
- data/spec/transforms/dft_spec.rb +71 -0
- data/spec/transforms/fft_spec.rb +84 -0
- data/spec/{lib → util}/gain_spec.rb +2 -2
- data/spec/{core → util}/limiters_spec.rb +0 -0
- data/spec/{lib → util}/saturate_spec.rb +0 -0
- data/spec/util/signal_generator_spec.rb +54 -0
- data/spec/windows/window_spec.rb +33 -0
- metadata +90 -42
- data/lib/spcore/lib/interpolation.rb +0 -15
- data/spec/lib/cookbook_filter_spec.rb +0 -44
- data/spec/lib/interpolation_spec.rb +0 -21
data/ChangeLog.rdoc
CHANGED
@@ -10,4 +10,17 @@
|
|
10
10
|
|
11
11
|
=== 0.1.1 / 2013-02-04
|
12
12
|
|
13
|
-
Add EnvelopeDetector#attack_time= and EnvelopeDetector#release_time=
|
13
|
+
Add EnvelopeDetector#attack_time= and EnvelopeDetector#release_time=
|
14
|
+
|
15
|
+
=== 0.1.3 / 2013-02-18
|
16
|
+
|
17
|
+
* Added:
|
18
|
+
** A .cubic_hermite method to Interpolation class (implements cubic hermite polynomial interpolation)
|
19
|
+
** Window classes (Blackman, Hann, Hamming, etc.)
|
20
|
+
** DFT class, with .forward and .inverse methods.
|
21
|
+
** FFT class, with .forward and .inverse methods.
|
22
|
+
** Windowed sinc filter, a FIR filter for lowpass and highpass-
|
23
|
+
** Dual windowed sinc filter, a FIR filter for bandpass and bandstop.
|
24
|
+
** Discrete and Polynomial resampling classes, each with an .upsample method.
|
25
|
+
** Plotter class to make graphing with gnuplot easier. Has #plot_1d and #plot_2d methods.
|
26
|
+
** Signal class for testing convenience. Contains signal data and has convenience methods for plotting, correlation, energy, etc.
|
data/README.rdoc
CHANGED
@@ -6,16 +6,24 @@
|
|
6
6
|
|
7
7
|
== Description
|
8
8
|
|
9
|
-
|
9
|
+
A library of signal processing methods and classes.
|
10
10
|
|
11
11
|
== Features
|
12
12
|
|
13
|
+
Resampling (discrete up, down and up/down, polynomial up, and hybrid up/down)
|
14
|
+
FFT transform (forward and inverse)
|
15
|
+
DFT transform (forward and inverse)
|
16
|
+
Windows (Blackman, Hamming, etc.)
|
17
|
+
Windowed sinc filter for lowpass and highpass.
|
18
|
+
Dual windowed sinc filter for bandpass and bandstop.
|
19
|
+
Interpolation (linear and polynomial)
|
20
|
+
Data plotting via gnuplot (must be installed to use).
|
13
21
|
Delay line
|
14
22
|
Biquad filters
|
15
23
|
Envelope detector
|
16
24
|
Conversion from dB-linear and linear-dB
|
17
|
-
Linear interpolation
|
18
25
|
Oscillator with selectable wave type (sine, square, triangle, sawtooth)
|
26
|
+
Signal abstraction class.
|
19
27
|
|
20
28
|
== Examples
|
21
29
|
|
data/lib/spcore.rb
CHANGED
@@ -1,19 +1,50 @@
|
|
1
1
|
require 'hashmake'
|
2
2
|
require 'spcore/version'
|
3
3
|
|
4
|
-
require 'spcore/core/
|
4
|
+
require 'spcore/core/circular_buffer'
|
5
5
|
require 'spcore/core/constants'
|
6
|
+
require 'spcore/core/delay_line'
|
7
|
+
require 'spcore/core/envelope_detector'
|
8
|
+
require 'spcore/core/oscillator'
|
9
|
+
require 'spcore/core/signal'
|
6
10
|
|
7
|
-
require 'spcore/
|
8
|
-
require 'spcore/
|
9
|
-
require 'spcore/
|
10
|
-
require 'spcore/
|
11
|
-
require 'spcore/
|
12
|
-
require 'spcore/
|
13
|
-
require 'spcore/
|
14
|
-
require 'spcore/
|
15
|
-
require 'spcore/
|
16
|
-
require 'spcore/
|
17
|
-
require 'spcore/
|
18
|
-
require 'spcore/
|
19
|
-
require 'spcore/
|
11
|
+
require 'spcore/windows/bartlett_hann_window'
|
12
|
+
require 'spcore/windows/bartlett_window'
|
13
|
+
require 'spcore/windows/blackman_harris_window'
|
14
|
+
require 'spcore/windows/blackman_nuttall_window'
|
15
|
+
require 'spcore/windows/blackman_window'
|
16
|
+
require 'spcore/windows/cosine_window'
|
17
|
+
require 'spcore/windows/flat_top_window'
|
18
|
+
require 'spcore/windows/gaussian_window'
|
19
|
+
require 'spcore/windows/hamming_window'
|
20
|
+
require 'spcore/windows/hann_window'
|
21
|
+
require 'spcore/windows/lanczos_window'
|
22
|
+
require 'spcore/windows/nuttall_window'
|
23
|
+
require 'spcore/windows/rectangular_window'
|
24
|
+
require 'spcore/windows/triangular_window'
|
25
|
+
|
26
|
+
require 'spcore/filters/fir/fir'
|
27
|
+
require 'spcore/filters/fir/sinc_filter'
|
28
|
+
require 'spcore/filters/fir/dual_sinc_filter'
|
29
|
+
require 'spcore/filters/iir/biquad_filter'
|
30
|
+
require 'spcore/filters/iir/cookbook_allpass_filter'
|
31
|
+
require 'spcore/filters/iir/cookbook_bandpass_filter'
|
32
|
+
require 'spcore/filters/iir/cookbook_highpass_filter'
|
33
|
+
require 'spcore/filters/iir/cookbook_lowpass_filter'
|
34
|
+
require 'spcore/filters/iir/cookbook_notch_filter'
|
35
|
+
|
36
|
+
require 'spcore/interpolation/interpolation'
|
37
|
+
|
38
|
+
require 'spcore/transforms/dft'
|
39
|
+
require 'spcore/transforms/fft'
|
40
|
+
|
41
|
+
require 'spcore/resampling/discrete_resampling'
|
42
|
+
require 'spcore/resampling/polynomial_resampling'
|
43
|
+
require 'spcore/resampling/hybrid_resampling'
|
44
|
+
|
45
|
+
require 'spcore/util/gain'
|
46
|
+
require 'spcore/util/limiters'
|
47
|
+
require 'spcore/util/plotter'
|
48
|
+
require 'spcore/util/saturation'
|
49
|
+
require 'spcore/util/scale'
|
50
|
+
require 'spcore/util/signal_generator'
|
@@ -1,9 +1,26 @@
|
|
1
1
|
module SPCore
|
2
|
+
# A circular (ring) buffer. The same array is used to store data over the life
|
3
|
+
# of the buffer, unless the size is changed. As data is pushed and popped indices
|
4
|
+
# are updated to track the oldest and newest elements.
|
5
|
+
#
|
6
|
+
# Push behavior can be modified by setting override_when_full. If true, when the
|
7
|
+
# fill count reaches buffer size then new data will override the oldest data. If
|
8
|
+
# false, when fill count is maxed the new data will raise an exception.
|
9
|
+
#
|
10
|
+
# Pop behavior can be modified by setting fifo to true or false. If true, pop will
|
11
|
+
# return the oldest data (data first pushed). If false, pop will return the newest
|
12
|
+
# data (data last pushed).
|
13
|
+
#
|
14
|
+
# @author James Tunnell
|
2
15
|
class CircularBuffer
|
3
16
|
|
4
17
|
attr_accessor :fifo, :override_when_full
|
5
18
|
attr_reader :fill_count
|
6
|
-
|
19
|
+
|
20
|
+
# A new instance of CircularBuffer.
|
21
|
+
# @param [Fixnum] size The buffer size (and maximum fill count).
|
22
|
+
# @param [Hash] opts Contain optional arguments to modify buffer push/pop behavior.
|
23
|
+
# Valid keys are :override_when_full and :fifo.
|
7
24
|
def initialize size, opts = {}
|
8
25
|
|
9
26
|
opts = { :fifo => true, :override_when_full => true }.merge opts
|
@@ -17,18 +34,22 @@ class CircularBuffer
|
|
17
34
|
@override_when_full = opts[:override_when_full]
|
18
35
|
end
|
19
36
|
|
37
|
+
# Return the buffer size (max fill count).
|
20
38
|
def size
|
21
39
|
return @buffer.count
|
22
40
|
end
|
23
41
|
|
42
|
+
# Return true if the buffer is empty.
|
24
43
|
def empty?
|
25
44
|
return @fill_count == 0
|
26
45
|
end
|
27
46
|
|
47
|
+
# Return true if the buffer is full (fill count == buffer size).
|
28
48
|
def full?
|
29
49
|
return (@fill_count == size)
|
30
50
|
end
|
31
51
|
|
52
|
+
# Change the buffer size, allocating a new backing array at the same time.
|
32
53
|
def resize size
|
33
54
|
rv = false
|
34
55
|
if(size != @buffer.count)
|
@@ -41,6 +62,8 @@ class CircularBuffer
|
|
41
62
|
return rv
|
42
63
|
end
|
43
64
|
|
65
|
+
# Return an array containing the data layed out contiguously (data normally is
|
66
|
+
# split somewhere along the backing array).
|
44
67
|
def to_ary
|
45
68
|
if empty?
|
46
69
|
return []
|
@@ -59,6 +82,7 @@ class CircularBuffer
|
|
59
82
|
end
|
60
83
|
end
|
61
84
|
|
85
|
+
# push a single data element.
|
62
86
|
def push element
|
63
87
|
if full?
|
64
88
|
raise ArgumentError, "buffer is full, and override_when_full is false" unless @override_when_full
|
@@ -81,12 +105,14 @@ class CircularBuffer
|
|
81
105
|
end
|
82
106
|
end
|
83
107
|
|
108
|
+
# push an array of data elements.
|
84
109
|
def push_ary ary
|
85
110
|
ary.each do |element|
|
86
111
|
push element
|
87
112
|
end
|
88
113
|
end
|
89
114
|
|
115
|
+
# access the latest data element.
|
90
116
|
def newest relative_index = 0
|
91
117
|
raise ArgumentError, "buffer is empty" if empty?
|
92
118
|
raise ArgumentError, "relative_index is greater or equal to @fill_count" if relative_index >= @fill_count
|
@@ -105,6 +131,7 @@ class CircularBuffer
|
|
105
131
|
return @buffer[absIdx];
|
106
132
|
end
|
107
133
|
|
134
|
+
# access the oldest data element.
|
108
135
|
def oldest relative_index = 0
|
109
136
|
raise ArgumentError, "buffer is empty" if empty?
|
110
137
|
raise ArgumentError, "relative_index is greater or equal to @fill_count" if relative_index >= @fill_count
|
@@ -1,15 +1,24 @@
|
|
1
1
|
module SPCore
|
2
|
+
# Delays samples for a period of time by pushing them through a circular buffer.
|
3
|
+
#
|
4
|
+
# @author James Tunnell
|
2
5
|
class DelayLine
|
3
6
|
include Hashmake::HashMakeable
|
4
7
|
|
5
8
|
attr_reader :sample_rate, :max_delay_seconds, :delay_seconds, :delay_samples
|
6
|
-
|
9
|
+
|
10
|
+
# Used to process hashed arguments in #initialize.
|
7
11
|
ARG_SPECS = [
|
8
12
|
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
9
13
|
Hashmake::ArgSpec.new(:reqd => true, :key => :max_delay_seconds, :type => Float, :validator => ->(a){ (a > 0.0) } ),
|
10
14
|
Hashmake::ArgSpec.new(:reqd => false, :key => :delay_seconds, :type => Float, :default => 0.0, :validator => ->(a){ a >= 0.0 } ),
|
11
15
|
]
|
12
16
|
|
17
|
+
# A new instance of DelayLine. The circular buffer is filled by pushing an array
|
18
|
+
# of zeros.
|
19
|
+
# @param [Hash] args Hashed arguments. Valid keys are :sample_rate (reqd),
|
20
|
+
# :max_delay_seconds (reqd) and :delay_seconds (not reqd).
|
21
|
+
# See ARG_SPECS for more details.
|
13
22
|
def initialize args
|
14
23
|
hash_make DelayLine::ARG_SPECS, args
|
15
24
|
raise ArgumentError, "delay_seconds #{delay_seconds} is greater than max_delay_seconds #{max_delay_seconds}" if @delay_seconds > @max_delay_seconds
|
@@ -18,20 +27,21 @@ class DelayLine
|
|
18
27
|
self.delay_seconds=(@delay_seconds)
|
19
28
|
end
|
20
29
|
|
30
|
+
# Set the delay in seconds. Actual delay will vary according because an
|
31
|
+
# integer number of delay samples is used.
|
21
32
|
def delay_seconds= delay_seconds
|
22
33
|
delay_samples_floor = (@sample_rate * delay_seconds).floor
|
23
34
|
@delay_samples = delay_samples_floor.to_i
|
24
35
|
@delay_seconds = delay_samples_floor / @sample_rate
|
25
|
-
|
26
|
-
#if @buffer.fill_count < @delay_samples
|
27
|
-
# @buffer.push_ary Array.new(@delay_samples - @buffer.fill_count, 0.0)
|
28
|
-
#end
|
29
36
|
end
|
30
37
|
|
38
|
+
# Push a new sample through the circular buffer, overriding the oldest.
|
31
39
|
def push_sample sample
|
32
40
|
@buffer.push sample
|
33
41
|
end
|
34
42
|
|
43
|
+
# Get the sample which is delayed by the number of samples that equates
|
44
|
+
# to the set delay in seconds.
|
35
45
|
def delayed_sample
|
36
46
|
return @buffer.newest(@delay_samples)
|
37
47
|
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
module SPCore
|
2
|
+
# Tracks the envelope of samples as they are passed in one by one.
|
3
|
+
#
|
4
|
+
# @author James Tunnell
|
2
5
|
class EnvelopeDetector
|
3
6
|
include Hashmake::HashMakeable
|
4
7
|
|
8
|
+
# Used to process hashed arguments in #initialize.
|
5
9
|
ARG_SPECS = [
|
6
10
|
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
7
11
|
Hashmake::ArgSpec.new(:reqd => true, :key => :attack_time, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
@@ -9,7 +13,12 @@ class EnvelopeDetector
|
|
9
13
|
]
|
10
14
|
|
11
15
|
attr_reader :envelope, :sample_rate, :attack_time, :release_time
|
12
|
-
|
16
|
+
|
17
|
+
# A new instance of EnvelopeDetector. The envelope is initialized to zero.
|
18
|
+
#
|
19
|
+
# @param [Hash] args Hashed arguments. Valid keys are :sample_rate (reqd),
|
20
|
+
# :attack_time (in seconds) (reqd) and :release_time
|
21
|
+
# (in seconds) (reqd). See ARG_SPECS for more details.
|
13
22
|
def initialize args
|
14
23
|
hash_make EnvelopeDetector::ARG_SPECS, args
|
15
24
|
|
@@ -19,18 +28,21 @@ class EnvelopeDetector
|
|
19
28
|
@envelope = 0.0
|
20
29
|
end
|
21
30
|
|
31
|
+
# Set the attack time (in seconds).
|
22
32
|
def attack_time= attack_time
|
23
33
|
raise ArgumentError, "attack_time is <= 0.0" if attack_time <= 0.0
|
24
34
|
@g_attack = Math.exp(-1.0 / (sample_rate * attack_time))
|
25
35
|
@attack_time = attack_time
|
26
36
|
end
|
27
37
|
|
38
|
+
# Set the release time (in seconds).
|
28
39
|
def release_time= release_time
|
29
40
|
raise ArgumentError, "release_time is <= 0.0" if release_time <= 0.0
|
30
41
|
@g_release = Math.exp(-1.0 / (sample_rate * release_time))
|
31
42
|
@release_time = release_time
|
32
43
|
end
|
33
44
|
|
45
|
+
# Process a sample, returning the updated envelope.
|
34
46
|
def process_sample sample
|
35
47
|
input_abs = sample.abs
|
36
48
|
|
@@ -1,16 +1,26 @@
|
|
1
1
|
module SPCore
|
2
|
+
# A generic oscillator base class, which can render a sample for any phase
|
3
|
+
# between -PI and +PI.
|
4
|
+
#
|
5
|
+
# @author James Tunnell
|
2
6
|
class Oscillator
|
3
7
|
include Hashmake::HashMakeable
|
4
8
|
attr_accessor :wave_type, :amplitude, :dc_offset
|
5
9
|
attr_reader :frequency, :sample_rate, :phase_offset
|
6
10
|
|
11
|
+
# Defines a sine wave type.
|
7
12
|
WAVE_SINE = :waveSine
|
13
|
+
# Defines a triangle wave type.
|
8
14
|
WAVE_TRIANGLE = :waveTriangle
|
15
|
+
# Defines a sawtooth wave type.
|
9
16
|
WAVE_SAWTOOTH = :waveSawtooth
|
17
|
+
# Defines a square wave type.
|
10
18
|
WAVE_SQUARE = :waveSquare
|
11
19
|
|
20
|
+
# Defines a list of the valid wave types.
|
12
21
|
WAVES = [WAVE_SINE, WAVE_TRIANGLE, WAVE_SAWTOOTH, WAVE_SQUARE]
|
13
22
|
|
23
|
+
# Used to process hashed arguments in #initialize.
|
14
24
|
ARG_SPECS = [
|
15
25
|
Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
|
16
26
|
Hashmake::ArgSpec.new(:reqd => false, :key => :wave_type, :type => Symbol, :default => WAVE_SINE, :validator => ->(a){ WAVES.include? a } ),
|
@@ -19,7 +29,14 @@ class Oscillator
|
|
19
29
|
Hashmake::ArgSpec.new(:reqd => false, :key => :phase_offset, :type => Float, :default => 0.0 ),
|
20
30
|
Hashmake::ArgSpec.new(:reqd => false, :key => :dc_offset, :type => Float, :default => 0.0 ),
|
21
31
|
]
|
22
|
-
|
32
|
+
|
33
|
+
# A new instance of Oscillator. The controllable wave parameters are frequency,
|
34
|
+
# amplitude, phase offset, and DC offset. The current phase angle is initialized
|
35
|
+
# to the given phase offset.
|
36
|
+
#
|
37
|
+
# @param [Hash] args Hashed arguments. Required key is :sample_rate. Optional keys are
|
38
|
+
# :wave_type, :frequency, :amplitude, :phase_offset, and :dc_offset.
|
39
|
+
# See ARG_SPECS for more details.
|
23
40
|
def initialize args
|
24
41
|
hash_make Oscillator::ARG_SPECS, args
|
25
42
|
|
@@ -27,25 +44,33 @@ class Oscillator
|
|
27
44
|
@current_phase_angle = @phase_offset
|
28
45
|
end
|
29
46
|
|
47
|
+
# Set the sample rate (also updates the rate at which phase angle increments).
|
48
|
+
# @raise [ArgumentError] if sample rate is not positive.
|
30
49
|
def sample_rate= sample_rate
|
50
|
+
raise ArgumentError, "sample_rate is not > 0" unless sample_rate > 0
|
31
51
|
@sample_rate = sample_rate
|
32
52
|
self.frequency = @frequency
|
33
53
|
end
|
34
54
|
|
55
|
+
# Set the frequency (also updates the rate at which phase angle increments).
|
35
56
|
def frequency= frequency
|
57
|
+
raise ArgumentError, "frequency is not > 0" unless frequency > 0
|
36
58
|
@frequency = frequency
|
37
59
|
@phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
|
38
60
|
end
|
39
61
|
|
62
|
+
# Set the phase angle offset. Update the current phase angle according to the
|
63
|
+
# difference between the current phase offset and the new phase offset.
|
40
64
|
def phase_offset= phase_offset
|
41
65
|
@current_phase_angle += (phase_offset - @phase_offset);
|
42
66
|
@phase_offset = phase_offset
|
43
67
|
end
|
44
68
|
|
69
|
+
# Step forward one sampling period and sample the oscillator waveform.
|
45
70
|
def sample
|
46
71
|
output = 0.0
|
47
72
|
|
48
|
-
while(@current_phase_angle <
|
73
|
+
while(@current_phase_angle < -Math::PI)
|
49
74
|
@current_phase_angle += TWO_PI
|
50
75
|
end
|
51
76
|
|
@@ -69,10 +94,13 @@ class Oscillator
|
|
69
94
|
@current_phase_angle += @phase_angle_incr
|
70
95
|
return output
|
71
96
|
end
|
72
|
-
|
97
|
+
|
98
|
+
# constant used to calculate sine wave
|
73
99
|
K_SINE_B = 4.0 / Math::PI
|
100
|
+
# constant used to calculate sine wave
|
74
101
|
K_SINE_C = -4.0 / (Math::PI * Math::PI)
|
75
102
|
# Q = 0.775
|
103
|
+
# constant used to calculate sine wave
|
76
104
|
K_SINE_P = 0.225
|
77
105
|
|
78
106
|
# generate a sine wave:
|
@@ -88,6 +116,7 @@ class Oscillator
|
|
88
116
|
return y
|
89
117
|
end
|
90
118
|
|
119
|
+
# constant used to calculate triangle wave
|
91
120
|
K_TRIANGLE_A = 2.0 / Math::PI;
|
92
121
|
|
93
122
|
# generate a triangle wave:
|
@@ -104,6 +133,7 @@ class Oscillator
|
|
104
133
|
(x >= 0.0) ? 1.0 : -1.0
|
105
134
|
end
|
106
135
|
|
136
|
+
# constant used to calculate sawtooth wave
|
107
137
|
K_SAWTOOTH_A = 1.0 / Math::PI
|
108
138
|
|
109
139
|
# generate a sawtooth wave:
|
@@ -0,0 +1,350 @@
|
|
1
|
+
module SPCore
|
2
|
+
# Store signal data and provide some useful methods for working with
|
3
|
+
# (testing and analyzing) the data.
|
4
|
+
#
|
5
|
+
# @author James Tunnell
|
6
|
+
class Signal
|
7
|
+
include Hashmake::HashMakeable
|
8
|
+
|
9
|
+
# Used to process hashed arguments in #initialize.
|
10
|
+
ARG_SPECS = [
|
11
|
+
Hashmake::ArgSpec.new(:key => :data, :reqd => true, :type => Array, :validator => ->(a){ a.any? }),
|
12
|
+
Hashmake::ArgSpec.new(:key => :sample_rate, :reqd => true, :type => Float, :validator => ->(a){ a > 0.0 })
|
13
|
+
]
|
14
|
+
|
15
|
+
attr_reader :data, :sample_rate
|
16
|
+
|
17
|
+
# A new instance of Signal.
|
18
|
+
#
|
19
|
+
# @param [Hash] args Hashed arguments. Required keys are :data and :sample_rate.
|
20
|
+
# See ARG_SPECS for more details.
|
21
|
+
def initialize hashed_args
|
22
|
+
hash_make Signal::ARG_SPECS, hashed_args
|
23
|
+
end
|
24
|
+
|
25
|
+
# Produce a new Signal object with the same data.
|
26
|
+
def clone
|
27
|
+
Signal.new(:data => @data.clone, :sample_rate => @sample_rate)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Produce a new Signal object with a subset of the current signal data.
|
31
|
+
# @param [Range] range Used to pick the data range.
|
32
|
+
def subset range
|
33
|
+
Signal.new(:data => @data[range], :sample_rate => @sample_rate)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Size of the signal data.
|
37
|
+
def size
|
38
|
+
@data.size
|
39
|
+
end
|
40
|
+
|
41
|
+
# Access signal data.
|
42
|
+
def [](arg)
|
43
|
+
@data[arg]
|
44
|
+
end
|
45
|
+
|
46
|
+
def plot_data
|
47
|
+
plotter = Plotter.new(:title => "signal data sequence", :xtitle => "sample numbers", :ytitle => "sample values")
|
48
|
+
plotter.plot_1d "signal data" => @data
|
49
|
+
end
|
50
|
+
|
51
|
+
# Increase the sample rate of signal data by the given factor using
|
52
|
+
# discrete upsampling method.
|
53
|
+
# @param [Fixnum] upsample_factor Increase the sample rate by this factor.
|
54
|
+
# @param [Fixnum] filter_order The filter order for the discrete lowpass filter.
|
55
|
+
def upsample_discrete upsample_factor, filter_order
|
56
|
+
@data = DiscreteResampling.upsample @data, @sample_rate, upsample_factor, filter_order
|
57
|
+
@sample_rate *= upsample_factor
|
58
|
+
return self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Decrease the sample rate of signal data by the given factor using
|
62
|
+
# discrete downsampling method.
|
63
|
+
# @param [Fixnum] downsample_factor Decrease the sample rate by this factor.
|
64
|
+
# @param [Fixnum] filter_order The filter order for the discrete lowpass filter.
|
65
|
+
def downsample_discrete downsample_factor, filter_order
|
66
|
+
@data = DiscreteResampling.downsample @data, @sample_rate, downsample_factor, filter_order
|
67
|
+
@sample_rate /= downsample_factor
|
68
|
+
return self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Change the sample rate of signal data by the given up/down factors, using
|
72
|
+
# discrete upsampling and downsampling methods.
|
73
|
+
# @param [Fixnum] upsample_factor Increase the sample rate by this factor.
|
74
|
+
# @param [Fixnum] downsample_factor Decrease the sample rate by this factor.
|
75
|
+
# @param [Fixnum] filter_order The filter order for the discrete lowpass filter.
|
76
|
+
def resample_discrete upsample_factor, downsample_factor, filter_order
|
77
|
+
@data = DiscreteResampling.resample @data, @sample_rate, upsample_factor, downsample_factor, filter_order
|
78
|
+
@sample_rate *= upsample_factor
|
79
|
+
@sample_rate /= downsample_factor
|
80
|
+
return self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Increase the sample rate of signal data by the given factor using
|
84
|
+
# polynomial interpolation.
|
85
|
+
# @param [Fixnum] upsample_factor Increase the sample rate by this factor.
|
86
|
+
def upsample_polynomial upsample_factor
|
87
|
+
@data = PolynomialResampling.upsample @data, @sample_rate, upsample_factor
|
88
|
+
@sample_rate *= upsample_factor
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Change the sample rate of signal data by the given up/down factors, using
|
93
|
+
# polynomial upsampling and discrete downsampling.
|
94
|
+
# @param [Fixnum] upsample_factor Increase the sample rate by this factor.
|
95
|
+
# @param [Fixnum] downsample_factor Decrease the sample rate by this factor.
|
96
|
+
# @param [Fixnum] filter_order The filter order for the discrete lowpass filter.
|
97
|
+
def resample_hybrid upsample_factor, downsample_factor, filter_order
|
98
|
+
@data = HybridResampling.resample @data, @sample_rate, upsample_factor, downsample_factor, filter_order
|
99
|
+
@sample_rate *= upsample_factor
|
100
|
+
@sample_rate /= downsample_factor
|
101
|
+
return self
|
102
|
+
end
|
103
|
+
|
104
|
+
# Run FFT on signal data to find magnitude of frequency components.
|
105
|
+
# @param convert_to_db If true, magnitudes are converted to dB values.
|
106
|
+
# @return [Hash] contains frequencies mapped to magnitudes.
|
107
|
+
def freq_magnitudes convert_to_db = false
|
108
|
+
fft_output = FFT.forward @data
|
109
|
+
|
110
|
+
fft_output = fft_output[0...(fft_output.size / 2)] # ignore second half
|
111
|
+
fft_output = fft_output.map {|x| x.magnitude } # map complex value to magnitude
|
112
|
+
|
113
|
+
if convert_to_db
|
114
|
+
fft_output = fft_output.map {|x| Gain.linear_to_db x}
|
115
|
+
end
|
116
|
+
|
117
|
+
freq_magnitudes = {}
|
118
|
+
fft_output.each_index do |i|
|
119
|
+
size = fft_output.size * 2 # mul by 2 because the second half of original fft_output was removed
|
120
|
+
freq = (@sample_rate * i) / size
|
121
|
+
freq_magnitudes[freq] = fft_output[i]
|
122
|
+
end
|
123
|
+
|
124
|
+
return freq_magnitudes
|
125
|
+
end
|
126
|
+
|
127
|
+
# Calculate the energy in current signal data.
|
128
|
+
def energy
|
129
|
+
return @data.inject(0.0){|sum,x| sum + (x * x)}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return a
|
133
|
+
def envelope attack_time, release_time
|
134
|
+
raise ArgumentError, "attack_time #{attack_time } is less than or equal to zero" if attack_time <= 0.0
|
135
|
+
raise ArgumentError, "release_time #{release_time} is less than or equal to zero" if release_time <= 0.0
|
136
|
+
|
137
|
+
env_detector = EnvelopeDetector.new(:attack_time => attack_time, :release_time => release_time, :sample_rate => @sample_rate)
|
138
|
+
|
139
|
+
envelope = Array.new(@data.count)
|
140
|
+
|
141
|
+
for i in 0...@data.count do
|
142
|
+
envelope[i] = env_detector.process_sample @data[i]
|
143
|
+
end
|
144
|
+
|
145
|
+
return envelope
|
146
|
+
end
|
147
|
+
|
148
|
+
# Add data in array or other signal to the beginning of current data.
|
149
|
+
def prepend other
|
150
|
+
if other.is_a?(Array)
|
151
|
+
@data = other.concat @data
|
152
|
+
elsif other.is_a?(Signal)
|
153
|
+
@data = other.data.concat @data
|
154
|
+
end
|
155
|
+
return self
|
156
|
+
end
|
157
|
+
|
158
|
+
# Add data in array or other signal to the end of current data.
|
159
|
+
def append other
|
160
|
+
if other.is_a?(Array)
|
161
|
+
@data = @data.concat other
|
162
|
+
elsif other.is_a?(Signal)
|
163
|
+
@data = @data.concat other.data
|
164
|
+
end
|
165
|
+
return self
|
166
|
+
end
|
167
|
+
|
168
|
+
# Add value, values in array, or values in other signal to the current
|
169
|
+
# data values, and update the current data with the results.
|
170
|
+
# @param other Can be Numeric (add same value to all data values), Array, or Signal.
|
171
|
+
def +(other)
|
172
|
+
if other.is_a?(Numeric)
|
173
|
+
@data.each_index do |i|
|
174
|
+
@data[i] += other
|
175
|
+
end
|
176
|
+
elsif other.is_a?(Signal)
|
177
|
+
raise ArgumentError, "other.data.size #{other.size} is not equal to data.size #{@data.size}" if other.data.size != @data.size
|
178
|
+
@data.each_index do |i|
|
179
|
+
@data[i] += other.data[i]
|
180
|
+
end
|
181
|
+
elsif other.is_a?(Array)
|
182
|
+
raise ArgumentError, "other.size #{other.size} is not equal to data.size #{@data.size}" if other.size != @data.size
|
183
|
+
@data.each_index do |i|
|
184
|
+
@data[i] += other[i]
|
185
|
+
end
|
186
|
+
else
|
187
|
+
raise ArgumentError, "other is not a Numeric, Signal, or Array"
|
188
|
+
end
|
189
|
+
return self
|
190
|
+
end
|
191
|
+
|
192
|
+
# Subtract value, values in array, or values in other signal from the current
|
193
|
+
# data values, and update the current data with the results.
|
194
|
+
# @param other Can be Numeric (subtract same value from all data values), Array, or Signal.
|
195
|
+
def -(other)
|
196
|
+
if other.is_a?(Numeric)
|
197
|
+
@data.each_index do |i|
|
198
|
+
@data[i] -= other
|
199
|
+
end
|
200
|
+
elsif other.is_a?(Signal)
|
201
|
+
raise ArgumentError, "other.data.size #{other.size} is not equal to data.size #{@data.size}" if other.data.size != @data.size
|
202
|
+
@data.each_index do |i|
|
203
|
+
@data[i] -= other.data[i]
|
204
|
+
end
|
205
|
+
elsif other.is_a?(Array)
|
206
|
+
raise ArgumentError, "other.size #{other.size} is not equal to data.size #{@data.size}" if other.size != @data.size
|
207
|
+
@data.each_index do |i|
|
208
|
+
@data[i] -= other[i]
|
209
|
+
end
|
210
|
+
else
|
211
|
+
raise ArgumentError, "other is not a Numeric, Signal, or Array"
|
212
|
+
end
|
213
|
+
return self
|
214
|
+
end
|
215
|
+
|
216
|
+
# Multiply value, values in array, or values in other signal with the current
|
217
|
+
# data values, and update the current data with the results.
|
218
|
+
# @param other Can be Numeric (multiply all data values by the same value),
|
219
|
+
# Array, or Signal.
|
220
|
+
def *(other)
|
221
|
+
if other.is_a?(Numeric)
|
222
|
+
@data.each_index do |i|
|
223
|
+
@data[i] *= other
|
224
|
+
end
|
225
|
+
elsif other.is_a?(Signal)
|
226
|
+
raise ArgumentError, "other.data.size #{other.size} is not equal to data.size #{@data.size}" if other.data.size != @data.size
|
227
|
+
@data.each_index do |i|
|
228
|
+
@data[i] *= other.data[i]
|
229
|
+
end
|
230
|
+
elsif other.is_a?(Array)
|
231
|
+
raise ArgumentError, "other.size #{other.size} is not equal to data.size #{@data.size}" if other.size != @data.size
|
232
|
+
@data.each_index do |i|
|
233
|
+
@data[i] *= other[i]
|
234
|
+
end
|
235
|
+
else
|
236
|
+
raise ArgumentError, "other is not a Numeric, Signal, or Array"
|
237
|
+
end
|
238
|
+
return self
|
239
|
+
end
|
240
|
+
|
241
|
+
# Divide value, values in array, or values in other signal into the current
|
242
|
+
# data values, and update the current data with the results.
|
243
|
+
# @param other Can be Numeric (divide same all data values by the same value),
|
244
|
+
# Array, or Signal.
|
245
|
+
def /(other)
|
246
|
+
if other.is_a?(Numeric)
|
247
|
+
@data.each_index do |i|
|
248
|
+
@data[i] /= other
|
249
|
+
end
|
250
|
+
elsif other.is_a?(Signal)
|
251
|
+
raise ArgumentError, "other.data.size #{other.size} is not equal to data.size #{@data.size}" if other.data.size != @data.size
|
252
|
+
@data.each_index do |i|
|
253
|
+
@data[i] /= other.data[i]
|
254
|
+
end
|
255
|
+
elsif other.is_a?(Array)
|
256
|
+
raise ArgumentError, "other.size #{other.size} is not equal to data.size #{@data.size}" if other.size != @data.size
|
257
|
+
@data.each_index do |i|
|
258
|
+
@data[i] /= other[i]
|
259
|
+
end
|
260
|
+
else
|
261
|
+
raise ArgumentError, "other is not a Numeric, Signal, or Array"
|
262
|
+
end
|
263
|
+
return self
|
264
|
+
end
|
265
|
+
|
266
|
+
alias_method :add, :+
|
267
|
+
alias_method :subtract, :-
|
268
|
+
alias_method :multiply, :*
|
269
|
+
alias_method :divide, :/
|
270
|
+
|
271
|
+
# Determine how well the another signal (g) correlates to the current signal (f).
|
272
|
+
# Correlation is determined at every point in f. The signal g must not be
|
273
|
+
# longer than f. Correlation involves moving g along f and performing
|
274
|
+
# convolution. Starting a the beginning of f, it continues until the end
|
275
|
+
# of g hits the end of f. Doesn't actually convolve, though. Instead, it
|
276
|
+
# adds
|
277
|
+
#
|
278
|
+
# @param [Array] other_signal The signal to look for in the current signal.
|
279
|
+
# @param [true/false] normalize Flag to indicate if normalization should be
|
280
|
+
# performed on input signals (performed on a copy
|
281
|
+
# of the original data).
|
282
|
+
# @raise [ArgumentError] if other_signal is not a Signal or Array.
|
283
|
+
# @raise [ArgumentError] if other_signal is longer than the current signal data.
|
284
|
+
def cross_correlation other_signal, normalize = true
|
285
|
+
if other_signal.is_a? Signal
|
286
|
+
other_data = other_signal.data
|
287
|
+
elsif other_signal.is_a? Array
|
288
|
+
other_data = other_signal
|
289
|
+
else
|
290
|
+
raise ArgumentError, "other_signal is not a Signal or Array"
|
291
|
+
end
|
292
|
+
|
293
|
+
f = @data
|
294
|
+
g = other_data
|
295
|
+
|
296
|
+
raise ArgumentError, "g.count #{g.count} is greater than f.count #{f.count}" if g.count > f.count
|
297
|
+
|
298
|
+
g_size = g.count
|
299
|
+
f_size = f.count
|
300
|
+
f_g_diff = f_size - g_size
|
301
|
+
|
302
|
+
cross_correlation = []
|
303
|
+
|
304
|
+
if normalize
|
305
|
+
max = (f.max_by {|x| x.abs }).abs.to_f
|
306
|
+
|
307
|
+
f = f.clone
|
308
|
+
g = g.clone
|
309
|
+
f.each_index {|i| f[i] = f[i] / max }
|
310
|
+
g.each_index {|i| g[i] = g[i] / max }
|
311
|
+
end
|
312
|
+
|
313
|
+
#puts "f: #{f.inspect}"
|
314
|
+
#puts "g: #{g.inspect}"
|
315
|
+
|
316
|
+
for n in 0..f_g_diff do
|
317
|
+
f_window = (n...(n + g_size)).entries
|
318
|
+
g_window = (0...g_size).entries
|
319
|
+
|
320
|
+
sample = 0.0
|
321
|
+
for i in 0...f_window.count do
|
322
|
+
i_f = f_window[i]
|
323
|
+
i_g = g_window[i]
|
324
|
+
|
325
|
+
#if use_relative_error
|
326
|
+
target = g[i_g].to_f
|
327
|
+
actual = f[i_f]
|
328
|
+
|
329
|
+
#if target == 0.0 && actual != 0.0 && normalize
|
330
|
+
# puts "target is #{target} and actual is #{actual}"
|
331
|
+
# error = 1.0
|
332
|
+
#else
|
333
|
+
error = (target - actual).abs# / target
|
334
|
+
#end
|
335
|
+
|
336
|
+
sample += error
|
337
|
+
|
338
|
+
#else
|
339
|
+
# sample += (f[i_f] * g[i_g])
|
340
|
+
#end
|
341
|
+
end
|
342
|
+
|
343
|
+
cross_correlation << (sample)# / g_size.to_f)
|
344
|
+
end
|
345
|
+
|
346
|
+
return cross_correlation
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|