mtk 0.0.3.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/INTRO.md +63 -31
  3. data/Rakefile +3 -1
  4. data/bin/mtk +75 -32
  5. data/examples/drum_pattern.rb +2 -2
  6. data/examples/dynamic_pattern.rb +1 -1
  7. data/examples/helpers/output_selector.rb +71 -0
  8. data/examples/notation.rb +5 -1
  9. data/examples/tone_row_melody.rb +1 -1
  10. data/lib/mtk.rb +1 -0
  11. data/lib/mtk/core/duration.rb +18 -3
  12. data/lib/mtk/core/intensity.rb +5 -3
  13. data/lib/mtk/core/interval.rb +21 -14
  14. data/lib/mtk/core/pitch.rb +2 -0
  15. data/lib/mtk/core/pitch_class.rb +6 -3
  16. data/lib/mtk/events/event.rb +2 -1
  17. data/lib/mtk/events/note.rb +1 -1
  18. data/lib/mtk/events/parameter.rb +1 -0
  19. data/lib/mtk/events/rest.rb +85 -0
  20. data/lib/mtk/events/timeline.rb +6 -2
  21. data/lib/mtk/io/jsound_input.rb +9 -3
  22. data/lib/mtk/io/midi_file.rb +38 -2
  23. data/lib/mtk/io/midi_input.rb +1 -1
  24. data/lib/mtk/io/midi_output.rb +95 -4
  25. data/lib/mtk/io/unimidi_input.rb +7 -3
  26. data/lib/mtk/lang/durations.rb +31 -26
  27. data/lib/mtk/lang/intensities.rb +29 -30
  28. data/lib/mtk/lang/intervals.rb +108 -41
  29. data/lib/mtk/lang/mtk_grammar.citrus +14 -4
  30. data/lib/mtk/lang/parser.rb +10 -5
  31. data/lib/mtk/lang/pitch_classes.rb +45 -17
  32. data/lib/mtk/lang/pitches.rb +169 -32
  33. data/lib/mtk/lang/tutorial.rb +279 -0
  34. data/lib/mtk/lang/tutorial_lesson.rb +87 -0
  35. data/lib/mtk/sequencers/event_builder.rb +29 -8
  36. data/spec/mtk/core/duration_spec.rb +14 -1
  37. data/spec/mtk/core/intensity_spec.rb +1 -1
  38. data/spec/mtk/events/event_spec.rb +10 -16
  39. data/spec/mtk/events/note_spec.rb +3 -3
  40. data/spec/mtk/events/rest_spec.rb +184 -0
  41. data/spec/mtk/events/timeline_spec.rb +5 -1
  42. data/spec/mtk/io/midi_file_spec.rb +13 -2
  43. data/spec/mtk/io/midi_output_spec.rb +42 -9
  44. data/spec/mtk/lang/durations_spec.rb +5 -5
  45. data/spec/mtk/lang/intensities_spec.rb +5 -5
  46. data/spec/mtk/lang/intervals_spec.rb +139 -13
  47. data/spec/mtk/lang/parser_spec.rb +65 -25
  48. data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
  49. data/spec/mtk/lang/pitches_spec.rb +0 -15
  50. data/spec/mtk/patterns/chain_spec.rb +7 -7
  51. data/spec/mtk/patterns/for_each_spec.rb +2 -2
  52. data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
  53. metadata +12 -22
