jack-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 +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +205 -0
- data/Rakefile +8 -0
- data/examples/audio_passthrough.rb +31 -0
- data/examples/meter.rb +29 -0
- data/examples/midi_generator.rb +35 -0
- data/examples/midi_monitor.rb +29 -0
- data/examples/port_connector.rb +22 -0
- data/examples/ringbuffer_recorder.rb +35 -0
- data/examples/simple_client.rb +20 -0
- data/examples/sine_generator.rb +30 -0
- data/examples/transport_control.rb +21 -0
- data/lib/jack/audio_port.rb +19 -0
- data/lib/jack/callback_manager.rb +29 -0
- data/lib/jack/client.rb +371 -0
- data/lib/jack/error.rb +41 -0
- data/lib/jack/ffi/lib_jack.rb +306 -0
- data/lib/jack/ffi/structs.rb +74 -0
- data/lib/jack/ffi/types.rb +123 -0
- data/lib/jack/metadata.rb +50 -0
- data/lib/jack/midi/event.rb +82 -0
- data/lib/jack/midi_port.rb +71 -0
- data/lib/jack/port.rb +200 -0
- data/lib/jack/ring_buffer.rb +83 -0
- data/lib/jack/session.rb +63 -0
- data/lib/jack/transport.rb +138 -0
- data/lib/jack/uuid.rb +54 -0
- data/lib/jack/version.rb +5 -0
- data/lib/jack.rb +64 -0
- metadata +91 -0
data/lib/jack/client.rb
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jack
|
|
4
|
+
class Client
|
|
5
|
+
attr_reader :name, :handle, :sample_rate, :buffer_size, :callback_manager
|
|
6
|
+
|
|
7
|
+
def initialize(name, no_start_server: false, server_name: nil, session_id: nil)
|
|
8
|
+
options = FFI::Types::JackNullOption
|
|
9
|
+
options |= FFI::Types::JackNoStartServer if no_start_server
|
|
10
|
+
options |= FFI::Types::JackServerName if server_name
|
|
11
|
+
options |= FFI::Types::JackSessionID if session_id
|
|
12
|
+
|
|
13
|
+
status_ptr = ::FFI::MemoryPointer.new(:int)
|
|
14
|
+
|
|
15
|
+
varargs = []
|
|
16
|
+
varargs.push(:string, server_name) if server_name
|
|
17
|
+
varargs.push(:string, session_id) if session_id
|
|
18
|
+
|
|
19
|
+
@handle = FFI::LibJack.jack_client_open(name, options, status_ptr, *varargs)
|
|
20
|
+
|
|
21
|
+
status = status_ptr.read_int
|
|
22
|
+
if @handle.null?
|
|
23
|
+
raise ClientOpenFailed, status
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@name = FFI::LibJack.jack_get_client_name(@handle)
|
|
27
|
+
@sample_rate = FFI::LibJack.jack_get_sample_rate(@handle)
|
|
28
|
+
@buffer_size = FFI::LibJack.jack_get_buffer_size(@handle)
|
|
29
|
+
@callback_manager = CallbackManager.new
|
|
30
|
+
@ports = []
|
|
31
|
+
@active = false
|
|
32
|
+
@closed = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.open(name, **options)
|
|
36
|
+
client = new(name, **options)
|
|
37
|
+
return client unless block_given?
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
yield client
|
|
41
|
+
ensure
|
|
42
|
+
client.close
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def activate
|
|
47
|
+
result = FFI::LibJack.jack_activate(@handle)
|
|
48
|
+
raise Error, "Failed to activate client (code: #{result})" unless result.zero?
|
|
49
|
+
|
|
50
|
+
@active = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def deactivate
|
|
54
|
+
result = FFI::LibJack.jack_deactivate(@handle)
|
|
55
|
+
raise Error, "Failed to deactivate client (code: #{result})" unless result.zero?
|
|
56
|
+
|
|
57
|
+
@active = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def close
|
|
61
|
+
return if @closed
|
|
62
|
+
|
|
63
|
+
deactivate if @active
|
|
64
|
+
@callback_manager.clear
|
|
65
|
+
@ports.clear
|
|
66
|
+
FFI::LibJack.jack_client_close(@handle)
|
|
67
|
+
@closed = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def active?
|
|
71
|
+
@active
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def closed?
|
|
75
|
+
@closed
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# --- Port operations ---
|
|
79
|
+
|
|
80
|
+
def register_audio_input(port_name)
|
|
81
|
+
register_port(port_name, FFI::Types::JACK_DEFAULT_AUDIO_TYPE,
|
|
82
|
+
FFI::Types::JackPortIsInput, AudioPort)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def register_audio_output(port_name)
|
|
86
|
+
register_port(port_name, FFI::Types::JACK_DEFAULT_AUDIO_TYPE,
|
|
87
|
+
FFI::Types::JackPortIsOutput, AudioPort)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def register_midi_input(port_name)
|
|
91
|
+
register_port(port_name, FFI::Types::JACK_DEFAULT_MIDI_TYPE,
|
|
92
|
+
FFI::Types::JackPortIsInput, MidiPort)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def register_midi_output(port_name)
|
|
96
|
+
register_port(port_name, FFI::Types::JACK_DEFAULT_MIDI_TYPE,
|
|
97
|
+
FFI::Types::JackPortIsOutput, MidiPort)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unregister_port(port)
|
|
101
|
+
result = FFI::LibJack.jack_port_unregister(@handle, port.handle)
|
|
102
|
+
raise Error, "Failed to unregister port" unless result.zero?
|
|
103
|
+
|
|
104
|
+
@ports.delete(port)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def connect(source, destination)
|
|
108
|
+
result = FFI::LibJack.jack_connect(@handle, source, destination)
|
|
109
|
+
raise ConnectionFailed, "Failed to connect #{source} -> #{destination} (code: #{result})" unless result.zero?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def disconnect(source, destination)
|
|
113
|
+
result = FFI::LibJack.jack_disconnect(@handle, source, destination)
|
|
114
|
+
raise Error, "Failed to disconnect #{source} -> #{destination} (code: #{result})" unless result.zero?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# --- Port searching ---
|
|
118
|
+
|
|
119
|
+
def get_ports(pattern: nil, type: nil, flags: 0)
|
|
120
|
+
ptr = FFI::LibJack.jack_get_ports(@handle, pattern, type, flags)
|
|
121
|
+
return [] if ptr.null?
|
|
122
|
+
|
|
123
|
+
ports = []
|
|
124
|
+
offset = 0
|
|
125
|
+
loop do
|
|
126
|
+
str_ptr = ptr.get_pointer(offset)
|
|
127
|
+
break if str_ptr.null?
|
|
128
|
+
|
|
129
|
+
ports << str_ptr.read_string
|
|
130
|
+
offset += ::FFI::Pointer.size
|
|
131
|
+
end
|
|
132
|
+
FFI::LibJack.jack_free(ptr)
|
|
133
|
+
ports
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def port_by_name(port_name)
|
|
137
|
+
port_handle = FFI::LibJack.jack_port_by_name(@handle, port_name)
|
|
138
|
+
return nil if port_handle.null?
|
|
139
|
+
|
|
140
|
+
port_type = FFI::LibJack.jack_port_type(port_handle)
|
|
141
|
+
klass = if port_type == FFI::Types::JACK_DEFAULT_MIDI_TYPE
|
|
142
|
+
MidiPort
|
|
143
|
+
else
|
|
144
|
+
AudioPort
|
|
145
|
+
end
|
|
146
|
+
klass.new(port_handle, self)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# --- Callback registration ---
|
|
150
|
+
|
|
151
|
+
def on_process(&block)
|
|
152
|
+
ffi_proc = proc { |nframes, _arg| block.call(nframes) }
|
|
153
|
+
@callback_manager.register(:process, block, ffi_proc)
|
|
154
|
+
FFI::LibJack.jack_set_process_callback(@handle, ffi_proc, nil)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def on_shutdown(&block)
|
|
158
|
+
ffi_proc = proc { |code, reason, _arg| block.call(code, reason) }
|
|
159
|
+
@callback_manager.register(:shutdown, block, ffi_proc)
|
|
160
|
+
FFI::LibJack.jack_on_info_shutdown(@handle, ffi_proc, nil)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def on_sample_rate_change(&block)
|
|
164
|
+
ffi_proc = proc { |nframes, _arg| block.call(nframes); 0 }
|
|
165
|
+
@callback_manager.register(:sample_rate, block, ffi_proc)
|
|
166
|
+
FFI::LibJack.jack_set_sample_rate_callback(@handle, ffi_proc, nil)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def on_buffer_size_change(&block)
|
|
170
|
+
ffi_proc = proc { |nframes, _arg| block.call(nframes); 0 }
|
|
171
|
+
@callback_manager.register(:buffer_size, block, ffi_proc)
|
|
172
|
+
FFI::LibJack.jack_set_buffer_size_callback(@handle, ffi_proc, nil)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def on_xrun(&block)
|
|
176
|
+
ffi_proc = proc { |_arg| block.call; 0 }
|
|
177
|
+
@callback_manager.register(:xrun, block, ffi_proc)
|
|
178
|
+
FFI::LibJack.jack_set_xrun_callback(@handle, ffi_proc, nil)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def on_port_connect(&block)
|
|
182
|
+
ffi_proc = proc { |a, b, connect, _arg| block.call(a, b, connect != 0) }
|
|
183
|
+
@callback_manager.register(:port_connect, block, ffi_proc)
|
|
184
|
+
FFI::LibJack.jack_set_port_connect_callback(@handle, ffi_proc, nil)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def on_port_registration(&block)
|
|
188
|
+
ffi_proc = proc { |port_id, registered, _arg| block.call(port_id, registered != 0) }
|
|
189
|
+
@callback_manager.register(:port_registration, block, ffi_proc)
|
|
190
|
+
FFI::LibJack.jack_set_port_registration_callback(@handle, ffi_proc, nil)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def on_port_rename(&block)
|
|
194
|
+
ffi_proc = proc { |port_id, old_name, new_name, _arg|
|
|
195
|
+
block.call(port_id, old_name, new_name)
|
|
196
|
+
0
|
|
197
|
+
}
|
|
198
|
+
@callback_manager.register(:port_rename, block, ffi_proc)
|
|
199
|
+
result = FFI::LibJack.jack_set_port_rename_callback(@handle, ffi_proc, nil)
|
|
200
|
+
raise Error, "Failed to register port rename callback (code: #{result})" unless result.zero?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def on_client_registration(&block)
|
|
204
|
+
ffi_proc = proc { |client_name, registered, _arg| block.call(client_name, registered != 0) }
|
|
205
|
+
@callback_manager.register(:client_registration, block, ffi_proc)
|
|
206
|
+
FFI::LibJack.jack_set_client_registration_callback(@handle, ffi_proc, nil)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def on_graph_order(&block)
|
|
210
|
+
ffi_proc = proc { |_arg| block.call; 0 }
|
|
211
|
+
@callback_manager.register(:graph_order, block, ffi_proc)
|
|
212
|
+
FFI::LibJack.jack_set_graph_order_callback(@handle, ffi_proc, nil)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def on_freewheel(&block)
|
|
216
|
+
ffi_proc = proc { |starting, _arg| block.call(starting != 0) }
|
|
217
|
+
@callback_manager.register(:freewheel, block, ffi_proc)
|
|
218
|
+
FFI::LibJack.jack_set_freewheel_callback(@handle, ffi_proc, nil)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def on_latency(&block)
|
|
222
|
+
ffi_proc = proc { |mode, _arg|
|
|
223
|
+
sym = mode.zero? ? :capture : :playback
|
|
224
|
+
block.call(sym)
|
|
225
|
+
}
|
|
226
|
+
@callback_manager.register(:latency, block, ffi_proc)
|
|
227
|
+
FFI::LibJack.jack_set_latency_callback(@handle, ffi_proc, nil)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# --- Server info ---
|
|
231
|
+
|
|
232
|
+
def realtime?
|
|
233
|
+
FFI::LibJack.jack_is_realtime(@handle) != 0
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def cpu_load
|
|
237
|
+
FFI::LibJack.jack_cpu_load(@handle)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def max_cpu_load
|
|
241
|
+
FFI::LibJack.jack_max_cpu_load(@handle)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def thread_id
|
|
245
|
+
FFI::LibJack.jack_client_thread_id(@handle)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def max_delayed_usecs
|
|
249
|
+
FFI::LibJack.jack_get_max_delayed_usecs(@handle)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def xrun_delayed_usecs
|
|
253
|
+
FFI::LibJack.jack_get_xrun_delayed_usecs(@handle)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def reset_max_delayed_usecs
|
|
257
|
+
FFI::LibJack.jack_reset_max_delayed_usecs(@handle)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def real_time_priority
|
|
261
|
+
FFI::LibJack.jack_client_real_time_priority(@handle)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def max_real_time_priority
|
|
265
|
+
FFI::LibJack.jack_client_max_real_time_priority(@handle)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def acquire_real_time_scheduling(thread_id: self.thread_id, priority: real_time_priority)
|
|
269
|
+
result = FFI::LibJack.jack_acquire_real_time_scheduling(thread_id, priority)
|
|
270
|
+
raise Error, "Failed to acquire real-time scheduling (code: #{result})" unless result.zero?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def drop_real_time_scheduling(thread_id: self.thread_id)
|
|
274
|
+
result = FFI::LibJack.jack_drop_real_time_scheduling(thread_id)
|
|
275
|
+
raise Error, "Failed to drop real-time scheduling (code: #{result})" unless result.zero?
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def stop_thread(thread_id)
|
|
279
|
+
result = FFI::LibJack.jack_client_stop_thread(@handle, thread_id)
|
|
280
|
+
raise Error, "Failed to stop client thread (code: #{result})" unless result.zero?
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def kill_thread(thread_id)
|
|
284
|
+
result = FFI::LibJack.jack_client_kill_thread(@handle, thread_id)
|
|
285
|
+
raise Error, "Failed to kill client thread (code: #{result})" unless result.zero?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def freewheel=(enabled)
|
|
289
|
+
result = FFI::LibJack.jack_set_freewheel(@handle, enabled ? 1 : 0)
|
|
290
|
+
raise Error, "Failed to set freewheel mode" unless result.zero?
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def buffer_size=(nframes)
|
|
294
|
+
result = FFI::LibJack.jack_set_buffer_size(@handle, nframes)
|
|
295
|
+
raise Error, "Failed to set buffer size" unless result.zero?
|
|
296
|
+
|
|
297
|
+
@buffer_size = nframes
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# --- Transport ---
|
|
301
|
+
|
|
302
|
+
def transport
|
|
303
|
+
@transport ||= Transport.new(self)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# --- Session ---
|
|
307
|
+
|
|
308
|
+
def session
|
|
309
|
+
@session ||= Session.new(self)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# --- Metadata ---
|
|
313
|
+
|
|
314
|
+
def metadata
|
|
315
|
+
@metadata ||= Metadata.new(self)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# --- Internal clients ---
|
|
319
|
+
|
|
320
|
+
def internal_client_name(internal_client)
|
|
321
|
+
pointer = FFI::LibJack.jack_get_internal_client_name(@handle, internal_client)
|
|
322
|
+
return nil if pointer.null?
|
|
323
|
+
|
|
324
|
+
name = pointer.read_string
|
|
325
|
+
FFI::LibJack.jack_free(pointer)
|
|
326
|
+
name
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def internal_client_handle(client_name)
|
|
330
|
+
status_ptr = ::FFI::MemoryPointer.new(:int)
|
|
331
|
+
internal_client = FFI::LibJack.jack_internal_client_handle(@handle, client_name, status_ptr)
|
|
332
|
+
raise Error, "Failed to get internal client handle for '#{client_name}'" if internal_client.zero?
|
|
333
|
+
|
|
334
|
+
internal_client
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def load_internal_client(client_name, load_name: nil, load_init: nil)
|
|
338
|
+
options = FFI::Types::JackNullOption
|
|
339
|
+
options |= FFI::Types::JackLoadName if load_name
|
|
340
|
+
options |= FFI::Types::JackLoadInit if load_init
|
|
341
|
+
|
|
342
|
+
status_ptr = ::FFI::MemoryPointer.new(:int)
|
|
343
|
+
varargs = []
|
|
344
|
+
varargs.push(:string, load_name) if load_name
|
|
345
|
+
varargs.push(:string, load_init) if load_init
|
|
346
|
+
|
|
347
|
+
internal_client = FFI::LibJack.jack_internal_client_load(
|
|
348
|
+
@handle, client_name, options, status_ptr, *varargs
|
|
349
|
+
)
|
|
350
|
+
raise Error, "Failed to load internal client '#{client_name}'" if internal_client.zero?
|
|
351
|
+
|
|
352
|
+
internal_client
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def unload_internal_client(internal_client)
|
|
356
|
+
result = FFI::LibJack.jack_internal_client_unload(@handle, internal_client)
|
|
357
|
+
raise Error, "Failed to unload internal client" unless result.zero?
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
|
|
362
|
+
def register_port(port_name, type, flags, klass)
|
|
363
|
+
port_handle = FFI::LibJack.jack_port_register(@handle, port_name, type, flags, 0)
|
|
364
|
+
raise PortRegistrationFailed, "Failed to register port '#{port_name}'" if port_handle.null?
|
|
365
|
+
|
|
366
|
+
port = klass.new(port_handle, self)
|
|
367
|
+
@ports << port
|
|
368
|
+
port
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
data/lib/jack/error.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jack
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ServerNotRunning < Error; end
|
|
7
|
+
|
|
8
|
+
class ClientOpenFailed < Error
|
|
9
|
+
attr_reader :status
|
|
10
|
+
|
|
11
|
+
def initialize(status)
|
|
12
|
+
@status = status
|
|
13
|
+
super(decode_status(status))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def decode_status(status)
|
|
19
|
+
messages = []
|
|
20
|
+
messages << "Failure" if (status & FFI::Types::JackFailure) != 0
|
|
21
|
+
messages << "Invalid option" if (status & FFI::Types::JackInvalidOption) != 0
|
|
22
|
+
messages << "Name not unique" if (status & FFI::Types::JackNameNotUnique) != 0
|
|
23
|
+
messages << "Server started" if (status & FFI::Types::JackServerStarted) != 0
|
|
24
|
+
messages << "Server failed" if (status & FFI::Types::JackServerFailed) != 0
|
|
25
|
+
messages << "Server error" if (status & FFI::Types::JackServerError) != 0
|
|
26
|
+
messages << "No such client" if (status & FFI::Types::JackNoSuchClient) != 0
|
|
27
|
+
messages << "Load failure" if (status & FFI::Types::JackLoadFailure) != 0
|
|
28
|
+
messages << "Init failure" if (status & FFI::Types::JackInitFailure) != 0
|
|
29
|
+
messages << "SHM failure" if (status & FFI::Types::JackShmFailure) != 0
|
|
30
|
+
messages << "Version error" if (status & FFI::Types::JackVersionError) != 0
|
|
31
|
+
messages << "Backend error" if (status & FFI::Types::JackBackendError) != 0
|
|
32
|
+
messages << "Client zombie" if (status & FFI::Types::JackClientZombie) != 0
|
|
33
|
+
"Failed to open JACK client: #{messages.join(", ")} (status: 0x#{status.to_s(16)})"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class PortRegistrationFailed < Error; end
|
|
38
|
+
class ConnectionFailed < Error; end
|
|
39
|
+
class InvalidOperation < Error; end
|
|
40
|
+
class NotImplementedError < Error; end
|
|
41
|
+
end
|