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
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+ require 'gnuplot'
3
+ require 'pry'
4
+
5
+ describe 'cookbook filter' do
6
+ it 'should produce a nice frequency response graph' do
7
+ #sample_rate = 4000.0
8
+ #crit_freq = 400.0
9
+ #min_test_freq = 10.0
10
+ #max_test_freq = (sample_rate / 2.0) - 1.0
11
+ #bw = 0.3
12
+ #
13
+ #[SPCore::CookbookLowpassFilter, SPCore::CookbookHighpassFilter, SPCore::CookbookBandpassFilter].each do |filter_class|
14
+ # filter = filter_class.new sample_rate
15
+ # filter.set_critical_freq_and_bw crit_freq, bw
16
+ #
17
+ # freq_response = {}
18
+ # Scale.exponential(min_test_freq..max_test_freq, 200).each do |freq|
19
+ # mag = filter.get_freq_magnitude_response freq
20
+ # freq_response[freq] = SPCore::Gain.linear_to_db(mag)
21
+ # end
22
+ #
23
+ # plotter = Plotter.new(
24
+ # :title => "Frequency Magnitude Response for #{filter_class} with critical freq of #{crit_freq}",
25
+ # :logscale => "x"
26
+ # )
27
+ # plotter.plot_2d "magnitude (dB)" => freq_response
28
+ #end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
1
+ require 'pry'
2
+ require 'benchmark'
3
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
+
5
+ describe SPCore::Interpolation do
6
+ context '.linear' do
7
+ it 'should interpolate floating-point values' do
8
+ result = SPCore::Interpolation.linear 2.0, 4.0, 0.5
9
+ result.should eq(3.0)
10
+ end
11
+
12
+ it 'should interpolate integer values' do
13
+ result = SPCore::Interpolation.linear 20, 40, 0.5
14
+ result.should eq(30)
15
+ end
16
+ end
17
+
18
+ context '.cubic_hermite' do
19
+ it 'should look like...' do
20
+ y0, y1, y2, y3 = 1.75, 1, 0.75, -1.5
21
+
22
+ x_ary = []
23
+ y_ary = []
24
+
25
+ (0.0..1.0).step(0.05) do |x|
26
+ y = SPCore::Interpolation.cubic_hermite y0, y1, y2, y3, x
27
+ x_ary << x
28
+ y_ary << y
29
+ end
30
+
31
+ #Gnuplot.open do |gp|
32
+ # Gnuplot::Plot.new(gp) do |plot|
33
+ # plot.title "interpolated values"
34
+ # plot.xlabel "x"
35
+ # plot.ylabel "f(x)"
36
+ #
37
+ # plot.data = [
38
+ # Gnuplot::DataSet.new( [ x_ary, y_ary ] ) { |ds|
39
+ # ds.with = "linespoints"
40
+ # ds.title = "cubic hermite"
41
+ # ds.linewidth = 1
42
+ # }
43
+ # ]
44
+ # end
45
+ #end
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,81 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'gnuplot'
3
+
4
+ describe SPCore::DiscreteResampling do
5
+
6
+ context '.upsample' do
7
+ it 'should produce output signal with the same max frequency (put through forward FFT)' do
8
+ sample_rate = 400.0
9
+ test_freq = 10.0
10
+ size = 64
11
+ upsample_factor = 4
12
+ order = (sample_rate / test_freq).to_i
13
+
14
+ signal1 = SignalGenerator.new(:sample_rate => sample_rate, :size => size).make_signal [test_freq]
15
+ signal1 *= BlackmanWindow.new(size).data
16
+ signal2 = signal1.clone.upsample_discrete upsample_factor, order
17
+
18
+ #plotter = Plotter.new(:title => "Discrete upsampling by #{upsample_factor}")
19
+ #plotter.plot_1d("original signal" => signal1.data, "upsampled signal" => signal2.data)
20
+
21
+ signal2.size.should eq(signal1.size * upsample_factor)
22
+
23
+ max_freq1 = signal1.freq_magnitudes.max_by{|freq, mag| mag }[0]
24
+ max_freq2 = signal2.freq_magnitudes.max_by{|freq, mag| mag }[0]
25
+
26
+ percent_error = (max_freq1 - max_freq2).abs / max_freq1
27
+ percent_error.should be_within(0.1).of(0.0)
28
+ end
29
+ end
30
+
31
+ context '.downsample' do
32
+ it 'should produce output signal with the same max frequency (put through forward FFT)' do
33
+ sample_rate = 400.0
34
+ test_freq = 10.0
35
+ size = 128
36
+ downsample_factor = 4
37
+ order = (sample_rate / test_freq).to_i
38
+
39
+ signal1 = SignalGenerator.new(:sample_rate => sample_rate, :size => size).make_signal [test_freq]
40
+ signal1 *= BlackmanWindow.new(size).data
41
+ signal2 = signal1.clone.downsample_discrete downsample_factor, order
42
+
43
+ #plotter = Plotter.new(:title => "Discrete downsampling by #{downsample_factor}")
44
+ #plotter.plot_1d("original signal" => signal1.data, "downsampled signal" => signal2.data)
45
+
46
+ signal2.size.should eq(signal1.size / downsample_factor)
47
+
48
+ max_freq1 = signal1.freq_magnitudes.max_by{|freq, mag| mag }[0]
49
+ max_freq2 = signal2.freq_magnitudes.max_by{|freq, mag| mag }[0]
50
+
51
+ percent_error = (max_freq1 - max_freq2).abs / max_freq1
52
+ percent_error.should be_within(0.1).of(0.0)
53
+ end
54
+ end
55
+
56
+ context '.resample' do
57
+ it 'should produce output signal with the same max frequency (put through forward FFT)' do
58
+ sample_rate = 400.0
59
+ test_freq = 10.0
60
+ size = 64
61
+ upsample_factor = 4
62
+ downsample_factor = 4
63
+ order = (sample_rate / test_freq).to_i
64
+
65
+ signal1 = SignalGenerator.new(:sample_rate => sample_rate, :size => size).make_signal [test_freq]
66
+ signal1 *= BlackmanWindow.new(size).data
67
+ signal2 = signal1.clone.resample_discrete upsample_factor, downsample_factor, order
68
+
69
+ #plotter = Plotter.new(:title => "Discrete resampling, up by #{upsampling_factor}, down by #{downsample_factor}")
70
+ #plotter.plot_1d("original signal" => signal1.data, "resampled signal" => signal2.data)
71
+
72
+ signal2.size.should eq(signal1.size * upsample_factor / downsample_factor)
73
+
74
+ max_freq1 = signal1.freq_magnitudes.max_by{|freq, mag| mag }[0]
75
+ max_freq2 = signal2.freq_magnitudes.max_by{|freq, mag| mag }[0]
76
+
77
+ percent_error = (max_freq1 - max_freq2).abs / max_freq1
78
+ percent_error.should be_within(0.1).of(0.0)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'gnuplot'
3
+
4
+ describe SPCore::HybridResampling do
5
+
6
+ context '.resample' do
7
+ it 'should produce output signal with the same max frequency (put through forward FFT)' do
8
+ sample_rate = 400.0
9
+ test_freq = 10.0
10
+ size = 64
11
+ upsample_factor = 4
12
+ downsample_factor = 4
13
+ order = (sample_rate / test_freq).to_i
14
+
15
+ signal1 = SignalGenerator.new(:sample_rate => sample_rate, :size => size).make_signal [test_freq]
16
+ signal1 *= BlackmanWindow.new(size).data
17
+ signal2 = signal1.clone.resample_hybrid upsample_factor, downsample_factor, order
18
+
19
+ #plotter = Plotter.new(:title => "Discrete resampling, up by #{upsampling_factor}, down by #{downsample_factor}")
20
+ #plotter.plot_1d("original signal" => signal1.data, "resampled signal" => signal2.data)
21
+
22
+ signal2.size.should eq(signal1.size * upsample_factor / downsample_factor)
23
+
24
+ max_freq1 = signal1.freq_magnitudes.max_by{|freq, mag| mag }[0]
25
+ max_freq2 = signal2.freq_magnitudes.max_by{|freq, mag| mag }[0]
26
+
27
+ percent_error = (max_freq1 - max_freq2).abs / max_freq1
28
+ percent_error.should be_within(0.1).of(0.0)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'gnuplot'
3
+
4
+ describe SPCore::PolynomialResampling do
5
+
6
+ context '.upsample' do
7
+ it 'should produce output signal with the same max frequency (put through forward DFT)' do
8
+ sample_rate = 400.0
9
+ test_freq = 10.0
10
+ size = (sample_rate * 5.0 / test_freq).to_i
11
+ upsample_factor = 2.5
12
+
13
+ generator = SignalGenerator.new :sample_rate => sample_rate, :size => size
14
+ signal1 = generator.make_signal [test_freq]
15
+ signal2 = signal1.clone.upsample_polynomial upsample_factor
16
+
17
+ #plotter = Plotter.new(:title => "Polynomial upsampling by #{upsample_factor}")
18
+ #plotter.plot_1d("original signal" => signal1.data, "upsampled signal" => signal2.data)
19
+
20
+ signal2.size.should eq(signal1.size * upsample_factor)
21
+
22
+ max_freq1 = signal1.freq_magnitudes.max_by{|freq, mag| mag }[0]
23
+ max_freq2 = signal2.freq_magnitudes.max_by{|freq, mag| mag }[0]
24
+
25
+ percent_error = (max_freq1 - max_freq2).abs / max_freq1
26
+ percent_error.should be_within(0.1).of(0.0)
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'pry'
3
+ require 'gnuplot'
4
+
5
+ describe SPCore::DFT do
6
+ context '.forward' do
7
+
8
+ it 'should produce a freq magnitude response peak that is within 10 percent of the test freq' do
9
+ dft_size = 64
10
+ sample_rate = 400.0
11
+
12
+ test_freqs = [
13
+ 20.0,
14
+ 40.0,
15
+ ]
16
+
17
+ test_freqs.each do |freq|
18
+ signal = SignalGenerator.new(:sample_rate => sample_rate, :size => dft_size).make_signal([freq])
19
+ signal *= BlackmanHarrisWindow.new(dft_size).data
20
+
21
+ output = DFT.forward signal.data
22
+ output = output[0...(output.size / 2)] # skip second half
23
+ output = output.map { |x| x.magnitude } # map complex values to magnitude
24
+
25
+ freq_magnitudes = {}
26
+ output.each_index do |i|
27
+ f = (sample_rate * i) / dft_size
28
+ freq_magnitudes[f] = output[i]
29
+ end
30
+
31
+ max_freq = freq_magnitudes.max_by {|f,mag| mag}[0]
32
+ percent_err = (max_freq - freq).abs / freq
33
+ percent_err.should be_within(0.10).of(0.0)
34
+ end
35
+ end
36
+ end
37
+
38
+ context '.inverse' do
39
+
40
+ it 'should produce a near-identical signal to the original sent into the forward DFT (with energy that is within 10 percent error of original signal)' do
41
+ dft_size = 32
42
+ sample_rate = 400.0
43
+
44
+ test_freqs = [
45
+ 20.0,
46
+ 40.0,
47
+ ]
48
+
49
+ test_freqs.each do |freq|
50
+ input = SignalGenerator.new(:sample_rate => sample_rate, :size => dft_size).make_signal([freq])
51
+ input *= BlackmanHarrisWindow.new(dft_size).data
52
+
53
+ output = DFT.forward input.data
54
+ input2 = DFT.inverse output
55
+
56
+ energy1 = input.energy
57
+ energy2 = input2.inject(0.0){|sum,x| sum + (x * x)}
58
+
59
+ percent_err = (energy2 - energy1).abs / energy1
60
+ percent_err.should be_within(0.10).of(0.0)
61
+
62
+ #Plotter.new(
63
+ # :title => "#{dft_size}-point DFT on #{freq} Hz sine wave signal",
64
+ # :xlabel => "frequency (Hz)",
65
+ # :ylabel => "magnitude response",
66
+ #).plot_1d("input1" => input.data, "input2" => input2)
67
+ end
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,84 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'pry'
3
+ require 'gnuplot'
4
+
5
+ describe SPCore::FFT do
6
+ context '.forward' do
7
+ it 'should produce a freq magnitude response peak that is within 10 percent of the test freq' do
8
+ fft_size = 64
9
+ sample_rate = 400.0
10
+
11
+ test_freqs = [
12
+ 20.0,
13
+ 40.0,
14
+ ]
15
+
16
+ test_freqs.each do |freq|
17
+ osc = Oscillator.new(:frequency => freq, :sample_rate => sample_rate)
18
+
19
+ input_size = fft_size
20
+
21
+ input = Array.new(input_size)
22
+ window = BlackmanHarrisWindow.new(input_size)
23
+
24
+ input_size.times do |i|
25
+ input[i] = osc.sample * window.data[i]
26
+ end
27
+
28
+ output = FFT.forward input
29
+ output = output[0...(output.size / 2)]
30
+ output = output.map { |x| x.magnitude } # map complex values to magnitude
31
+
32
+ magn_response = {}
33
+ output.each_index do |n|
34
+ f = (sample_rate * n) / (output.size * 2)
35
+ magn_response[f] = output[n]
36
+ end
37
+
38
+ max_freq = magn_response.max_by {|f,magn| magn }[0]
39
+ percent_err = (max_freq - freq).abs / freq
40
+ percent_err.should be_within(0.10).of(0.0)
41
+
42
+ #Plotter.new(
43
+ # :title => "#{input_size}-point FFT on #{freq} Hz sine wave signal",
44
+ # :xlabel => "frequency (Hz)",
45
+ # :ylabel => "magnitude response",
46
+ #).plot_2d("magnitude response" => magn_response)
47
+ end
48
+ end
49
+ end
50
+
51
+ context '.inverse' do
52
+
53
+ it 'should produce a near-identical signal to the original sent into the forward DFT (with energy that is within 10 percent error of original signal)' do
54
+ fft_size = 32
55
+ sample_rate = 400.0
56
+
57
+ test_freqs = [
58
+ 20.0,
59
+ 40.0,
60
+ ]
61
+
62
+ test_freqs.each do |freq|
63
+ input = SignalGenerator.new(:sample_rate => sample_rate, :size => fft_size).make_signal([freq])
64
+ input *= BlackmanHarrisWindow.new(fft_size).data
65
+
66
+ output = FFT.forward input.data
67
+ input2 = FFT.inverse output
68
+
69
+ energy1 = input.energy
70
+ energy2 = input2.inject(0.0){|sum,x| sum + (x * x)}
71
+
72
+ percent_err = (energy2 - energy1).abs / energy1
73
+ percent_err.should be_within(0.10).of(0.0)
74
+
75
+ #Plotter.new(
76
+ # :title => "#{fft_size}-point FFT on #{freq} Hz sine wave signal",
77
+ # :xlabel => "frequency (Hz)",
78
+ # :ylabel => "magnitude response",
79
+ #).plot_1d("input1" => input.data, "input2" => input2)
80
+ end
81
+ end
82
+ end
83
+
84
+ end
@@ -19,7 +19,7 @@ describe SPCore::Gain do
19
19
 