@@ -0,0 +1,87 @@
1
+ module MTK
2
+ module Lang
3
+
4
+ # @private
5
+ class TutorialLesson
6
+
7
+ # Every TutorialLesson requires the options at construction title:
8
+ # title - The short summary displayed in the tutorial's table of contents.
9
+ # description - The full description displayed when entering the tutorial step.
10
+ # validate(input) - Validate user input entered after the description is displayed.
11
+ # success(input) - Perform an action on successful validation
12
+ # failure(input) - Instruct the user on failed validation
13
+ def initialize options
14
+ @title = options[:title]
15
+ @description = options[:description].split("\n").map{|line| line.strip }.join("\n") # trim extra whitespace
16
+ @validation = options[:validation]
17
+ end
18
+
19
+
20
+ def run(output)
21
+ puts
22
+ puts Tutorial::SEPARATOR
23
+ puts
24
+ puts "Lesson: #{@title}".bold.yellow
25
+ puts @description
26
+ puts
27
+ print "Try it now: ".blue
28
+
29
+ did_it_once = false
30
+ input = gets.strip
31
+ until did_it_once and input.empty?
32
+ until validate(input)
33
+ failure(input)
34
+ print "Try again: ".blue
35
+ input = gets.strip
36
+ end
37
+
38
+ success(input, output)
39
+ did_it_once = true
40
+ puts
41
+ puts "Good! ".bold.green + "Try again, or press enter to exit this lesson:".blue
42
+ input = gets.strip
43
+ end
44
+ end
45
+
46
+
47
+ def validate(input)
48
+ return false if input.empty?
49
+
50
+ case @validation
51
+ when Symbol
52
+ MTK::Lang::Parser.parse(input, @validation)
53
+ true
54
+ else # Assume Regexp
55
+ (input =~ @validation) != nil and MTK::Lang::Parser.parse(input)
56
+ end
57
+ rescue Citrus::ParseError
58
+ false
59
+ end
60
+
61
+
62
+ def success(input, output)
63
+ sequencer = MTK::Lang::Parser.parse(input)
64
+ if sequencer
65
+ output.play sequencer.to_timeline
66
+ else
67
+ STDERR.puts "Nothing to play for \"#{input}\""
68
+ end
69
+ rescue Citrus::ParseError
70
+ STDERR.puts $!
71
+ end
72
+
73
+
74
+ def failure(input)
75
+ puts
76
+ puts "Invalid entry \"#{input}\"".bold.red
77
+ end
78
+
79
+
80
+ def to_s
81
+ @title
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
@@ -29,15 +29,31 @@ module MTK
29
29
  @patterns.each do |pattern|
30
30
  pattern_value = pattern.next
31
31
 
32
- elements = pattern_value.is_a?(Enumerable) ? pattern_value : [pattern_value]
32
+ elements = if pattern_value.is_a? Enumerable and not pattern_value.is_a? MTK::Groups::PitchCollection then
33
+ pattern_value
34
+ else
35
+ [pattern_value]
36
+ end
37
+
33
38
  elements.each do |element|
34
39
  return nil if element.nil? or element == :skip
35
40
 
36
41
  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
42
+ when ::MTK::Core::Pitch
43
+ pitches << element
44
+ @previous_pitch = element
45
+
46
+ when ::MTK::Core::PitchClass
47
+ pitches += pitches_for_pitch_classes([element], @previous_pitch)
48
+ @previous_pitch = pitches.last
49
+
50
+ when ::MTK::Groups::PitchClassSet
51
+ pitches += pitches_for_pitch_classes(element, @previous_pitch)
52
+ @previous_pitch = pitches.last
53
+
54
+ when ::MTK::Groups::PitchCollection
55
+ pitches += element.pitches # this must be after the PitchClassSet case, because that is also a PitchCollection
56
+ @previous_pitch = pitches.last
41
57
 
42
58
  when ::MTK::Core::Duration
43
59
  duration ||= 0
@@ -52,6 +68,7 @@ module MTK
52
68
  else
53
69
  pitches << (@previous_pitch + element)
54
70
  end
71
+ @previous_pitch = pitches.last
55
72
 
56
73
  # TODO? String/Symbols for special behaviors like :skip, or :break (something like StopIteration for the current Pattern?)
57
74
 
@@ -62,7 +79,7 @@ module MTK
62
79
  end
63
80
 
64
81
  pitches << @previous_pitch if pitches.empty?
65
- duration ||= @previous_duration
82
+ duration ||= @previous_duration.abs
66
83
 
67
84
  if intensities.empty?
68
85
  intensity = @previous_intensity
@@ -80,7 +97,11 @@ module MTK
80
97
  @previous_intensity = intensity
81
98
  @previous_duration = duration
82
99
 
