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
@@ -0,0 +1,23 @@
1
+ module MTK
2
+ module Groups
3
+
4
+ # An extension to {Collection}, which provides additional transformations for pitch-like collections.
5
+ #
6
+ module PitchCollection
7
+ include Collection
8
+
9
+ # Transpose all elements upward by the given interval
10
+ # @param interval_in_semitones [Numeric] an interval in semitones
11
+ def transpose interval_in_semitones
12
+ map{|elem| elem + interval_in_semitones }
13
+ end
14
+
15
+ # Invert all elements around the given inversion point
16
+ # @param inversion_point [Numeric] the value around which all elements will be inverted (defaults to the first element in the collection)
17
+ def invert(inversion_point=first)
18
+ map{|elem| elem.invert(inversion_point) }
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,146 @@
1
+ require 'ffi'
2
+
3
+ module MTK
4
+ module IO
5
+
6
+ # An output device for Apple's built-in "DLS" synthesizer on OS X
7
+ class DLSSynthDevice
8
+
9
+ # @private
10
+ module AudioToolbox
11
+ extend FFI::Library
12
+ ffi_lib '/System/Library/Frameworks/AudioToolbox.framework/Versions/Current/AudioToolbox'
13
+ ffi_lib '/System/Library/Frameworks/AudioUnit.framework/Versions/Current/AudioUnit'
14
+
15
+ # @private
16
+ class ComponentDescription < FFI::Struct
17
+ layout :componentType, :int,
18
+ :componentSubType, :int,
19
+ :componentManufacturer, :int,
20
+ :componentFlags, :int,
21
+ :componentFlagsMask, :int
22
+ end
23
+
24
+ def self.to_bytes(s)
25
+ bytes = 0
26
+ s.each_byte do |byte|
27
+ bytes <<= 8
28
+ bytes += byte
29
+ end
30
+ return bytes
31
+ end
32
+
33
+ AUDIO_UNIT_MANUFACTURER_APPLE = to_bytes('appl')
34
+ AUDIO_UNIT_TYPE_MUSIC_DEVICE = to_bytes('aumu')
35
+ AUDIO_UNIT_SUBTYPE_DLS_SYNTH = to_bytes('dls ')
36
+ AUDIO_UNIT_TYPE_OUTPUT = to_bytes('auou')
37
+ AUDIO_UNIT_SUBTYPE_DEFAULT_OUTPUT = to_bytes('def ')
38
+
39
+ # int NewAUGraph(void *)
40
+ attach_function :NewAUGraph, [:pointer], :int
41
+
42
+ # int AUGraphAddNode(void *, ComponentDescription *, void *)
43
+ attach_function :AUGraphAddNode, [:pointer, :pointer, :pointer], :int
44
+
45
+ # int AUGraphOpen(void *)
46
+ attach_function :AUGraphOpen, [:pointer], :int
47
+
48
+ # int AUGraphConnectNodeInput(void *, void *, int, void *, int)
49
+ attach_function :AUGraphConnectNodeInput, [:pointer, :pointer, :int, :pointer, :int], :int
50
+
51
+ # int AUGraphNodeInfo(void *, void *, ComponentDescription *, void *)
52
+ attach_function :AUGraphNodeInfo, [:pointer, :pointer, :pointer, :pointer], :int
53
+
54
+ # int AUGraphInitialize(void *)
55
+ attach_function :AUGraphInitialize, [:pointer], :int
56
+
57
+ # int AUGraphStart(void *)
58
+ attach_function :AUGraphStart, [:pointer], :int
59
+
60
+ # int AUGraphStop(void *)
61
+ attach_function :AUGraphStop, [:pointer], :int
62
+
63
+ # int DisposeAUGraph(void *)
64
+ attach_function :DisposeAUGraph, [:pointer], :int
65
+
66
+ # void * CAShow(void *)
67
+ attach_function :CAShow, [:pointer], :void
68
+
69
+ # void * MusicDeviceMIDIEvent(void *, int, int, int, int)
70
+ attach_function :MusicDeviceMIDIEvent, [:pointer, :int, :int, :int, :int], :void
71
+
72
+ end
73
+
74
+
75
+ ##################################
76
+
77
+ def name
78
+ 'Apple DLS Synthesizer'
79
+ end
80
+
81
+
82
+ def require_noerr(action_description, &block)
83
+ if block.call != 0
84
+ fail "Failed to #{action_description}"
85
+ end
86
+ end
87
+
88
+
89
+ def open
90
+ synth_pointer = FFI::MemoryPointer.new(:pointer)
91
+ graph_pointer = FFI::MemoryPointer.new(:pointer)
92
+ synth_node_pointer = FFI::MemoryPointer.new(:pointer)
93
+ out_node_pointer = FFI::MemoryPointer.new(:pointer)
94
+
95
+ cd = AudioToolbox::ComponentDescription.new
96
+ cd[:componentManufacturer] = AudioToolbox::AUDIO_UNIT_MANUFACTURER_APPLE
97
+ cd[:componentFlags] = 0
98
+ cd[:componentFlagsMask] = 0
99
+
100
+ require_noerr('create AUGraph') { AudioToolbox.NewAUGraph(graph_pointer) }
101
+ @graph = graph_pointer.get_pointer(0)
102
+
103
+ cd[:componentType] = AudioToolbox::AUDIO_UNIT_TYPE_MUSIC_DEVICE
104
+ cd[:componentSubType] = AudioToolbox::AUDIO_UNIT_SUBTYPE_DLS_SYNTH
105
+ require_noerr('add synthNode') { AudioToolbox.AUGraphAddNode(@graph, cd, synth_node_pointer) }
106
+ synth_node = synth_node_pointer.get_pointer(0)
107
+
108
+ cd[:componentType] = AudioToolbox::AUDIO_UNIT_TYPE_OUTPUT
109
+ cd[:componentSubType] = AudioToolbox::AUDIO_UNIT_SUBTYPE_DEFAULT_OUTPUT
110
+ require_noerr('add outNode') { AudioToolbox.AUGraphAddNode(@graph, cd, out_node_pointer) }
111
+ out_node = out_node_pointer.get_pointer(0)
112
+
113
+ require_noerr('open graph') { AudioToolbox.AUGraphOpen(@graph) }
114
+
115
+ require_noerr('connect synth to out') { AudioToolbox.AUGraphConnectNodeInput(@graph, synth_node, 0, out_node, 0) }
116
+
117
+ require_noerr('graph info') { AudioToolbox.AUGraphNodeInfo(@graph, synth_node, nil, synth_pointer) }
118
+ @synth = synth_pointer.get_pointer(0)
119
+
120
+ require_noerr('init graph') { AudioToolbox.AUGraphInitialize(@graph) }
121
+ require_noerr('start graph') { AudioToolbox.AUGraphStart(@graph) }
122
+
123
+ # AudioToolbox.CAShow(@graph) # for debugging
124
+ end
125
+
126
+
127
+ def message(*args)
128
+ arg0 = args[0] || 0
129
+ arg1 = args[1] || 0
130
+ arg2 = args[2] || 0
131
+ AudioToolbox.MusicDeviceMIDIEvent(@synth, arg0, arg1, arg2, 0)
132
+ end
133
+
134
+
135
+ def close
136
+ if @graph
137
+ AudioToolbox.AUGraphStop(@graph)
138
+ AudioToolbox.DisposeAUGraph(@graph)
139
+ end
140
+ end
141
+ end
142
+
143
+ end
144
+ end
145
+
146
+
@@ -0,0 +1,62 @@
1
+ require 'mtk/io/dls_synth_device'
2
+
3
+ module MTK
4
+ module IO
5
+
6
+ # Provides realtime MIDI output on OS X to the built-in "DLS" Synthesizer
7
+ # @note This class is optional and only available if you require 'mtk/midi/dls_synth_output'.
8
+ # It depends on the 'gamelan' gem.
9
+ class DLSSynthOutput < MIDIOutput
10
+
11
+ public_class_method :new
12
+
13
+ def self.devices
14
+ @devices ||= [DLSSynthDevice.new]
15
+ end
16
+
17
+ def self.devices_by_name
18
+ @devices_by_name ||= {devices.first.name => devices.first}
19
+ end
20
+
21
+
22
+ ######################
23
+ protected
24
+
25
+ # (see MIDIOutput#note_on)
26
+ def note_on(pitch, velocity, channel)
27
+ @device.message(0x90|channel, pitch, velocity)
28
+ end
29
+
30
+ # (see MIDIOutput#note_off)
31
+ def note_off(pitch, velocity, channel)
32
+ @device.message(0x80|channel, pitch, velocity)
33
+ end
34
+
35
+ # (see MIDIOutput#control)
36
+ def control(number, midi_value, channel)
37
+ @device.message(0xB0|channel, number, midi_value)
38
+ end
39
+
40
+ # (see MIDIOutput#channel_pressure)
41
+ def channel_pressure(midi_value, channel)
42
+ @device.message(0xD0|channel, midi_value, 0)
43
+ end
44
+
45
+ # (see MIDIOutput#poly_pressure)
46
+ def poly_pressure(pitch, midi_value, channel)
47
+ @device.message(0xA0|channel, pitch, midi_value)
48
+ end
49
+
50
+ # (see MIDIOutput#bend)
51
+ def bend(midi_value, channel)
52
+ @device.message(0xE0|channel, midi_value & 127, (midi_value >> 7) & 127)
53
+ end
54
+
55
+ # (see MIDIOutput#program)
56
+ def program(number, channel)
57
+ @device.message(0xC0|channel, number, 0)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,87 @@
1
+ require 'jsound'
2
+
3
+ module MTK
4
+ module IO
5
+
6
+ # Provides realtime MIDI input for JRuby via the jsound gem.
7
+ # @note This class is optional and only available if you require 'mtk/midi/jsound_input'.
8
+ # It depends on the 'jsound' gem.
9
+ class JSoundInput < MIDIInput
10
+
11
+ public_class_method :new
12
+
13
+ def self.devices
14
+ @devices ||= ::JSound::Midi::INPUTS.devices
15
+ end
16
+
17
+ def self.devices_by_name
18
+ @devices_by_name ||= devices.each_with_object( Hash.new ){|device,hash| hash[device.description] = device }
19
+ end
20
+
21
+
22
+ attr_reader :device
23
+
24
+ def initialize(input_device, options={})
25
+ @device = input_device
26
+ @recorder = ::JSound::Midi::Devices::Recorder.new(false)
27
+ @device.open
28
+ end
29
+
30
+ def name
31
+ @device.description
32
+ end
33
+
34
+ def record(options={})
35
+ if options[:monitor]
36
+ @monitor = ::JSound::Midi::Devices::Monitor.new
37
+ @device >> [@monitor, @recorder]
38
+ else
39
+ @device >> @recorder
40
+ end
41
+
42
+ @recorder.clear
43
+ @recorder.start
44
+ end
45
+
46
+ def stop
47
+ @recorder.stop
48
+ end
49
+
50
+ def to_timeline(options={})
51
+ bpm = options.fetch :bmp, 120
52
+ beats_per_second = bpm.to_f/60
53
+ timeline = Timeline.new
54
+ note_ons = {}
55
+ start = nil
56
+
57
+ @recorder.messages_with_timestamps.each do |message,time|
58
+ start = time unless start
59
+ time -= start
60
+ time /= beats_per_second
61
+
62
+ case message.type
63
+ when :note_on
64
+ note_ons[message.pitch] = [message,time]
65
+
66
+ when :note_off
67
+ if note_ons.has_key? message.pitch
68
+ note_on, start_time = note_ons.delete(message.pitch)
69
+ duration = time - start_time
70
+ note = MTK::Events::Note.from_midi(note_on.pitch, note_on.velocity, duration, message.channel)
71
+ timeline.add time,note
72
+ end
73
+
74
+ else timeline.add time, MTK::Events::Parameter.from_midi([message.type, message.channel], message.data1, message.data2)
75
+ end
76
+ end
77
+
78
+ timeline.quantize! options[:quantize] if options.key? :quantize
79
+ timeline.shift_to! options[:shift_to] if options.key? :shift_to
80
+
81
+ timeline
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,82 @@
1
+ require 'jsound'
2
+
3
+ module MTK
4
+ module IO
5
+
6
+ # Provides realtime MIDI output for JRuby via the jsound and gamelan gems.
7
+ # @note This class is optional and only available if you require 'mtk/midi/jsound_output'.
8
+ # It depends on the 'jsound' and 'gamelan' gems.
9
+ class JSoundOutput < MIDIOutput
10
+
11
+ public_class_method :new
12
+
13
+ def self.devices
14
+ @devices ||= ::JSound::Midi::OUTPUTS.devices
15
+ end
16
+
17
+ def self.devices_by_name
18
+ @devices_by_name ||= devices.each_with_object( Hash.new ){|device,hash| hash[device.description] = device }
19
+ end
20
+
21
+
22
+ def initialize(device, options={})
23
+ @device = device
24
+
25
+ # and create an object for generating MIDI message to send to the output:
26
+ @generator = ::JSound::Midi::Devices::Generator.new
27
+
28
+ if options[:monitor]
29
+ @monitor = ::JSound::Midi::Devices::Monitor.new
30
+ @generator >> [@monitor, @device]
31
+ else
32
+ @generator >> @device
33
+ end
34
+ @device.open
35
+ end
36
+
37
+ def name
38
+ @device.description
39
+ end
40
+
41
+ ######################
42
+ protected
43
+
44
+ # (see MIDIOutput#note_on)
45
+ def note_on(pitch, velocity, channel)
46
+ @generator.note_on(pitch, velocity, channel)
47
+ end
48
+
49
+ # (see MIDIOutput#note_off)
50
+ def note_off(pitch, velocity, channel)
51
+ @generator.note_off(pitch, velocity, channel)
52
+ end
53
+
54
+ # (see MIDIOutput#control)
55
+ def control(number, midi_value, channel)
56
+ @generator.control_change(number, midi_value, channel)
57
+ end
58
+
59
+ # (see MIDIOutput#channel_pressure)
60
+ def channel_pressure(midi_value, channel)
61
+ @generator.channel_pressure(midi_value, channel)
62
+ end
63
+
64
+ # (see MIDIOutput#poly_pressure)
65
+ def poly_pressure(pitch, midi_value, channel)
66
+ @generator.poly_pressure(pitch, midi_value, channel)
67
+ end
68
+
69
+ # (see MIDIOutput#bend)
70
+ def bend(midi_value, channel)
71
+ @generator.pitch_bend(midi_value, channel)
72
+ end
73
+
74
+ # (see MIDIOutput#program)
75
+ def program(number, channel)
76
+ @generator.program_change(number, channel)
77
+ end
78
+
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,209 @@
1
+ require 'midilib'
2
+
3
+ module MTK
4
+ module IO
5
+
6
+ # MIDI file I/O: reads MIDI files into {Events::Timeline}s and writes {Events::Timeline}s to MIDI files.
7
+ # @note This class is optional and only available if you require 'mtk/midi/file'.
8
+ # It depends on the 'midilib' gem.
9
+ class MIDIFile
10
+ def initialize file
11
+ if file.respond_to? :path
12
+ @file = file.path
13
+ else
14
+ @file = file.to_s
15
+ end
16
+ end
17
+
18
+ # Read a MIDI file into an Array of {Events::Timeline}s
19
+ #
20
+ # @return [Timeline]
21
+ #
22
+ def to_timelines
23
+ timelines = []
24
+
25
+ ::File.open(@file, 'rb') do |f|
26
+ sequence = ::MIDI::Sequence.new
27
+ sequence.read(f)
28
+ pulses_per_beat = sequence.ppqn.to_f
29
+ track_idx = -1
30
+
31
+ sequence.each do |track|
32
+ track_idx += 1
33
+ timeline = MTK::Events::Timeline.new
34
+ note_ons = {}
35
+ #puts "TRACK #{track_idx}"
36
+
37
+ track.each do |event|
38
+ #puts "#{event.class}: #{event} @#{event.time_from_start}"
39
+ time = (event.time_from_start)/pulses_per_beat
40
+
41
+ case event
42
+ when ::MIDI::NoteOn
43
+ note_ons[event.note] = [time,event]
44
+
45
+ when ::MIDI::NoteOff
46
+ on_time,on_event = note_ons.delete(event.note)
47
+ if on_event
48
+ duration = time - on_time
49
+ note = MTK::Events::Note.from_midi(event.note, on_event.velocity, duration, event.channel)
50
+ timeline.add on_time, note
51
+ end
52
+
53
+ when ::MIDI::Controller, ::MIDI::PolyPressure, ::MIDI::ChannelPressure, ::MIDI::PitchBend, ::MIDI::ProgramChange
54
+ timeline.add time, MTK::Events::Parameter.from_midi(*event.data_as_bytes)
55
+
56
+ when ::MIDI::Tempo
57
+ # Not sure if event.tempo needs to be converted? TODO: test!
58
+ timeline.add time, MTK::Events::Parameter.new(:tempo, :value => event.tempo)
59
+ end
60
+ end
61
+ timelines << timeline
62
+ end
63
+ end
64
+ timelines
65
+ end
66
+
67
+ def write(anything)
68
+ case anything
69
+ when MTK::Events::Timeline then write_timeline(anything)
70
+ when Enumerable then write_timelines(anything)
71
+ else raise "#{self.class}#write doesn't understand #{anything.class}"
72
+ end
73
+ end
74
+
75
+ def write_timelines(timelines, parent_sequence=nil)
76
+ sequence = parent_sequence || ::MIDI::Sequence.new
77
+ timelines.each{|timeline| write_timeline(timeline, sequence) }
78
+ write_to_disk sequence unless parent_sequence
79
+ end
80
+
81
+ # Write the Timeline as a MIDI file
82
+ #
83
+ # @param timeline [Timeline]
84
+ def write_timeline(timeline, parent_sequence=nil)
85
+ sequence = parent_sequence || ::MIDI::Sequence.new
86
+ clock_rate = sequence.ppqn
87
+ track = add_track sequence
88
+
89
+ timeline.each do |time,events|
90
+ time *= clock_rate
91
+
92
+ events.each do |event|
93
+ next if event.rest?
94
+
95
+ channel = (event.channel || 1) - 1 # midilib seems to count channels from 0, hence the -1
96
+
97
+ case event.type
98
+ when :note
99
+ pitch, velocity = event.midi_pitch, event.velocity
100
+ add_event track, time => note_on(channel, pitch, velocity)
101
+ duration = event.duration_in_pulses(clock_rate)
102
+ add_event track, time+duration => note_off(channel, pitch, velocity)
103
+
104
+ when :control
105
+ add_event track, time => cc(channel, event.number, event.midi_value)
106
+
107
+ when :pressure
108
+ if event.number
109
+ add_event track, time => poly_pressure(channel, event.number, event.midi_value)
110
+ else
111
+ add_event track, time => channel_pressure(channel, event.midi_value)
112
+ end
113
+
114
+ when :bend
115
+ add_event track, time => pitch_bend(channel, event.midi_value)
116
+
117
+ when :program
118
+ add_event track, time => program(channel, event.midi_value)
119
+
120
+ when :tempo
121
+ add_event track, time => tempo(event.value)
122
+ end
123
+ end
124
+ end
125
+ track.recalc_delta_from_times
126
+
127
+ write_to_disk sequence unless parent_sequence
128
+ end
129
+
130
+
131
+ ########################
132
+ private
133
+
134
+ def write_to_disk(sequence)
135
+ puts "Writing file #{@file}" unless $__RUNNING_RSPEC_TESTS__
136
+ ::File.open(@file, 'wb') { |f| sequence.write f }
137
+ end
138
+
139
+ def print_midi sequence
140
+ sequence.each do |track|
141
+ puts "\n*** track \"#{track.name}\""
142
+ puts "#{track.events.length} events"
143
+ track.each do |event|
144
+ puts "#{event.to_s} (#{event.time_from_start})"
145
+ end
146
+ end
147
+ end
148
+
149
+ # Set tempo in terms of Quarter Notes per Minute (aka BPM)
150
+ def tempo(bpm)
151
+ ms_per_quarter_note = ::MIDI::Tempo.bpm_to_mpq(bpm)
152
+ ::MIDI::Tempo.new(ms_per_quarter_note)
153
+ end
154
+
155
+ def program(channel, program_number)
156
+ ::MIDI::ProgramChange.new(channel, program_number)
157
+ end
158
+
159
+ def note_on(channel, pitch, velocity)
160
+ ::MIDI::NoteOn.new(channel, pitch.to_i, velocity)
161
+ end
162
+
163
+ def note_off(channel, pitch, velocity)
164
+ ::MIDI::NoteOff.new(channel, pitch.to_i, velocity)
165
+ end
166
+
167
+ def cc(channel, controller, value)
168
+ ::MIDI::Controller.new(channel, controller, value)
169
+ end
170
+
171
+ def poly_pressure(channel, pitch, value)
172
+ ::MIDI::PolyPressure(channel, pitch.to_i, value)
173
+ end
174
+
175
+ def channel_pressure(channel, value)
176
+ ::MIDI::ChannelPressure(channel, value)
177
+ end
178
+
179
+ def pitch_bend(channel, value)
180
+ ::MIDI::PitchBend.new(channel, value)
181
+ end
182
+
183
+ def add_track sequence, opts={}
184
+ track = ::MIDI::Track.new(sequence)
185
+ track.name = opts.fetch :name, ''
186
+ sequence.tracks << track
187
+ track
188
+ end
189
+
190
+ def add_event track, event_hash
191
+ for time, event in event_hash
192
+ event.time_from_start = time.round # MIDI file event times must be in whole number pulses (typically 480 or 960 per quarter note)
193
+ track.events << event
194
+ event
195
+ end
196
+ end
197
+
198
+ end
199
+ end
200
+
201
+ # Shortcut for MTK::IO::MIDIFile.new
202
+ # @note Only available if you require 'mtk/midi/file'
203
+ def MIDIFile(f)
204
+ ::MTK::IO::MIDIFile.new(f)
205
+ end
206
+ module_function :MIDIFile
207
+
208
+ end
209
+