jmtk 0.0.3.3-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 (110) hide show
  1. data/.yardopts +10 -0
  2. data/DEVELOPMENT_NOTES.md +115 -0
  3. data/INTRO.md +129 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.md +50 -0
  6. data/Rakefile +102 -0
  7. data/bin/jmtk +250 -0
  8. data/bin/mtk +250 -0
  9. data/examples/crescendo.rb +20 -0
  10. data/examples/drum_pattern.rb +23 -0
  11. data/examples/dynamic_pattern.rb +36 -0
  12. data/examples/gets_and_play.rb +27 -0
  13. data/examples/notation.rb +22 -0
  14. data/examples/play_midi.rb +17 -0
  15. data/examples/print_midi.rb +13 -0
  16. data/examples/random_tone_row.rb +18 -0
  17. data/examples/syntax_to_midi.rb +28 -0
  18. data/examples/test_output.rb +7 -0
  19. data/examples/tone_row_melody.rb +23 -0
  20. data/lib/mtk.rb +76 -0
  21. data/lib/mtk/core/duration.rb +213 -0
  22. data/lib/mtk/core/intensity.rb +158 -0
  23. data/lib/mtk/core/interval.rb +157 -0
  24. data/lib/mtk/core/pitch.rb +154 -0
  25. data/lib/mtk/core/pitch_class.rb +194 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/events/timeline.rb +232 -0
  30. data/lib/mtk/groups/chord.rb +56 -0
  31. data/lib/mtk/groups/collection.rb +196 -0
  32. data/lib/mtk/groups/melody.rb +96 -0
  33. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  34. data/lib/mtk/groups/pitch_collection.rb +23 -0
  35. data/lib/mtk/io/dls_synth_device.rb +146 -0
  36. data/lib/mtk/io/dls_synth_output.rb +62 -0
  37. data/lib/mtk/io/jsound_input.rb +87 -0
  38. data/lib/mtk/io/jsound_output.rb +82 -0
  39. data/lib/mtk/io/midi_file.rb +209 -0
  40. data/lib/mtk/io/midi_input.rb +97 -0
  41. data/lib/mtk/io/midi_output.rb +195 -0
  42. data/lib/mtk/io/notation.rb +162 -0
  43. data/lib/mtk/io/unimidi_input.rb +117 -0
  44. data/lib/mtk/io/unimidi_output.rb +140 -0
  45. data/lib/mtk/lang/durations.rb +57 -0
  46. data/lib/mtk/lang/intensities.rb +61 -0
  47. data/lib/mtk/lang/intervals.rb +73 -0
  48. data/lib/mtk/lang/mtk_grammar.citrus +237 -0
  49. data/lib/mtk/lang/parser.rb +29 -0
  50. data/lib/mtk/lang/pitch_classes.rb +29 -0
  51. data/lib/mtk/lang/pitches.rb +52 -0
  52. data/lib/mtk/lang/pseudo_constants.rb +26 -0
  53. data/lib/mtk/lang/variable.rb +32 -0
  54. data/lib/mtk/numeric_extensions.rb +66 -0
  55. data/lib/mtk/patterns/chain.rb +49 -0
  56. data/lib/mtk/patterns/choice.rb +43 -0
  57. data/lib/mtk/patterns/cycle.rb +18 -0
  58. data/lib/mtk/patterns/for_each.rb +71 -0
  59. data/lib/mtk/patterns/function.rb +39 -0
  60. data/lib/mtk/patterns/lines.rb +54 -0
  61. data/lib/mtk/patterns/palindrome.rb +45 -0
  62. data/lib/mtk/patterns/pattern.rb +171 -0
  63. data/lib/mtk/patterns/sequence.rb +20 -0
  64. data/lib/mtk/sequencers/event_builder.rb +132 -0
  65. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  66. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  67. data/lib/mtk/sequencers/sequencer.rb +111 -0
  68. data/lib/mtk/sequencers/step_sequencer.rb +26 -0
  69. data/spec/mtk/core/duration_spec.rb +372 -0
  70. data/spec/mtk/core/intensity_spec.rb +289 -0
  71. data/spec/mtk/core/interval_spec.rb +265 -0
  72. data/spec/mtk/core/pitch_class_spec.rb +343 -0
  73. data/spec/mtk/core/pitch_spec.rb +297 -0
  74. data/spec/mtk/events/event_spec.rb +234 -0
  75. data/spec/mtk/events/note_spec.rb +174 -0
  76. data/spec/mtk/events/parameter_spec.rb +220 -0
  77. data/spec/mtk/events/timeline_spec.rb +430 -0
  78. data/spec/mtk/groups/chord_spec.rb +85 -0
  79. data/spec/mtk/groups/collection_spec.rb +374 -0
  80. data/spec/mtk/groups/melody_spec.rb +225 -0
  81. data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
  82. data/spec/mtk/io/midi_file_spec.rb +243 -0
  83. data/spec/mtk/io/midi_output_spec.rb +102 -0
  84. data/spec/mtk/lang/durations_spec.rb +89 -0
  85. data/spec/mtk/lang/intensities_spec.rb +101 -0
  86. data/spec/mtk/lang/intervals_spec.rb +143 -0
  87. data/spec/mtk/lang/parser_spec.rb +603 -0
  88. data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
  89. data/spec/mtk/lang/pitches_spec.rb +56 -0
  90. data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
  91. data/spec/mtk/lang/variable_spec.rb +52 -0
  92. data/spec/mtk/numeric_extensions_spec.rb +83 -0
  93. data/spec/mtk/patterns/chain_spec.rb +110 -0
  94. data/spec/mtk/patterns/choice_spec.rb +97 -0
  95. data/spec/mtk/patterns/cycle_spec.rb +123 -0
  96. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  97. data/spec/mtk/patterns/function_spec.rb +120 -0
  98. data/spec/mtk/patterns/lines_spec.rb +77 -0
  99. data/spec/mtk/patterns/palindrome_spec.rb +108 -0
  100. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  101. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  102. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  103. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  104. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  105. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  106. data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
  107. data/spec/spec_coverage.rb +2 -0
  108. data/spec/spec_helper.rb +12 -0
  109. data/spec/test.mid +0 -0
  110. metadata +226 -0
