midi-communications 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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