Class: Deftones::Source::UserMedia

Inherits:
Core::Source show all
Defined in:
lib/deftones/source/user_media.rb

Defined Under Namespace

Classes: DeviceInfo, PortAudioCapture

Instance Attribute Summary collapse

Attributes inherited from Core::Source

#mute, #onstop, #volume

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Core::Source

#active_at?, #apply_volume!, #cancel_stop, #clear_transport_event, #mute?, #notify_stop_in_window, #number_of_inputs, #render, #render_block, #resolve_time, #resolve_transport_time, #restart, #schedule_transport_event, #source_type, #sync, #synced?, #unsync, #uses_legacy_render_for_block?

Constructor Details

#initialize(buffer: nil, provider: nil, loop: false, live: false, capture_backend: nil, device_id: nil, group_id: nil, label: nil, channels: nil, context: Deftones.context) ⇒ UserMedia

Returns a new instance of UserMedia.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/deftones/source/user_media.rb', line 10

def initialize(buffer: nil, provider: nil, loop: false, live: false, capture_backend: nil,
               device_id: nil, group_id: nil, label: nil, channels: nil, context: Deftones.context)
  super(context: context)
  @buffer = normalize_buffer(buffer)
  @provider = normalize_provider(provider)
  @loop = loop
  @live_requested = live || !capture_backend.nil?
  @capture_backend = normalize_capture_backend(capture_backend, live, channels)
  @device_id = device_id
  @group_id = group_id
  @label = label
  @channels = normalize_channel_count(channels)
  @sample_cursor = 0
  @provider_exhausted = false
  @underflow_count = 0
  @overflow_count = 0
  @opened = false
end

Instance Attribute Details

#bufferObject (readonly)

Returns the value of attribute buffer.



8
9
10
# File 'lib/deftones/source/user_media.rb', line 8

def buffer
  @buffer
end

#device_idObject (readonly)

Returns the value of attribute device_id.



8
9
10
# File 'lib/deftones/source/user_media.rb', line 8

def device_id
  @device_id
end

#group_idObject (readonly)

Returns the value of attribute group_id.



8
9
10
# File 'lib/deftones/source/user_media.rb', line 8

def group_id
  @group_id
end

#labelObject (readonly)

Returns the value of attribute label.



8
9
10
# File 'lib/deftones/source/user_media.rb', line 8

def label
  @label
end

#providerObject (readonly)

Returns the value of attribute provider.



8
9
10
# File 'lib/deftones/source/user_media.rb', line 8

def provider
  @provider
end

Class Method Details

.build_device_info(device) ⇒ Object (private)



166
167
168
169
170
171
172
173
174
# File 'lib/deftones/source/user_media.rb', line 166

def build_device_info(device)
  DeviceInfo.new(
    device_id: extract_device_id(device),
    label: exposed_device_label(device),
    group_id: exposed_device_group_id(device),
    input_channels: extract_device_channels(device, :input),
    output_channels: extract_device_channels(device, :output)
  )
end

.enumerate_devicesObject Also known as: enumerateDevices



140
141
142
# File 'lib/deftones/source/user_media.rb', line 140

def enumerate_devices
  input_devices
end

.exposed_device_group_id(device) ⇒ Object (private)



203
204
205
# File 'lib/deftones/source/user_media.rb', line 203

def exposed_device_group_id(device)
  permissions_granted? ? extract_device_group_id(device) : nil
end

.exposed_device_label(device) ⇒ Object (private)



199
200
201
# File 'lib/deftones/source/user_media.rb', line 199

def exposed_device_label(device)
  permissions_granted? ? extract_device_label(device) : ""
end

.extract_device_channels(device, direction) ⇒ Object (private)



207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/deftones/source/user_media.rb', line 207

def extract_device_channels(device, direction)
  methods =
    case direction
    when :input then %i[max_input_channels input_channels channels]
    when :output then %i[max_output_channels output_channels channels]
    else []
    end

  methods.each do |method_name|
    return device.public_send(method_name).to_i if device.respond_to?(method_name)
  end

  0
end

.extract_device_group_id(device) ⇒ Object (private)



191
192
193
194
195
196
197
# File 'lib/deftones/source/user_media.rb', line 191

def extract_device_group_id(device)
  return device.group_id if device.respond_to?(:group_id)
  return device.host_api if device.respond_to?(:host_api)
  return device.host_api_name if device.respond_to?(:host_api_name)

  nil
end

.extract_device_id(device) ⇒ Object (private)



176
177
178
179
180
181
182
# File 'lib/deftones/source/user_media.rb', line 176

