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
@@ -133,25 +133,35 @@ grammar MTK_Grammar
|
|
133
133
|
end
|
134
134
|
|
135
135
|
rule pitch_class
|
136
|
-
(
|
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
|
-
(
|
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' | '
|
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? [
|
164
|
+
( rest:'-'? multiplier:number? [whqesrx] ('.'|'t')* ) {
|
155
165
|
MTK::Core::Duration.from_s(to_s)
|
156
166
|
}
|
157
167
|
end
|
data/lib/mtk/lang/parser.rb
CHANGED
@@ -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
|
15
|
+
# Parser for the {file:lib/mtk/lang/mtk_grammar.citrus MTK syntax}
|
16
16
|
class Parser
|
17
17
|
|
18
|
-
|
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
|
-
|
22
|
-
puts
|
23
|
-
|
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
|
-
#
|
8
|
-
|
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
|
data/lib/mtk/lang/pitches.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
9
|
-
#
|
10
|
-
# @
|
11
|
-
#
|
12
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|