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
@@ -133,25 +133,35 @@ grammar MTK_Grammar
133
133
  end
134
134
 
135
135
  rule pitch_class
136
- ( [A-Ga-g] [#b]*2 ) {
136
+ ( diatonic_pitch_class accidental? ) {
137
137
  MTK::Core::PitchClass[to_s]
138
138
  }
139
139
  end
140
140
 
141
+ rule diatonic_pitch_class
142
+ ( [A-G] ) {
143
+ MTK::Core::PitchClass[to_s]
144
+ }
145
+ end
146
+
147
+ rule accidental
148
+ ('#'1*2 | 'b'1*2)
149
+ end
150
+
141
151
  rule interval
142
- ( [Pp] [1458] | ('maj'|'min'|[Mm]) [2367] | 'TT' | 'tt' ) {
152
+ ( 'P' [1458] | [Mm] [2367] | 'a' [1-7] | 'd' [2-8] | 'TT' ) {
143
153
  MTK::Core::Interval.from_s(to_s)
144
154
  }
145
155
  end
146
156
 
147
157
  rule intensity
148
- ( ('p'1*3 | 'mp' | 'mf' | 'o' | 'f'2*3) ('+'|'-')? ) {
158
+ ( ('p'1*3 | 'mp' | 'mf' | 'f'1*3) ('+'|'-')? ) {
149
159
  MTK::Core::Intensity.from_s(to_s)
150
160
  }
151
161
  end
152
162
 
153
163
  rule duration
154
- ( rest:'-'? multiplier:number? [whqisrx] ('.'|'t')* ) {
164
+ ( rest:'-'? multiplier:number? [whqesrx] ('.'|'t')* ) {
155
165
  MTK::Core::Duration.from_s(to_s)
156
166
  }
157
167
  end
@@ -12,15 +12,20 @@ Citrus.load File.join(File.dirname(__FILE__),'mtk_grammar')
12
12
  module MTK
13
13
  module Lang
14
14
 
15
- # Parser for the {file:lib/mtk/lang/mtk_grammar.citrus MTK grammar}
15
+ # Parser for the {file:lib/mtk/lang/mtk_grammar.citrus MTK syntax}
16
16
  class Parser
17
17
 
18
- def self.parse(syntax, root=:root, dump=false)
18
+ # Parse the given MTK syntax according to the {file:lib/mtk/lang/mtk_grammar.citrus grammar rules}
19
+ # @return [Sequencers::LegatoSequencer] by default
20
+ # @return [Core,Patterns,Sequencers] a core object, pattern or sequencer when an optional grammar rule
21
+ # is given. Depends on the rule.
22
+ # @raise [Citrus::ParseError] for invalid syntax
23
+ def self.parse(syntax, grammar_rule=:root, dump=false)
19
24
  syntax = syntax.to_s.strip
20
25
  return nil if syntax.empty?
21
- matcher = ::MTK_Grammar.parse(syntax, :root => root)
22
- puts matcher.dump if dump
23
- matcher.value
26
+ match = ::MTK_Grammar.parse(syntax, root: grammar_rule)
27
+ puts match.dump if dump
28
+ match.value
24
29
  end
25
30
 
26
31
  end
@@ -1,29 +1,57 @@
1
1
  module MTK
2
2
  module Lang
3
3
 
4
- # Defines a constant for each {PitchClass} in the Western chromatic scale.
4
+ # Defines a constant for each {Core::PitchClass} in the Western chromatic scale.
5
+ #
6
+ # Because '#' is not a valid identifier character in Ruby. All chromatic pitch classes are defined as
7
+ # the flat of a diatonic pitch class, for example Eb is a constant because D# is not a valid Ruby constant name.
8
+ #
9
+ # To help automate the documentation, the constants are listed under "Instance Attribute Summary" on this page.
10
+ #
11
+ # @see Core::PitchClass
12
+ #
5
13
  module PitchClasses
6
14
 
7
- # The values of all constants defined in this module
8
- PITCH_CLASSES = MTK::Core::PitchClass::PITCH_CLASSES
15
+ # @private
16
+ # @!macro [attach] define_pitch_class
17
+ # PitchClass $1 $3
18
+ # @!attribute [r]
19
+ # @return [MTK::Core::PitchClass] PitchClass $1 (value $2)
20
+ def self.define_pitch_class name, value, more_info=nil
21
+ const_set name, MTK::Core::PitchClass.from_name(name)
22
+ end
23
+
24
+ define_pitch_class 'C', 0
25
+
26
+ define_pitch_class 'Db', 1, '(also known as C#)'
27
+
28
+ define_pitch_class 'D', 2
29
+
30
+ define_pitch_class 'Eb', 3, '(also known as D#)'
31
+
32
+ define_pitch_class 'E', 4
33
+
34
+ define_pitch_class 'F', 5
35
+
36
+ define_pitch_class 'Gb', 6, '(also known as F#)'
37
+
38
+ define_pitch_class 'G', 7
39
+
40
+ define_pitch_class 'Ab', 8, '(also known as G#)'
41
+
42
+ define_pitch_class 'A', 9
43
+
44
+ define_pitch_class 'Bb', 10, '(also known as A#)'
45
+
46
+ define_pitch_class 'B', 11
47
+
48
+ # All constants defined in this module
49
+ PITCH_CLASSES = [C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B].freeze
9
50
 
10
51
  # The names of all constants defined in this module
52
+ # @see MTK::Core::PitchClass::NAMES
11
53
  PITCH_CLASS_NAMES = MTK::Core::PitchClass::NAMES
12
54
 
13
- PITCH_CLASSES.each { |pc| const_set pc.name, pc }
14
-
15
- # Lookup the value of an pitch class constant by name.
16
- # @example lookup value of 'C'
17
- # MTK::Core::PitchClasses['C']
18
- # @see Groups::PitchClass.[]
19
- def self.[](name)
20
- begin
21
- const_get name
22
- rescue
23
- nil
24
- end
25
- end
26
-
27
55
  end
28
56
  end
29
57
  end
@@ -2,51 +2,188 @@ module MTK
2
2
  module Lang
3
3
 
4
4
  # Defines a constants for each {Core::Pitch} in the standard MIDI range using scientific pitch notation.
5
+ # The constants range from C-1 (MIDI value 0) to G9 (MIDI value)
5
6
  #
6
- # See http://en.wikipedia.org/wiki/Scientific_pitch_notation
7
+ # Because the character '#' cannot be used in the name of a constant,
8
+ # the "black key" pitches are all named as flats with 'b' (for example, Gb3 or Db4).
9
+ # And because the character '-' (minus) cannot be used in the name of a constant,
10
+ # the low pitches use '_' (underscore) in place of '-' (minus) (for example C_1).
7
11
  #
8
- # @note Because the character '#' cannot be used in the name of a constant,
9
- # the "black key" pitches are all named as flats with 'b' (for example, Gb3 or Cb4)
10
- # @note Because the character '-' (minus) cannot be used in the name of a constant,
11
- # the low pitches use '_' (underscore) in place of '-' (minus) (for example C_1).
12
- module Pitches
12
+ # To help automate the documentation, the constants are listed under "Instance Attribute Summary" on this page.
13
+ #
14
+ # @see Core::Pitch
15
+ # @see Events::Note
16
+ # @see http://en.wikipedia.org/wiki/Scientific_pitch_notation
17
+ module Pitches
18
+
19
+ # @private
20
+ # @!macro [attach] define_pitch
21
+ # Pitch $1 (MIDI pitch $2)
22
+ # @!attribute [r]
23
+ # @return [MTK::Core::Pitch] Pitch $1 (value $2)
24
+ def self.define_pitch name, value
25
+ pitch = MTK::Core::Pitch.from_i(value)
26
+ const_set name, pitch
27
+ PITCHES << pitch
28
+ PITCH_NAMES << name
29
+ end
13
30
 
14
31
  # The values of all constants defined in this module
32
+ # @note This is populated dynamically so the documentation does not reflect the actual value
15
33
  PITCHES = []
16
34
 
17
35
  # The names of all constants defined in this module
36
+ # @note This is populated dynamically so the documentation does not reflect the actual value
18
37
  PITCH_NAMES = []
19
38
 
20
- 128.times do |note_number|
21
- pitch = MTK::Core::Pitch.from_i( note_number )
22
- PITCHES << pitch
23
-
24
- octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
25
- name = "#{pitch.pitch_class}#{octave_str}"
26
- PITCH_NAMES << name
27
-
28
- # TODO? also define lower case pseudo constants for consistency with the grammar?
29
-
30
- const_set name, pitch
31
- end
39
+ define_pitch 'C_1', 0
40
+ define_pitch 'Db_1', 1
41
+ define_pitch 'D_1', 2
42
+ define_pitch 'Eb_1', 3
43
+ define_pitch 'E_1', 4
44
+ define_pitch 'F_1', 5
45
+ define_pitch 'Gb_1', 6
46
+ define_pitch 'G_1', 7
47
+ define_pitch 'Ab_1', 8
48
+ define_pitch 'A_1', 9
49
+ define_pitch 'Bb_1', 10
50
+ define_pitch 'B_1', 11
51
+ define_pitch 'C0', 12
52
+ define_pitch 'Db0', 13
53
+ define_pitch 'D0', 14
54
+ define_pitch 'Eb0', 15
55
+ define_pitch 'E0', 16
56
+ define_pitch 'F0', 17
57
+ define_pitch 'Gb0', 18
58
+ define_pitch 'G0', 19
59
+ define_pitch 'Ab0', 20
60
+ define_pitch 'A0', 21
61
+ define_pitch 'Bb0', 22
62
+ define_pitch 'B0', 23
63
+ define_pitch 'C1', 24
64
+ define_pitch 'Db1', 25
65
+ define_pitch 'D1', 26
66
+ define_pitch 'Eb1', 27
67
+ define_pitch 'E1', 28
68
+ define_pitch 'F1', 29
69
+ define_pitch 'Gb1', 30
70
+ define_pitch 'G1', 31
71
+ define_pitch 'Ab1', 32
72
+ define_pitch 'A1', 33
73
+ define_pitch 'Bb1', 34
74
+ define_pitch 'B1', 35
75
+ define_pitch 'C2', 36
76
+ define_pitch 'Db2', 37
77
+ define_pitch 'D2', 38
78
+ define_pitch 'Eb2', 39
79
+ define_pitch 'E2', 40
80
+ define_pitch 'F2', 41
81
+ define_pitch 'Gb2', 42
82
+ define_pitch 'G2', 43
83
+ define_pitch 'Ab2', 44
84
+ define_pitch 'A2', 45
85
+ define_pitch 'Bb2', 46
86
+ define_pitch 'B2', 47
87
+ define_pitch 'C3', 48
88
+ define_pitch 'Db3', 49
89
+ define_pitch 'D3', 50
90
+ define_pitch 'Eb3', 51
91
+ define_pitch 'E3', 52
92
+ define_pitch 'F3', 53
93
+ define_pitch 'Gb3', 54
94
+ define_pitch 'G3', 55
95
+ define_pitch 'Ab3', 56
96
+ define_pitch 'A3', 57
97
+ define_pitch 'Bb3', 58
98
+ define_pitch 'B3', 59
99
+ define_pitch 'C4', 60
100
+ define_pitch 'Db4', 61
101
+ define_pitch 'D4', 62
102
+ define_pitch 'Eb4', 63
103
+ define_pitch 'E4', 64
104
+ define_pitch 'F4', 65
105
+ define_pitch 'Gb4', 66
106
+ define_pitch 'G4', 67
107
+ define_pitch 'Ab4', 68
108
+ define_pitch 'A4', 69
109
+ define_pitch 'Bb4', 70
110
+ define_pitch 'B4', 71
111
+ define_pitch 'C5', 72
112
+ define_pitch 'Db5', 73
113
+ define_pitch 'D5', 74
114
+ define_pitch 'Eb5', 75
115
+ define_pitch 'E5', 76
116
+ define_pitch 'F5', 77
117
+ define_pitch 'Gb5', 78
118
+ define_pitch 'G5', 79
119
+ define_pitch 'Ab5', 80
120
+ define_pitch 'A5', 81
121
+ define_pitch 'Bb5', 82
122
+ define_pitch 'B5', 83
123
+ define_pitch 'C6', 84
124
+ define_pitch 'Db6', 85
125
+ define_pitch 'D6', 86
126
+ define_pitch 'Eb6', 87
127
+ define_pitch 'E6', 88
128
+ define_pitch 'F6', 89
129
+ define_pitch 'Gb6', 90
130
+ define_pitch 'G6', 91
131
+ define_pitch 'Ab6', 92
132
+ define_pitch 'A6', 93
133
+ define_pitch 'Bb6', 94
134
+ define_pitch 'B6', 95
135
+ define_pitch 'C7', 96
136
+ define_pitch 'Db7', 97
137
+ define_pitch 'D7', 98
138
+ define_pitch 'Eb7', 99
139
+ define_pitch 'E7', 100
140
+ define_pitch 'F7', 101
141
+ define_pitch 'Gb7', 102
142
+ define_pitch 'G7', 103
143
+ define_pitch 'Ab7', 104
144
+ define_pitch 'A7', 105
145
+ define_pitch 'Bb7', 106
146
+ define_pitch 'B7', 107
147
+ define_pitch 'C8', 108
148
+ define_pitch 'Db8', 109
149
+ define_pitch 'D8', 110
150
+ define_pitch 'Eb8', 111
151
+ define_pitch 'E8', 112
152
+ define_pitch 'F8', 113
153
+ define_pitch 'Gb8', 114
154
+ define_pitch 'G8', 115
155
+ define_pitch 'Ab8', 116
156
+ define_pitch 'A8', 117
157
+ define_pitch 'Bb8', 118
158
+ define_pitch 'B8', 119
159
+ define_pitch 'C9', 120
160
+ define_pitch 'Db9', 121
161
+ define_pitch 'D9', 122
162
+ define_pitch 'Eb9', 123
163
+ define_pitch 'E9', 124
164
+ define_pitch 'F9', 125
165
+ define_pitch 'Gb9', 126
166
+ define_pitch 'G9', 127
32
167
 
33
168
  PITCHES.freeze
34
169
  PITCH_NAMES.freeze
35
170
 
36
- # Lookup the value of an pitch constant by name.
37
- # @example lookup value of 'C3'
38
- # MTK::Core::Pitches['C3']
39
- # @see Core::Pitch.from_s
40
- # @note Unlike {Core::Pitch.from_s} this method will accept either '_' (underscore) or '-' (minus) and treat it like '-' (minus)
41
- # @note Unlike {Core::Pitch.from_s} this method only accepts the accidental 'b'
42
- def self.[](name)
43
- begin
44
- const_get name.sub('-','_')
45
- rescue
46
- nil
47
- end
48
- end
49
-
50
171
  end
51
172
  end
52
173
  end
174
+
175
+
176
+
177
+ =begin
178
+
179
+ Script for generating the define_pitch statements in this file.
180
+ Run in the irb console after requiring 'mtk'
181
+
182
+ 128.times do |value|
183
+ pitch = MTK::Core::Pitch.from_i(value)
184
+ octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
185
+ name = "#{pitch.pitch_class}#{octave_str}"
186
+ puts " define_pitch '#{name}', #{value}"
187
+ end
188
+
189
+ =end
@@ -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