mtk 0.0.3.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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