Class: Deftones::Event::Transport

Inherits:
Object
  • Object
show all
Defined in:
lib/deftones/event/transport.rb

Defined Under Namespace

Classes: TimingContext

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bpm: 120.0, time_signature: [4, 4], ppq: 192, clock: nil) ⇒ Transport

Returns a new instance of Transport.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/deftones/event/transport.rb', line 15

def initialize(bpm: 120.0, time_signature: [4, 4], ppq: 192, clock: nil)
  @bpm = Core::Signal.new(
    value: bpm,
    units: :number,
    context: TimingContext.new(Deftones::Context::DEFAULT_SAMPLE_RATE)
  )
  @ppq = ppq.to_i
  @state = :stopped
  self.time_signature = time_signature
  @loop = false
  @loop_start = 0.0
  @loop_end = 0.0
  @swing = 0.0
  @swing_subdivision = "8n"
  @timeline = {}
  @state_timeline = []
  @clock = clock
  @next_id = 0
  @next_state_id = 0
  @started_at = 0.0
  @position_seconds = 0.0
  @last_state_update_time = 0.0
end

Instance Attribute Details

#loopObject

Returns the value of attribute loop.



6
7
8
# File 'lib/deftones/event/transport.rb', line 6

def loop
  @loop
end

#loop_endObject

Returns the value of attribute loop_end.



6
7
8
# File 'lib/deftones/event/transport.rb', line 6

def loop_end
  @loop_end
end

#loop_startObject

Returns the value of attribute loop_start.



6
7
8
# File 'lib/deftones/event/transport.rb', line 6

def loop_start
  @loop_start
end

#ppqObject

Returns the value of attribute ppq.



7
8
9
# File 'lib/deftones/event/transport.rb', line 7

def ppq
  @ppq
end

#stateObject (readonly)

Returns the value of attribute state.



7
8
9
# File 'lib/deftones/event/transport.rb', line 7

def state
  @state
end

#swingObject

Returns the value of attribute swing.



6
7
8
# File 'lib/deftones/event/transport.rb', line 6

def swing
  @swing
end

#time_signatureObject

Returns the value of attribute time_signature.



7
8
9
# File 'lib/deftones/event/transport.rb', line 7

def time_signature
  @time_signature
end

Instance Method Details

#add_event(payload) ⇒ Object (private)



266
267
268
269
270
271
# File 'lib/deftones/event/transport.rb', line 266

def add_event(payload)
  event_id = @next_id
  @timeline[event_id] = payload
  @next_id += 1
  event_id
end

#add_state_event(state, time) ⇒ Object (private)



273
274
275
276
277
278
# File 'lib/deftones/event/transport.rb', line 273

def add_state_event(state, time)
  event_id = @next_state_id
  @state_timeline << { id: event_id, state: state, time: time }
  @next_state_id += 1
  event_id
end

#apply_due_state_events(up_to_time) ⇒ Object (private)



357
358
359
360
361
362
363
364
365
366
367
# File 'lib/deftones/event/transport.rb', line 357

def apply_due_state_events(up_to_time)
  due, pending = @state_timeline.partition { |event| event[:time] <= up_to_time }
  @state_timeline = pending
  due.sort_by { |event| [event[:time], event[:id]] }.each do |event|
    case event[:state]
    when :started then apply_start(event[:time])
    when :stopped then apply_stop(event[:time])
    when :paused then apply_pause(event[:time])
    end
  end
end

#apply_pause(time) ⇒ Object (private)



381
382
383
384
385
# File 'lib/deftones/event/transport.rb', line 381

def apply_pause(time)
  @position_seconds = state_position_at(time)
  @state = :paused
  @last_state_update_time = time
end

#apply_start(time) ⇒ Object (private)



369
370
371
372
373
# File 'lib/deftones/event/transport.rb', line 369

def apply_start(time)
  @state = :started
  @started_at = time - @position_seconds
  @last_state_update_time = time
end

#apply_stop(time) ⇒ Object (private)



375
376
377
378
379
# File 'lib/deftones/event/transport.rb', line 375

def apply_stop(time)
  @position_seconds = state_position_at(time)
  @state = :stopped
  @last_state_update_time = time
end

#apply_swing(time, interval, occurrence = 0) ⇒ Object (private)