def extract_device_id(device)
  return device.device_id if device.respond_to?(:device_id)
  return device.index if device.respond_to?(:index)
  return device.device_index if device.respond_to?(:device_index)

  extract_device_label(device)
end

.extract_device_label(device) ⇒ Object (private)



184
185
186
187
188
189
# File 'lib/deftones/source/user_media.rb', line 184

def extract_device_label(device)
  return device.label if device.respond_to?(:label)
  return device.name if device.respond_to?(:name)

  device.to_s
end

.grant_permissions!Object



119
120
121
122
# File 'lib/deftones/source/user_media.rb', line 119

def grant_permissions!
  @permission_state = :granted
  self
end

.input_devicesObject



133
134
135
136
137
138
# File 'lib/deftones/source/user_media.rb', line 133

def input_devices
  portaudio_devices.filter_map do |device|
    info = build_device_info(device)
    info if info.input_channels.positive?
  end
end

.permission_stateObject Also known as: permissionState



115
116
117
# File 'lib/deftones/source/user_media.rb', line 115

def permission_state
  @permission_state ||= :prompt
end

.permissions_granted?Boolean (private)

Returns:

  • (Boolean)


150
151
152
# File 'lib/deftones/source/user_media.rb', line 150

def permissions_granted?
  permission_state == :granted
end

.portaudio_devicesObject (private)



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/deftones/source/user_media.rb', line 154

def portaudio_devices
  return [] unless Deftones.portaudio_available?
  return [] unless defined?(PortAudio::Device)

  return Array(PortAudio::Device.all) if PortAudio::Device.respond_to?(:all)
  return Array(PortAudio::Device.devices) if PortAudio::Device.respond_to?(:devices)

  []
rescue StandardError
  []
end

.reset_permissions!Object



124
125
126
127
# File 'lib/deftones/source/user_media.rb', line 124

def reset_permissions!
  @permission_state = :prompt
  self
end

.supported?Boolean Also known as: supported

Returns:

  • (Boolean)


129
130
131
# File 'lib/deftones/source/user_media.rb', line 129

def supported?
  true
end

Instance Method Details

#backend_stat(name) ⇒ Object (private)



436
437
438
439
440
# File 'lib/deftones/source/user_media.rb', line 436

def backend_stat(name)
  return 0 unless @capture_backend.respond_to?(name)

  @capture_backend.public_send(name).to_i
end

#capture_channelsObject (private)



243
244
245
246
247
248
# File 'lib/deftones/source/user_media.rb', line 243

def capture_channels
  return @buffer.channels if @buffer
  return normalize_channel_count(@capture_backend.channels) if @capture_backend.respond_to?(:channels)

  @channels
end

#close(time = nil) ⇒ Object



51
52
53
54
55
56
# File 'lib/deftones/source/user_media.rb', line 51

def close(time = nil)
  stop(time)
  @capture_backend&.close if @capture_backend.respond_to?(:close)
  @opened = false
  self
end

#disposeObject



109
110
111
112
# File 'lib/deftones/source/user_media.rb', line 109

def dispose
  close
  super
end

#live?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/deftones/source/user_media.rb', line 66

def live?
  @live_requested
end

#mark_capture_frame_underflow!Object (private)



431
432
433
434
# File 'lib/deftones/source/user_media.rb', line 431

def mark_capture_frame_underflow!
  @underflow_count += 1
  Array.new(capture_channels, 0.0)
end

#mark_provider_exhausted!Object (private)



420
421
422
423
424
# File 'lib/deftones/source/user_media.rb', line 420

def mark_provider_exhausted!
  @provider_exhausted = true
  @opened = false
  mark_underflow!
end

#mark_underflow!Object (private)



426
427
428
429
# File 'lib/deftones/source/user_media.rb', line 426

def mark_underflow!
  @underflow_count += 1
  0.0
end

#multichannel_process?Boolean (private)

Returns:

  • (Boolean)


239
240
241
# File 'lib/deftones/source/user_media.rb', line 239

def multichannel_process?
  (@buffer && @buffer.channels > 1) || capture_channels > 1
end

#next_capture_frameObject (private)



369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/deftones/source/user_media.rb', line 369

def next_capture_frame
  return Array.new(capture_channels, 0.0) unless @capture_backend

  frame = if @capture_backend.respond_to?(:next_frame)
    @capture_backend.next_frame
  else
    @capture_backend.next_sample
  end
  return mark_capture_frame_underflow! if frame.nil?

  frame = [frame] unless frame.is_a?(Array)
  normalized = frame.map(&:to_f)
  normalized.fill(0.0, normalized.length...capture_channels)
end

#next_capture_sampleObject (private)