83
- pitches.map{|pitch| MTK::Events::Note.new(pitch,duration,intensity,@channel) }
100
+ if duration.rest?
101
+ [MTK::Events::Rest.new(duration,@channel)]
102
+ else
103
+ pitches.map{|pitch| MTK::Events::Note.new(pitch,duration,intensity,@channel) }
104
+ end
84
105
  end
85
106
 
86
107
  # Reset the EventBuilder to its initial state
@@ -98,7 +119,7 @@ module MTK
98
119
  private
99
120
 
100
121
  def pitches_for_pitch_classes(pitch_classes, previous_pitch)
101
- pitch_classes.map{|pitch_class| previous_pitch.nearest(pitch_class) }
122
+ pitch_classes.to_a.map{|pitch_class| previous_pitch.nearest(pitch_class) }
102
123
  end
103
124
 
104
125
  def constrain_pitch(pitches)
@@ -8,7 +8,7 @@ describe MTK::Core::Duration do
8
8
 
9
9
  describe 'NAMES' do
10
10
  it "is the list of base duration names available" do
11
- Duration::NAMES.should =~ %w( w h q i s r x )
11
+ Duration::NAMES.should =~ %w( w h q e s r x )
12
12
  end
13
13
 
14
14
  it "is immutable" do
@@ -176,6 +176,19 @@ describe MTK::Core::Duration do
176
176
  end
177
177
 
178
178
 
179
+ describe '#abs' do
180
+ it 'returns the Duration is the value is positive' do
181
+ d = MTK.Duration(2)
182
+ d.abs.should be d
183
+ end
184
+
185
+ it 'returns the negation of the Duration is the value is negative' do
186
+ d = MTK.Duration(-2)
187
+ d.abs.should == -d
188
+ end
189
+ end
190
+
191
+
179
192
  describe '#to_f' do
180
193
  it "is the value as a floating point number" do
181
194
  f = Duration.new(Rational(1,2)).to_f
@@ -7,7 +7,7 @@ describe MTK::Core::Intensity do
7
7
 
8
8
  describe 'NAMES' do
9
9
  it "is the list of base intensity names available" do
10
- Intensity::NAMES.should =~ %w( ppp pp p mp mf o ff fff )
10
+ Intensity::NAMES.should =~ %w( ppp pp p mp mf f ff fff )
11
11
  end
12
12
 
13
13
  it "is immutable" do
@@ -12,7 +12,7 @@ describe MTK::Events::Event do
12
12
 
13
13
 
14
14
  describe "#type" do
15
- it "is the first argument passed to AbstractEvent.new" do
15
+ it "is the first argument passed to Event.new" do
16
16
  event.type.should == type
17
17
  end
18
18
 
@@ -22,7 +22,7 @@ describe MTK::Events::Event do
22
22
  end
23
23
 
24
24
  describe "#value" do
25
- it "is the value of the :value key in the options hash passed to AbstractEvent.new" do
25
+ it "is the value of the :value key in the options hash passed to Event.new" do
26
26
  event.value.should == options[:value]
27
27
  end
28
28
 
@@ -39,7 +39,7 @@ describe MTK::Events::Event do
39
39
  end
40
40
 
41
41
  describe "#duration" do
42
- it "is the value of the :duration key in the options hash passed to AbstractEvent.new" do
42
+ it "is the value of the :duration key in the options hash passed to Event.new" do
43
43
  event.duration.should == options[:duration]
44
44
  end
45
45
 
@@ -56,7 +56,7 @@ describe MTK::Events::Event do
56
56
  end
57
57
 
58
58
  describe "#number" do
59
- it "is the value of the :number key in the options hash passed to AbstractEvent.new" do
59
+ it "is the value of the :number key in the options hash passed to Event.new" do
60
60
  event.number.should == options[:number]
61
61
  end
62
62
 
@@ -73,7 +73,7 @@ describe MTK::Events::Event do
73
73
  end
74
74
 
75
75
  describe "#channel" do
