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