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
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZWM0YWVjOGJiYzM4ZjIxOWUxNWVkOWU3MDYyYWU5MDY1ZmUxMmJhOQ==
5
+ data.tar.gz: !binary |-
6
+ ZDYwNTg1ZGMyMjMyMzBiMzljZDUyM2RkNDI4MTczMjVmY2FkNTJkMA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZTRlNGRmYmEyZDBjMGRkMDYxM2U0ZGQ3YjQ5NThmZDk3MGRhNTBmYjY3MDBm
10
+ NzdiMmM0NWE2NDY0YzFkNTM1YTc4MTNhZmY1MGM5MjFiMWY3NWNjYWM0N2Nh
11
+ ZGYyMThhODA4MGJlOGNiZDIyM2EzYTFhOGY2YjgwOTNlZDNlM2U=
12
+ data.tar.gz: !binary |-
13
+ MGNiZGVmOTAyZDM4MmVjOThmOWY0ZjE3NjgwZjRkN2M2NzY0MTRkMzZmNTk1
14
+ ZjgyMzJkMTNiODAwNzE3Y2M4MDE4MjI3YWY5MWQ1NDc2ZGY0MDlhMzg1MDMz
15
+ MjJiMjQzNzc4NjgyOTRjNDY0MzRmMjY4YzEyYjcyNTY2M2Q4MWY=
data/INTRO.md CHANGED
@@ -20,9 +20,12 @@ it does not change the value in-place. For example:
20
20
 
21
21
  Intensity values are intended to range from 0.0 (minimum intensity) to 1.0 (maximum intensity).
22
22
 
23
- A Duration of 1 is one beat (usually a quarter note, depending on the meter). By convention, negative durations
23
+ A Duration of 1 is one beat (usually a quarter note, depending on the meter, but note the quarter note value in this
24
+ library is 1 beat, so adjust accordingly for non-standard meters). By convention, negative durations
24
25
  indicate a rest that lasts for the absolute value duration.
25
26
 
27
+ Additionally there is a core type Interval to model intervals between pitches and pitch classes.
28
+
26
29
 
27
30
  <br/>
28
31
  ### Events
@@ -30,16 +33,23 @@ indicate a rest that lasts for the absolute value duration.
30
33
  Events group together the core data types together to represent a musical event, like a single Note.
31
34
  Events can be converted to and from MIDI.
32
35
 
33
- * event
34
- * note
36
+ * Event
37
+ * Note
38
+ * Parameter
35
39
 
40
+ Paramter represents any non-note event like a MIDI CC, pitch bend, aftertouch (pressure), etc.
36
41
 
37
- <br/>
38
- ### Core collections
42
+ Events with a negative duration are considered a rest. There is also a dedicated rest class so you
43
+ don't have to specifiy unnecessary properties like pitch for a rest.
44
+
45
+ Events are organized in time via the Timeline
39
46
 
40
- A collection of timed events, such as a melody, a riff, or one track of an entire song.
41
47
 
42
- * timeline
48
+ <br/>
49
+ ### Collections
50
+
51
+ The collection classes need some work... The interface is likely to change so it's best to not rely on them too much
52
+ right now.
43
53
 
44
54
 
45
55
  <br/>
@@ -53,7 +63,7 @@ Structured collections of core data types and other patterns. Used to generate m
53
63
  * line (linear interpolation between 2 points)
54
64
  * function (dynamically generate elements with a lambda)
55
65
 
56
- Future?
66
+ To be added in the Future?
57
67
  * curve (exponential/curved interpolation between points)
58
68
  * permutation (cycle that permutes itself each cycle period)
59
69
  * markov chain
@@ -65,7 +75,23 @@ Future?
65
75
  <br/>
66
76
  ### Sequencers
67
77
 
