jmtk 0.0.3.3-java → 0.4-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.
- checksums.yaml +15 -0
- data/DEVELOPMENT_NOTES.md +20 -0
- data/INTRO.md +63 -31
- data/README.md +9 -3
- data/Rakefile +42 -42
- data/bin/jmtk +75 -32
- 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,279 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'mtk/io/midi_output'
|
4
|
+
require 'mtk/lang/tutorial_lesson'
|
5
|
+
|
6
|
+
module MTK
|
7
|
+
module Lang
|
8
|
+
|
9
|
+
# @private
|
10
|
+
class Tutorial
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@current_lesson_index = 0
|
14
|
+
|
15
|
+
@lessons = [
|
16
|
+
###################### 79 character width for description #####################
|
17
|
+
{
|
18
|
+
title: 'Pitch Classes (diatonic)',
|
19
|
+
description: "
|
20
|
+
The diatonic pitch classes are the 7 white keys on a piano in a given octave.
|
21
|
+
They can be used to play, for example, the C major or A natural minor scales.
|
22
|
+
|
23
|
+
To play a diatonic pitch class, enter #{'one'.bold.underline} of the following letters
|
24
|
+
(must be upper-case):
|
25
|
+
|
26
|
+
C D E F G A B
|
27
|
+
",
|
28
|
+
validation: :diatonic_pitch_class,
|
29
|
+
},
|
30
|
+
{
|
31
|
+
title: 'Pitch Classes (chromatic)',
|
32
|
+
description: "
|
33
|
+
The chromatic pitch classes are the 12 white or black keys on a piano in a
|
34
|
+
given octave. They form the basis of all 'western' music theory.
|
35
|
+
|
36
|
+
To play a chromatic pitch class, enter any of the 7 diatonic pitch classes
|
37
|
+
immediately followed by 0, 1, or 2 flats (b) or sharps (#). Each flat (b)
|
38
|
+
lowers the pitch by a half step and each sharp (#) raises by a half step.
|
39
|
+
|
40
|
+
Here are some examples, try entering #{'one'.bold.underline} of the following:
|
41
|
+
|
42
|
+
C# Bb A## Ebb F
|
43
|
+
",
|
44
|
+
validation: :pitch_class,
|
45
|
+
},
|
46
|
+
{
|
47
|
+
title: 'Pitches',
|
48
|
+
description: "
|
49
|
+
To play a pitch, enter a (chromatic) pitch class immediately following by an
|
50
|
+
octave number.
|
51
|
+
|
52
|
+
There is no universal standard numbering scheme for octaves in music.
|
53
|
+
This library uses \"scientific pitch notation\", which defines
|
54
|
+
C4 as \"Middle C\" and A4 as the standard tuning pitch A440.
|
55
|
+
|
56
|
+
In this library, C-1 is the lowest note available and G9 is the highest,
|
57
|
+
corresponding to MIDI pitch values 0 and 127. If you try to play a pitch
|
58
|
+
outside this range, it will be mapped to the closest available pitch.
|
59
|
+
|
60
|
+
Here are some examples, try entering #{'one'.bold.underline} of the following:
|
61
|
+
|
62
|
+
G3 Eb4 F#5 B-1 C##9 Dbb6
|
63
|
+
",
|
64
|
+
validation: :pitch,
|
65
|
+
},
|
66
|
+
{
|
67
|
+
title: 'Sequences',
|
68
|
+
description: "
|
69
|
+
To play a sequence of pitches or pitch classes, enter them with spaces in
|
70
|
+
between. Pitches and pitch classes may be interchanged in a given sequence.
|
71
|
+
Any pitch class will output a pitch closest to the previous pitch (starting
|
72
|
+
from C4 at the beginning of the sequence).
|
73
|
+
|
74
|
+
Here is an example (Note, unlike previous lessons, enter the #{'entire line'.bold.underline}):
|
75
|
+
|
76
|
+
C5 C G5 G A A G
|
77
|
+
",
|
78
|
+
validation: :bare_sequence,
|
79
|
+
},
|
80
|
+
{
|
81
|
+
title: 'Repetition',
|
82
|
+
description: "
|
83
|
+
To repeat a note, suffix a pitch or pitch class with *N, where N is the
|
84
|
+
number of repetitions. You can also wrap a subsequence of notes with
|
85
|
+
parentheses and repeat them. Here is an example sequence with repetition:
|
86
|
+
|
87
|
+
C*3 D (E D)*2 C
|
88
|
+
|
89
|
+
You can also nest repetitions (optional whitespace added for readability):
|
90
|
+
|
91
|
+
( C5 (E G)*2 )*2
|
92
|
+
",
|
93
|
+
validation: /\*/,
|
94
|
+
},
|
95
|
+
|
96
|
+
].map{|lesson_options| TutorialLesson.new(lesson_options) }
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def run(output)
|
101
|
+
puts SEPARATOR
|
102
|
+
puts
|
103
|
+
puts "Welcome to the MTK syntax tutorial!".bold.yellow
|
104
|
+
puts
|
105
|
+
puts "MTK is the Music Tool Kit for Ruby, which includes a custom syntax for"
|
106
|
+
puts "generating musical patterns. This tutorial has a variety of lessons to teach"
|
107
|
+
puts "you the syntax. It assumes basic familiarity with music theory."
|
108
|
+
puts
|
109
|
+
puts "Make sure your speakers are on and the volume is turned up."
|
110
|
+
puts
|
111
|
+
puts "This is a work in progress. Check back in future versions for more lessons."
|
112
|
+
puts
|
113
|
+
puts "#{'NOTE:'.bold} MTK syntax is case-sensitive. Upper vs lower-case matters."
|
114
|
+
puts
|
115
|
+
|
116
|
+
output = ensure_output(output)
|
117
|
+
loop{ select_lesson.run(output) }
|
118
|
+
|
119
|
+
rescue SystemExit, Interrupt
|
120
|
+
puts
|
121
|
+
puts
|
122
|
+
puts "Goodbye!"
|
123
|
+
puts
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# table of contents
|
128
|
+
def toc
|
129
|
+
@lessons.map.with_index do |lesson,index|
|
130
|
+
"#{'> '.bold.yellow if @current_lesson_index == index}#{index+1}: #{lesson}"
|
131
|
+
end.join("\n")
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def select_lesson
|
136
|
+
puts
|
137
|
+
puts SEPARATOR
|
138
|
+
puts
|
139
|
+
puts "Lessons".bold.yellow
|
140
|
+
puts
|
141
|
+
|
142
|
+
all_done = @current_lesson_index >= @lessons.length
|
143
|
+
lesson = nil
|
144
|
+
while lesson == nil
|
145
|
+
puts toc
|
146
|
+
puts
|
147
|
+
if all_done
|
148
|
+
puts "You've completed the last lesson!"
|
149
|
+
puts "To explore more, try running #{$0} with the --eval option."
|
150
|
+
puts
|
151
|
+
end
|
152
|
+
puts "Press Ctrl+C to exit at any time.".bold
|
153
|
+
puts
|
154
|
+
print "Select a lesson number, or press enter to ".blue
|
155
|
+
if all_done
|
156
|
+
puts "exit:".blue
|
157
|
+
else
|
158
|
+
puts "go to the next one ".blue + "(indicated by " + '>'.bold.yellow + "):"
|
159
|
+
end
|
160
|
+
|
161
|
+
input = gets.strip
|
162
|
+
lesson_index = case input
|
163
|
+
when /^\d+$/
|
164
|
+
input.to_i - 1
|
165
|
+
when ''
|
166
|
+
if all_done then raise SystemExit.new else @current_lesson_index end
|
167
|
+
else
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
lesson = @lessons[lesson_index] if lesson_index and lesson_index >= 0
|
172
|
+
|
173
|
+
puts "Invalid lesson number: #{input}\n\n" if lesson.nil?
|
174
|
+
end
|
175
|
+
|
176
|
+
@current_lesson_index = lesson_index + 1
|
177
|
+
lesson
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
#####################################################
|
182
|
+
private
|
183
|
+
|
184
|
+
def ensure_output(output)
|
185
|
+
unless output
|
186
|
+
puts SEPARATOR
|
187
|
+
puts
|
188
|
+
puts "Select an output".bold.yellow
|
189
|
+
puts
|
190
|
+
puts "You need to select a MIDI output device before you can hear sound."
|
191
|
+
puts
|
192
|
+
|
193
|
+
require 'rbconfig'
|
194
|
+
case RbConfig::CONFIG['host_os'].downcase
|
195
|
+
when /darwin/
|
196
|
+
puts "You appear to be on OS X."
|
197
|
+
puts "You can use the \"Apple DLS Synthesizer\" to hear sound with no extra setup."
|
198
|
+
puts "It's recommended you use this unless you have a good reason to do otherwise."
|
199
|
+
when /win/
|
200
|
+
puts "You appear to be on Windows"
|
201
|
+
puts "You can use the \"Microsoft Synthesizer\" to hear sound with no extra setup."
|
202
|
+
puts "It's recommended you use this unless you have a good reason to do otherwise."
|
203
|
+
end
|
204
|
+
|
205
|
+
until output
|
206
|
+
puts
|
207
|
+
puts "Available MIDI outputs:".bold.yellow
|
208
|
+
MTK::IO::MIDIOutput.devices.each.with_index do |device,index|
|
209
|
+
name = device.name
|
210
|
+
name += " (#{device.id})" if device.respond_to? :id
|
211
|
+
puts "#{index+1}: #{name}"
|
212
|
+
end
|
213
|
+
|
214
|
+
puts "Select an output number:".blue
|
215
|
+
|
216
|
+
input = STDIN.gets.strip
|
217
|
+
if input =~ /^\d+$/
|
218
|
+
number = input.to_i
|
219
|
+
if number > 0
|
220
|
+
device = MTK::IO::MIDIOutput.devices[number-1]
|
221
|
+
output = MTK::IO::MIDIOutput.open(device) if device
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
puts "Invalid selection.".red unless device
|
226
|
+
end
|
227
|
+
|
228
|
+
puts
|
229
|
+
puts "OK! Using output '#{output.name}'".bold.green
|
230
|
+
puts "#{'NOTE:'.bold} You can skip the output selection step in the future by running "
|
231
|
+
puts "#{$0} with the --output option."
|
232
|
+
puts "Press enter to continue".blue
|
233
|
+
gets
|
234
|
+
end
|
235
|
+
output
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
####################################################################################################
|
244
|
+
# Patch String to support terminal colors
|
245
|
+
|
246
|
+
# @private
|
247
|
+
class String
|
248
|
+
{
|
249
|
+
bold: 1,
|
250
|
+
underline: 4,
|
251
|
+
red: 31,
|
252
|
+
green: 32,
|
253
|
+
yellow: 33,
|
254
|
+
blue: 36 # really this cyan but the standard blue is too dark IMO
|
255
|
+
}.each do |effect,code|
|
256
|
+
if $tutorial_color
|
257
|
+
define_method effect do
|
258
|
+
"\e[#{code}m#{self}\e[0m"
|
259
|
+
end
|
260
|
+
else
|
261
|
+
define_method effect do
|
262
|
+
self
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
####################################################################################################
|
271
|
+
# And now that we've got some colors we can define colored constants
|
272
|
+
module MTK
|
273
|
+
module Lang
|
274
|
+
# @private
|
275
|
+
class Tutorial
|
276
|
+
SEPARATOR = "===============================================================================".bold.yellow
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -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
|