20
20
  it 'should prove to be the inverse of .db_to_linear' do
21
21
  20.times do
22
- x = SPCore::Interpolation.linear(0.0, -SPCore::Gain::MAX_DB_ABS, 1.0, SPCore::Gain::MAX_DB_ABS, rand)
22
+ x = SPCore::Interpolation.linear(-SPCore::Gain::MAX_DB_ABS, SPCore::Gain::MAX_DB_ABS, rand)
23
23
  y = SPCore::Gain::db_to_linear(x)
24
24
  z = SPCore::Gain::linear_to_db(y)
25
25
  ((z - x).abs / x).should be_within(1e-5).of(0.0)
@@ -38,7 +38,7 @@ describe SPCore::Gain do
38
38
  20.times do
39
39
  max_gain_linear = SPCore::Gain::db_to_linear(SPCore::Gain::MAX_DB_ABS)
40
40
  min_gain_linear = SPCore::Gain::db_to_linear(-SPCore::Gain::MAX_DB_ABS)
41
- x = SPCore::Interpolation.linear(0.0, min_gain_linear, 1.0, max_gain_linear, rand)
41
+ x = SPCore::Interpolation.linear(min_gain_linear, max_gain_linear, rand)
42
42
  y = SPCore::Gain::linear_to_db(x)
