rtmidi-ruby 0.1.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/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +327 -0
- data/Rakefile +8 -0
- data/examples/list_ports.rb +16 -0
- data/examples/receive_callback.rb +25 -0
- data/examples/receive_polling.rb +27 -0
- data/examples/send_note.rb +18 -0
- data/examples/sysex_send.rb +20 -0
- data/examples/virtual_port.rb +19 -0
- data/lib/rtmidi/api.rb +73 -0
- data/lib/rtmidi/error.rb +52 -0
- data/lib/rtmidi/message.rb +196 -0
- data/lib/rtmidi/midi_in.rb +291 -0
- data/lib/rtmidi/midi_out.rb +361 -0
- data/lib/rtmidi/native.rb +198 -0
- data/lib/rtmidi/version.rb +5 -0
- data/lib/rtmidi.rb +9 -0
- metadata +77 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rtmidi
|
|
4
|
+
module Message
|
|
5
|
+
NoteOff = Struct.new(:channel, :note, :velocity, keyword_init: true)
|
|
6
|
+
NoteOn = Struct.new(:channel, :note, :velocity, keyword_init: true)
|
|
7
|
+
PolyAftertouch = Struct.new(:channel, :note, :pressure, keyword_init: true)
|
|
8
|
+
ControlChange = Struct.new(:channel, :controller, :value, keyword_init: true)
|
|
9
|
+
ProgramChange = Struct.new(:channel, :program, keyword_init: true)
|
|
10
|
+
ChannelAftertouch = Struct.new(:channel, :pressure, keyword_init: true)
|
|
11
|
+
PitchBend = Struct.new(:channel, :value, keyword_init: true)
|
|
12
|
+
SysEx = Struct.new(:data, keyword_init: true)
|
|
13
|
+
TimeCodeQuarterFrame = Struct.new(:message_type, :value, keyword_init: true)
|
|
14
|
+
SongPositionPointer = Struct.new(:position, keyword_init: true)
|
|
15
|
+
SongSelect = Struct.new(:song, keyword_init: true)
|
|
16
|
+
TuneRequest = Struct.new(nil, keyword_init: true)
|
|
17
|
+
TimingClock = Struct.new(nil, keyword_init: true)
|
|
18
|
+
Start = Struct.new(nil, keyword_init: true)
|
|
19
|
+
Continue = Struct.new(nil, keyword_init: true)
|
|
20
|
+
Stop = Struct.new(nil, keyword_init: true)
|
|
21
|
+
ActiveSensing = Struct.new(nil, keyword_init: true)
|
|
22
|
+
SystemReset = Struct.new(nil, keyword_init: true)
|
|
23
|
+
Unknown = Struct.new(:bytes, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def parse(message)
|
|
27
|
+
bytes = normalize_bytes(message, allow_empty: false)
|
|
28
|
+
status = bytes[0]
|
|
29
|
+
|
|
30
|
+
return parse_system_message(status, bytes) if status >= 0xF0
|
|
31
|
+
|
|
32
|
+
parse_channel_message(status, bytes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize(message)
|
|
36
|
+
case message
|
|
37
|
+
when Array
|
|
38
|
+
normalize_bytes(message, allow_empty: false)
|
|
39
|
+
when NoteOff
|
|
40
|
+
[status_byte(0x80, message.channel), data_byte(message.note, "note"), data_byte(message.velocity, "velocity")]
|
|
41
|
+
when NoteOn
|
|
42
|
+
[status_byte(0x90, message.channel), data_byte(message.note, "note"), data_byte(message.velocity, "velocity")]
|
|
43
|
+
when PolyAftertouch
|
|
44
|
+
[status_byte(0xA0, message.channel), data_byte(message.note, "note"), data_byte(message.pressure, "pressure")]
|
|
45
|
+
when ControlChange
|
|
46
|
+
[status_byte(0xB0, message.channel), data_byte(message.controller, "controller"), data_byte(message.value, "value")]
|
|
47
|
+
when ProgramChange
|
|
48
|
+
[status_byte(0xC0, message.channel), data_byte(message.program, "program")]
|
|
49
|
+
when ChannelAftertouch
|
|
50
|
+
[status_byte(0xD0, message.channel), data_byte(message.pressure, "pressure")]
|
|
51
|
+
when PitchBend
|
|
52
|
+
bend = integer_in_range(message.value, 0, 16_383, "value")
|
|
53
|
+
[status_byte(0xE0, message.channel), bend & 0x7F, (bend >> 7) & 0x7F]
|
|
54
|
+
when SysEx
|
|
55
|
+
serialize_sysex(message.data)
|
|
56
|
+
when TimeCodeQuarterFrame
|
|
57
|
+
message_type = integer_in_range(message.message_type, 0, 7, "message_type")
|
|
58
|
+
value = integer_in_range(message.value, 0, 15, "value")
|
|
59
|
+
[0xF1, (message_type << 4) | value]
|
|
60
|
+
when SongPositionPointer
|
|
61
|
+
position = integer_in_range(message.position, 0, 16_383, "position")
|
|
62
|
+
[0xF2, position & 0x7F, (position >> 7) & 0x7F]
|
|
63
|
+
when SongSelect
|
|
64
|
+
[0xF3, data_byte(message.song, "song")]
|
|
65
|
+
when TuneRequest
|
|
66
|
+
[0xF6]
|
|
67
|
+
when TimingClock
|
|
68
|
+
[0xF8]
|
|
69
|
+
when Start
|
|
70
|
+
[0xFA]
|
|
71
|
+
when Continue
|
|
72
|
+
[0xFB]
|
|
73
|
+
when Stop
|
|
74
|
+
[0xFC]
|
|
75
|
+
when ActiveSensing
|
|
76
|
+
[0xFE]
|
|
77
|
+
when SystemReset
|
|
78
|
+
[0xFF]
|
|
79
|
+
when Unknown
|
|
80
|
+
normalize_bytes(message.bytes, allow_empty: false)
|
|
81
|
+
else
|
|
82
|
+
raise ArgumentError, "unsupported MIDI message: #{message.inspect}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def parse_channel_message(status, bytes)
|
|
89
|
+
channel = status & 0x0F
|
|
90
|
+
|
|
91
|
+
case status & 0xF0
|
|
92
|
+
when 0x80
|
|
93
|
+
expect_length!(bytes, 3, "note off")
|
|
94
|
+
NoteOff.new(channel: channel, note: bytes[1], velocity: bytes[2])
|
|
95
|
+
when 0x90
|
|
96
|
+
expect_length!(bytes, 3, "note on")
|
|
97
|
+
NoteOn.new(channel: channel, note: bytes[1], velocity: bytes[2])
|
|
98
|
+
when 0xA0
|
|
99
|
+
expect_length!(bytes, 3, "poly aftertouch")
|
|
100
|
+
PolyAftertouch.new(channel: channel, note: bytes[1], pressure: bytes[2])
|
|
101
|
+
when 0xB0
|
|
102
|
+
expect_length!(bytes, 3, "control change")
|
|
103
|
+
ControlChange.new(channel: channel, controller: bytes[1], value: bytes[2])
|
|
104
|
+
when 0xC0
|
|
105
|
+
expect_length!(bytes, 2, "program change")
|
|
106
|
+
ProgramChange.new(channel: channel, program: bytes[1])
|
|
107
|
+
when 0xD0
|
|
108
|
+
expect_length!(bytes, 2, "channel aftertouch")
|
|
109
|
+
ChannelAftertouch.new(channel: channel, pressure: bytes[1])
|
|
110
|
+
when 0xE0
|
|
111
|
+
expect_length!(bytes, 3, "pitch bend")
|
|
112
|
+
PitchBend.new(channel: channel, value: bytes[1] | (bytes[2] << 7))
|
|
113
|
+
else
|
|
114
|
+
Unknown.new(bytes: bytes)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_system_message(status, bytes)
|
|
119
|
+
case status
|
|
120
|
+
when 0xF0
|
|
121
|
+
SysEx.new(data: bytes[1..-2] || [])
|
|
122
|
+
when 0xF1
|
|
123
|
+
expect_length!(bytes, 2, "time code quarter frame")
|
|
124
|
+
TimeCodeQuarterFrame.new(message_type: (bytes[1] >> 4) & 0x07, value: bytes[1] & 0x0F)
|
|
125
|
+
when 0xF2
|
|
126
|
+
expect_length!(bytes, 3, "song position pointer")
|
|
127
|
+
SongPositionPointer.new(position: bytes[1] | (bytes[2] << 7))
|
|
128
|
+
when 0xF3
|
|
129
|
+
expect_length!(bytes, 2, "song select")
|
|
130
|
+
SongSelect.new(song: bytes[1])
|
|
131
|
+
when 0xF6
|
|
132
|
+
TuneRequest.new
|
|
133
|
+
when 0xF8
|
|
134
|
+
TimingClock.new
|
|
135
|
+
when 0xFA
|
|
136
|
+
Start.new
|
|
137
|
+
when 0xFB
|
|
138
|
+
Continue.new
|
|
139
|
+
when 0xFC
|
|
140
|
+
Stop.new
|
|
141
|
+
when 0xFE
|
|
142
|
+
ActiveSensing.new
|
|
143
|
+
when 0xFF
|
|
144
|
+
SystemReset.new
|
|
145
|
+
else
|
|
146
|
+
Unknown.new(bytes: bytes)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def serialize_sysex(data)
|
|
151
|
+
bytes = normalize_bytes(data, allow_empty: true)
|
|
152
|
+
return [0xF0, 0xF7] if bytes.empty?
|
|
153
|
+
|
|
154
|
+
return bytes if bytes[0] == 0xF0 && bytes[-1] == 0xF7
|
|
155
|
+
raise ArgumentError, "sysex data with 0xF0 prefix must end with 0xF7" if bytes[0] == 0xF0
|
|
156
|
+
|
|
157
|
+
[0xF0, *bytes, 0xF7]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def status_byte(base, channel)
|
|
161
|
+
base | integer_in_range(channel, 0, 15, "channel")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def data_byte(value, label)
|
|
165
|
+
integer_in_range(value, 0, 127, label)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def integer_in_range(value, min, max, label)
|
|
169
|
+
integer = begin
|
|
170
|
+
Integer(value)
|
|
171
|
+
rescue ArgumentError, TypeError
|
|
172
|
+
raise ArgumentError, "#{label} must be an Integer"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
return integer if integer >= min && integer <= max
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, "#{label} must be within #{min}..#{max}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def normalize_bytes(message, allow_empty:)
|
|
181
|
+
raise ArgumentError, "message must be an Array" unless message.is_a?(Array)
|
|
182
|
+
raise ArgumentError, "message must not be empty" if !allow_empty && message.empty?
|
|
183
|
+
|
|
184
|
+
message.each_with_index.map do |byte, index|
|
|
185
|
+
integer_in_range(byte, 0, 255, "message[#{index}]")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def expect_length!(bytes, minimum, label)
|
|
190
|
+
return if bytes.length >= minimum
|
|
191
|
+
|
|
192
|
+
raise ArgumentError, "#{label} message must contain at least #{minimum} bytes"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rtmidi
|
|
4
|
+
class MidiIn
|
|
5
|
+
DEFAULT_CLIENT_NAME = "Rtmidi Ruby Client"
|
|
6
|
+
DEFAULT_PORT_NAME = "Rtmidi Input"
|
|
7
|
+
DEFAULT_QUEUE_SIZE_LIMIT = 1024
|
|
8
|
+
POLL_BUFFER_SIZE = 65_536
|
|
9
|
+
|
|
10
|
+
class Release
|
|
11
|
+
def initialize(handle)
|
|
12
|
+
@handle = handle
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(*)
|
|
17
|
+
free
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def free
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
return if @handle.nil? || @handle.null?
|
|
23
|
+
|
|
24
|
+
Native.rtmidi_in_free(@handle)
|
|
25
|
+
@handle = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(api: :unspecified, client_name: DEFAULT_CLIENT_NAME, queue_size_limit: DEFAULT_QUEUE_SIZE_LIMIT)
|
|
31
|
+
Native.ensure_loaded!
|
|
32
|
+
queue_limit = integer_for(queue_size_limit, "queue_size_limit")
|
|
33
|
+
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@port_open = false
|
|
36
|
+
@closed = false
|
|
37
|
+
@last_async_error = nil
|
|
38
|
+
@user_error_callback = nil
|
|
39
|
+
@error_callback = nil
|
|
40
|
+
@message_callback = nil
|
|
41
|
+
|
|
42
|
+
@handle = Native.rtmidi_in_create(Api.normalize(api), client_name.to_s, queue_limit)
|
|
43
|
+
raise Error, "failed to create RtMidi input device" if @handle.nil? || @handle.null?
|
|
44
|
+
|
|
45
|
+
@release = Release.new(@handle)
|
|
46
|
+
ObjectSpace.define_finalizer(self, @release)
|
|
47
|
+
|
|
48
|
+
setup_error_callback
|
|
49
|
+
Native.check_error(@handle)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def port_count
|
|
53
|
+
with_device_lock do
|
|
54
|
+
port_count_locked
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def port_name(number)
|
|
59
|
+
with_device_lock do
|
|
60
|
+
port_name_locked(normalize_port_number(number, port_count_locked))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def port_names
|
|
65
|
+
with_device_lock do
|
|
66
|
+
count = port_count_locked
|
|
67
|
+
Array.new(count) { |index| port_name_locked(index) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def open_port(number, name: DEFAULT_PORT_NAME)
|
|
72
|
+
with_device_lock do
|
|
73
|
+
index = normalize_port_number(number, port_count_locked)
|
|
74
|
+
Native.rtmidi_open_port(@handle, index, name.to_s)
|
|
75
|
+
Native.check_error(@handle)
|
|
76
|
+
@port_open = true
|
|
77
|
+
end
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def open_virtual_port(name: "Rtmidi Virtual Input")
|
|
82
|
+
with_device_lock do
|
|
83
|
+
Native.rtmidi_open_virtual_port(@handle, name.to_s)
|
|
84
|
+
Native.check_error(@handle)
|
|
85
|
+
@port_open = true
|
|
86
|
+
end
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def close_port
|
|
91
|
+
with_device_lock do
|
|
92
|
+
Native.rtmidi_close_port(@handle)
|
|
93
|
+
Native.check_error(@handle)
|
|
94
|
+
@port_open = false
|
|
95
|
+
end
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def port_open?
|
|
100
|
+
@mutex.synchronize { @port_open }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def current_api
|
|
104
|
+
with_device_lock do
|
|
105
|
+
Api.normalize(Native.rtmidi_in_get_current_api(@handle))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def current_api_name
|
|
110
|
+
Rtmidi.api_name(current_api)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def current_api_display_name
|
|
114
|
+
Rtmidi.api_display_name(current_api)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def set_callback(parsed: false, &block)
|
|
118
|
+
raise ArgumentError, "block required" unless block
|
|
119
|
+
|
|
120
|
+
with_device_lock do
|
|
121
|
+
raise InvalidUseError, "callback already set" if @message_callback
|
|
122
|
+
|
|
123
|
+
@message_callback = proc do |timestamp, msg_ptr, msg_size, _user_data|
|
|
124
|
+
bytes = if msg_ptr.nil? || msg_ptr.null? || msg_size.to_i <= 0
|
|
125
|
+
[]
|
|
126
|
+
else
|
|
127
|
+
msg_ptr.read_array_of_uint8(msg_size.to_i)
|
|
128
|
+
end
|
|
129
|
+
payload = parsed ? Rtmidi::Message.parse(bytes) : bytes
|
|
130
|
+
block.call(payload, timestamp)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
@mutex.synchronize { @last_async_error ||= e }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Native.rtmidi_in_set_callback(@handle, @message_callback, nil)
|
|
136
|
+
Native.check_error(@handle)
|
|
137
|
+
end
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
alias on_message set_callback
|
|
141
|
+
|
|
142
|
+
def cancel_callback
|
|
143
|
+
with_device_lock do
|
|
144
|
+
if @message_callback
|
|
145
|
+
Native.rtmidi_in_cancel_callback(@handle)
|
|
146
|
+
Native.check_error(@handle)
|
|
147
|
+
@message_callback = nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def get_message(parsed: false)
|
|
154
|
+
with_device_lock do
|
|
155
|
+
raise InvalidUseError, "get_message cannot be used while callback is set" if @message_callback
|
|
156
|
+
|
|
157
|
+
buffer = FFI::MemoryPointer.new(:uint8, POLL_BUFFER_SIZE)
|
|
158
|
+
size_ptr = FFI::MemoryPointer.new(:size_t, 1)
|
|
159
|
+
Native.write_size_t(size_ptr, POLL_BUFFER_SIZE)
|
|
160
|
+
|
|
161
|
+
timestamp = Native.rtmidi_in_get_message(@handle, buffer, size_ptr)
|
|
162
|
+
Native.check_error(@handle)
|
|
163
|
+
|
|
164
|
+
size = Native.read_size_t(size_ptr)
|
|
165
|
+
return nil if size.zero?
|
|
166
|
+
|
|
167
|
+
bytes = buffer.read_array_of_uint8(size)
|
|
168
|
+
[parsed ? Rtmidi::Message.parse(bytes) : bytes, timestamp]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ignore_types(sysex: true, timing: true, active_sensing: true)
|
|
173
|
+
with_device_lock do
|
|
174
|
+
Native.rtmidi_in_ignore_types(@handle, !!sysex, !!timing, !!active_sensing)
|
|
175
|
+
Native.check_error(@handle)
|
|
176
|
+
end
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def on_error(&block)
|
|
181
|
+
raise ArgumentError, "block required" unless block
|
|
182
|
+
|
|
183
|
+
@mutex.synchronize { @user_error_callback = block }
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def close
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
return if @closed
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
if @message_callback
|
|
193
|
+
Native.rtmidi_in_cancel_callback(@handle)
|
|
194
|
+
Native.check_error(@handle)
|
|
195
|
+
@message_callback = nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if @port_open
|
|
199
|
+
Native.rtmidi_close_port(@handle)
|
|
200
|
+
Native.check_error(@handle)
|
|
201
|
+
@port_open = false
|
|
202
|
+
end
|
|
203
|
+
ensure
|
|
204
|
+
@release&.free
|
|
205
|
+
@handle = nil
|
|
206
|
+
@closed = true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def closed?
|
|
213
|
+
@mutex.synchronize { @closed }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def setup_error_callback
|
|
219
|
+
return unless Native.function_available?(:rtmidi_set_error_callback)
|
|
220
|
+
|
|
221
|
+
@error_callback = proc do |type, error_text, _user_data|
|
|
222
|
+
callback = @mutex.synchronize { @user_error_callback }
|
|
223
|
+
|
|
224
|
+
if callback
|
|
225
|
+
callback.call(type, error_text)
|
|
226
|
+
elsif Rtmidi.warning_type?(type)
|
|
227
|
+
warn "[Rtmidi #{type}] #{error_text}"
|
|
228
|
+
else
|
|
229
|
+
@mutex.synchronize { @last_async_error ||= Rtmidi.error_for(type, error_text) }
|
|
230
|
+
end
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
@mutex.synchronize { @last_async_error ||= e }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
Native.rtmidi_set_error_callback(@handle, @error_callback, nil)
|
|
236
|
+
Native.check_error(@handle)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def with_device_lock
|
|
240
|
+
@mutex.synchronize do
|
|
241
|
+
raise InvalidUseError, "device is closed" if @closed
|
|
242
|
+
|
|
243
|
+
raise_pending_async_error!
|
|
244
|
+
yield
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def raise_pending_async_error!
|
|
249
|
+
error = @last_async_error
|
|
250
|
+
@last_async_error = nil
|
|
251
|
+
raise error unless error.nil?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def port_count_locked
|
|
255
|
+
count = Native.rtmidi_get_port_count(@handle)
|
|
256
|
+
Native.check_error(@handle)
|
|
257
|
+
count
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def port_name_locked(number)
|
|
261
|
+
len_ptr = FFI::MemoryPointer.new(:int, 1)
|
|
262
|
+
len_ptr.write_int(0)
|
|
263
|
+
Native.rtmidi_get_port_name(@handle, number, nil, len_ptr)
|
|
264
|
+
Native.check_error(@handle)
|
|
265
|
+
|
|
266
|
+
length = len_ptr.read_int
|
|
267
|
+
return "" if length <= 0
|
|
268
|
+
|
|
269
|
+
buffer = FFI::MemoryPointer.new(:char, length)
|
|
270
|
+
len_ptr.write_int(length)
|
|
271
|
+
Native.rtmidi_get_port_name(@handle, number, buffer, len_ptr)
|
|
272
|
+
Native.check_error(@handle)
|
|
273
|
+
buffer.read_string
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def normalize_port_number(number, count)
|
|
277
|
+
port = integer_for(number, "port")
|
|
278
|
+
raise NoDevicesError, "no MIDI ports available" if count.zero?
|
|
279
|
+
|
|
280
|
+
return port if port >= 0 && port < count
|
|
281
|
+
|
|
282
|
+
raise InvalidPortError, "invalid port index #{port} (available: 0...#{count})"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def integer_for(value, label)
|
|
286
|
+
Integer(value)
|
|
287
|
+
rescue ArgumentError, TypeError
|
|
288
|
+
raise ArgumentError, "#{label} must be an Integer"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|