midi-communications 0.7.0 → 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/README.md +3 -3
- data/lib/midi-communications/adapter/macos.rb +11 -0
- data/lib/midi-communications/adapter/windows.rb +12 -7
- data/lib/midi-communications/device.rb +5 -1
- data/lib/midi-communications/input/stream_reader.rb +17 -11
- data/lib/midi-communications/loader.rb +86 -19
- data/lib/midi-communications/physical_layer.rb +105 -0
- data/lib/midi-communications/type_conversion.rb +10 -1
- data/lib/midi-communications/version.rb +1 -1
- data/lib/midi-communications.rb +1 -0
- data/midi-communications.gemspec +8 -2
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21b452b0402e76c284ff8e74011e54e193528f27eccc6e67e03d90be46eeaf98
|
|
4
|
+
data.tar.gz: e5bc3c0d00284f259f2808fe2e3f6b231eadea633c6cf7bf6a9217f49cdeb0d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86c68969777a5d3322ffab60927763a39b742149746165251ae51cecb6f3c718daa0f3043bd37f7386fcb60db5a787ad5a9d8df66c4ee45287ea5ab74914b22d
|
|
7
|
+
data.tar.gz: fb2441905249105c97d21f648a2fb347a702de332cde9d02013dc4b2b239341f1eee76d0b4faaa2dfdb5ee52552cf7e179e7b269feadc6c7fd7a542a60f16a47
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Notify Plugin Rebuild
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
notify:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Trigger nota-plugin rebuild
|
|
12
|
+
uses: peter-evans/repository-dispatch@v3
|
|
13
|
+
with:
|
|
14
|
+
token: ${{ secrets.PLUGIN_REBUILD_PAT }}
|
|
15
|
+
repository: javier-sy/nota-plugin
|
|
16
|
+
event-type: source-updated
|
|
17
|
+
client-payload: '{"repo": "${{ github.repository }}", "sha": "${{ github.sha }}"}'
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ This library is part of a suite of Ruby libraries for MIDI:
|
|
|
15
15
|
| Low level MIDI interface to MacOS | [MIDI Communications MacOS Layer](https://github.com/javier-sy/midi-communications-macos) |
|
|
16
16
|
| 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)) |
|
|
17
17
|
| 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))|
|
|
18
|
-
| Low level MIDI interface to Windows |
|
|
18
|
+
| Low level MIDI interface to Windows | [MIDI Communications Windows Layer](https://github.com/javier-sy/midi-communications-windows) |
|
|
19
19
|
|
|
20
20
|
This library is based on [Ari Russo's](http://github.com/arirusso) library [UniMIDI](https://github.com/arirusso/unimidi).
|
|
21
21
|
|
|
@@ -37,7 +37,7 @@ Platform
|
|
|
37
37
|
* OSX: [midi-communications-macos](http://github.com/javier-sy/midi-communications-macos)
|
|
38
38
|
* JRuby: [midi-jruby](http://github.com/arirusso/midi-jruby) (**TODO: update to midi-communications-jruby**)
|
|
39
39
|
* Linux: [alsa-rawmidi](http://github.com/arirusso/alsa-rawmidi) (**TODO: update to midi-communications-linux**)
|
|
40
|
-
* Windows
|
|
40
|
+
* Windows: [midi-communications-windows](https://github.com/javier-sy/midi-communications-windows)
|
|
41
41
|
|
|
42
42
|
### Install
|
|
43
43
|
|
|
@@ -134,6 +134,6 @@ Thanks to [Ari Russo](http://github.com/arirusso) for his ruby library [unimidi]
|
|
|
134
134
|
|
|
135
135
|
### License
|
|
136
136
|
|
|
137
|
-
[MIDI Communications](https://github.com/javier-sy/midi-communications) Copyright (c) 2021-
|
|
137
|
+
[MIDI Communications](https://github.com/javier-sy/midi-communications) Copyright (c) 2021-2026 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
|
138
138
|
|
|
139
139
|
[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)
|
|
@@ -26,6 +26,17 @@ module MIDICommunications
|
|
|
26
26
|
def outputs
|
|
27
27
|
::MIDICommunicationsMacOS::Endpoint.all_by_type[:destination]
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# Discards the Core MIDI device list so that the next enumeration
|
|
31
|
+
# walks the system again.
|
|
32
|
+
#
|
|
33
|
+
# Guarded on `populated?` because Device.refresh clears a list that
|
|
34
|
+
# does not exist until something has enumerated once.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def refresh
|
|
38
|
+
::MIDICommunicationsMacOS::Device.refresh if ::MIDICommunicationsMacOS::Device.populated?
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
end
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
require 'midi-
|
|
1
|
+
require 'midi-communications-windows'
|
|
2
2
|
|
|
3
3
|
module MIDICommunications
|
|
4
4
|
module Adapter
|
|
5
|
-
# Windows adapter using the midi-
|
|
5
|
+
# Windows adapter using the midi-communications-windows gem.
|
|
6
6
|
#
|
|
7
|
-
# Uses Windows Multimedia API to communicate with MIDI devices.
|
|
7
|
+
# Uses the Windows Multimedia (WinMM) API to communicate with MIDI devices.
|
|
8
|
+
#
|
|
9
|
+
# There is no `refresh` here, unlike the macOS adapter: that gem asks WinMM
|
|
10
|
+
# afresh on every enumeration and keeps no list of its own, so there is
|
|
11
|
+
# nothing to invalidate. {Loader} only calls `refresh` on an adapter that
|
|
12
|
+
# answers to it.
|
|
8
13
|
#
|
|
9
14
|
# @api private
|
|
10
15
|
module Windows
|
|
@@ -14,15 +19,15 @@ module MIDICommunications
|
|
|
14
19
|
extend self
|
|
15
20
|
|
|
16
21
|
# Returns all available MIDI input devices.
|
|
17
|
-
# @return [Array<
|
|
22
|
+
# @return [Array<MIDICommunicationsWindows::Input>]
|
|
18
23
|
def inputs
|
|
19
|
-
::
|
|
24
|
+
::MIDICommunicationsWindows::Device.all_by_type[:input]
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
# Returns all available MIDI output devices.
|
|
23
|
-
# @return [Array<
|
|
28
|
+
# @return [Array<MIDICommunicationsWindows::Output>]
|
|
24
29
|
def outputs
|
|
25
|
-
::
|
|
30
|
+
::MIDICommunicationsWindows::Device.all_by_type[:output]
|
|
26
31
|
end
|
|
27
32
|
end
|
|
28
33
|
end
|
|
@@ -45,8 +45,12 @@ module MIDICommunications
|
|
|
45
45
|
|
|
46
46
|
# Finds a device by its name.
|
|
47
47
|
#
|
|
48
|
+
# Returns the **first** device with that name. A name is a label rather
|
|
49
|
+
# than an identifier and two devices may share one — see the note on names
|
|
50
|
+
# in {PhysicalLayer} — so where it matters, select by {#id} instead.
|
|
51
|
+
#
|
|
48
52
|
# @param name [String, Symbol] the device name to search for
|
|
49
|
-
# @return [Input, Output, nil] the matching device or nil
|
|
53
|
+
# @return [Input, Output, nil] the first matching device, or nil
|
|
50
54
|
#
|
|
51
55
|
# @example
|
|
52
56
|
# output = MIDICommunications::Output.find_by_name("IAC Driver Bus 1")
|
|
@@ -7,25 +7,30 @@ module MIDICommunications
|
|
|
7
7
|
#
|
|
8
8
|
# @api public
|
|
9
9
|
module StreamReader
|
|
10
|
-
# Reads MIDI messages from the input
|
|
10
|
+
# Reads MIDI messages from the input.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# **Blocks until at least one message has arrived**, and then returns
|
|
13
|
+
# every message that accumulated. It never returns an empty array.
|
|
14
|
+
#
|
|
15
|
+
# This is part of the contract every platform adapter implements, not an
|
|
16
|
+
# accident of one of them — see {PhysicalLayer}. A reader loop therefore
|
|
17
|
+
# needs no delay of its own, and adding one only delays the messages that
|
|
18
|
+
# are already waiting.
|
|
14
19
|
#
|
|
15
20
|
# @param args [Object] arguments passed to the underlying device
|
|
16
|
-
# @return [Array<Hash>]
|
|
21
|
+
# @return [Array<Hash>] message hashes with :data and :timestamp keys
|
|
17
22
|
#
|
|
18
23
|
# @example
|
|
19
24
|
# messages = input.gets
|
|
20
25
|
# # => [{ data: [144, 60, 100], timestamp: 1024 },
|
|
21
26
|
# # { data: [128, 60, 100], timestamp: 1100 }]
|
|
22
27
|
#
|
|
23
|
-
# @example
|
|
28
|
+
# @example Read messages as they arrive
|
|
24
29
|
# loop do
|
|
25
|
-
#
|
|
26
|
-
# messages.each { |m| puts m[:data].inspect }
|
|
27
|
-
# sleep(0.01)
|
|
30
|
+
# input.gets.each { |message| puts message[:data].inspect }
|
|
28
31
|
# end
|
|
32
|
+
#
|
|
33
|
+
# @see PhysicalLayer the contract this method's behaviour comes from
|
|
29
34
|
def gets(*args)
|
|
30
35
|
@device.gets(*args)
|
|
31
36
|
rescue SystemExit, Interrupt
|
|
@@ -34,7 +39,8 @@ module MIDICommunications
|
|
|
34
39
|
|
|
35
40
|
# Reads MIDI messages as hex strings.
|
|
36
41
|
#
|
|
37
|
-
#
|
|
42
|
+
# Blocks exactly as {#gets} does, and returns the same messages with
|
|
43
|
+
# `:data` as a hex string instead of an array of bytes.
|
|
38
44
|
#
|
|
39
45
|
# @param args [Object] arguments passed to the underlying device
|
|
40
46
|
# @return [Array<Hash>] array of message hashes with :data (String) and :timestamp keys
|
|
@@ -53,8 +59,8 @@ module MIDICommunications
|
|
|
53
59
|
|
|
54
60
|
# Reads MIDI data as a flat array of bytes.
|
|
55
61
|
#
|
|
56
|
-
#
|
|
57
|
-
# without timestamps.
|
|
62
|
+
# Blocks as {#gets} does, and returns all message data concatenated into
|
|
63
|
+
# a single array, without timestamps.
|
|
58
64
|
#
|
|
59
65
|
# @param args [Object] arguments passed to the underlying device
|
|
60
66
|
# @return [Array<Integer>] flat array of all MIDI bytes
|
|
@@ -1,37 +1,104 @@
|
|
|
1
1
|
module MIDICommunications
|
|
2
|
-
# Populates MIDI devices
|
|
2
|
+
# Populates MIDI devices from the platform adapter.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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.
|
|
6
22
|
#
|
|
7
23
|
# @api private
|
|
8
24
|
class Loader
|
|
9
25
|
class << self
|
|
10
26
|
# Sets the platform-specific loader to use.
|
|
11
27
|
#
|
|
12
|
-
#
|
|
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}
|
|
13
33
|
# @return [Module] the loader
|
|
14
34
|
def use(loader)
|
|
15
35
|
@loader = loader
|
|
36
|
+
@devices = nil
|
|
37
|
+
|
|
38
|
+
loader
|
|
16
39
|
end
|
|
17
40
|
|
|
18
41
|
# Returns all MIDI devices, optionally filtered by direction.
|
|
19
42
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# @param
|
|
23
|
-
# @
|
|
24
|
-
# @return [Array<Input>, Array<Output>]
|
|
25
|
-
def devices(
|
|
26
|
-
if @devices.nil?
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
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
|
|
97
|
+
# @return [Array<Input>, Array<Output>]
|
|
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) }
|
|
35
102
|
end
|
|
36
103
|
end
|
|
37
104
|
end
|
|
@@ -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
|
|
@@ -7,14 +7,23 @@ module MIDICommunications
|
|
|
7
7
|
|
|
8
8
|
# Converts an array of numeric bytes to a hex string.
|
|
9
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
|
+
#
|
|
10
15
|
# @param bytes [Array<Integer>] array of numeric bytes (e.g., [0x90, 0x40, 0x40])
|
|
11
16
|
# @return [String] hex string representation (e.g., "904040")
|
|
12
17
|
#
|
|
13
18
|
# @example
|
|
14
19
|
# TypeConversion.numeric_byte_array_to_hex_string([0x90, 0x40, 0x40])
|
|
15
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"
|
|
16
25
|
def numeric_byte_array_to_hex_string(bytes)
|
|
17
|
-
bytes.map { |
|
|
26
|
+
bytes.map { |byte| format('%02X', byte) }.join
|
|
18
27
|
end
|
|
19
28
|
end
|
|
20
29
|
end
|
data/lib/midi-communications.rb
CHANGED
data/midi-communications.gemspec
CHANGED
|
@@ -3,7 +3,7 @@ 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']
|
|
@@ -20,10 +20,16 @@ Gem::Specification.new do |s|
|
|
|
20
20
|
'documentation_uri' => 'https://www.rubydoc.info/gems/midi-communications'
|
|
21
21
|
}
|
|
22
22
|
|
|
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.
|
|
23
29
|
s.add_runtime_dependency 'midi-communications-macos', '~> 0.7'
|
|
30
|
+
s.add_runtime_dependency 'midi-communications-windows', '~> 0.0.1'
|
|
24
31
|
# s.add_runtime_dependency 'alsa-rawmidi', '~> 0.3', '>= 0.3.1'
|
|
25
32
|
# s.add_runtime_dependency 'midi-jruby', '~> 0.1', '>= 0.1.4'
|
|
26
|
-
# s.add_runtime_dependency 'midi-winmm', '~> 0.1', '>= 0.1.10'
|
|
27
33
|
|
|
28
34
|
s.add_development_dependency 'minitest', '~>5', '>= 5.14.4'
|
|
29
35
|
s.add_development_dependency 'rake', '~>13', '>= 13.0.6'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: midi-communications
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Javier Sánchez Yeste
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: midi-communications-macos
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: midi-communications-windows
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.0.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.0.1
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: minitest
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -132,6 +146,7 @@ executables: []
|
|
|
132
146
|
extensions: []
|
|
133
147
|
extra_rdoc_files: []
|
|
134
148
|
files:
|
|
149
|
+
- ".github/workflows/notify-plugin.yml"
|
|
135
150
|
- ".gitignore"
|
|
136
151
|
- ".version"
|
|
137
152
|
- ".yardopts"
|
|
@@ -154,6 +169,7 @@ files:
|
|
|
154
169
|
- lib/midi-communications/input/stream_reader.rb
|
|
155
170
|
- lib/midi-communications/loader.rb
|
|
156
171
|
- lib/midi-communications/output.rb
|
|
172
|
+
- lib/midi-communications/physical_layer.rb
|
|
157
173
|
- lib/midi-communications/platform.rb
|
|
158
174
|
- lib/midi-communications/type_conversion.rb
|
|
159
175
|
- lib/midi-communications/version.rb
|