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.
Files changed (68) hide show
  1. data/ChangeLog.rdoc +14 -1
  2. data/README.rdoc +10 -2
  3. data/lib/spcore.rb +45 -14
  4. data/lib/spcore/{lib → core}/circular_buffer.rb +28 -1
  5. data/lib/spcore/core/constants.rb +7 -1
  6. data/lib/spcore/{lib → core}/delay_line.rb +15 -5
  7. data/lib/spcore/{lib → core}/envelope_detector.rb +13 -1
  8. data/lib/spcore/{lib → core}/oscillator.rb +33 -3
  9. data/lib/spcore/core/signal.rb +350 -0
  10. data/lib/spcore/filters/fir/dual_sinc_filter.rb +84 -0
  11. data/lib/spcore/filters/fir/fir.rb +87 -0
  12. data/lib/spcore/filters/fir/sinc_filter.rb +68 -0
  13. data/lib/spcore/{lib → filters/iir}/biquad_filter.rb +7 -0
  14. data/lib/spcore/{lib → filters/iir}/cookbook_allpass_filter.rb +2 -0
  15. data/lib/spcore/{lib → filters/iir}/cookbook_bandpass_filter.rb +2 -0
  16. data/lib/spcore/{lib → filters/iir}/cookbook_highpass_filter.rb +2 -0
  17. data/lib/spcore/{lib → filters/iir}/cookbook_lowpass_filter.rb +2 -0
  18. data/lib/spcore/{lib → filters/iir}/cookbook_notch_filter.rb +2 -0
  19. data/lib/spcore/interpolation/interpolation.rb +64 -0
  20. data/lib/spcore/resampling/discrete_resampling.rb +78 -0
  21. data/lib/spcore/resampling/hybrid_resampling.rb +30 -0
  22. data/lib/spcore/resampling/polynomial_resampling.rb +56 -0
  23. data/lib/spcore/transforms/dft.rb +47 -0
  24. data/lib/spcore/transforms/fft.rb +125 -0
  25. data/lib/spcore/{lib → util}/gain.rb +3 -0
  26. data/lib/spcore/{core → util}/limiters.rb +10 -1
  27. data/lib/spcore/util/plotter.rb +91 -0
  28. data/lib/spcore/{lib → util}/saturation.rb +2 -9
  29. data/lib/spcore/util/scale.rb +41 -0
  30. data/lib/spcore/util/signal_generator.rb +48 -0
  31. data/lib/spcore/version.rb +2 -1
  32. data/lib/spcore/windows/bartlett_hann_window.rb +15 -0
  33. data/lib/spcore/windows/bartlett_window.rb +13 -0
  34. data/lib/spcore/windows/blackman_harris_window.rb +14 -0
  35. data/lib/spcore/windows/blackman_nuttall_window.rb +14 -0
  36. data/lib/spcore/windows/blackman_window.rb +18 -0
  37. data/lib/spcore/windows/cosine_window.rb +13 -0
  38. data/lib/spcore/windows/flat_top_window.rb +27 -0
  39. data/lib/spcore/windows/gaussian_window.rb +15 -0
  40. data/lib/spcore/windows/hamming_window.rb +16 -0
  41. data/lib/spcore/windows/hann_window.rb +13 -0
  42. data/lib/spcore/windows/lanczos_window.rb +15 -0
  43. data/lib/spcore/windows/nuttall_window.rb +14 -0
  44. data/lib/spcore/windows/rectangular_window.rb +10 -0
  45. data/lib/spcore/windows/triangular_window.rb +14 -0
  46. data/spcore.gemspec +11 -3
  47. data/spec/{lib → core}/circular_buffer_spec.rb +0 -0
  48. data/spec/{lib → core}/delay_line_spec.rb +1 -1
  49. data/spec/{lib → core}/envelope_detector_spec.rb +3 -3
  50. data/spec/{lib → core}/oscillator_spec.rb +0 -0
  51. data/spec/filters/fir/dual_sinc_filter_spec.rb +64 -0
  52. data/spec/filters/fir/sinc_filter_spec.rb +57 -0
  53. data/spec/filters/iir/cookbook_filter_spec.rb +30 -0
  54. data/spec/interpolation/interpolation_spec.rb +49 -0
  55. data/spec/resampling/discrete_resampling_spec.rb +81 -0
  56. data/spec/resampling/hybrid_resampling_spec.rb +31 -0
  57. data/spec/resampling/polynomial_resampling_spec.rb +30 -0
  58. data/spec/transforms/dft_spec.rb +71 -0
  59. data/spec/transforms/fft_spec.rb +84 -0
  60. data/spec/{lib → util}/gain_spec.rb +2 -2
  61. data/spec/{core → util}/limiters_spec.rb +0 -0
  62. data/spec/{lib → util}/saturate_spec.rb +0 -0
  63. data/spec/util/signal_generator_spec.rb +54 -0
  64. data/spec/windows/window_spec.rb +33 -0
  65. metadata +90 -42
  66. data/lib/spcore/lib/interpolation.rb +0 -15
  67. data/spec/lib/cookbook_filter_spec.rb +0 -44
  68. data/spec/lib/interpolation_spec.rb +0 -21
@@ -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.
@@ -6,16 +6,24 @@
6
6
 
7
7
  == Description
8
8
 
9
- Contains core signal processing functions.
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
 
@@ -1,19 +1,50 @@
1
1
  require 'hashmake'
2
2
  require 'spcore/version'
3
3
 
4
- require 'spcore/core/limiters'
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/lib/interpolation'
8
- require 'spcore/lib/circular_buffer'
9
- require 'spcore/lib/delay_line'
10
- require 'spcore/lib/gain'
11
- require 'spcore/lib/oscillator'
12
- require 'spcore/lib/biquad_filter'
13
- require 'spcore/lib/cookbook_allpass_filter'
14
- require 'spcore/lib/cookbook_bandpass_filter'
15
- require 'spcore/lib/cookbook_highpass_filter'
16
- require 'spcore/lib/cookbook_lowpass_filter'
17
- require 'spcore/lib/cookbook_notch_filter'
18
- require 'spcore/lib/envelope_detector'
19
- require 'spcore/lib/saturation'
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,4 +1,10 @@
1
1
  module SPCore
2
+ # Two times PI
2
3
  TWO_PI = Math::PI * 2.0
3
- NEG_PI = -Math::PI
4
+ # Four times PI
5
+ FOUR_PI = Math::PI * 4.0
6
+ # Six times PI
7
+ SIX_PI = Math::PI * 6.0
8
+ # Eight times PI
9
+ EIGHT_PI = Math::PI * 8.0
4
10
  end
@@ -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 < NEG_PI)
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