midi-events 0.5.0

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.
@@ -0,0 +1,13 @@
1
+ Copyright 2011-2015 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.md ADDED
@@ -0,0 +1,170 @@
1
+ # MIDI Events
2
+
3
+ **Ruby MIDI Events Objects**
4
+
5
+ This library is part of a suite of Ruby libraries for MIDI:
6
+
7
+ | Function | Library |
8
+ | --- | --- |
9
+ | MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) |
10
+ | MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) |
11
+ | MIDI communication with Instruments and Control Surfaces | [MIDI Communications](https://github.com/javier-sy/midi-communications) |
12
+ | Low level MIDI interface to MacOS | [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) |
13
+ | Low level MIDI interface to Linux | **TO DO** (by now [MIDI Communications](https://github.com/javier-sy/midi-communications) uses [alsa-rawmidi](http://github.com/arirusso/alsa-rawmidi)) |
14
+ | Low level MIDI interface to JRuby | **TO DO** (by now [MIDI Communications](https://github.com/javier-sy/midi-communications) uses [midi-jruby](http://github.com/arirusso/midi-jruby))|
15
+ | Low level MIDI interface to Windows | **TO DO** (by now [MIDI Communications](https://github.com/javier-sy/midi-communications) uses [midi-winm](http://github.com/arirusso/midi-winmm)) |
16
+
17
+ This library is based on [Ari Russo's](http://github.com/arirusso) library [MIDI Message](https://github.com/arirusso/midi-message).
18
+
19
+ ## Features
20
+
21
+ * Flexible API to accommodate various sources and destinations of MIDI data
22
+ * Simple approach to System Exclusive data and devices
23
+ * [YAML dictionary of MIDI constants](https://github.com/javier-sy/midi-events/blob/master/lib/midi.yml)
24
+
25
+ ## Install
26
+
27
+ `gem install midi-events`
28
+
29
+ Or if you're using Bundler, add this to your Gemfile
30
+
31
+ `gem "midi-events"`
32
+
33
+ ## Usage
34
+
35
+ ```ruby
36
+ require "midi-events"
37
+ ```
38
+
39
+ #### Basic Messages
40
+
41
+ There are a few ways to create a new MIDI event. Here are some examples:
42
+
43
+ ```ruby
44
+ MIDIEvents::NoteOn.new(0, 64, 64)
45
+
46
+ MIDIEvents::NoteOn["E4"].new(0, 100)
47
+
48
+ MIDIEvents.with(:channel => 0, :velocity => 100) { note_on("E4") }
49
+ ```
50
+
51
+ Those expressions all evaluate to the same object:
52
+
53
+ ```ruby
54
+ #<MIDIEvents::NoteOn:0x9c1c240
55
+ @channel=0,
56
+ @data=[64, 64],
57
+ @name="E4",
58
+ @note=64,
59
+ @status=[9, 0],
60
+ @velocity=64,
61
+ @verbose_name="Note On: E4">
62
+ ```
63
+
64
+ #### SysEx Messages
65
+
66
+ As with any kind of message, you can begin with raw data:
67
+
68
+ ```ruby
69
+ MIDIEvents::SystemExclusive.new(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
70
+ ```
71
+
72
+ Or in a more object oriented way:
73
+
74
+ ```ruby
75
+ synth = SystemExclusive::Node.new(0x41, model_id: 0x42, device_id: 0x10)
76
+
77
+ SystemExclusive::Command.new([0x40, 0x7F, 0x00], 0x00, node: synth)
78
+ ```
79
+
80
+ 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.
81
+
82
+ You can use the Node to instantiate a message:
83
+
84
+ ```ruby
85
+ synth.command([0x40, 0x7F, 0x00], 0x00)
86
+ ```
87
+
88
+ One way or another, you will wind up with a pair of objects like this:
89
+
90
+ ```ruby
91
+ #<MIDIEvents::SystemExclusive::Command:0x9c1e57c
92
+ @address=[64, 0, 127],
93
+ @checksum=[65],
94
+ @data=[0],
95
+ @node=
96
+ #<MIDIMessage::SystemExclusive::Node:0x9c1e5a4
97
+ @device_id=16,
98
+ @manufacturer_id=65,
99
+ @model_id=66>>
100
+ ```
101
+
102
+ ## Documentation
103
+
104
+ * (**TO DO**) [rdoc](http://rubydoc.info/github/javier-sy/midi-events)
105
+
106
+ ## Differences between [MIDI Events](https://github.com/javier-sy/midi-events) library and [MIDI Message](https://github.com/arirusso/midi-message) library
107
+
108
+ [MIDI Events](https://github.com/javier-sy/midi-events) is mostly a clone of [MIDI Message](https://github.com/arirusso/midi-message) with some modifications:
109
+ * Renamed gem to midi-events instead of midi-message
110
+ * Renamed module to MIDIEvents instead of MIDIMessage
111
+ * Removed parsing features (in favour of the more complete parser [MIDI Parser](https://github.com/javier-sy/midi-parser))
112
+ * TODO: update tests to use rspec instead of rake
113
+ * TODO: migrate to (or confirm it's working ok on) Ruby 3.0 and Ruby 3.1
114
+
115
+ ## Then, why does exist this library if it is mostly a clone of another library?
116
+
117
+ The author has been developing since 2016 a Ruby project called
118
+ [Musa DSL](https://github.com/javier-sy/musa-dsl) that needs a way
119
+ of representing MIDI Events and a way of communicating with
120
+ MIDI Instruments and MIDI Control Surfaces.
121
+
122
+ [Ari Russo](https://github.com/arirusso) has done a great job creating
123
+ several interdependent Ruby libraries that allow
124
+ MIDI Events representation ([MIDI Message](https://github.com/arirusso/midi-message)
125
+ and [Nibbler](https://github.com/arirusso/nibbler))
126
+ and communication with MIDI Instruments and MIDI Control Surfaces
127
+ ([unimidi](https://github.com/arirusso/unimidi),
128
+ [ffi-coremidi](https://github.com/arirusso/ffi-coremidi) and others)
129
+ that, **with some modifications**, I've been using in MusaDSL.
130
+
131
+ After thinking about the best approach to publish MusaDSL
132
+ I've decided to publish my own renamed version of the modified dependencies because:
133
+
134
+ * Some differences on the approach of the modifications vs the original library doesn't allow to merge the modifications on the original libraries.
135
+ * Then the renaming of the libraries is needed to avoid confusing existent users of the original libraries.
136
+ * Due to some of the interdependencies of Ari Russo libraries,
137
+ the modification and renaming on some of the low level libraries (ffi-coremidi, etc.)
138
+ forces to modify and rename unimidi library.
139
+ * The original libraries have features
140
+ (very detailed logging and processing history information, not locking behaviour when waiting input midi messages)
141
+ that are not needed in MusaDSL and, in fact,
142
+ can degrade the performance on some use case scenarios in MusaDSL.
143
+
144
+ All in all I have decided to publish a suite of libraries optimized for MusaDSL use case that also can be used by other people in their projects.
145
+
146
+ | Function | Library | Based on Ari Russo's| Difference |
147
+ | --- | --- | --- | --- |
148
+ | MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) | [MIDI Message](https://github.com/arirusso/midi-message) | removed parsing, small improvements |
149
+ | MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) | [Nibbler](https://github.com/arirusso/nibbler) | removed process history information, minor optimizations |
150
+ | MIDI communication with Instruments and Control Surfaces | [MIDI Communications](https://github.com/javier-sy/midi-communications) | [unimidi](https://github.com/arirusso/unimidi) | use of [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos)
151
+ | Low level MIDI interface to MacOS | [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) | [ffi-coremidi](https://github.com/arirusso/ffi-coremidi) | removed process history information, locking behaviour when waiting midi events, improved midi devices name detection, minor optimizations |
152
+ | Low level MIDI interface to Linux | **TO DO** | | |
153
+ | Low level MIDI interface to JRuby | **TO DO** | | |
154
+ | Low level MIDI interface to Windows | **TO DO** | | |
155
+
156
+ ## Author
157
+
158
+ * [Javier Sánchez Yeste](https://github.com/javier-sy)
159
+
160
+ ## Acknowledgements
161
+
162
+ Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [MIDI Message](https://github.com/arirusso/midi-message) licensed as Apache License 2.0.
163
+
164
+ ## License
165
+
166
+ [MIDI Events](https://github.com/javier-sy/midi-events) Copyright (c) 2021 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
167
+
168
+ [MIDI Message](https://github.com/arirusso/midi-message) Copyright (c) 2011-2015 [Ari Russo](http://arirusso.com), licensed under Apache License 2.0 (see the file LICENSE.midi-message)
169
+
170
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: [:test]
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # How to use constants
4
+ #
5
+
6
+ $:.unshift(File.join("..", "lib"))
7
+
8
+ require "midi-events"
9
+
10
+ # some messages for a sequencer
11
+
12
+ pp MIDIEvents::SystemRealtime["Start"].new
13
+ pp MIDIEvents::NoteOn["E4"].new(0, 100)
14
+ pp MIDIEvents::SystemRealtime["Stop"].new
15
+
16
+ # this should output something like:
17
+
18
+ #
19
+ # #<MIDIMessage::SystemRealtime:0x89fda3c
20
+ # @name="Start",
21
+ # @status=[15, 250],
22
+ # @verbose_name="System Realtime: Start">
23
+ #
24
+ # #<MIDIMessage::NoteOn:0x9363cac
25
+ # @channel=0,
26
+ # @data=[64, 100],
27
+ # @name="C3",
28
+ # @note=64,
29
+ # @status=[9, 0],
30
+ # @velocity=100,
31
+ # @verbose_name="Note On: C3">
32
+ #
33
+ # #<MIDIMessage::SystemRealtime:0x89fc600
34
+ # @name="Stop",
35
+ # @status=[15, 252],
36
+ # @verbose_name="System Realtime: Stop">
37
+ #
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Use a block loaded with velocity and channel
4
+ #
5
+
6
+ $:.unshift(File.join("..", "lib"))
7
+
8
+ require "midi-events"
9
+
10
+ MIDIEvents.with(:channel => 0, :velocity => 100) do
11
+
12
+ note_on("E4")
13
+ note_off("E4")
14
+
15
+ note_on("C4")
16
+ note_off("C4")
17
+
18
+ control_change("Portamento", 64)
19
+
20
+ note_on("E4")
21
+ pp note_off("E4")
22
+
23
+ pp program_change(20)
24
+
25
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Construct a melody
4
+ #
5
+
6
+ $:.unshift(File.join("..", "lib"))
7
+
8
+ require "midi-events"
9
+
10
+ channel = 0
11
+ notes = [36, 40, 43] # C E G
12
+ octaves = 2
13
+ velocity = 100
14
+
15
+ melody = []
16
+
17
+ (0..((octaves-1)*12)).step(12) do |oct|
18
+
19
+ notes.each { |note| melody << MIDIEvents::NoteOn.new(channel, note + oct, velocity) }
20
+
21
+ end
22
+
23
+ pp melody
24
+
25
+ # this should output something like:
26
+
27
+ # (will add when I have the constants yaml more filled out)
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Walk through of different ways to instantiate short (non-sysex) MIDI Messages
4
+ #
5
+
6
+ $:.unshift(File.join("..", "lib"))
7
+
8
+ require "midi-events"
9
+
10
+ # Here are examples of different ways to construct messages, going from low to high-level
11
+
12
+ pp MIDIEvents.parse(0x90, 0x40, 0x40)
13
+
14
+ channel_msg = MIDIEvents::ChannelMessage.new(0x9, 0x0, 0x40, 0x40)
15
+
16
+ pp channel_msg
17
+
18
+ # this will return a NoteOn object with the properties of channel_msg
19
+ pp channel_msg.to_type
20
+
21
+ pp MIDIEvents::ChannelMessage.new(MIDIEvents::Constant::Status["Note On"], 0x0, 0x40, 0x40)
22
+
23
+ pp MIDIEvents::ChannelMessage.new(MIDIEvents::Constant::Status["Note On"], 0x0, 0x40, 0x40).to_type
24
+
25
+ pp MIDIEvents::NoteOn.new(0, 64, 64) # or NoteOn.new(0x0, 0x64, 0x64)
26
+
27
+ # some message properties are mutable
28
+
29
+ pp msg = MIDIEvents::NoteOn["E4"].new(0, 100)
30
+
31
+ msg.note += 5
32
+
33
+ pp msg
data/examples/sysex.rb ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Walk through of different ways to instantiate System Exclusive (SysEx) messages
4
+ #
5
+
6
+ $:.unshift(File.join("..", "lib"))
7
+
8
+ require "midi-events"
9
+
10
+ # you can create a message by parsing bytes
11
+
12
+ pp MIDIEvents.parse(0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7)
13
+
14
+ # or create a Node (destination) object and then send messages to that.
15
+ # a Node represents a device that you"re sending a message to
16
+ # (eg. your Yamaha DX7 is a Node).
17
+
18
+ node = MIDIEvents::SystemExclusive::Node.new(0x41, model_id: 0x42, device_id: 0x10)
19
+
20
+ # The following will create a command object for address "407F00" with value "00"
21
+ # associated with our node
22
+ #
23
+ # A command is a sysex message where the status (byte index 4) is 0x12
24
+ #
25
+ # A Request type message (SystemExclusive::Request) has a status byte
26
+ # equal to 0x11
27
+
28
+ pp MIDIEvents::SystemExclusive::Command.new([0x40, 0x7F, 0x00], 0x00, node: node)
29
+
30
+ # it is actually optional to pass a node to your message-- one case where not
31
+ # doing so is useful is when want to have a generic message prototype used with
32
+ # multiple nodes
33
+
34
+ prototype = MIDIEvents::SystemExclusive::Command.new([0x40, 0x7F, 0x00], 0x00)
35
+
36
+ pp node.new_message_from(prototype) # this will create a new message using the prototype"s data and the node"s information
37
+
38
+ # you can also generate a totally new message from the Node
39
+
40
+ pp node.command([0x40, 0x7F, 0x00], 0x00)
41
+
42
+ # each of the print statements in this example should output a message something like:
43
+
44
+ #
45
+ # #<MIDIMessage::SystemExclusive::Command:0x9c1e57c
46
+ # @address=[64, 0, 127],
47
+ # @checksum=[65],
48
+ # @data=[0],
49
+ # @node=
50
+ # #<MIDIMessage::SystemExclusive::Node:0x9c1e5a4
51
+ # @device_id=16,
52
+ # @manufacturer=65,
53
+ # @model_id=66>>
54
+ #
55
+
56
+ # read more about SysEx messages in general here: http://www.2writers.com/eddie/TutSysEx.htm
@@ -0,0 +1,152 @@
1
+ module MIDIEvents
2
+ # Common behavior amongst Channel Message types
3
+ module ChannelMessage
4
+ include MIDIEvents # this enables ..kind_of?(MIDIEvents)
5
+
6
+ attr_reader :data, :name
7
+
8
+ # Shortcut to RawChannelMessage.new
9
+ # aka build a ChannelMessage from raw nibbles and bytes
10
+ # eg ChannelMessage.new(0x9, 0x0, 0x40, 0x40)
11
+ # @param [*Fixnum] data The status nibbles and data bytes
12
+ # @return [RawChannelMessage] The resulting RawChannelMessage object
13
+ def self.new(*data, &block)
14
+ Message.new(*data, &block)
15
+ end
16
+
17
+ # @param [*Fixnum] data The status nibbles and data bytes
18
+ def initialize(*data)
19
+ data = data.dup
20
+ options = data.last.is_a?(Hash) ? data.pop : {}
21
+ add_constant_value(options[:const], data) unless options[:const].nil?
22
+ initialize_channel_message(self.class.type_for_status, *data)
23
+ end
24
+
25
+ private
26
+
27
+ def self.included(base)
28
+ base.send(:include, ::MIDIEvents::Message)
29
+ base.send(:extend, ClassMethods)
30
+ end
31
+
32
+ # Add the given constant to message data
33
+ def add_constant_value(constant, data)
34
+ index = Constant::Loader.get_index(self)
35
+ data.insert(index, constant.value)
36
+ end
37
+
38
+ # Assign the message data
39
+ def assign_data(status_nibble_1, status_nibble_2, data_byte_1, data_byte_2 = 0)
40
+ @status = [status_nibble_1, status_nibble_2]
41
+ @data = [data_byte_1]
42
+ @data[1] = data_byte_2 if self.class.second_data_byte?
43
+ end
44
+
45
+ # Initialize the message: assign data, decorate with accessors
46
+ def initialize_channel_message(status_nibble_1, status_nibble_2, data_byte_1, data_byte_2 = 0)
47
+ assign_data(status_nibble_1, status_nibble_2, data_byte_1, data_byte_2)
48
+ Accessors.initialize(self) unless self.class.properties.nil?
49
+ initialize_message(status_nibble_1, status_nibble_2)
50
+ end
51
+
52
+ class Accessors
53
+ SCHEMA = [
54
+ { name: :status, index: 1 }, # second status nibble
55
+ { name: :data, index: 0 }, # first data byte
56
+ { name: :data, index: 1 } # second data byte
57
+ ].freeze
58
+
59
+ # @param [Class] klass
60
+ # @return [Class]
61
+ def self.decorate(klass)
62
+ decorator = new(klass)
63
+ decorator.decorate
64
+ end
65
+
66
+ # Initialize a message object with it's properties
67
+ # @param [MIDIEvents] message
68
+ # @return [Boolean]
69
+ def self.initialize(message)
70
+ message.class.properties.each_with_index do |property, i|
71
+ data_mapping = SCHEMA[i]
72
+ container = message.send(data_mapping[:name])
73
+ index = data_mapping[:index]
74
+ message.send(:instance_variable_set, "@#{property.to_s}", container[index])
75
+ end
76
+ true
77
+ end
78
+
79
+ # @param [Class] klass
80
+ def initialize(klass)
81
+ @klass = klass
82
+ end
83
+
84
+ # @return [Class]
85
+ def decorate
86
+ @klass.properties.each_with_index do |property, i|
87
+ data_mapping = SCHEMA[i]
88
+ define_getter(property)
89
+ define_setter(property, data_mapping)
90
+ end
91
+ @klass
92
+ end
93
+
94
+ private
95
+
96
+ # @param [Symbol, String] property
97
+ # @return [Boolean]
98
+ def define_getter(property)
99
+ @klass.send(:attr_reader, property)
100
+ true
101
+ end
102
+
103
+ # @param [Symbol, String] property
104
+ # @param [Hash] mapping
105
+ # @return [Boolean]
106
+ def define_setter(property, mapping)
107
+ index = mapping[:index]
108
+ @klass.send(:define_method, "#{property.to_s}=") do |value|
109
+ send(:instance_variable_set, "@#{property.to_s}", value)
110
+ send(mapping[:name])[index] = value
111
+ send(:update)
112
+ return self
113
+ end
114
+ true
115
+ end
116
+
117
+ end
118
+
119
+ # For defining Channel Message class types
120
+ module ClassMethods
121
+ def properties
122
+ const_get('DATA') if const_defined?('DATA')
123
+ end
124
+
125
+ # Does the schema of this Channel Message carry a second data byte?
126
+ # eg. NoteMessage does, and ProgramChange doesn"t
127
+ # @return [Boolean] Is there a second data byte on this message type?
128
+ def second_data_byte?
129
+ properties.nil? || (properties.length - 1) > 1
130
+ end
131
+
132
+ end
133
+
134
+ # Use this if you want to instantiate a raw channel message
135
+ #
136
+ # For example ChannelMessage::Message.new(0x9, 0x0, 0x40, 0x57)
137
+ # creates a raw note-on message
138
+ class Message
139
+ include ChannelMessage
140
+
141
+ DISPLAY_NAME = 'Channel Message'.freeze
142
+
143
+ # Build a Channel Message from raw nibbles and bytes
144
+ # eg ChannelMessage.new(0x9, 0x0, 0x40, 0x40)
145
+ # @param [*Fixnum] data The status nibbles and data bytes
146
+ # @return [RawChannelMessage] The resulting RawChannelMessage object
147
+ def initialize(*data)
148
+ initialize_channel_message(*data)
149
+ end
150
+ end
151
+ end
152
+ end