Class: Deftones::OfflineContext

Inherits:
Context
  • Object
show all
Defined in:
lib/deftones/offline_context.rb

Defined Under Namespace

Classes: RenderCancelled, RenderResult

Constant Summary

Constants inherited from Context

Context::DEFAULT_BUFFER_SIZE, Context::DEFAULT_CHANNELS, Context::DEFAULT_SAMPLE_RATE

Instance Attribute Summary collapse

Attributes inherited from Context

#buffer_size, #draw, #latency_hint, #look_ahead, #on_stream_error, #output_device_id, #output_device_label, #sample_rate, #stream_error, #stream_status_flags, #transport

Instance Method Summary collapse

Methods inherited from Context

#block_time, #build_realtime_backend, #close, #handle_stream_error, #monotonic_time, #normalize_stream_error_mode, #output, #pull_realtime_samples, #raw_context, #realtime?, #record_stream_status_flags, #render_block_frames, #render_frames, #reset!, #resume, #running?, #sample_time, #start, #start_realtime_stream, #stop, #stream_error_mode, #stream_error_mode=

Constructor Details

#initialize(duration:, channels: 2, sample_rate: DEFAULT_SAMPLE_RATE, buffer_size: DEFAULT_BUFFER_SIZE, transport: nil, draw: nil) ⇒ OfflineContext

Returns a new instance of OfflineContext.



12
13
14
15
16
17
18
19
20
21
# File 'lib/deftones/offline_context.rb', line 12

def initialize(duration:, channels: 2, sample_rate: DEFAULT_SAMPLE_RATE,
               buffer_size: DEFAULT_BUFFER_SIZE, transport: nil, draw: nil)
  super(sample_rate: sample_rate, buffer_size: buffer_size, channels: channels, autostart: false,
        transport: transport, draw: draw)
  @duration = duration.to_f
  @total_frames = (@duration * sample_rate).ceil
  @current_frame = 0
  @rendering = false
  @last_render_metadata = nil
end

Instance Attribute Details

#channelsObject (readonly)

Returns the value of attribute channels.



10
11
12
# File 'lib/deftones/offline_context.rb', line 10

def channels
  @channels
end

#current_frameObject (readonly)

Returns the value of attribute current_frame.



10
11
12
# File 'lib/deftones/offline_context.rb', line 10

def current_frame
  @current_frame
end

#durationObject (readonly)

Returns the value of attribute duration.



10
11
12
# File 'lib/deftones/offline_context.rb', line 10

def duration
  @duration
end

#last_render_metadataObject (readonly)

Returns the value of attribute last_render_metadata.



10
11
12
# File 'lib/deftones/offline_context.rb', line 10

def 
  @last_render_metadata
end

#total_framesObject (readonly)

Returns the value of attribute total_frames.



10
11
12
# File 'lib/deftones/offline_context.rb', line 10

def total_frames
  @total_frames
end

Instance Method Details

#advance_schedulers(start_frame, chunk_frames) ⇒ Object (private)



106
107
108
109
110
111
112
113
# File 'lib/deftones/offline_context.rb', line 106

def advance_schedulers(start_frame, chunk_frames)
  window_start = start_frame.to_f / sample_rate
  window_end = (start_frame + chunk_frames).to_f / sample_rate
  transport.prepare_render_window(window_start, window_end)
  draw.advance_to(window_end)
  Deftones.transport.prepare_render_window(window_start, window_end) unless Deftones.transport.equal?(transport)
  Deftones.draw.advance_to(window_end) unless Deftones.draw.equal?(draw)
end

#current_timeObject



23
24
25
# File 'lib/deftones/offline_context.rb', line 23

def current_time
  @current_frame.to_f / sample_rate
end

#dither_sample(sample, bit_depth, rng) ⇒ Object (private)



210
211
212
213
214
# File 'lib/deftones/offline_context.rb', line 210

def dither_sample(sample, bit_depth, rng)
  random = rng || Random
  step = 1.0 / ((2**(bit_depth - 1)) - 1)
  sample.to_f + ((random.rand - random.rand) * step)
end

#encode_streamed_wav(input_path, output_path, format) ⇒ Object (private)



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/deftones/offline_context.rb', line 150

