midi-communications-macos 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +10 -0
- data/LICENSE +674 -0
- data/LICENSE.ffi-coremidi +13 -0
- data/LICENSE.midiator +22 -0
- data/LICENSE.prp +22 -0
- data/README.md +118 -0
- data/Rakefile +10 -0
- data/examples/input.rb +25 -0
- data/examples/list_endpoints.rb +9 -0
- data/examples/output.rb +23 -0
- data/examples/sysex_output.rb +12 -0
- data/lib/midi-communications-macos/api.rb +259 -0
- data/lib/midi-communications-macos/destination.rb +142 -0
- data/lib/midi-communications-macos/device.rb +110 -0
- data/lib/midi-communications-macos/endpoint.rb +109 -0
- data/lib/midi-communications-macos/entity.rb +106 -0
- data/lib/midi-communications-macos/source.rb +234 -0
- data/lib/midi-communications-macos/type_conversion.rb +18 -0
- data/lib/midi-communications-macos.rb +26 -0
- data/midi-communications-macos.gemspec +24 -0
- metadata +84 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
module MIDICommunicationsMacOS
|
2
|
+
# A MIDI device may have multiple logically distinct sub-components. For example, one device may
|
3
|
+
# encompass a MIDI synthesizer and a pair of MIDI ports, both addressable via a USB port. Each
|
4
|
+
# such element of a device is called a MIDI entity.
|
5
|
+
#
|
6
|
+
# https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html
|
7
|
+
class Device
|
8
|
+
attr_reader :entities,
|
9
|
+
:id, # Unique Numeric id
|
10
|
+
:name # Device name from midi-communications-macos
|
11
|
+
|
12
|
+
# @param [Integer] id The ID for the device
|
13
|
+
# @param [Object] device_pointer The underlying device pointer
|
14
|
+
# @param [Boolean] include_offline Whether to include offline entities (default: false)
|
15
|
+
def initialize(id, device_pointer, include_offline: false)
|
16
|
+
@id = id
|
17
|
+
@resource = device_pointer
|
18
|
+
@entities = []
|
19
|
+
populate(include_offline: include_offline)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Endpoints for this device
|
23
|
+
# @return [Array<Endpoint>]
|
24
|
+
def endpoints
|
25
|
+
endpoints = { source: [], destination: [] }
|
26
|
+
endpoints.each_key do |key|
|
27
|
+
endpoint_group = entities.map { |entity| entity.endpoints[key] }.flatten
|
28
|
+
endpoints[key] += endpoint_group
|
29
|
+
end
|
30
|
+
endpoints
|
31
|
+
end
|
32
|
+
|
33
|
+
# Assign all of this Device's endpoints an consecutive local id
|
34
|
+
# @param [Integer] last_id The highest already used endpoint ID
|
35
|
+
# @return [Integer] The highest used endpoint ID after populating this device's endpoints
|
36
|
+
def populate_endpoint_ids(last_id)
|
37
|
+
id = 0
|
38
|
+
entities.each { |entity| id += entity.populate_endpoint_ids(id + last_id) }
|
39
|
+
id
|
40
|
+
end
|
41
|
+
|
42
|
+
# All cached devices
|
43
|
+
# @param [Hash] options The options to select devices with
|
44
|
+
# @option options [Boolean] :cache If false, the device list will never be cached. This would be useful if one needs to alter the device list (e.g. plug in a USB MIDI interface) while their program is running.
|
45
|
+
# @option options [Boolean] :include_offline If true, devices marked offline by midi-communications-macos will be included in the list
|
46
|
+
# @return [Array<Device>] All cached devices
|
47
|
+
def self.all(options = {})
|
48
|
+
use_cache = options[:cache] || true
|
49
|
+
include_offline = options[:include_offline] || false
|
50
|
+
if !populated? || !use_cache
|
51
|
+
@devices = []
|
52
|
+
counter = 0
|
53
|
+
while !(device_pointer = API.MIDIGetDevice(counter)).null?
|
54
|
+
device = new(counter, device_pointer, include_offline: include_offline)
|
55
|
+
@devices << device
|
56
|
+
counter += 1
|
57
|
+
end
|
58
|
+
populate_endpoint_ids
|
59
|
+
end
|
60
|
+
@devices
|
61
|
+
end
|
62
|
+
|
63
|
+
# Refresh the Device cache. This is needed if, for example a USB MIDI device is plugged in while the program is running
|
64
|
+
# @return [Array<Device>] The Device cache
|
65
|
+
def self.refresh
|
66
|
+
@devices.clear
|
67
|
+
@devices
|
68
|
+
end
|
69
|
+
|
70
|
+
# Has the device list been populated?
|
71
|
+
def self.populated?
|
72
|
+
defined?(@devices) && !@devices.nil? && !@devices.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Populate the device name
|
78
|
+
def populate_name
|
79
|
+
@name = API.get_string(@resource, 'name')
|
80
|
+
raise "Can't get device name" unless @name
|
81
|
+
end
|
82
|
+
|
83
|
+
# All of the endpoints for all devices a consecutive local id
|
84
|
+
def self.populate_endpoint_ids
|
85
|
+
counter = 0
|
86
|
+
all.each { |device| counter += device.populate_endpoint_ids(counter) }
|
87
|
+
counter
|
88
|
+
end
|
89
|
+
|
90
|
+
# Populates the entities for this device. These entities are in turn used to gather the endpoints.
|
91
|
+
# @param [Hash] options
|
92
|
+
# @option options [Boolean] :include_offline Whether to include offline entities (default: false)
|
93
|
+
# @return [Integer] The number of entities populated
|
94
|
+
def populate_entities(options = {})
|
95
|
+
include_if_offline = options[:include_offline] || false
|
96
|
+
i = 0
|
97
|
+
while !(entity_pointer = API.MIDIDeviceGetEntity(@resource, i)).null?
|
98
|
+
@entities << Entity.new(entity_pointer, include_offline: include_if_offline)
|
99
|
+
i += 1
|
100
|
+
end
|
101
|
+
i
|
102
|
+
end
|
103
|
+
|
104
|
+
# Populate the instance
|
105
|
+
def populate(include_offline:)
|
106
|
+
populate_name
|
107
|
+
populate_entities(include_offline: include_offline)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module MIDICommunicationsMacOS
|
2
|
+
# A source or destination of a 16-channel MIDI stream
|
3
|
+
#
|
4
|
+
# https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html
|
5
|
+
module Endpoint
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :enabled, # has the endpoint been initialized?
|
9
|
+
:entity,
|
10
|
+
:id, # unique local Numeric id of the endpoint
|
11
|
+
:resource_id, # :input or :output
|
12
|
+
:type
|
13
|
+
|
14
|
+
def_delegators :entity, :manufacturer, :model, :name, :display_name
|
15
|
+
|
16
|
+
alias enabled? enabled
|
17
|
+
|
18
|
+
# @param [Integer] resource_id
|
19
|
+
# @param [Entity] entity
|
20
|
+
def initialize(resource_id, entity)
|
21
|
+
@entity = entity
|
22
|
+
@resource_id = resource_id
|
23
|
+
@type = get_type
|
24
|
+
@enabled = false
|
25
|
+
|
26
|
+
@name = nil
|
27
|
+
|
28
|
+
@threads_sync_semaphore = Mutex.new
|
29
|
+
@threads_waiting = []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Is this endpoint online?
|
33
|
+
# @return [Boolean]
|
34
|
+
def online?
|
35
|
+
@entity.online? && connect?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set the id for this endpoint (the id is immutable)
|
39
|
+
# @param [Integer] id
|
40
|
+
# @return [Integer]
|
41
|
+
def id=(id)
|
42
|
+
@id ||= id
|
43
|
+
end
|
44
|
+
|
45
|
+
# Select the first endpoint of the specified type
|
46
|
+
# @return [Destination, Source]
|
47
|
+
def self.first(type)
|
48
|
+
all_by_type[type].first
|
49
|
+
end
|
50
|
+
|
51
|
+
# Select the last endpoint of the specified type
|
52
|
+
# @return [Destination, Source]
|
53
|
+
def self.last(type)
|
54
|
+
all_by_type[type].last
|
55
|
+
end
|
56
|
+
|
57
|
+
# All source endpoints
|
58
|
+
# @return [Array<Source>]
|
59
|
+
def self.sources
|
60
|
+
Device.all.map { |d| d.endpoints[:source] }.flatten
|
61
|
+
end
|
62
|
+
|
63
|
+
# All destination endpoints
|
64
|
+
# @return [Array<Destination>]
|
65
|
+
def self.destinations
|
66
|
+
Device.all.map { |d| d.endpoints[:destination] }.flatten
|
67
|
+
end
|
68
|
+
|
69
|
+
# A Hash of :source and :destination endpoints
|
70
|
+
# @return [Hash]
|
71
|
+
def self.all_by_type
|
72
|
+
{
|
73
|
+
source: sources,
|
74
|
+
destination: destinations
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# All endpoints of both types
|
79
|
+
# @return [Array<Destination, Source>]
|
80
|
+
def self.all
|
81
|
+
Device.all.map(&:endpoints).flatten
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get the class for the given endpoint type name
|
85
|
+
# @param [Symbol] type The endpoint type eg :source, :destination
|
86
|
+
# @return [Class] eg Source, Destination
|
87
|
+
def self.get_class(type)
|
88
|
+
case type
|
89
|
+
when :source then Source
|
90
|
+
when :destination then Destination
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Constructs the endpoint type (eg source, destination) for easy consumption
|
97
|
+
def get_type
|
98
|
+
class_name = self.class.name.split('::').last
|
99
|
+
class_name.downcase.to_sym
|
100
|
+
end
|
101
|
+
|
102
|
+
# Enables the midi-communications-macos MIDI client that will go with this endpoint
|
103
|
+
def enable_client
|
104
|
+
client = API.create_midi_client(@resource_id, @name)
|
105
|
+
@client = client[:resource]
|
106
|
+
client[:error]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module MIDICommunicationsMacOS
|
2
|
+
# A MIDI entity can have any number of MIDI endpoints, each of which is a source or destination
|
3
|
+
# of a 16-channel MIDI stream. By grouping a device's endpoints into entities, the system has
|
4
|
+
# enough information for an application to make reasonable default assumptions about how to
|
5
|
+
# communicate in a bi-directional manner with each entity, as is necessary in MIDI librarian
|
6
|
+
# applications.
|
7
|
+
#
|
8
|
+
# https://developer.apple.com/library/ios/documentation/CoreMidi/Reference/MIDIServices_Reference/Reference/reference.html
|
9
|
+
class Entity
|
10
|
+
attr_reader :endpoints,
|
11
|
+
:manufacturer,
|
12
|
+
:model,
|
13
|
+
:name,
|
14
|
+
:resource
|
15
|
+
|
16
|
+
# @param [FFI::Pointer] resource A pointer to the underlying entity
|
17
|
+
# @param [Boolean] include_offline Include offline endpoints in the list
|
18
|
+
def initialize(resource, include_offline: false)
|
19
|
+
@endpoints = {
|
20
|
+
source: [],
|
21
|
+
destination: []
|
22
|
+
}
|
23
|
+
@resource = resource
|
24
|
+
populate(include_offline: include_offline)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Assign all of this Entity's endpoints an consecutive local id
|
28
|
+
# @param [Integer] starting_id
|
29
|
+
# @return [Integer]
|
30
|
+
def populate_endpoint_ids(starting_id)
|
31
|
+
counter = 0
|
32
|
+
@endpoints.values.flatten.each do |endpoint|
|
33
|
+
endpoint.id = counter + starting_id
|
34
|
+
counter += 1
|
35
|
+
end
|
36
|
+
counter
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is the entity online?
|
40
|
+
# @return [Boolean]
|
41
|
+
def online?
|
42
|
+
get_int(:offline).zero?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Construct a display name for the entity
|
46
|
+
# @return [String]
|
47
|
+
def display_name
|
48
|
+
"#{@manufacturer} #{@model} (#{@name})"
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Populate endpoints of a specified type for this entity
|
54
|
+
# @param [Symbol] type The endpoint type eg :source, :destination
|
55
|
+
# @param [Boolean] include_offline Include offline endpoints in the list
|
56
|
+
# @return [Integer]
|
57
|
+
def populate_endpoints_by_type(type, include_offline:)
|
58
|
+
endpoint_class = Endpoint.get_class(type)
|
59
|
+
num_endpoints = number_of_endpoints(type)
|
60
|
+
(0..num_endpoints).each do |i|
|
61
|
+
endpoint = endpoint_class.new(i, self)
|
62
|
+
@endpoints[type] << endpoint if endpoint.online? || include_offline
|
63
|
+
end
|
64
|
+
@endpoints[type].size
|
65
|
+
end
|
66
|
+
|
67
|
+
# Populate the endpoints for this entity
|
68
|
+
# @param [Boolean] include_offline Include offline endpoints in the list
|
69
|
+
# @return [Integer]
|
70
|
+
def populate_endpoints(include_offline:)
|
71
|
+
@endpoints.keys.map { |type| populate_endpoints_by_type(type, include_offline: include_offline) }.reduce(&:+)
|
72
|
+
end
|
73
|
+
|
74
|
+
# The number of endpoints for this entity
|
75
|
+
# @param [Symbol] type The endpoint type eg :source, :destination
|
76
|
+
def number_of_endpoints(type)
|
77
|
+
case type
|
78
|
+
when :source then API.MIDIEntityGetNumberOfSources(@resource)
|
79
|
+
when :destination then API.MIDIEntityGetNumberOfDestinations(@resource)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A CFString property from the underlying entity
|
84
|
+
# @param [Symbol, String] name The property name
|
85
|
+
# @return [String, nil]
|
86
|
+
def get_string(name)
|
87
|
+
API.get_string(@resource, name)
|
88
|
+
end
|
89
|
+
|
90
|
+
# An Integer property from the underlying entity
|
91
|
+
# @param [Symbol, String] name The property name
|
92
|
+
# @return [Integer, nil]
|
93
|
+
def get_int(name)
|
94
|
+
API.get_int(@resource, name)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Populate the entity properties from the underlying resource
|
98
|
+
# @param [Boolean] include_offline Include offline endpoints in the list
|
99
|
+
def populate(include_offline:)
|
100
|
+
@manufacturer = get_string(:manufacturer)
|
101
|
+
@model = get_string(:model)
|
102
|
+
@name = get_string(:name)
|
103
|
+
populate_endpoints(include_offline: include_offline)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
module MIDICommunicationsMacOS
|
2
|
+
# Type of endpoint used for input
|
3
|
+
class Source
|
4
|
+
include Endpoint
|
5
|
+
|
6
|
+
#
|
7
|
+
# An array of MIDI event hashes as such:
|
8
|
+
# [
|
9
|
+
# { data: [144, 60, 100], timestamp: 1024 },
|
10
|
+
# { data: [128, 60, 100], timestamp: 1100 },
|
11
|
+
# { data: [144, 40, 120], timestamp: 1200 }
|
12
|
+
# ]
|
13
|
+
#
|
14
|
+
# The data is an array of Numeric bytes
|
15
|
+
# The timestamp is the number of millis since this input was enabled
|
16
|
+
#
|
17
|
+
# @return [Array<Hash>]
|
18
|
+
def gets
|
19
|
+
get_queue_new_messages
|
20
|
+
end
|
21
|
+
alias read gets
|
22
|
+
|
23
|
+
# Same as Source#gets except that it returns message data as string of hex
|
24
|
+
# digits as such:
|
25
|
+
# [
|
26
|
+
# { data: "904060", timestamp: 904 },
|
27
|
+
# { data: "804060", timestamp: 1150 },
|
28
|
+
# { data: "90447F", timestamp: 1300 }
|
29
|
+
# ]
|
30
|
+
#
|
31
|
+
# @return [Array<Hash>]
|
32
|
+
def gets_s
|
33
|
+
messages = gets
|
34
|
+
messages.each do |message|
|
35
|
+
message[:data] = TypeConversion.numeric_bytes_to_hex_string(message[:data])
|
36
|
+
end
|
37
|
+
messages
|
38
|
+
end
|
39
|
+
alias gets_bytestr gets_s
|
40
|
+
|
41
|
+
# Enable this the input for use; can be passed a block
|
42
|
+
# @return [Source]
|
43
|
+
def enable
|
44
|
+
@enabled ||= true
|
45
|
+
if block_given?
|
46
|
+
begin
|
47
|
+
yield(self)
|
48
|
+
ensure
|
49
|
+
close
|
50
|
+
end
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
alias open enable
|
55
|
+
alias start enable
|
56
|
+
|
57
|
+
# Close this input
|
58
|
+
# @return [Boolean]
|
59
|
+
def close
|
60
|
+
#error = API.MIDIPortDisconnectSource( @handle, @resource )
|
61
|
+
#raise "MIDIPortDisconnectSource returned error code #{error}" unless error.zero?
|
62
|
+
#error = API.MIDIClientDispose(@handle)
|
63
|
+
#raise "MIDIClientDispose returned error code #{error}" unless error.zero?
|
64
|
+
#error = API.MIDIPortDispose(@handle)
|
65
|
+
#raise "MIDIPortDispose returned error code #{error}" unless error.zero?
|
66
|
+
#error = API.MIDIEndpointDispose(@resource)
|
67
|
+
#raise "MIDIEndpointDispose returned error code #{error}" unless error.zero?
|
68
|
+
if @enabled
|
69
|
+
@enabled = false
|
70
|
+
true
|
71
|
+
else
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Shortcut to the first available input endpoint
|
77
|
+
# @return [Source]
|
78
|
+
def self.first
|
79
|
+
Endpoint.first(:source)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Shortcut to the last available input endpoint
|
83
|
+
# @return [Source]
|
84
|
+
def self.last
|
85
|
+
Endpoint.last(:source)
|
86
|
+
end
|
87
|
+
|
88
|
+
# All input endpoints
|
89
|
+
# @return [Array<Source>]
|
90
|
+
def self.all
|
91
|
+
Endpoint.all_by_type[:source]
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Gets new received messages from the callback queue
|
97
|
+
def get_queue_new_messages
|
98
|
+
messages = []
|
99
|
+
|
100
|
+
if @queue.empty?
|
101
|
+
@threads_sync_semaphore.synchronize do
|
102
|
+
@threads_waiting << Thread.current
|
103
|
+
end
|
104
|
+
sleep
|
105
|
+
end
|
106
|
+
|
107
|
+
messages << @queue.pop until @queue.empty?
|
108
|
+
|
109
|
+
messages
|
110
|
+
end
|
111
|
+
|
112
|
+
# Base initialization for this endpoint -- done whether or not the endpoint is enabled to check whether
|
113
|
+
# it is truly available for use
|
114
|
+
def connect
|
115
|
+
enable_client
|
116
|
+
initialize_port
|
117
|
+
@resource = API.MIDIEntityGetSource(@entity.resource, @resource_id)
|
118
|
+
error = API.MIDIPortConnectSource(@handle, @resource, nil)
|
119
|
+
|
120
|
+
@queue = Queue.new
|
121
|
+
@sysex_buffer = []
|
122
|
+
|
123
|
+
error.zero?
|
124
|
+
end
|
125
|
+
alias connect? connect
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Add a single message to the callback queue
|
130
|
+
# @param [Array<Fixnum>] bytes Message data
|
131
|
+
# @param [Float] timestamp The system float timestamp
|
132
|
+
# @return [Array<Hash>] The resulting buffer
|
133
|
+
def enqueue_message(bytes, timestamp)
|
134
|
+
if bytes.first.eql?(0xF0) || !@sysex_buffer.empty?
|
135
|
+
@sysex_buffer += bytes
|
136
|
+
if bytes.last.eql?(0xF7)
|
137
|
+
bytes = @sysex_buffer.dup
|
138
|
+
@sysex_buffer.clear
|
139
|
+
end
|
140
|
+
end
|
141
|
+
message = get_message_formatted(bytes, timestamp)
|
142
|
+
@queue << message
|
143
|
+
|
144
|
+
@threads_sync_semaphore.synchronize do
|
145
|
+
@threads_waiting.each(&:run)
|
146
|
+
@threads_waiting.clear
|
147
|
+
end
|
148
|
+
|
149
|
+
message
|
150
|
+
end
|
151
|
+
|
152
|
+
# The callback fired by midi-communications-macos when new MIDI messages are received
|
153
|
+
def get_event_callback
|
154
|
+
Thread.abort_on_exception = true
|
155
|
+
proc do |new_packets, _refcon_ptr, _connrefcon_ptr|
|
156
|
+
begin
|
157
|
+
# p "packets received: #{new_packets[:numPackets]}"
|
158
|
+
timestamp = Time.now.to_f
|
159
|
+
messages = get_messages(new_packets)
|
160
|
+
messages.each do |message|
|
161
|
+
enqueue_message(message, timestamp)
|
162
|
+
end
|
163
|
+
rescue Exception => exception
|
164
|
+
Thread.main.raise(exception)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Get MIDI messages from the given CoreMIDI packet list
|
170
|
+
# @param [API::MIDIPacketList] packet_list The packet list
|
171
|
+
# @return [Array<Array<Fixnum>>] A collection of MIDI messages
|
172
|
+
def get_messages(packet_list)
|
173
|
+
count = packet_list[:numPackets]
|
174
|
+
first = packet_list[:packet][0]
|
175
|
+
data = first[:data].to_a
|
176
|
+
messages = []
|
177
|
+
messages << data.slice!(0, first[:length])
|
178
|
+
(count - 1).times do |i|
|
179
|
+
length_index = find_next_length_index(data)
|
180
|
+
message_length = data[length_index]
|
181
|
+
|
182
|
+
next if message_length.nil?
|
183
|
+
|
184
|
+
packet_start_index = length_index + 2
|
185
|
+
packet_end_index = packet_start_index + message_length
|
186
|
+
|
187
|
+
next unless data.length >= packet_end_index + 1
|
188
|
+
|
189
|
+
packet = data.slice!(0..packet_end_index)
|
190
|
+
message = packet.slice(packet_start_index, message_length)
|
191
|
+
messages << message
|
192
|
+
end
|
193
|
+
messages
|
194
|
+
end
|
195
|
+
|
196
|
+
# Get the next index for "length" from the blob of MIDI data
|
197
|
+
# @param [Array<Fixnum>] data
|
198
|
+
# @return [Fixnum]
|
199
|
+
def find_next_length_index(data)
|
200
|
+
last_is_zero = false
|
201
|
+
data.each_with_index do |num, i|
|
202
|
+
if num.zero?
|
203
|
+
if last_is_zero
|
204
|
+
return i + 1
|
205
|
+
else
|
206
|
+
last_is_zero = true
|
207
|
+
end
|
208
|
+
else
|
209
|
+
last_is_zero = false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Give a message its timestamp and package it in a Hash
|
215
|
+
# @return [Hash]
|
216
|
+
def get_message_formatted(raw, time)
|
217
|
+
{
|
218
|
+
data: raw,
|
219
|
+
timestamp: time
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
# Initialize a midi-communications-macos port for this endpoint
|
224
|
+
# @return [Boolean]
|
225
|
+
def initialize_port
|
226
|
+
@callback = get_event_callback
|
227
|
+
port = API.create_midi_input_port(@client, @resource_id, @name, @callback)
|
228
|
+
@handle = port[:handle]
|
229
|
+
raise "MIDIInputPortCreate returned error code #{port[:error]}" unless port[:error].zero?
|
230
|
+
|
231
|
+
true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MIDICommunicationsMacOS
|
2
|
+
# Helper for convertig MIDI data
|
3
|
+
module TypeConversion
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# Convert an array of numeric byes to a hex string (e.g. [0x90, 0x40, 0x40] becomes "904040")
|
7
|
+
# @param [Array<Integer>] bytes
|
8
|
+
# @return [String]
|
9
|
+
def numeric_bytes_to_hex_string(bytes)
|
10
|
+
string_bytes = bytes.map do |byte|
|
11
|
+
str = byte.to_s(16).upcase
|
12
|
+
str = "0#{str}" if byte < 16
|
13
|
+
str
|
14
|
+
end
|
15
|
+
string_bytes.join
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# midi-communications-macos
|
3
|
+
# Realtime MIDI IO with Ruby for OSX
|
4
|
+
#
|
5
|
+
# (c)2021 Javier Sánchez Yeste for the modifications, licensed under LGPL 3.0 License
|
6
|
+
# (c)2011-2017 Ari Russo
|
7
|
+
#
|
8
|
+
|
9
|
+
# Libs
|
10
|
+
require 'ffi'
|
11
|
+
require 'forwardable'
|
12
|
+
|
13
|
+
# Modules
|
14
|
+
require 'midi-communications-macos/api'
|
15
|
+
require 'midi-communications-macos/endpoint'
|
16
|
+
require 'midi-communications-macos/type_conversion'
|
17
|
+
|
18
|
+
# Classes
|
19
|
+
require 'midi-communications-macos/entity'
|
20
|
+
require 'midi-communications-macos/device'
|
21
|
+
require 'midi-communications-macos/source'
|
22
|
+
require 'midi-communications-macos/destination'
|
23
|
+
|
24
|
+
module MIDICommunicationsMacOS
|
25
|
+
VERSION = '0.5.0'.freeze
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'midi-communications-macos'
|
3
|
+
s.version = '0.5.0'
|
4
|
+
s.date = '2021-11-15'
|
5
|
+
s.summary = 'Realtime MIDI IO with Ruby for OSX'
|
6
|
+
s.description = 'Access the Apple Core MIDI framework API with Ruby.'
|
7
|
+
s.authors = ['Javier Sánchez Yeste']
|
8
|
+
s.email = ['javier.sy@gmail.com']
|
9
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
10
|
+
s.homepage = 'http://rubygems.org/gems/midi-communications-macos'
|
11
|
+
s.license = 'LGPL-3.0'
|
12
|
+
|
13
|
+
s.required_ruby_version = '~> 2.7'
|
14
|
+
|
15
|
+
s.add_runtime_dependency 'ffi', '~> 1.15', '>= 1.15.4'
|
16
|
+
|
17
|
+
# TODO
|
18
|
+
#s.metadata = {
|
19
|
+
# "source_code_uri" => "https://",
|
20
|
+
# "homepage_uri" => "",
|
21
|
+
# "documentation_uri" => "",
|
22
|
+
# "changelog_uri" => ""
|
23
|
+
#}
|
24
|
+
end
|