jmtk 0.0.3.3-java

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,10 @@
1
+ --readme README.md
2
+ --title 'MTK (Music Tool Kit for Ruby)'
3
+ --charset utf-8
4
+ --protected
5
+ --no-private
6
+ lib/**/*.rb
7
+ - INTRO.md
8
+ - lib/**/*.citrus
9
+ - LICENSE.txt
10
+ - examples/**/*.rb
@@ -0,0 +1,115 @@
1
+ MTK
2
+ ===
3
+
4
+ [![Build Status](https://secure.travis-ci.org/adamjmurray/mtk.png)](http://travis-ci.org/adamjmurray/mtk)
5
+
6
+ Music Tool Kit for Ruby
7
+ -----------------------
8
+
9
+ Classes for modeling music with a focus on simplicity. Support for reading/writing MIDI files and realtime MIDI.
10
+
11
+
12
+
13
+ Getting Started
14
+ ---------------
15
+
16
+ gem install mtk
17
+
18
+ or download the source from here and add mtk/lib to your $LOAD_PATH. Then...
19
+
20
+ require 'mtk'
21
+
22
+ Some examples are available in the examples folder (more coming soon).
23
+ The specs provide a lot of details of usage...
24
+
25
+
26
+
27
+ Goals
28
+ -----
29
+
30
+ * Build musical generators to assist with composing music
31
+ * Re-implement Cosy (http://compusition.com/web/software/cosy) using these models as the "backend"
32
+
33
+
34
+
35
+ Status
36
+ ------
37
+
38
+ Alpha phase, API subject to change. Feedback welcome!
39
+
40
+
41
+
42
+ Requirements
43
+ ------------
44
+
45
+ ### Ruby Version
46
+
47
+ Ruby 1.9+ or JRuby 1.6+
48
+
49
+
50
+ ### Dependencies
51
+
52
+ MTK's core features should not depend on anything outside of the Ruby standard library.
53
+
54
+
55
+ MTK's optional features typically require gems. Currently the following gems are required:
56
+
57
+ * MIDI file I/O requires the __midilib__ gem
58
+
59
+ * realtime MIDI I/O with (MRI/YARV) Ruby requires the __unimidi__ and __gamelan__ gems
60
+
61
+ * realtime MIDI I/O with JRuby require the __jsound__ and __gamelan__ gems
62
+
63
+ * The custom MTK syntax (work in progress) requires the __citrus__ gem
64
+
65
+
66
+ Development requires the gems for optional features, plus the following:
67
+
68
+ * rake
69
+ * rspec (tests)
70
+ * yard (docs)
71
+
72
+ You shouldn't need to worry about the dependencies too much. A Gemfile is provided to sort this out for you:
73
+
74
+ gem install bundler
75
+ bundle install
76
+
77
+ [rvm](https://rvm.beginrescueend.com/) is required for cross version testing (see Development Notes below)
78
+
79
+
80
+
81
+ Documentation
82
+ -------------
83
+
84
+ Generate with:
85
+
86
+ bundle exec rake doc
87
+
88
+ Or view online @ http://rubydoc.info/github/adamjmurray/mtk/master/frames
89
+
90
+
91
+ Development Notes
92
+ -----------------
93
+
94
+ ### Run Tests ###
95
+
96
+ Test with current version of Ruby:
97
+
98
+ bundle exec rake test
99
+
100
+ Test with all supported versions of Ruby (requires [rvm](https://rvm.beginrescueend.com/), YARV 1.9.3, and JRuby 1.6.7):
101
+
102
+ bundle exec rake test:all
103
+
104
+ The test:all test must pass for a pull request to be accepted or for a release of the mtk gem.
105
+
106
+
107
+ ### Generate Docs ###
108
+
109
+ bundle exec rake doc
110
+ open doc/frames.html
111
+
112
+ or, to automatically refresh the documentation as you work:
113
+
114
+ bundle exec yard server -r
115
+ open http://localhost:8808
@@ -0,0 +1,129 @@
1
+ (Work in progress)
2
+
3
+ Core Concepts
4
+ -------------
5
+
6
+ ### Core data types
7
+
8
+ These model the basic properties of musical events:
9
+ * PitchClass
10
+ * Pitch
11
+ * Duration
12
+ * Intensity
13
+
14
+ These types are all immutable. This helps avoid confusing bugs.
15
+ Mostly you don't need to worry about this. Just remember when you call methods that change the value, like #invert,
16
+ it does not change the value in-place. For example:
17
+
18
+ p = PitchClass[G]
19
+ p = p.invert(E) # because p.invert(E) does not change p
20
+
21
+ Intensity values are intended to range from 0.0 (minimum intensity) to 1.0 (maximum intensity).
22
+
23
+ A Duration of 1 is one beat (usually a quarter note, depending on the meter). By convention, negative durations
24
+ indicate a rest that lasts for the absolute value duration.
25
+
26
+
27
+ <br/>
28
+ ### Events
29
+
30
+ Events group together the core data types together to represent a musical event, like a single Note.
31
+ Events can be converted to and from MIDI.
32
+
33
+ * event
34
+ * note
35
+
36
+
37
+ <br/>
38
+ ### Core collections
39
+
40
+ A collection of timed events, such as a melody, a riff, or one track of an entire song.
41
+
42
+ * timeline
43
+
44
+
45
+ <br/>
46
+ ### Patterns
47
+
48
+ Structured collections of core data types and other patterns. Used to generate musical material.
49
+
50
+ * cycle
51
+ * palindrome
52
+ * choice (randomly select from weghted distribution)
53
+ * line (linear interpolation between 2 points)
54
+ * function (dynamically generate elements with a lambda)
55
+
56
+ Future?
57
+ * curve (exponential/curved interpolation between points)
58
+ * permutation (cycle that permutes itself each cycle period)
59
+ * markov chain
60
+ * graph
61
+ * petri net (special type of graph??)
62
+ * custom (just implement enumerator pattern?)
63
+
64
+
65
+ <br/>
66
+ ### Sequencers
67
+
68
+ Convert patterns into timelines
69
+
70
+
71
+ <br/>
72
+ ### Syntax
73
+
74
+ Basic Tenants
75
+
76
+ * 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
80
+
81
+ Because pitch class and duration are such important properties, and we want to minimize typing, we represent these with 1 letter.
82
+
83
+ **Diatonic Pitch Class: c d e f g a b**
84
+
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#**
86
+
87
+ Double-sharps (c##) and double-flats (dbb) are also allowed. You may prefer Bb to bb, etc
88
+
89
+ **Pitch (Pitch Class + Octave Number): c4 db5 c-1 g9**
90
+
91
+ c-1 (that's octave number: negative 1) is the lowest note. g9 is the highest.
92
+
93
+ **Duration: w h q i s r x (that's: whole note, half, quarter, eighth, sixteenth, thirty-second, sixty-fourth)**
94
+
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.
97
+
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:
99
+
100
+ **Intensity: ppp pp p mp mf o ff fff**
101
+
102
+ (You may find it helpful to think "loud" for "o")
103
+
104
+
105
+ TODO: keep documenting this...
106
+
107
+
108
+ Summary of single-letter assignments:
109
+ ```
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
116
+
117
+ h -> half note duration
118
+ i -> eighth note duration
119
+
120
+ o -> forte intensity
121
+ p -> piano intensity
122
+ q -> quarter note duration
123
+ r -> thirty-second note duration
124
+ s -> sixteenth note duration
125
+ t -> triplet modifier on durations
126
+
127
+ w -> whole note duration
128
+ x -> sixty-fourth note duration
129
+ ```
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2011-2013, Adam Murray (adam@compusition.com).
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use of the "MTK" Ruby library, in source and binary forms,
6
+ with or without modification, are permitted provided that the following
7
+ conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright
10
+ notice, this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright
13
+ notice, this list of conditions and the following disclaimer in
14
+ the documentation and/or other materials provided with the
15
+ distribution.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,50 @@
1
+ MTK: a Music Tool Kit for Ruby
2
+ ===
3
+
4
+ MTK is a Ruby library and custom [domain-specific language](https://en.wikipedia.org/wiki/Domain-specific_language) (DSL) for generating musical material.
5
+
6
+ MTK is flexible: You may use the custom music-generating language without writing any Ruby code, or you may avoid the custom language
7
+ and only program in Ruby, or anywhere in between.
8
+
9
+
10
+ Features
11
+ --------
12
+ * A minimalist syntax (DSL) for generating musical patterns
13
+ * Read and write MIDI files
14
+ * Record and output realtime MIDI signals
15
+ * Sequence MIDI-compatible event streams
16
+ * Manipulate musical objects like pitch, duration, and intensity
17
+ * Generate deterministic and non-deterministic music via flexible patterns such as looping sequences and random choices
18
+
19
+ Getting Started
20
+ ---------------
21
+
22
+ MTK works with Ruby 1.9, Ruby 2.0, and JRuby
23
+
24
+ 0. Install
25
+
26
+ gem install mtk
27
+
28
+ or if using JRuby:
29
+
30
+ jgem install mtk
31
+
32
+ 0. Learn the command-line interface:
33
+
34
+ mtk --help
35
+
36
+ 0. Learn the MTK syntax: TODO... documentation forthcoming. In the meantime, see the unit tests @ https://github.com/adamjmurray/mtk/blob/master/spec/mtk/lang/parser_spec.rb
37
+
38
+ 0. Check out examples: https://github.com/adamjmurray/mtk/tree/master/examples
39
+
40
+ 0. Read the [MTK Ruby library documentation](http://rubydoc.info/github/adamjmurray/mtk/master/frames)
41
+
42
+
43
+ About this project
44
+ ------------------
45
+ This project is developed by [Adam Murray (github.com/adamjmurray)](http://github.com/adamjmurray).
46
+
47
+ It is a free and open source project licensed under [a permissive BSD-style license](https://raw.github.com/adamjmurray/mtk/master/LICENSE.txt).
48
+ I simply ask for credit by including my copyright in derivative works.
49
+
50
+ You can learn more about the development of this project at the [development notes page](https://github.com/adamjmurray/mtk/blob/master/DEVELOPMENT_NOTES.md).
@@ -0,0 +1,102 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rake/clean'
3
+
4
+ GEM_VERSION = '0.0.3.3'
5
+
6
+ SUPPORTED_RUBIES = %w[ 1.9.3 2.0 jruby-1.7.4 ]
7
+ ENV['JRUBY_OPTS'] = '--1.9'
8
+
9
+ task :default => :test
10
+
11
+ CLEAN.include('html','doc','coverage.data','coverage', '*.gem') # clean and clobber do the same thing for now
12
+
13
+ desc "Run RSpec tests with full output"
14
+ RSpec::Core::RakeTask.new('test') do |spec|
15
+ spec.rspec_opts = ["--color", "--format", "nested"]
16
+ if ARGV[1]
17
+ # only run specs with filenames starting with the command line argument
18
+ spec.pattern = "spec/**/#{ARGV[1]}*"
19
+ end
20
+ end
21
+
22
+ task :spec => :test
23
+
24
+
25
+ namespace :gem do
26
+ desc "Install gems for supported versions of Ruby: #{SUPPORTED_RUBIES.join ', '}"
27
+ task :install_dependencies do
28
+ fail unless system("rvm #{SUPPORTED_RUBIES.join ','} do bundle install")
29
+ end
30
+
31
+ desc "Build the CRuby and JRuby gems for distribution"
32
+ task :build do
33
+ gem_version = GEM_VERSION
34
+
35
+ gem_name = 'mtk'
36
+ platform_specific_depedencies = {unimidi:'~> 0.3'}
37
+ additional_gem_specifications = {}
38
+ generate_gemspec(binding)
39
+
40
+ gem_name = 'jmtk'
41
+ platform_specific_depedencies = {jsound:'~> 0.1'}
42
+ additional_gem_specifications = {platform:'java'}
43
+ generate_gemspec(binding)
44
+ end
45
+
46
+ def generate_gemspec(erb_bindings)
47
+ gem_name = erb_bindings.eval('gem_name')
48
+
49
+ erb = ERB.new(IO.read 'mtk.gemspec.erb')
50
+ gemspec = erb.result(erb_bindings)
51
+
52
+ gemspec_filename = "#{gem_name}.gemspec"
53
+ puts "Generating #{gemspec_filename}"
54
+ IO.write(gemspec_filename, gemspec)
55
+
56
+ if gem_name == 'jmtk'
57
+ `cp bin/mtk bin/jmtk` # jmtk gem uses this as the binary
58
+ end
59
+ puts "Building gem"
60
+ puts `gem build #{gemspec_filename}`
61
+ ensure
62
+ `rm bin/jmtk`
63
+ end
64
+ end
65
+
66
+
67
+ namespace :test do
68
+ desc "Run RSpec tests with summary output and fast failure"
69
+ RSpec::Core::RakeTask.new(:fast) do |spec|
70
+ spec.rspec_opts = ["--color", "--fail-fast"]
71
+ end
72
+
73
+ desc "Run RSpec tests and generate a coverage report"
74
+ if RUBY_PLATFORM == "java"
75
+ task :cov do |t|
76
+ fail "#{t} task is not compatible with JRuby. Use Ruby 1.9 instead."
77
+ end
78
+ else
79
+ RSpec::Core::RakeTask.new(:cov) do |spec|
80
+ spec.rspec_opts = ["--color", "-r", "#{File.dirname __FILE__}/spec/spec_coverage.rb"]
81
+ end
82
+ end
83
+
84
+ desc "Profile RSpec tests and report 10 slowest"
85
+ RSpec::Core::RakeTask.new(:prof) do |spec|
86
+ spec.rspec_opts = ["--color", "-p"]
87
+ end
88
+
89
+ desc "Run RSpec tests on all supported versions of Ruby: #{SUPPORTED_RUBIES.join ', '}"
90
+ task :all do
91
+ fail unless system("rvm #{SUPPORTED_RUBIES.join ','} do bundle exec rake -f #{__FILE__} test:fast")
92
+ end
93
+ end
94
+
95
+
96
+ begin
97
+ require 'yard'
98
+ YARD::Rake::YardocTask.new(:doc) do |yard|
99
+ yard.files = ['lib/**/*.rb']
100
+ end
101
+ rescue Exception # yard is optional, so don't cause rake to fail if it's missing
102
+ end
@@ -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