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
@@ -5,13 +5,27 @@ module Deftones
5
5
  class Buffers
6
6
  include Enumerable
7
7
 
8
- def initialize(buffers = {})
8
+ class BulkLoadError < Deftones::Error
9
+ attr_reader :errors
10
+
11
+ def initialize(errors)
12
+ @errors = errors
13
+ super("Failed to load #{errors.length} buffer(s): #{errors.keys.join(', ')}")
14
+ end
15
+ end
16
+
17
+ attr_reader :load_errors
18
+
19
+ def initialize(buffers = {}, aggregate_errors: false, **keyword_buffers)
9
20
  @buffers = {}
21
+ @load_errors = {}
10
22
  @disposed = false
11
- merge(buffers)
23
+ merge(buffers.merge(keyword_buffers), aggregate_errors: aggregate_errors)
12
24
  end
13
25
 
14
26
  def add(name, buffer)
27
+ raise Deftones::Error, "cannot add buffer to disposed Buffers" if @disposed
28
+
15
29
  @buffers[key_for(name)] = normalize_buffer(buffer)
16
30
  self
17
31
  end
@@ -50,8 +64,18 @@ module Deftones
50
64
  @buffers.dup
51
65
  end
52
66
 
53
- def merge(buffers)
54
- buffers.each { |name, buffer| add(name, buffer) }
67
+ def merge(buffers, aggregate_errors: false)
68
+ errors = {}
69
+ buffers.each do |name, buffer|
70
+ add(name, buffer)
71
+ rescue StandardError => error
72
+ raise unless aggregate_errors
73
+
74
+ errors[key_for(name)] = error
75
+ end
76
+ @load_errors.merge!(errors)
77
+ raise BulkLoadError, errors if errors.any?
78
+
55
79
  self
56
80
  end
57
81
 
@@ -67,7 +67,8 @@ module Deftones
67
67
  else
68
68
  seconds = [duration.to_f, 1.0 / @context.sample_rate].max
69
69
  frames = (seconds * @context.sample_rate).ceil
70
- block = @node.send(:render_block, frames, 0, {}).fit_channels(@context.channels)
70
+ start_frame = ((@started_at || 0.0) * @context.sample_rate).floor
71
+ block = @node.send(:render_block, frames, start_frame, {}).fit_channels(@context.channels)
71
72
  Buffer.new(block.interleaved, channels: @context.channels, sample_rate: @context.sample_rate)
72
73
  end
73
74
  end
@@ -105,14 +105,19 @@ module Deftones
105
105
  end
106
106
 
107
107
  def parse(value)
108
- case value
109
- when Numeric
110
- value.to_f
111
- when /\A(\d+(?:\.\d+)?)hz\z/i
112
- Regexp.last_match(1).to_f
113
- else
114
- Note.to_frequency(value)
115
- end
108
+ frequency =
109
+ case value
110
+ when Numeric
111
+ value.to_f
112
+ when /\A(-?\d+(?:\.\d+)?)hz\z/i
113
+ Regexp.last_match(1).to_f
114
+ else
115
+ Note.to_frequency(value)
116
+ end
117
+
118
+ raise Deftones::InvalidFrequencyError, "Frequency must be positive" unless frequency.positive? && frequency.finite?
119
+
120
+ frequency
116
121
  end
117
122
 
118
123
  def to_period(value)
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "unimidi"
5
- rescue LoadError
6
- nil
7
- end
8
-
9
3
  module Deftones
10
4
  module Music
11
5
  class Midi
@@ -105,6 +99,7 @@ module Deftones
105
99
  end
106
100
 
107
101
  def available?
102
+ load_backend!
108
103
  !!defined?(UniMIDI)
109
104
  end
110
105
 
@@ -136,6 +131,17 @@ module Deftones
136
131
  open_device(find_output(name), *args, &block)
137
132
  end
138
133
 
134
+ def open_output_session(name = nil, *args)
135
+ session = OutputSession.new(open_output(name, *args))
136
+ return session unless block_given?
137
+
138
+ begin
139
+ yield session
140
+ ensure
141
+ session.close
142
+ end
143
+ end
144
+
139
145
  def receive(name = nil, *args)
140
146
  open_input(name) do |input|
141
147
  input.gets(*args)
@@ -163,16 +169,84 @@ module Deftones
163
169
  )
164
170
  end
165
171
 