404
405
406
407
408
409
410
# File 'lib/deftones/event/transport.rb', line 404

def apply_swing(time, interval, occurrence = 0)
  return time if @swing.zero?
  return time if resolve_time(@swing_subdivision) != interval
  return time if occurrence.even?

  time + (interval * 0.5 * @swing)
end

#beats_between(start_time, end_time) ⇒ Object



456
457
458
459
460
461
462
463
464
465
# File 'lib/deftones/event/transport.rb', line 456

def beats_between(start_time, end_time)
  from = start_time.to_f
  to = end_time.to_f
  return 0.0 if to <= from

  integration_points(from, to).each_cons(2).sum do |left, right|
    midpoint = (left + right) * 0.5
    ((right - left) * bpm_at(midpoint)) / 60.0
  end
end

#bpmObject



39
40
41
# File 'lib/deftones/event/transport.rb', line 39

def bpm
  @bpm.value
end

#bpm=(value) ⇒ Object



43
44
45
# File 'lib/deftones/event/transport.rb', line 43

def bpm=(value)
  @bpm.value = value
end

#bpm_at(time) ⇒ Object Also known as: bpmAt



139
140
141
# File 'lib/deftones/event/transport.rb', line 139

def bpm_at(time)
  @bpm.get_value_at_time(resolve_time(time))
end

#bpm_eventsObject (private)



481
482
483
# File 'lib/deftones/event/transport.rb', line 481

def bpm_events
  @bpm.instance_variable_get(:@events) || []
end

#bpm_signalObject Also known as: bpmSignal



47
48
49
# File 'lib/deftones/event/transport.rb', line 47

def bpm_signal
  @bpm
end

#cancel(after_time = 0, event_id: nil) ⇒ Object



172
173
174
175
176
177
178
179
180
181
# File 'lib/deftones/event/transport.rb', line 172

def cancel(after_time = 0, event_id: nil)
  return @timeline.delete(event_id) if event_id

  threshold = resolve_time(after_time)
  @timeline.delete_if do |_id, event|
    event_time = event[:kind] == :repeat ? event[:start_time] : event[:time]
    event_time >= threshold
  end
  self
end

#clear(event_id) ⇒ Object



168
169
170
# File 'lib/deftones/event/transport.rb', line 168

def clear(event_id)
  cancel(event_id: event_id)
end

#clock_timeObject (private)



418
419
420
421
422
# File 'lib/deftones/event/transport.rb', line 418

def clock_time
  return @clock.current_time if @clock&.respond_to?(:current_time)

  Deftones.now
end

#due_events(window_start, window_end) ⇒ Object (private)



280
281
282
283
284
285
286
287
# File 'lib/deftones/event/transport.rb', line 280

def due_events(window_start, window_end)
  return looped_due_events(window_start, window_end) if loop_active?

  events = @timeline.flat_map do |_id, event|
    event[:kind] == :repeat ? materialize_repeat_event(event, window_start, window_end) : materialize_one_shot(event, window_start, window_end)
  end
  events.sort_by { |event| event[:time] }
end

#due_events_without_loop(window_start, window_end) ⇒ Object (private)



308
309
310
311
312
313
314
315
316
# File 'lib/deftones/event/transport.rb', line 308

def due_events_without_loop(window_start, window_end)
  @timeline.flat_map do |_id, event|
    if event[:kind] == :repeat
      materialize_repeat_event(event, window_start, window_end)
    else
      materialize_one_shot(event, window_start, window_end)
    end
  end
end

#first_repeat_occurrence(start_time, interval, window_start) ⇒ Object (private)



341
342
343
344
345
# File 'lib/deftones/event/transport.rb', line 341

def first_repeat_occurrence(start_time, interval, window_start)
  return 0 if window_start <= start_time

  ((window_start - start_time) / interval).floor + 1
end

#future_time?(time) ⇒ Boolean (private)

Returns:

  • (Boolean)


351
352
353
354
355
# File 'lib/deftones/event/transport.rb', line 351

def future_time?(time)
  return false unless @clock

  time > clock_time
end

#immediateObject



193
194
195
# File 'lib/deftones/event/transport.rb', line 193

def immediate
  seconds
end

#integration_points(start_time, end_time) ⇒ Object (private)



471
472
473
474
475
476
477
478
479
# File 'lib/deftones/event/transport.rb', line 471

