deftones 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -6
  3. data/README.md +5 -0
  4. data/Rakefile +50 -1
  5. data/lib/deftones/analysis/meter.rb +22 -2
  6. data/lib/deftones/component/channel.rb +1 -1
  7. data/lib/deftones/component/compressor.rb +127 -22
  8. data/lib/deftones/component/filter.rb +29 -19
  9. data/lib/deftones/component/merge.rb +14 -0
  10. data/lib/deftones/component/multiband_compressor.rb +1 -1
  11. data/lib/deftones/component/one_pole_filter.rb +10 -3
  12. data/lib/deftones/component/panner.rb +25 -2
  13. data/lib/deftones/component/panner3d.rb +0 -10
  14. data/lib/deftones/component/split.rb +14 -0
  15. data/lib/deftones/context.rb +90 -9
  16. data/lib/deftones/core/audio_block.rb +64 -5
  17. data/lib/deftones/core/audio_node.rb +98 -8
  18. data/lib/deftones/core/gain.rb +0 -8
  19. data/lib/deftones/core/instrument.rb +52 -10
  20. data/lib/deftones/core/param.rb +51 -1
  21. data/lib/deftones/core/signal.rb +79 -28
  22. data/lib/deftones/core/source.rb +71 -11
  23. data/lib/deftones/destination.rb +41 -17
  24. data/lib/deftones/draw.rb +6 -10
  25. data/lib/deftones/dsp/biquad.rb +9 -4
  26. data/lib/deftones/dsp/delay_line.rb +2 -2
  27. data/lib/deftones/dsp/helpers.rb +7 -0
  28. data/lib/deftones/effect/bit_crusher.rb +10 -2
  29. data/lib/deftones/effect/chebyshev.rb +7 -3
  30. data/lib/deftones/effect/distortion.rb +5 -3
  31. data/lib/deftones/effect/feedback_delay.rb +2 -1
  32. data/lib/deftones/effect/oversampling.rb +43 -0
  33. data/lib/deftones/effect/phaser.rb +2 -1
  34. data/lib/deftones/effect/pitch_shift.rb +1 -2
  35. data/lib/deftones/effect/reverb.rb +73 -5
  36. data/lib/deftones/event/callback_behavior.rb +7 -3
  37. data/lib/deftones/event/loop.rb +7 -2
  38. data/lib/deftones/event/part.rb +18 -3
  39. data/lib/deftones/event/pattern.rb +51 -6
  40. data/lib/deftones/event/sequence.rb +19 -5
  41. data/lib/deftones/event/tone_event.rb +7 -2
  42. data/lib/deftones/event/transport.rb +243 -21
  43. data/lib/deftones/instrument/poly_synth.rb +81 -15
  44. data/lib/deftones/instrument/sampler.rb +53 -10
  45. data/lib/deftones/io/buffer.rb +376 -55
  46. data/lib/deftones/io/buffers.rb +28 -4
  47. data/lib/deftones/io/recorder.rb +2 -1
  48. data/lib/deftones/music/frequency.rb +13 -8
  49. data/lib/deftones/music/midi.rb +132 -9
  50. data/lib/deftones/music/note.rb +13 -3
  51. data/lib/deftones/music/time.rb +42 -4
  52. data/lib/deftones/offline_context.rb +194 -17
  53. data/lib/deftones/portaudio_support.rb +68 -9
  54. data/lib/deftones/source/fat_oscillator.rb +28 -9
  55. data/lib/deftones/source/grain_player.rb +49 -2
  56. data/lib/deftones/source/noise.rb +42 -10
  57. data/lib/deftones/source/omni_oscillator.rb +1 -2
  58. data/lib/deftones/source/oscillator.rb +83 -19
  59. data/lib/deftones/source/player.rb +24 -6
  60. data/lib/deftones/source/players.rb +39 -6
  61. data/lib/deftones/source/tone_buffer_source.rb +12 -6
  62. data/lib/deftones/source/tone_oscillator_node.rb +4 -3
  63. data/lib/deftones/source/user_media.rb +83 -10
  64. data/lib/deftones/version.rb +1 -1
  65. data/lib/deftones.rb +108 -31
  66. metadata +3 -44
@@ -12,7 +12,7 @@ module Deftones
12
12
  end
13
13
  end
14
14
 
