mtk 0.0.3.3 → 0.4

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 (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
+