172
+ def sync_transport(message, transport: Deftones.transport)
173
+ status = message_data(message).first.to_i
174
+ case status
175
+ when 0xF8
176
+ transport.ticks = transport.ticks + (transport.ppq / 24.0)
177
+ :clock
178
+ when 0xFA
179
+ transport.ticks = 0
180
+ transport.start(0)
181
+ :start
182
+ when 0xFB
183
+ transport.start(transport.seconds)
184
+ :continue
185
+ when 0xFC
186
+ transport.stop
187
+ :stop
188
+ else
189
+ :ignored
190
+ end
191
+ end
192
+
193
+ def trigger_from_message(message, target:, time: nil, velocity_scale: 127.0)
194
+ data = message_data(message)
195
+ status = data.first.to_i
196
+ command = status & 0xF0
197
+ return :ignored unless [0x80, 0x90].include?(command)
198
+
199
+ note = Note.from_midi(normalize_data_byte(data[1]))
200
+ velocity = normalize_data_byte(data[2]) / [velocity_scale.to_f, 1.0].max
201
+ if command == 0x90 && velocity.positive?
202
+ target.trigger_attack(note, time, velocity)
203
+ :note_on
204
+ else
205
+ trigger_release(target, note, time)
206
+ :note_off
207
+ end
208
+ end
209
+
166
210
  private
167
211
 
212
+ def load_backend!
213
+ return true if defined?(UniMIDI)
214
+
215
+ require "unimidi"
216
+ true
217
+ rescue LoadError
218
+ false
219
+ end
220
+
221
+ def message_data(message)
222
+ data = message.is_a?(Hash) ? message.fetch(:data, message) : message
223
+ Array(data).map(&:to_i)
224
+ end
225
+
168
226
  def find_device(devices, name)
169
227
  return devices.first if name.nil?
170
228
 
171
- devices.find { |device| device.name == name.to_s }
229
+ matched_by_id = devices.find { |device| matches_device_id?(device, name) }
230
+ return matched_by_id if matched_by_id
231
+ return devices[name] if name.is_a?(Integer) && name >= 0 && name < devices.length
232
+
233
+ matcher = name.is_a?(Regexp) ? name : Regexp.new(Regexp.escape(name.to_s), Regexp::IGNORECASE)
234
+ devices.find { |device| device.respond_to?(:name) && device.name.to_s.match?(matcher) }
235
+ end
236
+
237
+ def matches_device_id?(device, selector)
238
+ return false if selector.is_a?(Regexp)
239
+
240
+ candidates = []
241
+ candidates << device.id if device.respond_to?(:id)
242
+ candidates << device.device_id if device.respond_to?(:device_id)
243
+ candidates << device.index if device.respond_to?(:index)
244
+ candidates << device.device_index if device.respond_to?(:device_index)
245
+ candidates.compact.any? { |candidate| candidate.to_s == selector.to_s }
172
246
  end
173
247
 
174
248
  def open_device(device, *args, &block)
175
- raise ArgumentError, "MIDI support is unavailable" unless available?
249
+ raise Deftones::MissingMidiBackendError, "MIDI support is unavailable. Install the unimidi gem to enable MIDI I/O." unless available?
176
250
  raise ArgumentError, "No matching MIDI device found" unless device
177
251
 
178
252
  return device.open(*args) unless block
@@ -197,8 +271,57 @@ module Deftones
197
271
  base + normalize_channel(channel)
198
272
  end
199
273
 
274
+ def trigger_release(target, note, time)
275
+ target.trigger_release(note, time)
276
+ rescue ArgumentError
277
+ target.trigger_release(time)
278
+ end
279
+
200
280
  def normalize_channel(channel)
201
- [[channel.to_i - 1, 0].max, 15].min
281
+ integer = channel.to_i
282
+ raise ArgumentError, "MIDI channel must be between 1 and 16" unless (1..16).cover?(integer)
283
+
284
+ integer - 1
285
+ end
286
+ end
287
+
288
+ class OutputSession
289
+ def initialize(output)
290
+ @output = output
291
+ @closed = false
292
+ end
293
+
294
+ def send(message)
295
+ raise IOError, "MIDI output session is closed" if @closed
296
+
297
+ @output.puts(message)
298
+ self
299
+ end
300
+
301
+ def note_on(note, velocity: 100, channel: 1)
302
+ send([self.class.parent_status_byte(0x90, channel), Midi.send(:normalize_note, note),
303
+ Midi.send(:normalize_data_byte, velocity)])
304
+ end
305
+
306
+ def note_off(note, velocity: 0, channel: 1)
307
+ send([self.class.parent_status_byte(0x80, channel), Midi.send(:normalize_note, note),
308
+ Midi.send(:normalize_data_byte, velocity)])
309
+ end
310
+
311
+ def close
312
+ return self if @closed
313
+
314
+ @output.close if @output.respond_to?(:close)
315
+ @closed = true
316
+ self
317
+ end
318
+
319
+ def closed?
320
+ @closed
321
+ end
322
+
323
+ def self.parent_status_byte(base, channel)
324
+ Midi.send(:status_byte, base, channel)
202
325
  end
