mtk 0.0.3.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/INTRO.md +63 -31
- data/Rakefile +3 -1
- data/bin/mtk +75 -32
- data/examples/drum_pattern.rb +2 -2
- data/examples/dynamic_pattern.rb +1 -1
- data/examples/helpers/output_selector.rb +71 -0
- data/examples/notation.rb +5 -1
- data/examples/tone_row_melody.rb +1 -1
- data/lib/mtk.rb +1 -0
- data/lib/mtk/core/duration.rb +18 -3
- data/lib/mtk/core/intensity.rb +5 -3
- data/lib/mtk/core/interval.rb +21 -14
- data/lib/mtk/core/pitch.rb +2 -0
- data/lib/mtk/core/pitch_class.rb +6 -3
- data/lib/mtk/events/event.rb +2 -1
- data/lib/mtk/events/note.rb +1 -1
- data/lib/mtk/events/parameter.rb +1 -0
- data/lib/mtk/events/rest.rb +85 -0
- data/lib/mtk/events/timeline.rb +6 -2
- data/lib/mtk/io/jsound_input.rb +9 -3
- data/lib/mtk/io/midi_file.rb +38 -2
- data/lib/mtk/io/midi_input.rb +1 -1
- data/lib/mtk/io/midi_output.rb +95 -4
- data/lib/mtk/io/unimidi_input.rb +7 -3
- data/lib/mtk/lang/durations.rb +31 -26
- data/lib/mtk/lang/intensities.rb +29 -30
- data/lib/mtk/lang/intervals.rb +108 -41
- data/lib/mtk/lang/mtk_grammar.citrus +14 -4
- data/lib/mtk/lang/parser.rb +10 -5
- data/lib/mtk/lang/pitch_classes.rb +45 -17
- data/lib/mtk/lang/pitches.rb +169 -32
- data/lib/mtk/lang/tutorial.rb +279 -0
- data/lib/mtk/lang/tutorial_lesson.rb +87 -0
- data/lib/mtk/sequencers/event_builder.rb +29 -8
- data/spec/mtk/core/duration_spec.rb +14 -1
- data/spec/mtk/core/intensity_spec.rb +1 -1
- data/spec/mtk/events/event_spec.rb +10 -16
- data/spec/mtk/events/note_spec.rb +3 -3
- data/spec/mtk/events/rest_spec.rb +184 -0
- data/spec/mtk/events/timeline_spec.rb +5 -1
- data/spec/mtk/io/midi_file_spec.rb +13 -2
- data/spec/mtk/io/midi_output_spec.rb +42 -9
- data/spec/mtk/lang/durations_spec.rb +5 -5
- data/spec/mtk/lang/intensities_spec.rb +5 -5
- data/spec/mtk/lang/intervals_spec.rb +139 -13
- data/spec/mtk/lang/parser_spec.rb +65 -25
- data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
- data/spec/mtk/lang/pitches_spec.rb +0 -15
- data/spec/mtk/patterns/chain_spec.rb +7 -7
- data/spec/mtk/patterns/for_each_spec.rb +2 -2
- data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
- 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?
|
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
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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::
|
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
|
+
|