rsynth 0.0.1.alpha0

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