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
@@ -0,0 +1,30 @@
1
+ module SPCore
2
+ # Provide resampling method using polynomial upsampling and discrete filtering
3
+ # for downsampling.
4
+ class HybridResampling
5
+ def self.resample input, sample_rate, upsample_factor, downsample_factor, filter_order
6
+ raise ArgumentError, "input.size is less than four" unless input.size >= 4
7
+ raise ArgumentError, "upsample_factor is not greater than 1" unless upsample_factor > 1
8
+ raise ArgumentError, "downsample_factor is not a Fixnum" unless downsample_factor.is_a?(Fixnum)
9
+ raise ArgumentError, "downsample_factor is not greater than 1" unless downsample_factor > 1
10
+ raise ArgumentError, "sample_rate is not greater than 0" unless sample_rate > 0
11
+
12
+ upsampled = PolynomialResampling.upsample input, sample_rate, upsample_factor
13
+
14
+ needed_samples = upsampled.size % downsample_factor
15
+ if needed_samples == 0
16
+ upsampled += Array.new(needed_samples, 0.0)
17
+ end
18
+
19
+ target_rate = sample_rate * upsample_factor / downsample_factor
20
+ cutoff = (target_rate < sample_rate) ? (target_rate / 2.0) : (sample_rate / 2.0)
21
+
22
+ filter = SincFilter.new(
23
+ :sample_rate => (sample_rate * upsample_factor), :order => filter_order,
24
+ :cutoff_freq => cutoff, :window_class => NuttallWindow
25
+ )
26
+ filtered = filter.lowpass(upsampled)
27
+ return Array.new(filtered.size / downsample_factor){ |i| filtered[i * downsample_factor] }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module SPCore
2
+ # Provide resampling methods (upsampling and downsampling) using
3
+ # polynomial interpolation.
4
+ class PolynomialResampling
5
+
6
+ def self.upsample input, sample_rate, upsample_factor
7
+ raise ArgumentError, "input.size is less than four" unless input.size >= 4
8
+ raise ArgumentError, "upsample_factor is not greater than 1" unless upsample_factor > 1
9
+ raise ArgumentError, "sample_rate is not greater than 0" unless sample_rate > 0
10
+
11
+ output = Array.new((upsample_factor * input.size).to_i)
12
+
13
+ input_size_f = input.size.to_f
14
+ input_size_minus_1 = input.size - 1
15
+ input_size_minus_2 = input.size - 2
16
+ output_size_f = output.size.to_f
17
+ output.each_index do |i|
18
+
19
+ i_f = i.to_f
20
+ index_into_input = (i_f / output_size_f) * input_size_f
21
+ index_into_input_i = index_into_input.to_i
22
+
23
+ if(index_into_input <= 1.0) # before second sample
24
+ point1 = input[0]
25
+ point2 = input[0]
26
+ point3 = input[1]
27
+ point4 = input[2]
28
+ x = index_into_input
29
+ output[i] = Interpolation.cubic_hermite(point1, point2, point3, point4, x)
30
+ elsif(index_into_input >= input_size_minus_1) # past last sample
31
+ point1 = input[input_size_minus_1 - 1]
32
+ point2 = input[input_size_minus_1]
33
+ point3 = input[input_size_minus_1]
34
+ point4 = input[input_size_minus_1]
35
+ x = index_into_input - index_into_input.floor
36
+ output[i] = Interpolation.cubic_hermite(point1, point2, point3, point4, x)
37
+ elsif(index_into_input >= input_size_minus_2) # past second-to-last sample
38
+ point1 = input[index_into_input_i - 1]
39
+ point2 = input[index_into_input_i]
40
+ point3 = input[input_size_minus_1]
41
+ point4 = input[input_size_minus_1]
42
+ x = index_into_input - index_into_input.floor
43
+ output[i] = Interpolation.cubic_hermite(point1, point2, point3, point4, x)
44
+ else # general case
45
+ point1 = input[index_into_input_i - 1]
46
+ point2 = input[index_into_input_i]
47
+ point3 = input[index_into_input_i + 1]
48
+ point4 = input[index_into_input_i + 2]
49
+ x = index_into_input - index_into_input.floor
50
+ output[i] = Interpolation.cubic_hermite(point1, point2, point3, point4, x)
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ module SPCore
2
+ # Perform DFT transforms, forward and inverse.
3
+ # @author James Tunnell
4
+ class DFT
5
+ # @param [Array] input array of real values, representing the time domain
6
+ # signal to be passed into the forward DFT.
7
+ def self.forward input
8
+ input_size = input.size
9
+ raise ArgumentError, "input.size is not even" unless (input_size % 2 == 0)
10
+
11
+ output_size = input_size
12
+ output = Array.new(output_size)
13
+
14
+ output.each_index do |k|
15
+ sum = Complex(0.0)
16
+ input.each_index do |n|
17
+ a = TWO_PI * n * k / input_size
18
+ sum += Complex(input[n] * Math::cos(a), -input[n] * Math::sin(a))
19
+ end
20
+ output[k] = sum
21
+ end
22
+
23
+ return output
24
+ end
25
+
26
+ # @param [Array] input array of complex values, representing the frequency domain
27
+ # signal obtained from the forward DFT.
28
+ def self.inverse input
29
+ input_size = input.size
30
+ raise ArgumentError, "input.size is not even" unless (input_size % 2 == 0)
31
+
32
+ output = Array.new(input_size)
33
+ output_size = output.size
34
+
35
+ output.each_index do |k|
36
+ sum = Complex(0.0)
37
+ input.each_index do |n|
38
+ a = TWO_PI * n * k / input_size
39
+ sum += Complex(input[n] * Math::cos(a), input[n] * Math::sin(a))
40
+ end
41
+ output[k] = sum / output_size
42
+ end
43
+
44
+ return output
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,125 @@
1
+ module SPCore
2
+ # Perform FFT transforms, forward and inverse.
3
+ # @author James Tunnell
4
+ class FFT
5
+
6
+ # Forward Radix-2 FFT transform using decimation-in-time. Operates on an array
7
+ # of real values, representing a time domain signal.
8
+ # Ported from unlicensed MATLAB code which was posted to the MathWorks file
9
+ # exchange by Dinesh Dileep Gaurav.
10
+ # See http://www.mathworks.com/matlabcentral/fileexchange/17778.
11
+ # @param [Array] input An array of numeric values. If size is not an exact
12
+ # radix-2 number, then zeros will be appended until it is,
13
+ # unless force_radix_2_size is set false.
14
+ # @param force_radix_2_size If true, any input that does not have an exact
15
+ # power-of-two size will be appended with zeros
16
+ # until it does. Set true by default.
17
+ # @raise [ArgumentError] if input size is not an exact radix-2 number and
18
+ # force_radix_2_size is false.
19
+ def self.forward input, force_radix_2_size = true
20
+ size = input.size
21
+ power_of_two = Math::log2(size)
22
+ if power_of_two.floor != power_of_two # input size is not an even power of two
23
+ if force_radix_2_size
24
+ new_size = 2**(power_of_two.to_i() + 1)
25
+ input += Array.new(new_size - size, 0.0)
26
+
27
+ size = input.size
28
+ power_of_two = Math::log2(size)
29
+ else
30
+ raise ArgumentError, "input.size #{size} is not a radix-2 number"
31
+ end
32
+ end
33
+ power_of_two = power_of_two.to_i
34
+ x = bit_reverse_order input, power_of_two
35
+
36
+ phase = Array.new(size/2){|n|
37
+ Complex(Math::cos(TWO_PI*n/size), -Math::sin(TWO_PI*n/size))
38
+ }
39
+ for a in 1..power_of_two
40
+ l = 2**a
41
+ phase_level = []
42
+
43
+ 0.step(size/2, size/l) do |i|
44
+ phase_level << phase[i]
45
+ end
46
+
47
+ ks = []
48
+ 0.step(size-l, l) do |k|
49
+ ks << k
50
+ end
51
+
52
+ ks.each do |k|
53
+ for n in 0...l/2
54
+ idx1 = n+k
55
+ idx2 = n+k + (l/2)
56
+
57
+ first = x[idx1]
58
+ second = x[idx2] * phase_level[n]
59
+ x[idx1] = first + second
60
+ x[idx2] = first - second
61
+ end
62
+ end
63
+ end
64
+ return x
65
+ end
66
+
67
+ # Inverse Radix-2 FFT transform. Operates on an array of complex values, as
68
+ # one would obtain from the forward FFT transform.
69
+ # Ported from unlicensed MATLAB code which was posted to the MathWorks file
70
+ # exchange by Dinesh Dileep Gaurav.
71
+ # See http://www.mathworks.com/matlabcentral/fileexchange/17778.
72
+ # @param [Array] input An array of complex values. Must have a radix-2 size
73
+ # (2, 4, 8, 16, 32, ...).
74
+ # @raise [ArgumentError] if input size is not an exact power-of-two.
75
+ def self.inverse input
76
+ size = input.size
77
+ power_of_two = Math::log2(size)
78
+ raise ArgumentError, "input.size #{size} is not power of 2" if power_of_two.floor != power_of_two
79
+ power_of_two = power_of_two.to_i
80
+ x = bit_reverse_order input, power_of_two
81
+
82
+ phase = Array.new(size/2){|n|
83
+ Complex(Math::cos(TWO_PI*n/size), -Math::sin(TWO_PI*n/size))
84
+ }
85
+ for a in 1..power_of_two
86
+ l = 2**a
87
+ phase_level = []
88
+
89
+ 0.step(size/2, size/l) do |i|
90
+ phase_level << phase[i]
91
+ end
92
+
93
+ ks = []
94
+ 0.step(size-l, l) do |k|
95
+ ks << k
96
+ end
97
+
98
+ ks.each do |k|
99
+ for n in 0...l/2
100
+ idx1 = n+k
101
+ idx2 = n+k + (l/2)
102
+
103
+ first = x[idx1]
104
+ second = x[idx2] * phase_level[n]
105
+ x[idx1] = (first + second)
106
+ x[idx2] = (first - second)
107
+ end
108
+ end
109
+ end
110
+
111
+ return x.map {|val| val / size }
112
+ end
113
+
114
+ private
115
+
116
+ def self.get_bit_reversed_addr i, nbits
117
+ i.to_s(2).rjust(nbits, '0').reverse!.to_i(2)
118
+ end
119
+
120
+ def self.bit_reverse_order input, m
121
+ Array.new(input.size){|i| input[get_bit_reversed_addr(i, m)] }
122
+ end
123
+
124
+ end
125
+ end
@@ -1,4 +1,5 @@
1
1
  module SPCore
