Skip to content

Commit

Permalink
Updates to syntax:
Browse files Browse the repository at this point in the history
* Introduced numerical intervals
* Numeric pitch prefix is pit/pitch instead of y
* Numeric velocity prefix is vel/velocity in addition to v
* Numeric duration prefix is dur/duration instead of u

Fixed issues with Ruby expressions for these numerical nodes not re-evaluating when looping.
  • Loading branch information
adamjmurray committed Dec 4, 2008
1 parent fd9fad4 commit cf61a49
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 31 deletions.
18 changes: 17 additions & 1 deletion lib/cosy/constants.rb
Expand Up @@ -66,7 +66,7 @@ module Cosy
# Maps unison, second, third, fourth, etc to number of semitones
# in the perfect/major interval.
INTERVAL_DEGREE = {
0 => 11, # under mod 7 arithmetic, the 0 degree is a 7th
0 => 11, # under mod 7 arithmetic, the 0 degree is a 7th (unison is a special case)
1 => 0,
2 => 2,
3 => 4,
Expand All @@ -75,6 +75,22 @@ module Cosy
6 => 9,
7 => 11
}

# Maps semitones to interval quality and degree
INTERVAL_VALUES = {
0 => [:perfect, 0],
1 => [:minor, 2],
2 => [:major, 2],
3 => [:minor, 3],
4 => [:major, 3],
5 => [:perfect, 4],
6 => [:diminished, 5],
7 => [:perfect, 5],
8 => [:minor, 6],
9 => [:major, 6],
10 => [:minor, 7],
11 => [:major, 7]
}

#######################################
# MIDI controls
Expand Down
38 changes: 34 additions & 4 deletions lib/cosy/model/syntax_tree.rb
Expand Up @@ -370,7 +370,12 @@ def value(context=nil)
class NumericPitchNode < PitchNode
def value(context=nil)
if not @value
@value = Pitch.new(number.value)
value = Pitch.new(number.value(context))
if number.is_a? RubyNode
# don't cache, allow re-evaluation
return value
end
@value = value
end
return @value
end
Expand All @@ -387,6 +392,21 @@ def value(context=nil)
return @value
end
end


class NumericIntervalNode < IntervalNode
def value(context=nil)
if not @value
value = Interval.new(number.value(context))
if number.is_a? RubyNode
# don't cache, allow re-evaluation
return value
end
@value = value
end
return @value
end
end


class DurationNode < TerminalNode
Expand All @@ -409,7 +429,12 @@ def value(context=nil)
class NumericDurationNode < DurationNode
def value(context=nil)
if not @value
@value = Duration.new(number.value)
value = Duration.new(number.value(context))
if number.is_a? RubyNode
# don't cache, allow re-evaluation
return value
end
@value = value
end
return @value
end
Expand All @@ -419,7 +444,7 @@ def value(context=nil)
class VelocityNode < TerminalNode
def value(context=nil)
if not @value
@value = Velocity.new(text_value,text_value)
@value = Velocity.new(text_value)
end
return @value
end
Expand All @@ -429,7 +454,12 @@ def value(context=nil)
class NumericVelocityNode < TerminalNode
def value(context=nil)
if not @value
@value = Velocity.new(number.value, number.text_value)
value = Velocity.new(number.value(context))
if number.is_a? RubyNode
# don't cache, allow re-evaluation
return value
end
@value = value
end
return @value
end
Expand Down
31 changes: 24 additions & 7 deletions lib/cosy/model/values.rb
Expand Up @@ -48,10 +48,12 @@ class Pitch < Value
attr_reader :pitch_class, :accidental, :octave

def initialize(*args)
need_to_recalc_value = true
case args.length
when 1
if args[0].is_a? Numeric
self.value = args[0]
need_to_recalc_value = false
else
self.pitch_class = args[0]
end
Expand All @@ -62,10 +64,9 @@ def initialize(*args)
self.pitch_class = args[0]
self.accidental = args[1]
@octave = args[2]
@initialized = true
end
@initialized = true
recalc_value
recalc_value if need_to_recalc_value
end

def pitch_class=(pitch_class)
Expand Down Expand Up @@ -165,14 +166,18 @@ def recalc_text_value


class Interval < Value

attr_accessor :quality, :degree, :text_value

def initialize(*args)
need_to_recalc_value = true
case args.length
when 1
@text_value = args[0]
if @text_value =~ /(-?)([A-Za-z]*)(\d*)/
if @text_value.is_a? Numeric
self.value = @text_value
@text_value = @text_value.to_s
need_to_recalc_value = false
elsif @text_value =~ /(-?)([A-Za-z]*)(\d*)/
sign = $1
self.quality = $2
@degree = $3.to_i
Expand All @@ -187,7 +192,7 @@ def initialize(*args)
raise "Bad arguments #{args.inspect}"
end
@initialized = true
recalc_value
recalc_value if need_to_recalc_value
end

def quality=(quality)
Expand All @@ -208,13 +213,25 @@ def degree=(degree)
recalc_value
end

def value=(value)
@value = value
negative = value < 0
semitones = value.abs
@quality,@degree = INTERVAL_VALUES[semitones % 12]
@degree += 8*semitones/12
end

# TODO text_value

private
def recalc_value
if @initialized
deg = @degree.abs % 7
@value = INTERVAL_DEGREE[deg]
if @degrees == 0
@value = deg = 0
else
deg = @degree.abs % 7
@value = INTERVAL_DEGREE[deg]
end
# now value is set to a perfect/major interval, so
# adjust if needed for the other possible interval qualities
case @quality
Expand Down
12 changes: 7 additions & 5 deletions lib/cosy/parser/grammar.treetop
Expand Up @@ -85,17 +85,19 @@ module Cosy
rule pitch
note_name:[A-Ga-g] ![A-Zac-z] accidentals:('#'/'b'/'+'/'_')* octave:(int)? <PitchNode>
/
'y' number:(number/ruby) <NumericPitchNode>
('pitch'/'pit'/'PIT') number:(number/ruby) <NumericPitchNode>
end


