jmtk 0.0.3.3-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.yardopts +10 -0
- data/DEVELOPMENT_NOTES.md +115 -0
- data/INTRO.md +129 -0
- data/LICENSE.txt +27 -0
- data/README.md +50 -0
- data/Rakefile +102 -0
- data/bin/jmtk +250 -0
- data/bin/mtk +250 -0
- data/examples/crescendo.rb +20 -0
- data/examples/drum_pattern.rb +23 -0
- data/examples/dynamic_pattern.rb +36 -0
- data/examples/gets_and_play.rb +27 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +17 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +7 -0
- data/examples/tone_row_melody.rb +23 -0
- data/lib/mtk.rb +76 -0
- data/lib/mtk/core/duration.rb +213 -0
- data/lib/mtk/core/intensity.rb +158 -0
- data/lib/mtk/core/interval.rb +157 -0
- data/lib/mtk/core/pitch.rb +154 -0
- data/lib/mtk/core/pitch_class.rb +194 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/groups/collection.rb +196 -0
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/groups/pitch_collection.rb +23 -0
- data/lib/mtk/io/dls_synth_device.rb +146 -0
- data/lib/mtk/io/dls_synth_output.rb +62 -0
- data/lib/mtk/io/jsound_input.rb +87 -0
- data/lib/mtk/io/jsound_output.rb +82 -0
- data/lib/mtk/io/midi_file.rb +209 -0
- data/lib/mtk/io/midi_input.rb +97 -0
- data/lib/mtk/io/midi_output.rb +195 -0
- data/lib/mtk/io/notation.rb +162 -0
- data/lib/mtk/io/unimidi_input.rb +117 -0
- data/lib/mtk/io/unimidi_output.rb +140 -0
- data/lib/mtk/lang/durations.rb +57 -0
- data/lib/mtk/lang/intensities.rb +61 -0
- data/lib/mtk/lang/intervals.rb +73 -0
- data/lib/mtk/lang/mtk_grammar.citrus +237 -0
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/lang/pitch_classes.rb +29 -0
- data/lib/mtk/lang/pitches.rb +52 -0
- data/lib/mtk/lang/pseudo_constants.rb +26 -0
- data/lib/mtk/lang/variable.rb +32 -0
- data/lib/mtk/numeric_extensions.rb +66 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/patterns/choice.rb +43 -0
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/patterns/lines.rb +54 -0
- data/lib/mtk/patterns/palindrome.rb +45 -0
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/sequencers/event_builder.rb +132 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/sequencers/sequencer.rb +111 -0
- data/lib/mtk/sequencers/step_sequencer.rb +26 -0
- data/spec/mtk/core/duration_spec.rb +372 -0
- data/spec/mtk/core/intensity_spec.rb +289 -0
- data/spec/mtk/core/interval_spec.rb +265 -0
- data/spec/mtk/core/pitch_class_spec.rb +343 -0
- data/spec/mtk/core/pitch_spec.rb +297 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/events/timeline_spec.rb +430 -0
- data/spec/mtk/groups/chord_spec.rb +85 -0
- data/spec/mtk/groups/collection_spec.rb +374 -0
- data/spec/mtk/groups/melody_spec.rb +225 -0
- data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
- data/spec/mtk/io/midi_file_spec.rb +243 -0
- data/spec/mtk/io/midi_output_spec.rb +102 -0
- data/spec/mtk/lang/durations_spec.rb +89 -0
- data/spec/mtk/lang/intensities_spec.rb +101 -0
- data/spec/mtk/lang/intervals_spec.rb +143 -0
- data/spec/mtk/lang/parser_spec.rb +603 -0
- data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
- data/spec/mtk/lang/pitches_spec.rb +56 -0
- data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/variable_spec.rb +52 -0
- data/spec/mtk/numeric_extensions_spec.rb +83 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/patterns/choice_spec.rb +97 -0
- data/spec/mtk/patterns/cycle_spec.rb +123 -0
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/patterns/function_spec.rb +120 -0
- data/spec/mtk/patterns/lines_spec.rb +77 -0
- data/spec/mtk/patterns/palindrome_spec.rb +108 -0
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.mid +0 -0
- metadata +226 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module MTK
|
2
|
+
module Patterns
|
3
|
+
|
4
|
+
# A finite list of elements, which can be enumerated one at a time.
|
5
|
+
class Sequence < Pattern
|
6
|
+
|
7
|
+
###################
|
8
|
+
protected
|
9
|
+
|
10
|
+
# (see Pattern#advance)
|
11
|
+
def advance
|
12
|
+
@index += 1
|
13
|
+
raise StopIteration if @index >= @elements.length
|
14
|
+
@current = @elements[@index]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A special pattern that takes a list of event properties and/or patterns and emits lists of {Events::Event}s
|
5
|
+
class EventBuilder
|
6
|
+
|
7
|
+
DEFAULT_PITCH = MTK.Pitch(60)
|
8
|
+
DEFAULT_DURATION = MTK.Duration(1)
|
9
|
+
DEFAULT_INTENSITY = MTK.Intensity(0.75)
|
10
|
+
|
11
|
+
def initialize(patterns, options={})
|
12
|
+
@patterns = patterns
|
13
|
+
@options = options
|
14
|
+
@default_pitch = if options.has_key? :default_pitch then MTK.Pitch( options[:default_pitch]) else DEFAULT_PITCH end
|
15
|
+
@default_duration = if options.has_key? :default_duration then MTK.Duration( options[:default_duration]) else DEFAULT_DURATION end
|
16
|
+
@default_intensity = if options.has_key? :default_intensity then MTK.Intensity(options[:default_intensity]) else DEFAULT_INTENSITY end
|
17
|
+
@channel = options[:channel]
|
18
|
+
@max_interval = options.fetch(:max_interval, 127)
|
19
|
+
rewind
|
20
|
+
end
|
21
|
+
|
22
|
+
# Build a list of events from the next element in each {Patterns::Pattern}
|
23
|
+
# @return [Array] an array of events
|
24
|
+
def next
|
25
|
+
pitches = []
|
26
|
+
intensities = []
|
27
|
+
duration = nil
|
28
|
+
|
29
|
+
@patterns.each do |pattern|
|
30
|
+
pattern_value = pattern.next
|
31
|
+
|
32
|
+
elements = pattern_value.is_a?(Enumerable) ? pattern_value : [pattern_value]
|
33
|
+
elements.each do |element|
|
34
|
+
return nil if element.nil? or element == :skip
|
35
|
+
|
36
|
+
case element
|
37
|
+
when ::MTK::Core::Pitch then pitches << element
|
38
|
+
when ::MTK::Core::PitchClass then pitches += pitches_for_pitch_classes([element], @previous_pitch)
|
39
|
+
when ::MTK::Groups::PitchClassSet then pitches += pitches_for_pitch_classes(element, @previous_pitch)
|
40
|
+
when ::MTK::Groups::PitchCollection then pitches += element.pitches # this must be after the PitchClassSet case, because that is also a PitchCollection
|
41
|
+
|
42
|
+
when ::MTK::Core::Duration
|
43
|
+
duration ||= 0
|
44
|
+
duration += element
|
45
|
+
|
46
|
+
when ::MTK::Core::Intensity
|
47
|
+
intensities << element
|
48
|
+
|
49
|
+
when ::MTK::Core::Interval
|
50
|
+
if @previous_pitches
|
51
|
+
pitches += @previous_pitches.map{|pitch| pitch + element }
|
52
|
+
else
|
53
|
+
pitches << (@previous_pitch + element)
|
54
|
+
end
|
55
|
+
|
56
|
+
# TODO? String/Symbols for special behaviors like :skip, or :break (something like StopIteration for the current Pattern?)
|
57
|
+
|
58
|
+
else STDERR.puts "#{self.class}#next: Unexpected type '#{element.class}'"
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
pitches << @previous_pitch if pitches.empty?
|
65
|
+
duration ||= @previous_duration
|
66
|
+
|
67
|
+
if intensities.empty?
|
68
|
+
intensity = @previous_intensity
|
69
|
+
else
|
70
|
+
intensity = MTK::Core::Intensity[intensities.map{|i| i.to_f }.inject(:+)/intensities.length] # average the intensities
|
71
|
+
end
|
72
|
+
|
73
|
+
# Not using this yet, maybe later...
|
74
|
+
# return nil if duration==:skip or intensities.include? :skip or pitches.include? :skip
|
75
|
+
|
76
|
+
constrain_pitch(pitches)
|
77
|
+
|
78
|
+
@previous_pitch = pitches.last # Consider doing something different, maybe averaging?
|
79
|
+
@previous_pitches = pitches.length > 1 ? pitches : nil
|
80
|
+
@previous_intensity = intensity
|
81
|
+
@previous_duration = duration
|
82
|
+
|
83
|
+
pitches.map{|pitch| MTK::Events::Note.new(pitch,duration,intensity,@channel) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reset the EventBuilder to its initial state
|
87
|
+
def rewind
|
88
|
+
@previous_pitch = @default_pitch
|
89
|
+
@previous_pitches = [@default_pitch]
|
90
|
+
@previous_intensity = @default_intensity
|
91
|
+
@previous_duration = @default_duration
|
92
|
+
@max_pitch = nil
|
93
|
+
@min_pitch = nil
|
94
|
+
@patterns.each{|pattern| pattern.rewind if pattern.is_a? MTK::Patterns::Pattern }
|
95
|
+
end
|
96
|
+
|
97
|
+
########################
|
98
|
+
private
|
99
|
+
|
100
|
+
def pitches_for_pitch_classes(pitch_classes, previous_pitch)
|
101
|
+
pitch_classes.map{|pitch_class| previous_pitch.nearest(pitch_class) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def constrain_pitch(pitches)
|
105
|
+
if @max_pitch.nil? or @min_pitch.nil?
|
106
|
+
first_pitch = pitches.first
|
107
|
+
|
108
|
+
@max_pitch = first_pitch + @max_interval
|
109
|
+
@max_pitch = 127 if @max_pitch > 127
|
110
|
+
|
111
|
+
@min_pitch = first_pitch - @max_interval
|
112
|
+
@min_pitch = 0 if @min_pitch < 0
|
113
|
+
|
114
|
+
@small_max_span = (@max_pitch - @min_pitch < 12)
|
115
|
+
end
|
116
|
+
|
117
|
+
pitches.map! do |pitch|
|
118
|
+
if @small_max_span
|
119
|
+
pitch = @max_pitch if pitch > @max_pitch
|
120
|
+
pitch = @min_pitch if pitch < @max_pitch
|
121
|
+
else
|
122
|
+
pitch -= 12 while pitch > @max_pitch
|
123
|
+
pitch += 12 while pitch < @min_pitch
|
124
|
+
end
|
125
|
+
pitch
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer which uses the longest duration of the events at each step to determine
|
5
|
+
# the delta times between entries in the {Events::Timeline}.
|
6
|
+
class LegatoSequencer < Sequencer
|
7
|
+
|
8
|
+
# (see Sequencer#next)
|
9
|
+
def next
|
10
|
+
@previous_events = super
|
11
|
+
end
|
12
|
+
|
13
|
+
########################
|
14
|
+
protected
|
15
|
+
|
16
|
+
# (see Sequencer#advance)
|
17
|
+
def advance
|
18
|
+
@time += @previous_events.map{|event| event.length }.max
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer which uses a :rhythm type {Patterns::Pattern} to determine the delta times between entries in the {Events::Timeline}.
|
5
|
+
class RhythmicSequencer < Sequencer
|
6
|
+
|
7
|
+
def initialize(patterns, options={})
|
8
|
+
super
|
9
|
+
@rhythm = options[:rhythm] or raise ArgumentError.new(":rhythm option is required")
|
10
|
+
end
|
11
|
+
|
12
|
+
def rewind
|
13
|
+
super
|
14
|
+
@rhythm.rewind if @rhythm
|
15
|
+
end
|
16
|
+
|
17
|
+
########################
|
18
|
+
protected
|
19
|
+
|
20
|
+
# (see Sequencer#advance)
|
21
|
+
def advance
|
22
|
+
@time += @rhythm.next.length
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer produces {Events::Timeline}s from a collection of {Patterns::Pattern}s.
|
5
|
+
#
|
6
|
+
# @abstract Subclass and override {#advance} to implement a Sequencer.
|
7
|
+
#
|
8
|
+
class Sequencer
|
9
|
+
|
10
|
+
# The maximum number of [time,event_list] entries that will be generated for the {Events::Timeline}.
|
11
|
+
# nil means no maximum (be careful of infinite loops!)
|
12
|
+
attr_accessor :max_steps
|
13
|
+
|
14
|
+
# The maximum time (key) that will be generated for the {Events::Timeline}.
|
15
|
+
# nil means no maximum (be careful of infinite loops!)
|
16
|
+
attr_accessor :max_time
|
17
|
+
|
18
|
+
# Used by {#to_timeline} to builds event lists from the results of {Patterns::Pattern#next} for the {Patterns::Pattern}s in this Sequencer.
|
19
|
+
attr_reader :event_builder
|
20
|
+
|
21
|
+
# The current time offset for the sequencer. Used for the {Events::Timeline} times.
|
22
|
+
attr_reader :time
|
23
|
+
|
24
|
+
# The current sequencer step index (the number of times-1 that {#next} has been called), or -1 if the sequencer has not yet started.
|
25
|
+
attr_reader :step
|
26
|
+
|
27
|
+
attr_reader :patterns
|
28
|
+
|
29
|
+
# @param patterns [Array] the list of patterns to be sequenced into a {Events::Timeline}
|
30
|
+
# @param options [Hash] the options to create a message with.
|
31
|
+
# @option options [String] :max_steps set {#max_steps}
|
32
|
+
# @option options [String] :max_time set {#max_time}
|
33
|
+
# @option options [Proc] :filter a Proc that will replace the events generated by {#next} with the results of the Proc[events]
|
34
|
+
# @option options [Class] :event_builder replace the {Sequencers::EventBuilder} with a custom Event pattern
|
35
|
+
def initialize(patterns, options={})
|
36
|
+
@patterns = patterns
|
37
|
+
@max_steps = options[:max_steps]
|
38
|
+
@max_time = options[:max_time]
|
39
|
+
@filter = options[:filter]
|
40
|
+
|
41
|
+
event_builder_class = options.fetch :event_builder, ::MTK::Sequencers::EventBuilder
|
42
|
+
@event_builder = event_builder_class.new(patterns, options)
|
43
|
+
|
44
|
+
rewind
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# Produce a {Events::Timeline} from the {Patterns::Pattern}s in this Sequencer.
|
49
|
+
def to_timeline
|
50
|
+
rewind
|
51
|
+
timeline = MTK::Events::Timeline.new
|
52
|
+
loop do
|
53
|
+
events = self.next
|
54
|
+
if events
|
55
|
+
events = events.reject{|e| e.rest? }
|
56
|
+
timeline[@time] = events unless events.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
timeline
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Advanced the step index and time, and return the next list of events built from the sequencer patterns.
|
64
|
+
# @note this is called automatically by {#to_timeline},
|
65
|
+
# so you can ignore this method unless you want to hack on sequencers at a lower level.
|
66
|
+
def next
|
67
|
+
if @step >= 0
|
68
|
+
advance
|
69
|
+
raise StopIteration if @max_time and @time > @max_time
|
70
|
+
end
|
71
|
+
@step += 1
|
72
|
+
raise StopIteration if @max_steps and @step >= @max_steps
|
73
|
+
events = @event_builder.next
|
74
|
+
events = @filter[events] if @filter
|
75
|
+
events
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Reset the sequencer and all its patterns.
|
80
|
+
# @note this is called automatically at the beginning of {#to_timeline},
|
81
|
+
# so you can ignore this method unless you want to hack on sequencers at a lower level.
|
82
|
+
def rewind
|
83
|
+
@time = 0
|
84
|
+
@step = -1
|
85
|
+
@event_builder.rewind
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
########################
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Advance @time to the next time for the {Events::Timeline} being produced by {#to_timeline}
|
93
|
+
def advance
|
94
|
+
@time += 1 # default behavior simply advances one beat at a time
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.inherited(subclass)
|
98
|
+
# Define a convenience method like MTK::Patterns.Sequence()
|
99
|
+
# that can handle varargs or a single array argument, plus any Hash options
|
100
|
+
classname = subclass.name.sub /.*::/, '' # Strip off module prefixes
|
101
|
+
MTK::Sequencers.define_singleton_method classname do |*args|
|
102
|
+
options = (args[-1].is_a? Hash) ? args.pop : {}
|
103
|
+
args = args[0] if args.length == 1 and args[0].is_a? Array
|
104
|
+
subclass.new(args,options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer which has a constant {#step_size} time between {Events::Timeline} entries.
|
5
|
+
class StepSequencer < Sequencer
|
6
|
+
|
7
|
+
# The time between entries in the {Events::Timeline}.
|
8
|
+
attr_accessor :step_size
|
9
|
+
|
10
|
+
def initialize(patterns, options={})
|
11
|
+
super
|
12
|
+
@step_size = options.fetch :step_size, 1
|
13
|
+
end
|
14
|
+
|
15
|
+
########################
|
16
|
+
protected
|
17
|
+
|
18
|
+
# (see Sequencer#advance)
|
19
|
+
def advance
|
20
|
+
@time += @step_size
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Core::Duration do
|
4
|
+
|
5
|
+
let(:one_beat) { Duration[1] }
|
6
|
+
let(:two_beats) { Duration[2] }
|
7
|
+
|
8
|
+
|
9
|
+
describe 'NAMES' do
|
10
|
+
it "is the list of base duration names available" do
|
11
|
+
Duration::NAMES.should =~ %w( w h q i s r x )
|
12
|
+
end
|
13
|
+
|
14
|
+
it "is immutable" do
|
15
|
+
lambda{ Duration::NAMES << 'z' }.should raise_error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
describe 'VALUES_BY_NAME' do
|
21
|
+
it 'maps names to values' do
|
22
|
+
Duration::VALUES_BY_NAME.each do |name,value|
|
23
|
+
Duration.from_s(name).value.should == value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'is immutable' do
|
28
|
+
lambda{ Duration::VALUES_BY_NAME << 'z' }.should raise_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
describe '.new' do
|
34
|
+
it "constructs a Duration with whatever value is given" do
|
35
|
+
float = 0.5
|
36
|
+
value = Duration.new(float).value
|
37
|
+
value.should be_equal float
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
describe '.[]' do
|
43
|
+
it "constructs and caches a duration from a Numeric" do
|
44
|
+
Duration[1].should be_equal Duration[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "retains the new() method's ability to construct uncached objects" do
|
48
|
+
Duration.new(1).should_not be_equal Duration[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "uses the argument as the value, if the argument is a Fixnum" do
|
52
|
+
value = Duration[4].value
|
53
|
+
value.should be_equal 4
|
54
|
+
end
|
55
|
+
|
56
|
+
it "converts other Numerics to Rational values" do
|
57
|
+
value = Duration[0.5].value
|
58
|
+
value.should be_a Rational
|
59
|
+
value.should == Rational(1,2)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
describe '.from_i' do
|
65
|
+
it "uses the argument as the value" do
|
66
|
+
value = Duration.from_i(4).value
|
67
|
+
value.should be_equal 4
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '.from_f' do
|
72
|
+
it "converts Floats to Rational" do
|
73
|
+
value = Duration.from_f(0.5).value
|
74
|
+
value.should be_a Rational
|
75
|
+
value.should == Rational(1,2)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '.from_s' do
|
80
|
+
it "converts any of the duration NAMES into a Duration with the value from the VALUES_BY_NAME mapping" do
|
81
|
+
for name in Duration::NAMES
|
82
|
+
Duration.from_s(name).value.should == Duration::VALUES_BY_NAME[name]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it "converts any of the duration names prefixed with a '-' to the negative value" do
|
87
|
+
for name in Duration::NAMES
|
88
|
+
Duration.from_s("-#{name}").value.should == -1 * Duration::VALUES_BY_NAME[name]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it "converts any of the duration names suffixed a 't' to 2/3 of the value" do
|
93
|
+
for name in Duration::NAMES
|
94
|
+
Duration.from_s("#{name}t").value.should == Rational(2,3) * Duration::VALUES_BY_NAME[name]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "converts any of the duration names suffixed a '.' to 3/2 of the value" do
|
99
|
+
for name in Duration::NAMES
|
100
|
+
Duration.from_s("#{name}.").value.should == Rational(3,2) * Duration::VALUES_BY_NAME[name]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it "converts suffix combinations of 't' and '.' (multiplying by 2/3 and 3/2 for each)" do
|
105
|
+
trip = Rational(2,3)
|
106
|
+
dot = Rational(3,2)
|
107
|
+
for name in Duration::NAMES
|
108
|
+
for suffix,multiplier in {'tt' => trip*trip, 't.' => trip*dot, '..' => dot*dot, 't..t.' => trip*dot*dot*trip*dot}
|
109
|
+
Duration.from_s("#{name}#{suffix}").value.should == multiplier * Duration::VALUES_BY_NAME[name]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "parses durations with integer multipliers" do
|
115
|
+
Durations::DURATION_NAMES.each_with_index do |duration, index|
|
116
|
+
multiplier = index+5
|
117
|
+
Duration.from_s("#{multiplier}#{duration}").should == multiplier * Duration(duration)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it "parses durations with float multipliers" do
|
122
|
+
Durations::DURATION_NAMES.each_with_index do |duration, index|
|
123
|
+
multiplier = (index+1)*1.123
|
124
|
+
Duration.from_s("#{multiplier}#{duration}").should == multiplier * Duration(duration)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "parses durations with float multipliers" do
|
129
|
+
Durations::DURATION_NAMES.each_with_index do |duration, index|
|
130
|
+
multiplier = Rational(index+1, index+2)
|
131
|
+
Duration.from_s("#{multiplier}#{duration}").should == multiplier * Duration(duration)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "parses combinations of all modifiers" do
|
136
|
+
Duration.from_s("-4/5qt.").value.should == -4.0/5 * 1 * 2/3.0 * 3/2.0
|
137
|
+
Duration.from_s("-11.234h.tt").value.should == 2 * 3/2.0 * 2/3.0 * 2/3.0 * -11.234
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '.from_name' do
|
142
|
+
it "acts like .from_s" do
|
143
|
+
for name in Duration::NAMES
|
144
|
+
Duration.from_name(name).value.should == Duration::VALUES_BY_NAME[name]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
describe '#value' do
|
151
|
+
it "is the value used to construct the Duration" do
|
152
|
+
Duration.new(1.111).value.should == 1.111
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
describe '#length' do
|
158
|
+
it 'is the value for positive values' do
|
159
|
+
Duration.new(4).length.should == 4
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'is the absolute value for negative values' do
|
163
|
+
Duration.new(-4).length.should == 4
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
describe '#rest?' do
|
169
|
+
it 'is false for positive values' do
|
170
|
+
Duration.new(4).rest?.should be_false
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'is true for negative values' do
|
174
|
+
Duration.new(-4).rest?.should be_true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
describe '#to_f' do
|
180
|
+
it "is the value as a floating point number" do
|
181
|
+
f = Duration.new(Rational(1,2)).to_f
|
182
|
+
f.should be_a Float
|
183
|
+
f.should == 0.5
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe '#to_i' do
|
188
|
+
it "is the value rounded to the nearest integer" do
|
189
|
+
i = Duration.new(0.5).to_i
|
190
|
+
i.should be_a Fixnum
|
191
|
+
i.should == 1
|
192
|
+
Duration.new(0.49).to_i.should == 0
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe '#to_s' do
|
197
|
+
it "is value.to_s suffixed with 'beats' for beat values > 1" do
|
198
|
+
for value in [2, Rational(3,2), 3.25]
|
199
|
+
Duration.new(value).to_s.should == value.to_s + ' beats'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it "is value.to_s suffixed with 'beats' for positive, non-zero beat values < 1" do
|
204
|
+
for value in [Rational(1,2), 0.25]
|
205
|
+
Duration.new(value).to_s.should == value.to_s + ' beat'
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
it "is value.to_s suffixed with 'beats' for a value of 0" do
|
210
|
+
for value in [0, 0.0, Rational(0,2)]
|
211
|
+
Duration.new(value).to_s.should == value.to_s + ' beats'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
it "is value.to_s suffixed with 'beats' for beat values < -1" do
|
216
|
+
for value in [-2, -Rational(3,2), -3.25]
|
217
|
+
Duration.new(value).to_s.should == value.to_s + ' beats'
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it "is value.to_s suffixed with 'beat' for negative, non-zero beat values > -1" do
|
222
|
+
for value in [-Rational(1,2), -0.25]
|
223
|
+
Duration.new(value).to_s.should == value.to_s + ' beat'
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
it "rounds to 2 decimal places when value.to_s is overly long" do
|
228
|
+
Duration.new(Math.sqrt 2).to_s.should == '1.41 beats'
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe '#inspect' do
|
233
|
+
it 'is "#<MTK::Core::Duration:{object_id} @value={value}>"' do
|
234
|
+
for value in [0, 60, 60.5, 127]
|
235
|
+
duration = Duration.new(value)
|
236
|
+
duration.inspect.should == "#<MTK::Core::Duration:#{duration.object_id} @value=#{value}>"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe '#==' do
|
242
|
+
it "compares two duration values for equality" do
|
243
|
+
Duration.new(Rational(1,2)).should == Duration.new(0.5)
|
244
|
+
Duration.new(4).should == Duration.new(4)
|
245
|
+
Duration.new(1.1).should_not == Duration.new(1)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe "#<=>" do
|
250
|
+
it "orders durations based on their underlying value" do
|
251
|
+
( Duration.new(1) <=> Duration.new(1.1) ).should < 0
|
252
|
+
( Duration.new(2) <=> Duration.new(1) ).should > 0
|
253
|
+
( Duration.new(1.0) <=> Duration.new(1) ).should == 0
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
|
259
|
+
describe '#+' do
|
260
|
+
it 'adds #value to the Numeric argument' do
|
261
|
+
(one_beat + 1.5).should == Duration[2.5]
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'works with a Duration argument' do
|
265
|
+
(one_beat + Duration[1.5]).should == Duration[2.5]
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'returns a new duration (Duration is immutable)' do
|
269
|
+
original = one_beat
|
270
|
+
modified = one_beat + 2
|
271
|
+
original.should_not == modified
|
272
|
+
original.should == Duration[1]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe '#-' do
|
277
|
+
it 'subtract the Numeric argument from #value' do
|
278
|
+
(one_beat - 0.5).should == Duration[0.5]
|
279
|
+
end
|
280
|
+
|
281
|
+
it 'works with a Duration argument' do
|
282
|
+
(one_beat - Duration[0.5]).should == Duration[0.5]
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'returns a new duration (Duration is immutable)' do
|
286
|
+
original = one_beat
|
287
|
+
modified = one_beat - 0.5
|
288
|
+
original.should_not == modified
|
289
|
+
original.should == Duration[1]
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
describe '#*' do
|
295
|
+
it 'multiplies #value to the Numeric argument' do
|
296
|
+
(two_beats * 3).should == Duration[6]
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'works with a Duration argument' do
|
300
|
+
(two_beats * Duration[3]).should == Duration[6]
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'returns a new duration (Duration is immutable)' do
|
304
|
+
original = one_beat
|
305
|
+
modified = one_beat * 2
|
306
|
+
original.should_not == modified
|
307
|
+
original.should == Duration[1]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe '#/' do
|
312
|
+
it 'divides #value by the Numeric argument' do
|
313
|
+
(two_beats / 4.0).should == Duration[0.5]
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'works with a Duration argument' do
|
317
|
+
(two_beats / Duration[4]).should == Duration[0.5]
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'returns a new duration (Duration is immutable)' do
|
321
|
+
original = one_beat
|
322
|
+
modified = one_beat / 2
|
323
|
+
original.should_not == modified
|
324
|
+
original.should == Duration[1]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
describe '#coerce' do
|
329
|
+
it 'allows a Duration to be added to a Numeric' do
|
330
|
+
(2 + one_beat).should == Duration[3]
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'allows a Duration to be subtracted from a Numeric' do
|
334
|
+
(7 - two_beats).should == Duration[5]
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'allows a Duration to be multiplied to a Numeric' do
|
338
|
+
(3 * two_beats).should == Duration[6]
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'allows a Numeric to be divided by a Duration' do
|
342
|
+
(8.0 / two_beats).should == Duration[4]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
|
348
|
+
describe MTK do
|
349
|
+
|
350
|
+
describe '#Duration' do
|
351
|
+
it "acts like .from_s if the argument is a String" do
|
352
|
+
Duration('w').should == Duration.from_s('w')
|
353
|
+
end
|
354
|
+
|
355
|
+
it "acts like .from_s if the argument is a Symbol" do
|
356
|
+
Duration(:w).should == Duration.from_s('w')
|
357
|
+
end
|
358
|
+
|
359
|
+
it "acts like .[] if the argument is a Numeric" do
|
360
|
+
Duration(3.5).should be_equal Duration[Rational(7,2)]
|
361
|
+
end
|
362
|
+
|
363
|
+
it "returns the argument if it's already a Duration" do
|
364
|
+
Duration(Duration[1]).should be_equal Duration[1]
|
365
|
+
end
|
366
|
+
|
367
|
+
it "raises an error for types it doesn't understand" do
|
368
|
+
lambda{ Duration({:not => :compatible}) }.should raise_error
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|