76
- it "is the value of the :channel key in the options hash passed to AbstractEvent.new" do
76
+ it "is the value of the :channel key in the options hash passed to Event.new" do
77
77
  event.channel.should == options[:channel]
78
78
  end
79
79
 
@@ -155,17 +155,6 @@ describe MTK::Events::Event do
155
155
  end
156
156
  end
157
157
 
158
- describe "#duration_in_pulses" do
159
- it "multiplies the #length times the argument and rounds to the nearest integer" do
160
- event.duration_in_pulses(111).should == (event.length * 111).round
161
- end
162
-
163
- it "is 0 when the #duration is nil" do
164
- event.duration = nil
165
- event.duration_in_pulses(111).should == 0
166
- end
167
- end
168
-
169
158
  describe "from_h" do
170
159
  it "constructs an Event using a hash" do
171
160
  EVENT.from_h(hash).should == event
@@ -187,6 +176,11 @@ describe MTK::Events::Event do
187
176
  EVENT.new(type, :duration => 1.5).duration_in_pulses(59).should == 89
188
177
  end
189
178
 
179
+ it "is 0 when the #duration is nil" do
180
+ event.duration = nil
181
+ event.duration_in_pulses(111).should == 0
182
+ end
183
+
190
184
  it "is always positive (uses absolute value of the duration used to construct the Event)" do
191
185
  EVENT.new(type, :duration => -1).duration_in_pulses(60).should == 60
192
186
  end
@@ -160,12 +160,12 @@ describe MTK do
160
160
  Note(q,mf,C4).should == NOTE.new(C4,q,mf)
161
161
  end
162
162
 
163
- it "fills in a missing duration type from an number" do
163
+ it "fills in a missing duration argument from an number" do
164
164
  Note(C4,mf,5.25).should == NOTE.new(C4,MTK.Duration(5.25),mf)
165
165
  end
166
166
 
167
- it '' do
168
- Note(MTK::Lang::Pitches::C4, MTK::Lang::Intensities::o, 5.25).should == Note(C4, 5.25, 0.75)
167
+ it 'fills in a missing intensity and duration arguments from numbers' do
168
+ Note(MTK::Lang::Pitches::C4, MTK::Lang::Intensities::f, 5.25).should == Note(C4, 5.25, 0.75)
169
169
  end
170
170
 
