rsynth 0.0.1.alpha0

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.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,2 @@
1
+ require "bundler/setup"
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,18 @@
1
+ module RSynth
2
+ class Combiner
3
+ include RSynth::Functions
4
+ attr_accessor :a, :b
5
+
6
+ def initialize(a, b, proc=nil)
7
+ proc = Proc.new{|a,b| yield a, b} if block_given?
8
+ raise 'No block or proc given' if proc.nil?
9
+ @proc = proc
10
+ @a = a
11
+ @b = b
12
+ end
13
+
14
+ def value_at(time)
15
+ @proc.call(@a.value_at(time), @b.value_at(time))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,91 @@
1
+ module RSynth
2
+ module Functions
3
+ def combine(type, other)
4
+ case type
5
+ when :add, :plus
6
+ RSynth::Combiner.new(self, other) { |a, b| a + b }
7
+ when :sub, :subtract, :minus
8
+ RSynth::Combiner.new(self, other) { |a, b| a - b }
9
+ when :div, :divide
10
+ RSynth::Combiner.new(self, other) { |a, b| a / b }
11
+ when :mul, :multiply, :times
12
+ RSynth::Combiner.new(self, other) { |a, b| a * b }
13
+ when :exp, :exponent, :pow, :power
14
+ RSynth::Combiner.new(self, other) { |a, b| a ** b }
15
+ when :mod, :modulo, :rem, :remainder
16
+ RSynth::Combiner.new(self, other) { |a, b| a % b }
17
+ when :mix
18
+ RSynth::Combiner.new(self, other) do |a, b|
19
+ # Normalise between 0 and 1
20
+ a = (a + 1) / 2
21
+ b = (b + 1) / 2
22
+ z = (a < 0.5 and b < 0.5)? 2*a*b : 2*(a+b) - (2*a*b) - 1
23
+ # Convert back
24
+ (z * 2) - 1
25
+ end
26
+ else
27
+ raise "Unknown combiner type: #{type}"
28
+ end
29
+ end
30
+
31
+ def phase_shift(offset=nil)
32
+ offset = Proc.new{ |time| yield time } if block_given?
33
+ raise 'No offset or block given' if offset.nil?
34
+ RSynth::PhaseShifer.new(self, offset)
35
+ end
36
+
37
+ # won't work for numeric or proc (unless we override but there'd be a huge performance hit)
38
+ def +(other)
39
+ combine(:add, other)
40
+ end
41
+
42
+ def -(other)
43
+ combine(:sub, other)
44
+ end
45
+
46
+ def /(other)
47
+ combine(:div, other)
48
+ end
49
+
50
+ def *(other)
51
+ combine(:mul, other)
52
+ end
53
+
54
+ def **(other)
55
+ combine(:exp, other)
56
+ end
57
+
58
+ def %(other)
59
+ combine(:mod, other)
60
+ end
61
+
62
+ def &(other)
63
+ combine(:mix, other)
64
+ end
65
+
66
+ def <<(offset)
67
+ phase_shift(-offset)
68
+ end
69
+
70
+ def >>(offset)
71
+ phase_shift(offset)
72
+ end
73
+ end
74
+ end
75
+
76
+ class Proc
77
+ include RSynth::Functions
78
+
79
+ def value_at(time)
80
+ self.call(time)
81
+ end
82
+ end
83
+
84
+ # Add mixins to int and float :O
85
+ class Numeric
86
+ include RSynth::Functions
87
+
88
+ def value_at(time)
89
+ self
90
+ end
91
+ end
@@ -0,0 +1,101 @@
1
+ module RSynth
2
+ class Note
3
+ ChromaticInterval = 2**(1.0/12)
4
+ ChromaticNotes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
5
+ IntervalMap = [
6
+ %w(perfect_unison unison P1 diminished_second d2),
7
+ %w(minor_second m2 augmented_unison A1 semitone S),
8
+ %w(major_second M2 diminished_third d3 tone whole_tone T),
9
+ %w(minor_third m3 augmented_second A2),
10
+ %w(major_third M3 diminished_fourth d4),
11
+ %w(perfect_fourth fourth P4 augmented_third A3),
12
+ %w(tritone diminished_fifth d5 augmented_fourth A4 TT),
13
+ %w(perfect_fifth fifth P5 diminished_sixth d6),
14
+ %w(minor_sixth m6 augmented_fifth A5),
15
+ %w(major_sixth M6 diminished_seventh d7),
16
+ %w(minor_seventh m7 augmented_sixth A6),
17
+ %w(major_seventh M7 diminished_octave d8),
18
+ %w(perfect_octave octave P8 augmented_seventh A7)
19
+ ]
20
+ include RSynth::Functions
21
+
22
+ attr_reader :note, :octave, :freq
23
+
24
+ def initialize(note, octave)
25
+ @note = note.upcase
26
+ @octave = octave
27
+ raise "Invalid note value: #{note}" unless ChromaticNotes.include?(@note)
28
+ raise "Invalid octave: #{octave}, must be >= 0" if octave < 0
29
+
30
+ # Calculate frequency (notes are relative to A4 (440Hz))
31
+ rn = -(ChromaticNotes.index('A') - ChromaticNotes.index(@note))
32
+ ro = @octave - 4
33
+ steps = (ro * 12) + rn
34
+ @freq = 440*(ChromaticInterval**steps)
35
+ end
36
+
37
+ def value_at(time)
38
+ @freq
39
+ end
40
+
41
+ def next
42
+ transpose(1)
43
+ end
44
+
45
+ def previous
46
+ transpose(-1)
47
+ end
48
+
49
+ def previous_octave
50
+ transpose(-12)
51
+ end
52
+
53
+ def transpose(count=1)
54
+ oct = @octave
55
+ ni = ChromaticNotes.index(@note)
56
+ ni += count
57
+ while ni >= ChromaticNotes.length
58
+ oct += 1
59
+ ni -= ChromaticNotes.length
60
+ end
61
+ while ni < 0
62
+ oct -= 1
63
+ ni += ChromaticNotes.length
64
+ end
65
+ Note.retrieve(ChromaticNotes[ni], oct)
66
+ end
67
+
68
+ def to_s
69
+ "#@note#@octave"
70
+ end
71
+
72
+ def <=>(note)
73
+ os = @octave <=> note.octave
74
+ return os unless os == 0
75
+ ChromaticNotes.index(@note) <=> ChromaticNotes.index(note.note)
76
+ end
77
+
78
+ def self.retrieve(note, octave)
79
+ note.upcase!
80
+ name = "#{note.sub('#', 'Sharp')}#{octave}"
81
+ RSynth.const_set(name, Note.new(note, octave)) unless RSynth.const_defined?(name)
82
+ RSynth.const_get(name)
83
+ end
84
+
85
+ # Define the interval methods...
86
+ IntervalMap.each_with_index do |names, interval|
87
+ names.each do |name|
88
+ define_method(name.to_sym) do
89
+ self.transpose(interval)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Define the chromatic scale at octaves 0-9
96
+ 9.times do |o|
97
+ Note::ChromaticNotes.each do |n|
98
+ Note.retrieve(n, o)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,32 @@
1
+ module RSynth
2
+ class Oscillator
3
+ PI2 = Math::PI*2
4
+ TableLength = 1024
5
+ SinTable = TableLength.times.map{|i| Math.sin((PI2 * i) / TableLength)}
6
+ CosTable = TableLength.times.map{|i| Math.cos((PI2 * i) / TableLength)}
7
+
8
+ include RSynth::Functions
9
+
10
+ attr_accessor :freq, :func
11
+
12
+ def initialize(func, freq)
13
+ @freq = freq
14
+ @func = func
15
+ @phase = 0
16
+ end
17
+
18
+ def value_at(time)
19
+ f = @freq.value_at(time)
20
+ i = @phase
21
+ @phase = (@phase + (f.to_f / RSynth::SampleRate)) % 1.0
22
+ case @func
23
+ when :sin, :sine then SinTable[(i*TableLength).to_i]
24
+ when :cos, :cosine then CosTable[(i*TableLength).to_i]
25
+ when :sq, :square then i <= 0.5? 1: -1
26
+ when :tri, :triangle then i <= 0.25? (i * 4): i <= 0.5? (1 - (i - 0.25) * 4): i <= 0.75? (0 - (i - 0.5) * 4): (i - 0.75) * 4 - 1
27
+ when :saw, :sawtooth then i * 2 - 1
28
+ else @func.value_at(@phase)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module RSynth
2
+ class PhaseShifter
3
+ include RSynth::Functions
4
+ attr_accessor :source, :offset
5
+
6
+ def initialize(source, offset=nil)
7
+ offset = Proc.new {|time| yield time} if block_given?
8
+ raise 'No offset or block defined' if offset.nil?
9
+ @source = source
10
+ @offset = offset
11
+ end
12
+
13
+ def value_at(time)
14
+ @source.value_at(time + @offset.value_at(time))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,84 @@
1
+ module RSynth
2
+ module Pipe
3
+ class Pipeline
4
+ NumBuffers = 2 # Number of buffers we keep in our ring buffer
5
+ FramesPerBuffer = 512 # How many bytes per buffer (higher is higher latency, obviously...)
6
+
7
+ attr_accessor :sequence, :source
8
+ attr_reader :time
9
+
10
+ def initialize(source, *sequence)
11
+ source = Proc.new { |time| yield time } if block_given?
12
+ raise 'No source or block given' if source.nil?
13
+ @sequence = sequence || []
14
+ @source = source
15
+ @stopped = true
16
+ end
17
+
18
+ def start(time=Float::INFINITY)
19
+ return unless @stopped
20
+ @stopped = false
21
+ @length = time
22
+ Thread.new {generate}
23
+ end
24
+
25
+ def stop
26
+ @stopped = true
27
+ end
28
+
29
+ def restart
30
+ stop
31
+ start
32
+ end
33
+
34
+ def consume(sequence)
35
+ sidx = @sequence.index(sequence) || @sequence.length
36
+ bidx = @seqpos[sidx] || 0
37
+ @generated[bidx]
38
+ end
39
+
40
+ def consumed(sequence)
41
+ sidx = @sequence.index(sequence) || @sequence.length
42
+ bidx = @seqpos[sidx] || 0
43
+ @seqpos[sidx] = bidx + 1 # increment this sequences buffer index entry...
44
+
45
+ # If all sequences have consumed the bottom buffer, release it
46
+ if @seqpos.all?{|s| s > 0}
47
+ @buffers.push(@generated.shift)
48
+ @seqpos.map!{|i| i - 1}
49
+ @time += FramesPerBuffer * RSynth::TimeStep
50
+ end
51
+ end
52
+
53
+ def generate
54
+ @time = 0
55
+ @generate_time = 0
56
+ timePerLoop = FramesPerBuffer * RSynth::TimeStep
57
+ @buffers = 10.times.map{Array.new(FramesPerBuffer)}
58
+ @generated = []
59
+ @seqpos = @sequence.length.times.map{0}
60
+ @sequence.each { |s| s.start(self) }
61
+ while not @stopped
62
+ if @generate_time < @length
63
+ sleep(RSynth::TimeStep) while (@buffers.length == 0 or @generated.length >= NumBuffers) and not @stopped
64
+ buf = @buffers.shift
65
+ len = @generate_time + timePerLoop > @length ? (@length - @generate_time) / RSynth::TimeStep : FramesPerBuffer
66
+ len.times do |i|
67
+ buf[i] = @source.value_at(@generate_time)
68
+ @generate_time += RSynth::TimeStep
69
+ end
70
+ (FramesPerBuffer - len).times do |i|
71
+ buf[i] = 0
72
+ end
73
+ @generated << buf
74
+ end
75
+ end
76
+ @sequence.each{ |s| s.stop }
77
+ end
78
+
79
+ def to_s
80
+ "RSynth::Pipe::Pipeline source=#{@source},seq=#{@sequence}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,93 @@
1
+ require 'ffi-portaudio'
2
+
3
+ module RSynth
4
+ module Pipe
5
+ class PortAudio
6
+ include ::FFI::PortAudio
7
+
8
+ def initialize
9
+ wrap_call('initialise portaudio') { | | API.Pa_Initialize() }
10
+ @buf = FFI::Buffer.new(:float, RSynth::Pipe::Pipeline::FramesPerBuffer, true) # Create our native buffer
11
+ end
12
+
13
+ def start(pipeline)
14
+ @source = pipeline
15
+ open_stream
16
+ end
17
+
18
+ def stop
19
+ close_stream
20
+ @source = nil
21
+ end
22
+
23
+ private
24
+ def process input, output, frame_count, time_info, status_flags, user_data
25
+ n = @source.consume(self)
26
+ return :pcComplete if n.nil?
27
+ output.write_array_of_float(n)
28
+ @source.consumed(self)
29
+ :paContinue
30
+ end
31
+
32
+ def open_stream
33
+ close_stream unless @stream.nil?
34
+ # Get the audio host
35
+ info = wrap_call('get default audio host') do | |
36
+ return idx if error?(idx = API.Pa_GetDefaultHostApi())
37
+ API.Pa_GetHostApiInfo(idx)
38
+ end
39
+
40
+ # Get the default device
41
+ inIdx = info[:defaultInputDevice] # Todo: add sampling for luls
42
+ outIdx = info[:defaultOutputDevice]
43
+ outDev = wrap_call 'get device info' do | |
44
+ API.Pa_GetDeviceInfo(outIdx)
45
+ end
46
+
47
+ # Create parameters for the output stream
48
+ outopts = API::PaStreamParameters.new
49
+ outopts[:device] = outIdx
50
+ outopts[:channelCount] = RSynth::Channels
51
+ outopts[:sampleFormat] = API::Float32
52
+ outopts[:suggestedLatency] = outDev[:defaultHighOutputLatency]
53
+ outopts[:hostApiSpecificStreamInfo] = nil
54
+
55
+ @stream = FFI::Buffer.new :pointer
56
+ @callback = method(:process)
57
+ wrap_call('open output stream') do | |
58
+ API.Pa_OpenStream(
59
+ @stream, # Stream
60
+ nil, # Input options
61
+ outopts, # Output options
62
+ RSynth::SampleRate, # the sample rate
63
+ RSynth::Pipe::Pipeline::FramesPerBuffer, # frames per buffer
64
+ API::PrimeOutputBuffersUsingStreamCallback, # flags
65
+ @callback, # Callback
66
+ nil # User data
67
+ )
68
+ end
69
+
70
+ # Start the stream playing
71
+ wrap_call('starting output stream') do | |
72
+ API.Pa_StartStream @stream.read_pointer
73
+ end
74
+ end
75
+
76
+ def close_stream
77
+ return if @stream.nil?
78
+ API.Pa_CloseStream(@stream.read_pointer)
79
+ @stream = nil
80
+ end
81
+
82
+ def error?(val)
83
+ (Symbol === val and val != :paNoError) or (Integer === val and val < 0)
84
+ end
85
+
86
+ def wrap_call(msg, &block)
87
+ ret = yield block
88
+ raise "Failed to #{msg}: #{API.Pa_GetErrorText(ret)}" if error?(ret)
89
+ return ret
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'pipe/pipeline'
2
+ require_relative 'pipe/portaudio'
3
+ require_relative 'functions'
4
+ require_relative 'combiner'
5
+ require_relative 'phase_shifter'
6
+ require_relative 'oscillator'
7
+ require_relative 'notes'
8
+ require_relative 'scales'
9
+
10
+ module RSynth
11
+ SampleRate = 22050
12
+ TimeStep = 1.0/SampleRate
13
+ Channels = 1
14
+ end
@@ -0,0 +1,41 @@
1
+ module RSynth
2
+ class Note
3
+ # Add some methods to the note class :O
4
+ def major_scale
5
+ DiatonicScale.new(self, :ionian)
6
+ end
7
+ end
8
+
9
+ # A diatonic scale :O
10
+ class DiatonicScale < Array
11
+ Degrees = %w(tonic supertonic mediant subdominant dominant submediant leading_tone octave).map(&:to_sym)
12
+ Modes = %w(ionian dorian phyrgian lydian aeolian locrian).map(&:to_sym)
13
+ Pattern = %w(2 2 1 2 2 2 1).map(&:to_i)
14
+
15
+ def initialize(key, mode)
16
+ @key = key
17
+ @mode = mode
18
+
19
+ # Define degrees...
20
+ pat = Pattern.rotate(Modes.index(mode))
21
+ semitones = 0
22
+ Degrees.each_with_index do |name, i|
23
+ idx = Degrees.index(name)
24
+ self << @key.transpose(pat.take(idx).inject(:+) || 0)
25
+ semitones += pat[i] unless i >= pat.length
26
+ end
27
+ end
28
+
29
+ Modes.each do |name|
30
+ define_method name do
31
+ DiatonicScale.new(@key, name)
32
+ end
33
+ end
34
+
35
+ Degrees.each_with_index do |name, i|
36
+ define_method name do
37
+ self[i]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module RSynth
2
+ VERSION = '0.0.1.alpha0'
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require 'rsynth/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "rsynth"
8
+ s.version = RSynth::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = %w(James Lawrence)
11
+ s.email = %w(james@kukee.co.uk)
12
+
13
+ s.summary = "Rsynth aims to be a simple to use audio synthesis library for use in ruby"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } # Stolen from github gem, uses git to list files, very nice!
18
+ s.require_paths = %w(lib)
19
+
20
+ s.add_dependency "ffi-portaudio", "~>0.1"
21
+
22
+ s.add_development_dependency "rake"
23
+ s.add_development_dependency "rspec", "~>1.3.1"
24
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rsynth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha0
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - James
9
+ - Lawrence
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-01-19 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: ffi-portaudio
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '0.1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: '0.1'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rake
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.3.1
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: 1.3.1
63
+ description:
64
+ email:
65
+ - james@kukee.co.uk
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - Gemfile
71
+ - Rakefile
72
+ - lib/rsynth/combiner.rb
73
+ - lib/rsynth/functions.rb
74
+ - lib/rsynth/notes.rb
75
+ - lib/rsynth/oscillator.rb
76
+ - lib/rsynth/phase_shifter.rb
77
+ - lib/rsynth/pipe/pipeline.rb
78
+ - lib/rsynth/pipe/portaudio.rb
79
+ - lib/rsynth/rsynth.rb
80
+ - lib/rsynth/scales.rb
81
+ - lib/rsynth/version.rb
82
+ - rsynth.gemspec
83
+ homepage:
84
+ licenses: []
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>'
99
+ - !ruby/object:Gem::Version
100
+ version: 1.3.1
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 1.8.24
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Rsynth aims to be a simple to use audio synthesis library for use in ruby
107
+ test_files: []