def integration_points(start_time, end_time)
  points = [start_time, end_time]
  bpm_events.each do |event|
    points << event[:time] if event[:time] && event[:time].between?(start_time, end_time)
    points << event[:start_time] if event[:start_time] && event[:start_time].between?(start_time, end_time)
    points << event[:end_time] if event[:end_time] && event[:end_time].between?(start_time, end_time)
  end
  points.uniq.sort
end

#loop_active?Boolean (private)

Returns:

  • (Boolean)


347
348
349
# File 'lib/deftones/event/transport.rb', line 347

def loop_active?
  @loop && (resolve_time(@loop_end) - resolve_time(@loop_start)).positive?
end

#looped_due_events(window_start, window_end) ⇒ Object (private)



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/deftones/event/transport.rb', line 289

def looped_due_events(window_start, window_end)
  loop_start_seconds = resolve_time(@loop_start)
  loop_end_seconds = resolve_time(@loop_end)
  span = loop_end_seconds - loop_start_seconds
  return [] unless span.positive?

  first_iteration = [(window_start - loop_end_seconds) / span, 0.0].max.floor
  last_iteration = [(window_end - loop_start_seconds) / span, 0.0].max.ceil
  (first_iteration..last_iteration).flat_map do |iteration|
    offset = iteration * span
    due_events_without_loop(loop_start_seconds, loop_end_seconds).filter_map do |event|
      global_time = event[:time] + offset
      next unless time_in_window?(global_time, window_start, window_end)

      event.merge(time: global_time)
    end
  end.sort_by { |event| event[:time] }
end

#materialize_one_shot(event, window_start, window_end) ⇒ Object (private)



318
319
320
321
322
# File 'lib/deftones/event/transport.rb', line 318

def materialize_one_shot(event, window_start, window_end)
  return [] unless time_in_window?(event[:time], window_start, window_end)

  [{ time: apply_swing(event[:time], event[:time]), callback: event[:callback] }]
end

#materialize_repeat_event(event, window_start, window_end) ⇒ Object (private)



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/deftones/event/transport.rb', line 324

def materialize_repeat_event(event, window_start, window_end)
  interval = [event[:interval], 1.0e-6].max
  limit = event[:duration] ? [event[:start_time] + event[:duration], window_end].min : window_end
  events = []
  occurrence = first_repeat_occurrence(event[:start_time], interval, window_start)
  current_time = event[:start_time] + (occurrence * interval)

  while current_time <= limit
    actual_time = apply_swing(current_time, interval, occurrence)
    events << { time: actual_time, callback: event[:callback] } if time_in_window?(actual_time, window_start, window_end)
    current_time += interval
    occurrence += 1
  end

  events
end

#next_subdivision(subdivision) ⇒ Object Also known as: nextSubdivision



208
209
210
211
212
213
214
# File 'lib/deftones/event/transport.rb', line 208

def next_subdivision(subdivision)
  interval = resolve_time(subdivision)
  return nil unless interval.positive?

  current = seconds
  (((current / interval).floor) + 1) * interval
end

#pause(time = nil) ⇒ Object



88
89
90
91
92
93
94
95
96
# File 'lib/deftones/event/transport.rb', line 88

def pause(time = nil)
  resolved_time = time.nil? ? clock_time : resolve_time(time)
  if future_time?(resolved_time)
    add_state_event(:paused, resolved_time)
  else
    apply_pause(resolved_time)
  end
  self
end

#positionObject



111
112
113
# File 'lib/deftones/event/transport.rb', line 111

def position
  seconds_to_position(@position_seconds)
end

#position=(value) ⇒ Object



115
116
117
# File 'lib/deftones/event/transport.rb', line 115

def position=(value)
  @position_seconds = resolve_time(value)
end

#prepare_render(duration) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
# File 'lib/deftones/event/transport.rb', line 240

def prepare_render(duration)
  render_duration = resolve_time(duration)
  cursor = 0.0

  while cursor < render_duration
    window_end = [cursor + 1.0, render_duration].min
    prepare_render_window(cursor, window_end)
    cursor = window_end
  end
  self
end

#prepare_render_window(start_time, end_time) ⇒ Object Also known as: prepareRenderWindow