rule interval
sign:[+-]?
quality:(
'major' / 'maj' / 'minor' / 'min' / 'perfect' / 'per' / 'augmented' / 'aug' / 'diminished' / 'dim' /
'MAJOR' / 'MAJ' / 'MINOR' / 'MIN' / 'PERFECT' / 'PER' / 'AUGMENTED' / 'AUG' / 'DIMINISHED' / 'DIM'
/ [MmpP])
'MAJOR' / 'MAJ' / 'MINOR' / 'MIN' / 'PERFECT' / 'PER' / 'AUGMENTED' / 'AUG' / 'DIMINISHED' / 'DIM' /
[MmpP] )
degree:[0-9]+ <IntervalNode>
/
('interval'/[Ii]) number:(number/ruby) <NumericIntervalNode>
end


Expand All @@ -105,14 +107,14 @@ module Cosy
'MP' / 'PPP' / 'PP' / 'P' / 'MF' / 'FFF' / 'FF' / 'FO'
) ![\w] <VelocityNode>
/
'v' number:(number/ruby) <NumericVelocityNode>
('velocity'/'vel'/'VEL'/[Vv]) number:(number/ruby) <NumericVelocityNode>
end


rule duration
multiplier:(number/'-')? metrical_duration:metrical_duration modifier:duration_modifier* <DurationNode>
/
'u' number:(number/ruby) <NumericDurationNode>
('duration'/'dur'/'DUR') number:(number/ruby) <NumericDurationNode>
end


Expand Down
18 changes: 16 additions & 2 deletions spec/parser_spec.rb
Expand Up @@ -71,6 +71,8 @@
end
end

# TODO: numeric pitches

it 'should parse pitch chords' do
@parser.parse('[C E G]').value.should == [Pitch.new('C'), Pitch.new('E'), Pitch.new('G')]
end
Expand All @@ -95,6 +97,14 @@
end
end

it 'should parse numeric intervals' do
for semitones in -12..12 do
interval = Interval.new(semitones)
@parser.parse("i#{semitones}").value.should == interval
@parser.parse("I#{semitones}").value.should == interval
end
end

it 'should parse velocity symbols' do
for intensity in [
'ppp','pp','p','mp','mf','fo','ff','fff',
Expand All @@ -106,6 +116,8 @@
end
end

# TODO: numeric velocities

it 'should parse duration symbols' do
for base_duration in [
'w','h','q','ei','s','r','x',
Expand All @@ -124,6 +136,8 @@
end
end
end

# TODO: numeric durations

it 'should parse ruby expressions' do
['1 + 2', "'}'", '"}"'].each do |ruby_expression|
Expand All @@ -139,7 +153,7 @@
end
end

end # describing primitive value behaviors
end # describing atomic values


describe 'OSC Support' do
Expand Down Expand Up @@ -175,7 +189,7 @@
end
end

end
end # described OSC support

def node_should_match_value node,value
case value
Expand Down
55 changes: 55 additions & 0 deletions spec/sequencer_spec.rb
@@ -0,0 +1,55 @@
require File.dirname(__FILE__)+'/spec_helper'

SEQUENCE_COUNT_LIMIT = 1000

describe Cosy::Sequencer do

describe 'Ruby Support' do

it 'should re-evaluate Ruby values for numeric pitches' do
$x = nil
sequence('pit{$x ||= 0; $x += 1}*3').should == [1,2,3]
end

it 'should re-evaluate Ruby values for numeric intervals' do
$x = nil
sequence('i{$x ||= 0; $x += 1}*3').should == [1,2,3]
end

it 'should re-evaluate Ruby values for numeric velocities' do
$x = nil
sequence('v{$x ||= 0; $x += 1}*3').should == [1,2,3]
end

it 'should re-evaluate Ruby values for numeric durations' do
$x = nil
sequence('dur{$x ||= 0; $x += 1}*3').should == [1,2,3]
end

end # described Ruby support


def sequencer(input)
Sequencer.new(input)
end

def sequence(input)
if input.is_a? Sequencer
sequener = input
else
sequener = sequencer(input)
end
sequence = []
count = 0
while value=sequener.next and count < SEQUENCE_COUNT_LIMIT
sequence << value
count += 1
end
if count == SEQUENCE_COUNT_LIMIT
# this is for infinite loop prevention
fail "#{input} output more than #{SEQUENCE_COUNT_LIMIT}"
end
return sequence
end

end
8 changes: 6 additions & 2 deletions test/test_parser.rb
Expand Up @@ -257,15 +257,19 @@ def test_multiplier_triplet_dotted_durations
end

def test_numeric_pitch
parse 'y60'
parse 'pit60'
parse 'pitch60'
end

def test_numeric_velocity
parse 'v60'
parse 'vel60'
parse 'velocity60'
end

def test_numeric_duration
parse 'u60'
parse 'dur60'
parse 'duration60'
end

def test_interval
Expand Down
2 changes: 1 addition & 1 deletion test/test_renderer.rb
Expand Up @@ -143,7 +143,7 @@ def test_chaining
end

def test_repeated_numeric_pitches
assert_sequence [n(60)]*4, 'y60*4'
assert_sequence [n(60)]*4, 'pit60*4'
end

def test_intervals
Expand Down

0 comments on commit cf61a49

Please sign in to comment.