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,97 @@
1
+ module MTK
2
+ module IO
3
+
4
+ # Common behavior for realtime MIDI input.
5
+ #
6
+ class MIDIInput
7
+
8
+ class << self
9
+
10
+ def inherited(subclass)
11
+ available_input_types << subclass
12
+ end
13
+
14
+ def available_input_types
15
+ @available_input_types ||= []
16
+ end
17
+
18
+ def input_types_by_device
19
+ @input_types_by_device ||= (
20
+ available_input_types.each_with_object( Hash.new ) do |input_type,hash|
21
+ input_type.devices.each{|device| hash[device] = input_type }
22
+ end
23
+ )
24
+ end
25
+
26
+ # All available input devices.
27
+ def devices
28
+ @devices ||= available_input_types.map{|input_type| input_type.devices }.flatten
29
+ end
30
+
31
+ # Maps input device names to the input device.
32
+ def devices_by_name
33
+ @devices_by_name ||= (
34
+ available_input_types.each_with_object( Hash.new ) do |input_type,hash|
35
+ hash.merge!( input_type.devices_by_name )
36
+ end
37
+ )
38
+ end
39
+
40
+ def find_by_name(name)
41
+ if name.is_a? Regexp
42
+ matching_name = devices_by_name.keys.find{|device_name| device_name =~ name }
43
+ device = devices_by_name[matching_name]
44
+ else
45
+ device = devices_by_name[name.to_s]
46
+ end
47
+ open(device) if device
48
+ end
49
+
50
+ def open(device)
51
+ input_type = input_types_by_device[device]
52
+ input_type.new(device) if input_type
53
+ end
54
+ end
55
+
56
+
57
+ def initialize(input_device, options={})
58
+ @device = input_device
59
+ @device.open
60
+ @options = options
61
+ end
62
+ private_class_method :new
63
+
64
+
65
+ ########################
66
+ public
67
+
68
+ # The underlying output device implementation wrapped by this class.
69
+ # The device type depends on the platform.
70
+ attr_reader :device
71
+
72
+ def name
73
+ @device.name
74
+ end
75
+
76
+ def record
77
+ end
78
+
79
+ def stop
80
+ end
81
+
82
+ def to_timeline
83
+ Timeline.new
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+
90
+
91
+ unless $__RUNNING_RSPEC_TESTS__ # I can't get this working on Travis-CI, problem installing native dependencies
92
+ if RUBY_PLATFORM == 'java'
93
+ require 'mtk/io/jsound_input'
94
+ else
95
+ require 'mtk/io/unimidi_input'
96
+ end
97
+ end
@@ -0,0 +1,195 @@
1
+ require 'rbconfig'
2
+ require 'gamelan'
3
+
4
+ module MTK
5
+ module IO
6
+
7
+ # Provides a scheduler and common behavior for realtime MIDI output, using the gamelan gem for scheduling.
8
+ #
9
+ # @abstract Subclasses must provide {.devices}, {.devices_by_name}, {#note_on}, {#note_off}, {#control}, {#channel_pressure}, {#poly_pressure}, {#bend}, and {#program} to implement a MIDI output.
10
+ #
11
+ class MIDIOutput
12
+
13
+ class << self
14
+
15
+ def inherited(subclass)
16
+ available_output_types << subclass
17
+ end
18
+
19
+ def available_output_types
20
+ @available_output_types ||= []
21
+ end
22
+
23
+ def output_types_by_device
24
+ @output_types_by_device ||= (
25
+ available_output_types.each_with_object( Hash.new ) do |output_type,hash|
26
+ output_type.devices.each{|device| hash[device] = output_type }
27
+ end
28
+ )
29
+ end
30
+
31
+ # All available output devices.
32
+ def devices
33
+ @devices ||= available_output_types.map{|output_type| output_type.devices }.flatten
34
+ end
35
+
36
+ # Maps output device names to the output device.
37
+ def devices_by_name
38
+ @devices_by_name ||= (
39
+ available_output_types.each_with_object( Hash.new ) do |output_type,hash|
40
+ hash.merge!( output_type.devices_by_name )
41
+ end
42
+ )
43
+ end
44
+
45
+ def find_by_name(name)
46
+ if name.is_a? Regexp
47
+ matching_name = devices_by_name.keys.find{|device_name| device_name =~ name }
48
+ device = devices_by_name[matching_name]
49
+ else
50
+ device = devices_by_name[name.to_s]
51
+ end
52
+ open(device) if device
53
+ end
54
+
55
+ def open(device)
56
+ output_type = output_types_by_device[device]
57
+ output_type.new(device) if output_type
58
+ end
59
+ end
60
+
61
+
62
+ def initialize(output_device, options={})
63
+ @device = output_device
64
+ @device.open
65
+ @options = options
66
+ end
67
+ private_class_method :new
68
+
69
+
70
+ ########################
71
+ public
72
+
73
+ # The underlying output device implementation wrapped by this class.
74
+ # The device type depends on the platform.
75
+ attr_reader :device
76
+
77
+ def name
78
+ @device.name
79
+ end
80
+
81
+ def play(anything, options={})
82
+ timeline = case anything
83
+ when MTK::Events::Timeline then anything
84
+ when Hash then MTK::Events::Timeline.from_h anything
85
+ when Enumerable,MTK::Events::Event then MTK::Events::Timeline.from_h(0 => anything)
86
+ else raise "#{self.class}.play() doesn't understand #{anything} (#{anything.class})"
87
+ end
88
+ timeline = timeline.flatten
89
+
90
+ scheduler_rate = options.fetch :scheduler_rate, 500 # default: 500 Hz
91
+ trailing_buffer = options.fetch :trailing_buffer, 2 # default: continue playing for 2 beats after the end of the timeline
92
+ in_background = options.fetch :background, false # default: don't run in background Thread
93
+ bpm = options.fetch :bmp, 120 # default: 120 beats per minute
94
+
95
+ @scheduler = Gamelan::Scheduler.new :tempo => bpm, :rate => scheduler_rate
96
+
97
+ timeline.each do |time,events|
98
+ events.each do |event|
99
+ next if event.rest?
100
+
101
+ channel = event.channel || 0
102
+
103
+ case event.type
104
+ when :note
105
+ pitch = event.midi_pitch
106
+ velocity = event.velocity
107
+ duration = event.duration.to_f
108
+ @scheduler.at(time) { note_on(pitch,velocity,channel) }
109
+ @scheduler.at(time + duration) { note_off(pitch,velocity,channel) }
110
+
111
+ when :control
112
+ @scheduler.at(time) { control(event.number, event.midi_value, channel) }
113
+
114
+ when :pressure
115
+ if event.number
116
+ @scheduler.at(time) { poly_pressure(event.number, event.midi_value, channel) }
117
+ else
118
+ @scheduler.at(time) { channel_pressure(event.midi_value, channel) }
119
+ end
120
+
121
+ when :bend
122
+ @scheduler.at(time) { bend(event.midi_value, channel) }
123
+
124
+ when :program
125
+ @scheduler.at(time) { program(event.number, channel) }
126
+ end
127
+ end
128
+ end
129
+
130
+ end_time = timeline.times.last + trailing_buffer
131
+ @scheduler.at(end_time) { @scheduler.stop }
132
+
133
+ thread = @scheduler.run
134
+ thread.join if not in_background
135
+ end
136
+
137
+
138
+ ########################
139
+ protected
140
+
141
+ # these all return stubbed data for testing purposes
142
+
143
+ # Send a note on event to the MIDI output
144
+ def note_on(midi_pitch, velocity, channel)
145
+ [:note_on, midi_pitch, velocity, channel] # stubbed data for testing purposes
146
+ end
147
+
148
+ # Send a note off event to the MIDI output
149
+ def note_off(midi_pitch, velocity, channel)
150
+ [:note_off, midi_pitch, velocity, channel]
151
+ end
152
+
153
+ # Send a control change event to the MIDI output
154
+ def control(number, midi_value, channel)
155
+ [:control, number, midi_value, channel]
156
+ end
157
+
158
+ # Send a poly pressure event to the MIDI output.
159
+ def poly_pressure(midi_pitch, midi_value, channel)
160
+ [:poly_pressure, midi_pitch, midi_value, channel]
161
+ end
162
+
163
+ # Send a channel pressure event to the MIDI output.
164
+ def channel_pressure(midi_value, channel)
165
+ [:channel_pressure, midi_value, channel]
166
+ end
167
+
168
+ # Send a pitch bend event to the MIDI output.
169
+ def bend(midi_value, channel)
170
+ [:bend, midi_value, channel]
171
+ end
172
+
173
+ # Send a program change event to the MIDI output.
174
+ def program(number, channel)
175
+ [:program, number, channel]
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+ end
182
+
183
+
184
+ unless $__RUNNING_RSPEC_TESTS__ # I can't get this working on Travis-CI, problem installing native dependencies
185
+ if RbConfig::CONFIG['host_os'] =~ /darwin/
186
+ # We're running on OS X
187
+ require 'mtk/io/dls_synth_output'
188
+ end
189
+
190
+ if RUBY_PLATFORM == 'java'
191
+ require 'mtk/io/jsound_output'
192
+ else
193
+ require 'mtk/io/unimidi_output'
194
+ end
195
+ end
@@ -0,0 +1,162 @@
1
+ require 'tempfile'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ module MTK
6
+ module IO
7
+
8
+ # Uses {Events::Timeline}s to generates music notation graphics with {http://lilypond.org/ Lilypond}.
9
+ # @note This class is optional and only available if you require 'mtk/io/lilypond'.
10
+ # @note To make notation graphics, {http://lilypond.org/download.html Lilypond} must be installed
11
+ # and you must follow the "Running on the command-line" instructions (found on the download page for
12
+ # your operating system). If the lilypond command is not on your PATH, set the environment variable LILYPOND_PATH
13
+ class Notation
14
+
15
+ LILYPOND_PATH = ENV['LILYPOND_PATH'] || 'lilypond'
16
+
17
+ VALID_FORMATS = %w( png pdf ps )
18
+
19
+
20
+ def initialize(file, options={})
21
+ @file = file
22
+ @options = options
23
+
24
+ @format = File.extname(file)[1..-1].downcase
25
+ raise ArgumentError.new("Invalid file format '#{@format}'") unless VALID_FORMATS.include? @format
26
+
27
+ @dpi = options[:dpi]
28
+ end
29
+
30
+ def self.open(file, options={})
31
+ new(file,options)
32
+ end
33
+
34
+
35
+ def write(timeline)
36
+ lilypond_syntax = syntax_for_timeline(timeline)
37
+ puts lilypond_syntax
38
+ puts "_____________________________"
39
+ Tempfile.open('mtk_lilypond') do |lilypond_file|
40
+ Dir.mktmpdir do |tmpdir|
41
+ # use the directory...
42
+ #open("#{dir}/foo", "w") { ... }
43
+
44
+ lilypond_file.write(lilypond_syntax)
45
+ lilypond_file.flush
46
+
47
+ cmd = ['lilypond', '-dbackend=eps', "-f#{@format}", "--output=\"#{tmpdir}\""]
48
+ cmd << "-dresolution=#{@dpi}" if @dpi
49
+ cmd << lilypond_file.path
50
+ cmd = cmd.join(' ')
51
+
52
+ puts cmd if $DEBUG
53
+ lilypond_command_output = `#{cmd}`
54
+ puts lilypond_command_output if $DEBUG
55
+
56
+ output_file = Dir["#{tmpdir}/*.#{@format}"].first
57
+
58
+ FileUtils.cp output_file, @file
59
+
60
+ puts "Wrote #{@file}"
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ ########################
67
+ private
68
+
69
+ QUANTIZATION_INTERVAL = 0.0625 # 64th note granularity
70
+
71
+ SYNTAX_PREFIX = '
72
+ \language "english"
73
+ \paper{
74
+ oddFooterMarkup=##f
75
+ oddHeaderMarkup=##f
76
+ }
77
+ \new PianoStaff {
78
+ \autochange {
79
+ '
80
+
81
+ SYNTAX_SUFFIX = '
82
+ }
83
+ }
84
+ '
85
+
86
+ def syntax_for_timeline(timeline)
87
+ quantized_timeline = timeline.flatten.quantize(QUANTIZATION_INTERVAL)
88
+ last_time = 0
89
+ last_duration = 0
90
+
91
+ s = ''
92
+ s << SYNTAX_PREFIX
93
+
94
+ for time,events in quantized_timeline
95
+
96
+ # handle rests between notes
97
+ delta = time - last_time
98
+ if delta > last_duration
99
+ s << 'r'+syntax_for_duration(delta - last_duration)
100
+ s << ' '
101
+ end
102
+
103
+ notes = events.find_all{|event| event.type == :note }
104
+
105
+ if notes.length > 1
106
+ # a chord
107
+ s << '<'
108
+ total_duration = 0
109
+ for note in notes
110
+ total_duration += note.duration
111
+ s << syntax_for_pitch(note.pitch)
112
+ s << ' '
113
+ end
114
+ s << '>'
115
+ duration = total_duration.to_f/notes.length
116
+ s << syntax_for_duration(duration)
117
+
118
+ else # a single note
119
+ note = notes.first
120
+ s << syntax_for_pitch(note.pitch)
121
+ duration = note.duration
122
+ s << syntax_for_duration(duration)
123
+ end
124
+
125
+ last_time = time
126
+ last_duration = duration
127
+ s << ' '
128
+ end
129
+
130
+ s << SYNTAX_SUFFIX
131
+ s
132
+ end
133
+
134
+ def syntax_for_pitch(pitch)
135
+ syntax = pitch.pitch_class.name.downcase
136
+ if syntax.length > 0
137
+ syntax = syntax[0] + syntax[1..-1].gsub('b','f') # .gsub('#','s') pitch class names never have '#'
138
+ end
139
+ oct = pitch.octave
140
+ while oct > 3
141
+ syntax << "'"
142
+ oct -= 1
143
+ end
144
+ while oct < 3
145
+ syntax << ","
146
+ oct += 1
147
+ end
148
+ syntax
149
+ end
150
+
151
+ def syntax_for_duration(duration)
152
+ # TODO: handle dots, triplets, and ties of arbitrary durations
153
+ duration = MTK::Events::Timeline.quantize_time(duration.to_f.abs, QUANTIZATION_INTERVAL)
154
+ syntax = (4.0/duration).round
155
+ syntax = 1 if syntax < 1
156
+ syntax.to_s
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end