384
385
386
387
388
389
390
391
# File 'lib/deftones/source/user_media.rb', line 384

def next_capture_sample
  return 0.0 unless @capture_backend

  sample = @capture_backend.next_sample
  return mark_underflow! if sample.nil?

  sample.to_f
end

#next_enumerator_sampleObject (private)



407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/deftones/source/user_media.rb', line 407

def next_enumerator_sample
  @provider.next
rescue StopIteration
  return mark_provider_exhausted! unless @loop && @provider.respond_to?(:rewind)

  begin
    @provider.rewind
    @provider.next
  rescue StopIteration
    mark_provider_exhausted!
  end
end

#next_provider_sampleObject (private)



393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/deftones/source/user_media.rb', line 393

def next_provider_sample
  return 0.0 unless @provider

  sample = if @provider.respond_to?(:call)
    @provider.call(@sample_cursor)
  else
    next_enumerator_sample
  end
  @sample_cursor += 1
  return mark_underflow! if sample.nil?

  sample.to_f
end

#normalize_buffer(buffer) ⇒ Object (private)



250
251
252
253
254
# File 'lib/deftones/source/user_media.rb', line 250

def normalize_buffer(buffer)
  return if buffer.nil?

  buffer.is_a?(IO::Buffer) ? buffer : IO::Buffer.load(buffer)
end

#normalize_capture_backend(capture_backend, live, channels) ⇒ Object (private)



263
264
265
266
267
268
269
270
271
272
# File 'lib/deftones/source/user_media.rb', line 263

def normalize_capture_backend(capture_backend, live, channels)
  return capture_backend if capture_backend
  return unless live && Deftones.portaudio_available?

  PortAudioCapture.new(
    sample_rate: context.sample_rate,
    buffer_size: context.buffer_size,
    channels: normalize_channel_count(channels || context.channels)
  )
end

#normalize_channel_count(channels) ⇒ Object (private)



274
275
276
# File 'lib/deftones/source/user_media.rb', line 274

def normalize_channel_count(channels)
  [channels.to_i, 1].max
end

#normalize_provider(provider) ⇒ Object (private)



256
257
258
259
260
261
# File 'lib/deftones/source/user_media.rb', line 256

def normalize_provider(provider)
  return if provider.nil?
  return provider if provider.respond_to?(:call)

  provider.to_enum
end

#open(time = nil, device_id: nil, group_id: nil, label: nil) ⇒ Object



41
42
43
44
45
46
47
48
49
# File 'lib/deftones/source/user_media.rb', line 41

def open(time = nil, device_id: nil, group_id: nil, label: nil)
  @device_id = device_id unless device_id.nil?
  @group_id = group_id unless group_id.nil?
  @label = label unless label.nil?
  open_capture_backend!
  sync_capture_metadata!
  self.class.grant_permissions!
  start(time)
end

#open_capture_backend!Object (private)



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/deftones/source/user_media.rb', line 278

def open_capture_backend!
  return self unless live?

  raise Deftones::MissingRealtimeBackendError, "UserMedia live capture backend is unavailable" unless @capture_backend

  if @capture_backend.respond_to?(:open)
    @capture_backend.open(
      device_id: @device_id,
      group_id: @group_id,
      label: @label,
      channels: capture_channels
    )
  end

  self
end

#opened?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/deftones/source/user_media.rb', line 70

def opened?
  @opened
end

#overflow_countObject Also known as: overflowCount



82
83
84
# File 'lib/deftones/source/user_media.rb', line 82

def overflow_count
  @overflow_count + backend_stat(:overflow_count)
end

#permission_stateObject



93
94
95
# File 'lib/deftones/source/user_media.rb', line 93

def permission_state
  self.class.permission_state
end

#permissionStateObject



97
98
99
# File 'lib/deftones/source/user_media.rb', line 97

def permission_state
  self.class.permission_state
end

#process(_input_buffer, num_frames, start_frame, _cache) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/deftones/source/user_media.rb', line 223

def process(_input_buffer, num_frames, start_frame, _cache)
  return render_buffer_block(num_frames, start_frame) if @buffer && @buffer.channels > 1
  return render_capture_block(num_frames, start_frame) if @capture_backend && capture_channels > 1
  return render_buffer(num_frames, start_frame) if @buffer
  return render_capture(num_frames, start_frame) if @capture_backend

  Array.new(num_frames) do |index|
    current_time = (start_frame + index).to_f / context.sample_rate
    next 0.0 unless active_at?(current_time)

    next_provider_sample
  end
end

#provider_exhausted?Boolean Also known as: providerExhausted

Returns:

  • (Boolean)


