Skip to content

Commit

Permalink
Added specifications, a pluggable RNG, include? and splice() operatio…
Browse files Browse the repository at this point in the history
…ns for MusicStructure, and some semantic changes to Repeat. Also fixed a bug in Chord#transpose and handle the case where there may be nothing to transpose.
  • Loading branch information
Jeremy Voorhis committed Jul 6, 2008
1 parent e19b425 commit eb6c946
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 8 deletions.
4 changes: 4 additions & 0 deletions Rakefile
@@ -0,0 +1,4 @@
require 'spec/rake/spectask'

Spec::Rake::SpecTask.new
task :default => :spec
104 changes: 96 additions & 8 deletions lib/music.rb
Expand Up @@ -16,7 +16,7 @@

class Array
def rand
self[(Kernel::rand * size).floor]
self[(Music::rand * size).floor]
end
end

Expand Down Expand Up @@ -54,6 +54,17 @@ def self.Hertz(pitch)
end
end

# Pluggable random number generator support. The default RNG may be
# replaced, e.g. for deterministic unit testing.
class RNG
def rand; Kernel.rand end
end
def Music.rng; @rng end
def Music.rng=(rng) @rng = rng end
Music.rng = RNG.new

def Music.rand; Music.rng.rand end

class PitchClass
include Comparable

Expand Down Expand Up @@ -125,6 +136,23 @@ def surface
def structure
StructureIterator.new(self)
end

def include?(structure)
self == structure || has_next? && next_structure.include?(structure)
end

def splice(structure)
if has_next?
next_structure.splice(structure)
else
self >> structure
end
end

# Convenient access to the RNG
def rand
Music::rng.rand
end
end

class MusicEvent
Expand All @@ -140,13 +168,15 @@ class Surface
include Enumerable
extend Forwardable

def_delegators :@surface, :[], :[]=, :each, :first, :last
def_delegators :@surface, :[], :[]=, :each, :first, :last, :size, :length

def initialize(head)
@head, @surface = head, []
generate
end

def to_a; @surface end

private
def generate
return if @head.nil?
Expand All @@ -168,6 +198,10 @@ def first; @head end

def last; map { |s| s }[-1] end

def include?(structure)
detect { |s| s == structure } ? true : false
end

def each
return if @head.nil?
cursor = @head
Expand All @@ -186,6 +220,13 @@ def initialize(duration)
@duration = duration
end

def ==(other)
case other
when Silence: @duration == other.duration
else false
end
end

def perform(performance)
performance.play_silence(self)
end
Expand All @@ -199,6 +240,14 @@ def initialize(pitch, duration, effort)
@pitch, @duration, @effort = pitch, duration, effort
end

def ==(other)
case other
when Note
[@pitch, @duration, @effort] == [other.pitch, other.duration, other.effort]
else false
end
end

def perform(performance)
performance.play_note(self)
end
Expand All @@ -219,6 +268,14 @@ def initialize(pitches, duration, effort)
@pitches, @duration, @effort = pitches, duration, effort
end

def ==(other)
case other
when Chord
[@pitches, @duration, @effort] == [other.pitches, other.duration, other.effort]
else false
end
end

# Iterate over each pitch in the chord, with its corresponding effort value.
def pitch_with_effort
e = Array(effort)
Expand All @@ -234,7 +291,7 @@ def pitch_class
end

def transpose(hsteps, dur=self.duration, eff=self.effort)
self.class.new(pitch.map { |p| p+hsteps }, dur, eff)
self.class.new(pitches.map { |p| p+hsteps }, dur, eff)
end
end

Expand All @@ -252,7 +309,12 @@ def initialize(*args) # pitch, duration, effort
end

def generate(surface)
surface.last.transpose(*@args)
# Scan backwards for a transposable event.
if ev = surface.to_a.reverse.detect { |e| e.respond_to?(:transpose) }
ev.transpose(*@args)
else
Silence.new(0)
end
end
end

Expand All @@ -270,6 +332,14 @@ def activate
end
choice.activate
end

def include?(structure)
self == structure || @choices.any? { |c| c.include?(structure) } || (has_next? && next_structure.include?(structure))
end

