spcore 0.1.0

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