74
75
76
# File 'lib/deftones/source/user_media.rb', line 74

def provider_exhausted?
  @provider_exhausted
end

#render_buffer(num_frames, start_frame) ⇒ Object (private)



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/deftones/source/user_media.rb', line 304

def render_buffer(num_frames, start_frame)
  Array.new(num_frames) do |index|
    current_time = (start_frame + index).to_f / context.sample_rate
    next 0.0 unless active_at?(current_time)

    sample_position = (current_time - @start_time) * @buffer.sample_rate
    if @loop && @buffer.frames.positive?
      sample_position %= @buffer.frames
    elsif sample_position >= @buffer.frames
      @opened = false
      next 0.0
    end

    @buffer.sample_at(sample_position)
  end
end

#render_buffer_block(num_frames, start_frame) ⇒ Object (private)



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/deftones/source/user_media.rb', line 321

def render_buffer_block(num_frames, start_frame)
  output = Array.new(@buffer.channels) { Array.new(num_frames, 0.0) }

  num_frames.times do |index|
    current_time = (start_frame + index).to_f / context.sample_rate
    next unless active_at?(current_time)

    sample_position = (current_time - @start_time) * @buffer.sample_rate
    if @loop && @buffer.frames.positive?
      sample_position %= @buffer.frames
    elsif sample_position >= @buffer.frames
      @opened = false
      next
    end

    @buffer.channels.times do |channel_index|
      output[channel_index][index] = @buffer.sample_at(sample_position, channel_index)
    end
  end

  Core::AudioBlock.from_channel_data(output)
end

#render_capture(num_frames, start_frame) ⇒ Object (private)



344
345
346
347
348
349
350
351
# File 'lib/deftones/source/user_media.rb', line 344

def render_capture(num_frames, start_frame)
  Array.new(num_frames) do |index|
    current_time = (start_frame + index).to_f / context.sample_rate
    next 0.0 unless active_at?(current_time)

    next_capture_sample
  end
end

#render_capture_block(num_frames, start_frame) ⇒ Object (private)



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/deftones/source/user_media.rb', line 353

def render_capture_block(num_frames, start_frame)
  output = Array.new(capture_channels) { Array.new(num_frames, 0.0) }

  num_frames.times do |index|
    current_time = (start_frame + index).to_f / context.sample_rate
    next unless active_at?(current_time)

    frame = next_capture_frame
    capture_channels.times do |channel_index|
      output[channel_index][index] = frame[channel_index] || 0.0
    end
  end

  Core::AudioBlock.from_channel_data(output)
end

#reset_statsObject Also known as: resetStats



86
87
88
89
90
91
# File 'lib/deftones/source/user_media.rb', line 86

def reset_stats
  @underflow_count = 0
  @overflow_count = 0
  @capture_backend.reset_stats if @capture_backend.respond_to?(:reset_stats)
  self
end

#rewindObject



58
59
60
61
62
63
64
# File 'lib/deftones/source/user_media.rb', line 58

def rewind
  @sample_cursor = 0
  @provider_exhausted = false
  @provider.rewind if @provider.respond_to?(:rewind)
  @capture_backend&.rewind if @capture_backend.respond_to?(:rewind)
  self
end

#start(time = nil) ⇒ Object



29
30
31
32
33
34
# File 'lib/deftones/source/user_media.rb', line 29

def start(time = nil)
  rewind
  @capture_backend&.start
  @opened = true
  super
end

#state(time = context.current_time) ⇒ Object



103
104
105
106
107
# File 'lib/deftones/source/user_media.rb', line 103

def state(time = context.current_time)
  return :stopped unless @opened

  active_at?(resolve_time(time)) ? :started : :stopped
end

#stop(time = nil) ⇒ Object



36
37
38
39
# File 'lib/deftones/source/user_media.rb', line 36

def stop(time = nil)
  @capture_backend&.stop
  super
end

#sync_capture_metadata!Object (private)



295
296
297
298
299
300
301
302
# File 'lib/deftones/source/user_media.rb', line 295

def sync_capture_metadata!
  return self unless @capture_backend

  @device_id = @capture_backend.device_id if @capture_backend.respond_to?(:device_id) && !@capture_backend.device_id.nil?
  @group_id = @capture_backend.group_id if @capture_backend.respond_to?(:group_id) && !@capture_backend.group_id.nil?
  @label = @capture_backend.label if @capture_backend.respond_to?(:label) && !@capture_backend.label.nil?
  self
end

#underflow_countObject Also known as: underflowCount



78
79
80
# File 'lib/deftones/source/user_media.rb', line 78

def underflow_count
  @underflow_count + backend_stat(:underflow_count)
end