171
171
  end
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+
3
+ describe MTK::Events::Rest do
4
+
5
+ REST = MTK::Events::Rest
6
+
7
+ let(:duration) { -7.5 }
8
+ let(:channel) { 3 }
9
+ let(:rest) { REST.new duration, channel }
10
+ let(:hash) { {type: :rest, duration: duration, channel: channel} }
11
+
12
+
13
+ describe ".new" do
14
+ it "requires a duration as the first argument" do
15
+ lambda{ REST.new() }.should raise_error
16
+ end
17
+
18
+ it "coerces the duration to an MTK::Core::Duration" do
19
+ rest.duration.should be_a MTK::Core::Duration
20
+ end
21
+
22
+ it "forces the duration value negative to be a rest, if needed" do
23
+ REST.new(5).duration.should == -5
24
+ end
25
+ end
26
+
27
+ describe "#type" do
28
+ it "is :rest" do
29
+ rest.type.should == :rest
30
+ end
31
+
32
+ it "is a read-only attribute" do
33
+ lambda{ rest.type = :anything }.should raise_error
34
+ end
35
+ end
36
+
37
+ describe "#duration" do
38
+ it "is the value of the first argument to Rest.new, if the value was negative (indicating a rest)" do
39
+ rest.duration.should == duration
40
+ end
41
+ end
42
+
43
+ describe "#duration=" do
44
+ it "sets #duration" do
45
+ rest.duration = -42
46
+ rest.duration.should == -42
47
+ end
48
+
49
+ it "forces coerces argument to a MTK::Core::Duration" do
50
+ rest.duration = 42
51
+ rest.duration.should be_a MTK::Core::Duration
52
+ end
53
+
54
+ it "forces the duration value negative to be a rest, if needed" do
55
+ rest.duration = 42
56
+ rest.duration.should == -42
57
+ end
58
+ end
59
+
60
+ describe "#channel" do
61
+ it "is the value of the :channel key in the options hash passed to Rest.new" do
62
+ rest.channel.should == channel
63
+ end
64
+
65
+ it "defaults to nil" do
66
+ REST.new(duration).channel.should be_nil
67
+ end
68
+ end
69
+
70
+ describe "#channel=" do
71
+ it "sets #channel" do
72
+ rest.channel = 12
73
+ rest.channel.should == 12
74
+ end
75
+ end
76
+
77
+ describe "#midi_value" do
78
+ it "is nil" do
79
+ rest.midi_value.should be_nil
80
+ end
81
+ end
82
+
83
+ describe "#length" do
84
+ it "is the absolute value of duration" do
85
+ rest.duration = -5
86
+ rest.length.should == 5
87
+ end
88
+ end
89
+
90
+ describe "#rest?" do
91
+ it "is true when the duration is negative" do
92
+ rest.rest?.should be_true
93
+ end
94
+
95
+ it "is true event with a positive duration, because the duration was forced negative" do
96
+ rest.duration = 5
97
+ rest.rest?.should be_true
98
+ end
99
+ end
100
+
101
+ describe "from_h" do
102
+ it "constructs an Event using a hash" do
103
+ REST.from_h(hash).should == rest
104
+ end
105
+ end
106
+
107
+ describe "#to_h" do
108
+ it "is a hash containing all the attributes of the Rest" do
109
+ rest.to_h.should == hash
110
+ end
111
+ end
112
+
113
+
114
+ describe "#==" do
115
+ it "is true when the duration and channel are equal" do
116
+ rest.should == REST.new(duration, channel)
117
+ end
118
+
119
+ it "is false when the durations are not equal" do
120
+ rest.should_not == REST.new(duration+1, channel)
121
+ end
122
+
123
+ it "is false when the channels are not equal" do
124
+ rest.should_not == REST.new(duration, channel+1)
125
+ end
126
+ end
127
+
128
+ describe "#to_s" do
129
+ it "includes #duration to 2 decimal places" do
130
+ REST.new(Duration(-1/3.0)).to_s.should == "Rest(-0.33 beat)"
131
+ end
132
+ end
133
+
134
+ describe "#inspect" do
135
+ it 'is "#<MTK::Events::Rest:{object_id} @duration={duration.inspect}, @channel={channel}>"' do
136
+ rest.inspect.should == "#<MTK::Events::Rest:#{rest.object_id} @duration=#{rest.duration.inspect}, @channel=#{channel}>"
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+
143
+ describe MTK do
144
+
145
+ describe '#Rest' do
146
+
147
+ it "acts like new for multiple arguments" do
148
+ MTK::Rest(4,2).should == REST.new(4,2)
149
+ end
150
+
151
+ it "acts like new for an Array of arguments by unpacking (splatting) them" do
152
+ MTK::Rest([4,2]).should == REST.new(4,2)
153
+ end
154
+
155
+ it "returns the argument if it's already a Rest" do
156
+ rest = REST.new(4,2)
157
+ MTK::Rest(rest).should be_equal(rest)
158
+ end
159
+
160
+ it "makes a Rest of the same duration and channel from other Event types" do
161
+ event = MTK::Events::Event.new(:event_type, duration:5, channel:3)
162
+ MTK::Rest(event).should == REST.new(5,3)
163
+ end
164
+
165
+ it "handles a single Numeric argument" do
166
+ MTK::Rest(4).should == REST.new(4)
167
+ end
168
+
169
+ it "handles a single Duration argument" do
170
+ MTK.Rest(MTK.Duration(4)).should == REST.new(4)
171
+ end
172
+
173
+ it "raises an error for types it doesn't understand" do
174
+ lambda{ MTK::Rest({:not => :compatible}) }.should raise_error
175
+ end
176
+
177
+ it "handles out of order arguments for recognized Duration types" do
178
+ MTK::Rest(2,q).should == REST.new(q,2)
179
+ end
180
+
181
+ end
182
+
183
+ end
184
+