mb-sound-jackffi 0.0.2.usegit

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f83396720dca4e6728fb1a7a245544aaa5c4eab27b2b769875838f9da7ed26b0
4
+ data.tar.gz: e58f921b2d1523158ffb0cd9390f0365656c224b00ddb9f5ec808fac31e9a448
5
+ SHA512:
6
+ metadata.gz: 24e03a3967758a8fee6d05e9300294200cfe4f812ad239ecc6054ea2d1ac099ba538776b442dfc8c4cadba377ebc18c58701b4b3474252339f8aac278abacef9
7
+ data.tar.gz: '01012439f2c0eeeb1e3b6f76b1a02faf1f480e535da55beb96bf8f909442acb4fcc8b9e67a95b8657d17ef2dcb3c8a3b394d4cd1579a63f32d3765492f36819d'
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .*.swp
10
+ .*.swo
11
+ /*.flac
12
+ /*.wav
13
+ /spec/examples.txt
14
+ Gemfile.lock
@@ -0,0 +1 @@
1
+ mb-jack-ffi
@@ -0,0 +1 @@
1
+ 2.7.2
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2020, Mike Bourgeous
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,75 @@
1
+ # mb-sound-jackffi
2
+
3
+ An *UNSTABLE* (as in it occasionally crashes, hangs, or drops audio) Ruby FFI
4
+ interface for the [JACK Audio Connection Kit][1]. I've only tested this on
5
+ Linux.
6
+
7
+ This FFI interface can be used by my [mb-sound][2] gem, a companion library to
8
+ an [educational video series I'm making about sound][0].
9
+
10
+ ## Rationale
11
+
12
+ The [mb-sound][2] gem uses standalone command line tools for playing and
13
+ recording audio, via `popen`. This works well enough, but has high and
14
+ unpredictable latency. Some of the things I want to do with audio and video
15
+ will require tighter control over latency and synchronization between audio and
16
+ video.
17
+
18
+ There was another FFI interface for JACK, but the homepage no longer exists and
19
+ it hasn't been updated in ages.
20
+
21
+ ## Installation
22
+
23
+ There are some base packages you'll probably want first, though they might not
24
+ be required for your system:
25
+
26
+ ```bash
27
+ # Debian-/Ubuntu-based Linux (macOS/Arch/CentOS will differ)
28
+ sudo apt-get install libffi-dev
29
+ ```
30
+
31
+ Then you'll want to install Ruby 2.7.2 (I recommend [RVM](https://rvm.io)).
32
+
33
+ Finally, you can add the Gem (via Git) to your Gemfile:
34
+
35
+ ```ruby
36
+ # your-project/Gemfile
37
+ gem 'mb-sound-jackffi', git: 'git@github.com:mike-bourgeous/mb-sound-jackffi.git'
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ The `MB::Sound::JackFFI` class represents a connection to a JACK server with a
43
+ specific client name. Its `input` and `output` instance methods will create
44
+ input or output ports on the JACK client.
45
+
46
+ ```ruby
47
+ require 'mb-sound-jackffi' # Or 'mb/sound/jack_ffi'
48
+
49
+ # Enjoy silence
50
+ out = MB::Sound::JackFFI[client_name: 'my app'].output(port_names: ['left', 'right'])
51
+ loop do
52
+ out.write([Numo::SFloat.zeros(out.buffer_size)] * out.channels)
53
+ end
54
+ ```
55
+
56
+ Also check out `bin/loopback.rb` and `bin/invert.rb`.
57
+
58
+ ## Testing
59
+
60
+ Testing is mostly manual. I might eventually figure out a way to install jackd
61
+ in a CI environment and run loopback tests. If that happens, you will be able
62
+ to run the integrated test suite with `rspec`, but not yet.
63
+
64
+ ## Contributing
65
+
66
+ Pull requests are welcome.
67
+
68
+ ## License
69
+
70
+ This project is released under a 2-clause BSD license. See the LICENSE file.
71
+
72
+
73
+ [0]: https://www.youtube.com/playlist?list=PLpRqC8LaADXnwve3e8gI239eDNRO3Nhya
74
+ [1]: https://jackaudio.org
75
+ [2]: https://github.com/mike-bourgeous/mb-sound
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mb-sound-jackffi"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # Passes audio directly from input ports to output ports, with a 180 degree
3
+ # phase inversion.
4
+
5
+ require "bundler/setup"
6
+ require 'mb-sound-jackffi'
7
+
8
+ channels = ARGV[0]&.to_i || 2
9
+ puts "Running with #{channels} channels"
10
+
11
+ jack = MB::Sound::JackFFI[client_name: 'invert']
12
+
13
+ input = jack.input(channels: channels)
14
+ output = jack.output(channels: channels)
15
+
16
+ loop do
17
+ output.write(input.read.map { |c| -(c.inplace) })
18
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # Passes audio directly from input ports to output ports unmodified.
3
+
4
+ require "bundler/setup"
5
+ require 'mb-sound-jackffi'
6
+
7
+ channels = ARGV[0]&.to_i || 2
8
+ puts "Running with #{channels} channels"
9
+
10
+ jack = MB::Sound::JackFFI[client_name: 'loopback']
11
+
12
+ input = jack.input(channels: channels)
13
+ output = jack.output(channels: channels)
14
+
15
+ loop do
16
+ output.write(input.read)
17
+ end
@@ -0,0 +1 @@
1
+ require 'mb/sound/jack_ffi'
@@ -0,0 +1,528 @@
1
+ require 'forwardable'
2
+ require 'numo/narray'
3
+ require 'ffi'
4
+
5
+ module MB
6
+ module Sound
7
+ # This is the base connection to JACK, representing a client_name/server_name
8
+ # pair. Multiple input and output instances may be created for a single
9
+ # client, which will show up as ports on a single JACK client.
10
+ #
11
+ # Examples:
12
+ #
13
+ # # Create two unconnected input ports
14
+ # MB::Sound::JackFFI[].input(channels: 2)
15
+ #
16
+ # # TODO: examples, and make sure they work
17
+ #
18
+ #
19
+ #
20
+ # TODO: Maybe split into separate files
21
+ # TODO: Maybe support environment variables for client name, server name, port names, etc.
22
+ # TODO: Support connecting ports after creating them
23
+ class JackFFI
24
+ # The default size of the buffer queues for communicating between Ruby and
25
+ # JACK. This is separate from JACK's own internal buffers. The
26
+ # :queue_size parameter to #input and #output allows overriding these
27
+ # defaults.
28
+ INPUT_QUEUE_SIZE = 2
29
+ OUTPUT_QUEUE_SIZE = 2
30
+
31
+ # Raw FFI interface to JACK. Don't use this directly; instead use
32
+ # JackFFIInput and JackFFIOutput, which will use JackFFI[] to retrieve a
33
+ # connection.
34
+ #
35
+ # References:
36
+ #
37
+ # https://github.com/jackaudio/jack2/blob/b2ba349a4eb4c9a5a51dbc7a7af487851ade8cba/example-clients/simple_client.c
38
+ # https://jackaudio.org/api/simple__client_8c.html#a0ddf1224851353fc92bfbff6f499fa97
39
+ # https://github.com/ffi/ffi/blob/6d31bf845e6527cc7f67d236a95c0161df969b12/lib/ffi/library.rb#L515
40
+ # https://github.com/ffi/ffi/blob/f7c5b607e07b7f00e3c7a46f427c76cad65fbb78/ext/ffi_c/FunctionInfo.c
41
+ # https://github.com/ffi/ffi/wiki/Pointers
42
+ module Jack
43
+ extend FFI::Library
44
+ ffi_lib ['jack', 'libjack.so.0.1.0', 'libjack.so.0']
45
+
46
+ AUDIO_TYPE = "32 bit float mono audio"
47
+
48
+ @blocking = true
49
+
50
+ bitmask :jack_options_t, [
51
+ :JackNoStartServer,
52
+ :JackUseExactName,
53
+ :JackServerName,
54
+ :JackLoadName,
55
+ :JackLoadInit,
56
+ :JackSessionID,
57
+ ]
58
+
59
+ bitmask :jack_status_t, [
60
+ :JackFailure,
61
+ :JackInvalidOption,
62
+ :JackNameNotUnique,
63
+ :JackServerStarted,
64
+ :JackServerFailed,
65
+ :JackServerError,
66
+ :JackNoSuchClient,
67
+ :JackLoadFailure,
68
+ :JackInitFailure,
69
+ :JackShmFailure,
70
+ :JackVersionError,
71
+ :JackBackendError,
72
+ :JackClientZombie,
73
+ ]
74
+
75
+ # jack_port_register accepts "unsigned long" for some reason, so make sure this is the right size
76
+ bitmask FFI::Type::ULONG, :jack_port_flags, [
77
+ :JackPortIsInput,
78
+ :JackPortIsOutput,
79
+ :JackPortIsPhysical,
80
+ :JackPortCanMonitor,
81
+ :JackPortIsTerminal,
82
+ ]
83
+
84
+ class JackStatusWrapper < FFI::Struct
85
+ layout :status, :jack_status_t
86
+ end
87
+
88
+ typedef :uint32_t, :jack_nframes_t
89
+
90
+ # Client management functions
91
+ # Note: jack_deactivate, or if you don't call that, jack_client_close,
92
+ # cause Ruby/FFI to perform invalid reads often, and sometimes crash.
93
+ # It's probably okay to leave the connection to JACK open and let the
94
+ # OS clean up when the application exits.
95
+ typedef :pointer, :jack_client
96
+ attach_function :jack_client_open, [:string, :jack_options_t, JackStatusWrapper.by_ref, :varargs], :jack_client
97
+ attach_function :jack_client_close, [:jack_client], :int
98
+ attach_function :jack_get_client_name, [:jack_client], :string
99
+ attach_function :jack_activate, [:jack_client], :int
100
+ attach_function :jack_deactivate, [:jack_client], :int
101
+
102
+ # Server status functions
103
+ attach_function :jack_get_buffer_size, [:jack_client], :jack_nframes_t
104
+ attach_function :jack_get_sample_rate, [:jack_client], :jack_nframes_t
105
+
106
+ # Callback functions
107
+ typedef :pointer, :jack_user_data
108
+ callback :jack_process_callback, [:jack_nframes_t, :jack_user_data], :void
109
+ callback :jack_shutdown_callback, [:jack_user_data], :void
110
+ attach_function :jack_set_process_callback, [:jack_client, :jack_process_callback, :jack_user_data], :int
111
+ attach_function :jack_on_shutdown, [:jack_client, :jack_shutdown_callback, :jack_user_data], :void
112
+
113
+ # Port management functions
114
+ typedef :pointer, :jack_port
115
+ attach_function :jack_port_register, [:jack_client, :string, :string, :jack_port_flags, :ulong], :jack_port
116
+ attach_function :jack_port_unregister, [:jack_client, :jack_port], :int
117
+ attach_function :jack_port_get_buffer, [:jack_port, :jack_nframes_t], :pointer
118
+ end
119
+
120
+ # Returned by JackFFI#input. E.g. use JackFFI[client_name: 'my
121
+ # client'].input(channels: 2) to get two input ports on the client.
122
+ class Input
123
+ extend Forwardable
124
+
125
+ def_delegators :@jack_ffi, :buffer_size, :rate
126
+
127
+ attr_reader :channels, :ports
128
+
129
+ # Called by JackFFI to initialize an audio input handle. You generally
130
+ # won't use this constructor directly. Instead use JackFFI#input.
131
+ #
132
+ # +:jack_ffi+ - The JackFFI instance that contains this input.
133
+ # +:ports+ - An Array of JACK port names.
134
+ def initialize(jack_ffi:, ports:)
135
+ @jack_ffi = jack_ffi
136
+ @ports = ports
137
+ @channels = ports.length
138
+ end
139
+
140
+ # Removes this input object's ports from the client.
141
+ def close
142
+ @jack_ffi.remove(self)
143
+ end
144
+
145
+ # Reads one #buffer_size buffer of frames as an Array of Numo::SFloat.
146
+ # Any frame count parameter is ignored, as JACK operates in lockstep with
147
+ # a fixed buffer size. The returned Array will have one element for each
148
+ # input port.
149
+ def read(_ignored = nil)
150
+ @jack_ffi.read_ports(@ports)
151
+ end
152
+ end
153
+
154
+ # Returned by JackFFI#output. E.g. use JackFFI[client_name: 'my
155
+ # client'].output(channels: 2) to get two output ports on the client.
156
+ class Output
157
+ extend Forwardable
158
+
159
+ def_delegators :@jack_ffi, :buffer_size, :rate
160
+
161
+ attr_reader :channels, :ports
162
+
163
+ # Called by JackFFI to initialize an audio output handle. You generally
164
+ # won't use this constructor directly. Instead use JackFFI#output.
165
+ #
166
+ # +:jack_ffi+ - The JackFFI instance that contains this output.
167
+ # +:ports+ - An Array of JACK port names.
168
+ def initialize(jack_ffi:, ports:)
169
+ @jack_ffi = jack_ffi
170
+ @ports = ports
171
+ @channels = ports.length
172
+ end
173
+
174
+ # Removes this output object's ports from the client.
175
+ def close
176
+ @jack_ffi.remove(self)
177
+ end
178
+
179
+ # Writes the given Array of data (Numo::SFloat recommended). The Array
180
+ # should contain one element for each output port.
181
+ def write(data)
182
+ @jack_ffi.write_ports(@ports, data)
183
+ end
184
+ end
185
+
186
+ # Retrieves a base client instance for the given client name and server
187
+ # name.
188
+ #
189
+ # Note that if there is already a client with the given name connected to
190
+ # JACK, the client name will be changed by JACK. Use JackFFI#client_name
191
+ # to get the true client name if needed.
192
+ def self.[](client_name: 'ruby', server_name: nil)
193
+ @instances ||= {}
194
+ @instances[name] ||= new(client_name: client_name, server_name: server_name)
195
+ end
196
+
197
+ # Internal API called by JackFFI#close. Removes an instance of JackFFI
198
+ # that is no longer functioning, so that future calls to JackFFI[] will
199
+ # create a new connection.
200
+ def self.remove(jack_ffi)
201
+ @instances.reject! { |k, v| v == jack_ffi }
202
+ end
203
+
204
+ attr_reader :client_name, :server_name, :buffer_size, :rate
205
+
206
+ # Generally you don't need to create a JackFFI instance yourself. Instead,
207
+ # use JackFFI[] (the array indexing operator) to retrieve a connection, and
208
+ # JackFFI#input and JackFFI#output to get an input or output object with a
209
+ # read or write method.
210
+ #
211
+ # You might want to use this class directly if you want to override the
212
+ # #process method to run custom code in the JACK realtime thread instead of
213
+ # reading and writing data through JackFFIInput and JackFFIOutput.
214
+ #
215
+ # Every JackFFI instance lives until the Ruby VM exits, because JACK's
216
+ # callback APIs cause invalid memory accesses (and thus crashes) in the FFI
217
+ # library when the JACK C client is shut down.
218
+ def initialize(client_name: 'ruby', server_name: nil)
219
+ @client_name = client_name || 'ruby'
220
+ @server_name = server_name
221
+
222
+ @run = true
223
+
224
+ # Port maps use the port name as key, with a Hash as value. See #create_io.
225
+ @input_ports = {}
226
+ @output_ports = {}
227
+
228
+ # Montonically increasing indices used to number prefix-named ports.
229
+ @port_indices = {
230
+ JackPortIsInput: 0,
231
+ JackPortIsOutput: 0,
232
+ }
233
+
234
+ @init_mutex = Mutex.new
235
+
236
+ @init_mutex.synchronize {
237
+ status = Jack::JackStatusWrapper.new
238
+ @client = Jack.jack_client_open(
239
+ client_name,
240
+ server_name ? :JackServerName : 0,
241
+ status,
242
+ :string, server_name
243
+ )
244
+
245
+ if @client.nil? || @client.null?
246
+ raise "Failed to open JACK client; status: #{status[:status]}"
247
+ end
248
+
249
+ if status[:status].include?(:JackServerStarted)
250
+ log "Server was started as a result of trying to connect"
251
+ end
252
+
253
+ @client_name = Jack.jack_get_client_name(@client)
254
+ if status[:status].include?(:JackNameNotUnique)
255
+ log "Server assigned a new client name (replacing #{client_name.inspect}): #{@client_name.inspect}"
256
+ end
257
+
258
+ @buffer_size = Jack.jack_get_buffer_size(@client)
259
+ @rate = Jack.jack_get_sample_rate(@client)
260
+ @zero = Numo::SFloat.zeros(@buffer_size)
261
+
262
+ @process_handle = method(:process) # Assigned to variable to prevent GC
263
+ result = Jack.jack_set_process_callback(@client, @process_handle, nil)
264
+ raise "Error setting JACK process callback: #{result}" if result != 0
265
+
266
+ @shutdown_handle = method(:shutdown)
267
+ Jack.jack_on_shutdown(@client, @shutdown_handle, nil)
268
+
269
+ # TODO: Maybe set a buffer size callback
270
+
271
+ result = Jack.jack_activate(@client)
272
+ raise "Error activating JACK client: #{result}" if result != 0
273
+ }
274
+
275
+ rescue Exception
276
+ close if @client
277
+ raise
278
+ end
279
+
280
+ # Returns a new JackFFI::Input and creates corresponding new input ports on
281
+ # the JACK client.
282
+ #
283
+ # If +:port_names+ is a String, then it is used as a prefix to create
284
+ # +channels+ numbered ports. If +:port_names+ is an Array of Strings, then
285
+ # those port names will be created directly without numbering.
286
+ #
287
+ # Port names must be unique.
288
+ #
289
+ # +:channels+ - The number of ports to create if +:port_names+ is a String.
290
+ # +:port_names+ - A String (without a trailing underscore) to create
291
+ # prefixed and numbered ports, or an Array of Strings to
292
+ # create a list of ports directly by name.
293
+ # +:connections+ - TODO (maybe String client name, maybe list of ports)
294
+ # +:queue_size+ - Optional: number of audio buffers to hold between Ruby
295
+ # and the JACK thread (higher means more latency but less
296
+ # risk of dropouts). Default is INPUT_QUEUE_SIZE. Sane
297
+ # values range from 1 to 4.
298
+ def input(channels: nil, port_names: 'in', connections: nil, queue_size: nil)
299
+ create_io(
300
+ channels: channels,
301
+ port_names: port_names,
302
+ connections: connections,
303
+ portmap: @input_ports,
304
+ jack_direction: :JackPortIsInput,
305
+ queue_size: queue_size || INPUT_QUEUE_SIZE,
306
+ io_class: Input
307
+ )
308
+ end
309
+
310
+ # Returns a new JackFFI::Input and creates corresponding new input ports on
311
+ # the JACK client.
312
+ #
313
+ # Parameters are the same as for #input, with the default for +:queue_size+
314
+ # being OUTPUT_QUEUE_SIZE.
315
+ def output(channels: nil, port_names: 'out', connections: nil, queue_size: nil)
316
+ create_io(
317
+ channels: channels,
318
+ port_names: port_names,
319
+ connections: connections,
320
+ portmap: @output_ports,
321
+ jack_direction: :JackPortIsOutput,
322
+ queue_size: queue_size || OUTPUT_QUEUE_SIZE,
323
+ io_class: Output
324
+ )
325
+ end
326
+
327
+ # Internal API used by JackFFI::Input#close and JackFFI::Output#close.
328
+ # Removes all of a given input's or output's ports from the client.
329
+ def remove(input_or_output)
330
+ case input_or_output
331
+ when Input
332
+ portmap = @input_ports
333
+
334
+ when Output
335
+ portmap = @output_ports
336
+ end
337
+
338
+ input_or_output.ports.each do |name|
339
+ port_info = portmap.delete(name)
340
+ if port_info
341
+ result = Jack.jack_port_unregister(@client, port_info[:port_id])
342
+ log "Error unregistering port #{port_info[:name]}: #{result}" if result != 0
343
+ end
344
+ end
345
+ end
346
+
347
+ # This generally doesn't need to be called. This method stops background
348
+ # processing, but the JACK thread continues to run because stopping it
349
+ # often causes Ruby to crash with SIGSEGV (Valgrind shows invalid reads
350
+ # when FFI invokes the process callback after jack_deactivate starts).
351
+ def close
352
+ @init_mutex&.synchronize {
353
+ @run = false
354
+ JackFFI.remove(self)
355
+ }
356
+ end
357
+
358
+ # Writes the given +data+ to the ports represented by the given Array of
359
+ # port IDs. Used internally by JackFFI::Output.
360
+ def write_ports(ports, data)
361
+ raise "JACK connection is closed" unless @run
362
+
363
+ check_for_processing_error
364
+
365
+ # TODO: Maybe support different write sizes by writing into big ring buffers
366
+ raise 'Must supply the same number of data arrays as ports' unless ports.length == data.length
367
+ raise "Output buffer must be #{@buffer_size} samples long" unless data.all? { |c| c.length == @buffer_size }
368
+
369
+ ports.each_with_index do |name, idx|
370
+ @output_ports[name][:queue].push(data[idx])
371
+ end
372
+
373
+ nil
374
+ end
375
+
376
+ # Reads one buffer_size chunk of data for the given Array of port IDs.
377
+ # This is generally for internal use by the JackFFI::Input class.
378
+ def read_ports(ports)
379
+ raise "JACK connection is closed" unless @run
380
+
381
+ check_for_processing_error
382
+
383
+ ports.map { |name|
384
+ @input_ports[name][:queue].pop
385
+ }
386
+ end
387
+
388
+ private
389
+
390
+ # Common code for creating ports shared by #input and #output. API subject to change.
391
+ def create_io(channels:, port_names:, connections:, portmap:, jack_direction:, queue_size:, io_class:)
392
+ raise "Queue size must be positive" if queue_size <= 0
393
+
394
+ case port_names
395
+ when Array
396
+ raise "Do not specify a channel count when an array of port names is given" if channels
397
+
398
+ when String
399
+ raise "Channel count must be given for prefix-named ports" unless channels.is_a?(Integer)
400
+
401
+ port_names = channels.times.map { |c|
402
+ "#{port_names}_#{@port_indices[jack_direction]}".tap { @port_indices[jack_direction] += 1 }
403
+ }
404
+
405
+ else
406
+ raise "Pass a String or an Array of Strings for :port_names (received #{port_names.class})"
407
+ end
408
+
409
+ port_names.each do |name|
410
+ raise "Port #{name} already exists" if portmap.include?(name)
411
+ end
412
+
413
+ # Use a separate array so that ports can be cleaned up if a later port
414
+ # fails to initialize.
415
+ ports = []
416
+
417
+ # TODO: if having one SizedQueue per port is too slow, maybe have one SizedQueue per IO object
418
+
419
+ io = io_class.new(jack_ffi: self, ports: port_names)
420
+
421
+ port_names.each do |name|
422
+ port_id = Jack.jack_port_register(@client, name, Jack::AUDIO_TYPE, jack_direction, 0)
423
+ if port_id.nil? || port_id.null?
424
+ ports.each do |p|
425
+ Jack.jack_port_unregister(@client, p[:port])
426
+ end
427
+
428
+ raise "Error creating port #{name}"
429
+ end
430
+
431
+ ports << {
432
+ name: name,
433
+ io: io,
434
+ port_id: port_id,
435
+ queue: SizedQueue.new(queue_size),
436
+ drops: 0
437
+ }
438
+ end
439
+
440
+ ports.each do |port_info|
441
+ portmap[port_info[:name]] = port_info
442
+ end
443
+
444
+ # TODO Connections
445
+ raise NotImplementedError if connections
446
+
447
+ io
448
+ end
449
+
450
+ def log(msg)
451
+ puts "JackFFI(#{@server_name}/#{@client_name}): #{msg}"
452
+ end
453
+
454
+ def check_for_processing_error
455
+ if @processing_error
456
+ # Re-raise the error so we can set it as the cause on another error
457
+ e = @processing_error
458
+ @processing_error = nil
459
+ begin
460
+ raise e
461
+ rescue
462
+ raise "An error occurred in the processing thread: #{e.message}"
463
+ end
464
+ end
465
+ end
466
+
467
+ # Called by JACK within its realtime thread when new audio data should be
468
+ # read and written. Only the bare minimum of processing should be done
469
+ # here (and really, Ruby itself is not ideal for realtime use).
470
+ def process(frames, user_data)
471
+ @init_mutex&.synchronize {
472
+ return unless @run && @client && @input_ports && @output_ports
473
+
474
+ @input_ports.each do |name, port_info|
475
+ # FIXME: Avoid allocation in this function; use a buffer pool or something
476
+ buf = Jack.jack_port_get_buffer(port_info[:port_id], frames)
477
+
478
+ queue = port_info[:queue]
479
+
480
+ if queue.length == queue.max
481
+ log "Input port #{name} buffer queue is full" if port_info[:drops] == 0
482
+ queue.pop rescue nil
483
+ port_info[:drops] += 1
484
+ else
485
+ log "Input port #{name} buffer queue recovered after #{port_info[:drops]} dropped buffers" if port_info[:drops] > 0
486
+ port_info[:drops] = 0
487
+ end
488
+
489
+ queue.push(Numo::SFloat.from_binary(buf.read_bytes(frames * 4)), true)
490
+ end
491
+
492
+ @output_ports.each do |name, port_info|
493
+ queue = port_info[:queue]
494
+ data = queue.pop(true) rescue nil unless queue.empty?
495
+ if data.nil?
496
+ log "Output port #{name} ran out of data to write" if port_info[:drops] == 0
497
+ port_info[:drops] += 1
498
+ data = @zero
499
+ else
500
+ log "Output port #{name} recovered after #{port_info[:drops]} dropped buffers" if port_info[:drops] > 0
501
+ port_info[:drops] = 0
502
+ end
503
+
504
+ buf = Jack.jack_port_get_buffer(port_info[:port_id], frames)
505
+ buf.write_bytes(data.to_binary)
506
+ end
507
+ }
508
+ rescue => e
509
+ @processing_error = e
510
+ log "Error processing: #{e}"
511
+ end
512
+
513
+ # Called when either the JACK server is shut down, or a severe enough
514
+ # client error occurs that JACK kicks the client out of the server.
515
+ def shutdown(user_data)
516
+ return unless @client
517
+
518
+ log "JACK is shutting down"
519
+ @run = false
520
+
521
+ # Can't close JACK from within its own shutdown callback
522
+ Thread.new do sleep 0.25; close end
523
+ rescue
524
+ nil
525
+ end
526
+ end
527
+ end
528
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "mb-sound-jackffi"
5
+ spec.version = '0.0.2.usegit'
6
+ spec.authors = ["Mike Bourgeous"]
7
+ spec.email = ["mike@mikebourgeous.com"]
8
+
9
+ spec.summary = %q{A Ruby FFI interface to the JACK Audio Connection Kit}
10
+ spec.homepage = "https://github.com/mike-bourgeous/mb-sound-jackffi"
11
+
12
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
13
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
14
+ if spec.respond_to?(:metadata)
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ else
19
+ raise "RubyGems 2.0 or newer is required to protect against " \
20
+ "public gem pushes."
21
+ end
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
+ f.match(%r{^(test|spec|features)/})
25
+ end
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 2.1.0"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+
31
+ spec.add_runtime_dependency 'numo-narray', '~> 0.9.1'
32
+ spec.add_runtime_dependency 'ffi', '~> 1.13.0'
33
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mb-sound-jackffi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.usegit
5
+ platform: ruby
6
+ authors:
7
+ - Mike Bourgeous
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.1.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: numo-narray
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.9.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.9.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: ffi
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.13.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.13.0
69
+ description:
70
+ email:
71
+ - mike@mikebourgeous.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".ruby-gemset"
78
+ - ".ruby-version"
79
+ - Gemfile
80
+ - LICENSE
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/invert.rb
85
+ - bin/loopback.rb
86
+ - lib/mb-sound-jackffi.rb
87
+ - lib/mb/sound/jack_ffi.rb
88
+ - mb-sound-jackffi.gemspec
89
+ homepage: https://github.com/mike-bourgeous/mb-sound-jackffi
90
+ licenses: []
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ homepage_uri: https://github.com/mike-bourgeous/mb-sound-jackffi
94
+ source_code_uri: https://github.com/mike-bourgeous/mb-sound-jackffi
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">"
107
+ - !ruby/object:Gem::Version
108
+ version: 1.3.1
109
+ requirements: []
110
+ rubygems_version: 3.1.4
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: A Ruby FFI interface to the JACK Audio Connection Kit
114
+ test_files: []