spcore 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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