spcore 0.1.0

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.
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.rdoc
3
+ LICENSE.txt
@@ -0,0 +1,10 @@
1
+ Gemfile.lock
2
+
3
+ # Gem artifacts
4
+ pkg/
5
+ vendor/cache/*.gem
6
+
7
+ # YARD artifacts
8
+ .yardoc
9
+ _yardoc
10
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+
@@ -0,0 +1 @@
1
+ --markup rdoc --title "spcore Documentation" --protected
@@ -0,0 +1,10 @@
1
+ === 0.1.0 / 2013-02-04
2
+
3
+ * Initial release:
4
+ ** Delay line
5
+ ** Biquad filters
6
+ ** Envelope detector
7
+ ** Conversion from dB-linear and linear-dB
8
+ ** Linear interpolation
9
+ ** Oscillator with selectable wave type (sine, square, triangle, sawtooth)
10
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 James Tunnell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ = spcore
2
+
3
+ * {Homepage}[https://rubygems.org/gems/spcore]
4
+ * {Documentation}[http://rubydoc.info/gems/spcore/frames]
5
+ * {Email}[mailto:jamestunnell at lavabit.com]
6
+
7
+ == Description
8
+
9
+ Contains core signal processing functions.
10
+
11
+ == Features
12
+
13
+ Delay line
14
+ Biquad filters
15
+ Envelope detector
16
+ Conversion from dB-linear and linear-dB
17
+ Linear interpolation
18
+ Oscillator with selectable wave type (sine, square, triangle, sawtooth)
19
+
20
+ == Examples
21
+
22
+ require 'spcore'
23
+
24
+ == Requirements
25
+
26
+ == Install
27
+
28
+ $ gem install spcore
29
+
30
+ == Copyright
31
+
32
+ Copyright (c) 2013 James Tunnell
33
+
34
+ See LICENSE.txt for details.
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler'
7
+ rescue LoadError => e
8
+ warn e.message
9
+ warn "Run `gem install bundler` to install Bundler."
10
+ exit -1
11
+ end
12
+
13
+ begin
14
+ Bundler.setup(:development)
15
+ rescue Bundler::BundlerError => e
16
+ warn e.message
17
+ warn "Run `bundle install` to install missing gems."
18
+ exit e.status_code
19
+ end
20
+
21
+ require 'rake'
22
+
23
+ require 'rspec/core/rake_task'
24
+ RSpec::Core::RakeTask.new
25
+
26
+ task :test => :spec
27
+ task :default => :spec
28
+
29
+ require 'yard'
30
+ YARD::Rake::YardocTask.new
31
+ task :doc => :yard
32
+
33
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ require 'hashmake'
2
+ require 'spcore/version'
3
+
4
+ require 'spcore/core/limiters'
5
+ require 'spcore/core/constants'
6
+
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'
@@ -0,0 +1,4 @@
1
+ module SPCore
2
+ TWO_PI = Math::PI * 2.0
3
+ NEG_PI = -Math::PI
4
+ end
@@ -0,0 +1,51 @@
1
+ module SPCore
2
+ class Limiters
3
+ def self.make_no_limiter
4
+ return lambda do |input|
5
+ return input
6
+ end
7
+ end
8
+
9
+ def self.make_range_limiter range
10
+ return lambda do |input|
11
+ if input < range.first
12
+ return range.first
13
+ elsif input > range.last
14
+ return range.last
15
+ else
16
+ return input
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.make_upper_limiter limit
22
+ return lambda do |input|
23
+ if input > limit
24
+ return limit
25
+ else
26
+ return input
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.make_lower_limiter limit
32
+ return lambda do |input|
33
+ if input < limit
34
+ return limit
35
+ else
36
+ return input
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.make_enum_limiter good_values
42
+ return lambda do |input, current|
43
+ if good_values.include?(input)
44
+ return input
45
+ else
46
+ return current
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,72 @@
1
+ module SPCore
2
+
3
+ # Based on the "simple implementation of Biquad filters" by Tom St Denis,
4
+ # which is based on the work "Cookbook formulae for audio EQ biquad filter
5
+ # coefficients" by Robert Bristow-Johnson, pbjrbj@viconet.com a.k.a.
6
+ # robert@audioheads.com. Available on the web at
7
+ # http://www.smartelectronix.com/musicdsp/text/filters005.txt
8
+ class BiquadFilter
9
+
10
+ # used in subclasses to calculate IIR filter coefficients
11
+ LN_2 = Math::log(2)
12
+
13
+ # this holds the data required to update samples thru a filter
14
+ Struct.new("BiquadState", :b0, :b1, :b2, :a0, :a1, :a2, :x1, :x2, :y1, :y2)
15
+
16
+ def initialize sample_rate
17
+ @sample_rate = sample_rate
18
+ @biquad = Struct::BiquadState.new(0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0)
19
+ @critical_freq = 0.0
20
+ @bandwidth = 0.0
21
+ end
22
+
23
+ def set_critical_freq_and_bw critical_freq, bandwidth
24
+ raise NotImplementedError, "set_critical_freq_and_bW should be implemented in the derived class!"
25
+ end
26
+
27
+ def critical_freq= critical_freq
28
+ set_critical_freq_and_bw(critical_freq, @bandwidth);
29
+ end
30
+
31
+ def bandwidth= bandwidth
32
+ set_critical_freq_and_bw(@critical_freq, bandwidth);
33
+ end
34
+
35
+ # Calculate biquad output using Direct Form I:
36
+ #
37
+ # y[n] = (b0/a0)*x[n] + (b1/a0)*x[n-1] + (b2/a0)*x[n-2]
38
+ # - (a1/a0)*y[n-1] - (a2/a0)*y[n-2]
39
+ #
40
+ # Note: coefficients are already divided by a0 when they
41
+ # are calculated. So that step is left out during processing.
42
+ #
43
+ def process_sample sample
44
+ # compute result
45
+ result = @biquad.b0 * sample + @biquad.b1 * @biquad.x1 + @biquad.b2 * @biquad.x2 -
46
+ @biquad.a1 * @biquad.y1 - @biquad.a2 * @biquad.y2;
47
+
48
+ # shift x1 to x2, sample to x1
49
+ @biquad.x2 = @biquad.x1;
50
+ @biquad.x1 = sample;
51
+
52
+ # shift y1 to y2, result to y1
53
+ @biquad.y2 = @biquad.y1;
54
+ @biquad.y1 = result;
55
+
56
+ return result
57
+ end
58
+
59
+ def get_freq_magnitude_response test_freq
60
+ # Method for determining freq magnitude response is from:
61
+ # http://rs-met.com/documents/dsp/BasicDigitalFilters.pdf
62
+ omega = 2.0 * Math::PI * test_freq / @sample_rate
63
+ b0, b1, b2 = @biquad.b0, @biquad.b1, @biquad.b2
64
+ a0, a1, a2 = 1, @biquad.a1, @biquad.a2
65
+ b = (b0**2) + (b1**2) + (b2**2) + (2 * (b0 * b1 + b1 * b2) * Math::cos(omega)) + (2 * b0 * b2 * Math::cos(2 * omega))
66
+ a = (a0**2) + (a1**2) + (a2**2) + (2 * (a0 * a1 + a1 * a2) * Math::cos(omega)) + (2 * a0 * a2 * Math::cos(2 * omega))
67
+ return Math::sqrt(b/a)
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,148 @@
1
+ module SPCore
2
+ class CircularBuffer
3
+
4
+ attr_accessor :fifo, :override_when_full
5
+ attr_reader :fill_count
6
+
7
+ def initialize size, opts = {}
8
+
9
+ opts = { :fifo => true, :override_when_full => true }.merge opts
10
+
11
+ @buffer = Array.new(size)
12
+ @oldest = 0;
13
+ @newest = 0;
14
+ @fill_count = 0;
15
+
16
+ @fifo = opts[:fifo]
17
+ @override_when_full = opts[:override_when_full]
18
+ end
19
+
20
+ def size
21
+ return @buffer.count
22
+ end
23
+
24
+ def empty?
25
+ return @fill_count == 0
26
+ end
27
+
28
+ def full?
29
+ return (@fill_count == size)
30
+ end
31
+
32
+ def resize size
33
+ rv = false
34
+ if(size != @buffer.count)
35
+ rv = true
36
+ @buffer = Array.new(size)
37
+ @oldest = 0
38
+ @newest = 0
39
+ @fill_count = 0
40
+ end
41
+ return rv
42
+ end
43
+
44
+ def to_ary
45
+ if empty?
46
+ return []
47
+ end
48
+
49
+ # newest index is actually @newest - 1
50
+ newest_idx = @newest - 1;
51
+ if(newest_idx < 0)
52
+ newest_idx += @buffer.count;
53
+ end
54
+
55
+ if newest_idx >= @oldest
56
+ return @buffer[@oldest..newest_idx]
57
+ else
58
+ return @buffer[@oldest...@buffer.count] + @buffer[0..newest_idx]
59
+ end
60
+ end
61
+
62
+ def push element
63
+ if full?
64
+ raise ArgumentError, "buffer is full, and override_when_full is false" unless @override_when_full
65
+
66
+ @buffer[@newest] = element;
67
+ @newest += 1
68
+ @oldest += 1
69
+ else
70
+ @buffer[@newest] = element;
71
+ @newest += 1
72
+ @fill_count += 1
73
+ end
74
+
75
+ if @oldest >= @buffer.count
76
+ @oldest = 0
77
+ end
78
+
79
+ if @newest >= @buffer.count
80
+ @newest = 0
81
+ end
82
+ end
83
+
84
+ def push_ary ary
85
+ ary.each do |element|
86
+ push element
87
+ end
88
+ end
89
+
90
+ def newest relative_index = 0
91
+ raise ArgumentError, "buffer is empty" if empty?
92
+ raise ArgumentError, "relative_index is greater or equal to @fill_count" if relative_index >= @fill_count
93
+
94
+ # newestIdx is actually @newest - 1
95
+ newestIdx = @newest - 1;
96
+ if(newestIdx < 0)
97
+ newestIdx += @buffer.count;
98
+ end
99
+
100
+ absIdx = newestIdx - relative_index;
101
+ if(absIdx < 0)
102
+ absIdx += @buffer.count;
103
+ end
104
+
105
+ return @buffer[absIdx];
106
+ end
107
+
108
+ def oldest relative_index = 0
109
+ raise ArgumentError, "buffer is empty" if empty?
110
+ raise ArgumentError, "relative_index is greater or equal to @fill_count" if relative_index >= @fill_count
111
+
112
+ absIdx = @oldest + relative_index;
113
+ if(absIdx >= @buffer.count)
114
+ absIdx -= @buffer.count;
115
+ end
116
+
117
+ return @buffer[absIdx];
118
+ end
119
+
120
+ # Pop the oldest/newest element, depending on @fifo flag. When true, pop the oldest,
121
+ # otherwise pop the newest. Set to true by default, can override during initialize or
122
+ # later using fifo=.
123
+ def pop
124
+ raise ArgumentError, "buffer is empty" if empty?
125
+
126
+ if @fifo
127
+ # FIFO - pop the oldest element
128
+ element = oldest
129
+ @oldest += 1
130
+ if(@oldest >= @buffer.count)
131
+ @oldest = 0
132
+ end
133
+ @fill_count -= 1
134
+ return element
135
+ else
136
+ # FILO - pop the newest element
137
+ element = newest
138
+ if(@newest > 0)
139
+ @newest -= 1
140
+ else
141
+ @newest = @buffer.count - 1
142
+ end
143
+ @fill_count -= 1
144
+ return element
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,38 @@
1
+ module SPCore
2
+ class CookbookAllpassFilter < BiquadFilter
3
+ def initialize sample_rate
4
+ super(sample_rate)
5
+ end
6
+
7
+ def set_critical_freq_and_bw critical_freq, bandwidth
8
+ @critical_freq = critical_freq
9
+ @bandwidth = bandwidth
10
+
11
+ # setup variables
12
+ omega = 2.0 * Math::PI * @critical_freq / @sample_rate
13
+ sn = Math::sin(omega)
14
+ cs = Math::cos(omega)
15
+ alpha = sn * Math::sinh(BiquadFilter::LN_2 / 2.0 * @bandwidth * omega / sn)
16
+
17
+ b0 = 1.0 - alpha
18
+ b1 = -2.0 * cs
19
+ b2 = 1.0 + alpha
20
+ a0 = 1.0 + alpha
21
+ a1 = -2.0 * cs
22
+ a2 = 1.0 - alpha
23
+
24
+ # precompute the coefficients
25
+ @biquad.b0 = b0 / a0
26
+ @biquad.b1 = b1 / a0
27
+ @biquad.b2 = b2 / a0
28
+ @biquad.a0 = a0 / a0
29
+ @biquad.a1 = a1 / a0
30
+ @biquad.a2 = a2 / a0
31
+
32
+ ## zero initial samples
33
+ #@biquad.x1 = @biquad.x2 = 0
34
+ #@biquad.y1 = @biquad.y2 = 0
35
+ end
36
+
37
+ end
38
+ end