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