midi-message 0.2.2

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/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