203
326
  end
204
327
  end
@@ -32,22 +32,32 @@ module Deftones
32
32
  end
33
33
 
34
34
  def from_frequency(frequency)
35
- midi_number = (12 * Math.log2(frequency.to_f / 440.0) + 69).round
35
+ normalized_frequency = frequency.to_f
36
+ raise Deftones::InvalidFrequencyError, "Frequency must be positive" unless normalized_frequency.positive? && normalized_frequency.finite?
37
+
38
+ midi_number = (12 * Math.log2(normalized_frequency / 440.0) + 69).round
36
39
  from_midi(midi_number)
37
40
  end
38
41
 
39
42
  private
40
43
 
41
44
  def parse_note_name(note_name)
45
+ validate_accidentals!(note_name)
42
46
  match = note_name.to_s.match(/\A([A-Ga-g][#b]?)(-?\d+)\z/)
43
- raise ArgumentError, "Invalid note: #{note_name}" unless match
47
+ raise Deftones::InvalidNoteError, "Invalid note: #{note_name}" unless match
44
48
 
45
49
  normalized_name = normalize_name(match[1])
46
- raise ArgumentError, "Unsupported note name: #{note_name}" unless NOTE_NAMES.include?(normalized_name)
50
+ raise Deftones::InvalidNoteError, "Unsupported note name: #{note_name}" unless NOTE_NAMES.include?(normalized_name)
47
51
 
48
52
  [normalized_name, match[2].to_i]
49
53
  end
50
54
 
55
+ def validate_accidentals!(note_name)
56
+ return unless note_name.to_s.match?(/\A[A-Ga-g](?:##|bb)/)
57
+
58
+ raise Deftones::InvalidNoteError, "Double accidentals are unsupported: #{note_name}"
59
+ end
60
+
51
61
  def normalize_name(token)
52
62
  canonical = token[0].upcase + token[1..]
53
63
  FLAT_MAP.fetch(canonical, canonical.upcase)
@@ -106,8 +106,10 @@ module Deftones
106
106
  else
107
107
  return value.to_seconds if value.respond_to?(:to_seconds)
108
108
  end
109
- rescue KeyError, ArgumentError
110
- raise ArgumentError, "Unknown time format: #{value}"
109
+ rescue KeyError, ArgumentError => error
110
+ raise error if error.is_a?(Deftones::InvalidTimeError)
111
+
112
+ raise Deftones::InvalidTimeError, "Unknown time format: #{value}"
111
113
  end
112
114
 
113
115
  private
@@ -133,7 +135,10 @@ module Deftones
133
135
  beats_per_measure = Array(time_signature).first || 4
134
136
  ((bars * beats_per_measure) + beats + (sixteenths * 0.25)) * beat_duration(bpm)
135
137
  when /\A(\d+(?:\.\d+)?)hz\z/i
136
- 1.0 / Regexp.last_match(1).to_f
138
+ frequency = Regexp.last_match(1).to_f
139
+ raise Deftones::InvalidTimeError, "Hz time values must be positive" unless frequency.positive?
140
+
141
+ 1.0 / frequency
137
142
  when /\A(-?\d+(?:\.\d+)?)i\z/i
138
143
  (Regexp.last_match(1).to_f / ppq.to_f) * beat_duration(bpm)
139
144
  else
@@ -154,7 +159,19 @@ module Deftones
154
159
  end
155
160
 
156
161
  def tokenize(expression)
157
- expression.scan(/\d+:\d+:\d+|\d+(?:\.\d+)?hz|-?\d+(?:\.\d+)?i|\d+n\.?|\d+t|\d+m|[()+\-*\/]|\d+(?:\.\d+)?/)
162
+ tokens = []
163
+ offset = 0
164
+ pattern = /\G\s*(\d+:\d+:\d+|\d+(?:\.\d+)?hz|-?\d+(?:\.\d+)?i|\d+n\.?|\d+t|\d+m|[()+\-*\/]|-?\d+(?:\.\d+)?)/
165
+
166
+ while offset < expression.length
167
+ match = expression.match(pattern, offset)
168
+ raise Deftones::InvalidTimeError, "Invalid time expression: #{expression}" unless match
169
+
170
+ tokens << match[1]
171
+ offset = match.end(0)
172
+ end
173
+
174
+ normalize_unary_minus(tokens)
158
175
  end
159
176
 
160
177
  def to_rpn(tokens)
@@ -170,6 +187,8 @@ module Deftones
170
187
  elsif token == "("
171
188
  operators << token
172
189
  elsif token == ")"
190
+ raise Deftones::InvalidTimeError, "Mismatched parentheses" unless operators.include?("(")
191
+
173
192
  output << operators.pop until operators.last == "("
174
193
  operators.pop
175
194
  else
@@ -177,6 +196,8 @@ module Deftones
177
196
  end
178
197
  end
179
198
 
199
+ raise Deftones::InvalidTimeError, "Mismatched parentheses" if operators.any? { |operator| ["(", ")"].include?(operator) }
200
+
180
201
  output.concat(operators.reverse)
181
202
  end
182
203
 
@@ -185,6 +206,8 @@ module Deftones
185
206
 
186
207
  tokens.each do |token|
187
208
  if operator?(token)
209
+ raise Deftones::InvalidTimeError, "Invalid time expression" if stack.length < 2
210
+
188
211
  right = stack.pop
189
212
  left = stack.pop
190
213
  stack << left.public_send(token, right)
@@ -193,9 +216,24 @@ module Deftones
193
216
  end
194
217
  end
195
218
 
219
+ raise Deftones::InvalidTimeError, "Invalid time expression" unless stack.length == 1
220
+
196
221
  stack.first
197
222
  end
198
223
 
224
+ def normalize_unary_minus(tokens)
225
+ normalized = []
226
+
227
+ tokens.each_with_index do |token, index|
228
+ if token == "-" && (index.zero? || operator?(tokens[index - 1]) || tokens[index - 1] == "(")
229
+ normalized << "0"
230
+ end
231
+ normalized << token
232
+ end
233
+
234
+ normalized
235
+ end
236
+
199
237
  def operator?(token)
200
238
  %w[+ - * /].include?(token)
201
239
  end
@@ -1,46 +1,223 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tempfile"
4
+
3
5
  module Deftones
4
6
  class OfflineContext < Context
5
- attr_reader :channels, :duration, :total_frames
7
+ RenderResult = Struct.new(:buffer, :metadata, keyword_init: true)
8
+ class RenderCancelled < Deftones::Error; end
9
+
10
+ attr_reader :channels, :current_frame, :duration, :last_render_metadata, :total_frames
6
11
 
7
12
  def initialize(duration:, channels: 2, sample_rate: DEFAULT_SAMPLE_RATE,
8
- buffer_size: DEFAULT_BUFFER_SIZE)
9
- super(sample_rate: sample_rate, buffer_size: buffer_size, channels: channels, autostart: false)
13
+ buffer_size: DEFAULT_BUFFER_SIZE, transport: nil, draw: nil)
14
+ super(sample_rate: sample_rate, buffer_size: buffer_size, channels: channels, autostart: false,
15
+ transport: transport, draw: draw)
10
16
  @duration = duration.to_f
11
17
  @total_frames = (@duration * sample_rate).ceil
18
+ @current_frame = 0
19
+ @rendering = false
20
+ @last_render_metadata = nil
12
21
  end
13
22
 
14
23
  def current_time
15
- 0.0
24
+ @current_frame.to_f / sample_rate
16
25
  end
17
26
 
18
27
  def state
19
28
  "suspended"
20
29
  end
21
30
 
22
- def render
23
- Deftones.transport.prepare_render(@duration)
24
- Deftones.draw.prepare_render(@duration)
31
+ def render(seed: nil, metadata: false, progress: nil, cancel: nil)
25
32
  samples = Array.new(@total_frames * @channels, 0.0)
26
- frames_processed = 0
27
33
 
28
- while frames_processed < @total_frames
29
- chunk_frames = [buffer_size, @total_frames - frames_processed].min
30
- interleaved = render_block_frames(chunk_frames, frames_processed).fit_channels(@channels).interleaved
31
- start_index = frames_processed * @channels
34
+ render_each_block(seed: seed, progress: progress, cancel: cancel) do |block, start_frame|
35
+ interleaved = block.fit_channels(@channels).interleaved
36
+ start_index = start_frame * @channels
32
37
 
33
38
  samples[start_index, interleaved.length] = interleaved
34
- frames_processed += chunk_frames
35
39
  end
36
40
 
37
- IO::Buffer.new(samples, channels: @channels, sample_rate: sample_rate)
41
+ buffer = IO::Buffer.new(samples, channels: @channels, sample_rate: sample_rate)
42
+ @last_render_metadata = metadata_for(buffer)
43
+ return RenderResult.new(buffer: buffer, metadata: @last_render_metadata) if metadata
44
+
45
+ buffer
46
+ end
47
+
48
+ def render_with_metadata(**options)
49
+ render(**options, metadata: true)
50
+ end
51
+
52
+ def render_each_block(seed: nil, progress: nil, cancel: nil)
53
+ return enum_for(:render_each_block) unless block_given?
54
+
55
+ with_render_state(seed: seed) do
56
+ frames_processed = 0
57
+
58
+ while frames_processed < @total_frames
59
+ raise RenderCancelled, "render cancelled" if cancel&.call(frames_processed.to_f / @total_frames)
60
+
61
+ chunk_frames = [buffer_size, @total_frames - frames_processed].min
62
+ @current_frame = frames_processed
63
+ advance_schedulers(frames_processed, chunk_frames)
64
+ yield render_block_frames(chunk_frames, frames_processed).fit_channels(@channels), frames_processed
65
+ frames_processed += chunk_frames
66
+ progress&.call(frames_processed.to_f / @total_frames)
67
+ end
68
+
69
+ @current_frame = @total_frames
70
+ end
71
+
72
+ self
38
73
  end
39
74
 
40
- def render_to_file(path, format: nil)
41
- rendered_buffer = render
42
- rendered_buffer.save(path, format: format)
75
+ alias renderEachBlock render_each_block
76
+ alias renderWithMetadata render_with_metadata
77
+
78
+ def render_to_file(path, format: nil, streaming: false, bit_depth: 16, dither: false, dither_rng: nil,
79
+ **render_options)
80
+ if streaming
81
+ return stream_to_file(path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng,
82
+ **render_options)
83
+ end
84
+
85
+ rendered_buffer = render(**render_options)
86
+ buffer = rendered_buffer.respond_to?(:buffer) ? rendered_buffer.buffer : rendered_buffer
87
+ buffer.save(path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
43
88
  rendered_buffer
44
89
  end
90
+
91
+ private
92
+
93
+ def with_render_state(seed: nil)
94
+ previous_frame = @current_frame
95
+ previous_rendering = @rendering
96
+ previous_seed = seed ? srand(seed) : nil
97
+ @rendering = true
98
+ @current_frame = 0
99
+ yield
100
+ ensure
101
+ srand(previous_seed) if seed
102
+ @current_frame = previous_frame
103
+ @rendering = previous_rendering
104
+ end
105
+
106
+ def advance_schedulers(start_frame, chunk_frames)
107
+ window_start = start_frame.to_f / sample_rate
108
+ window_end = (start_frame + chunk_frames).to_f / sample_rate
109
+ transport.prepare_render_window(window_start, window_end)
110
+ draw.advance_to(window_end)
111
+ Deftones.transport.prepare_render_window(window_start, window_end) unless Deftones.transport.equal?(transport)
112
+ Deftones.draw.advance_to(window_end) unless Deftones.draw.equal?(draw)
113
+ end
114
+
115
+ def stream_to_file(path, format: nil, bit_depth:, dither:, dither_rng:, **render_options)
116
+ resolved_format = format || File.extname(path).delete_prefix(".").downcase.to_sym
117
+ resolved_format = :wav if resolved_format.nil? || resolved_format == :""
118
+ resolved_format = :ogg if resolved_format.to_sym == :oga
119
+ unless IO::Buffer::SAVEABLE_FORMATS.include?(resolved_format.to_sym)
120
+ raise UnsupportedAudioFormatError, "Unsupported streaming render format: #{resolved_format}"
121
+ end
122
+ return stream_compressed_to_file(path, resolved_format.to_sym, bit_depth: bit_depth, dither: dither,
123
+ dither_rng: dither_rng, **render_options) unless resolved_format.to_sym == :wav
124
+
125
+ normalized_bit_depth = validate_wav_bit_depth(bit_depth)
126
+
127
+ File.open(path, "wb") do |file|
128
+ file.write(wav_header(normalized_bit_depth))
129
+ render_each_block(**render_options) do |block, _start_frame|
130
+ file.write(pcm_payload(block.fit_channels(@channels).interleaved,
131
+ bit_depth: normalized_bit_depth,
132
+ dither: dither,
133
+ dither_rng: dither_rng))
134
+ end
135
+ end
136
+
137
+ path
138
+ end
139
+
140
+ def stream_compressed_to_file(path, format, bit_depth:, dither:, dither_rng:, **render_options)
141
+ Tempfile.create(["deftones-stream-render", ".wav"]) do |tempfile|
142
+ tempfile.close
143
+ stream_to_file(tempfile.path, format: :wav, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng,
144
+ **render_options)
145
+ encode_streamed_wav(tempfile.path, path, format)
146
+ end
147
+ path
148
+ end
149
+
150
+ def encode_streamed_wav(input_path, output_path, format)
151
+ backend = IO::Buffer.send(:encoder_backend_for, format)
152
+ raise MissingCodecBackendError, IO::Buffer.send(:missing_encoder_message, format) unless backend
153
+
154
+ if IO::Buffer.send(:custom_codec_backend?, backend)
155
+ backend.encode(input_path, output_path, format: format, sample_rate: sample_rate, channels: channels)
156
+ return
157
+ end
158
+
159
+ command = IO::Buffer.send(:encoder_command, backend, input_path, output_path, format, sample_rate, channels)
160
+ stdout, stderr, status = IO::Buffer.send(:capture_codec_command, *command)
161
+ return if status.success?
162
+
163
+ IO::Buffer.send(:raise_codec_command_error, "Failed to encode #{format}", command, stdout, stderr, status)
164
+ end
165
+
166
+ def metadata_for(buffer)
167
+ {
168
+ duration: duration,
169
+ frames: total_frames,
170
+ channels: channels,
171
+ sample_rate: sample_rate,
172
+ peak: buffer.peak,
173
+ rms: buffer.rms,
174
+ clip_count: buffer.clip_count
175
+ }
176
+ end
177
+
178
+ def wav_header(bit_depth)
179
+ bytes_per_sample = bit_depth / 8
180
+ data_size = @total_frames * @channels * bytes_per_sample
181
+ byte_rate = sample_rate * @channels * bytes_per_sample
182
+ block_align = @channels * bytes_per_sample
183
+
184
+ "RIFF" \
185
+ + [36 + data_size].pack("V") \
186
+ + "WAVEfmt " \
187
+ + [16, 1, @channels, sample_rate, byte_rate, block_align, bit_depth].pack("VvvVVvv") \
188
+ + "data" \
189
+ + [data_size].pack("V")
190
+ end
191
+
192
+ def pcm_payload(samples, bit_depth:, dither:, dither_rng:)
193
+ quantized = samples.map { |sample| quantize_pcm(sample, bit_depth, dither: dither, dither_rng: dither_rng) }
194
+
195
+ case bit_depth
196
+ when 16 then quantized.pack("s<*")
197
+ when 24 then quantized.map { |value| [value & 0xFFFFFF].pack("V")[0, 3] }.join
198
+ when 32 then quantized.pack("l<*")
199
+ end
200
+ end
201
+
202
+ def quantize_pcm(sample, bit_depth, dither:, dither_rng:)
203
+ max = (2**(bit_depth - 1)) - 1
204
+ min = -(2**(bit_depth - 1))
205
+ value = dither ? dither_sample(sample, bit_depth, dither_rng) : sample.to_f
206
+ scaled = Deftones::DSP::Helpers.clamp(value, -1.0, 1.0) * max
207
+ Deftones::DSP::Helpers.clamp(scaled.round, min, max)
208
+ end
209
+
210
+ def dither_sample(sample, bit_depth, rng)
211
+ random = rng || Random
212
+ step = 1.0 / ((2**(bit_depth - 1)) - 1)
213
+ sample.to_f + ((random.rand - random.rand) * step)
214
+ end
215
+
216
+ def validate_wav_bit_depth(bit_depth)
217
+ normalized = bit_depth.to_i
218
+ return normalized if IO::Buffer::WAV_BIT_DEPTHS.include?(normalized)
219
+
220
+ raise ArgumentError, "Unsupported WAV bit depth: #{bit_depth}"
221
+ end
45
222
  end
46
223
  end