2
+ # Provide utility functions to convert between a linear and decibel (logarithm) unit.
2
3
  class Gain
3
4
 
4
5
  #MAX_DB = 72
@@ -167,12 +168,14 @@ class Gain
167
168
 
168
169
  MAX_DB_ABS = 6000.0
169
170
 
171
+ # Convert a decibel value to a linear value.
170
172
  def self.db_to_linear db
171
173
  db_abs = db.abs
172
174
  raise ArgumentError, "|db| is #{db_abs}, which is greater than the allowed #{MAX_DB_ABS}" if db_abs > MAX_DB_ABS
173
175
  return 10.0**(db / 20.0)
174
176
  end
175
177
 
178
+ # Convert a linear value to a decibel value.
176
179
  def self.linear_to_db linear
177
180
  raise ArgumentError, "linear value #{linear} is less than or equal to 0.0" if linear <= 0.0
178
181
  return 20.0 * Math::log10(linear)
@@ -1,11 +1,15 @@
1
1
  module SPCore
2
+ # Provides methods to make limiting Proc objects, bound to
3
+ # the given limits.
2
4
  class Limiters
5
+ # make a limiter that actually doesn't limit at all.
3
6
  def self.make_no_limiter
4
7
  return lambda do |input|
5
8
  return input
6
9
  end