252
253
254
255
256
257
258
259
260
261
262
# File 'lib/deftones/event/transport.rb', line 252

def prepare_render_window(start_time, end_time)
  window_start = resolve_time(start_time)
  window_end = resolve_time(end_time)
  return self if window_end < window_start

  apply_due_state_events(window_end)
  due_events(window_start, window_end).each do |event|
    event[:callback].call(event[:time])
  end
  self
end

#progressObject



197
198
199
200
201
202
203
204
205
206
# File 'lib/deftones/event/transport.rb', line 197

def progress
  return nil unless @loop

  loop_start_seconds = resolve_time(@loop_start)
  loop_end_seconds = resolve_time(@loop_end)
  span = loop_end_seconds - loop_start_seconds
  return nil unless span.positive?

  ((seconds - loop_start_seconds) % span) / span
end

#resolve_time(value) ⇒ Object (private)



412
413
414
415
416
# File 'lib/deftones/event/transport.rb', line 412

def resolve_time(value)
  return @position_seconds if value.nil?

  Deftones::Music::Time.parse(value, bpm: bpm, time_signature: time_signature, ppq: @ppq)
end

#schedule(time, &block) ⇒ Object

Raises:

  • (ArgumentError)


143
144
145
146
147
# File 'lib/deftones/event/transport.rb', line 143

def schedule(time, &block)
  raise ArgumentError, "Transport callback is required" unless block

  add_event(kind: :once, time: resolve_time(time), callback: block)
end

#schedule_once(time, &block) ⇒ Object Also known as: scheduleOnce



149
150
151
# File 'lib/deftones/event/transport.rb', line 149

def schedule_once(time, &block)
  schedule(time, &block)
end

#schedule_repeat(interval, start_time: 0, duration: nil, &block) ⇒ Object Also known as: scheduleRepeat

Raises:

  • (ArgumentError)


153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/deftones/event/transport.rb', line 153

def schedule_repeat(interval, start_time: 0, duration: nil, &block)
  raise ArgumentError, "Transport callback is required" unless block

  resolved_interval = resolve_time(interval)
  raise ArgumentError, "repeat interval must be positive" unless resolved_interval.positive?

  add_event(
    kind: :repeat,
    interval: resolved_interval,
    start_time: resolve_time(start_time),
    duration: duration.nil? ? nil : resolve_time(duration),
    callback: block
  )
end

#secondsObject



132
133
134
135
136
137
# File 'lib/deftones/event/transport.rb', line 132

def seconds
  apply_due_state_events(clock_time)
  return @position_seconds unless @state == :started

  [clock_time - @started_at, 0.0].max
end

#seconds=(value) ⇒ Object



119
120
121
# File 'lib/deftones/event/transport.rb', line 119

def seconds=(value)
  @position_seconds = resolve_time(value)
end

#seconds_to_position(seconds) ⇒ Object



424
425
426
427
428
429
430
431
432
# File 'lib/deftones/event/transport.rb', line 424

def seconds_to_position(seconds)
  beats_per_measure = Array(@time_signature).first || 4
  beat_duration = 60.0 / bpm
  total_beats = seconds.to_f / beat_duration
  bars = (total_beats / beats_per_measure).floor
  beats = (total_beats % beats_per_measure).floor
  sixteenths = (((total_beats - total_beats.floor) / 0.25).round) % 4
  "#{bars}:#{beats}:#{sixteenths}"
end

#seconds_to_ticks(seconds) ⇒ Object



434
435
436
# File 'lib/deftones/event/transport.rb', line 434

def seconds_to_ticks(seconds)
  (beats_between(0.0, seconds.to_f) * @ppq).round
end

#set_bpm_at_time(value, time) ⇒ Object Also known as: setBpmAtTime



51
52
53
54
# File 'lib/deftones/event/transport.rb', line 51

def set_bpm_at_time(value, time)
  @bpm.set_value_at_time(value, resolve_time(time))
  self
end

#set_loop_points(start_time, end_time) ⇒ Object Also known as: setLoopPoints



183
184
185
186
187
# File 'lib/deftones/event/transport.rb', line 183

def set_loop_points(start_time, end_time)
  @loop_start = resolve_time(start_time)
  @loop_end = resolve_time(end_time)
  self
end

#start(time = nil) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/deftones/event/transport.rb', line 68