15
- def initialize(bpm: 120.0, time_signature: [4, 4], ppq: 192)
15
+ def initialize(bpm: 120.0, time_signature: [4, 4], ppq: 192, clock: nil)
16
16
  @bpm = Core::Signal.new(
17
17
  value: bpm,
18
18
  units: :number,
@@ -27,9 +27,13 @@ module Deftones
27
27
  @swing = 0.0
28
28
  @swing_subdivision = "8n"
29
29
  @timeline = {}
30
+ @state_timeline = []
31
+ @clock = clock
30
32
  @next_id = 0
33
+ @next_state_id = 0
31
34
  @started_at = 0.0
32
35
  @position_seconds = 0.0
36
+ @last_state_update_time = 0.0
33
37
  end
34
38
 
35
39
  def bpm
@@ -40,6 +44,15 @@ module Deftones
40
44
  @bpm.value = value
41
45
  end
42
46
 
47
+ def bpm_signal
48
+ @bpm
49
+ end
50
+
51
+ def set_bpm_at_time(value, time)
52
+ @bpm.set_value_at_time(value, resolve_time(time))
53
+ self
54
+ end
55
+
43
56
  def swing_subdivision
44
57
  @swing_subdivision
45
58
  end
@@ -53,23 +66,48 @@ module Deftones
53
66
  end
54
67
 
55
68
  def start(time = nil)
56
- @state = :started
57
- @started_at = resolve_time(time)
69
+ resolved_time = resolve_time(time)
70
+ if future_time?(resolved_time)
71
+ add_state_event(:started, resolved_time)
72
+ else
73
+ apply_start(resolved_time)
74
+ end
58
75
  self
59
76
  end
60
77
 
61
78
  def stop(time = nil)
62
- @state = :stopped
63
- @position_seconds = time.nil? ? seconds : resolve_time(time)
79
+ resolved_time = time.nil? ? clock_time : resolve_time(time)
80
+ if future_time?(resolved_time)
81
+ add_state_event(:stopped, resolved_time)
82
+ else
83
+ apply_stop(resolved_time)
84
+ end
64
85
  self
65
86
  end
66
87
 
67
88
  def pause(time = nil)
68
- @state = :paused
69
- @position_seconds = time.nil? ? seconds : resolve_time(time)
89
+ resolved_time = time.nil? ? clock_time : resolve_time(time)
90
+ if future_time?(resolved_time)
91
+ add_state_event(:paused, resolved_time)
92
+ else
93
+ apply_pause(resolved_time)
94
+ end
70
95
  self
71
96
  end
72
97
 
98
+ def state_at(time)
99
+ resolved_time = resolve_time(time)
100
+ effective_state = state_before_timeline
101
+
102
+ @state_timeline.sort_by { |event| [event[:time], event[:id]] }.each do |event|
103
+ break if event[:time] > resolved_time
104
+
105
+ effective_state = event[:state]
106
+ end
107
+
108
+ effective_state
109
+ end
110
+
73
111
  def position
74
112
  seconds_to_position(@position_seconds)
75
113
  end
@@ -92,12 +130,19 @@ module Deftones
92
130
  end
93
131
 
94
132
  def seconds
133
+ apply_due_state_events(clock_time)
95
134
  return @position_seconds unless @state == :started
96
135
 
97
- [Deftones.now - @started_at, 0.0].max
136
+ [clock_time - @started_at, 0.0].max
137
+ end
138
+
139
+ def bpm_at(time)
140
+ @bpm.get_value_at_time(resolve_time(time))
98
141
  end
99
142
 
100
143
  def schedule(time, &block)
144
+ raise ArgumentError, "Transport callback is required" unless block
145
+
101
146
  add_event(kind: :once, time: resolve_time(time), callback: block)
102
147
  end
103
148
 
@@ -106,9 +151,14 @@ module Deftones
106
151
  end
107
152
 
108
153
  def schedule_repeat(interval, start_time: 0, duration: nil, &block)
154
+ raise ArgumentError, "Transport callback is required" unless block
155
+
156
+ resolved_interval = resolve_time(interval)
157
+ raise ArgumentError, "repeat interval must be positive" unless resolved_interval.positive?
158
+
109
159
  add_event(
110
160
  kind: :repeat,
111
- interval: resolve_time(interval),
161
+ interval: resolved_interval,
112
162
  start_time: resolve_time(start_time),
113
163
  duration: duration.nil? ? nil : resolve_time(duration),
114
164
  callback: block
@@ -189,7 +239,23 @@ module Deftones
189
239
 
190
240
  def prepare_render(duration)
191
241
  render_duration = resolve_time(duration)
192
- due_events(render_duration).each do |event|
242
+ cursor = 0.0
243
+
244
+ while cursor < render_duration
245
+ window_end = [cursor + 1.0, render_duration].min
246
+ prepare_render_window(cursor, window_end)
247
+ cursor = window_end
248
+ end
249
+ self
250
+ end
251
+
252
+ def prepare_render_window(start_time, end_time)
253
+ window_start = resolve_time(start_time)
254
+ window_end = resolve_time(end_time)
255
+ return self if window_end < window_start
256
+
257
+ apply_due_state_events(window_end)
258
+ due_events(window_start, window_end).each do |event|
193
259
  event[:callback].call(event[:time])
194
260
  end
195
261
  self
@@ -204,29 +270,67 @@ module Deftones
204
270
  event_id
205
271
  end
206
272
 
207
- def due_events(duration)
273
+ def add_state_event(state, time)
274
+ event_id = @next_state_id
275
+ @state_timeline << { id: event_id, state: state, time: time }
276
+ @next_state_id += 1
277
+ event_id
278
+ end
279
+
280
+ def due_events(window_start, window_end)
281
+ return looped_due_events(window_start, window_end) if loop_active?
282
+
208
283
  events = @timeline.flat_map do |_id, event|
209
- event[:kind] == :repeat ? materialize_repeat_event(event, duration) : materialize_one_shot(event, duration)
284
+ event[:kind] == :repeat ? materialize_repeat_event(event, window_start, window_end) : materialize_one_shot(event, window_start, window_end)
210
285
  end
211
286
  events.sort_by { |event| event[:time] }
212
287
  end
213
288
 
214
- def materialize_one_shot(event, duration)
215
- return [] if event[:time] > duration
289
+ def looped_due_events(window_start, window_end)
290
+ loop_start_seconds = resolve_time(@loop_start)
291
+ loop_end_seconds = resolve_time(@loop_end)
292
+ span = loop_end_seconds - loop_start_seconds
293
+ return [] unless span.positive?
294
+
295
+ first_iteration = [(window_start - loop_end_seconds) / span, 0.0].max.floor
296
+ last_iteration = [(window_end - loop_start_seconds) / span, 0.0].max.ceil
297
+ (first_iteration..last_iteration).flat_map do |iteration|
298
+ offset = iteration * span
299
+ due_events_without_loop(loop_start_seconds, loop_end_seconds).filter_map do |event|
300
+ global_time = event[:time] + offset
301
+ next unless time_in_window?(global_time, window_start, window_end)
302
+
303
+ event.merge(time: global_time)
304
+ end
305
+ end.sort_by { |event| event[:time] }
306
+ end
307
+
308
+ def due_events_without_loop(window_start, window_end)
309
+ @timeline.flat_map do |_id, event|
310
+ if event[:kind] == :repeat
311
+ materialize_repeat_event(event, window_start, window_end)
312
+ else
313
+ materialize_one_shot(event, window_start, window_end)
314
+ end
315
+ end
316
+ end
317
+
318
+ def materialize_one_shot(event, window_start, window_end)
319
+ return [] unless time_in_window?(event[:time], window_start, window_end)
216
320
 
217
321
  [{ time: apply_swing(event[:time], event[:time]), callback: event[:callback] }]
218
322
  end
219
323
 
220
- def materialize_repeat_event(event, duration)
324
+ def materialize_repeat_event(event, window_start, window_end)
221
325
  interval = [event[:interval], 1.0e-6].max
222
- limit = event[:duration] ? [event[:start_time] + event[:duration], duration].min : duration
326
+ limit = event[:duration] ? [event[:start_time] + event[:duration], window_end].min : window_end
223
327
  events = []
224
- occurrence = 0
225
- current_time = event[:start_time]
328
+ occurrence = first_repeat_occurrence(event[:start_time], interval, window_start)
329
+ current_time = event[:start_time] + (occurrence * interval)
226
330
 
227
331
  while current_time <= limit
228
332
  actual_time = apply_swing(current_time, interval, occurrence)
229
- events << { time: actual_time, callback: event[:callback] }
333
+ events << { time: actual_time, callback: event[:callback] } if time_in_window?(actual_time, window_start, window_end)
230
334
  current_time += interval
231
335
  occurrence += 1
232
336
  end
@@ -234,6 +338,69 @@ module Deftones
234
338
  events
235
339
  end
236
340
 
341
+ def first_repeat_occurrence(start_time, interval, window_start)
342
+ return 0 if window_start <= start_time
343
+
344
+ ((window_start - start_time) / interval).floor + 1
345
+ end
346
+
347
+ def loop_active?
348
+ @loop && (resolve_time(@loop_end) - resolve_time(@loop_start)).positive?
349
+ end
350
+
351
+ def future_time?(time)
352
+ return false unless @clock
353
+
354
+ time > clock_time
355
+ end
356
+
357
+ def apply_due_state_events(up_to_time)
358
+ due, pending = @state_timeline.partition { |event| event[:time] <= up_to_time }
359
+ @state_timeline = pending
360
+ due.sort_by { |event| [event[:time], event[:id]] }.each do |event|
361
+ case event[:state]
362
+ when :started then apply_start(event[:time])
363
+ when :stopped then apply_stop(event[:time])
364
+ when :paused then apply_pause(event[:time])
365
+ end
366
+ end
367
+ end
368
+
369
+ def apply_start(time)
370
+ @state = :started
371
+ @started_at = time - @position_seconds
372
+ @last_state_update_time = time
373
+ end
374
+
375
+ def apply_stop(time)
376
+ @position_seconds = state_position_at(time)
377
+ @state = :stopped
378
+ @last_state_update_time = time
379
+ end
380
+
381
+ def apply_pause(time)
382
+ @position_seconds = state_position_at(time)
383
+ @state = :paused
384
+ @last_state_update_time = time
385
+ end
386
+
387
+ def state_position_at(time)
388
+ return @position_seconds unless @state == :started
389
+
390
+ [time - @started_at, 0.0].max
391
+ end
392
+
393
+ def state_before_timeline
394
+ @state
395
+ end
396
+
397
+ def time_in_window?(time, window_start, window_end)
398
+ return false if time > window_end
399
+ return time >= window_start if window_start.zero?
400
+
401
+ time > window_start
402
+ end
403
+
237
404
  def apply_swing(time, interval, occurrence = 0)
238
405
  return time if @swing.zero?
239
406
  return time if resolve_time(@swing_subdivision) != interval
@@ -248,6 +415,12 @@ module Deftones
248
415
  Deftones::Music::Time.parse(value, bpm: bpm, time_signature: time_signature, ppq: @ppq)
249
416
  end
250
417
 
418
+ def clock_time
419
+ return @clock.current_time if @clock&.respond_to?(:current_time)
420
+
421
+ Deftones.now
422
+ end
423
+
251
424
  def seconds_to_position(seconds)
252
425
  beats_per_measure = Array(@time_signature).first || 4
253
426
  beat_duration = 60.0 / bpm
@@ -259,18 +432,67 @@ module Deftones
259
432
  end
260
433
 
261
434
  def seconds_to_ticks(seconds)
262
- ((seconds.to_f / (60.0 / bpm)) * @ppq).round
435
+ (beats_between(0.0, seconds.to_f) * @ppq).round
263
436
  end
264
437
 
265
438
  def ticks_to_seconds(ticks)
266
- (ticks.to_f / @ppq) * (60.0 / bpm)
439
+ target_beats = ticks.to_f / @ppq
440
+ return target_beats * (60.0 / bpm) unless tempo_automated?
441
+
442
+ upper = [target_beats * (60.0 / [bpm, 1.0].max), 1.0e-6].max
443
+ upper *= 2.0 while beats_between(0.0, upper) < target_beats
444
+ lower = 0.0
445
+ 40.times do
446
+ midpoint = (lower + upper) * 0.5
447
+ if beats_between(0.0, midpoint) < target_beats
448
+ lower = midpoint
449
+ else
450
+ upper = midpoint
451
+ end
452
+ end
453
+ upper
454
+ end
455
+
456
+ def beats_between(start_time, end_time)
457
+ from = start_time.to_f
458
+ to = end_time.to_f
459
+ return 0.0 if to <= from
460
+
461
+ integration_points(from, to).each_cons(2).sum do |left, right|
462
+ midpoint = (left + right) * 0.5
463
+ ((right - left) * bpm_at(midpoint)) / 60.0
464
+ end
465
+ end
466
+
467
+ def tempo_automated?
468
+ bpm_events.any?
469
+ end
470
+
471
+ def integration_points(start_time, end_time)
472
+ points = [start_time, end_time]
473
+ bpm_events.each do |event|
474
+ points << event[:time] if event[:time] && event[:time].between?(start_time, end_time)
475
+ points << event[:start_time] if event[:start_time] && event[:start_time].between?(start_time, end_time)
476
+ points << event[:end_time] if event[:end_time] && event[:end_time].between?(start_time, end_time)
477
+ end
478
+ points.uniq.sort
479
+ end
480
+
481
+ def bpm_events
482
+ @bpm.instance_variable_get(:@events) || []
267
483
  end
268
484
 
269
485
  public :seconds_to_position, :seconds_to_ticks, :ticks_to_seconds
486
+ public :beats_between
270
487
  alias scheduleOnce schedule_once
271
488
  alias scheduleRepeat schedule_repeat
489
+ alias prepareRenderWindow prepare_render_window
272
490
  alias setLoopPoints set_loop_points
273
491
  alias nextSubdivision next_subdivision
492
+ alias bpmAt bpm_at
493
+ alias bpmSignal bpm_signal
494
+ alias setBpmAtTime set_bpm_at_time
495
+ alias stateAt state_at
274
496
  end
275
497
  end
276
498
  end
@@ -3,13 +3,19 @@
3
3
  module Deftones
4
4
  module Instrument
5
5
  class PolySynth < Core::Instrument
6
- attr_reader :voice_pool
6
+ VOICE_STEALING_POLICIES = %i[oldest newest quietest released_first].freeze
7
+ RETRIGGER_POLICIES = %i[restart ignore].freeze
7
8
 
8
- def initialize(voice_class = Synth, voices: 8, context: Deftones.context, **voice_options)
9
+ attr_reader :voice_pool, :voice_stealing, :retrigger
10
+
11
+ def initialize(voice_class = Synth, voices: 8, voice_stealing: :oldest, retrigger: :restart,
12
+ context: Deftones.context, **voice_options)
9
13
  super(context: context)
10
14
  @voice_class = voice_class
11
15
  @voice_pool = Array.new(voices) { @voice_class.new(context: context, **voice_options) }
12
16
  @voice_pool.each { |voice| voice >> @output }
17
+ @voice_stealing = normalize_voice_stealing(voice_stealing)
18
+ @retrigger = normalize_retrigger(retrigger)
13
19
  @active_voices = {}
14
20
  end
15
21
 
@@ -29,20 +35,35 @@ module Deftones
29
35
  end
30
36
 
31
37
  def trigger_attack(note, time = nil, velocity = 1.0)
32
- voice = allocate_voice(note)
38
+ scheduled_time = resolve_time(time)
39
+ return self if @active_voices.key?(note) && @retrigger == :ignore
40
+
41
+ voice = allocate_voice(note, scheduled_time)
33
42
  @active_voices.delete(note)
34
- @active_voices[note] = voice
35
- voice.trigger_attack(note, resolve_time(time), velocity)
43
+ @active_voices[note] = {
44
+ voice: voice,
45
+ attacked_at: scheduled_time,
46
+ released_at: nil,
47
+ velocity: velocity.to_f
48
+ }
49
+ voice.trigger_attack(note, scheduled_time, velocity)
36
50
  self
37
51
  end
38
52
 
39
53
  def trigger_release(note, time = nil)
40
- voice = @active_voices.delete(note)
41
- voice&.trigger_release(resolve_time(time))
54
+ entry = @active_voices[note]
55
+ return self unless entry
56
+
57
+ scheduled_time = resolve_time(time)
58
+ entry[:released_at] = scheduled_time
59
+ entry[:voice].trigger_release(scheduled_time)
42
60
  self
43
61
  end
44
62
 
45
- def set(**params)
63
+ def set(strict: false, **params)
64
+ unknown = params.keys.reject { |key| @voice_pool.all? { |voice| voice.respond_to?(:"#{key}=") } }
65
+ raise ArgumentError, "Unknown parameter(s): #{unknown.join(', ')}" if strict && unknown.any?
66
+
46
67
  @voice_pool.each do |voice|
47
68
  params.each do |key, value|
48
69
  writer = :"#{key}="
@@ -54,7 +75,7 @@ module Deftones
54
75
 
55
76
  def release_all(time = nil)
56
77
  scheduled_time = resolve_time(time)
57
- @active_voices.each_value { |voice| voice.trigger_release(scheduled_time) }
78
+ @active_voices.each_value { |entry| entry[:voice].trigger_release(scheduled_time) }
58
79
  @active_voices.clear
59
80
  self
60
81
  end
@@ -64,7 +85,7 @@ module Deftones
64
85
  end
65
86
 
66
87
  def loaded?
67
- true
88
+ !disposed?
68
89
  end
69
90
 
70
91
  alias loaded loaded?
@@ -74,16 +95,61 @@ module Deftones
74
95
  @voice_pool.any?(&:active?)
75
96
  end
76
97
 
98
+ def active_notes
99
+ @active_voices.keys
100
+ end
101
+
102
+ def active_voice_count
103
+ @active_voices.length
104
+ end
105
+
77
106
  private
78
107
 
79
- def allocate_voice(note)
80
- return @active_voices[note] if @active_voices.key?(note)
108
+ def allocate_voice(note, time)
109
+ cleanup_inactive_voices!
110
+ return @active_voices[note][:voice] if @active_voices.key?(note)
81
111
 
82
- available_voice = @voice_pool.find { |voice| !@active_voices.value?(voice) }
112
+ active_voice_ids = @active_voices.values.map { |entry| entry[:voice].object_id }
113
+ available_voice = @voice_pool.find { |voice| !active_voice_ids.include?(voice.object_id) }
83
114
  return available_voice if available_voice
84
115
 
85
- _, stolen_voice = @active_voices.shift
86
- stolen_voice
116
+ stolen_note, stolen_entry = steal_voice_entry(time)
117
+ @active_voices.delete(stolen_note)
118
+ stolen_entry[:voice]
119
+ end
120
+
121
+ def steal_voice_entry(time)
122
+ case @voice_stealing
123
+ when :newest
124
+ @active_voices.max_by { |_, entry| entry[:attacked_at] }
125
+ when :quietest
126
+ @active_voices.min_by { |_, entry| entry[:velocity] }
127
+ when :released_first
128
+ released = @active_voices.select { |_, entry| entry[:released_at] && entry[:released_at] <= time }
129
+ return released.min_by { |_, entry| entry[:released_at] } if released.any?
130
+
131
+ @active_voices.min_by { |_, entry| entry[:attacked_at] }
132
+ else
133
+ @active_voices.min_by { |_, entry| entry[:attacked_at] }
134
+ end
135
+ end
136
+
137
+ def cleanup_inactive_voices!
138
+ @active_voices.delete_if { |_, entry| entry[:released_at] && !entry[:voice].active? }
139
+ end
140
+
141
+ def normalize_voice_stealing(policy)
142
+ normalized = policy.to_sym
143
+ return normalized if VOICE_STEALING_POLICIES.include?(normalized)
144
+
145
+ raise ArgumentError, "Unsupported voice stealing policy: #{policy}"
146
+ end
147
+
148
+ def normalize_retrigger(policy)
149
+ normalized = policy.to_sym
150
+ return normalized if RETRIGGER_POLICIES.include?(normalized)
151
+
152
+ raise ArgumentError, "Unsupported retrigger policy: #{policy}"
87
153
  end
88
154
 
89
155
  def resolve_time(time)
@@ -3,13 +3,19 @@
3
3
  module Deftones
4
4
  module Instrument
5
5
  class Sampler < Core::Instrument
6
- attr_reader :samples, :voices
6
+ attr_reader :choke_group, :release, :samples, :voices
7
+ attr_reader :one_shot
7
8
 
8
- def initialize(samples:, max_voices: 8, context: Deftones.context)
9
+ def initialize(samples:, max_voices: 8, release: 0.0, one_shot: false, choke_group: nil,
10
+ context: Deftones.context)
9
11
  super(context: context)
10
12
  @samples = samples.transform_keys(&:to_s)
11
13
  @max_voices = max_voices
14
+ @release = release.to_f
15
+ @one_shot = !!one_shot
16
+ @choke_group = choke_group
12
17
  @voices = []
18
+ rebuild_root_note_cache
13
19
  end
14
20
 
15
21
  def play(notes, duration: "8n", at: nil, velocity: 1.0)
@@ -22,17 +28,21 @@ module Deftones
22
28
 
23
29
  def trigger_attack(note, time = nil, velocity = 1.0)
24
30
  buffer_note, buffer = closest_sample(note)
31
+ scheduled_time = resolve_time(time)
32
+ choke_matching_voices(scheduled_time)
25
33
  playback_rate = Deftones::Music::Note.to_frequency(note) / Deftones::Music::Note.to_frequency(buffer_note)
26
- player = Source::Player.new(buffer: buffer, playback_rate: playback_rate, context: context)
34
+ player = Source::Player.new(buffer: buffer, playback_rate: playback_rate, fade_out: @release, context: context)
27
35
  gain = Core::Gain.new(gain: velocity, context: context)
28
36
  player >> gain >> @output
29
- player.start(resolve_time(time))
30
- @voices << { note: note, player: player }
31
- @voices.shift if @voices.length > @max_voices
37
+ player.start(scheduled_time)
38
+ @voices << { note: note, player: player, choke_group: @choke_group }
39
+ steal_oldest_voice(scheduled_time) if @voices.length > @max_voices
32
40
  self
33
41
  end
34
42
 
35
43
  def trigger_release(note, time = nil)
44
+ return self if @one_shot
45
+
36
46
  voice = @voices.find { |entry| entry[:note] == note }
37
47
  voice&.fetch(:player)&.stop(resolve_time(time))
38
48
  self
@@ -47,6 +57,7 @@ module Deftones
47
57
 
48
58
  def add(note, buffer)
49
59
  @samples[note.to_s] = buffer.is_a?(Deftones::IO::Buffer) ? buffer : Deftones::IO::Buffer.load(buffer)
60
+ rebuild_root_note_cache
50
61
  self
51
62
  end
52
63
 
@@ -58,18 +69,20 @@ module Deftones
58
69
  @samples.key?(note.to_s)
59
70
  end
60
71
 
61
- def release_all(time = nil)
72
+ def release_all(time = nil, force: false)
62
73
  scheduled_time = resolve_time(time)
74
+ return self if @one_shot && !force
75
+
63
76
  @voices.each { |voice| voice[:player].stop(scheduled_time) }
64
77
  self
65
78
  end
66
79
 
67
80
  def loaded?
68
- true
81
+ !disposed?
69
82
  end
70
83
 
71
84
  def dispose
72
- release_all(context.current_time)
85
+ release_all(context.current_time, force: true)
73
86
  @voices.clear
74
87
  super
75
88
  end
@@ -77,13 +90,43 @@ module Deftones
77
90
  alias loaded loaded?
78
91
  alias triggerAttackRelease trigger_attack_release
79
92
  alias releaseAll release_all
93
+ alias oneShot one_shot
80
94
 
81
95
  private
82
96
 
83
97
  def closest_sample(note)
98
+ raise ArgumentError, "Sampler requires at least one sample" if @samples.empty?
99
+
84
100
  target_midi = Deftones::Music::Note.to_midi(note)
85
101
  @samples.min_by do |sample_note, _|
86
- (Deftones::Music::Note.to_midi(sample_note) - target_midi).abs
102
+ (@root_note_cache.fetch(sample_note) - target_midi).abs
103
+ end
104
+ end
105
+
106
+ def steal_oldest_voice(time)
107
+ stolen = @voices.shift
108
+ return unless stolen
109
+
110
+ player = stolen[:player]
111
+ player.stop(time)
112
+ player.dispose
113
+ end
114
+
115
+ def choke_matching_voices(time)
116
+ return unless @choke_group
117
+
118
+ @voices.delete_if do |voice|
119
+ next false unless voice[:choke_group] == @choke_group
120
+
121
+ voice[:player].stop(time)
122
+ voice[:player].dispose
123
+ true
124
+ end
125
+ end
126
+
127
+ def rebuild_root_note_cache
128
+ @root_note_cache = @samples.each_key.to_h do |sample_note|
129
+ [sample_note, Deftones::Music::Note.to_midi(sample_note)]
87
130
  end
88
131
  end
89
132