def encode_streamed_wav(input_path, output_path, format)
  backend = IO::Buffer.send(:encoder_backend_for, format)
  raise MissingCodecBackendError, IO::Buffer.send(:missing_encoder_message, format) unless backend

  if IO::Buffer.send(:custom_codec_backend?, backend)
    backend.encode(input_path, output_path, format: format, sample_rate: sample_rate, channels: channels)
    return
  end

  command = IO::Buffer.send(:encoder_command, backend, input_path, output_path, format, sample_rate, channels)
  stdout, stderr, status = IO::Buffer.send(:capture_codec_command, *command)
  return if status.success?

  IO::Buffer.send(:raise_codec_command_error, "Failed to encode #{format}", command, stdout, stderr, status)
end

#metadata_for(buffer) ⇒ Object (private)



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/deftones/offline_context.rb', line 166

def (buffer)
  {
    duration: duration,
    frames: total_frames,
    channels: channels,
    sample_rate: sample_rate,
    peak: buffer.peak,
    rms: buffer.rms,
    clip_count: buffer.clip_count
  }
end

#pcm_payload(samples, bit_depth:, dither:, dither_rng:) ⇒ Object (private)



192
193
194
195
196
197
198
199
200
# File 'lib/deftones/offline_context.rb', line 192

def pcm_payload(samples, bit_depth:, dither:, dither_rng:)
  quantized = samples.map { |sample| quantize_pcm(sample, bit_depth, dither: dither, dither_rng: dither_rng) }

  case bit_depth
  when 16 then quantized.pack("s<*")
  when 24 then quantized.map { |value| [value & 0xFFFFFF].pack("V")[0, 3] }.join
  when 32 then quantized.pack("l<*")
  end
end

#quantize_pcm(sample, bit_depth, dither:, dither_rng:) ⇒ Object (private)



202
203
204
205
206
207
208
# File 'lib/deftones/offline_context.rb', line 202

def quantize_pcm(sample, bit_depth, dither:, dither_rng:)
  max = (2**(bit_depth - 1)) - 1
  min = -(2**(bit_depth - 1))
  value = dither ? dither_sample(sample, bit_depth, dither_rng) : sample.to_f
  scaled = Deftones::DSP::Helpers.clamp(value, -1.0, 1.0) * max
  Deftones::DSP::Helpers.clamp(scaled.round, min, max)
end

#render(seed: nil, metadata: false, progress: nil, cancel: nil) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/deftones/offline_context.rb', line 31

def render(seed: nil, metadata: false, progress: nil, cancel: nil)
  samples = Array.new(@total_frames * @channels, 0.0)

  render_each_block(seed: seed, progress: progress, cancel: cancel) do |block, start_frame|
    interleaved = block.fit_channels(@channels).interleaved
    start_index = start_frame * @channels

    samples[start_index, interleaved.length] = interleaved
  end

  buffer = IO::Buffer.new(samples, channels: @channels, sample_rate: sample_rate)
  @last_render_metadata = (buffer)
  return RenderResult.new(buffer: buffer, metadata: @last_render_metadata) if 

  buffer
end

#render_each_block(seed: nil, progress: nil, cancel: nil) ⇒ Object Also known as: renderEachBlock



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/deftones/offline_context.rb', line 52

def render_each_block(seed: nil, progress: nil, cancel: nil)
  return enum_for(:render_each_block) unless block_given?

  with_render_state(seed: seed) do
    frames_processed = 0

    while frames_processed < @total_frames
      raise RenderCancelled, "render cancelled" if cancel&.call(frames_processed.to_f / @total_frames)

      chunk_frames = [buffer_size, @total_frames - frames_processed].min
      @current_frame = frames_processed
      advance_schedulers(frames_processed, chunk_frames)
      yield render_block_frames(chunk_frames, frames_processed).fit_channels(@channels), frames_processed
      frames_processed += chunk_frames
      progress&.call(frames_processed.to_f / @total_frames)
    end

    @current_frame = @total_frames
  end

  self
end

#render_to_file(path, format: nil, streaming: false, bit_depth: 16, dither: false, dither_rng: nil, **render_options) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/deftones/offline_context.rb', line 78

