From c7d4b981637d26a8f910f58f38de54655a7029b1 Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Mon, 30 Apr 2012 22:54:17 -0700 Subject: [PATCH 01/16] add .rvmrc --- .rvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .rvmrc diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 0000000..d88fc14 --- /dev/null +++ b/.rvmrc @@ -0,0 +1 @@ +rvm use jruby From faa435f37b5ec0d6966a1392ffa894ef0eb58532 Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Mon, 30 Apr 2012 23:46:59 -0700 Subject: [PATCH 02/16] tweaks --- src/opaz_bootstrap.rb | 2 +- tasks/tools.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/opaz_bootstrap.rb b/src/opaz_bootstrap.rb index 38d820f..19208f5 100644 --- a/src/opaz_bootstrap.rb +++ b/src/opaz_bootstrap.rb @@ -8,7 +8,7 @@ # plugin libs require 'irb' -require 'Plugin.jar' +require 'Plugin.jar' if File.exists?( File.join(PLUGIN_RESOURCES_FOLDER, "Plugin.jar") ) # this is to be able to call the static log() method in VSTPluginAdapter include_class 'jvst.wrapper.VSTPluginAdapter' diff --git a/tasks/tools.rb b/tasks/tools.rb index 4f22d1a..d9d517a 100644 --- a/tasks/tools.rb +++ b/tasks/tools.rb @@ -75,7 +75,8 @@ def package_plugin(plugin_name,plugin_folder,platforms=PLATFORMS) resources_folder = platform_build_folder + "/wrapper.vst" + (platform == :osx ? "/Contents/Resources" : "") # copy platform template - cp_r template(platform), platform_build_folder + # cp_r template(platform), platform_build_folder + cp_r template(platform), build_folder(plugin_folder) # create ini file ini_file = resources_folder + "/" + (platform == :osx ? "wrapper.jnilib.ini" : "wrapper.ini") From 9b2a28739ad0970742087cc9336b3f62bfbe65bd Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Tue, 1 May 2012 13:24:39 -0700 Subject: [PATCH 03/16] polyphonic synth voicer --- plugins/SuperSynth/SuperSynth.rb | 202 ++++++++++++++++++++++++++++ plugins/SuperSynth/dsp.rb | 11 ++ plugins/SuperSynth/midi.rb | 27 ++++ plugins/SuperSynth/phasor.rb | 51 +++++++ plugins/SuperSynth/phasor.rb.simple | 45 +++++++ 5 files changed, 336 insertions(+) create mode 100644 plugins/SuperSynth/SuperSynth.rb create mode 100644 plugins/SuperSynth/dsp.rb create mode 100644 plugins/SuperSynth/midi.rb create mode 100644 plugins/SuperSynth/phasor.rb create mode 100644 plugins/SuperSynth/phasor.rb.simple diff --git a/plugins/SuperSynth/SuperSynth.rb b/plugins/SuperSynth/SuperSynth.rb new file mode 100644 index 0000000..175eac5 --- /dev/null +++ b/plugins/SuperSynth/SuperSynth.rb @@ -0,0 +1,202 @@ +include_class 'IRBPluginGUI' + +class SuperSynthGUI + attr_reader :frame, :plugin + def initialize(plugin, frame) + @frame = frame + @plugin = plugin + + # spawn and attach an IRB session alongside the plugin GUI + # (stdout (puts, etc.) is redirected to the IRB session + # irb = IRBPluginGUI.new(JRuby.runtime) # comment out if you are done debugging + + + frame.setTitle("SuperSynth GUI") + frame.setSize(200, 120) + end + + # Check RubyGain for a more elaborate GUI example +end + +require 'midi' +require 'phasor' + +class SuperSynth < OpazPlug + editor SuperSynthGUI + + # plugin "plugin-name", "product-name", "vendor-name" + plugin "SuperSynth", "SuperSynth", "jVSTwRapper" + + can_do "receiveVstEvents", "receiveVstMidiEvent" + def getPlugCategory + VSTPluginAdapter.PLUG_CATEG_SYNTH + end + + unique_id "OWSS" + + + param :gain, "Gain", 0.8 #, "dB" + + def initialize wrapper, opts={ :bus => "0x1", :synth => true } + super wrapper + log "Booting #{getEffectName}:#{getProductString}:#{getVendorString}\n with opts=#{opts.inspect}" + opts[:in],opts[:out] = opts[:bus].split("x").map(&:to_i) if opts[:bus] + opts = {:in=>1,:out=>1}.merge(opts) + setNumInputs opts[:in] + setNumOutputs opts[:out] + canProcessReplacing(true) + setUniqueID(unique_id) + if opts[:synth] + isSynth( @synth = true ) + # suspend + end + + @osc = Phasor.new( @@samplerate ) + canProcessReplacing(true); + end + + def setSampleRate( srate ) + super + self.sampleRate = srate + end + + def self.sampleRate= srate + @@sampleRate = srate + end + + def self.sampleRate + # VSTTimeInfo time = this.getTimeInfo(VSTTimeInfo.VST_TIME_AUTOMATION_READING|VSTTimeInfo.VST_TIME_AUTOMATION_WRITING|VSTTimeInfo.VST_TIME_CLOCK_VALID|VSTTimeInfo.VS + # tempo = time.getTempo(); + # samplePos = time.getSamplePos(); + # sampleRate = time.getSampleRate(); + @@sampleRate + end + + + # TODO: move this to Java/Mirah + def processReplacing(inputs, outputs, sampleFrames) + # inBuffer, outBuffer = inputs[0], outputs[0] + outBuffer = outputs[0] + if @silence + outBuffer.fill(0,0...sampleFrames) + else + sampleFrames.times do |i| + # TODO @delta + outBuffer[i] = 0.99 * gain * @amp * @osc.tick() + end + end + end + + + def processEvents(events) + Midi.process(events) do |type, note, velocity, delta| + case type + when :all_notes_off + log "all notes off" + @silence = true + when :note_on + if velocity.zero? && note == @currentNote + log "note on zero" + @silence = true + else + log "note on" + @silence = false + @osc.freq = Midi.note_to_freq( note ) + @amp = velocity / 127.0 + @delta = delta # TODO ??? + end + end + end + 1 # want more + end + +end + +class PolyphonicSynth + class Voice + # attr_accessor :note #, :velocity, :delta + + def initialize( voicer, synth, opts={} ) + @voicer = voicer + @synth = synth.new(self, opts) + self + end + + def tick + output = @synth.tick + # stop if @synth.silent? # PERF TODO: move this to a callback in the synth? and/or just return 0 in synth + output + end + + def play( note, velocity, delta ) + @note, @velocity, @delta = note,velocity,delta + @synth.freq = Midi.note_to_freq( note ) + @synth.trigger(:attack) + self + end + + def release + @synth.trigger(:release) + end + + def stop + @voicer.free_voice self + end + + end + + def initialize synth, synth_opts, num_voices=8 + @voice_scaling = (1..num_voices).map{|i| 1.0 / Math.sqrt(i) } # lookup table + @voice_pool = (1..num_voices).map{ Voice.new( self, synth, synth_opts ) } + @notes_playing = {} + end + + def all_notes_off + active_voices.each(&:stop) + @notes_playing = {} + end + + def note_on note, velocity=100, delta=0 + return note_off(note) if velocity.zero? + voice = @notes_playing.delete[ note ] || allocate_voice + @notes_playing[note] = voice.play( note, velocity, delta ) + end + + def note_off note + @notes_playing[note].try(&:release) + end + + def free_voice voice + if voice = @notes_playing.delete(note) # return to pool if playing + @voice_pool << voice + end + end + + def tick + voices = active_voices + @voice_scaling[ voices.count ] * voices.inject(0){ |sum,voice| sum + voice.tick } + end + + def ticks samples + require 'matrix' + voices = active_voices + zeros = Vector[ *Dsp.zeros(samples) ] + output = voices.inject( zeros ){ |sum,voice| sum + voice.tick } + ( @voice_scaling[ voices.count ] * output ).to_a + end + + private + + def allocate_voice + @voice_pool.pop || steal_voice + end + + def steal_voice # FIXME: we might need a fade out to prevent clicks here? + active_voices.sort_by(&:delta).first + end + + def active_voices + @notes_playing.values + end + +end \ No newline at end of file diff --git a/plugins/SuperSynth/dsp.rb b/plugins/SuperSynth/dsp.rb new file mode 100644 index 0000000..80c31b6 --- /dev/null +++ b/plugins/SuperSynth/dsp.rb @@ -0,0 +1,11 @@ +module Dsp + extend self + + def noise + 2 * Random.rand - 1 + end + + def zeros num + [].fill(0,0...num) + end +end diff --git a/plugins/SuperSynth/midi.rb b/plugins/SuperSynth/midi.rb new file mode 100644 index 0000000..6cfbec8 --- /dev/null +++ b/plugins/SuperSynth/midi.rb @@ -0,0 +1,27 @@ +module Midi + extend self + + A = 432.0 + def note_to_freq( note ) + A * 2.0**((note-69)/12.0) + end + + def process(events) + events.get_events().each do |event| + next unless event.getType == VSTEvent::VST_EVENT_MIDI_TYPE + midiData = event.getData + channel = midiData[0] & 0x0f # is this correct?? + + case status = midiData[0] & 0xf0 # ignore channel + when 0x90, 0x80 + note = midiData[1] & 0x7f # we only look at notes + velocity = (status == 0x80) ? 0 : midiData[2] & 0x7f + yield :note_on, note, velocity, event.getDeltaFrames + when 0xb0 + yield :all_notes_off if [0x7e, 0x7b].include?( midiData[1] ) + end + end + end + +end + diff --git a/plugins/SuperSynth/phasor.rb b/plugins/SuperSynth/phasor.rb new file mode 100644 index 0000000..a2f0970 --- /dev/null +++ b/plugins/SuperSynth/phasor.rb @@ -0,0 +1,51 @@ +# TODO: move to java +require './midi' +require './dsp' + +module Tickable + def tick + raise "not implemented!" + end + + def ticks samples + (1..samples).map{ tick } + end +end + +class Oscillator + include Tickable + + def initialize( srate=44.1e3 ) # srate== OpazPlug.sampleRate ) + @inv_srate = 1.0 / srate + self.freq = Midi::A + end + + def freq + @freq + end + + def freq= freq + raise "not implemented!" + end +end + +class Phasor < Oscillator + attr_accessor :phase + + OFFSET = { true => 0.0, false => 1.0 } # branchless trick from Urs Heckmann + + def initialize( srate=44.1e3, phase = Dsp.noise ) + super srate + @phase = phase + end + + def tick + @phase += @inc # increment + @phase -= OFFSET[ @phase <= 1.0 ] # wrap + end + + def freq= freq + @freq = freq + @inc = @freq * @inv_srate + end +end diff --git a/plugins/SuperSynth/phasor.rb.simple b/plugins/SuperSynth/phasor.rb.simple new file mode 100644 index 0000000..eb2f80b --- /dev/null +++ b/plugins/SuperSynth/phasor.rb.simple @@ -0,0 +1,45 @@ +# TODO: move to java +require './midi' + + +class Phasor + attr_accessor :phase + + def freq + @freq + end + + def freq= freq + @freq = freq + @inc = @freq * @inv_srate + end + + def initialize( srate=44.1e3, phase = Dsp.noise ) # srate== OpazPlug.sampleRate ) + @inv_srate = 1.0 / srate + @phase = phase + self.freq = Midi::A + end + + OFFSET = { true => 0.0, false => 1.0 } # branchless trick from Urs Heckmann + + def tick + @phase += @inc # increment + @phase -= OFFSET[ @phase <= 1.0 ] # wrap + end + + def ticks times + (1..times).map{ tick } + end + +end + + +module Dsp + extend self + + def noise + 2 * Random.rand - 1 + end + +end + \ No newline at end of file From ad900d8b62778ff5d74a200be1ed23d292a374cc Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Tue, 1 May 2012 14:18:11 -0700 Subject: [PATCH 04/16] Krystal scale; use delta in voice#ticks --- plugins/SuperSynth/SuperSynth.rb | 25 ++++++++++++++++--------- plugins/SuperSynth/midi.rb | 10 +++++++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/plugins/SuperSynth/SuperSynth.rb b/plugins/SuperSynth/SuperSynth.rb index 175eac5..d5a7cc8 100644 --- a/plugins/SuperSynth/SuperSynth.rb +++ b/plugins/SuperSynth/SuperSynth.rb @@ -81,7 +81,6 @@ def processReplacing(inputs, outputs, sampleFrames) outBuffer.fill(0,0...sampleFrames) else sampleFrames.times do |i| - # TODO @delta outBuffer[i] = 0.99 * gain * @amp * @osc.tick() end end @@ -101,7 +100,7 @@ def processEvents(events) else log "note on" @silence = false - @osc.freq = Midi.note_to_freq( note ) + @osc.freq = Midi.krystal_freq( note ) # .note_to_freq(note) @amp = velocity / 127.0 @delta = delta # TODO ??? end @@ -123,12 +122,20 @@ def initialize( voicer, synth, opts={} ) end def tick - output = @synth.tick - # stop if @synth.silent? # PERF TODO: move this to a callback in the synth? and/or just return 0 in synth - output + @synth.tick end - def play( note, velocity, delta ) + def ticks samples + if @delta > 0 + @delta -= samples if @delta >= samples + output = Dsp.zeros(@delta) + zeros(@delta) + (@delta+1..samples).map{ @synth.tick } + else + (1..samples).map{ @synth.tick } + end + end + + def play( note, velocity, delta=0 ) @note, @velocity, @delta = note,velocity,delta @synth.freq = Midi.note_to_freq( note ) @synth.trigger(:attack) @@ -138,8 +145,8 @@ def play( note, velocity, delta ) def release @synth.trigger(:release) end - - def stop + + def stop # TODO: call this when synth release envelope stops @voicer.free_voice self end @@ -181,7 +188,7 @@ def ticks samples require 'matrix' voices = active_voices zeros = Vector[ *Dsp.zeros(samples) ] - output = voices.inject( zeros ){ |sum,voice| sum + voice.tick } + output = voices.inject( zeros ){ |sum,voice| sum + voice.ticks(samples) } ( @voice_scaling[ voices.count ] * output ).to_a end diff --git a/plugins/SuperSynth/midi.rb b/plugins/SuperSynth/midi.rb index 6cfbec8..3efdbb6 100644 --- a/plugins/SuperSynth/midi.rb +++ b/plugins/SuperSynth/midi.rb @@ -1,9 +1,8 @@ module Midi extend self - A = 432.0 - def note_to_freq( note ) - A * 2.0**((note-69)/12.0) + def note_to_freq( note, a = 432.0 ) # equal tempered + a * 2.0**((note-69)/12.0) end def process(events) @@ -23,5 +22,10 @@ def process(events) end end + KRYSTAL = [ 256.0, 272.0, 288.0, 305.0, 320.0, 1024.0/3, 360.0, 384.0, 405.0, 432.0, 455.1, 480.0 ] + def krystal_freq( note ) + KRYSTAL[ note % 12 ] * 2.0**( note / 12 - 5 ) + end + end From 97649315988f3aa3f2752a1728abf1fb0c97cddc Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Tue, 1 May 2012 17:11:42 -0700 Subject: [PATCH 05/16] more oscillators --- .rvmrc | 2 +- plugins/SuperSynth/SuperSynth.rb | 98 ------------------------ plugins/SuperSynth/dsp.rb | 34 ++++++++- plugins/SuperSynth/midi.rb | 4 +- plugins/SuperSynth/phasor.rb | 99 ++++++++++++++++++++---- plugins/SuperSynth/polyphonic_synth.rb | 100 +++++++++++++++++++++++++ plugins/SuperSynth/super_saw.rb | 63 ++++++++++++++++ plugins/SuperSynth/tri.rb | 0 8 files changed, 281 insertions(+), 119 deletions(-) create mode 100644 plugins/SuperSynth/polyphonic_synth.rb create mode 100644 plugins/SuperSynth/super_saw.rb create mode 100644 plugins/SuperSynth/tri.rb diff --git a/.rvmrc b/.rvmrc index d88fc14..929cda8 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1 +1 @@ -rvm use jruby +rvm use jruby-1.6.7 diff --git a/plugins/SuperSynth/SuperSynth.rb b/plugins/SuperSynth/SuperSynth.rb index d5a7cc8..8a8726e 100644 --- a/plugins/SuperSynth/SuperSynth.rb +++ b/plugins/SuperSynth/SuperSynth.rb @@ -18,7 +18,6 @@ def initialize(plugin, frame) # Check RubyGain for a more elaborate GUI example end -require 'midi' require 'phasor' class SuperSynth < OpazPlug @@ -110,100 +109,3 @@ def processEvents(events) end end - -class PolyphonicSynth - class Voice - # attr_accessor :note #, :velocity, :delta - - def initialize( voicer, synth, opts={} ) - @voicer = voicer - @synth = synth.new(self, opts) - self - end - - def tick - @synth.tick - end - - def ticks samples - if @delta > 0 - @delta -= samples if @delta >= samples - output = Dsp.zeros(@delta) - zeros(@delta) + (@delta+1..samples).map{ @synth.tick } - else - (1..samples).map{ @synth.tick } - end - end - - def play( note, velocity, delta=0 ) - @note, @velocity, @delta = note,velocity,delta - @synth.freq = Midi.note_to_freq( note ) - @synth.trigger(:attack) - self - end - - def release - @synth.trigger(:release) - end - - def stop # TODO: call this when synth release envelope stops - @voicer.free_voice self - end - - end - - def initialize synth, synth_opts, num_voices=8 - @voice_scaling = (1..num_voices).map{|i| 1.0 / Math.sqrt(i) } # lookup table - @voice_pool = (1..num_voices).map{ Voice.new( self, synth, synth_opts ) } - @notes_playing = {} - end - - def all_notes_off - active_voices.each(&:stop) - @notes_playing = {} - end - - def note_on note, velocity=100, delta=0 - return note_off(note) if velocity.zero? - voice = @notes_playing.delete[ note ] || allocate_voice - @notes_playing[note] = voice.play( note, velocity, delta ) - end - - def note_off note - @notes_playing[note].try(&:release) - end - - def free_voice voice - if voice = @notes_playing.delete(note) # return to pool if playing - @voice_pool << voice - end - end - - def tick - voices = active_voices - @voice_scaling[ voices.count ] * voices.inject(0){ |sum,voice| sum + voice.tick } - end - - def ticks samples - require 'matrix' - voices = active_voices - zeros = Vector[ *Dsp.zeros(samples) ] - output = voices.inject( zeros ){ |sum,voice| sum + voice.ticks(samples) } - ( @voice_scaling[ voices.count ] * output ).to_a - end - - private - - def allocate_voice - @voice_pool.pop || steal_voice - end - - def steal_voice # FIXME: we might need a fade out to prevent clicks here? - active_voices.sort_by(&:delta).first - end - - def active_voices - @notes_playing.values - end - -end \ No newline at end of file diff --git a/plugins/SuperSynth/dsp.rb b/plugins/SuperSynth/dsp.rb index 80c31b6..98fa761 100644 --- a/plugins/SuperSynth/dsp.rb +++ b/plugins/SuperSynth/dsp.rb @@ -1,11 +1,41 @@ +# see also /Users/dfl/projects/dsptest/FastMath.h + module Dsp extend self def noise - 2 * Random.rand - 1 + bipolar( random ) end - + + def random + rand # Random.rand + end + + def bipolar x + 2.0*x - 1.0 + end + + def xfade( a, b, x ) + (b-a)*x + a + end + + def clamp x, min, max + [min, x, max].sort[1] + end + + def lookup_table bits=7 + size = 2 ** bits + scale = 1.0 / size + (1..size).map{|x| yield( scale * x ) } + end + +end + + +module ArrayExtensions def zeros num [].fill(0,0...num) end end + +Array.send :include, ArrayExtensions \ No newline at end of file diff --git a/plugins/SuperSynth/midi.rb b/plugins/SuperSynth/midi.rb index 3efdbb6..8bee468 100644 --- a/plugins/SuperSynth/midi.rb +++ b/plugins/SuperSynth/midi.rb @@ -1,7 +1,7 @@ module Midi extend self - - def note_to_freq( note, a = 432.0 ) # equal tempered + A = 432.0 + def note_to_freq( note, a = A ) # equal tempered a * 2.0**((note-69)/12.0) end diff --git a/plugins/SuperSynth/phasor.rb b/plugins/SuperSynth/phasor.rb index a2f0970..1579401 100644 --- a/plugins/SuperSynth/phasor.rb +++ b/plugins/SuperSynth/phasor.rb @@ -1,8 +1,8 @@ -# TODO: move to java +# TODO: convert to mirah require './midi' require './dsp' -module Tickable +class Generator def tick raise "not implemented!" end @@ -12,21 +12,15 @@ def ticks samples end end -class Oscillator - include Tickable - +class Oscillator < Generator + attr_accessor :freq + def initialize( srate=44.1e3 ) # srate== OpazPlug.sampleRate ) @inv_srate = 1.0 / srate self.freq = Midi::A + self end - def freq - @freq - end - - def freq= freq - raise "not implemented!" - end end class Phasor < Oscillator @@ -35,17 +29,90 @@ class Phasor < Oscillator OFFSET = { true => 0.0, false => 1.0 } # branchless trick from Urs Heckmann def initialize( srate=44.1e3, phase = Dsp.noise ) - super srate @phase = phase + super srate end - + def tick @phase += @inc # increment @phase -= OFFSET[ @phase <= 1.0 ] # wrap end - def freq= freq - @freq = freq + def freq= arg + @freq = arg @inc = @freq * @inv_srate end end + + +class PhasorOscillator < Oscillator + def initialize( srate=44.1e3, phase=0 ) + @phasor = Phasor.new( srate, phase ) + super srate + end + + def freq= arg + @phasor.freq = arg + super + end + def phase= arg + @phasor.phase = Dsp.clamp(arg, 0.0, 1.0) + end + def phase + @phasor.phase + end + + def tock + @phasor.phase.tap{ @phasor.tick } + end +end + +class Tri < PhasorOscillator + FACTOR = { true => 1.0, false => -1.0 } + + def tick + idx = phase < 0.5 + 4*( FACTOR[idx]*tock + Phasor::OFFSET[idx] ) - 1 + end +end + +class Pulse < PhasorOscillator + FACTOR = { true => 1.0, false => -1.0 } + + def initialize( srate=44.1e3, phase=0 ) + super + @duty = 0.5 + end + def duty= arg + @duty = Dsp.clamp(arg, 0.0, 1.0) + end + + def tick + FACTOR[ tock <= @duty ] + end +end + +class RpmSaw < PhasorOscillator + TWO_PI = 2.0*Math::PI + + def initialize( srate=44.1e3, phase=0 ) + super + @beta = 1.0 + @state = @last_out = 0 + end + def beta= arg + @beta = Dsp.clamp(arg, 0.0, 2.0) + end + + def tick + @state = 0.5*(@state + @last_out) # one-pole averager + @last_out = Math.sin( TWO_PI * tock + @beta * @state ) + end +end + +class RpmSquare < RpmSaw + def tick + @state = 0.5*(@state + @last_out*@last_out) # one-pole averager, squared + @last_out = Math.sin( TWO_PI * tock - @beta * @state ) + end +end \ No newline at end of file diff --git a/plugins/SuperSynth/polyphonic_synth.rb b/plugins/SuperSynth/polyphonic_synth.rb new file mode 100644 index 0000000..6374484 --- /dev/null +++ b/plugins/SuperSynth/polyphonic_synth.rb @@ -0,0 +1,100 @@ +require 'matrix' +require './dsp' +require './midi' +require './phasor' + +class PolyphonicSynth + class Voice + # attr_accessor :note #, :velocity, :delta + + def initialize( voicer, synth, opts={} ) + @voicer = voicer + @synth = synth.new(opts[:srate]) #, opts TODO: pass self for stop callback + self + end + + def tick + @synth.tick + end + + def ticks samples + if @delta > 0 + @delta -= samples if @delta >= samples + output = Array.zeros(@delta) + zeros(@delta) + (@delta+1..samples).map{ @synth.tick } + else + (1..samples).map{ @synth.tick } + end + end + + def play( note, velocity, delta=0 ) + @note, @velocity, @delta = note,velocity,delta + @synth.freq = Midi.note_to_freq( note ) + @synth.trigger(:attack) + self + end + + def release + @synth.trigger(:release) + end + + def stop # TODO: call this when synth release envelope stops + @voicer.free_voice self + end + end + attr_accessor :srate + def initialize synth, num_voices=8, synth_opts={} + synth_opts = {:srate => 44.1e3}.merge!( synth_opts ) + @voice_scaling = (1..num_voices).map{|i| 1.0 / Math.sqrt(i) } # lookup table + @voice_pool = (1..num_voices).map{ Voice.new( self, synth, synth_opts ) } + @notes_playing = {} + end + + def all_notes_off + active_voices.each(&:stop) + @notes_playing = {} + end + + def note_on note, velocity=100, delta=0 + return note_off(note) if velocity.zero? + voice = @notes_playing.delete[ note ] || allocate_voice + @notes_playing[note] = voice.play( note, velocity, delta ) + end + + def note_off note + @notes_playing[note].try(&:release) + end + + def free_voice voice + if voice = @notes_playing.delete(note) # return to pool if playing + @voice_pool << voice + end + end + + def tick + voices = active_voices + @voice_scaling[ voices.count ] * voices.inject(0){ |sum,voice| sum + voice.tick } + end + + def ticks samples + voices = active_voices + zeros = Vector[ *Array.zeros(samples) ] + output = voices.inject( zeros ){ |sum,voice| sum + voice.ticks(samples) } + ( @voice_scaling[ voices.count ] * output ).to_a + end + + private + + def allocate_voice + @voice_pool.pop || steal_voice + end + + def steal_voice # FIXME: we might need a fade out to prevent clicks here? + active_voices.sort_by(&:delta).first + end + + def active_voices + @notes_playing.values + end + +end \ No newline at end of file diff --git a/plugins/SuperSynth/super_saw.rb b/plugins/SuperSynth/super_saw.rb new file mode 100644 index 0000000..72ed2e0 --- /dev/null +++ b/plugins/SuperSynth/super_saw.rb @@ -0,0 +1,63 @@ +require './phasor' + +class SuperSaw < Oscillator + def initialize srate=44.1e3, num=7 + @master = Phasor.new(srate) + @phasors = (1..num-1).map{ Phasor.new(srate) } + setup_tables + @phat = 12/127.0 # default knob + # self.freq = Midi::A + # self + super srate + end + + def freq= f + @freq = @master.freq = f + @phasors.each_with_index{ |p,i| p.freq = f + @@detune[@phat] * @@offsets[i] } + end + + def tick + @@center[ @phat ] * @master.tick + + @@side[ @phat ] * @phasors.inject(0){|sum,p| sum + p.tick } + end + + private + + def setup_tables + @@offsets ||= [ -0.11002313, -0.06288439, -0.01952356, 0.01991221, 0.06216538, 0.10745242 ] + @@detune ||= Dsp.lookup_table{|x| calc_detune(x) } + @@side ||= Dsp.lookup_table{|x| calc_side(x) } + @@center ||= Dsp.lookup_table{|x| calc_center(x) } + end + + def calc_detune x + 1.0 - (1.0-x) ** 0.2 + end + + def calc_side x + -0.73754*x*x + 1.2841*x + 0.044372 + end + + def calc_center x + -0.55366*x + 0.99785 + end + +end + + +# require 'wavefile' +# s = SuperSaw.new +# cycle = s.ticks( 10000 ) +# +# include WaveFile +# +# format = Format.new(:mono, 16, 44100) +# writer = Writer.new("super.wav", format) +# +# # Write a 1 second long 440Hz square wave +# buffer = Buffer.new(cycle, format) +# 220.times do +# writer.write(buffer) +# end +# +# writer.close() diff --git a/plugins/SuperSynth/tri.rb b/plugins/SuperSynth/tri.rb new file mode 100644 index 0000000..e69de29 From a6277784a3847dc71fb8ec2cbf48549aabdbc697 Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Wed, 2 May 2012 02:17:08 -0700 Subject: [PATCH 06/16] HPF on SuperSaw --- plugins/SuperSynth/dsp.rb | 22 +++++- plugins/SuperSynth/phasor.rb | 14 ++-- plugins/SuperSynth/super_saw.rb | 128 ++++++++++++++++++++++++++++++-- 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/plugins/SuperSynth/dsp.rb b/plugins/SuperSynth/dsp.rb index 98fa761..e87e15f 100644 --- a/plugins/SuperSynth/dsp.rb +++ b/plugins/SuperSynth/dsp.rb @@ -1,6 +1,12 @@ # see also /Users/dfl/projects/dsptest/FastMath.h +require 'matrix' module Dsp + TWO_PI = 2.0*Math::PI + SQRT2_2 = Math.sqrt(2) / 2 + # INV_SQRT2 = 1.0 / Math.sqrt(2) + + extend self def noise @@ -33,9 +39,21 @@ def lookup_table bits=7 module ArrayExtensions + def full_of(val,num) + [].fill(val,0...num) + end + def zeros num - [].fill(0,0...num) + full_of(0,num) end end -Array.send :include, ArrayExtensions \ No newline at end of file +Array.send :extend, ArrayExtensions + +# module VectorExtensions +# def full_of(val,num) +# Vector[ *Array.full_of(val,num) ] +# end +# end +# +# Vector.send :extend, VectorExtensions \ No newline at end of file diff --git a/plugins/SuperSynth/phasor.rb b/plugins/SuperSynth/phasor.rb index 1579401..8f4aa88 100644 --- a/plugins/SuperSynth/phasor.rb +++ b/plugins/SuperSynth/phasor.rb @@ -10,13 +10,18 @@ def tick def ticks samples (1..samples).map{ tick } end + + def initialize srate=44.1e3 + @inv_srate = 1.0 / srate + self + end end class Oscillator < Generator attr_accessor :freq def initialize( srate=44.1e3 ) # srate== OpazPlug.sampleRate ) - @inv_srate = 1.0 / srate + super self.freq = Midi::A self end @@ -93,26 +98,25 @@ def tick end class RpmSaw < PhasorOscillator - TWO_PI = 2.0*Math::PI - def initialize( srate=44.1e3, phase=0 ) super @beta = 1.0 @state = @last_out = 0 end + def beta= arg @beta = Dsp.clamp(arg, 0.0, 2.0) end def tick @state = 0.5*(@state + @last_out) # one-pole averager - @last_out = Math.sin( TWO_PI * tock + @beta * @state ) + @last_out = Math.sin( Dsp::WO_PI * tock + @beta * @state ) end end class RpmSquare < RpmSaw def tick @state = 0.5*(@state + @last_out*@last_out) # one-pole averager, squared - @last_out = Math.sin( TWO_PI * tock - @beta * @state ) + @last_out = Math.sin( Dsp::TWO_PI * tock - @beta * @state ) end end \ No newline at end of file diff --git a/plugins/SuperSynth/super_saw.rb b/plugins/SuperSynth/super_saw.rb index 72ed2e0..38b4d94 100644 --- a/plugins/SuperSynth/super_saw.rb +++ b/plugins/SuperSynth/super_saw.rb @@ -1,4 +1,112 @@ require './phasor' +#require './dsp' + +class Processor + def tick(s) + raise "not implemented!" + end + + def ticks inputs + inputs.map{|s| tick(s) } + end + + def initialize srate=44.1e3 + @srate = srate + @inv_srate = 1.0 / @srate + self + end +end + +class Biquad < Processor + def initialize( srate=44.1e3, a=[1.0,0,0], b=[1.0,0,0], norm=false ) + update( Vector[*a], Vector[*b] ) + normalize if norm + clear + super srate + end + + def clear + @input = @output = [0,0,0] + @_a = @_b = @a = @b + end + + def update a, b + @_a,@_b = @a,@b + @a,@b = a,b + interpolate if interpolating? + end + + def normalize + inv_a0 = 1.0/a0 + @a *= inv_a0 + @b *= inv_a0 + end + + def interpolate # TODO: interpolate over VST sample frame ? + @interp_period = (@srate * 1e-3).floor # 1ms + t = 1.0 / @interp_period + @delta_a = (@a - @_a) * t + @delta_b = (@b - @_b) * t + @interp_ticks = 0 + end + + def interpolating? + @_a && @_b + end + + def tick input + if interpolating? + @_a += @delta_a + @_b += @delta_b + process( input, @_a, @_b ).tap do + @_a = _b = nil if (@interp_ticks += 1) >= @interp_period + end + else + process( input, @a, @b ) + end + end + + def process input, a, b + @input[0] = a[0] * input + output = b[0] * @input[0] + b[1] * @input[1] + b[2] * @input[2] + output -= a[2] * @output[2] + a[1] * @output[1] + @input[2] = @input[1] + @input[1] = @input[0] + @output[2] = @output[1] + @output[1] = @output[0] + end +end + +class Hpf < Biquad + def initialize( srate, f, qq=Dsp::SQRT2_2 ) + @inv_q = 1.0 / qq + freq = f # triggers recalc + super srate + end + + def q= arg + @inv_q = 1.0 / arg + recalc + end + + def freq= arg + @w = arg * Dsp::TWO_PI * @inv_srate + recalc + end + + def recalc # from RBJ cookbook @ http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt + alpha = 0.5 * @inv_q * Math.sin(@w) + cw = Math.cos(@w) + ocw = 1+cw + b0 = b2 = 0.5*ocw + b1 = -ocw + a0 = 1 + alpha + a1 = -2.0*cw + a2 = 1 - alpha + update( Vector[a0, a1, a2], Vector[b0, b1, b2] ) + end + +end class SuperSaw < Oscillator def initialize srate=44.1e3, num=7 @@ -6,19 +114,29 @@ def initialize srate=44.1e3, num=7 @phasors = (1..num-1).map{ Phasor.new(srate) } setup_tables @phat = 12/127.0 # default knob - # self.freq = Midi::A - # self + @hpf = Hpf.new( srate, @master.freq ) super srate end + def clear + @hpf.clear + end + def freq= f - @freq = @master.freq = f + @hpf.freq = @master.freq = @freq = f @phasors.each_with_index{ |p,i| p.freq = f + @@detune[@phat] * @@offsets[i] } end def tick - @@center[ @phat ] * @master.tick + - @@side[ @phat ] * @phasors.inject(0){|sum,p| sum + p.tick } + osc = @@center[ @phat ] * @master.tick + osc += @@side[ @phat ] * @phasors.inject(0){|sum,p| sum + p.tick } + @hpf.tick( osc ) + end + + def ticks samples + osc = @@center[ @phat ] * Vector[*@master.ticks(samples)] + osc += @@side[ @phat ] * @phasors.inject( osc ){|sum,p| sum + Vector[*p.ticks(samples)] } + @hpf.ticks( osc.to_a ) end private From 58d4e3a1453270d8b3ee4fc03b23483ccf0667b5 Mon Sep 17 00:00:00 2001 From: David Lowenfels Date: Wed, 2 May 2012 13:49:37 -0700 Subject: [PATCH 07/16] wave output --- plugins/SuperSynth/RAFL_wav.rb | 494 ++++++++++++++++++++++++++++++++ plugins/SuperSynth/dsp.rb | 51 +++- plugins/SuperSynth/phasor.rb | 50 ++-- plugins/SuperSynth/super_saw.rb | 136 +++++---- 4 files changed, 648 insertions(+), 83 deletions(-) create mode 100644 plugins/SuperSynth/RAFL_wav.rb diff --git a/plugins/SuperSynth/RAFL_wav.rb b/plugins/SuperSynth/RAFL_wav.rb new file mode 100644 index 0000000..c5290c5 --- /dev/null +++ b/plugins/SuperSynth/RAFL_wav.rb @@ -0,0 +1,494 @@ +#!/usr/bin/env ruby -wKU + +# +# RAFL_wav.rb by Aaron Cohen +# +# RAFL - Ruby Audio File Library +# A means of reading, analyzing, and generating audio files from within Ruby +# +# This library is licensed under the LGPL. See COPYING.LESSER. +# +# +# TODO: +# Fix Sine Wave Generator - experiencing significant rounding error +# BEXT writing +# Fix Peak & RMS calculation on 24 bit audio + +require 'rexml/document' +include REXML + +class RiffFile + + VALID_RIFF_TYPES = [ 'WAVE' ] + HEADER_PACK_FORMAT = "A4V" + AUDIO_PACK_FORMAT_16 = "s*" + + attr_accessor :format, :found_chunks, :bext_meta, :ixml_meta, :raw_audio_data + + def initialize(file, mode) + @file = File.open(file, mode) + @file.binmode + if mode == 'r' + read_chunks if riff? && VALID_RIFF_TYPES.include?(riff_type) + end + if block_given? + yield self + close + end + end + + def close + @file.close + end + + def riff? + @file.seek(0) + riff, @file_end = read_chunk_header + return true if riff == 'RIFF' + end + + def riff_type + if riff? + @file.seek(8) + @file.read(4) + end + end + + def read_chunks + @found_chunks = [] + while @file.tell < @file_end + @file.seek(@file.tell + @file.tell % 2) #adds padding if last chunk was an odd length + chunk_name, chunk_length = read_chunk_header + chunk_position = @file.tell + identify_chunk(chunk_name, chunk_position, chunk_length) + @file.seek(chunk_position + chunk_length) + end + end + + def read_chunk_header + @file.read(8).unpack(HEADER_PACK_FORMAT) + end + + def identify_chunk(chunk_name, chunk_position, chunk_length) + @found_chunks << chunk_name + case chunk_name + when 'fmt' then process_fmt_chunk(chunk_position, chunk_length) + when 'bext' then process_bext_chunk(chunk_position, chunk_length) + when 'data' then process_data_chunk(chunk_position, chunk_length) + when 'iXML' then process_ixml_chunk(chunk_position, chunk_length) + end + end + + def process_fmt_chunk(chunk_position, chunk_length) + @file.seek(chunk_position) + @format = WaveFmtChunk.new(@file.read(chunk_length)) + end + + def process_bext_chunk(chunk_position, chunk_length) + @file.seek(chunk_position) + @bext_meta = BextChunk.new(@file.read(chunk_length)) + end + + def process_data_chunk(chunk_position, chunk_length) + @data_begin, @data_end = chunk_position, chunk_position + chunk_length + #@file.seek(chunk_position) + #@raw_audio_data = [] + #for sample in (0..total_samples) + # @raw_audio_data << read_sample(sample) + #end + end + + def process_ixml_chunk(chunk_position, chunk_length) + @file.seek(chunk_position) + @ixml_meta = IxmlChunk.new(@file.read(chunk_length)) + end + + def unpack_samples(samples, bit_depth) + if bit_depth == 24 + #return samples.scan(/.../).map {|s| (s.reverse + 0.chr ).unpack("V")}.flatten + return samples.scan(/.../).map {|s| (s + 0.chr).unpack("V")}.flatten + else + return samples.unpack(AUDIO_PACK_FORMAT_16) + end + end + + def pack_samples(samples, bit_depth) + if bit_depth == 24 + return samples.map { |s| [s].pack("VX") }.join + else + return samples.pack(AUDIO_PACK_FORMAT_16) + end + end + + def read_sample(sample_number, channel) #returns an individual sample value + + @file.seek(@data_begin + (sample_number * @format.block_align) + (channel * (@format.block_align / @format.num_channels))) + + return @file.read(@format.block_align / @format.num_channels) + + #return sample_array + end + + def simple_read #returns all sample values for entire file + @file.seek(@data_begin) + #@file.read(@data_end - @data_begin).unpack(@audio_pack_format)#.join.to_i + unpack_samples(@file.read(@data_end - @data_begin), @format.bit_depth) + end + + def read_samples_by_channel(channel) #returns all of the sample values for a single audio channel + samples = [] + for sample in (0..total_samples) + samples << read_sample(sample, channel) + end + #samples.collect! { |samp| sample.unpack(@audio_pack_format).join.to_i } #bug, should be samp.unpack? + samples.collect! { |samp| unpack_samples(samp, @format.bit_depth).join.to_i } + return samples + end + + def write(channels, sample_rate, bit_depth, audio_data) #writes to audio file + write_riff_type + write_fmt_chunk(channels, sample_rate, bit_depth) + write_data_chunk audio_data #channels==1 ? [[*audio_data]] : audio_data + write_riff_header + end + + def write_riff_header + @file.seek(0) + @file.print(["RIFF", @file_end].pack(HEADER_PACK_FORMAT)) + end + + def write_riff_type + @file.seek(8) + @file.print(["WAVE"].pack("A4")) + end + + def write_fmt_chunk(num_channels, sample_rate, bit_depth) + @file.seek(12) + @write_format = WaveFmtChunk.new + @write_format.audio_format = 1 + @write_format.num_channels = num_channels + @write_format.bit_depth = bit_depth + @write_format.set_sample_rate(sample_rate) + + @file.print(["fmt ", 16].pack(HEADER_PACK_FORMAT)) # the 16 here means PCM, not bit depth + @file.print(@write_format.pack_header_data) + end + + def write_data_chunk(audio_data) + data_chunk_begin = @file.tell + @file.seek(8, IO::SEEK_CUR) + @data_begin = @file.tell + + #interleave arrays + if @write_format.num_channels > 1 && audio_data.length > 1 + interleaved_audio_data = audio_data[0].zip(*audio_data[1..-1]).flatten + else + interleaved_audio_data = audio_data[0] + end + + puts interleaved_audio_data.length + interleaved_audio_data.each_index do |sample| + if interleaved_audio_data[sample].nil? + puts "Sample number #{sample.to_s} is nil" + end + end + + @file.print(pack_samples(interleaved_audio_data, @write_format.bit_depth)) + @data_end = @file.tell + @file_end = @file.tell + @file.seek(data_chunk_begin) + @file.print(["data", @data_end - @data_begin].pack(HEADER_PACK_FORMAT)) + end + + def duration + (@data_end - @data_begin) / @format.byte_rate + end + + def total_samples + (@data_end - @data_begin) / @format.block_align + end +end + +class WaveFmtChunk + + attr_accessor :audio_format, :num_channels, + :sample_rate, :byte_rate, + :block_align, :bit_depth + + PACK_FMT = "vvVVvv" + + def initialize(*binary_data) + unpack_header_data(binary_data[0]) if !binary_data.empty? + end + + def unpack_header_data(binary_data) + @audio_format, @num_channels, + @sample_rate, @byte_rate, + @block_align, @bit_depth = binary_data.unpack(PACK_FMT) + end + + def pack_header_data + [ @audio_format, @num_channels, + @sample_rate, @byte_rate, + @block_align, @bit_depth ].pack(PACK_FMT) + end + + def set_sample_rate(rate) + @byte_rate = calc_byte_rate(rate) + @sample_rate = rate + @block_align = calc_block_align + end + + def calc_block_align + @num_channels * (@bit_depth) + end + + def calc_byte_rate(sample_rate, num_channels = @num_channels, bit_depth = @bit_depth) + sample_rate * num_channels * (bit_depth / 8) + end + +end + +class BextChunk + + attr_accessor :description, :originator, :originator_reference, :origination_date, :origination_time, + :time_reference, :version, :umid, :reserved, :coding_history + + PACK_FMT = "A256A32A32A10A8QvB64A190M*" + + def initialize(*binary_data) + unpack_bext_data(binary_data[0]) if !binary_data.empty? + end + + def unpack_bext_data(binary_data) + @description, @originator, @originator_reference, @origination_date, @origination_time, + @time_reference, @version, @umid, @reserved, @coding_history = binary_data.unpack(PACK_FMT) + end + + def calc_time_offset(sample_rate) + time = @time_reference / sample_rate + return [time/3600, time/60 % 60, time % 60].map{|t| t.to_s.rjust(2,'0')}.join(':') + end +end + +class IxmlChunk + #see http://www.gallery.co.uk/ixml/object_Details.html + + attr_accessor :raw_xml#, :ixml_version, +# :project, :scene, :take, :tape, :circled, :no_good, :false_start, +# :wild_track, :file_uid, :ubits, :note, +# +# :sync_point_count, :sync_points, +# +# :speed_note, :speed_master, :speed_current, :speed_timecode_rate, :speed_timecode_flag, +# :speed_file_sample_rate, :speed_bit_depth, :speed_digitizer_sample_rate, +# :speed_timestamp_samples_since_midnight, :speed_timestamp_sample_rate, +# +# :history_original_filename, :history_parent_filename, :history_parent_uid, +# +# :fileset_total_files, :fileset_family_uid, :fileset_family_name, :fileset_index, +# +# :tracklist_count, :tracklist_tracks, +# +# :dep_pre_record_samplecount, +# +# :bwf_description, :bwf_originator, :bwf_originator_reference, :bwf_origination_date, +# :bwf_origination_time, :bwf_time_reference, :bwf_version, :bwf_umid, :bwf_reserved, :bwf_coding_history, +# +# :user + + PACK_FMT = "a*" + + def initialize(*binary_data) + unpack_ixml_data(binary_data[0]) if !binary_data.empty? + end + + def unpack_ixml_data(binary_data) + @raw_xml = Document.new(binary_data.unpack(PACK_FMT)[0]) + #read_xml_values + end + + def read_xml_values #not in use, looking for a better way + bwfxml = @raw_xml.elements["BWFXML"] + + @ixml_version = bwfxml.elements["IXML_VERSION"].text + @project = bwfxml.elements["PROJECT"].text + @scene = bwfxml.elements["SCENE"].text + @take = bwfxml.elements["TAKE"].text + @tape = bwfxml.elements["TAPE"].text + @circled = bwfxml.elements["CIRCLED"].text + @file_uid = bwfxml.elements["FILE_UID"].text + @ubits = bwfxml.elements["UBITS"].text + @note = bwfxml.elements["NOTE"].text + + @speed_note = bwfxml.elements["SPEED"].elements["NOTE"].text + @speed_master = bwfxml.elements["SPEED"].elements["MASTER_SPEED"].text + @speed_current = bwfxml.elements["SPEED"].elements["CURRENT_SPEED"].text + @speed_timecode_rate = bwfxml.elements["SPEED"].elements["TIMECODE_RATE"].text + @speed_timecode_flag = bwfxml.elements["SPEED"].elements["TIMECODE_FLAG"].text + + @history_original_filename = bwfxml.elements["HISTORY"].elements["ORIGINAL_FILENAME"].text + @history_parent_filename = bwfxml.elements["HISTORY"].elements["PARENT_FILENAME"].text + @history_parent_uid = bwfxml.elements["HISTORY"].elements["PARENT_UID"].text + + @fileset_total_files = bwfxml.elements["FILE_SET"].elements["TOTAL_FILES"].text + @fileset_family_uid = bwfxml.elements["FILE_SET"].elements["FAMILY_UID"].text + @fileset_family_name = bwfxml.elements["FILE_SET"].elements["FAMILY_NAME"].text + @fileset_index = bwfxml.elements["FILE_SET"].elements["FILE_SET_INDEX"].text + + @tracklist_count, @tracklist_tracks = read_tracklist + + @bwf_description = bwfxml.elements["BEXT"].elements["BWF_DESCRIPTION"].text + @bwf_originator = bwfxml.elements["BEXT"].elements["BWF_ORIGINATOR"].text + @bwf_originator_reference = bwfxml.elements["BEXT"].elements["BWF_ORIGINATOR_REFERENCE"].text + @bwf_origination_date = bwfxml.elements["BEXT"].elements["BWF_ORIGINATION_DATE"].text + @bwf_origination_time = bwfxml.elements["BEXT"].elements["BWF_ORIGINATION_TIME"].text + @bwf_time_reference = bwfxml.elements["BEXT"].elements["BWF_TIME_REFERENCE_HIGH"].text + bwfxml.elements["BEXT"].elements["BWF_TIME_REFERENCE_LOW"].text + @bwf_version = bwfxml.elements["BEXT"].elements["BWF_VERSION"].text + @bwf_umid = bwfxml.elements["BEXT"].elements["BWF_UMID"].text + @bwf_reserved = bwfxml.elements["BEXT"].elements["BWF_RESERVED"].text + @bwf_coding_history = bwfxml.elements["BEXT"].elements["BWF_CODING_HISTORY"].text + + @user = bwfxml.elements["USER"].text + + #iXML 1.52 + if @ixml_version >= "1.52" + @no_good = bwfxml.elements["NO_GOOD"].text + @false_start = bwfxml.elements["FALSE_START"].text + @wild_track = bwfxml.elements["WILD_TRACK"].text + + #iXML 1.5 + elsif @ixml_version >= "1.5" + #iXML Readers should ignore this information and instead take the data from the official fmt and bext chunks in the file + #Generic utilities changing file data might change the fmt or bxt chunk but not update the iXML SPEED tag, and for EBU officially specified BWF data like timestamp, the EBU data takes precedence (unlike the unofficial informal bext metadata which is superceded by iXML) + @speed_file_sample_rate = bwfxml.elements["SPEED"].elements["FILE_SAMPLE_RATE"].text + @speed_bit_depth = bwfxml.elements["SPEED"].elements["AUDIO_BIT_DEPTH"].text + @speed_digitizer_sample_rate = bwfxml.elements["SPEED"].elements["DIGITIZER_SAMPLE_RATE"].text + @speed_timestamp_samples_since_midnight = bwfxml.elements["SPEED"].elements["TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_HI"].text + bwfxml.elements["SPEED"].elements["TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_HI"].text + @speed_timestamp_sample_rate = bwfxml.elements["SPEED"].elements["TIMESTAMP_SAMPLE_RATE"].text + + #iXML 1.4 + elsif @ixml_version >= "1.4" + @sync_point_count, @sync_points = read_sync_points + + #iXML 1.3 + elsif @ixml_version == "1.3" + @dep_pre_record_samplecount = read_pre_record_samplecount + end + end + + def read_sync_points + #TODO + end + + def read_tracklist + #TODO + end + + def read_pre_record_samplecount + #TODO + end + + def translate_frame_rates(frame_rate) + #TODO + end +end + + +##################### +# Standalone methods +##################### + +def calc_rms(audio_samples, format) + #squaresum = 0 + #audio_samples.each { |value| squaresum += (value ** 2) } + #sample_rms = Math.sqrt(squaresum / audio_samples.length) + sample_rms = Math.sqrt((audio_samples.inject { |sum, item| sum + (item ** 2) }) / audio_samples.length) + + calc_dbfs(sample_rms, format.bit_depth) +end + +def calc_peak(audio_samples, format) + calc_dbfs(audio_samples.max.to_f, format.bit_depth) +end + +def calc_dbfs(sample_value, bit_depth) + range = (2 ** bit_depth) / 2 + (20*Math.log10(sample_value.to_f / range)).round_to(2) +end + +def calc_sample_value(dbfs_value, bit_depth) + range = (2 ** bit_depth / 2) + (range * Math::E ** (1/20.0 * dbfs_value * (Math.log(2) + Math.log(5)))) - 1 +end + +def generate_white_noise(length_secs, peak_db, sample_rate, bit_depth) + num_samples = (length_secs * sample_rate).to_i + peak_samples = calc_sample_value(peak_db, bit_depth) + output = [] + num_samples.times do + output << (rand(65536) - 32768) * peak_samples + end + return output +end + +def generate_pink_noise(length_secs, peak_db, sample_rate, bit_depth) + num_samples = (length_secs * sample_rate).to_i + peak_samples = calc_sample_value(peak_db, bit_depth) + output = [] + amplitude_scaling = [3.8024, 2.9694, 2.5970, 3.0870, 3.4006] + update_probability = [0.00198, 0.01280, 0.04900, 0.17000, 0.68200] + probability_sum = [0.00198, 0.01478, 0.06378, 0.23378, 0.91578] + + contributed_values = [0, 0, 0, 0, 0] + + num_samples.times do + + ur1 = rand + 5.times do |stage| + if ur1 <= probability_sum[stage] + ur2 = rand + contributed_values[stage] = 2 * (ur2 - 0.5) * amplitude_scaling[stage] + break + end + end + + sample = contributed_values.inject(0){|sum,item| sum + item} + + output << sample + end + + scale_amount = peak_samples / output.max + output.map! { |item| (item * scale_amount).round_to(0).to_i } + + return output +end + +def generate_sine_wave(length_secs, peak_db, freq, sample_rate, bit_depth) #defective...drifts over time - rounding error? + peak_samples = calc_sample_value(peak_db, bit_depth) + output = [] + period = 1.0 / freq + angular_freq = (2 * Math::PI) / period + + time = 0 + while time <= length_secs do + output << (Math.sin(angular_freq * time) * peak_samples).round_to(0).to_i + time += (1.0 / sample_rate) + end + return output.slice(0..-2) #kludge...need to pick this apart to find extra sample +end + + +class Float + def round_to(x) + (self * 10**x).round.to_f / 10**x + end + + def ceil_to(x) + (self * 10**x).ceil.to_f / 10**x + end + + def floor_to(x) + (self * 10**x).floor.to_f / 10**x + end +end diff --git a/plugins/SuperSynth/dsp.rb b/plugins/SuperSynth/dsp.rb index e87e15f..750ff46 100644 --- a/plugins/SuperSynth/dsp.rb +++ b/plugins/SuperSynth/dsp.rb @@ -1,11 +1,12 @@ # see also /Users/dfl/projects/dsptest/FastMath.h require 'matrix' +require './RAFL_wav' module Dsp + PI_2 = 0.5*Math::PI TWO_PI = 2.0*Math::PI - SQRT2_2 = Math.sqrt(2) / 2 - # INV_SQRT2 = 1.0 / Math.sqrt(2) - + SQRT2 = Math.sqrt(2) + SQRT2_2 = 0.5*Math.sqrt(2) extend self @@ -29,10 +30,31 @@ def clamp x, min, max [min, x, max].sort[1] end - def lookup_table bits=7 - size = 2 ** bits - scale = 1.0 / size - (1..size).map{|x| yield( scale * x ) } + def to_wav( gen, seconds, filename=nil ) + filename ||= "#{gen.class}.wav" + filename += ".wav" unless filename =~ /\.wav^/i + RiffFile.new(filename,"wb+") do |wav| + data = gen.ticks( gen.sampleRate * seconds ) + rescale = calc_sample_value(-0.5, 16) / data.max # normalize to -0.5dBfs + data.map!{|d| (d*rescale).round.to_f.to_i } + wav.write(1, gen.sampleRate, 16, [data] ) + end + end + + class LookupTable # linear interpolated, input goes from 0 to 1 + def initialize bits=7 + @size = 2 ** bits + scale = 1.0 / (@size) + @table = (0..@size).map{|x| yield( scale * x ) } + end + + def []( arg ) # from 0 to 1 + offset = arg * @size + idx = offset.floor + frac = offset - idx + return @table.last if idx >= @size + Dsp.xfade( @table[idx], @table[idx+1], frac ) + end end end @@ -56,4 +78,17 @@ def zeros num # end # end # -# Vector.send :extend, VectorExtensions \ No newline at end of file +# Vector.send :extend, VectorExtensions + + +# test LUT +# require './dsp' +# +# def calc_detune x +# 1.0 - (1.0-x) ** 0.2 +# end +# +# @@detune = Dsp::LookupTable.new{|x| calc_detune(x) } +# +# @@detune[0] +# @@detune[1] diff --git a/plugins/SuperSynth/phasor.rb b/plugins/SuperSynth/phasor.rb index 8f4aa88..e38a193 100644 --- a/plugins/SuperSynth/phasor.rb +++ b/plugins/SuperSynth/phasor.rb @@ -2,7 +2,26 @@ require './midi' require './dsp' -class Generator +class AudioDSP + @@srate = 44.1e3 + @@inv_srate = 1.0 / @@srate + + def self.sampleRate + @@srate + end + + def self.sampleRate= srate + @@srate = srate + @@inv_srate = 1.0 / @@srate + end + + def sampleRate + @@srate + end + +end + +class Generator < AudioDSP def tick raise "not implemented!" end @@ -10,22 +29,16 @@ def tick def ticks samples (1..samples).map{ tick } end - - def initialize srate=44.1e3 - @inv_srate = 1.0 / srate - self - end end class Oscillator < Generator attr_accessor :freq + DEFAULT_FREQ = Midi::A / 2 - def initialize( srate=44.1e3 ) # srate== OpazPlug.sampleRate ) - super - self.freq = Midi::A + def initialize freq=DEFAULT_FREQ + self.freq = freq self end - end class Phasor < Oscillator @@ -33,9 +46,9 @@ class Phasor < Oscillator OFFSET = { true => 0.0, false => 1.0 } # branchless trick from Urs Heckmann - def initialize( srate=44.1e3, phase = Dsp.noise ) + def initialize( freq = DEFAULT_FREQ, phase = Dsp.noise ) @phase = phase - super srate + super freq end def tick @@ -45,13 +58,13 @@ def tick def freq= arg @freq = arg - @inc = @freq * @inv_srate + @inc = @freq * @@inv_srate end end class PhasorOscillator < Oscillator - def initialize( srate=44.1e3, phase=0 ) + def initialize( freq = DEFAULT_FREQ, phase=0 ) @phasor = Phasor.new( srate, phase ) super srate end @@ -84,10 +97,11 @@ def tick class Pulse < PhasorOscillator FACTOR = { true => 1.0, false => -1.0 } - def initialize( srate=44.1e3, phase=0 ) - super + def initialize( freq=DEFAULT_FREQ, phase=0 ) @duty = 0.5 + super end + def duty= arg @duty = Dsp.clamp(arg, 0.0, 1.0) end @@ -98,10 +112,10 @@ def tick end class RpmSaw < PhasorOscillator - def initialize( srate=44.1e3, phase=0 ) - super + def initialize( freq=MIDI::A, phase=0 ) @beta = 1.0 @state = @last_out = 0 + super end def beta= arg diff --git a/plugins/SuperSynth/super_saw.rb b/plugins/SuperSynth/super_saw.rb index 38b4d94..7242625 100644 --- a/plugins/SuperSynth/super_saw.rb +++ b/plugins/SuperSynth/super_saw.rb @@ -1,7 +1,7 @@ require './phasor' #require './dsp' -class Processor +class Processor < AudioDSP def tick(s) raise "not implemented!" end @@ -10,24 +10,20 @@ def ticks inputs inputs.map{|s| tick(s) } end - def initialize srate=44.1e3 - @srate = srate - @inv_srate = 1.0 / @srate - self - end end class Biquad < Processor - def initialize( srate=44.1e3, a=[1.0,0,0], b=[1.0,0,0], norm=false ) + def initialize( a=[1.0,0,0], b=[1.0,0,0], norm=false ) update( Vector[*a], Vector[*b] ) normalize if norm clear - super srate + self end def clear - @input = @output = [0,0,0] - @_a = @_b = @a = @b + @input = [0,0,0] + @output = [0,0,0] + stop_interpolation end def update a, b @@ -36,14 +32,18 @@ def update a, b interpolate if interpolating? end - def normalize + def normalize # what about b0 (gain) inv_a0 = 1.0/a0 @a *= inv_a0 @b *= inv_a0 end + def stop_interpolation + @_a = @_b = nil + end + def interpolate # TODO: interpolate over VST sample frame ? - @interp_period = (@srate * 1e-3).floor # 1ms + @interp_period = (@@srate * 1e-3).floor # 1ms t = 1.0 / @interp_period @delta_a = (@a - @_a) * t @delta_b = (@b - @_b) * t @@ -55,33 +55,63 @@ def interpolating? end def tick input - if interpolating? + if interpolating? # process with interpolated state @_a += @delta_a @_b += @delta_b process( input, @_a, @_b ).tap do - @_a = _b = nil if (@interp_ticks += 1) >= @interp_period + stop_interpolation if (@interp_ticks += 1) >= @interp_period end else - process( input, @a, @b ) + process( input ) end end - def process input, a, b - @input[0] = a[0] * input - output = b[0] * @input[0] + b[1] * @input[1] + b[2] * @input[2] - output -= a[2] * @output[2] + a[1] * @output[1] + def process input, a=@a, b=@b # default to normal state + output = a[0]*input + a[1]*@input[1] + a[2]*@input[2] + output -= b[1]*@output[1] + b[2]*@output[2] @input[2] = @input[1] - @input[1] = @input[0] + @input[1] = input @output[2] = @output[1] - @output[1] = @output[0] + @output[1] = output + end +end + +class ButterHpf < Biquad + def initialize f + self.freq = f # triggers recalc + clear + self + end + + def freq= arg + @w = arg * @@inv_srate # (0..0.5) + recalc + end + + def recalc + # from /Developer/Examples/CoreAudio/AudioUnits/AUPinkNoise/Utility/Biquad.cpp + k = Math.tan( Math::PI * @w ) + kk = k*k; + g = Dsp::SQRT2*k + kk + d_inv = 1.0 / (1 + g); + + a0 = d_inv + a1 = -2 * d_inv + a2 = d_inv + b0 = 1.0 # gain + b1 = 2*kk-1 * d_inv + b2 = (1 - g) * d_inv + update( Vector[a0, a1, a2], Vector[b0, b1, b2] ) end end + class Hpf < Biquad - def initialize( srate, f, qq=Dsp::SQRT2_2 ) - @inv_q = 1.0 / qq - freq = f # triggers recalc - super srate + def initialize( f, q=nil ) + @inv_q = q ? 1.0 / q : Math.sqrt(2) # default to butterworth + self.freq = f # triggers recalc + clear + self end def q= arg @@ -90,7 +120,7 @@ def q= arg end def freq= arg - @w = arg * Dsp::TWO_PI * @inv_srate + @w = arg * Dsp::TWO_PI * @@inv_srate recalc end @@ -100,6 +130,7 @@ def recalc # from RBJ cookbook @ http://www.musicdsp.org/files/Audio-EQ-Cookbo ocw = 1+cw b0 = b2 = 0.5*ocw b1 = -ocw + a0 = 1 + alpha a1 = -2.0*cw a2 = 1 - alpha @@ -109,22 +140,31 @@ def recalc # from RBJ cookbook @ http://www.musicdsp.org/files/Audio-EQ-Cookbo end class SuperSaw < Oscillator - def initialize srate=44.1e3, num=7 - @master = Phasor.new(srate) - @phasors = (1..num-1).map{ Phasor.new(srate) } + attr_accessor :phat + + def initialize freq = DEFAULT_FREQ, spread=0.5, num=7 + @master = Phasor.new + @phasors = (1..num-1).map{ Phasor.new } + @phat = spread setup_tables - @phat = 12/127.0 # default knob - @hpf = Hpf.new( srate, @master.freq ) - super srate + @hpf = ButterHpf.new( @master.freq ) + randomize_phase + self.freq = freq + self end + def randomize_phase + @phasors.each{|p| p.phase = Dsp.random } + end + def clear @hpf.clear + randomize_phase end def freq= f @hpf.freq = @master.freq = @freq = f - @phasors.each_with_index{ |p,i| p.freq = f + @@detune[@phat] * @@offsets[i] } + @phasors.each_with_index{ |p,i| p.freq = (1 + @@detune[@phat] * @@offsets[i]) * f; puts "#{i+1}: #{p.freq}" } end def tick @@ -133,9 +173,9 @@ def tick @hpf.tick( osc ) end - def ticks samples + def ticks samples osc = @@center[ @phat ] * Vector[*@master.ticks(samples)] - osc += @@side[ @phat ] * @phasors.inject( osc ){|sum,p| sum + Vector[*p.ticks(samples)] } + osc = @@side[ @phat ] * @phasors.inject( osc ){|sum,p| sum + Vector[*p.ticks(samples)] } @hpf.ticks( osc.to_a ) end @@ -143,9 +183,9 @@ def ticks samples def setup_tables @@offsets ||= [ -0.11002313, -0.06288439, -0.01952356, 0.01991221, 0.06216538, 0.10745242 ] - @@detune ||= Dsp.lookup_table{|x| calc_detune(x) } - @@side ||= Dsp.lookup_table{|x| calc_side(x) } - @@center ||= Dsp.lookup_table{|x| calc_center(x) } + @@detune ||= Dsp::LookupTable.new{|x| calc_detune(x) } + @@side ||= Dsp::LookupTable.new{|x| calc_side(x) } + @@center ||= Dsp::LookupTable.new{|x| calc_center(x) } end def calc_detune x @@ -160,22 +200,4 @@ def calc_center x -0.55366*x + 0.99785 end -end - - -# require 'wavefile' -# s = SuperSaw.new -# cycle = s.ticks( 10000 ) -# -# include WaveFile -# -# format = Format.new(:mono, 16, 44100) -# writer = Writer.new("super.wav", format) -# -# # Write a 1 second long 440Hz square wave -# buffer = Buffer.new(cycle, format) -# 220.times do -# writer.write(buffer) -# end -# -# writer.close() +end \ No newline at end of file From 95508731cb91f362801cf928a240c73d693b8914 Mon Sep 17 00:00:00 2001 From: weirdpercent Date: Mon, 2 Jul 2012 10:02:02 -0400 Subject: [PATCH 08/16] some ignores and rm old stuff --- .gitignore | 13 ++++++++++--- .rvmrc | 1 - use_jruby.sh | 2 -- 3 files changed, 10 insertions(+), 6 deletions(-) delete mode 100644 .rvmrc delete mode 100755 use_jruby.sh diff --git a/.gitignore b/.gitignore index 68b9e66..a94577c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ .DS_Store -stuff *.class build deploy -plugins/**/*.java plugins/**/*.jar +plugins/build +plugins/ToneMatrix +plugins/Stuff gui-labs -javafx \ No newline at end of file +javafx +._redcar +build.xml +nbproject +manifest.mf +.rvmrc +.irbrc \ No newline at end of file diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index 929cda8..0000000 --- a/.rvmrc +++ /dev/null @@ -1 +0,0 @@ -rvm use jruby-1.6.7 diff --git a/use_jruby.sh b/use_jruby.sh deleted file mode 100755 index 85829bd..0000000 --- a/use_jruby.sh +++ /dev/null @@ -1,2 +0,0 @@ -export PATH=~/jruby-1.3.0/bin/:$PATH -bash \ No newline at end of file From a0dc9cac12e50e2e06af5015ad07442bfa7ad118 Mon Sep 17 00:00:00 2001 From: weirdpercent Date: Mon, 2 Jul 2012 21:50:40 -0400 Subject: [PATCH 09/16] refactor to change all Duby to Mirah --- plugins/MirahFilta/MirahFilta.rb | 20 ++++++++++ plugins/MirahFilta/MirahTools.mirah | 33 ++++++++++++++++ plugins/MirahFilta/README | 1 + plugins/MirahFreeComp/MirahFreeComp.rb | 22 +++++++++++ .../MirahFreeComp/MirahFreeCompTools.mirah | 38 +++++++++++++++++++ plugins/MirahGain/MirahGain.rb | 13 +++++++ plugins/MirahGain/MirahGainTools.mirah | 9 +++++ plugins/MirahGain/README | 9 +++++ 8 files changed, 145 insertions(+) create mode 100644 plugins/MirahFilta/MirahFilta.rb create mode 100644 plugins/MirahFilta/MirahTools.mirah create mode 100644 plugins/MirahFilta/README create mode 100644 plugins/MirahFreeComp/MirahFreeComp.rb create mode 100644 plugins/MirahFreeComp/MirahFreeCompTools.mirah create mode 100644 plugins/MirahGain/MirahGain.rb create mode 100644 plugins/MirahGain/MirahGainTools.mirah create mode 100644 plugins/MirahGain/README diff --git a/plugins/MirahFilta/MirahFilta.rb b/plugins/MirahFilta/MirahFilta.rb new file mode 100644 index 0000000..f20ca43 --- /dev/null +++ b/plugins/MirahFilta/MirahFilta.rb @@ -0,0 +1,20 @@ +include_class Java::MirahTools + +class MirahFilta < OpazPlug + plugin "MirahFilta", "Opaz", "LoGeek" + can_do "1in1out", "plugAsChannelInsert", "plugAsSend" + unique_id "mflt" + + param :cut_off, "Cut Off", 1.0 + param :resonance, "Resonance", 0.1 + param :mode, "Mode (LP or HP)", 0 + + def filter + @filter ||= MirahTools.new + end + + def process(inputs, outputs, sampleFrames) + filter.recompute_parameters(cut_off, resonance, (mode < 0.5 ? +1 : -1), sample_rate) + filter.apply(inputs[0], outputs[0], sampleFrames) + end +end diff --git a/plugins/MirahFilta/MirahTools.mirah b/plugins/MirahFilta/MirahTools.mirah new file mode 100644 index 0000000..68b482e --- /dev/null +++ b/plugins/MirahFilta/MirahTools.mirah @@ -0,0 +1,33 @@ +class MirahTools + def not_below(val:float,min:float) + val < min ? min : val + end + + def recompute_parameters(cutoff:float, resonance:float, mode:float, sampleRate:float) + @r = not_below( (1-resonance) * 10, float(0.1) ) + @f = not_below( cutoff * sampleRate / 4, float(40.0) ) + c = Math.tan(3.141592653589793 * @f / sampleRate) + @c = float((mode == 1) ? (1/c) : c) + @a1 = 1 / ( 1 + @r * @c + @c * @c) + @a2 = mode * 2* @a1 + @a3 = @a1 + @b1 = mode * 2 * ( 1 - @c*@c) * @a1 + @b2 = ( 1 - @r * @c + @c * @c) * @a1 + end + + def apply(input0:float[], output0:float[], sampleFrames:int) + output0[0] = @a1 * input0[0] + @a2 * @ih1_1 + @a3 * @ih1_2 - @b1*@oh1_1 - @b2*@oh1_2 + output0[1] = @a1 * input0[1] + @a2 * input0[0] + @a3 * @ih1_1 - @b1*output0[0] - @b2*@oh1_1 + + sample = 2 + while sample < sampleFrames + output0[sample] = @a1*input0[sample] + @a2*input0[sample-1] + @a3*input0[sample-2] - @b1*output0[sample-1] - @b2*output0[sample-2] + sample += 1 + end + + @ih1_1 = input0[sampleFrames-1] + @ih1_2 = input0[sampleFrames-2] + @oh1_1 = output0[sampleFrames-1] + @oh1_2 = output0[sampleFrames-2] + end +end \ No newline at end of file diff --git a/plugins/MirahFilta/README b/plugins/MirahFilta/README new file mode 100644 index 0000000..d364a1b --- /dev/null +++ b/plugins/MirahFilta/README @@ -0,0 +1 @@ +Work in progress of filter implemented in JRuby + Mirah \ No newline at end of file diff --git a/plugins/MirahFreeComp/MirahFreeComp.rb b/plugins/MirahFreeComp/MirahFreeComp.rb new file mode 100644 index 0000000..2002947 --- /dev/null +++ b/plugins/MirahFreeComp/MirahFreeComp.rb @@ -0,0 +1,22 @@ +include_class Java::MirahFreeCompTools + +class MirahFreeComp < OpazPlug + plugin "MirahFreeComp", "Opaz", "LoGeek" + can_do "1in1out", "plugAsChannelInsert", "plugAsSend" + unique_id "mfcp" + + param :threshold, "Threshold", 0, "dB", (-60.0..6.0) + param :ratio, "Ratio", 1, "n:1", (1.0..100.0) + param :attack, "Attack", 20, "ms", (0.0..250.0) + param :release, "Release", 200, "ms", (25.0..2500.0) + param :output, "Output", 0, "dB", (0.0..30.0) + + def tools + @tools ||= MirahFreeCompTools.new + end + + def process(inputs, outputs, sampleFrames) + tools.slider(threshold,ratio,attack,release,output,sample_rate) + tools.process(inputs[0],outputs[0], sampleFrames) + end +end diff --git a/plugins/MirahFreeComp/MirahFreeCompTools.mirah b/plugins/MirahFreeComp/MirahFreeCompTools.mirah new file mode 100644 index 0000000..fbab945 --- /dev/null +++ b/plugins/MirahFreeComp/MirahFreeCompTools.mirah @@ -0,0 +1,38 @@ +class MirahFreeCompTools + def initialize + @final_gain = 1.0 + @env = 0.0 + end + + def slider(threshold:float,ratio:float,attack:float,release:float,output:float,sample_rate:int) + @env_rel = Math.exp(-1/(0.25*release*sample_rate)) + @thresh = Math.pow(10,threshold/20.0) + @transA = (1/ratio) - 1 + @transB = Math.pow(10,output/20.0) * Math.pow(@thresh,1-(1/ratio)) + @output_gain = Math.pow(10,output/20.0) + @att_coef = Math.exp(-1 / (attack/1000.0*sample_rate)) + @rel_coef = Math.exp(-1 / (release/1000.0*sample_rate)) + end + + def sample(spl0:float) + det = Math.abs(spl0) # instead of [spl0.abs, spl1.abs].max + det += float(Math.pow(10,-29)) # cannot use 10e-30 currently + + @env = det >= @env ? det : float(det+@env_rel*(@env-det)) + gain = @env > @thresh ? Math.pow(@env,@transA)*@transB : @output_gain + + @final_gain = float(gain < @final_gain ? gain+@att_coef*(@final_gain-gain) : gain+@rel_coef*(@final_gain-gain)) + spl0 *= float(@final_gain) + #spl1 *= final_gain + #[spl0, spl1] + spl0 + end + + def process(inBuffer:float[], outBuffer:float[], sampleFrames:int) + i = 0 + while i < sampleFrames + outBuffer[i] = sample(inBuffer[i]) + i += 1 + end + end +end \ No newline at end of file diff --git a/plugins/MirahGain/MirahGain.rb b/plugins/MirahGain/MirahGain.rb new file mode 100644 index 0000000..3ecf0dd --- /dev/null +++ b/plugins/MirahGain/MirahGain.rb @@ -0,0 +1,13 @@ +include_class Java::MirahGainTools + +class MirahGain < OpazPlug + plugin "MirahGain", "Opaz", "LoGeek" + can_do "1in1out", "plugAsChannelInsert", "plugAsSend" + unique_id "MGaN" + + param :gain, "Gain", 1.0, "dB" + + def process(inputs, outputs, sampleFrames) + MirahGainTools.process(inputs[0], outputs[0], sampleFrames, gain) + end +end diff --git a/plugins/MirahGain/MirahGainTools.mirah b/plugins/MirahGain/MirahGainTools.mirah new file mode 100644 index 0000000..a94e95b --- /dev/null +++ b/plugins/MirahGain/MirahGainTools.mirah @@ -0,0 +1,9 @@ +class MirahGainTools + def self.process(inBuffer:float[], outBuffer:float[], sampleFrames:int, gain:float) + i = 0 + while i < sampleFrames + outBuffer[i] = inBuffer[i] * gain + i += 1 + end + end +end diff --git a/plugins/MirahGain/README b/plugins/MirahGain/README new file mode 100644 index 0000000..9e6afb8 --- /dev/null +++ b/plugins/MirahGain/README @@ -0,0 +1,9 @@ +Example of Mirah+JRuby plugin. + +see http://www.mirah.org + +Mirah must be installed first: + +jgem install mirah + +.mirah files are compiled to .java then .class automatically. \ No newline at end of file From 0c27a7e1a31f67908eb2880521b716e143d03377 Mon Sep 17 00:00:00 2001 From: weirdpercent Date: Mon, 2 Jul 2012 22:06:54 -0400 Subject: [PATCH 10/16] remove all ref to Duby --- plugins/DubyFilta/DubyFilta.rb | 20 ----------- plugins/DubyFilta/DubyTools.mirah | 33 ----------------- plugins/DubyFilta/README | 1 - plugins/DubyFreeComp/DubyFreeComp.rb | 22 ------------ plugins/DubyFreeComp/DubyFreeCompTools.mirah | 38 -------------------- plugins/DubyGain/DubyGain.rb | 13 ------- plugins/DubyGain/DubyGainTools.mirah | 9 ----- plugins/DubyGain/README | 9 ----- 8 files changed, 145 deletions(-) delete mode 100644 plugins/DubyFilta/DubyFilta.rb delete mode 100644 plugins/DubyFilta/DubyTools.mirah delete mode 100644 plugins/DubyFilta/README delete mode 100644 plugins/DubyFreeComp/DubyFreeComp.rb delete mode 100644 plugins/DubyFreeComp/DubyFreeCompTools.mirah delete mode 100644 plugins/DubyGain/DubyGain.rb delete mode 100644 plugins/DubyGain/DubyGainTools.mirah delete mode 100644 plugins/DubyGain/README diff --git a/plugins/DubyFilta/DubyFilta.rb b/plugins/DubyFilta/DubyFilta.rb deleted file mode 100644 index e2215e7..0000000 --- a/plugins/DubyFilta/DubyFilta.rb +++ /dev/null @@ -1,20 +0,0 @@ -include_class Java::DubyTools - -class DubyFilta < OpazPlug - plugin "DubyFilta", "Opaz", "LoGeek" - can_do "1in1out", "plugAsChannelInsert", "plugAsSend" - unique_id "dflt" - - param :cut_off, "Cut Off", 1.0 - param :resonance, "Resonance", 0.1 - param :mode, "Mode (LP or HP)", 0 - - def filter - @filter ||= DubyTools.new - end - - def process(inputs, outputs, sampleFrames) - filter.recompute_parameters(cut_off, resonance, (mode < 0.5 ? +1 : -1), sample_rate) - filter.apply(inputs[0], outputs[0], sampleFrames) - end -end diff --git a/plugins/DubyFilta/DubyTools.mirah b/plugins/DubyFilta/DubyTools.mirah deleted file mode 100644 index 50c6a26..0000000 --- a/plugins/DubyFilta/DubyTools.mirah +++ /dev/null @@ -1,33 +0,0 @@ -class DubyTools - def not_below(val:float,min:float) - val < min ? min : val - end - - def recompute_parameters(cutoff:float, resonance:float, mode:float, sampleRate:float) - @r = not_below( (1-resonance) * 10, float(0.1) ) - @f = not_below( cutoff * sampleRate / 4, float(40.0) ) - c = Math.tan(3.141592653589793 * @f / sampleRate) - @c = float((mode == 1) ? (1/c) : c) - @a1 = 1 / ( 1 + @r * @c + @c * @c) - @a2 = mode * 2* @a1 - @a3 = @a1 - @b1 = mode * 2 * ( 1 - @c*@c) * @a1 - @b2 = ( 1 - @r * @c + @c * @c) * @a1 - end - - def apply(input0:float[], output0:float[], sampleFrames:int) - output0[0] = @a1 * input0[0] + @a2 * @ih1_1 + @a3 * @ih1_2 - @b1*@oh1_1 - @b2*@oh1_2 - output0[1] = @a1 * input0[1] + @a2 * input0[0] + @a3 * @ih1_1 - @b1*output0[0] - @b2*@oh1_1 - - sample = 2 - while sample < sampleFrames - output0[sample] = @a1*input0[sample] + @a2*input0[sample-1] + @a3*input0[sample-2] - @b1*output0[sample-1] - @b2*output0[sample-2] - sample += 1 - end - - @ih1_1 = input0[sampleFrames-1] - @ih1_2 = input0[sampleFrames-2] - @oh1_1 = output0[sampleFrames-1] - @oh1_2 = output0[sampleFrames-2] - end -end \ No newline at end of file diff --git a/plugins/DubyFilta/README b/plugins/DubyFilta/README deleted file mode 100644 index 496e633..0000000 --- a/plugins/DubyFilta/README +++ /dev/null @@ -1 +0,0 @@ -Work in progress of filter implemented in JRuby + Duby \ No newline at end of file diff --git a/plugins/DubyFreeComp/DubyFreeComp.rb b/plugins/DubyFreeComp/DubyFreeComp.rb deleted file mode 100644 index 25f1be3..0000000 --- a/plugins/DubyFreeComp/DubyFreeComp.rb +++ /dev/null @@ -1,22 +0,0 @@ -include_class Java::DubyFreeCompTools - -class DubyFreeComp < OpazPlug - plugin "DubyFreeComp", "Opaz", "LoGeek" - can_do "1in1out", "plugAsChannelInsert", "plugAsSend" - unique_id "dfcp" - - param :threshold, "Threshold", 0, "dB", (-60.0..6.0) - param :ratio, "Ratio", 1, "n:1", (1.0..100.0) - param :attack, "Attack", 20, "ms", (0.0..250.0) - param :release, "Release", 200, "ms", (25.0..2500.0) - param :output, "Output", 0, "dB", (0.0..30.0) - - def tools - @tools ||= DubyFreeCompTools.new - end - - def process(inputs, outputs, sampleFrames) - tools.slider(threshold,ratio,attack,release,output,sample_rate) - tools.process(inputs[0],outputs[0], sampleFrames) - end -end diff --git a/plugins/DubyFreeComp/DubyFreeCompTools.mirah b/plugins/DubyFreeComp/DubyFreeCompTools.mirah deleted file mode 100644 index ce5f211..0000000 --- a/plugins/DubyFreeComp/DubyFreeCompTools.mirah +++ /dev/null @@ -1,38 +0,0 @@ -class DubyFreeCompTools - def initialize - @final_gain = 1.0 - @env = 0.0 - end - - def slider(threshold:float,ratio:float,attack:float,release:float,output:float,sample_rate:int) - @env_rel = Math.exp(-1/(0.25*release*sample_rate)) - @thresh = Math.pow(10,threshold/20.0) - @transA = (1/ratio) - 1 - @transB = Math.pow(10,output/20.0) * Math.pow(@thresh,1-(1/ratio)) - @output_gain = Math.pow(10,output/20.0) - @att_coef = Math.exp(-1 / (attack/1000.0*sample_rate)) - @rel_coef = Math.exp(-1 / (release/1000.0*sample_rate)) - end - - def sample(spl0:float) - det = Math.abs(spl0) # instead of [spl0.abs, spl1.abs].max - det += float(Math.pow(10,-29)) # cannot use 10e-30 currently - - @env = det >= @env ? det : float(det+@env_rel*(@env-det)) - gain = @env > @thresh ? Math.pow(@env,@transA)*@transB : @output_gain - - @final_gain = float(gain < @final_gain ? gain+@att_coef*(@final_gain-gain) : gain+@rel_coef*(@final_gain-gain)) - spl0 *= float(@final_gain) - #spl1 *= final_gain - #[spl0, spl1] - spl0 - end - - def process(inBuffer:float[], outBuffer:float[], sampleFrames:int) - i = 0 - while i < sampleFrames - outBuffer[i] = sample(inBuffer[i]) - i += 1 - end - end -end \ No newline at end of file diff --git a/plugins/DubyGain/DubyGain.rb b/plugins/DubyGain/DubyGain.rb deleted file mode 100644 index 5066054..0000000 --- a/plugins/DubyGain/DubyGain.rb +++ /dev/null @@ -1,13 +0,0 @@ -include_class Java::DubyGainTools - -class DubyGain < OpazPlug - plugin "DubyGain", "Opaz", "LoGeek" - can_do "1in1out", "plugAsChannelInsert", "plugAsSend" - unique_id "DGaN" - - param :gain, "Gain", 1.0, "dB" - - def process(inputs, outputs, sampleFrames) - DubyGainTools.process(inputs[0], outputs[0], sampleFrames, gain) - end -end diff --git a/plugins/DubyGain/DubyGainTools.mirah b/plugins/DubyGain/DubyGainTools.mirah deleted file mode 100644 index c3c83b7..0000000 --- a/plugins/DubyGain/DubyGainTools.mirah +++ /dev/null @@ -1,9 +0,0 @@ -class DubyGainTools - def self.process(inBuffer:float[], outBuffer:float[], sampleFrames:int, gain:float) - i = 0 - while i < sampleFrames - outBuffer[i] = inBuffer[i] * gain - i += 1 - end - end -end diff --git a/plugins/DubyGain/README b/plugins/DubyGain/README deleted file mode 100644 index 2b71a9e..0000000 --- a/plugins/DubyGain/README +++ /dev/null @@ -1,9 +0,0 @@ -Example of Mirah+JRuby plugin. - -see http://www.mirah.org - -Mirah must be installed first: - -jgem install mirah - -.mirah files are compiled to .java then .class automatically. \ No newline at end of file From 1640f2d5f2687cfd2b029d746640075f779661ce Mon Sep 17 00:00:00 2001 From: weirdpercent Date: Mon, 2 Jul 2012 22:15:04 -0400 Subject: [PATCH 11/16] remove stale JRuby version, Rakefile now fetches 1.6.7.2 --- libs/jruby-complete-1.6.1.jar | Bin 13411189 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 libs/jruby-complete-1.6.1.jar diff --git a/libs/jruby-complete-1.6.1.jar b/libs/jruby-complete-1.6.1.jar deleted file mode 100644 index 822d3d14b3d96da09e787244ead52ebd6061c31b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13411189 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