Skip to content

Commit

Permalink
Dynamic attribute generation with Gens.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeremy Voorhis committed Nov 1, 2008
1 parent 8c47183 commit c63f455
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 15 deletions.
13 changes: 6 additions & 7 deletions lib/music.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require 'music/key'
require 'music/interpreter'
require 'music/score'
require 'music/gens'
require 'music/timeline'
require 'music/smf_writer'

Expand Down Expand Up @@ -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
23 changes: 23 additions & 0 deletions lib/music/gens.rb
Original file line number Diff line number Diff line change
@@ -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
26 changes: 19 additions & 7 deletions lib/music/interpreter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -20,14 +20,20 @@ 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
end

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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/music/score.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions spec/gens_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c63f455

Please sign in to comment.