def splice(structure)
@choices.each { |c| c.splice(structure) unless c.include?(structure) }
end
end

class Cycle < MusicStructure
Expand All @@ -279,13 +349,20 @@ def initialize(*structures)

def activate
structure = @structures[next_index]
unless structure.has_next?
structure = structure.dup
structure >> @next
if has_next?
structure.structure.last >> @next unless structure.structure.include?(@next)
end
structure.activate
end

def include?(structure)
self == structure || @structures.any? { |c| c.include?(structure) } || (has_next? && next_structure.include?(structure))
end

def splice(structure)
@structures.each { |c| c.splice(structure) unless c.include?(structure) }
end

private
def next_index
@pos = (@pos + 1) % @structures.size
Expand All @@ -301,12 +378,21 @@ def initialize(repititions, structure)

def activate
if @repititions.zero?
@next.activate if @next
@next.activate if has_next?
else
@repititions -= 1
@structure.splice(self) unless @structure.include?(self)
@structure.activate
end
end

def include?(structure)
self == structure || @structure.include?(structure) || (has_next? && next_structure.include?(structure))
end

def splice(structure)
@structure.splice(structure) unless @structure.include?(structure)
end
end

# Lifts a Proc into a MusicStructure.
Expand Down Expand Up @@ -355,6 +441,8 @@ def fun(&proc)
Fun.new(&proc)
end

def dupe; Dup.new end

def seq(*structures)
hd, *tl = structures
tl.inject(hd) { |s, k| s.structure.last >> k }
Expand Down
117 changes: 117 additions & 0 deletions spec/event_spec.rb
@@ -0,0 +1,117 @@
require File.join( File.dirname(__FILE__), 'spec_helper')

describe MusicEvent do

describe Silence do
before(:each) do
@event = Silence.new(1)
end

it "should have a duration" do
@event.duration.should == 1
end

it "can be performed" do
@event.should be_performed_with(:play_silence)
end
end

describe Note do
before(:each) do
@event = Note.new(60, 1, 127)
end

it "should have a pitch" do
@event.pitch.should == 60
end

it "should have a duration" do
@event.duration.should == 1
end

it "should have effort" do
@event.effort.should == 127
end

it "can be performed" do
@event.should be_performed_with(:play_note)
end

it "can be transposed" do
@event.transpose(2).should == Note.new(62, 1, 127)
end
end

describe Chord do
before(:each) do
@event = Chord.new([60, 64, 67], 1, 127)
end

it "should have a list of pitches" do
@event.pitches.should == [60, 64, 67]
end

it "should have a duration" do
@event.duration.should == 1
end

it "should have effort" do
@event.effort.should == 127
end

it "can enumerate its pitches with their respective efforts" do
data = []
@event.pitch_with_effort { |p, i| data << [p,i] }
data.should == [[60, 127], [64, 127], [67, 127]]
end

it "can have efforts specific to its pitches" do
chord = Chord.new([60, 64, 67], 1, [100, 110, 127])
data = []
chord.pitch_with_effort { |p, i| data << [p,i] }
data.should == [[60, 100], [64, 110], [67, 127]]
end

it "can be performed" do
@event.should be_performed_with(:play_chord)
end

it "can be transposed" do
@event.transpose(2).should == Chord.new([62, 66, 69], 1, 127)
end
end
end

class VisitorMatcher
class StubVisitor
def initialize(meth_sym)
@match = false
eval "def #{meth_sym}(*args) @match = true end"
end

def method_missing(meth_sym, *args)
@match = false
@__meth_sym = meth_sym
end

def __meth_sym; @__meth_sym.nil? ? "nothing" : @__meth_sym end

def __matches?; @match == true end
end

def initialize(meth_sym)
@visitor = StubVisitor.new(meth_sym)
@meth_sym = meth_sym
end

def matches?(ev)
ev.perform(@visitor)
@visitor.__matches?
end

def failure_message
"Expected visit with #@meth_sym. Got #{@visitor.__meth_sym}."
end
end

def be_performed_with(meth_sym) VisitorMatcher.new(meth_sym) end

0 comments on commit eb6c946

Please sign in to comment.