7
10
  end
8
-
11
+
12
+ # make a limiter that keeps values within the given range.
9
13
  def self.make_range_limiter range
10
14
  return lambda do |input|
11
15
  if input < range.first
@@ -18,6 +22,7 @@ class Limiters
18
22
  end
19
23
  end
20
24
 
25
+ # make a limiter that keeps values at or below the given upper limit.
21
26
  def self.make_upper_limiter limit
22
27
  return lambda do |input|
23
28
  if input > limit
@@ -28,6 +33,7 @@ class Limiters
28
33
  end
29
34
  end
30
35
 
36
+ # make a limiter that keeps values at or above the given lower limit.
31
37
  def self.make_lower_limiter limit
32
38
  return lambda do |input|
33
39
  if input < limit
@@ -38,6 +44,9 @@ class Limiters
38
44
  end
39
45
  end
40
46
 
47
+ # make a limiter that limits values to a set of good values. Given also the
48
+ # current value, it either returns the input value if it's included in
49
+ # the set of good values, or it returns the current value.
41
50
  def self.make_enum_limiter good_values
42
51
  return lambda do |input, current|
43
52
  if good_values.include?(input)
@@ -0,0 +1,91 @@
1
+ require 'gnuplot'
2
+
3
+ module SPCore
4
+
5
+ # Helps make plotting data even easier. Uses gnuplot.
6
+ class Plotter
7
+ include Hashmake::HashMakeable
8
+
9
+ # Used to process hashed args passed to #initialize.
10
+ ARG_SPECS = [
11
+ Hashmake::ArgSpec.new(:key => :title, :type => String, :reqd => false, :default => ""),
12
+ Hashmake::ArgSpec.new(:key => :xlabel, :type => String, :reqd => false, :default => "x"),
13
+ Hashmake::ArgSpec.new(:key => :ylabel, :type => String, :reqd => false, :default => "y"),
14
+ Hashmake::ArgSpec.new(:key => :linestyle, :type => String, :reqd => false, :default => "lines"),
15
+ Hashmake::ArgSpec.new(:key => :linewidth, :type => Fixnum, :reqd => false, :default => 1, :validator => ->(a){ a >= 1 }),
16
+ Hashmake::ArgSpec.new(:key => :logscale, :type => String, :reqd => false, :default => ""),
17
+ ]
18
+
19
+ # A new instance of Plotter.
20
+ # @param [Hash] hashed_args A hash containing initialization parameters.
21
+ # All params are optional. See ARG_SPECS for
22
+ # parameter details.
23
+ def initialize hashed_args = {}
24
+ hash_make Plotter::ARG_SPECS, hashed_args
25
+ end
26
+
27
+ # Plot XY datapoints.
28
+ # @param [Hash] titled_hashes A hash that maps dataset titles to data. The data itself
29
+ # is a hash also, that maps x values to y values. For example,
30
+ # plot_2d could be called passing it the hash { "somedata" => {0.0 => 4.0, 1.0 => 2.0}}
31
+ def plot_2d titled_hashes
32
+ datasets = []
33
+ titled_hashes.each do |title, hash|
34
+ dataset = Gnuplot::DataSet.new( [hash.keys, hash.values] ){ |ds|
35
+ ds.with = @linestyle
36
+ ds.title = title
37
+ ds.linewidth = @linewidth
38
+ }
39
+ datasets << dataset
40
+ end
41
+
42
+ plot_datasets datasets
43
+ end
44
+
45
+ # Plot a sequence of values.
46
+ # @param [Hash] titled_sequences A hash that maps sequence titles to data. The data itself
47
+ # is an array of values. In the plot, values will be mapped to
48
+ # their index in the sequence. For example, plot_1d could be
49
+ # called passing it the hash { "somedataseq" => [0,2,3,6,3,-1]}
50
+ # @param plot_against_fraction If true, instead of plotting samples against sample number, plot
51
+ # them against the fraction (sample_number / total_samples).
52
+ def plot_1d titled_sequences, plot_against_fraction = false
53
+ datasets = []
54
+ titled_sequences.each do |title, sequence|
55
+ indices = Array.new(sequence.size)
56
+ sequence.each_index do |i|
57
+ indices[i] = i
58
+ if plot_against_fraction
59
+ indices[i] /= sequence.size.to_f
60
+ end
61
+ end
62
+
63
+ dataset = Gnuplot::DataSet.new( [indices, sequence] ){ |ds|
64
+ ds.with = @linestyle
65
+ ds.title = title
66
+ ds.linewidth = @linewidth
67
+ }
68
+ datasets << dataset
69
+ end
70
+
71
+ plot_datasets datasets
72
+ end
73
+
74
+ # Plot Gnuplot::DataSet objects.
75
+ # @param [Array] datasets An array of Gnuplot::DataSet objects.
76
+ def plot_datasets datasets
77
+ Gnuplot.open do |gp|
78
+ Gnuplot::Plot.new(gp) do |plot|
79
+ plot.title @title
80
+ plot.xlabel @xlabel
81
+ plot.ylabel @ylabel
82
+ plot.data = datasets
83
+
84
+ if @logscale_x
85
+ plot.logscale "x"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,4 +1,5 @@
1
1
  module SPCore
