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.
@@ -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