def start(time = nil)
  resolved_time = resolve_time(time)
  if future_time?(resolved_time)
    add_state_event(:started, resolved_time)
  else
    apply_start(resolved_time)
  end
  self
end

#state_at(time) ⇒ Object Also known as: stateAt



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/deftones/event/transport.rb', line 98

def state_at(time)
  resolved_time = resolve_time(time)
  effective_state = state_before_timeline

  @state_timeline.sort_by { |event| [event[:time], event[:id]] }.each do |event|
    break if event[:time] > resolved_time

    effective_state = event[:state]
  end

  effective_state
end

#state_before_timelineObject (private)



393
394
395
# File 'lib/deftones/event/transport.rb', line 393

def state_before_timeline
  @state
end

#state_position_at(time) ⇒ Object (private)



387
388
389
390
391
# File 'lib/deftones/event/transport.rb', line 387

def state_position_at(time)
  return @position_seconds unless @state == :started

  [time - @started_at, 0.0].max
end

#stop(time = nil) ⇒ Object



78
79
80
81
82
83
84
85
86
# File 'lib/deftones/event/transport.rb', line 78

def stop(time = nil)
  resolved_time = time.nil? ? clock_time : resolve_time(time)
  if future_time?(resolved_time)
    add_state_event(:stopped, resolved_time)
  else
    apply_stop(resolved_time)
  end
  self
end

#swing_subdivisionObject



56
57
58
# File 'lib/deftones/event/transport.rb', line 56

def swing_subdivision
  @swing_subdivision
end

#swing_subdivision=(value) ⇒ Object



60
61
62
# File 'lib/deftones/event/transport.rb', line 60

def swing_subdivision=(value)
  @swing_subdivision = value
end

#swingSubdivisionObject



232
233
234
# File 'lib/deftones/event/transport.rb', line 232

def swingSubdivision
  swing_subdivision
end

#swingSubdivision=(value) ⇒ Object



236
237
238
# File 'lib/deftones/event/transport.rb', line 236

def swingSubdivision=(value)
  self.swing_subdivision = value
end

#tempo_automated?Boolean (private)

Returns:

  • (Boolean)


467
468
469
# File 'lib/deftones/event/transport.rb', line 467

def tempo_automated?
  bpm_events.any?
end

#ticksObject



123
124
125
# File 'lib/deftones/event/transport.rb', line 123

def ticks
  seconds_to_ticks(@position_seconds)
end

#ticks=(value) ⇒ Object



127
128
129
130
# File 'lib/deftones/event/transport.rb', line 127

def ticks=(value)
  parsed_ticks = Deftones::Music::Ticks.parse(value, bpm: bpm, time_signature: time_signature, ppq: @ppq)
  @position_seconds = ticks_to_seconds(parsed_ticks)
end

#ticks_to_seconds(ticks) ⇒ Object



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/deftones/event/transport.rb', line 438

def ticks_to_seconds(ticks)
  target_beats = ticks.to_f / @ppq
  return target_beats * (60.0 / bpm) unless tempo_automated?

  upper = [target_beats * (60.0 / [bpm, 1.0].max), 1.0e-6].max
  upper *= 2.0 while beats_between(0.0, upper) < target_beats
  lower = 0.0
  40.times do
    midpoint = (lower + upper) * 0.5
    if beats_between(0.0, midpoint) < target_beats
      lower = midpoint
    else
      upper = midpoint
    end
  end
  upper
end

#time_in_window?(time, window_start, window_end) ⇒ Boolean (private)

Returns:

  • (Boolean)


397
398
399
400
401
402
# File 'lib/deftones/event/transport.rb', line 397

def time_in_window?(time, window_start, window_end)
  return false if time > window_end
  return time >= window_start if window_start.zero?

  time > window_start
end

#timeSignatureObject



224
225
226
# File 'lib/deftones/event/transport.rb', line 224

def timeSignature
  time_signature
end

#timeSignature=(signature) ⇒ Object



228
229
230
# File 'lib/deftones/event/transport.rb', line 228

def timeSignature=(signature)
  self.time_signature = signature
end

#toggle(time = nil) ⇒ Object



189
190
191
# File 'lib/deftones/event/transport.rb', line 189

def toggle(time = nil)
  @state == :started ? pause(time) : start(time)
end