43
43
  z = SPCore::Gain::db_to_linear(y)
44
44
  ((z - x).abs / x).should be_within(1e-5).of(0.0)
File without changes
File without changes
@@ -0,0 +1,54 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe SPCore::SignalGenerator do
4
+ before :all do
5
+ @sample_rate = 600.0
6
+ @test_freqs = [
7
+ 65.0, 100.0, 250.0
8
+ ]
9
+ end
10
+
11
+ context '.make_signal' do
12
+ context 'one freq at a time' do
13
+ it 'should produce the same output as a plain Oscillator' do
14
+ size = 60
15
+ generator = SignalGenerator.new :sample_rate => @sample_rate, :size => size
16
+
17
+ @test_freqs.each do |freq|
18
+ signal = generator.make_signal [freq]
19
+
20
+ osc = Oscillator.new(:sample_rate => @sample_rate, :frequency => freq)
21
+ osc_output = Array.new(size)
22
+ size.times do |i|
23
+ osc_output[i] = osc.sample
24
+ end
25
+
26
+ signal.data.should eq(osc_output)
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'many freqs at a time' do
32
+ it 'should produce the same output as equivalent plain Oscillators' do
33
+ size = 60
34
+ generator = SignalGenerator.new :sample_rate => @sample_rate, :size => size
35
+
36
+ oscs = []
37
+ @test_freqs.each do |freq|
38
+ oscs.push Oscillator.new(:sample_rate => @sample_rate, :frequency => freq)
39
+ end
40
+
41
+ signal = generator.make_signal @test_freqs
42
+
43
+ osc_output = Array.new(size, 0.0)
44
+ size.times do |i|
45
+ oscs.each do |osc|
46
+ osc_output[i] += osc.sample
47
+ end
48
+ end
49
+
50
+ signal.data.should eq(osc_output)
51
+ end
52
+ end
53
+ end
54
+ end