mb-sound-jackffi 0.0.2.usegit

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,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: []