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.
Files changed (110) hide show
  1. data/.yardopts +10 -0
  2. data/DEVELOPMENT_NOTES.md +115 -0
  3. data/INTRO.md +129 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.md +50 -0
  6. data/Rakefile +102 -0
  7. data/bin/jmtk +250 -0
  8. data/bin/mtk +250 -0
  9. data/examples/crescendo.rb +20 -0
  10. data/examples/drum_pattern.rb +23 -0
  11. data/examples/dynamic_pattern.rb +36 -0
  12. data/examples/gets_and_play.rb +27 -0
  13. data/examples/notation.rb +22 -0
  14. data/examples/play_midi.rb +17 -0
  15. data/examples/print_midi.rb +13 -0
  16. data/examples/random_tone_row.rb +18 -0
  17. data/examples/syntax_to_midi.rb +28 -0
  18. data/examples/test_output.rb +7 -0
  19. data/examples/tone_row_melody.rb +23 -0
  20. data/lib/mtk.rb +76 -0
  21. data/lib/mtk/core/duration.rb +213 -0
  22. data/lib/mtk/core/intensity.rb +158 -0
  23. data/lib/mtk/core/interval.rb +157 -0
  24. data/lib/mtk/core/pitch.rb +154 -0
  25. data/lib/mtk/core/pitch_class.rb +194 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/events/timeline.rb +232 -0
  30. data/lib/mtk/groups/chord.rb +56 -0
  31. data/lib/mtk/groups/collection.rb +196 -0
  32. data/lib/mtk/groups/melody.rb +96 -0
  33. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  34. data/lib/mtk/groups/pitch_collection.rb +23 -0
  35. data/lib/mtk/io/dls_synth_device.rb +146 -0
  36. data/lib/mtk/io/dls_synth_output.rb +62 -0
  37. data/lib/mtk/io/jsound_input.rb +87 -0
  38. data/lib/mtk/io/jsound_output.rb +82 -0
  39. data/lib/mtk/io/midi_file.rb +209 -0
  40. data/lib/mtk/io/midi_input.rb +97 -0
  41. data/lib/mtk/io/midi_output.rb +195 -0
  42. data/lib/mtk/io/notation.rb +162 -0
  43. data/lib/mtk/io/unimidi_input.rb +117 -0
  44. data/lib/mtk/io/unimidi_output.rb +140 -0
  45. data/lib/mtk/lang/durations.rb +57 -0
  46. data/lib/mtk/lang/intensities.rb +61 -0
  47. data/lib/mtk/lang/intervals.rb +73 -0
  48. data/lib/mtk/lang/mtk_grammar.citrus +237 -0
  49. data/lib/mtk/lang/parser.rb +29 -0
  50. data/lib/mtk/lang/pitch_classes.rb +29 -0
  51. data/lib/mtk/lang/pitches.rb +52 -0
  52. data/lib/mtk/lang/pseudo_constants.rb +26 -0
  53. data/lib/mtk/lang/variable.rb +32 -0
  54. data/lib/mtk/numeric_extensions.rb +66 -0
  55. data/lib/mtk/patterns/chain.rb +49 -0
  56. data/lib/mtk/patterns/choice.rb +43 -0
  57. data/lib/mtk/patterns/cycle.rb +18 -0
  58. data/lib/mtk/patterns/for_each.rb +71 -0
  59. data/lib/mtk/patterns/function.rb +39 -0
  60. data/lib/mtk/patterns/lines.rb +54 -0
  61. data/lib/mtk/patterns/palindrome.rb +45 -0
  62. data/lib/mtk/patterns/pattern.rb +171 -0
  63. data/lib/mtk/patterns/sequence.rb +20 -0
  64. data/lib/mtk/sequencers/event_builder.rb +132 -0
  65. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  66. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  67. data/lib/mtk/sequencers/sequencer.rb +111 -0
  68. data/lib/mtk/sequencers/step_sequencer.rb +26 -0
  69. data/spec/mtk/core/duration_spec.rb +372 -0
  70. data/spec/mtk/core/intensity_spec.rb +289 -0
  71. data/spec/mtk/core/interval_spec.rb +265 -0
  72. data/spec/mtk/core/pitch_class_spec.rb +343 -0
  73. data/spec/mtk/core/pitch_spec.rb +297 -0
  74. data/spec/mtk/events/event_spec.rb +234 -0
  75. data/spec/mtk/events/note_spec.rb +174 -0
  76. data/spec/mtk/events/parameter_spec.rb +220 -0
  77. data/spec/mtk/events/timeline_spec.rb +430 -0
  78. data/spec/mtk/groups/chord_spec.rb +85 -0
  79. data/spec/mtk/groups/collection_spec.rb +374 -0
  80. data/spec/mtk/groups/melody_spec.rb +225 -0
  81. data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
  82. data/spec/mtk/io/midi_file_spec.rb +243 -0
  83. data/spec/mtk/io/midi_output_spec.rb +102 -0
  84. data/spec/mtk/lang/durations_spec.rb +89 -0
  85. data/spec/mtk/lang/intensities_spec.rb +101 -0
  86. data/spec/mtk/lang/intervals_spec.rb +143 -0
  87. data/spec/mtk/lang/parser_spec.rb +603 -0
  88. data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
  89. data/spec/mtk/lang/pitches_spec.rb +56 -0
  90. data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
  91. data/spec/mtk/lang/variable_spec.rb +52 -0
  92. data/spec/mtk/numeric_extensions_spec.rb +83 -0
  93. data/spec/mtk/patterns/chain_spec.rb +110 -0
  94. data/spec/mtk/patterns/choice_spec.rb +97 -0
  95. data/spec/mtk/patterns/cycle_spec.rb +123 -0
  96. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  97. data/spec/mtk/patterns/function_spec.rb +120 -0
  98. data/spec/mtk/patterns/lines_spec.rb +77 -0
  99. data/spec/mtk/patterns/palindrome_spec.rb +108 -0
  100. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  101. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  102. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  103. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  104. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  105. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  106. data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
  107. data/spec/spec_coverage.rb +2 -0
  108. data/spec/spec_helper.rb +12 -0
  109. data/spec/test.mid +0 -0
  110. 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