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.
Files changed (56) hide show
  1. checksums.yaml +15 -0
  2. data/DEVELOPMENT_NOTES.md +20 -0
  3. data/INTRO.md +63 -31
  4. data/README.md +9 -3
  5. data/Rakefile +42 -42
  6. data/bin/jmtk +75 -32
  7. data/bin/mtk +75 -32
  8. data/examples/drum_pattern.rb +2 -2
  9. data/examples/dynamic_pattern.rb +1 -1
  10. data/examples/helpers/output_selector.rb +71 -0
  11. data/examples/notation.rb +5 -1
  12. data/examples/tone_row_melody.rb +1 -1
  13. data/lib/mtk.rb +1 -0
  14. data/lib/mtk/core/duration.rb +18 -3
  15. data/lib/mtk/core/intensity.rb +5 -3
  16. data/lib/mtk/core/interval.rb +21 -14
  17. data/lib/mtk/core/pitch.rb +2 -0
  18. data/lib/mtk/core/pitch_class.rb +6 -3
  19. data/lib/mtk/events/event.rb +2 -1
  20. data/lib/mtk/events/note.rb +1 -1
  21. data/lib/mtk/events/parameter.rb +1 -0
  22. data/lib/mtk/events/rest.rb +85 -0
  23. data/lib/mtk/events/timeline.rb +6 -2
  24. data/lib/mtk/io/jsound_input.rb +9 -3
  25. data/lib/mtk/io/midi_file.rb +38 -2
  26. data/lib/mtk/io/midi_input.rb +1 -1
  27. data/lib/mtk/io/midi_output.rb +95 -4
  28. data/lib/mtk/io/unimidi_input.rb +7 -3
  29. data/lib/mtk/lang/durations.rb +31 -26
  30. data/lib/mtk/lang/intensities.rb +29 -30
  31. data/lib/mtk/lang/intervals.rb +108 -41
  32. data/lib/mtk/lang/mtk_grammar.citrus +14 -4
  33. data/lib/mtk/lang/parser.rb +10 -5
  34. data/lib/mtk/lang/pitch_classes.rb +45 -17
  35. data/lib/mtk/lang/pitches.rb +169 -32
  36. data/lib/mtk/lang/tutorial.rb +279 -0
  37. data/lib/mtk/lang/tutorial_lesson.rb +87 -0
  38. data/lib/mtk/sequencers/event_builder.rb +29 -8
  39. data/spec/mtk/core/duration_spec.rb +14 -1
  40. data/spec/mtk/core/intensity_spec.rb +1 -1
  41. data/spec/mtk/events/event_spec.rb +10 -16
  42. data/spec/mtk/events/note_spec.rb +3 -3
  43. data/spec/mtk/events/rest_spec.rb +184 -0
  44. data/spec/mtk/events/timeline_spec.rb +5 -1
  45. data/spec/mtk/io/midi_file_spec.rb +13 -2
  46. data/spec/mtk/io/midi_output_spec.rb +42 -9
  47. data/spec/mtk/lang/durations_spec.rb +5 -5
  48. data/spec/mtk/lang/intensities_spec.rb +5 -5
  49. data/spec/mtk/lang/intervals_spec.rb +139 -13
  50. data/spec/mtk/lang/parser_spec.rb +65 -25
  51. data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
  52. data/spec/mtk/lang/pitches_spec.rb +0 -15
  53. data/spec/mtk/patterns/chain_spec.rb +7 -7
  54. data/spec/mtk/patterns/for_each_spec.rb +2 -2
  55. data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
  56. 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?(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