def render_to_file(path, format: nil, streaming: false, bit_depth: 16, dither: false, dither_rng: nil,
                   **render_options)
  if streaming
    return stream_to_file(path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng,
                                **render_options)
  end

  rendered_buffer = render(**render_options)
  buffer = rendered_buffer.respond_to?(:buffer) ? rendered_buffer.buffer : rendered_buffer
  buffer.save(path, format: format, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng)
  rendered_buffer
end

#render_with_metadata(**options) ⇒ Object Also known as: renderWithMetadata



48
49
50
# File 'lib/deftones/offline_context.rb', line 48

def (**options)
  render(**options, metadata: true)
end

#stateObject



27
28
29
# File 'lib/deftones/offline_context.rb', line 27

def state
  "suspended"
end

#stream_compressed_to_file(path, format, bit_depth:, dither:, dither_rng:, **render_options) ⇒ Object (private)



140
141
142
143
144
145
146
147
148
# File 'lib/deftones/offline_context.rb', line 140

def stream_compressed_to_file(path, format, bit_depth:, dither:, dither_rng:, **render_options)
  Tempfile.create(["deftones-stream-render", ".wav"]) do |tempfile|
    tempfile.close
    stream_to_file(tempfile.path, format: :wav, bit_depth: bit_depth, dither: dither, dither_rng: dither_rng,
                                  **render_options)
    encode_streamed_wav(tempfile.path, path, format)
  end
  path
end

#stream_to_file(path, format: nil, bit_depth:, dither:, dither_rng:, **render_options) ⇒ Object (private)



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/deftones/offline_context.rb', line 115

def stream_to_file(path, format: nil, bit_depth:, dither:, dither_rng:, **render_options)
  resolved_format = format || File.extname(path).delete_prefix(".").downcase.to_sym
  resolved_format = :wav if resolved_format.nil? || resolved_format == :""
  resolved_format = :ogg if resolved_format.to_sym == :oga
  unless IO::Buffer::SAVEABLE_FORMATS.include?(resolved_format.to_sym)
    raise UnsupportedAudioFormatError, "Unsupported streaming render format: #{resolved_format}"
  end
  return stream_compressed_to_file(path, resolved_format.to_sym, bit_depth: bit_depth, dither: dither,
                                                            dither_rng: dither_rng, **render_options) unless resolved_format.to_sym == :wav

  normalized_bit_depth = validate_wav_bit_depth(bit_depth)

  File.open(path, "wb") do |file|
    file.write(wav_header(normalized_bit_depth))
    render_each_block(**render_options) do |block, _start_frame|
      file.write(pcm_payload(block.fit_channels(@channels).interleaved,
                             bit_depth: normalized_bit_depth,
                             dither: dither,
                             dither_rng: dither_rng))
    end
  end

  path
end

#validate_wav_bit_depth(bit_depth) ⇒ Object (private)

Raises:

  • (ArgumentError)


216
217
218
219
220
221
# File 'lib/deftones/offline_context.rb', line 216

def validate_wav_bit_depth(bit_depth)
  normalized = bit_depth.to_i
  return normalized if IO::Buffer::WAV_BIT_DEPTHS.include?(normalized)

  raise ArgumentError, "Unsupported WAV bit depth: #{bit_depth}"
end

#wav_header(bit_depth) ⇒ Object (private)



178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/deftones/offline_context.rb', line 178

def wav_header(bit_depth)
  bytes_per_sample = bit_depth / 8
  data_size = @total_frames * @channels * bytes_per_sample
  byte_rate = sample_rate * @channels * bytes_per_sample
  block_align = @channels * bytes_per_sample

  "RIFF" \
    + [36 + data_size].pack("V") \
    + "WAVEfmt " \
    + [16, 1, @channels, sample_rate, byte_rate, block_align, bit_depth].pack("VvvVVvv") \
    + "data" \
    + [data_size].pack("V")
end

#with_render_state(seed: nil) ⇒ Object (private)



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/deftones/offline_context.rb', line 93

def with_render_state(seed: nil)
  previous_frame = @current_frame
  previous_rendering = @rendering
  previous_seed = seed ? srand(seed) : nil
  @rendering = true
  @current_frame = 0
  yield
ensure
  srand(previous_seed) if seed
  @current_frame = previous_frame
  @rendering = previous_rendering
end