2
+ # Provide simple saturation methods, that limit input above the given threshold value.
2
3
  class Saturation
3
4
  # Sigmoid-based saturation when input is above threshold.
4
5
  # From musicdsp.org, posted by Bram.
@@ -18,6 +19,7 @@ class Saturation
18
19
  end
19
20
  end
20
21
 
22
+ # A Gompertz-sigmoid-based saturation when input is above threshold.
21
23
  def self.gompertz input, threshold
22
24
  a = threshold
23
25
  b = -4
@@ -32,14 +34,5 @@ class Saturation
32
34
  end
33
35
  end
34
36
 
35
- private
36
-
37
- #def self.mock_sigmoid x
38
- # if(x.abs < 1.0)
39
- # return x * (1.5 - 0.5 * x * x)
40
- # else
41
- # return x > 0.0 ? 1.0 : -1.0
42
- # end
43
- #end
44
37
  end
45
38
  end
@@ -0,0 +1,41 @@
1
+ module SPCore
2
+ # Provide methods for generating sequences that scale linearly or exponentially.
3
+ class Scale
4
+ # Produce a sequence of values that progresses in a linear fashion.
5
+ # @param [Range] range The start and end values the set should include.
6
+ # @param [Fixnum] n_points The number of points to create for the sequence, including the start and end points.
7
+ # @raise [ArgumentError] if n_points is < 2.
8
+ def self.linear range, n_points
9
+ raise ArgumentError, "n_points is < 2" if n_points < 2
10
+ incr = (range.last - range.first) / (n_points - 1)
11
+ points = Array.new(n_points)
12
+ value = range.first
13
+
14
+ points.each_index do |i|
15
+ points[i] = value
16
+ value += incr
17
+ end
18
+
19
+ return points
20
+ end
21
+
22
+ # Produce a sequence of values that progresses in an exponential fashion.
23
+
24
+ # @param [Range] range The start and end values the set should include.
25
+ # @param [Fixnum] n_points The number of points to create for the sequence, including the start and end points.
26
+ # @raise [ArgumentError] if n_points is < 2.
27
+ def self.exponential range, n_points
28
+ raise ArgumentError, "n_points is < 2" if n_points < 2
29
+ multiplier = (range.last / range.first)**(1.0/(n_points-1))
30
+ points = Array.new(n_points)
31
+ value = range.first
32
+
33
+ points.each_index do |i|
34
+ points[i] = value
35
+ value *= multiplier
36
+ end
37
+
38
+ return points
39
+ end
40
+ end
41
+ end