midi-communications 0.5.1

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.unimidi ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2010-2017 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,149 @@
1
+ # MIDI Communications
2
+
3
+ **Platform independent realtime MIDI input and output for Ruby.**
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 [UniMIDI](https://github.com/arirusso/unimidi).
18
+
19
+ ### Features
20
+
21
+ * Supports OSX, Linux, JRuby, Windows and Cygwin
22
+ * No compilation required
23
+ * Both input and output to and from multiple devices concurrently
24
+ * Generalized handling of different MIDI and SysEx Message types
25
+ * On OSX use IAC to internally route MIDI to other programs
26
+ * No events history and no buffers optimization
27
+
28
+ ### Requirements
29
+
30
+ **MIDI Communications** uses one of the following libraries, depending on which platform you're using it on. The necessary library should install automatically with the midi-communications gem.
31
+
32
+ Platform
33
+
34
+ * OSX: [midi-communications-macos](http://github.com/javier-sy/midi-communications-macos)
35
+ * JRuby: [midi-jruby](http://github.com/arirusso/midi-jruby) (**TODO: update to midi-communications-jruby**)
36
+ * Linux: [alsa-rawmidi](http://github.com/arirusso/alsa-rawmidi) (**TODO: update to midi-communications-linux**)
37
+ * Windows/Cygwin: [midi-winmm](http://github.com/arirusso/midi-winmm) (**TODO: update to midi-communications-windows**)
38
+
39
+ ### Install
40
+
41
+ If you're using Bundler, add this line to your application's Gemfile:
42
+
43
+ `gem "midi-communications"`
44
+
45
+ Otherwise...
46
+
47
+ `gem install midi-communications`
48
+
49
+ ### Usage
50
+
51
+ Some examples are included with the library:
52
+
53
+ * [Selecting a device](http://github.com/arirusso/javier-sy/midi-communications/blob/master/examples/select_a_device.rb)
54
+ * [MIDI input](http://github.com/javier-sy/midi-communications/blob/master/examples/input.rb)
55
+ * [MIDI output](http://github.com/javier-sy/midi-communications/blob/master/examples/output.rb)
56
+ * [MIDI Sysex output](http://github.com/javier-sy/midi-communications/blob/master/examples/sysex_output.rb)
57
+
58
+ ### Tests
59
+
60
+ **MIDI Communications** includes a set of tests which assume that an output is connected to an input. You will be asked to select which input and output as the test is run.
61
+
62
+ The tests can be run using:
63
+
64
+ `rake test`
65
+
66
+ See below for additional notes on testing with JRuby.
67
+
68
+ ### Documentation
69
+
70
+ [rdoc](http://rdoc.info/gems/midi-communications) (**TODO**)
71
+
72
+ ### Platform Specific Notes
73
+
74
+ ##### JRuby
75
+
76
+ * (**TO CONFIRM**) You must be in 1.9 mode. This is normally accomplished by passing --1.9 to JRuby at the command line. For testing in 1.9 mode, use `jruby --1.9 -S rake test`
77
+ * (**TO CONFIRM**) javax.sound has some documented issues with SysEx messages in some versions OSX Snow Leopard which do affect this library.
78
+
79
+ ##### Linux
80
+
81
+ * (**TO CONFIRM**) *libasound* and *libasound-dev* packages are required
82
+
83
+ ## Differences between [MIDI Communications](https://github.com/javier-sy/midi-communications) library and [UniMIDI](https://github.com/arirusso/unimidi) library
84
+
85
+ [MIDI Communications](https://github.com/javier-sy/midi-communications) is mostly a clone of [UniMIDI](https://github.com/arirusso/unimidi) with some modifications:
86
+ * Uses [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) instead of [ffi-coremidi](https://github.com/arirusso/ffi-coremidi)
87
+ * Removed buffering (to reduce CPU usage in some scenarios)
88
+ * Source updated to Ruby 2.7 code conventions (method keyword parameters instead of options = {}, hash keys as 'key:' instead of ':key =>', etc.)
89
+ * Updated dependencies versions
90
+ * Renamed module to MIDICommunications instead of UniMIDI
91
+ * Renamed gem to midi-communications instead of unimidi
92
+ * TODO: update tests to use rspec instead of rake
93
+ * TODO: migrate to (or confirm it's working ok on) Ruby 3.0 and Ruby 3.1
94
+
95
+ ## Then, why does exist this library if it is mostly a clone of another library?
96
+
97
+ The author has been developing since 2016 a Ruby project called
98
+ [Musa DSL](https://github.com/javier-sy/musa-dsl) that needs a way
99
+ of representing MIDI Events and a way of communicating with
100
+ MIDI Instruments and MIDI Control Surfaces.
101
+
102
+ [Ari Russo](https://github.com/arirusso) has done a great job creating
103
+ several interdependent Ruby libraries that allow
104
+ MIDI Events representation ([MIDI Message](https://github.com/arirusso/midi-message)
105
+ and [Nibbler](https://github.com/arirusso/nibbler))
106
+ and communication with MIDI Instruments and MIDI Control Surfaces
107
+ ([unimidi](https://github.com/arirusso/unimidi),
108
+ [ffi-coremidi](https://github.com/arirusso/ffi-coremidi) and others)
109
+ that, **with some modifications**, I've been using in MusaDSL.
110
+
111
+ After thinking about the best approach to publish MusaDSL
112
+ I've decided to publish my own renamed versions of the modified dependencies because:
113
+
114
+ * The original libraries have features
115
+ (buffering, very detailed logging and processing history information, not locking behaviour when waiting input midi messages)
116
+ that are not needed in MusaDSL and, in fact,
117
+ can degrade the performance on some use cases in MusaDSL.
118
+ * The requirements for **Musa DSL** users probably will evolve in time, so it will be easier to maintain an independent source code base.
119
+ * Some differences on the approach of the modifications vs the original library doesn't allow to merge the modifications on the original libraries.
120
+ * Then the renaming of the libraries is needed to avoid confusing existent users of the original libraries.
121
+ * Due to some of the interdependencies of Ari Russo libraries,
122
+ the modification and renaming on some of the low level libraries (ffi-coremidi, etc.)
123
+ forces to modify and rename unimidi library.
124
+
125
+ 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.
126
+
127
+ | Function | Library | Based on Ari Russo's| Difference |
128
+ | --- | --- | --- | --- |
129
+ | MIDI Events representation | [MIDI Events](https://github.com/javier-sy/midi-events) | [MIDI Message](https://github.com/arirusso/midi-message) | removed parsing, small improvements |
130
+ | MIDI Data parsing | [MIDI Parser](https://github.com/javier-sy/midi-parser) | [Nibbler](https://github.com/arirusso/nibbler) | removed process history information, minor optimizations |
131
+ | 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, removed process history information, removed buffering, removed command line script)
132
+ | 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 buffering and process history information, locking behaviour when waiting midi events, improved midi devices name detection, minor optimizations |
133
+ | Low level MIDI interface to Linux | **TO DO** | | |
134
+ | Low level MIDI interface to JRuby | **TO DO** | | |
135
+ | Low level MIDI interface to Windows | **TO DO** | | |
136
+
137
+ ## Author
138
+
139
+ * [Javier Sánchez Yeste](https://github.com/javier-sy)
140
+
141
+ ## Acknowledgements
142
+
143
+ Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [unimidi](https://github.com/arirusso/unimidi) licensed under Apache License 2.0.
144
+
145
+ ### License
146
+
147
+ [MIDI Communications](https://github.com/javier-sy/midi-communications) Copyright (c) 2021 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
148
+
149
+ [unimidi](https://github.com/arirusso/unimidi) Copyright (c) 2010-2017 [Ari Russo](http://arirusso.com), licensed under Apache License 2.0 (see the file LICENSE.unimidi)
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ $LOAD_PATH.prepend __dir__
2
+ $LOAD_PATH.prepend File.join(__dir__, 'lib')
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'midi-communications'
7
+
8
+ namespace(:test) do
9
+ task all: [:unit, :integration]
10
+
11
+ Rake::TestTask.new(:integration) do |t|
12
+ t.libs << 'test'
13
+ t.test_files = FileList['test/integration/**/*_test.rb']
14
+ t.verbose = true
15
+ end
16
+
17
+ Rake::TestTask.new(:unit) do |t|
18
+ t.libs << 'test'
19
+ t.test_files = FileList['test/unit/**/*_test.rb']
20
+ t.verbose = true
21
+ end
22
+ end
23
+
24
+ Rake::Task['test'].enhance ['test:all']
25
+
26
+ platforms = ['generic', 'x86_64-darwin10.7.0', 'i386-mingw32', 'java', 'i686-linux']
27
+
28
+ task(:build) do
29
+ require 'midi-communications-gemspec'
30
+ platforms.each do |platform|
31
+ MIDICommunications::Gemspec.new(platform)
32
+ filename = "midi-communications-#{platform}.gemspec"
33
+ system "gem build #{filename}"
34
+ system "rm #{filename}"
35
+ end
36
+ end
37
+
38
+ task(release: :build) do
39
+ platforms.each do |platform|
40
+ system "gem push midi-communications-#{MIDICommunications::VERSION}-#{platform}.gem"
41
+ end
42
+ end
43
+
44
+ task(default: [:test])
data/examples/input.rb ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.prepend(File.expand_path('../lib', __dir__))
4
+
5
+ require 'midi-communications'
6
+
7
+ # Prompts the user to select a midi input
8
+ # Sends an inspection of the first 10 messages messages that input receives to standard out
9
+
10
+ num_messages = 10
11
+
12
+ # Prompt the user
13
+ input = MIDICommunications::Input.gets
14
+
15
+ # using their selection...
16
+
17
+ puts 'send some MIDI to your input now...'
18
+
19
+ num_messages.times do
20
+ m = input.gets
21
+ puts(m)
22
+ end
23
+
24
+ puts 'finished'
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.prepend(File.expand_path('../lib', __dir__))
4
+
5
+ require 'midi-communications'
6
+
7
+ # Prompts the user to select a midi output
8
+ # Sends some arpeggiated chords to the output
9
+
10
+ notes = [36, 40, 43] # C E G
11
+ octaves = 5
12
+ duration = 0.1
13
+
14
+ # Prompt the user to select an output
15
+ output = MIDICommunications::Output.gets
16
+
17
+ # using their selection...
18
+ (0..((octaves-1)*12)).step(12) do |oct|
19
+ notes.each do |note|
20
+ output.puts(0x90, note + oct, 100) # note on
21
+ sleep(duration) # wait
22
+ output.puts(0x80, note + oct, 100) # note off
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.prepend(File.expand_path('../lib', __dir__))
4
+
5
+ require 'midi-communications'
6
+
7
+ #
8
+ # This is an example that explains how to select an output.
9
+ # It's not really meant to be run.
10
+ #
11
+
12
+ # Prompt the user for selection in the console
13
+
14
+ output = MIDICommunications::Output.gets
15
+
16
+ # The user will see a list that reflects their local MIDI configuration, and be prompted to select a number
17
+
18
+ # Select a MIDI output
19
+ # 1) IAC Device
20
+ # 2) Roland UM-2 (1)
21
+ # 3) Roland UM-2 (2)
22
+ # >
23
+
24
+ # Once they've selected, the device that corresponds with their selection is returned.
25
+
26
+ # (Note that it's returned open so you don't need to call output.open)
27
+
28
+ # Hard-code the selection like this
29
+
30
+ output = MIDICommunications::Output.use(:first)
31
+ output = MIDICommunications::Output.use(0)
32
+
33
+ # or
34
+
35
+ output = MIDICommunications::Output.open(:first)
36
+ output = MIDICommunications::Output.open(0)
37
+
38
+ # If you want to wait to open the device, you can select it with any of these "finder" methods
39
+
40
+ output = MIDICommunications::Output.first
41
+ output = MIDICommunications::Output[0]
42
+ output = MIDICommunications::Output.all[0]
43
+ output = MIDICommunications::Output.all.first
44
+ output = MIDICommunications::Device.all_by_type(:output)[0]
45
+ output = MIDICommunications::Device.all_by_type(:output).first
46
+
47
+ # You'll need to call open on these before you use it or an exception will be raised
48
+
49
+ output.open
50
+
51
+ # It's also possible to select a device by name
52
+
53
+ output = MIDICommunications::Output.find_by_name('Roland UM-2 (1)').open
54
+
55
+ # or using regex match
56
+
57
+ output = MIDICommunications::Output.find { |device| device.name.match(/Launchpad/) }.open(:first)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.prepend(File.expand_path('../lib', __dir__))
4
+
5
+ require 'midi-communications'
6
+
7
+ # Prompts the user to select a MIDI output
8
+ # Sends a MIDI system exclusive message to that output
9
+
10
+ sysex_msg = [0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7]
11
+
12
+ output = MIDICommunications::Output.gets
13
+
14
+ output.puts(sysex_msg)
@@ -0,0 +1,22 @@
1
+ require 'midi-jruby'
2
+
3
+ module MIDICommunications
4
+ module Adapter
5
+ # Load underlying devices using the midi-jruby gem
6
+ module JRuby
7
+ module Loader
8
+ extend self
9
+
10
+ # @return [Array<JRuby::Input>]
11
+ def inputs
12
+ ::MIDIJRuby::Device.all_by_type[:input]
13
+ end
14
+
15
+ # @return [Array<JRuby::Output>]
16
+ def outputs
17
+ ::MIDIJRuby::Device.all_by_type[:output]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'alsa-rawmidi'
2
+
3
+ module MIDICommunications
4
+ module Adapter
5
+ # Load underlying devices using the alsa-rawmidi gem
6
+ module Linux
7
+ module Loader
8
+ extend self
9
+
10
+ # @return [Array<Linux::Input>]
11
+ def inputs
12
+ ::AlsaRawMIDI::Device.all_by_type[:input]
13
+ end
14
+
15
+ # @return [Array<Linux::Output>]
16
+ def outputs
17
+ ::AlsaRawMIDI::Device.all_by_type[:output]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'midi-communications-macos'
2
+
3
+ module MIDICommunications
4
+ module Adapter
5
+ # Load underlying devices using the coremidi gem
6
+ module MacOS
7
+ module Loader
8
+ extend self
9
+
10
+ # @return [Array<MacOS::Source>]
11
+ def inputs
12
+ ::MIDICommunicationsMacOS::Endpoint.all_by_type[:source]
13
+ end
14
+
15
+ # @return [Array<MacOS::Destination>]
16
+ def outputs
17
+ ::MIDICommunicationsMacOS::Endpoint.all_by_type[:destination]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'midi-winmm'
2
+
3
+ module MIDICommunications
4
+ module Adapter
5
+ # Load underlying devices using the midi-winmm gem
6
+ module Windows
7
+ module Loader
8
+
9
+ extend self
10
+
11
+ # @return [Array<Windows::Input>]
12
+ def inputs
13
+ ::MIDIWinMM::Device.all_by_type[:input]
14
+ end
15
+
16
+ # @return [Array<Windows::Output>]
17
+ def outputs
18
+ ::MIDIWinMM::Device.all_by_type[:output]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,195 @@
1
+ module MIDICommunications
2
+ # Common logic that is shared by both Input and Output devices
3
+ module Device
4
+ # Methods that are shared by both Input and Output classes
5
+ module ClassMethods
6
+ include Enumerable
7
+
8
+ # Iterate over all devices of this direction (eg Input, Output)
9
+ def each(&block)
10
+ all.each(&block)
11
+ end
12
+
13
+ # Prints ids and names of each device to the console
14
+ # @return [Array<String>]
15
+ def list
16
+ all.map do |device|
17
+ name = "#{device.id}) #{device.display_name}"
18
+ puts(name)
19
+ name
20
+ end
21
+ end
22
+
23
+ # Shortcut to select a device by its name
24
+ # @param [String, Symbol] name
25
+ # @return [Input, Output]
26
+ def find_by_name(name)
27
+ all.find { |device| name.to_s == device.name }
28
+ end
29
+
30
+ # Streamlined console prompt that asks the user to select a device
31
+ # When their input is received, the device is selected and enabled
32
+ def gets(&block)
33
+ device = nil
34
+ direction = get_direction
35
+ puts ''
36
+ puts "Select a MIDI #{direction}..."
37
+ while device.nil?
38
+ list
39
+ print '> '
40
+ selection = $stdin.gets.chomp
41
+ if selection != ''
42
+ selection = Integer(selection) rescue nil
43
+ device = all.find { |d| d.id == selection } unless selection.nil?
44
+ end
45
+ end
46
+ device.open(&block)
47
+ device
48
+ end
49
+
50
+ # Select the first device and enable it
51
+ # @return [Input, Output]
52
+ def first(&block)
53
+ use_device(all.first, &block)
54
+ end
55
+
56
+ # Select the last device and enable it
57
+ # @return [Input, Output]
58
+ def last(&block)
59
+ use_device(all.last, &block)
60
+ end
61
+
62
+ # Select the device at the given index and enable it
63
+ # @param [Integer] index
64
+ # @return [Input, Output]
65
+ def use(index, &block)
66
+ index = case index
67
+ when :first then 0
68
+ when :last then all.count - 1
69
+ else index
70
+ end
71
+ use_device(at(index), &block)
72
+ end
73
+ alias open use
74
+
75
+ # Select the device at the given index
76
+ # @param [Integer] index
77
+ # @return [Input, Output]
78
+ def at(index)
79
+ all[index]
80
+ end
81
+ alias [] at
82
+
83
+ private
84
+
85
+ # The direction of the device eg "input", "output"
86
+ # @return [String]
87
+ def get_direction
88
+ name.split('::').last.downcase
89
+ end
90
+
91
+ # Enable the given device
92
+ # @param [Input, Output] device
93
+ # @return [Input, Output]
94
+ def use_device(device, &block)
95
+ if device.enabled?
96
+ yield(device) if block_given?
97
+ else
98
+ device.open(&block)
99
+ end
100
+ device
101
+ end
102
+ end
103
+
104
+ # Methods that are shared by both Input and Output instances
105
+ module InstanceMethods
106
+ # @param [AlsaRawMIDI::Input, AlsaRawMIDI::Output, MIDICommunicationsMacOS::Destination, MIDICommunicationsMacOS::Source, MIDIJRuby::Input, MIDIJRuby::Output, MIDIWinMM::Input, MIDIWinMM::Output] device
107
+ def initialize(device)
108
+ @device = device
109
+ @enabled = false
110
+
111
+ populate_from_device
112
+ end
113
+
114
+ # Enable the device for use
115
+ # Params are passed to the underlying device object
116
+ # Can be passed a block to which the device will be passed in as the yieldparam
117
+ # @param [*Object] args
118
+ # @return [Input, Output] self
119
+ def open(*args)
120
+ unless @enabled
121
+ @device.open(*args)
122
+ @enabled = true
123
+ end
124
+ if block_given?
125
+ begin
126
+ yield(self)
127
+ ensure
128
+ close
129
+ end
130
+ else
131
+ at_exit do
132
+ close
133
+ end
134
+ end
135
+ self
136
+ end
137
+
138
+ # Close the device
139
+ # Params are passed to the underlying device object
140
+ # @param [*Object] args
141
+ # @return [Boolean]
142
+ def close(*args)
143
+ if @enabled
144
+ @device.close(*args)
145
+ @enabled = false
146
+ true
147
+ else
148
+ false
149
+ end
150
+ end
151
+
152
+ # Returns true if the device is not enabled
153
+ # @return [Boolean]
154
+ def closed?
155
+ !@enabled
156
+ end
157
+
158
+ # Add attributes for the device instance
159
+ # :direction, :id, :name
160
+ def self.included(base)
161
+ base.send(:attr_reader, :direction)
162
+ base.send(:attr_reader, :enabled)
163
+ base.send(:attr_reader, :id)
164
+ base.send(:attr_reader, :manufacturer)
165
+ base.send(:attr_reader, :model)
166
+ base.send(:attr_reader, :name)
167
+ base.send(:attr_reader, :display_name)
168
+ base.send(:alias_method, :enabled?, :enabled)
169
+ base.send(:alias_method, :type, :direction)
170
+ end
171
+
172
+ private
173
+
174
+ # Populate the direction attribute
175
+ def populate_direction
176
+ @direction = case @device.type
177
+ when :source, :input then :input
178
+ when :destination, :output then :output
179
+ end
180
+ end
181
+
182
+ # Populate attributes from the underlying device object
183
+ def populate_from_device
184
+ @id = @device.id
185
+
186
+ @manufacturer = @device.manufacturer
187
+ @model = @device.model
188
+ @name = @device.name
189
+ @display_name = @device.display_name
190
+
191
+ populate_direction
192
+ end
193
+ end
194
+ end
195
+ end