data/bin/mtk ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mtk'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+
9
+ #######################################################################
10
+
11
+ option_parser = OptionParser.new do |opts|
12
+
13
+ opts.banner = "Usage: #{$0} [options]"
14
+ opts.separator ''
15
+ opts.separator 'Options:'
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',
20
+ 'otherwise print the MIDI') {|file| options[:convert] = file }
21
+
22
+ opts.separator ''
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 }
27
+
28
+ opts.separator ''
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 }
32
+
33
+ opts.separator ''
34
+
35
+ opts.on('-h', '--help', 'Show this message') { puts opts; exit }
36
+
37
+ opts.separator ''
38
+
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 }
41
+
42
+ opts.separator ''
43
+
44
+ opts.on('-l', '--list', 'List available MIDI devices for --input and --output') { options[:list] = true }
45
+
46
+ opts.separator ''
47
+
48
+ opts.on('-m', '--monitor', 'Monitor MIDI input while recording') { options[:monitor] = true }
49
+
50
+ opts.separator ''
51
+
52
+ opts.on('-o OUTPUT', '--output OUTPUT', 'Set MIDI output for playing') {|output| options[:output] = output }
53
+
54
+ opts.separator ''
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 }
58
+
59
+ opts.separator ''
60
+
61
+ #opts.on('-t', '--tutorial', 'Start an interactive tutorial for the MTK syntax') { options[:tutorial] = true }
62
+ #
63
+ #opts.separator ''
64
+
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 }
67
+
68
+ end
69
+
70
+
71
+ #######################################################################
72
+
73
+ puts option_parser and exit if ARGV.length == 0
74
+ #p ARGV
75
+ #p options
76
+
77
+ ERROR_INVALID_COMMAND = 1
78
+ ERROR_FILE_NOT_FOUND = 2
79
+ ERROR_OUTPUT_NOT_FOUND = 3
80
+ ERROR_INPUT_NOT_FOUND = 4
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.
84
+ WARMUP = MTK::Events::Timeline.from_h( {0 => MTK.Note(60,-1)} )
85
+
86
+ #######################################################################
87
+
88
+ begin
89
+ option_parser.parse!
90
+ rescue OptionParser::MissingArgument, OptionParser::InvalidOption
91
+ puts "Invalid command, #{$!}"
92
+ puts "For command line help: #{$0} --help"
93
+ puts "For command line help: #{$0} --help"
94
+ exit ERROR_INVALID_COMMAND
95
+ end
96
+
97
+
98
+ def setup_io
99
+ require 'mtk/io/midi_input'
100
+ require 'mtk/io/midi_output'
101
+ end
102
+
103
+
104
+ def convert(mtk_syntax)
105
+ sequencer = MTK::Lang::Parser.parse(mtk_syntax)
106
+ if sequencer
107
+ timeline = sequencer.to_timeline
108
+ output(timeline)
109
+ end
110
+ rescue Citrus::ParseError
111
+ STDERR.puts $!
112
+ end
113
+
114
+
115
+ def output(timelines, print_header='Timeline')
116
+ timelines = [timelines] unless timelines.is_a? Array
117
+ if @output
118
+ @output.play WARMUP
119
+ @output.play timelines.first # TODO: support multiple timelines
120
+ elsif @file
121
+ require 'mtk/io/midi_file'
122
+ MTK.MIDIFile(@file).write timelines
123
+ else
124
+ puts print_header, timelines
125
+ puts
126
+ end
127
+ end
128
+
129
+
130
+ def record
131
+ if @input
132
+ print "Press Enter to begin recording MIDI input..."
133
+ gets
134
+ puts "Recording input. Press control-C to stop."
135
+ @input.record monitor:@monitor
136
+ Signal.trap("INT") do # SIGINT = control-C
137
+ @input.stop
138
+ output @input.to_timeline, "\nRecorded MIDI data"
139
+ exit
140
+ end
141
+ loop{ sleep 0.01 }
142
+ end
143
+ end
144
+
145
+
146
+ def watch_file_updated?
147
+ mtime = File.stat(@watch_file).mtime
148
+ updated = @watch_file_mtime.nil? || @watch_file_mtime < mtime
149
+ @watch_file_mtime = mtime
150
+ updated
151
+ end
152
+
153
+
154
+ #######################################################################
155
+
156
+ if options[:list]
157
+ setup_io
158
+ input_names = MTK::IO::MIDIInput.devices_by_name.keys
159
+ output_names = MTK::IO::MIDIOutput.devices_by_name.keys
160
+ puts
161
+ puts (['INPUTS:'] + input_names).join("\n * ")
162
+ puts
163
+ puts (['OUTPUTS:']+output_names).join("\n * ")
164
+ 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'"
167
+ puts
168
+ exit
169
+ end
170
+
171
+ @monitor = true if options[:monitor]
172
+
173
+ if options[:input]
174
+ setup_io
175
+ input_name = options[:input]
176
+ @input = MTK::IO::MIDIInput.find_by_name /#{input_name}/
177
+ if @input
178
+ puts "Using input '#{@input.name}'"
179
+ else
180
+ STDERR.puts "Input '#{input_name}' not found."
181
+ exit ERROR_INPUT_NOT_FOUND
182
+ end
183
+ end
184
+
185
+ if options[:output]
186
+ setup_io
187
+ output_name = options[:output]
188
+ @output = MTK::IO::MIDIOutput.find_by_name /#{output_name}/
189
+ if @output
190
+ puts "Using output '#{@output.name}'"
191
+ else
192
+ STDERR.puts "Output '#{output_name}' not found."
193
+ exit ERROR_OUTPUT_NOT_FOUND
194
+ end
195
+ end
196
+
197
+ file = options[:file]
198
+ @file = file if file
199
+
200
+ if options[:play]
201
+ filename = options[:play]
202
+ require 'mtk/io/midi_file'
203
+ timelines = MTK.MIDIFile(filename).to_timelines
204
+ output(timelines, "Timeline for #{filename}")
205
+ end
206
+
207
+ if options.has_key? :eval
208
+ mtk_syntax = options[:eval]
209
+ if mtk_syntax.nil?
210
+ puts "Starting the interactive interpreter."
211
+ begin
212
+ loop do
213
+ puts "Enter MTK syntax. Press Ctrl+C to exit."
214
+ convert(gets)
215
+ end
216
+ rescue SystemExit,Interrupt
217
+ Kernel.exit
218
+ end
219
+ else
220
+ convert(mtk_syntax)
221
+ end
222
+ end
223
+
224
+ if options[:convert]
225
+ mtk_syntax_file = options[:convert]
226
+ mtk_syntax = IO.read(mtk_syntax_file)
227
+ convert(mtk_syntax)
228
+ end
229
+
230
+ if options[:watch]
231
+ @watch_file = options[:watch]
232
+ puts "Watching #{@watch_file}. Press Ctrl+C to exit."
233
+ watch_file_updated? # prime the watcher
234
+ begin
235
+ loop do
236
+ mtk_syntax = IO.read(@watch_file)
237
+ convert(mtk_syntax)
238
+ Kernel.sleep(0.5) until watch_file_updated?
239
+ puts "#{Time.new}: #{@watch_file} updated"
240
+ end
241
+ rescue SystemExit,Interrupt
242
+ Kernel.exit
243
+ end
244
+ end
245
+
246
+ #if options.has_key? :tutorial
247
+ # puts "TODO: tutorial"
248
+ #end
249
+
250
+ record if @input
@@ -0,0 +1,20 @@
1
+ # Generate a MIDI file of the C-major scale with a crescendo (it increases in intensity)
2
+ #
3
+ # NOTE: this blindly overwrites any existing MTK-crescendo.mid file, unless an argument is provided
4
+
5
+ require 'mtk'
6
+ require 'mtk/io/midi_file'
7
+ include MTK
8
+ include Lang::Pitches
9
+ include Lang::Intensities
10
+
11
+ file = ARGV[0] || 'MTK-crescendo.mid'
12
+
13
+ scale = Patterns.Sequence C4,D4,E4,F4,G4,A4,B4,C5
14
+ crescendo = Patterns.Lines pp, [fff, scale.length-1] # step from pp to fff over the length of the scale
15
+
16
+ sequencer = Sequencers.StepSequencer scale, crescendo
17
+ timeline = sequencer.to_timeline
18
+
19
+ MIDIFile(file).write timeline
20
+
@@ -0,0 +1,23 @@
1
+ require 'mtk'
2
+ require 'mtk/io/midi_file'
3
+ include MTK
4
+ include Lang::Pitches
5
+ include Lang::Intensities
6
+
7
+ file = ARGV[0] || "MTK-#{File.basename(__FILE__,'.rb')}.mid"
8
+
9
+ _ = nil # defines _ as a rest
10
+
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
14
+ D2 => [ _, mp, _, mp, _, mp, _, mf, _, mp, _, mp, _, pp, _, mf] # snare
15
+ }
16
+
17
+ timeline = Events::Timeline.new
18
+ for pitch,intensities in pattern
19
+ track = Sequencers::StepSequencer( Patterns.Sequence(intensities), default_pitch:pitch, channel:10 )
20
+ timeline.merge track.to_timeline
21
+ end
22
+
23
+ MIDIFile(file).write timeline
@@ -0,0 +1,36 @@
1
+ # Generate a MIDI file using a lambda to dynamically generate the pitches
2
+ #
3
+ # NOTE: this blindly overwrites any existing MTK-dynamic_pattern.mid file, unless an argument is provided
4
+
5
+ require 'mtk'
6
+ require 'mtk/io/midi_file'
7
+ include MTK
8
+ include MTK::Lang::Pitches
9
+ include MTK::Lang::Intensities
10
+ include MTK::Lang::Intervals
11
+
12
+ file = ARGV[0] || "MTK-#{File.basename __FILE__,'.rb'}.mid"
13
+
14
+ interval_generator = lambda do
15
+ # Randomly return intervals
16
+ r = rand
17
+ case
18
+ when r < 0.1 then m2
19
+ when r < 0.4 then M2
20
+ when r < 0.5 then m3
21
+ when r < 0.6 then M3
22
+ when r < 0.7 then P4
23
+ when r < 0.8 then -M3
24
+ when r < 0.95 then -P5
25
+ else -P8
26
+ end
27
+ end
28
+
29
+ pitches = Patterns.Function( interval_generator, max_elements: 24 )
30
+
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 )
33
+
34
+ sequencer = Sequencers.StepSequencer( pitches,intensities, step_size: 0.5, max_interval: 17 )
35
+
36
+ MIDIFile(file).write( sequencer.to_timeline )
@@ -0,0 +1,27 @@
1
+ # Enter space-separated pitch classes (A,B,C,D,E,F,G) at the prompt and hear them play.
2
+
3
+ require 'mtk'
4
+ require_relative 'helpers/output_selector'
5
+ include MTK
6
+ include MTK::Core
7
+
8
+ output = OutputSelector.ensure_output ARGV[0]
9
+
10
+ def get_pitch_classes
11
+ puts "Enter pitch classes:"
12
+ input = STDIN.gets
13
+ input.strip.split(/\s+/).map{|name| PitchClass[name] } if input
14
+ rescue
15
+ puts "Invalid input."
16
+ end
17
+
18
+ while (pitch_classes = get_pitch_classes)
19
+ sequence = Patterns.Sequence pitch_classes
20
+ sequencer = Sequencers.StepSequencer sequence
21
+ timeline = sequencer.to_timeline
22
+
23
+ puts "Playing: #{pitch_classes}"
24
+ puts "Timeline:\n#{timeline}"
25
+ output.play timeline
26
+ end
27
+
@@ -0,0 +1,22 @@
1
+ require 'mtk'
2
+ require 'mtk/io/notation'
3
+ include MTK
4
+
5
+ def arg_error(error)
6
+ puts "Usage: ruby #$0 syntax output_file"
7
+ puts error
8
+ exit 1
9
+ end
10
+
11
+ syntax = ARGV[0]
12
+ arg_error "MTK syntax string not provided" unless syntax
13
+
14
+
15
+ file = ARGV[1]
16
+ arg_error "The output_file must end in '.png', '.pdf', or '.ps'" unless file
17
+
18
+
19
+ sequencer = MTK::Lang::Parser.parse(syntax)
20
+ timeline = sequencer.to_timeline
21
+ # MTK::IO::Notation.open(file).write(timeline)
22
+ MTK::IO::Notation.open(file, dpi:300).write(timeline) # higher resolution PNG
@@ -0,0 +1,17 @@
1
+ # Play a given MIDI file with the given MIDI output
2
+
3
+ file, output_name = ARGV[0], ARGV[1]
4
+ unless file
5
+ puts "Usage: ruby #$0 midi_file [output_device]"
6
+ exit 1
7
+ end
8
+
9
+ require 'mtk'
10
+ require 'mtk/io/midi_file'
11
+ require_relative 'helpers/output_selector'
12
+
13
+ output = OutputSelector.ensure_output(output_name)
14
+
15
+ timeline = MTK.MIDIFile(file).to_timelines
16
+
17
+ output.play(timeline)
@@ -0,0 +1,13 @@
1
+ # Print the notes in a MIDI file
2
+
3
+ file = ARGV[0]
4
+ unless file
5
+ puts "Usage: ruby #$0 midi_file"
6
+ exit 1
7
+ end
8
+
9
+ require 'mtk'
10
+ require 'mtk/io/midi_file'
11
+ include MTK
12
+
13
+ puts MIDIFile(file).to_timelines
@@ -0,0 +1,18 @@
1
+ # Generate a MIDI file of a random 12-tone row
2
+ #
3
+ # NOTE: this blindly overwrites any existing MTK-random_tone_row.mid file, unless an argument is provided
4
+
5
+ require 'mtk'
6
+ require 'mtk/io/midi_file'
7
+ include MTK
8
+
9
+ file = ARGV[0] || 'MTK-random_tone_row.mid'
10
+
11
+ row = Groups::PitchClassSet.random_row
12
+ sequence = Patterns.Sequence *row
13
+
14
+ sequencer = Sequencers.StepSequencer sequence
15
+ timeline = sequencer.to_timeline
16
+
17
+ MIDIFile(file).write timeline
18
+
@@ -0,0 +1,28 @@
1
+ # Generate a MIDI file by reading in a file containing the MTK custom syntax (see mtk_grammar.citrus and grammar_spec.rb)
2
+ #
3
+ # NOTE: this blindly overwrites any existing MTK-syntax_to_midi.mid file, unless a second argument is provided
4
+
5
+ require 'mtk'
6
+ require 'mtk/io/midi_file'
7
+
8
+ input = ARGV[0]
9
+ if input.nil?
10
+ STDERR.puts "Input file is required."
11
+ STDERR.puts "Usage: #{$0} input [output]"
12
+ exit 1
13
+ end
14
+
15
+ unless File.exists? input
16
+ STDERR.puts "Cannot read file: #{input}"
17
+ exit 2
18
+ end
19
+
20
+ output = ARGV[1] || "MTK-#{File.basename(__FILE__,'.rb')}.mid"
21
+
22
+
23
+ syntax = IO.read(input)
24
+ sequencer = MTK::Lang::Parser.parse(syntax)
25
+ timeline = sequencer.to_timeline
26
+
27
+ MTK::MIDIFile(output).write timeline
28
+