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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -6
- data/README.md +5 -0
- data/Rakefile +50 -1
- data/lib/deftones/analysis/meter.rb +22 -2
- data/lib/deftones/component/channel.rb +1 -1
- data/lib/deftones/component/compressor.rb +127 -22
- data/lib/deftones/component/filter.rb +29 -19
- data/lib/deftones/component/merge.rb +14 -0
- data/lib/deftones/component/multiband_compressor.rb +1 -1
- data/lib/deftones/component/one_pole_filter.rb +10 -3
- data/lib/deftones/component/panner.rb +25 -2
- data/lib/deftones/component/panner3d.rb +0 -10
- data/lib/deftones/component/split.rb +14 -0
- data/lib/deftones/context.rb +90 -9
- data/lib/deftones/core/audio_block.rb +64 -5
- data/lib/deftones/core/audio_node.rb +98 -8
- data/lib/deftones/core/gain.rb +0 -8
- data/lib/deftones/core/instrument.rb +52 -10
- data/lib/deftones/core/param.rb +51 -1
- data/lib/deftones/core/signal.rb +79 -28
- data/lib/deftones/core/source.rb +71 -11
- data/lib/deftones/destination.rb +41 -17
- data/lib/deftones/draw.rb +6 -10
- data/lib/deftones/dsp/biquad.rb +9 -4
- data/lib/deftones/dsp/delay_line.rb +2 -2
- data/lib/deftones/dsp/helpers.rb +7 -0
- data/lib/deftones/effect/bit_crusher.rb +10 -2
- data/lib/deftones/effect/chebyshev.rb +7 -3
- data/lib/deftones/effect/distortion.rb +5 -3
- data/lib/deftones/effect/feedback_delay.rb +2 -1
- data/lib/deftones/effect/oversampling.rb +43 -0
- data/lib/deftones/effect/phaser.rb +2 -1
- data/lib/deftones/effect/pitch_shift.rb +1 -2
- data/lib/deftones/effect/reverb.rb +73 -5
- data/lib/deftones/event/callback_behavior.rb +7 -3
- data/lib/deftones/event/loop.rb +7 -2
- data/lib/deftones/event/part.rb +18 -3
- data/lib/deftones/event/pattern.rb +51 -6
- data/lib/deftones/event/sequence.rb +19 -5
- data/lib/deftones/event/tone_event.rb +7 -2
- data/lib/deftones/event/transport.rb +243 -21
- data/lib/deftones/instrument/poly_synth.rb +81 -15
- data/lib/deftones/instrument/sampler.rb +53 -10
- data/lib/deftones/io/buffer.rb +376 -55
- data/lib/deftones/io/buffers.rb +28 -4
- data/lib/deftones/io/recorder.rb +2 -1
- data/lib/deftones/music/frequency.rb +13 -8
- data/lib/deftones/music/midi.rb +132 -9
- data/lib/deftones/music/note.rb +13 -3
- data/lib/deftones/music/time.rb +42 -4
- data/lib/deftones/offline_context.rb +194 -17
- data/lib/deftones/portaudio_support.rb +68 -9
- data/lib/deftones/source/fat_oscillator.rb +28 -9
- data/lib/deftones/source/grain_player.rb +49 -2
- data/lib/deftones/source/noise.rb +42 -10
- data/lib/deftones/source/omni_oscillator.rb +1 -2
- data/lib/deftones/source/oscillator.rb +83 -19
- data/lib/deftones/source/player.rb +24 -6
- data/lib/deftones/source/players.rb +39 -6
- data/lib/deftones/source/tone_buffer_source.rb +12 -6
- data/lib/deftones/source/tone_oscillator_node.rb +4 -3
- data/lib/deftones/source/user_media.rb +83 -10
- data/lib/deftones/version.rb +1 -1
- data/lib/deftones.rb +108 -31
- metadata +3 -44
data/lib/deftones/io/buffers.rb
CHANGED
|
@@ -5,13 +5,27 @@ module Deftones
|
|
|
5
5
|
class Buffers
|
|
6
6
|
include Enumerable
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/deftones/io/recorder.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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)
|
data/lib/deftones/music/midi.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/deftones/music/note.rb
CHANGED
|
@@ -32,22 +32,32 @@ module Deftones
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def from_frequency(frequency)
|
|
35
|
-
|
|
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
|
|
47
|
+
raise Deftones::InvalidNoteError, "Invalid note: #{note_name}" unless match
|
|
44
48
|
|
|
45
49
|
normalized_name = normalize_name(match[1])
|
|
46
|
-
raise
|
|
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)
|
data/lib/deftones/music/time.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|