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.
- data/.yardopts +10 -0
- data/DEVELOPMENT_NOTES.md +115 -0
- data/INTRO.md +129 -0
- data/LICENSE.txt +27 -0
- data/README.md +50 -0
- data/Rakefile +102 -0
- data/bin/jmtk +250 -0
- data/bin/mtk +250 -0
- data/examples/crescendo.rb +20 -0
- data/examples/drum_pattern.rb +23 -0
- data/examples/dynamic_pattern.rb +36 -0
- data/examples/gets_and_play.rb +27 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +17 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +7 -0
- data/examples/tone_row_melody.rb +23 -0
- data/lib/mtk.rb +76 -0
- data/lib/mtk/core/duration.rb +213 -0
- data/lib/mtk/core/intensity.rb +158 -0
- data/lib/mtk/core/interval.rb +157 -0
- data/lib/mtk/core/pitch.rb +154 -0
- data/lib/mtk/core/pitch_class.rb +194 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/groups/collection.rb +196 -0
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/groups/pitch_collection.rb +23 -0
- data/lib/mtk/io/dls_synth_device.rb +146 -0
- data/lib/mtk/io/dls_synth_output.rb +62 -0
- data/lib/mtk/io/jsound_input.rb +87 -0
- data/lib/mtk/io/jsound_output.rb +82 -0
- data/lib/mtk/io/midi_file.rb +209 -0
- data/lib/mtk/io/midi_input.rb +97 -0
- data/lib/mtk/io/midi_output.rb +195 -0
- data/lib/mtk/io/notation.rb +162 -0
- data/lib/mtk/io/unimidi_input.rb +117 -0
- data/lib/mtk/io/unimidi_output.rb +140 -0
- data/lib/mtk/lang/durations.rb +57 -0
- data/lib/mtk/lang/intensities.rb +61 -0
- data/lib/mtk/lang/intervals.rb +73 -0
- data/lib/mtk/lang/mtk_grammar.citrus +237 -0
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/lang/pitch_classes.rb +29 -0
- data/lib/mtk/lang/pitches.rb +52 -0
- data/lib/mtk/lang/pseudo_constants.rb +26 -0
- data/lib/mtk/lang/variable.rb +32 -0
- data/lib/mtk/numeric_extensions.rb +66 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/patterns/choice.rb +43 -0
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/patterns/lines.rb +54 -0
- data/lib/mtk/patterns/palindrome.rb +45 -0
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/sequencers/event_builder.rb +132 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/sequencers/sequencer.rb +111 -0
- data/lib/mtk/sequencers/step_sequencer.rb +26 -0
- data/spec/mtk/core/duration_spec.rb +372 -0
- data/spec/mtk/core/intensity_spec.rb +289 -0
- data/spec/mtk/core/interval_spec.rb +265 -0
- data/spec/mtk/core/pitch_class_spec.rb +343 -0
- data/spec/mtk/core/pitch_spec.rb +297 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/events/timeline_spec.rb +430 -0
- data/spec/mtk/groups/chord_spec.rb +85 -0
- data/spec/mtk/groups/collection_spec.rb +374 -0
- data/spec/mtk/groups/melody_spec.rb +225 -0
- data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
- data/spec/mtk/io/midi_file_spec.rb +243 -0
- data/spec/mtk/io/midi_output_spec.rb +102 -0
- data/spec/mtk/lang/durations_spec.rb +89 -0
- data/spec/mtk/lang/intensities_spec.rb +101 -0
- data/spec/mtk/lang/intervals_spec.rb +143 -0
- data/spec/mtk/lang/parser_spec.rb +603 -0
- data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
- data/spec/mtk/lang/pitches_spec.rb +56 -0
- data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/variable_spec.rb +52 -0
- data/spec/mtk/numeric_extensions_spec.rb +83 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/patterns/choice_spec.rb +97 -0
- data/spec/mtk/patterns/cycle_spec.rb +123 -0
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/patterns/function_spec.rb +120 -0
- data/spec/mtk/patterns/lines_spec.rb +77 -0
- data/spec/mtk/patterns/palindrome_spec.rb +108 -0
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.mid +0 -0
- 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
|
+
|