diff --git a/lib/music.rb b/lib/music.rb index 2af5f1d..a4aa286 100644 --- a/lib/music.rb +++ b/lib/music.rb @@ -21,6 +21,7 @@ require 'music/key' require 'music/interpreter' require 'music/score' +require 'music/gens' require 'music/timeline' require 'music/smf_writer' @@ -131,12 +132,10 @@ def par(*args) =end # Generate an attribute value from the context. - def gen(&fn) - lambda { |context| fn[context] } - end + def gen(&fn) Gen.new(&fn) end - # Generate an attribute value from the current time. - def env(&fn) - gen { |context| fn[context.time] } - end + # Transform a given attribute value. + def tr(&fn) Tr.new(&fn) end + + def env(&fn) Env.new(&fn) end end diff --git a/lib/music/gens.rb b/lib/music/gens.rb new file mode 100644 index 0000000..4249b6d --- /dev/null +++ b/lib/music/gens.rb @@ -0,0 +1,23 @@ +module Music + class Gen + def initialize(&fn) + @fn = fn + end + + def apply(name, val, context) + @fn.call(context) + end + end + + class Tr < Gen + def apply(name, val, context) + @fn.call(val) + end + end + + class Env < Gen + def apply(name, val, context) + @fn.call(val, context.phase) + end + end +end diff --git a/lib/music/interpreter.rb b/lib/music/interpreter.rb index 37e4ea3..8c3b71d 100644 --- a/lib/music/interpreter.rb +++ b/lib/music/interpreter.rb @@ -6,7 +6,7 @@ def self.eval(music) new.eval(music) end - def eval(music, context = Context.default) + def eval(music, context = Context.default(music.duration)) music.eval(self, context) end @@ -20,7 +20,9 @@ def eval_section(music, context) class Context attr_reader :time, :attributes - def self.default; new(0, {}) end + def self.default(duration) + new(0, { :section_start => 0, :section_duration => duration}) + end def initialize(time, attrs) @time, @attributes = time, attrs @@ -28,6 +30,10 @@ def initialize(time, attrs) def [](name) attributes[name] end + def phase + (@time - @attributes[:section_start]) / @attributes[:section_duration].to_f + end + def advance(dur) self.class.new(time + dur, attributes) end @@ -37,11 +43,17 @@ def push(a0) self.class.new(time, a1) end - def accept(attrs) - inherited = push(attributes.merge(attrs)) - push(inherited.attributes.inject({}) do |hsh, (name, val)| - hsh.merge(name => val.attr_eval(inherited)) - end) + def accept(a0) + names = a0.keys | self.attributes.keys + push(names.inject({}) { |a1, name| + a1.merge name => case new = self.attributes[name] + # If a0 yields a Gen, apply it + when Gen: new.apply(name, a0[name], self) + # otherwise, inherit the attribute value from a0 if + # we haven't defined it + else self.attributes[name] || a0[name] + end + }) end end end diff --git a/lib/music/score.rb b/lib/music/score.rb index 07358fc..a3d023f 100644 --- a/lib/music/score.rb +++ b/lib/music/score.rb @@ -207,7 +207,7 @@ def reverse end def eval(interpreter, c0) - c1 = c0.push(attributes) + c1 = c0.push(attributes.merge(:section_start => c0.time, :section_duration => duration)) m = score.eval(interpreter, c1) interpreter.eval_section(m, c0) end diff --git a/spec/gens_spec.rb b/spec/gens_spec.rb new file mode 100644 index 0000000..d678d67 --- /dev/null +++ b/spec/gens_spec.rb @@ -0,0 +1,52 @@ +require File.join( File.dirname(__FILE__), 'spec_helper') + +describe Gen do + before(:all) do + @score = s( n(c4, 1), + :amp => gen { |c| 0.5 }) + end + + it "should generate values from the given context" do + timeline = @score.to_timeline + timeline[0].amp.should == 0.5 + end +end + +describe Tr do + before(:all) do + @score = s( n(c4, 1, :amp => 0.5), + :amp => tr { |amp| amp * 0.5 } ) + end + + it "should transform attribute values" do + timeline = @score.to_timeline + timeline[0].amp.should == 0.25 + end +end + +describe Env do + it "should transform attribute values based on their phase" do + score = s( seq(n([c4, e4, g4], 1, :amp => 0.5)), + :amp => env { |value, phase| value * Math.sin(phase) } ) + timeline = score.to_timeline + timeline[0].amp.should == 0.0 + timeline[1].amp.should be_close(0.1636, 0.0001) + timeline[2].amp.should be_close(0.3092, 0.0001) + end + + it "can be used to implement various transforms" do + score = cresc(2, + seq(n([c4, e4, g4], 1, :velocity => 64))) + timeline = score.to_timeline + timeline[0].velocity.should == 64 + timeline[1].velocity.should == 85 + timeline[2].velocity.should == 106 + end + + def cresc(factor, score) + s(score, :velocity => env { |vel, ph| + multiplier = (factor - 1.0) * (1.0 + ph) + (vel * multiplier).to_i + }) + end +end