midi-message 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2010-2011 Ari Russo
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.rdoc ADDED
@@ -0,0 +1,100 @@
1
+ = MIDI Message
2
+
3
+ {pic}[http://images.treetrouble.net/images/mks80_small.jpg]
4
+
5
+ MIDI messages, objectified in Ruby
6
+
7
+ == Features
8
+
9
+ * Flexible API to accommodate various sources and destinations of MIDI data
10
+ * Simple OO approach to System Exclusive data and devices
11
+ * {YAML dictionary of MIDI constants}[https://github.com/arirusso/midi-message/blob/master/lib/midi.yml]
12
+
13
+ == Install
14
+
15
+ gem install midi-message
16
+
17
+ == Usage
18
+
19
+ require 'midi-message'
20
+
21
+ include MIDIMessage
22
+
23
+ ==== Basic Messages
24
+
25
+ There are a few ways to create a new MIDI message. Here are some examples
26
+
27
+ NoteOn.new(0, 64, 64)
28
+
29
+ NoteOn["E4"].new(0, 100)
30
+
31
+ with(:channel => 0, :velocity => 100) { note_on("E4") }
32
+
33
+ Those expressions all evaluate to the same object
34
+
35
+ #<MIDIMessage::NoteOn:0x9c1c240
36
+ @channel=0,
37
+ @data=[64, 64],
38
+ @name="E4",
39
+ @note=64,
40
+ @status=[9, 0],
41
+ @velocity=64,
42
+ @verbose_name="Note On: E4">
43
+
44
+ ==== SysEx Messages
45
+
46
+ As with any kind of message, you can begin with raw data
47
+
48
+ SystemExclusive.new(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
49
+
50
+ Or in a more object oriented way
51
+
52
+ synth = SystemExclusive::Node.new(0x41, :model_id => 0x42, :device_id => 0x10)
53
+
54
+ SystemExclusive::Command.new([0x40, 0x7F, 0x00], 0x00, :node => synth)
55
+
56
+ A Node represents a device that you're sending a message to (eg. your Yamaha DX7 is a Node). Sysex messages can either be a Command or Request
57
+
58
+ You can use the Node to instantiate a message
59
+
60
+ synth.command([0x40, 0x7F, 0x00], 0x00)
61
+
62
+ One way or another, you will wind up with a pair of objects like this
63
+
64
+ #<MIDIMessage::SystemExclusive::Command:0x9c1e57c
65
+ @address=[64, 0, 127],
66
+ @checksum=[65],
67
+ @data=[0],
68
+ @node=
69
+ #<MIDIMessage::SystemExclusive::Node:0x9c1e5a4
70
+ @device_id=16,
71
+ @manufacturer_id=65,
72
+ @model_id=66>>
73
+
74
+ ==== Parsing
75
+
76
+ The parse method will take any valid message data and return the object representation
77
+
78
+ MIDIMessage.parse(0x90, 0x40, 0x40)
79
+
80
+ #<MIDIMessage::NoteOn:0x9c1c240 ..>
81
+
82
+ MIDIMessage.parse(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
83
+
84
+ #<MIDIMessage::SystemExclusive::Command:0x9c1e57c ..>
85
+
86
+ Check out {nibbler}[http://github.com/arirusso/nibbler] for more advanced parsing
87
+
88
+ == API Documentation
89
+
90
+ * {rdoc}[http://rubydoc.info/github/arirusso/midi-message]
91
+
92
+ == Author
93
+
94
+ * {Ari Russo}[http://github.com/arirusso] <ari.russo at gmail.com>
95
+
96
+ == License
97
+
98
+ Apache 2.0, See the file LICENSE
99
+
100
+ Copyright (c) 2011 Ari Russo
data/TODO ADDED
File without changes
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ module MIDIMessage
4
+
5
+ # common behavior amongst Channel Message types
6
+ module ChannelMessage
7
+
8
+ attr_reader :data,
9
+ :name
10
+
11
+ def self.new(*a, &block)
12
+ RawChannelMessage.new(*a, &block)
13
+ end
14
+
15
+ def initialize(*a)
16
+ options = a.last.kind_of?(Hash) ? a.pop : {}
17
+ @const = options[:const]
18
+ unless @const.nil?
19
+ key = self.class.map_constants_to
20
+ ind = self.class.properties.index(key)
21
+ ind ||= 0
22
+ a.insert(ind, @const.value)
23
+ end
24
+ initialize_channel_message(self.class.type_for_status, *a)
25
+ end
26
+
27
+ def initialize_properties
28
+ props = [
29
+ { :name => :status, :index => 1 },
30
+ { :name => :data, :index => 0 },
31
+ { :name => :data, :index => 1 }
32
+ ]
33
+ properties = self.class.properties
34
+ unless properties.nil?
35
+ properties.each_with_index do |prop,i|
36
+ self.class.send(:attr_reader, prop)
37
+ self.class.send(:define_method, "#{prop}=") do |val|
38
+ send(:instance_variable_set, "@#{prop.to_s}", val)
39
+ send(props[i][:name])[props[i][:index]] = val
40
+ update
41
+ return self
42
+ end
43
+ instance_variable_set("@#{prop}", send(props[i][:name])[props[i][:index]])
44
+ end
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def self.included(base)
51
+ base.extend(ClassMethods)
52
+ end
53
+
54
+ private
55
+
56
+ def initialize_channel_message(status_nibble_1, status_nibble_2, data_byte_1, data_byte_2 = 0)
57
+ @status = [status_nibble_1, status_nibble_2]
58
+ @data = [data_byte_1]
59
+ @data[1] = data_byte_2 if self.class.second_data_byte?
60
+ initialize_properties
61
+ initialize_short_message(status_nibble_1, status_nibble_2)
62
+ end
63
+
64
+ module ClassMethods
65
+
66
+ attr_reader :properties
67
+
68
+ def type_for_status
69
+ @display_name.nil? ? nil : Status[@display_name]
70
+ end
71
+
72
+ def schema(*args)
73
+ @properties = args
74
+ end
75
+ alias_method :layout, :schema
76
+
77
+ def second_data_byte?
78
+ @properties.nil? || (@properties.length-1) > 1
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ # use this if you want to instantiate a raw channel message
86
+ #
87
+ # example = ChannelMessage.new(0x9, 0x0, 0x40, 0x57) # creates a raw note-on message
88
+ #
89
+ class RawChannelMessage
90
+
91
+ include ShortMessage
92
+ include ChannelMessage
93
+
94
+ use_display_name 'Channel Message'
95
+
96
+ def initialize(*a)
97
+ initialize_channel_message(*a)
98
+ end
99
+
100
+ def to_type
101
+ status = (@status[0] << 4) + (@status[1])
102
+ MIDIMessage.parse(status, *@data)
103
+ end
104
+
105
+ end
106
+
107
+ #
108
+ # MIDI Channel Aftertouch message
109
+ #
110
+ class ChannelAftertouch
111
+
112
+ include ShortMessage
113
+ include ChannelMessage
114
+
115
+ schema :channel, :value
116
+ use_display_name 'Channel Aftertouch'
117
+
118
+ end
119
+ ChannelPressure = ChannelAftertouch
120
+
121
+ #
122
+ # MIDI Control Change message
123
+ #
124
+ class ControlChange
125
+
126
+ include ShortMessage
127
+ include ChannelMessage
128
+
129
+ schema :channel, :index, :value
130
+ use_display_name 'Control Change'
131
+ use_constants 'Control Change', :for => :index
132
+
133
+ end
134
+ Controller = ControlChange #shortcut
135
+
136
+ #
137
+ # MIDI Pitch Bend message
138
+ #
139
+ class PitchBend
140
+
141
+ include ShortMessage
142
+ include ChannelMessage
143
+
144
+ schema :channel, :low, :high
145
+ use_display_name 'Pitch Bend'
146
+
147
+ end
148
+
149
+ #
150
+ # MIDI Polyphonic (note specific) Aftertouch message
151
+ #
152
+ class PolyphonicAftertouch
153
+
154
+ include ShortMessage
155
+ include ChannelMessage
156
+
157
+ schema :channel, :note, :value
158
+ use_display_name 'Polyphonic Aftertouch'
159
+ use_constants 'Note', :for => :note
160
+
161
+ end
162
+ PolyAftertouch = PolyphonicAftertouch
163
+ PolyPressure = PolyphonicAftertouch
164
+ PolyphonicPressure = PolyphonicAftertouch
165
+
166
+ #
167
+ # MIDI Program Change message
168
+ #
169
+ class ProgramChange
170
+
171
+ include ShortMessage
172
+ include ChannelMessage
173
+
174
+ schema :channel, :program
175
+ use_display_name 'Program Change'
176
+
177
+ end
178
+
179
+ end
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ module MIDIMessage
5
+
6
+ # MIDI Constants
7
+ class ConstantGroup
8
+
9
+ attr_reader :key, :value
10
+
11
+ def initialize(key, constants)
12
+ @key = key
13
+ @constants = constants.map { |k, v| Constant.new(k, v) }
14
+ end
15
+
16
+ def find(name)
17
+ @constants.find { |const| const.key.to_s.downcase.eql?(name.to_s.downcase) }
18
+ end
19
+ alias_method :[], :find
20
+
21
+ def find_by_value(value)
22
+ @constants.find { |const| const.value.to_s.downcase.eql?(value.to_s.downcase) }
23
+ end
24
+
25
+ def self.all
26
+ ensure_initialized
27
+ @groups
28
+ end
29
+
30
+ def self.[](key)
31
+ ensure_initialized
32
+ @groups.find { |g| g.key.to_s.downcase.eql?(key.to_s.downcase) }
33
+ end
34
+
35
+ private
36
+
37
+ # lazy initialize
38
+ def self.ensure_initialized
39
+ @dict ||= YAML.load_file(File.expand_path('../../midi.yml', __FILE__))
40
+ @groups ||= @dict.map { |k, v| new(k, v) }
41
+ end
42
+
43
+ end
44
+
45
+ class Constant
46
+
47
+ attr_reader :key, :value
48
+
49
+ def initialize(key, value)
50
+ @key = key
51
+ @value = value
52
+ end
53
+
54
+ def self.find(group_name, const_name)
55
+ group = ConstantGroup[group_name]
56
+ group.find(const_name)
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #
4
+
5
+ module MIDIMessage
6
+
7
+ class Context
8
+
9
+ attr_accessor :channel,
10
+ :velocity
11
+
12
+ def initialize(options = {})
13
+ @channel = options[:channel]
14
+ @velocity = options[:velocity]
15
+ end
16
+
17
+ def note_off(note, options = {})
18
+ channel = options[:channel] || @channel
19
+ velocity = options[:velocity] || @velocity
20
+ raise 'note_off requires both channel and velocity' if channel.nil? || velocity.nil?
21
+ if note.kind_of?(String)
22
+ NoteOff[note].new(channel, velocity, options)
23
+ else
24
+ NoteOff.new(channel, note, velocity, options)
25
+ end
26
+ end
27
+ alias_method :NoteOff, :note_off
28
+
29
+ def note_on(note, options = {})
30
+ channel = options[:channel] || @channel
31
+ velocity = options[:velocity] || @velocity
32
+ raise 'note_on requires both channel and velocity' if channel.nil? || velocity.nil?
33
+ if note.kind_of?(String)
34
+ NoteOn[note].new(channel, velocity, options)
35
+ else
36
+ NoteOn.new(channel, note, velocity, options)
37
+ end
38
+ end
39
+ alias_method :NoteOn, :note_on
40
+
41
+ def program_change(program, options = {})
42
+ channel = options[:channel] || @channel
43
+ raise 'program_change requires channel' if channel.nil?
44
+ if program.kind_of?(String)
45
+ ProgramChange[program].new(channel, options)
46
+ else
47
+ ProgramChange.new(channel, program, options)
48
+ end
49
+ end
50
+ alias_method :ProgramChange, :program_change
51
+
52
+ def control_change(index, value, options = {})
53
+ channel = options[:channel] || @channel
54
+ raise 'control_change requires channel' if channel.nil?
55
+ if index.kind_of?(String)
56
+ ControlChange[index].new(channel, value, options)
57
+ else
58
+ ControlChange.new(channel, index, value, options)
59
+ end
60
+ end
61
+ alias_method :ControlChange, :control_change
62
+ alias_method :Controller, :control_change
63
+ alias_method :controller, :control_change
64
+
65
+ def polyphonic_aftertouch(note, value, options = {})
66
+ channel = options[:channel] || @channel
67
+ raise 'channel_aftertouch requires a channel' if channel.nil?
68
+ if note.kind_of?(String)
69
+ PolyphonicAftertouch[note].new(channel, value, options)
70
+ else
71
+ PolyphonicAftertouch.new(channel, note, value, options)
72
+ end
73
+ end
74
+ alias_method :PolyphonicAftertouch, :polyphonic_aftertouch
75
+ alias_method :PolyAftertouch, :polyphonic_aftertouch
76
+ alias_method :PolyphonicPressure, :polyphonic_aftertouch
77
+ alias_method :PolyPressure, :polyphonic_aftertouch
78
+ alias_method :poly_aftertouch, :polyphonic_aftertouch
79
+ alias_method :poly_pressure, :polyphonic_aftertouch
80
+
81
+ def channel_aftertouch(value, options = {})
82
+ channel = options[:channel] || @channel
83
+ raise 'channel_aftertouch requires a channel' if channel.nil?
84
+ ChannelAftertouch.new(channel, value, options)
85
+ end
86
+ alias_method :ChannelAftertouch, :channel_aftertouch
87
+ alias_method :ChannelPressure, :channel_aftertouch
88
+ alias_method :channel_pressure, :channel_aftertouch
89
+
90
+ def pitch_bend(low, high, options = {})
91
+ channel = options[:channel] || @channel
92
+ raise 'channel_aftertouch requires a channel' if channel.nil?
93
+ PitchBend.new(channel, low, high, options)
94
+ end
95
+ alias_method :PitchBend, :pitch_bend
96
+
97
+ end
98
+
99
+ def with_context(options = {}, &block)
100
+ Context.new(options, &block).instance_eval(&block)
101
+ end
102
+ alias_method :with, :with_context
103
+
104
+ end
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ module MIDIMessage
4
+
5
+ #
6
+ # Common Note Message Behavior
7
+ #
8
+ module NoteMessage
9
+
10
+ # the octave number of the note
11
+ def octave
12
+ (note / 12) -1
13
+ end
14
+ alias_method :oct, :octave
15
+
16
+ # set the octave number of the note
17
+ def octave=(val)
18
+ self.note = ((val + 1) * 12) + abs_note
19
+ self
20
+ end
21
+ alias_method :oct=, :octave=
22
+
23
+ # how many half-steps is this note above the closest C
24
+ def abs_note
25
+ note - ((note / 12) * 12)
26
+ end
27
+
28
+ # the name of the note without its octave e.g. F#
29
+ def note_name
30
+ name.split(/-?\d\z/).first
31
+ end
32
+
33
+ end
34
+
35
+ #
36
+ # MIDI Note-Off message
37
+ #
38
+ class NoteOff
39
+
40
+ include NoteMessage
41
+ include ShortMessage
42
+ include ChannelMessage
43
+
44
+ schema :channel, :note, :velocity
45
+ use_display_name 'Note Off'
46
+ use_constants 'Note', :for => :note
47
+
48
+ end
49
+
50
+ #
51
+ # MIDI Note-On message
52
+ #
53
+ class NoteOn
54
+
55
+ include NoteMessage
56
+ include ShortMessage
57
+ include ChannelMessage
58
+
59
+ schema :channel, :note, :velocity
60
+ use_display_name 'Note On'
61
+ use_constants 'Note', :for => :note
62
+
63
+ # returns the NoteOff equivalent of this object
64
+ def to_note_off
65
+ NoteOff.new(channel, note, velocity)
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ module MIDIMessage
5
+
6
+ # very simple parsing
7
+ # for more advanced parsing check out {nibbler}[http://github.com/arirusso/nibbler]
8
+ class Parser
9
+
10
+ # can take either a hex string eg Parser.new("904040")
11
+ # or bytes eg Parser.new(0x90, 0x40, 0x40)
12
+ # or an array of bytes eg Parser.new([0x90, 0x40, 0x40])
13
+ def initialize(*a)
14
+ @data = case a.first
15
+ when Array then a.first
16
+ when Numeric then a
17
+ when String then TypeConversion.hex_string_to_numeric_byte_array(a.first)
18
+ end
19
+ end
20
+
21
+ def parse
22
+ first_nibble = ((@data.first & 0xF0) >> 4)
23
+ second_nibble = (@data.first & 0x0F)
24
+ case first_nibble
25
+ when 0x8 then NoteOff.new(second_nibble, @data[1], @data[2])
26
+ when 0x9 then NoteOn.new(second_nibble, @data[1], @data[2])
27
+ when 0xA then PolyphonicAftertouch.new(second_nibble, @data[1], @data[2])
28
+ when 0xB then ControlChange.new(second_nibble, @data[1], @data[2])
29
+ when 0xC then ProgramChange.new(second_nibble, @data[1])
30
+ when 0xD then ChannelAftertouch.new(second_nibble, @data[1])
31
+ when 0xE then PitchBend.new(second_nibble, @data[1], @data[2])
32
+ when 0xF then case second_nibble
33
+ when 0x0 then SystemExclusive.new(*@data)
34
+ when 0x1..0x6 then SystemCommon.new(second_nibble, @data[1], @data[2])
35
+ when 0x8..0xF then SystemRealtime.new(second_nibble)
36
+ else nil
37
+ end
38
+ else nil
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ def self.parse(*a)
45
+ Parser.new(*a).parse
46
+ end
47
+
48
+ end
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module MIDIMessage
4
+
5
+ module Process
6
+
7
+ # Use the Filter superclass when you need a multi-band filter
8
+ class Filter
9
+
10
+ include Processor
11
+
12
+ attr_reader :bandwidth, :property, :reject
13
+
14
+ def initialize(message, prop, bandwidth, options = {})
15
+ @bandwidth = [bandwidth].flatten
16
+ @message = message
17
+ @property = prop
18
+ @reject = options[:reject] || false
19
+ initialize_processor(message)
20
+ end
21
+
22
+ def process
23
+ val = @message.send(@property)
24
+ result = @bandwidth.map { |bw| val >= bw.min && val <= bw.max ? @message : nil }
25
+ result.include?(@message) ^ @reject ? @message : nil
26
+ end
27
+
28
+ end
29
+
30
+ class LowPassFilter < Filter
31
+ def initialize(message, prop, max, options = {})
32
+ super(message, prop, (0..max), options)
33
+ end
34
+ end
35
+
36
+ class HighPassFilter < Filter
37
+ def initialize(message, prop, min, options = {})
38
+ super(message, prop, (min..127), options)
39
+ end
40
+ end
41
+
42
+ class BandPassFilter < Filter
43
+ def initialize(message, prop, accept_range, options = {})
44
+ options[:reject] = false
45
+ super(message, prop, accept_range, options)
46
+ end
47
+ end
48
+
49
+ class BandRejectFilter < Filter
50
+ def initialize(message, prop, reject_range, options = {})
51
+ options[:reject] = true
52
+ super(message, prop, reject_range, options)
53
+ end
54
+ end
55
+ NotchFilter = BandRejectFilter
56
+
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module MIDIMessage
4
+
5
+ module Process
6
+
7
+ class Limit
8
+
9
+ include Processor
10
+
11
+ attr_reader :property, :range
12
+
13
+ def initialize(message, prop, range, options = {})
14
+ @range = range
15
+ @message = message
16
+ @property = prop
17
+ initialize_processor(message)
18
+ end
19
+
20
+ def process
21
+ val = @message.send(@property)
22
+ @message.send("#{@property}=", @range.min) if val < @range.min
23
+ @message.send("#{@property}=", @range.max) if val > @range.max
24
+ @message
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module MIDIMessage
4
+
5
+ module Process
6
+
7
+ module Processor
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ base.send(:attr_reader, :message)
12
+ end
13
+
14
+ module ClassMethods
15
+ def process(*a, &block)
16
+ new(*a).process(&block)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def initialize_processor(message)
23
+ @message = message
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ end