midi-communications 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/.github/workflows/notify-plugin.yml +17 -0
- data/.gitignore +2 -0
- data/.version +6 -0
- data/.yardopts +6 -0
- data/LICENSE +159 -668
- data/README.md +10 -20
- data/examples/select_a_device.rb +5 -6
- data/lib/midi-communications/adapter/jruby.rb +11 -3
- data/lib/midi-communications/adapter/linux.rb +11 -3
- data/lib/midi-communications/adapter/macos.rb +24 -3
- data/lib/midi-communications/adapter/windows.rb +19 -7
- data/lib/midi-communications/device.rb +151 -34
- data/lib/midi-communications/input/stream_reader.rb +55 -39
- data/lib/midi-communications/input.rb +32 -3
- data/lib/midi-communications/loader.rb +93 -17
- data/lib/midi-communications/output.rb +75 -20
- data/lib/midi-communications/physical_layer.rb +105 -0
- data/lib/midi-communications/platform.rb +12 -3
- data/lib/midi-communications/type_conversion.rb +21 -6
- data/lib/midi-communications/version.rb +2 -1
- data/lib/midi-communications.rb +39 -0
- data/midi-communications.gemspec +19 -11
- metadata +69 -6
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
require 'midi-communications/input/stream_reader'
|
|
2
2
|
|
|
3
3
|
module MIDICommunications
|
|
4
|
-
# A MIDI input device
|
|
4
|
+
# A MIDI input device for receiving MIDI messages.
|
|
5
|
+
#
|
|
6
|
+
# Input devices receive MIDI data from external controllers, instruments,
|
|
7
|
+
# or other MIDI sources. Use the class methods to discover and select
|
|
8
|
+
# available input devices.
|
|
9
|
+
#
|
|
10
|
+
# @example List available inputs
|
|
11
|
+
# MIDICommunications::Input.list
|
|
12
|
+
#
|
|
13
|
+
# @example Open the first input and read messages
|
|
14
|
+
# input = MIDICommunications::Input.first
|
|
15
|
+
# messages = input.gets
|
|
16
|
+
# # => [{ data: [144, 60, 100], timestamp: 1024 }]
|
|
17
|
+
#
|
|
18
|
+
# @example Interactive selection
|
|
19
|
+
# input = MIDICommunications::Input.gets
|
|
20
|
+
#
|
|
21
|
+
# @example Find by name
|
|
22
|
+
# input = MIDICommunications::Input.find_by_name("USB MIDI Device")
|
|
23
|
+
# input.open
|
|
24
|
+
#
|
|
25
|
+
# @see Output For sending MIDI messages
|
|
26
|
+
# @see StreamReader For reading methods (gets, gets_s, etc.)
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
5
29
|
class Input
|
|
6
30
|
extend Device::ClassMethods
|
|
7
31
|
include Device::InstanceMethods
|
|
8
32
|
include StreamReader
|
|
9
33
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
34
|
+
# Returns all available MIDI input devices.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<Input>] array of input devices
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# inputs = MIDICommunications::Input.all
|
|
40
|
+
# inputs.each { |i| puts i.name }
|
|
12
41
|
def self.all
|
|
13
42
|
Loader.devices(direction: :input)
|
|
14
43
|
end
|
|
@@ -1,28 +1,104 @@
|
|
|
1
1
|
module MIDICommunications
|
|
2
|
-
|
|
3
|
-
#
|
|
2
|
+
# Populates MIDI devices from the platform adapter.
|
|
3
|
+
#
|
|
4
|
+
# The device list is built once and kept, because enumerating is not free —
|
|
5
|
+
# on macOS it walks every device, entity and endpoint Core MIDI knows about —
|
|
6
|
+
# and most programs ask for it far more often than the machine's hardware
|
|
7
|
+
# changes. {refresh} is how a program that outlives a hardware change asks
|
|
8
|
+
# for the list again.
|
|
9
|
+
#
|
|
10
|
+
# ## What refreshing does not do
|
|
11
|
+
#
|
|
12
|
+
# A device that is still present comes back as the object it came back as
|
|
13
|
+
# before, rather than as a new wrapper around the same port. That matters
|
|
14
|
+
# because callers hold on to these: `Musa::Clock::InputMidiClock` keeps an
|
|
15
|
+
# {Input} for the length of a piece, `Musa::MIDIVoices` keeps an {Output}, and
|
|
16
|
+
# {Device::InstanceMethods#open} registers an `at_exit` on the instance.
|
|
17
|
+
# Replacing the list wholesale would leave those objects open, still due to be
|
|
18
|
+
# closed at exit, and no longer in {Input.all} — a device the program is
|
|
19
|
+
# actively using that the program can no longer find.
|
|
20
|
+
#
|
|
21
|
+
# "Still present" means the same id reporting the same name.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
4
24
|
class Loader
|
|
5
25
|
class << self
|
|
6
|
-
#
|
|
7
|
-
#
|
|
26
|
+
# Sets the platform-specific loader to use.
|
|
27
|
+
#
|
|
28
|
+
# Any devices already enumerated are discarded: they came from a
|
|
29
|
+
# different platform adapter and mean nothing to this one.
|
|
30
|
+
#
|
|
31
|
+
# @param loader [Module] a loader answering `inputs` and `outputs`, and
|
|
32
|
+
# optionally `refresh`; see {PhysicalLayer}
|
|
33
|
+
# @return [Module] the loader
|
|
8
34
|
def use(loader)
|
|
9
35
|
@loader = loader
|
|
36
|
+
@devices = nil
|
|
37
|
+
|
|
38
|
+
loader
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns all MIDI devices, optionally filtered by direction.
|
|
42
|
+
#
|
|
43
|
+
# Enumerates on the first call, and then answers from what it found.
|
|
44
|
+
#
|
|
45
|
+
# @param direction [Symbol, nil] `:input` or `:output`, or nil for both
|
|
46
|
+
# @param refresh [Boolean] ask the platform adapter again first
|
|
47
|
+
# @return [Array<Input>, Array<Output>] the devices
|
|
48
|
+
def devices(direction: nil, refresh: false)
|
|
49
|
+
populate if refresh || @devices.nil?
|
|
50
|
+
|
|
51
|
+
direction.nil? ? @devices.values.flatten : @devices[direction]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Asks the platform adapter what devices exist now.
|
|
55
|
+
#
|
|
56
|
+
# Call this when devices may have been plugged in or unplugged since the
|
|
57
|
+
# program started. Devices that are still there keep their identity; see
|
|
58
|
+
# the note on the class.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Input, Output>] every device, after re-enumerating
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# MIDICommunications::Loader.refresh
|
|
64
|
+
# MIDICommunications::Output.all # now includes what was just plugged in
|
|
65
|
+
def refresh
|
|
66
|
+
devices(refresh: true)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Asks the adapter for its devices and wraps them, keeping the wrapper of
|
|
72
|
+
# every device that was already there.
|
|
73
|
+
#
|
|
74
|
+
# The adapter is told to re-enumerate first if it can. Without that, an
|
|
75
|
+
# adapter with a cache of its own — the macOS one has — would hand back
|
|
76
|
+
# the same list it handed back before, and refreshing here would rebuild
|
|
77
|
+
# wrappers around a stale answer.
|
|
78
|
+
#
|
|
79
|
+
# Not on the first enumeration, though: there is nothing to invalidate
|
|
80
|
+
# yet, and asking a platform to walk its devices twice to answer the
|
|
81
|
+
# first question would make every program pay for a capability most of
|
|
82
|
+
# them never use.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def populate
|
|
86
|
+
@loader.refresh if @devices && @loader.respond_to?(:refresh)
|
|
87
|
+
|
|
88
|
+
@devices = {
|
|
89
|
+
input: wrap(:input, @loader.inputs) { |device| Input.new(device) },
|
|
90
|
+
output: wrap(:output, @loader.outputs) { |device| Output.new(device) }
|
|
91
|
+
}
|
|
10
92
|
end
|
|
11
93
|
|
|
12
|
-
#
|
|
13
|
-
# @param [
|
|
14
|
-
# @
|
|
94
|
+
# @param direction [Symbol] `:input` or `:output`
|
|
95
|
+
# @param devices [Array] the adapter's device objects
|
|
96
|
+
# @yield [device] builds a wrapper for a device not seen before
|
|
15
97
|
# @return [Array<Input>, Array<Output>]
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@devices = {
|
|
21
|
-
input: inputs,
|
|
22
|
-
output: outputs
|
|
23
|
-
}
|
|
24
|
-
end
|
|
25
|
-
options[:direction].nil? ? @devices.values.flatten : @devices[options[:direction]]
|
|
98
|
+
def wrap(direction, devices)
|
|
99
|
+
known = (@devices && @devices[direction] || []).to_h { |device| [[device.id, device.name], device] }
|
|
100
|
+
|
|
101
|
+
devices.map { |device| known[[device.id, device.name]] || yield(device) }
|
|
26
102
|
end
|
|
27
103
|
end
|
|
28
104
|
end
|
|
@@ -1,27 +1,69 @@
|
|
|
1
1
|
module MIDICommunications
|
|
2
|
-
|
|
3
|
-
#
|
|
2
|
+
# A MIDI output device for sending MIDI messages.
|
|
3
|
+
#
|
|
4
|
+
# Output devices send MIDI data to external instruments, software synthesizers,
|
|
5
|
+
# or other MIDI destinations. Use the class methods to discover and select
|
|
6
|
+
# available output devices.
|
|
7
|
+
#
|
|
8
|
+
# @example List available outputs
|
|
9
|
+
# MIDICommunications::Output.list
|
|
10
|
+
#
|
|
11
|
+
# @example Send a note to the first output
|
|
12
|
+
# output = MIDICommunications::Output.first
|
|
13
|
+
# output.puts(0x90, 60, 100) # Note On, middle C, velocity 100
|
|
14
|
+
# sleep(0.5)
|
|
15
|
+
# output.puts(0x80, 60, 0) # Note Off
|
|
16
|
+
#
|
|
17
|
+
# @example Send messages as hex strings
|
|
18
|
+
# output = MIDICommunications::Output.first
|
|
19
|
+
# output.puts_s("904060") # Note On
|
|
20
|
+
# output.puts_s("804060") # Note Off
|
|
21
|
+
#
|
|
22
|
+
# @example Interactive selection
|
|
23
|
+
# output = MIDICommunications::Output.gets
|
|
24
|
+
#
|
|
25
|
+
# @example Find by name
|
|
26
|
+
# output = MIDICommunications::Output.find_by_name("IAC Driver Bus 1")
|
|
27
|
+
# output.open
|
|
28
|
+
#
|
|
29
|
+
# @see Input For receiving MIDI messages
|
|
30
|
+
#
|
|
31
|
+
# @api public
|
|
4
32
|
class Output
|
|
5
33
|
extend Device::ClassMethods
|
|
6
34
|
include Device::InstanceMethods
|
|
7
35
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
36
|
+
# Returns all available MIDI output devices.
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<Output>] array of output devices
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# outputs = MIDICommunications::Output.all
|
|
42
|
+
# outputs.each { |o| puts o.name }
|
|
10
43
|
def self.all
|
|
11
44
|
Loader.devices(direction: :output)
|
|
12
45
|
end
|
|
13
46
|
|
|
14
|
-
# Sends a message to the output.
|
|
47
|
+
# Sends a MIDI message to the output.
|
|
15
48
|
#
|
|
16
|
-
#
|
|
49
|
+
# Accepts multiple message formats for flexibility:
|
|
50
|
+
# - Numeric bytes: `output.puts(0x90, 0x40, 0x40)`
|
|
51
|
+
# - Array of numeric bytes: `output.puts([0x90, 0x40, 0x40])`
|
|
52
|
+
# - Hex string: `output.puts("904040")`
|
|
53
|
+
# - Array of strings: `output.puts(["904040", "804040"])`
|
|
54
|
+
# - Objects with `to_bytes` method: `output.puts(midi_event)`
|
|
17
55
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# 3. A string of bytes eg "904040"
|
|
21
|
-
# 4. An array of strings ["904040", "804040"]
|
|
56
|
+
# @param messages [Array<Integer>, Array<String>, Integer, String] MIDI messages in any supported format
|
|
57
|
+
# @return [Array<Integer>, Array<String>] the messages sent
|
|
22
58
|
#
|
|
23
|
-
# @
|
|
24
|
-
#
|
|
59
|
+
# @example Send Note On as bytes
|
|
60
|
+
# output.puts(0x90, 60, 100)
|
|
61
|
+
#
|
|
62
|
+
# @example Send Note On as array
|
|
63
|
+
# output.puts([0x90, 60, 100])
|
|
64
|
+
#
|
|
65
|
+
# @example Send Note On as hex string
|
|
66
|
+
# output.puts("903C64")
|
|
25
67
|
def puts(*messages)
|
|
26
68
|
message = messages.first
|
|
27
69
|
case message
|
|
@@ -37,10 +79,17 @@ module MIDICommunications
|
|
|
37
79
|
end
|
|
38
80
|
end
|
|
39
81
|
|
|
40
|
-
# Sends a message
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
82
|
+
# Sends a MIDI message as a hex string.
|
|
83
|
+
#
|
|
84
|
+
# This is a lower-level method that does not perform type checking.
|
|
85
|
+
# Use {#puts} for automatic format detection.
|
|
86
|
+
#
|
|
87
|
+
# @param messages [String] one or more hex strings (e.g., "904040")
|
|
88
|
+
# @return [String, Array<String>] the message(s) sent
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# output.puts_s("904060") # Note On
|
|
92
|
+
# output.puts_s("804060") # Note Off
|
|
44
93
|
def puts_s(*messages)
|
|
45
94
|
@device.puts_s(*messages)
|
|
46
95
|
messages.count < 2 ? messages[0] : messages
|
|
@@ -48,10 +97,16 @@ module MIDICommunications
|
|
|
48
97
|
alias puts_bytestr puts_s
|
|
49
98
|
alias puts_hex puts_s
|
|
50
99
|
|
|
51
|
-
# Sends a message
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
100
|
+
# Sends a MIDI message as numeric bytes.
|
|
101
|
+
#
|
|
102
|
+
# This is a lower-level method that does not perform type checking.
|
|
103
|
+
# Use {#puts} for automatic format detection.
|
|
104
|
+
#
|
|
105
|
+
# @param messages [Integer] numeric byte values (e.g., 0x90, 0x40, 0x40)
|
|
106
|
+
# @return [Integer, Array<Integer>] the message bytes sent
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# output.puts_bytes(0x90, 0x40, 0x40) # Note On, note 64, velocity 64
|
|
55
110
|
def puts_bytes(*messages)
|
|
56
111
|
@device.puts_bytes(*messages)
|
|
57
112
|
messages.count < 2 ? messages[0] : messages
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module MIDICommunications
|
|
2
|
+
# The contract a platform adapter's device objects must satisfy.
|
|
3
|
+
#
|
|
4
|
+
# This module defines no behaviour. It exists because the contract between
|
|
5
|
+
# `midi-communications` and the low-level gem underneath it was, until now,
|
|
6
|
+
# implicit: spread across {Loader}, {Device}, {Input::StreamReader} and
|
|
7
|
+
# {Output}, and discoverable only by reading all four and inferring what they
|
|
8
|
+
# assume. That is workable while one adapter exists. It stops being workable
|
|
9
|
+
# the moment a second one is written, because the second author reads the
|
|
10
|
+
# documentation rather than the macOS gem's source, and the documentation did
|
|
11
|
+
# not say any of this.
|
|
12
|
+
#
|
|
13
|
+
# An adapter supplies a loader module answering `inputs` and `outputs` with
|
|
14
|
+
# arrays of device objects, and optionally `refresh` (see {Loader.refresh}).
|
|
15
|
+
# Each device object must behave as described below. {Input} and {Output}
|
|
16
|
+
# wrap them; nothing else in this library touches them.
|
|
17
|
+
#
|
|
18
|
+
# ## Attributes, readable before the device is opened
|
|
19
|
+
#
|
|
20
|
+
# {Input} and {Output} read these in their constructor, which runs while the
|
|
21
|
+
# device list is being built and long before anyone opens anything. An
|
|
22
|
+
# adapter that only knows a device's name once it is open does not satisfy
|
|
23
|
+
# this contract.
|
|
24
|
+
#
|
|
25
|
+
# | Method | Type | Meaning |
|
|
26
|
+
# | --- | --- | --- |
|
|
27
|
+
# | `id` | Integer | identifies the device; see the note on uniqueness below |
|
|
28
|
+
# | `name` | String | the device's name |
|
|
29
|
+
# | `display_name` | String | the name to show a person choosing a device |
|
|
30
|
+
# | `manufacturer` | String, nil | **nil when the platform does not report it** |
|
|
31
|
+
# | `model` | String, nil | **nil when the platform does not report it** |
|
|
32
|
+
# | `type` | Symbol | `:input`/`:source`, or `:output`/`:destination` |
|
|
33
|
+
#
|
|
34
|
+
# `manufacturer` and `model` are nullable on purpose. Core MIDI reports both
|
|
35
|
+
# as strings; the Windows Multimedia API reports numeric codes from a
|
|
36
|
+
# registry that stopped being maintained in the 1990s, from which no honest
|
|
37
|
+
# string can be derived. An adapter in that position returns nil rather than
|
|
38
|
+
# an empty string or an invention, so that a consumer filtering on either can
|
|
39
|
+
# tell the difference between "does not match" and "not known here".
|
|
40
|
+
#
|
|
41
|
+
# ### On the uniqueness of `name`
|
|
42
|
+
#
|
|
43
|
+
# `name` is a label, not an identifier. Two devices may report the same one,
|
|
44
|
+
# and a consumer matching on it may therefore be matching the wrong device.
|
|
45
|
+
#
|
|
46
|
+
# This is measured, not defensive. The Windows Multimedia API stores 31
|
|
47
|
+
# characters of a name and drops the rest without saying so, and two endpoints
|
|
48
|
+
# whose names differed only past that point came back identical in every field
|
|
49
|
+
# it reports — same name, same manufacturer code, same product code, same
|
|
50
|
+
# driver version — distinguishable only by their index. Two ports of one
|
|
51
|
+
# interface whose long names differ at the end collapse the same way.
|
|
52
|
+
#
|
|
53
|
+
# {Device::ClassMethods#find_by_name} returns the first match, which is all it
|
|
54
|
+
# can do.
|
|
55
|
+
#
|
|
56
|
+
# ### On the uniqueness of `id`
|
|
57
|
+
#
|
|
58
|
+
# `id` is unique **within a direction**. It is not necessarily unique across
|
|
59
|
+
# both: on Windows a device is identified by its index among inputs or among
|
|
60
|
+
# outputs, so input 0 and output 0 are different devices and both are valid.
|
|
61
|
+
# Core MIDI happens to number endpoints of both directions from a single
|
|
62
|
+
# counter, but that is a property of Core MIDI and not something a consumer
|
|
63
|
+
# may rely on. Nothing in this library compares an id across directions —
|
|
64
|
+
# `Input.all` and `Output.all` each search their own list.
|
|
65
|
+
#
|
|
66
|
+
# ## Lifecycle
|
|
67
|
+
#
|
|
68
|
+
# - `open(*args)` — makes the device usable. Opening an already-open device
|
|
69
|
+
# must succeed and do nothing, because {Device::InstanceMethods#open} may be
|
|
70
|
+
# called on a device a caller already holds open.
|
|
71
|
+
# - `close(*args)` — releases it. Closing an already-closed device must
|
|
72
|
+
# succeed.
|
|
73
|
+
#
|
|
74
|
+
# ## Input
|
|
75
|
+
#
|
|
76
|
+
# - `gets` — **blocks** until at least one message has arrived, then returns
|
|
77
|
+
# every message accumulated, as an Array of Hashes with:
|
|
78
|
+
# - `:data`, an Array of Integer bytes making up **one complete message**,
|
|
79
|
+
# System Exclusive included, already split per message;
|
|
80
|
+
# - `:timestamp`, a Float of seconds — when the library received the
|
|
81
|
+
# message, not a stamp applied by the driver. Both existing adapters take
|
|
82
|
+
# it with `Time.now.to_f` at the moment the message reaches Ruby.
|
|
83
|
+
# - `gets_s` — the same, with `:data` as a hex String.
|
|
84
|
+
#
|
|
85
|
+
# The blocking is the part most easily got wrong, and it is load-bearing.
|
|
86
|
+
# `Musa::Clock::InputMidiClock` reads MIDI Clock in a loop with no delay of
|
|
87
|
+
# its own, relying on `gets` to be where the thread waits. An adapter whose
|
|
88
|
+
# `gets` returned an empty array immediately would turn that loop into a spin
|
|
89
|
+
# on a full core — and would do it silently, because the music would still
|
|
90
|
+
# play.
|
|
91
|
+
#
|
|
92
|
+
# ## Output
|
|
93
|
+
#
|
|
94
|
+
# - `puts_bytes(*bytes)` — sends one message given as Integer bytes.
|
|
95
|
+
# - `puts_s(hex_string)` — sends one message given as hex.
|
|
96
|
+
#
|
|
97
|
+
# Both must accept System Exclusive.
|
|
98
|
+
#
|
|
99
|
+
# @see Loader how adapters are registered and their devices enumerated
|
|
100
|
+
# @see Input::StreamReader the reading methods built on `gets`
|
|
101
|
+
#
|
|
102
|
+
# @api public
|
|
103
|
+
module PhysicalLayer
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
module MIDICommunications
|
|
2
|
-
|
|
3
|
-
#
|
|
2
|
+
# Handles platform detection and adapter loading.
|
|
3
|
+
#
|
|
4
|
+
# Automatically detects the current platform (macOS, Linux, Windows, JRuby)
|
|
5
|
+
# and loads the appropriate low-level MIDI adapter.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
4
8
|
module Platform
|
|
5
9
|
extend self
|
|
6
10
|
|
|
7
|
-
# Loads the
|
|
11
|
+
# Loads the correct MIDI adapter for the current platform.
|
|
12
|
+
#
|
|
13
|
+
# Called automatically when the library is required. Detects the
|
|
14
|
+
# platform and loads the corresponding adapter gem.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
8
17
|
def bootstrap
|
|
9
18
|
require("midi-communications/adapter/#{platform_lib}")
|
|
10
19
|
Loader.use(platform_module::Loader)
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
module MIDICommunications
|
|
2
|
-
|
|
3
|
-
#
|
|
2
|
+
# Utility methods for converting between MIDI data formats.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
4
5
|
module TypeConversion
|
|
5
6
|
extend self
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Converts an array of numeric bytes to a hex string.
|
|
9
|
+
#
|
|
10
|
+
# Every byte becomes exactly two characters. Without the padding a byte
|
|
11
|
+
# below 0x10 produces one, and the string can no longer be read back as
|
|
12
|
+
# bytes: [0x90, 0x0A, 0x64] came out as "90a64", which is five characters
|
|
13
|
+
# and describes no MIDI message at all.
|
|
14
|
+
#
|
|
15
|
+
# @param bytes [Array<Integer>] array of numeric bytes (e.g., [0x90, 0x40, 0x40])
|
|
16
|
+
# @return [String] hex string representation (e.g., "904040")
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# TypeConversion.numeric_byte_array_to_hex_string([0x90, 0x40, 0x40])
|
|
20
|
+
# # => "904040"
|
|
21
|
+
#
|
|
22
|
+
# @example A byte below 0x10 still takes two characters
|
|
23
|
+
# TypeConversion.numeric_byte_array_to_hex_string([0x90, 0x0A, 0x64])
|
|
24
|
+
# # => "900A64"
|
|
10
25
|
def numeric_byte_array_to_hex_string(bytes)
|
|
11
|
-
bytes.map { |
|
|
26
|
+
bytes.map { |byte| format('%02X', byte) }.join
|
|
12
27
|
end
|
|
13
28
|
end
|
|
14
29
|
end
|
data/lib/midi-communications.rb
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# modules
|
|
10
10
|
require 'midi-communications/device'
|
|
11
|
+
require 'midi-communications/physical_layer'
|
|
11
12
|
require 'midi-communications/platform'
|
|
12
13
|
require 'midi-communications/type_conversion'
|
|
13
14
|
|
|
@@ -18,6 +19,44 @@ require 'midi-communications/output'
|
|
|
18
19
|
|
|
19
20
|
require_relative 'midi-communications/version'
|
|
20
21
|
|
|
22
|
+
# Platform-independent realtime MIDI input and output for Ruby.
|
|
23
|
+
#
|
|
24
|
+
# MIDICommunications provides a unified API for MIDI communication across
|
|
25
|
+
# different platforms (macOS, Linux, Windows, JRuby). It automatically
|
|
26
|
+
# detects the current platform and loads the appropriate low-level adapter.
|
|
27
|
+
#
|
|
28
|
+
# This library is part of the MusaDSL MIDI suite:
|
|
29
|
+
# - {https://github.com/javier-sy/midi-events MIDI Events} - MIDI message representation
|
|
30
|
+
# - {https://github.com/javier-sy/midi-parser MIDI Parser} - MIDI data parsing
|
|
31
|
+
# - {https://github.com/javier-sy/midi-communications MIDI Communications} - MIDI I/O (this library)
|
|
32
|
+
# - {https://github.com/javier-sy/midi-communications-macos MIDI Communications MacOS} - macOS adapter
|
|
33
|
+
#
|
|
34
|
+
# @example List all MIDI outputs
|
|
35
|
+
# MIDICommunications::Output.list
|
|
36
|
+
# # 0) IAC Driver Bus 1
|
|
37
|
+
# # 1) USB MIDI Device
|
|
38
|
+
#
|
|
39
|
+
# @example Send a note to the first output
|
|
40
|
+
# output = MIDICommunications::Output.first
|
|
41
|
+
# output.puts(0x90, 60, 100) # Note On, middle C, velocity 100
|
|
42
|
+
# sleep(0.5)
|
|
43
|
+
# output.puts(0x80, 60, 0) # Note Off
|
|
44
|
+
#
|
|
45
|
+
# @example Receive MIDI from an input
|
|
46
|
+
# input = MIDICommunications::Input.first
|
|
47
|
+
# loop do
|
|
48
|
+
# messages = input.gets
|
|
49
|
+
# messages.each { |m| puts m.inspect }
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# @example Interactive device selection
|
|
53
|
+
# output = MIDICommunications::Output.gets # Prompts user to select
|
|
54
|
+
# input = MIDICommunications::Input.gets
|
|
55
|
+
#
|
|
56
|
+
# @see Input MIDI input device class
|
|
57
|
+
# @see Output MIDI output device class
|
|
58
|
+
#
|
|
59
|
+
# @api public
|
|
21
60
|
module MIDICommunications
|
|
22
61
|
Platform.bootstrap
|
|
23
62
|
end
|
data/midi-communications.gemspec
CHANGED
|
@@ -3,31 +3,39 @@ require_relative 'lib/midi-communications/version'
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = 'midi-communications'
|
|
5
5
|
s.version = MIDICommunications::VERSION
|
|
6
|
-
s.date = '
|
|
6
|
+
s.date = '2026-09-06'
|
|
7
7
|
s.summary = 'Platform independent realtime MIDI input and output for Ruby'
|
|
8
8
|
s.description = 'Access MIDI devices for MacOS, Linux (wip), Windows (wip) and JRuby (wip).'
|
|
9
9
|
s.authors = ['Javier Sánchez Yeste']
|
|
10
10
|
s.email = ['javier.sy@gmail.com']
|
|
11
11
|
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
12
|
-
s.homepage = 'https://
|
|
12
|
+
s.homepage = 'https://github.com/javier-sy/midi-communications'
|
|
13
13
|
s.license = 'LGPL-3.0-or-later'
|
|
14
14
|
|
|
15
15
|
s.required_ruby_version = '>= 2.7'
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# "changelog_uri" => ""
|
|
23
|
-
#}
|
|
17
|
+
s.metadata = {
|
|
18
|
+
'homepage_uri' => s.homepage,
|
|
19
|
+
'source_code_uri' => s.homepage,
|
|
20
|
+
'documentation_uri' => 'https://www.rubydoc.info/gems/midi-communications'
|
|
21
|
+
}
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
# RubyGems has no platform-conditional dependencies, so both platform layers
|
|
24
|
+
# are declared unconditionally. That is harmless only because both are pure
|
|
25
|
+
# Ruby over a library the operating system already provides -- CoreMIDI and
|
|
26
|
+
# winmm.dll -- so each installs anywhere and is simply not required where it
|
|
27
|
+
# does not apply. A layer that shipped compiled binaries per platform could
|
|
28
|
+
# not be declared this way.
|
|
29
|
+
s.add_runtime_dependency 'midi-communications-macos', '~> 0.7'
|
|
30
|
+
s.add_runtime_dependency 'midi-communications-windows', '~> 0.0.1'
|
|
26
31
|
# s.add_runtime_dependency 'alsa-rawmidi', '~> 0.3', '>= 0.3.1'
|
|
27
32
|
# s.add_runtime_dependency 'midi-jruby', '~> 0.1', '>= 0.1.4'
|
|
28
|
-
# s.add_runtime_dependency 'midi-winmm', '~> 0.1', '>= 0.1.10'
|
|
29
33
|
|
|
30
34
|
s.add_development_dependency 'minitest', '~>5', '>= 5.14.4'
|
|
31
35
|
s.add_development_dependency 'rake', '~>13', '>= 13.0.6'
|
|
32
36
|
s.add_development_dependency 'shoulda-context', '~>2', '>= 2.0.0'
|
|
37
|
+
|
|
38
|
+
s.add_development_dependency 'yard', '~> 0.9'
|
|
39
|
+
s.add_development_dependency 'redcarpet', '~> 3.6'
|
|
40
|
+
s.add_development_dependency 'webrick', '~> 1.8'
|
|
33
41
|
end
|