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,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rtmidi
|
|
4
|
+
class MidiOut
|
|
5
|
+
DEFAULT_CLIENT_NAME = "Rtmidi Ruby Client"
|
|
6
|
+
DEFAULT_PORT_NAME = "Rtmidi Output"
|
|
7
|
+
|
|
8
|
+
class Release
|
|
9
|
+
def initialize(handle)
|
|
10
|
+
@handle = handle
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(*)
|
|
15
|
+
free
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def free
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
return if @handle.nil? || @handle.null?
|
|
21
|
+
|
|
22
|
+
Native.rtmidi_out_free(@handle)
|
|
23
|
+
@handle = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(api: :unspecified, client_name: DEFAULT_CLIENT_NAME)
|
|
29
|
+
Native.ensure_loaded!
|
|
30
|
+
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@port_open = false
|
|
33
|
+
@closed = false
|
|
34
|
+
@last_async_error = nil
|
|
35
|
+
@user_error_callback = nil
|
|
36
|
+
@error_callback = nil
|
|
37
|
+
|
|
38
|
+
@handle = Native.rtmidi_out_create(Api.normalize(api), client_name.to_s)
|
|
39
|
+
raise Error, "failed to create RtMidi output device" if @handle.nil? || @handle.null?
|
|
40
|
+
|
|
41
|
+
@release = Release.new(@handle)
|
|
42
|
+
ObjectSpace.define_finalizer(self, @release)
|
|
43
|
+
|
|
44
|
+
setup_error_callback
|
|
45
|
+
Native.check_error(@handle)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def port_count
|
|
49
|
+
with_device_lock do
|
|
50
|
+
port_count_locked
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def port_name(number)
|
|
55
|
+
with_device_lock do
|
|
56
|
+
port_name_locked(normalize_port_number(number, port_count_locked))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def port_names
|
|
61
|
+
with_device_lock do
|
|
62
|
+
count = port_count_locked
|
|
63
|
+
Array.new(count) { |index| port_name_locked(index) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def open_port(number, name: DEFAULT_PORT_NAME)
|
|
68
|
+
with_device_lock do
|
|
69
|
+
index = normalize_port_number(number, port_count_locked)
|
|
70
|
+
Native.rtmidi_open_port(@handle, index, name.to_s)
|
|
71
|
+
Native.check_error(@handle)
|
|
72
|
+
@port_open = true
|
|
73
|
+
end
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def open_virtual_port(name: "Rtmidi Virtual Output")
|
|
78
|
+
with_device_lock do
|
|
79
|
+
Native.rtmidi_open_virtual_port(@handle, name.to_s)
|
|
80
|
+
Native.check_error(@handle)
|
|
81
|
+
@port_open = true
|
|
82
|
+
end
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def close_port
|
|
87
|
+
with_device_lock do
|
|
88
|
+
Native.rtmidi_close_port(@handle)
|
|
89
|
+
Native.check_error(@handle)
|
|
90
|
+
@port_open = false
|
|
91
|
+
end
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def port_open?
|
|
96
|
+
@mutex.synchronize { @port_open }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def current_api
|
|
100
|
+
with_device_lock do
|
|
101
|
+
Api.normalize(Native.rtmidi_out_get_current_api(@handle))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_api_name
|
|
106
|
+
Rtmidi.api_name(current_api)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def current_api_display_name
|
|
110
|
+
Rtmidi.api_display_name(current_api)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def send_message(message)
|
|
114
|
+
bytes = normalize_message(message)
|
|
115
|
+
|
|
116
|
+
with_device_lock do
|
|
117
|
+
raise InvalidUseError, "port is not open" unless @port_open
|
|
118
|
+
|
|
119
|
+
buffer = FFI::MemoryPointer.new(:uint8, bytes.length)
|
|
120
|
+
buffer.write_array_of_uint8(bytes)
|
|
121
|
+
Native.rtmidi_out_send_message(@handle, buffer, bytes.length)
|
|
122
|
+
Native.check_error(@handle)
|
|
123
|
+
end
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def sysex(data)
|
|
128
|
+
send_message(Rtmidi::Message::SysEx.new(data: data))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def note_on(channel, note, velocity)
|
|
132
|
+
send_message([
|
|
133
|
+
status_byte(0x90, channel),
|
|
134
|
+
data_byte(note, "note"),
|
|
135
|
+
data_byte(velocity, "velocity")
|
|
136
|
+
])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def note_off(channel, note, velocity: 0)
|
|
140
|
+
send_message([
|
|
141
|
+
status_byte(0x80, channel),
|
|
142
|
+
data_byte(note, "note"),
|
|
143
|
+
data_byte(velocity, "velocity")
|
|
144
|
+
])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def control_change(channel, controller, value)
|
|
148
|
+
send_message([
|
|
149
|
+
status_byte(0xB0, channel),
|
|
150
|
+
data_byte(controller, "controller"),
|
|
151
|
+
data_byte(value, "value")
|
|
152
|
+
])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def program_change(channel, program)
|
|
156
|
+
send_message([
|
|
157
|
+
status_byte(0xC0, channel),
|
|
158
|
+
data_byte(program, "program")
|
|
159
|
+
])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def pitch_bend(channel, value)
|
|
163
|
+
bend = normalize_range(value, 0, 16_383, "value")
|
|
164
|
+
send_message([
|
|
165
|
+
status_byte(0xE0, channel),
|
|
166
|
+
bend & 0x7F,
|
|
167
|
+
(bend >> 7) & 0x7F
|
|
168
|
+
])
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def channel_aftertouch(channel, pressure)
|
|
172
|
+
send_message([
|
|
173
|
+
status_byte(0xD0, channel),
|
|
174
|
+
data_byte(pressure, "pressure")
|
|
175
|
+
])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def poly_aftertouch(channel, note, pressure)
|
|
179
|
+
send_message([
|
|
180
|
+
status_byte(0xA0, channel),
|
|
181
|
+
data_byte(note, "note"),
|
|
182
|
+
data_byte(pressure, "pressure")
|
|
183
|
+
])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def time_code_quarter_frame(message_type, value)
|
|
187
|
+
send_message(Rtmidi::Message::TimeCodeQuarterFrame.new(message_type: message_type, value: value))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def song_position_pointer(position)
|
|
191
|
+
send_message(Rtmidi::Message::SongPositionPointer.new(position: position))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def song_select(song)
|
|
195
|
+
send_message(Rtmidi::Message::SongSelect.new(song: song))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def tune_request
|
|
199
|
+
send_message(Rtmidi::Message::TuneRequest.new)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def timing_clock
|
|
203
|
+
send_message(Rtmidi::Message::TimingClock.new)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def start
|
|
207
|
+
send_message(Rtmidi::Message::Start.new)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def continue
|
|
211
|
+
send_message(Rtmidi::Message::Continue.new)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def stop
|
|
215
|
+
send_message(Rtmidi::Message::Stop.new)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def active_sensing
|
|
219
|
+
send_message(Rtmidi::Message::ActiveSensing.new)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def system_reset
|
|
223
|
+
send_message(Rtmidi::Message::SystemReset.new)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def nrpn(channel, parameter, value)
|
|
227
|
+
parameter_number = normalize_range(parameter, 0, 16_383, "parameter")
|
|
228
|
+
data_value = normalize_range(value, 0, 16_383, "value")
|
|
229
|
+
|
|
230
|
+
control_change(channel, 99, (parameter_number >> 7) & 0x7F)
|
|
231
|
+
control_change(channel, 98, parameter_number & 0x7F)
|
|
232
|
+
control_change(channel, 6, (data_value >> 7) & 0x7F)
|
|
233
|
+
control_change(channel, 38, data_value & 0x7F)
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def on_error(&block)
|
|
238
|
+
raise ArgumentError, "block required" unless block
|
|
239
|
+
|
|
240
|
+
@mutex.synchronize { @user_error_callback = block }
|
|
241
|
+
self
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def close
|
|
245
|
+
@mutex.synchronize do
|
|
246
|
+
return if @closed
|
|
247
|
+
|
|
248
|
+
begin
|
|
249
|
+
if @port_open
|
|
250
|
+
Native.rtmidi_close_port(@handle)
|
|
251
|
+
Native.check_error(@handle)
|
|
252
|
+
@port_open = false
|
|
253
|
+
end
|
|
254
|
+
ensure
|
|
255
|
+
@release&.free
|
|
256
|
+
@handle = nil
|
|
257
|
+
@closed = true
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def closed?
|
|
264
|
+
@mutex.synchronize { @closed }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
def setup_error_callback
|
|
270
|
+
return unless Native.function_available?(:rtmidi_set_error_callback)
|
|
271
|
+
|
|
272
|
+
@error_callback = proc do |type, error_text, _user_data|
|
|
273
|
+
callback = @mutex.synchronize { @user_error_callback }
|
|
274
|
+
|
|
275
|
+
if callback
|
|
276
|
+
callback.call(type, error_text)
|
|
277
|
+
elsif Rtmidi.warning_type?(type)
|
|
278
|
+
warn "[Rtmidi #{type}] #{error_text}"
|
|
279
|
+
else
|
|
280
|
+
@mutex.synchronize { @last_async_error ||= Rtmidi.error_for(type, error_text) }
|
|
281
|
+
end
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
@mutex.synchronize { @last_async_error ||= e }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
Native.rtmidi_set_error_callback(@handle, @error_callback, nil)
|
|
287
|
+
Native.check_error(@handle)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def with_device_lock
|
|
291
|
+
@mutex.synchronize do
|
|
292
|
+
raise InvalidUseError, "device is closed" if @closed
|
|
293
|
+
|
|
294
|
+
raise_pending_async_error!
|
|
295
|
+
yield
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def raise_pending_async_error!
|
|
300
|
+
error = @last_async_error
|
|
301
|
+
@last_async_error = nil
|
|
302
|
+
raise error unless error.nil?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def port_count_locked
|
|
306
|
+
count = Native.rtmidi_get_port_count(@handle)
|
|
307
|
+
Native.check_error(@handle)
|
|
308
|
+
count
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def port_name_locked(number)
|
|
312
|
+
len_ptr = FFI::MemoryPointer.new(:int, 1)
|
|
313
|
+
len_ptr.write_int(0)
|
|
314
|
+
Native.rtmidi_get_port_name(@handle, number, nil, len_ptr)
|
|
315
|
+
Native.check_error(@handle)
|
|
316
|
+
|
|
317
|
+
length = len_ptr.read_int
|
|
318
|
+
return "" if length <= 0
|
|
319
|
+
|
|
320
|
+
buffer = FFI::MemoryPointer.new(:char, length)
|
|
321
|
+
len_ptr.write_int(length)
|
|
322
|
+
Native.rtmidi_get_port_name(@handle, number, buffer, len_ptr)
|
|
323
|
+
Native.check_error(@handle)
|
|
324
|
+
buffer.read_string
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def normalize_port_number(number, count)
|
|
328
|
+
port = integer_for(number, "port")
|
|
329
|
+
raise NoDevicesError, "no MIDI ports available" if count.zero?
|
|
330
|
+
|
|
331
|
+
return port if port >= 0 && port < count
|
|
332
|
+
|
|
333
|
+
raise InvalidPortError, "invalid port index #{port} (available: 0...#{count})"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def normalize_message(message)
|
|
337
|
+
Rtmidi::Message.serialize(message)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def status_byte(base, channel)
|
|
341
|
+
base | normalize_range(channel, 0, 15, "channel")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def data_byte(value, label)
|
|
345
|
+
normalize_range(value, 0, 127, label)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def normalize_range(value, min, max, label)
|
|
349
|
+
integer = integer_for(value, label)
|
|
350
|
+
return integer if integer >= min && integer <= max
|
|
351
|
+
|
|
352
|
+
raise ArgumentError, "#{label} must be within #{min}..#{max}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def integer_for(value, label)
|
|
356
|
+
Integer(value)
|
|
357
|
+
rescue ArgumentError, TypeError
|
|
358
|
+
raise ArgumentError, "#{label} must be an Integer"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Rtmidi
|
|
6
|
+
module Native
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
LOAD_ERROR_MESSAGE = <<~MSG.freeze
|
|
10
|
+
librtmidi が見つかりません。
|
|
11
|
+
macOS: brew install rtmidi
|
|
12
|
+
Ubuntu/Debian: sudo apt install librtmidi-dev
|
|
13
|
+
環境変数 RTMIDI_LIB_PATH でパスを指定することもできます。
|
|
14
|
+
MSG
|
|
15
|
+
|
|
16
|
+
DEFAULT_LIBRARIES = [
|
|
17
|
+
"rtmidi",
|
|
18
|
+
"librtmidi",
|
|
19
|
+
"librtmidi.so.7",
|
|
20
|
+
"librtmidi.so",
|
|
21
|
+
"librtmidi.dylib",
|
|
22
|
+
"/opt/homebrew/lib/librtmidi.dylib",
|
|
23
|
+
"/usr/local/lib/librtmidi.dylib",
|
|
24
|
+
"rtmidi.dll",
|
|
25
|
+
"librtmidi.dll"
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
class Wrapper < FFI::Struct
|
|
29
|
+
layout :ptr, :pointer,
|
|
30
|
+
:data, :pointer,
|
|
31
|
+
:ok, :bool,
|
|
32
|
+
:msg, :pointer
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
enum :rtmidi_api, [
|
|
36
|
+
:unspecified, 0,
|
|
37
|
+
:macosx_core, 1,
|
|
38
|
+
:linux_alsa, 2,
|
|
39
|
+
:unix_jack, 3,
|
|
40
|
+
:windows_mm, 4,
|
|
41
|
+
:rtmidi_dummy, 5,
|
|
42
|
+
:web_midi_api, 6,
|
|
43
|
+
:windows_uwp, 7,
|
|
44
|
+
:android_amidi, 8,
|
|
45
|
+
:num_apis, 9
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
enum :rtmidi_error_type, [
|
|
49
|
+
:warning, 0,
|
|
50
|
+
:debug_warning, 1,
|
|
51
|
+
:unspecified, 2,
|
|
52
|
+
:no_devices_found, 3,
|
|
53
|
+
:invalid_device, 4,
|
|
54
|
+
:memory_error, 5,
|
|
55
|
+
:invalid_parameter, 6,
|
|
56
|
+
:invalid_use, 7,
|
|
57
|
+
:driver_error, 8,
|
|
58
|
+
:system_error, 9,
|
|
59
|
+
:thread_error, 10
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
callback :rtmidi_c_callback, [:double, :pointer, :size_t, :pointer], :void
|
|
63
|
+
callback :rtmidi_error_c_callback, [:rtmidi_error_type, :string, :pointer], :void
|
|
64
|
+
|
|
65
|
+
FUNCTION_SIGNATURES = {
|
|
66
|
+
rtmidi_get_version: [[], :string],
|
|
67
|
+
rtmidi_get_compiled_api: [[:pointer, :uint], :int],
|
|
68
|
+
rtmidi_api_name: [[:rtmidi_api], :string],
|
|
69
|
+
rtmidi_api_display_name: [[:rtmidi_api], :string],
|
|
70
|
+
rtmidi_compiled_api_by_name: [[:string], :rtmidi_api],
|
|
71
|
+
rtmidi_open_port: [[:pointer, :uint, :string], :void],
|
|
72
|
+
rtmidi_open_virtual_port: [[:pointer, :string], :void],
|
|
73
|
+
rtmidi_close_port: [[:pointer], :void],
|
|
74
|
+
rtmidi_get_port_count: [[:pointer], :uint],
|
|
75
|
+
rtmidi_get_port_name: [[:pointer, :uint, :pointer, :pointer], :int],
|
|
76
|
+
rtmidi_set_error_callback: [[:pointer, :rtmidi_error_c_callback, :pointer], :void],
|
|
77
|
+
rtmidi_in_create_default: [[], :pointer],
|
|
78
|
+
rtmidi_in_create: [[:rtmidi_api, :string, :uint], :pointer],
|
|
79
|
+
rtmidi_in_free: [[:pointer], :void],
|
|
80
|
+
rtmidi_in_get_current_api: [[:pointer], :rtmidi_api],
|
|
81
|
+
rtmidi_in_set_callback: [[:pointer, :rtmidi_c_callback, :pointer], :void],
|
|
82
|
+
rtmidi_in_cancel_callback: [[:pointer], :void],
|
|
83
|
+
rtmidi_in_ignore_types: [[:pointer, :bool, :bool, :bool], :void],
|
|
84
|
+
rtmidi_in_get_message: [[:pointer, :pointer, :pointer], :double],
|
|
85
|
+
rtmidi_out_create_default: [[], :pointer],
|
|
86
|
+
rtmidi_out_create: [[:rtmidi_api, :string], :pointer],
|
|
87
|
+
rtmidi_out_free: [[:pointer], :void],
|
|
88
|
+
rtmidi_out_get_current_api: [[:pointer], :rtmidi_api],
|
|
89
|
+
rtmidi_out_send_message: [[:pointer, :pointer, :int], :int]
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
OPTIONAL_FUNCTIONS = [
|
|
93
|
+
:rtmidi_set_error_callback
|
|
94
|
+
].freeze
|
|
95
|
+
|
|
96
|
+
@loaded = false
|
|
97
|
+
@load_error = nil
|
|
98
|
+
@available_functions = [].freeze
|
|
99
|
+
|
|
100
|
+
class << self
|
|
101
|
+
def loaded?
|
|
102
|
+
@loaded
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ensure_loaded!
|
|
106
|
+
return if loaded?
|
|
107
|
+
|
|
108
|
+
raise @load_error || LoadError, LOAD_ERROR_MESSAGE
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read_size_t(pointer)
|
|
112
|
+
if FFI::Platform::ADDRESS_SIZE == 64
|
|
113
|
+
pointer.get_uint64(0)
|
|
114
|
+
else
|
|
115
|
+
pointer.get_uint32(0)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write_size_t(pointer, value)
|
|
120
|
+
if FFI::Platform::ADDRESS_SIZE == 64
|
|
121
|
+
pointer.put_uint64(0, value)
|
|
122
|
+
else
|
|
123
|
+
pointer.put_uint32(0, value)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def check_error(device_ptr)
|
|
128
|
+
ensure_loaded!
|
|
129
|
+
return if device_ptr.nil? || device_ptr.null?
|
|
130
|
+
|
|
131
|
+
wrapper = Wrapper.new(device_ptr)
|
|
132
|
+
return if wrapper[:ok]
|
|
133
|
+
|
|
134
|
+
msg_ptr = wrapper[:msg]
|
|
135
|
+
message = msg_ptr.nil? || msg_ptr.null? ? "Unknown RtMidi error" : msg_ptr.read_string
|
|
136
|
+
raise Rtmidi::Error, message
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def function_available?(name)
|
|
140
|
+
@available_functions.include?(name.to_sym)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def library_candidates
|
|
146
|
+
candidates = []
|
|
147
|
+
lib_path = ENV["RTMIDI_LIB_PATH"]
|
|
148
|
+
candidates << lib_path unless lib_path.nil? || lib_path.empty?
|
|
149
|
+
candidates.concat(DEFAULT_LIBRARIES)
|
|
150
|
+
candidates.uniq
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def load_error_with_original(error)
|
|
154
|
+
LoadError.new("#{LOAD_ERROR_MESSAGE}\nOriginal error: #{error.message}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
ffi_lib(library_candidates)
|
|
160
|
+
@loaded = true
|
|
161
|
+
rescue LoadError => e
|
|
162
|
+
@loaded = false
|
|
163
|
+
@load_error = load_error_with_original(e)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if @loaded
|
|
167
|
+
begin
|
|
168
|
+
available_functions = []
|
|
169
|
+
|
|
170
|
+
FUNCTION_SIGNATURES.each do |name, signature|
|
|
171
|
+
attach_function(name, signature[0], signature[1])
|
|
172
|
+
available_functions << name
|
|
173
|
+
rescue FFI::NotFoundError => e
|
|
174
|
+
raise e unless OPTIONAL_FUNCTIONS.include?(name)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
@available_functions = available_functions.freeze
|
|
178
|
+
(OPTIONAL_FUNCTIONS - available_functions).each do |name|
|
|
179
|
+
define_singleton_method(name) do |*_args|
|
|
180
|
+
raise NotImplementedError, "RtMidi function #{name} is not available in the loaded librtmidi"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
rescue FFI::NotFoundError => e
|
|
184
|
+
@loaded = false
|
|
185
|
+
@available_functions = [].freeze
|
|
186
|
+
@load_error = load_error_with_original(e)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
unless @loaded
|
|
191
|
+
FUNCTION_SIGNATURES.each_key do |name|
|
|
192
|
+
define_singleton_method(name) do |*_args|
|
|
193
|
+
ensure_loaded!
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
data/lib/rtmidi.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rtmidi/version"
|
|
4
|
+
require_relative "rtmidi/error"
|
|
5
|
+
require_relative "rtmidi/message"
|
|
6
|
+
require_relative "rtmidi/native"
|
|
7
|
+
require_relative "rtmidi/api"
|
|
8
|
+
require_relative "rtmidi/midi_out"
|
|
9
|
+
require_relative "rtmidi/midi_in"
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rtmidi-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.15'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.15'
|
|
26
|
+
description: Complete Ruby bindings for RtMidi C API using FFI, including Ruby-idiomatic
|
|
27
|
+
MidiIn/MidiOut wrappers for realtime MIDI on macOS, Linux, and Windows.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- examples/list_ports.rb
|
|
39
|
+
- examples/receive_callback.rb
|
|
40
|
+
- examples/receive_polling.rb
|
|
41
|
+
- examples/send_note.rb
|
|
42
|
+
- examples/sysex_send.rb
|
|
43
|
+
- examples/virtual_port.rb
|
|
44
|
+
- lib/rtmidi.rb
|
|
45
|
+
- lib/rtmidi/api.rb
|
|
46
|
+
- lib/rtmidi/error.rb
|
|
47
|
+
- lib/rtmidi/message.rb
|
|
48
|
+
- lib/rtmidi/midi_in.rb
|
|
49
|
+
- lib/rtmidi/midi_out.rb
|
|
50
|
+
- lib/rtmidi/native.rb
|
|
51
|
+
- lib/rtmidi/version.rb
|
|
52
|
+
homepage: https://github.com/ydah/mtmidi
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
source_code_uri: https://github.com/ydah/mtmidi
|
|
57
|
+
changelog_uri: https://github.com/ydah/mtmidi/blob/main/CHANGELOG.md
|
|
58
|
+
documentation_uri: https://rubydoc.info/gems/rtmidi-ruby
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.1.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 4.0.6
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Ruby FFI bindings for RtMidi with high-level MIDI I/O API
|
|
77
|
+
test_files: []
|