68
- Convert patterns into timelines
78
+ Convert patterns into event Timelines
79
+
80
+ The sequencer classes differ in terms of how they determine how much time should occur between one event and the next.
81
+ So far the options are:
82
+ * LegatoSequencer - the end of each event is the start of the next event
83
+ * RhythmSequencer - requires a special rhythm-type patter to determine the inter-event time intervals
84
+ * StepSequencer - uses a constant amount of time between all events. In other words, works like a drum sequencer/
85
+
86
+
87
+ <br/>
88
+ ### Creating object conveniently
89
+
90
+ The Core types as well as Patterns and Note events have "convenience constructors" under the top-level MTK module.
91
+
92
+ You can construct objects from almost any arguments by using the methods such as MTK.Pitch() and MTK.Note().
93
+ These methods do their best to guess what you want from the arguments and even handle out-of-order arguments
94
+ in most cases. See the unit tests for ideas...
69
95
 
70
96
 
71
97
  <br/>
@@ -74,56 +100,62 @@ Convert patterns into timelines
74
100
  Basic Tenants
75
101
 
76
102
  * Minimal syntax: less typing means more music!
77
- * Be case-insensitive to avoid typos caused by lower vs upper case (there's one notable exception for intervals: m2 vs M2)
78
- * pitch and duration are the most important properties
79
- * pitch is more important than rhythm
103
+ * Case-sensitive to allow for more to be expressed in fewer characters.
104
+ * Pitch and duration are the most important properties
80
105
 
81
106
  Because pitch class and duration are such important properties, and we want to minimize typing, we represent these with 1 letter.
82
107
 
83
- **Diatonic Pitch Class: c d e f g a b**
108
+ **Diatonic Pitch Class: C D E F G A B**
84
109
 
85
- **Chromatic Pitch Class (Sharp/Flat): c c# db d d# eb e e# fb f f# gb g g# ab a a# bb b b#**
110
+ **Chromatic Pitch Class (Sharp/Flat): C/B# C#/Db D D#/Eb E/Fb E#/F F#/Gb G G#/Ab A A#/Bb B/Cb**
86
111
 
87
- Double-sharps (c##) and double-flats (dbb) are also allowed. You may prefer Bb to bb, etc
112
+ Double-sharps (C##) and double-flats (Dbb) are also allowed.
88
113
 
89
- **Pitch (Pitch Class + Octave Number): c4 db5 c-1 g9**
114
+ **Pitch (Pitch Class + Octave Number): C4 Db5 C-1 G9**
90
115
 
91
- c-1 (that's octave number: negative 1) is the lowest note. g9 is the highest.
116
+ C-1 (that's octave number: negative 1) is the lowest note. G9 is the highest.
117
+ Note: The MTK syntax expects C-1 as you'd expect, but the corresponding constant in Ruby is C_1 to ensure valid Ruby syntax.
92
118
 
93
- **Duration: w h q i s r x (that's: whole note, half, quarter, eighth, sixteenth, thirty-second, sixty-fourth)**
119
+ **Duration: w h q e s r x (that's: whole note, half, quarter, eighth, sixteenth, thirty-second, sixty-fourth)**
94
120
 
95
- Why "i", "r", and "x"? We're trying to keep these one letter. 'e', 'r', and 's' were already used,
96
- so we just move along the letters of the word until we find an unused letter.
121
+ In the MTK syntax, durations can be suffixed with "t" or "." for triplets and dotted-notes
122
+ (mathematically that multiplies the dureation by 2/3 or 1.5).
97
123
 
98
- For intensity, we'll try to use standard music notation, but 'f' conflicts, so we handle this the same way we did with durations:
124
+ Why "r", and "x"? We're trying to keep these one letter.
125
+ "t" is used for triplets, so the thirty-second note became "r".
126
+ "s" is used by sixteenth notes, so the sixty-fourth note became "x".
127
+ Hopefully this abnormal naming won't cause problems since those duration values are fairly uncommon.
99
128
 
100
- **Intensity: ppp pp p mp mf o ff fff**
101
129
 
102
- (You may find it helpful to think "loud" for "o")
130
+ **Intensity: ppp pp p mp mf f ff fff**
103
131
 
104
132
 
105
133
  TODO: keep documenting this...
134
+ TODO: explanation of intervals
106
135
 
107
136
 
108
137
  Summary of single-letter assignments:
109
138
  ```
110
- a -> pitch class a
111
- b -> pitch class b, flat modifier on pitch class
112
- c -> pitch class c
113
- d -> pitch class d
114
- e -> pitch class e
115
- f -> pitch class f
139
+ A -> pitch class A
140
+ B -> pitch class B
141
+ b -> flat modifier on pitch class
142
+ D -> pitch class c
143
+ D -> pitch class d
144
+ E -> pitch class e
145
+ e -> eighth note duration
146
+ F -> pitch class f
147
+ f -> forte intensity
116
148
 
117
149
  h -> half note duration
118
- i -> eighth note duration
150
+ i -> minor tonic chord (PLANNED, not implemented yet)
119
151
 
120
- o -> forte intensity
121
152
  p -> piano intensity
122
153
  q -> quarter note duration
123
154
  r -> thirty-second note duration
124
155
  s -> sixteenth note duration
125
156
  t -> triplet modifier on durations
126
157
 
158
+ v -> minor dominant chord (PLANNED, not implemented yet)
127
159
  w -> whole note duration
128
160
  x -> sixty-fourth note duration
129
161
  ```
data/Rakefile CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'rspec/core/rake_task'
2
2
  require 'rake/clean'
3
+ require 'erb'
3
4
 
4
- GEM_VERSION = '0.0.3.3'
5
+
6
+ GEM_VERSION = '0.4'
5
7
 
6
8
  SUPPORTED_RUBIES = %w[ 1.9.3 2.0 jruby-1.7.4 ]
7
9
 
data/bin/mtk CHANGED
@@ -10,38 +10,38 @@ options = {}
10
10
 
11
11
  option_parser = OptionParser.new do |opts|
12
12
 
13
- opts.banner = "Usage: #{$0} [options]"
13
+ opts.banner = "\nMTK: Music Tool Kit for Ruby\n\nUsage: #{$0} [options]"
14
14
  opts.separator ''
15
15
  opts.separator 'Options:'
16
16
 
17
- opts.on('-c FILE', '--convert FILE', 'Convert a file containing MTK syntax to MIDI',
18
- 'if a --file is given, write the MIDI to a file',
19
- 'if an --output is given, play the MIDI',
17
+ opts.on('-c FILE', '--convert FILE', 'Convert file containing MTK syntax to MIDI',
18
+ 'if --file is given, write the MIDI to file',
19
+ 'if --output is given, play the MIDI',
20
20
  'otherwise print the MIDI') {|file| options[:convert] = file }
21
21
 
22
22
  opts.separator ''
23
23
 
24
- opts.on('-e [syntax]', '--eval [syntax]', 'Convert the given MTK syntax String to MIDI',
25
- 'or start an interactive interpreter when [syntax] is omitted',
26
- 'Behaves like --convert when a --file or --output is given') {|syntax| options[:eval] = syntax }
24
+ opts.on('-e [syntax]', '--eval [syntax]', 'Convert the given MTK syntax to MIDI or',
25
+ 'interactive interpreter when no [syntax]',
26
+ 'Behaves like --convert for --file/--output') {|syntax| options[:eval] = syntax }
27
27
 
28
28
  opts.separator ''
29
29
 
30
- opts.on('-f FILE', '--file FILE', 'Write the output of --convert, --eval, --input, or --watch to a file'
31
- ) {|file| options[:file] = file }
30
+ opts.on('-f FILE', '--file FILE', 'Write output of --convert, --eval, --input',
31
+ 'or --watch to a file') {|file| options[:file] = file }
32
32
 
33
33
  opts.separator ''
34
34
 
35
- opts.on('-h', '--help', 'Show this message') { puts opts; exit }
35
+ opts.on('-h', '--help', 'Show these usage instructions') { puts opts; exit }
36
36
 
37
37
  opts.separator ''
38
38
 
39
39
  opts.on('-i INPUT', '--input INPUT', 'Set MIDI input for recording',
40
- 'if no --file is specified, prints the recorded MIDI') {|input| options[:input] = input }
40
+ 'if no --file given, prints recorded MIDI') {|input| options[:input] = input }
41
41
 
42
42
  opts.separator ''
43
43
 
44
- opts.on('-l', '--list', 'List available MIDI devices for --input and --output') { options[:list] = true }
44
+ opts.on('-l', '--list', 'List MIDI devices for --input and --output') { options[:list] = true }
45
45
 
46
46
  opts.separator ''
47
47
 
@@ -53,17 +53,19 @@ option_parser = OptionParser.new do |opts|
53
53
 
54
54
  opts.separator ''
55
55
 
56
- opts.on('-p FILE', '--play FILE', 'Play or print the contents of a MIDI file',
57
- 'if no --output is specified, print the MIDI file') {|file| options[:play] = file }
56
+ opts.on('-p FILE', '--play FILE', 'Play or print a MIDI file',
57
+ 'if no --output given, print the MIDI') {|file| options[:play] = file }
58
58
 
59
59
  opts.separator ''
60
60
 
61
- #opts.on('-t', '--tutorial', 'Start an interactive tutorial for the MTK syntax') { options[:tutorial] = true }
62
- #
63
- #opts.separator ''
61
+ opts.on('-t [color]', '--tutorial [color]', 'Interactive tutorial for MTK syntax',
62
+ 'Text color can be set on/off') {|color| options[:tutorial] = true; options[:color] = color }
64
63
 
65
- opts.on('-w FILE', '--watch FILE', 'Watch an MTK syntax file for changes and automatically convert to MIDI',
66
- 'Behaves like --convert when a --file or --output is given') {|file| options[:watch] = file }
64
+ opts.separator ''
65
+
66
+ opts.on('-w FILE', '--watch FILE', 'Watch an MTK syntax file for changes and',
67
+ 'automatically convert to MIDI',
68
+ 'Behaves like --convert for --file/--output') {|file| options[:watch] = file }
67
69
 
68
70
  end
69
71
 
@@ -71,18 +73,16 @@ end
71
73
  #######################################################################
72
74
 
73
75
  puts option_parser and exit if ARGV.length == 0
74
- #p ARGV
75
- #p options
76
76
 
77
77
  ERROR_INVALID_COMMAND = 1
78
78
  ERROR_FILE_NOT_FOUND = 2
79
79
  ERROR_OUTPUT_NOT_FOUND = 3
80
80
  ERROR_INPUT_NOT_FOUND = 4
81
81
 
82
- # Immediately trying to play output while Ruby is still "warming up" can cause timing issues with
83
- # the first couple notes. So we play this "empty" Timeline containing a rest to address that issue.
82
+ # Empty timeline used to prime the realtime output
84
83
  WARMUP = MTK::Events::Timeline.from_h( {0 => MTK.Note(60,-1)} )
85
84
 
85
+
86
86
  #######################################################################
87
87
 
88
88
  begin
@@ -115,6 +115,9 @@ end
115
115
  def output(timelines, print_header='Timeline')
116
116
  timelines = [timelines] unless timelines.is_a? Array
117
117
  if @output
118
+ # Immediately trying to play output while Ruby is still "warming up" can cause timing issues with
119
+ # the first couple notes. So we play this "empty" Timeline containing a rest to address that issue.
120
+ # TODO? move this into the output class and do it automatically when playing for the first time? (warmup code is also in output_selector))
118
121
  @output.play WARMUP
119
122
  @output.play timelines.first # TODO: support multiple timelines
120
123
  elsif @file
@@ -151,6 +154,33 @@ def watch_file_updated?
151
154
  end
152
155
 
153
156
 
157
+ def set_tutorial_color(color_option)
158
+ if color_option
159
+ case color_option.strip.downcase
160
+ when /^(on|yes|true|y|t|color)$/ then $tutorial_color = true
161
+ when /^(off|no|false|n|f)$/ then $tutorial_color = false
162
+ else
163
+ STDERR.puts "Invalid tutorial color setting '#{color}'. Try 'on' or 'off'."
164
+ exit ERROR_INVALID_COMMAND
165
+ end
166
+ else
167
+ require 'rbconfig'
168
+ os = RbConfig::CONFIG['host_os'].downcase
169
+ if os =~ /win/ and os !~ /darwin/
170
+ puts
171
+ puts "Windows command line text color is off by default."
172
+ puts "If you want color, use ANSI terminal software like Cygwin or Ansicon and "
173
+ puts "run #{$0} with the color option \"--tutorial on\""
174
+ $tutorial_color = false
175
+ else
176
+ $tutorial_color = true
177
+ end
178
+ end
179
+ puts "Tutorial color is #{if $tutorial_color then 'enabled' else 'disabled' end}."
180
+ puts
181
+ end
182
+
183
+
154
184
  #######################################################################
155
185
 
156
186
  if options[:list]
@@ -162,18 +192,21 @@ if options[:list]
162
192
  puts
163
193
  puts (['OUTPUTS:']+output_names).join("\n * ")
164
194
  puts
165
- puts 'When specifying --input INPUT or --output OUTPUT, the first substring match will be used.'
166
- puts "For example: --output IAC will use 'Apple Inc. IAC Driver' if it's the first OUTPUT containing 'IAC'"
195
+ puts 'When specifying --input INPUT or --output OUTPUT, the first substring match'
196
+ puts '(case-insensitive) will be used. For example: "--output iac" will use'
197
+ puts '"Apple Inc. IAC Driver" if it\'s the first OUTPUT containing "IAC".'
167
198
  puts
168
199
  exit
169
200
  end
170
201
 
202
+
171
203
  @monitor = true if options[:monitor]
172
204
 
205
+
173
206
  if options[:input]
174
207
  setup_io
175
208
  input_name = options[:input]
176
- @input = MTK::IO::MIDIInput.find_by_name /#{input_name}/
209
+ @input = MTK::IO::MIDIInput.find_by_name /#{input_name}/i
177
210
  if @input
178
211
  puts "Using input '#{@input.name}'"
179
212
  else
@@ -182,10 +215,11 @@ if options[:input]
182
215
  end
183
216
  end
184
217
 
218
+
185
219
  if options[:output]
186
220
  setup_io
187
221
  output_name = options[:output]
188
- @output = MTK::IO::MIDIOutput.find_by_name /#{output_name}/
222
+ @output = MTK::IO::MIDIOutput.find_by_name /#{output_name}/i
189
223
  if @output
190
224
  puts "Using output '#{@output.name}'"
191
225
  else
@@ -194,8 +228,9 @@ if options[:output]
194
228
  end
195
229
  end
196
230
 
197
- file = options[:file]
198
- @file = file if file
231
+
232
+ @file = options[:file]
233
+
199
234
 
200
235
  if options[:play]
201
236
  filename = options[:play]
@@ -204,6 +239,7 @@ if options[:play]
204
239
  output(timelines, "Timeline for #{filename}")
205
240
  end
206
241
 
242
+
207
243
  if options.has_key? :eval
208
244
  mtk_syntax = options[:eval]
209
245
  if mtk_syntax.nil?
@@ -221,12 +257,14 @@ if options.has_key? :eval
221
257
  end
222
258
  end
223
259
 
260
+
224
261
  if options[:convert]
225
262
  mtk_syntax_file = options[:convert]
226
263
  mtk_syntax = IO.read(mtk_syntax_file)
227
264
  convert(mtk_syntax)
228
265
  end
229
266
 
267
+
230
268
  if options[:watch]
231
269
  @watch_file = options[:watch]
232
270
  puts "Watching #{@watch_file}. Press Ctrl+C to exit."
@@ -243,8 +281,13 @@ if options[:watch]
243
281
  end
244
282
  end
245
283
 
246
- #if options.has_key? :tutorial
247
- # puts "TODO: tutorial"
248
- #end
284
+
285
+ if options.has_key? :tutorial
286
+ set_tutorial_color(options[:color])
287
+ require 'mtk/lang/tutorial'
288
+ tutorial = MTK::Lang::Tutorial.new
289
+ tutorial.run(@output)
290
+ end
291
+
249
292
 
250
293
  record if @input
@@ -9,8 +9,8 @@ file = ARGV[0] || "MTK-#{File.basename(__FILE__,'.rb')}.mid"
9
9
  _ = nil # defines _ as a rest
10
10
 
11
11
  pattern = {# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
12
- C2 => [fff, _, _, _, mf, _, _, _, o, _, _, _, mp, _, _, _], # kick
13
- Db2 => [ _, _, o, _, _, _, mp, _, _, _, o, _, _, _, mf, _], # rim shot
12
+ C2 => [fff, _, _, _, mf, _, _, _, f, _, _, _, mp, _, _, _], # kick
13
+ Db2 => [ _, _, f, _, _, _, mp, _, _, _, f, _, _, _, mf, _], # rim shot
14
14
  D2 => [ _, mp, _, mp, _, mp, _, mf, _, mp, _, mp, _, pp, _, mf] # snare
15
15
  }
16
16
 
@@ -29,7 +29,7 @@ end
29
29
  pitches = Patterns.Function( interval_generator, max_elements: 24 )
30
30
 
31
31
  # we'll also use a weighted choice to generate the intensities
32
- intensities = Patterns.Choice( mp,mf,o,ff,fff, weights: [1,2,3,2,1], max_cycles: 24 )
32
+ intensities = Patterns.Choice( mp,mf,f,ff,fff, weights: [1,2,3,2,1], max_cycles: 24 )
33
33
 
34
34
  sequencer = Sequencers.StepSequencer( pitches,intensities, step_size: 0.5, max_interval: 17 )
35
35
 
@@ -0,0 +1,71 @@
1
+ require 'mtk/io/midi_output'
2
+
3
+ # Assists with selecting an output.
4
+ class OutputSelector
5
+
6
+ # Empty timeline used to prime the realtime output
7
+ WARMUP = MTK::Events::Timeline.from_h( {0 => MTK.Note(60,-1)} )
8
+
9
+ class << self
10
+
11
+ def output
12
+ MTK::IO::MIDIOutput
13
+ end
14
+
15
+ # Look for an output by name using case insensitive matching,
16
+ # treating underscore like either an underscore or whitespace
17
+ def search output_name_pattern
18
+ output.find_by_name(/#{output_name_pattern.to_s.sub '_','(_|\\s+)'}/i)
19
+ end
20
+
21
+ # Command line interface to list output choices and select an output.
22
+ def prompt_for_output
23
+ devices_by_name = output.devices_by_name
24
+ names_by_number = {}
25
+
26
+ puts "Available MIDI outputs:"
27
+ devices_by_name.keys.each_with_index do |name,index|
28
+ number = index+1
29
+ names_by_number[number] = name
30
+ puts " #{number} => #{name}"
31
+ end
32
+
33
+ print "Enter the number of the output to test: "
34
+ device = nil
35
+ loop do
36
+ begin
37
+ # NOTE: invalid input will turn into 0, but that's ok because we index from 1 so it will be caught as invalid
38
+ number = STDIN.gets.to_i
39
+ name = names_by_number[number]
40
+ device = devices_by_name[name]
41
+ return output.open(device) if device
42
+ rescue
43
+ if $DEBUG
44
+ puts $!
45
+ puts $!.backtrace
46
+ end
47
+ # keep looping
48
+ end
49
+ print "Invalid input. Enter a number listed above: "
50
+ end
51
+ end
52
+
53
+
54
+ def ensure_output name=nil
55
+ output = nil
56
+ if name
57
+ output = search name
58
+ puts "Output '#{name}' not found." unless output
59
+ end
60
+ output ||= prompt_for_output
61
+
62
+ # Immediately trying to play output while Ruby is still "warming up" can cause timing issues with
63
+ # the first couple notes. So we play this "empty" Timeline containing a rest to address that issue.
64
+ output.play WARMUP
65
+
66
+ output
67
+ end
68
+
69
+ end
70
+ end
71
+