deftones 0.1.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 +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/Rakefile +8 -0
- data/examples/poly_chord.rb +14 -0
- data/examples/render_sampler.rb +16 -0
- data/examples/render_sequence.rb +13 -0
- data/examples/render_synth.rb +18 -0
- data/lib/deftones/analysis/analyser.rb +162 -0
- data/lib/deftones/analysis/dc_meter.rb +56 -0
- data/lib/deftones/analysis/fft.rb +128 -0
- data/lib/deftones/analysis/meter.rb +83 -0
- data/lib/deftones/analysis/waveform.rb +93 -0
- data/lib/deftones/component/amplitude_envelope.rb +8 -0
- data/lib/deftones/component/biquad_filter.rb +7 -0
- data/lib/deftones/component/channel.rb +109 -0
- data/lib/deftones/component/compressor.rb +64 -0
- data/lib/deftones/component/convolver.rb +99 -0
- data/lib/deftones/component/cross_fade.rb +67 -0
- data/lib/deftones/component/envelope.rb +160 -0
- data/lib/deftones/component/eq3.rb +73 -0
- data/lib/deftones/component/feedback_comb_filter.rb +75 -0
- data/lib/deftones/component/filter.rb +75 -0
- data/lib/deftones/component/follower.rb +55 -0
- data/lib/deftones/component/frequency_envelope.rb +24 -0
- data/lib/deftones/component/gate.rb +46 -0
- data/lib/deftones/component/lfo.rb +88 -0
- data/lib/deftones/component/limiter.rb +11 -0
- data/lib/deftones/component/lowpass_comb_filter.rb +43 -0
- data/lib/deftones/component/merge.rb +45 -0
- data/lib/deftones/component/mid_side_compressor.rb +43 -0
- data/lib/deftones/component/mid_side_merge.rb +53 -0
- data/lib/deftones/component/mid_side_split.rb +54 -0
- data/lib/deftones/component/mono.rb +8 -0
- data/lib/deftones/component/multiband_compressor.rb +70 -0
- data/lib/deftones/component/multiband_split.rb +133 -0
- data/lib/deftones/component/one_pole_filter.rb +71 -0
- data/lib/deftones/component/pan_vol.rb +26 -0
- data/lib/deftones/component/panner.rb +75 -0
- data/lib/deftones/component/panner3d.rb +322 -0
- data/lib/deftones/component/solo.rb +56 -0
- data/lib/deftones/component/split.rb +47 -0
- data/lib/deftones/component/volume.rb +31 -0
- data/lib/deftones/context.rb +213 -0
- data/lib/deftones/core/audio_block.rb +82 -0
- data/lib/deftones/core/audio_node.rb +262 -0
- data/lib/deftones/core/clock.rb +91 -0
- data/lib/deftones/core/computed_signal.rb +69 -0
- data/lib/deftones/core/delay.rb +44 -0
- data/lib/deftones/core/effect.rb +66 -0
- data/lib/deftones/core/emitter.rb +51 -0
- data/lib/deftones/core/gain.rb +39 -0
- data/lib/deftones/core/instrument.rb +109 -0
- data/lib/deftones/core/param.rb +31 -0
- data/lib/deftones/core/signal.rb +452 -0
- data/lib/deftones/core/signal_operator_methods.rb +73 -0
- data/lib/deftones/core/signal_operators.rb +138 -0
- data/lib/deftones/core/signal_shapers.rb +83 -0
- data/lib/deftones/core/source.rb +213 -0
- data/lib/deftones/core/synced_signal.rb +88 -0
- data/lib/deftones/destination.rb +132 -0
- data/lib/deftones/draw.rb +100 -0
- data/lib/deftones/dsp/biquad.rb +129 -0
- data/lib/deftones/dsp/delay_line.rb +41 -0
- data/lib/deftones/dsp/helpers.rb +25 -0
- data/lib/deftones/effect/auto_filter.rb +92 -0
- data/lib/deftones/effect/auto_panner.rb +57 -0
- data/lib/deftones/effect/auto_wah.rb +98 -0
- data/lib/deftones/effect/bit_crusher.rb +38 -0
- data/lib/deftones/effect/chebyshev.rb +36 -0
- data/lib/deftones/effect/chorus.rb +73 -0
- data/lib/deftones/effect/distortion.rb +22 -0
- data/lib/deftones/effect/feedback_delay.rb +38 -0
- data/lib/deftones/effect/freeverb.rb +11 -0
- data/lib/deftones/effect/frequency_shifter.rb +89 -0
- data/lib/deftones/effect/jc_reverb.rb +11 -0
- data/lib/deftones/effect/modulation_control.rb +159 -0
- data/lib/deftones/effect/phaser.rb +72 -0
- data/lib/deftones/effect/ping_pong_delay.rb +40 -0
- data/lib/deftones/effect/pitch_shift.rb +156 -0
- data/lib/deftones/effect/reverb.rb +71 -0
- data/lib/deftones/effect/stereo_widener.rb +34 -0
- data/lib/deftones/effect/tremolo.rb +52 -0
- data/lib/deftones/effect/vibrato.rb +47 -0
- data/lib/deftones/event/callback_behavior.rb +61 -0
- data/lib/deftones/event/loop.rb +53 -0
- data/lib/deftones/event/part.rb +51 -0
- data/lib/deftones/event/pattern.rb +94 -0
- data/lib/deftones/event/sequence.rb +87 -0
- data/lib/deftones/event/tone_event.rb +77 -0
- data/lib/deftones/event/transport.rb +276 -0
- data/lib/deftones/instrument/am_synth.rb +56 -0
- data/lib/deftones/instrument/duo_synth.rb +68 -0
- data/lib/deftones/instrument/fm_synth.rb +60 -0
- data/lib/deftones/instrument/membrane_synth.rb +60 -0
- data/lib/deftones/instrument/metal_synth.rb +61 -0
- data/lib/deftones/instrument/mono_synth.rb +88 -0
- data/lib/deftones/instrument/noise_synth.rb +56 -0
- data/lib/deftones/instrument/pluck_synth.rb +41 -0
- data/lib/deftones/instrument/poly_synth.rb +96 -0
- data/lib/deftones/instrument/sampler.rb +97 -0
- data/lib/deftones/instrument/synth.rb +60 -0
- data/lib/deftones/io/buffer.rb +352 -0
- data/lib/deftones/io/buffers.rb +77 -0
- data/lib/deftones/io/recorder.rb +89 -0
- data/lib/deftones/listener.rb +120 -0
- data/lib/deftones/music/frequency.rb +128 -0
- data/lib/deftones/music/midi.rb +206 -0
- data/lib/deftones/music/note.rb +58 -0
- data/lib/deftones/music/ticks.rb +106 -0
- data/lib/deftones/music/time.rb +209 -0
- data/lib/deftones/music/transport_time.rb +94 -0
- data/lib/deftones/music/unit_helpers.rb +30 -0
- data/lib/deftones/offline_context.rb +46 -0
- data/lib/deftones/portaudio_support.rb +112 -0
- data/lib/deftones/source/am_oscillator.rb +42 -0
- data/lib/deftones/source/fat_oscillator.rb +49 -0
- data/lib/deftones/source/fm_oscillator.rb +47 -0
- data/lib/deftones/source/grain_player.rb +198 -0
- data/lib/deftones/source/karplus_strong.rb +51 -0
- data/lib/deftones/source/noise.rb +99 -0
- data/lib/deftones/source/omni_oscillator.rb +175 -0
- data/lib/deftones/source/oscillator.rb +74 -0
- data/lib/deftones/source/player.rb +228 -0
- data/lib/deftones/source/players.rb +133 -0
- data/lib/deftones/source/pulse_oscillator.rb +38 -0
- data/lib/deftones/source/pwm_oscillator.rb +49 -0
- data/lib/deftones/source/tone_buffer_source.rb +136 -0
- data/lib/deftones/source/tone_oscillator_node.rb +65 -0
- data/lib/deftones/source/user_media.rb +519 -0
- data/lib/deftones/version.rb +5 -0
- data/lib/deftones.rb +542 -0
- metadata +221 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deftones
|
|
4
|
+
module Instrument
|
|
5
|
+
class NoiseSynth < Core::Instrument
|
|
6
|
+
attr_reader :noise, :filter, :envelope
|
|
7
|
+
|
|
8
|
+
def initialize(type: :white, filter_type: :bandpass, filter_frequency: 1_500.0, attack: 0.001,
|
|
9
|
+
decay: 0.12, sustain: 0.0, release: 0.1, context: Deftones.context, &block)
|
|
10
|
+
super(context: context)
|
|
11
|
+
@noise = Source::Noise.new(type: type, context: context)
|
|
12
|
+
@filter = Component::Filter.new(type: filter_type, frequency: filter_frequency, q: 0.8, context: context)
|
|
13
|
+
@envelope = Component::AmplitudeEnvelope.new(
|
|
14
|
+
attack: attack,
|
|
15
|
+
decay: decay,
|
|
16
|
+
sustain: sustain,
|
|
17
|
+
release: release,
|
|
18
|
+
context: context
|
|
19
|
+
)
|
|
20
|
+
@noise.start(0.0)
|
|
21
|
+
@noise >> @filter >> @envelope >> @output
|
|
22
|
+
block&.call(self)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def play(_note = nil, duration: "8n", at: nil, velocity: 1.0)
|
|
26
|
+
trigger_attack_release(duration, at, velocity)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def trigger_attack(time = nil, velocity = 1.0)
|
|
30
|
+
scheduled_time = resolve_time(time)
|
|
31
|
+
@envelope.trigger_attack(scheduled_time, velocity)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trigger_release(time = nil)
|
|
36
|
+
@envelope.trigger_release(resolve_time(time))
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def trigger_attack_release(duration, time = nil, velocity = 1.0)
|
|
41
|
+
scheduled_time = resolve_time(time)
|
|
42
|
+
trigger_attack(scheduled_time, velocity)
|
|
43
|
+
trigger_release(scheduled_time + Deftones::Music::Time.parse(duration))
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def resolve_time(time)
|
|
50
|
+
return context.current_time if time.nil?
|
|
51
|
+
|
|
52
|
+
Deftones::Music::Time.parse(time)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deftones
|
|
4
|
+
module Instrument
|
|
5
|
+
class PluckSynth < Core::Instrument
|
|
6
|
+
attr_reader :resonator
|
|
7
|
+
|
|
8
|
+
def initialize(decay: 0.995, damping: 0.5, context: Deftones.context, &block)
|
|
9
|
+
super(context: context)
|
|
10
|
+
@resonator = Source::KarplusStrong.new(decay: decay, damping: damping, context: context)
|
|
11
|
+
@resonator >> @output
|
|
12
|
+
block&.call(self)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def play(note, duration: "8n", at: nil, velocity: 1.0)
|
|
16
|
+
trigger_attack_release(note, duration, at, velocity)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def trigger_attack(note, time = nil, velocity = 1.0)
|
|
20
|
+
@resonator.trigger(note, resolve_time(time), velocity)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def trigger_release(_time = nil)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def trigger_attack_release(note, _duration, time = nil, velocity = 1.0)
|
|
29
|
+
trigger_attack(note, resolve_time(time), velocity)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def resolve_time(time)
|
|
35
|
+
return context.current_time if time.nil?
|
|
36
|
+
|
|
37
|
+
Deftones::Music::Time.parse(time)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deftones
|
|
4
|
+
module Instrument
|
|
5
|
+
class PolySynth < Core::Instrument
|
|
6
|
+
attr_reader :voice_pool
|
|
7
|
+
|
|
8
|
+
def initialize(voice_class = Synth, voices: 8, context: Deftones.context, **voice_options)
|
|
9
|
+
super(context: context)
|
|
10
|
+
@voice_class = voice_class
|
|
11
|
+
@voice_pool = Array.new(voices) { @voice_class.new(context: context, **voice_options) }
|
|
12
|
+
@voice_pool.each { |voice| voice >> @output }
|
|
13
|
+
@active_voices = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def play(notes, duration: "8n", at: nil, velocity: 1.0)
|
|
17
|
+
scheduled_time = resolve_time(at)
|
|
18
|
+
|
|
19
|
+
Array(notes).compact.each do |note|
|
|
20
|
+
trigger_attack(note, scheduled_time, velocity)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
release_time = scheduled_time + Deftones::Music::Time.parse(duration)
|
|
24
|
+
Array(notes).compact.each do |note|
|
|
25
|
+
trigger_release(note, release_time)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def trigger_attack(note, time = nil, velocity = 1.0)
|
|
32
|
+
voice = allocate_voice(note)
|
|
33
|
+
@active_voices.delete(note)
|
|
34
|
+
@active_voices[note] = voice
|
|
35
|
+
voice.trigger_attack(note, resolve_time(time), velocity)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def trigger_release(note, time = nil)
|
|
40
|
+
voice = @active_voices.delete(note)
|
|
41
|
+
voice&.trigger_release(resolve_time(time))
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set(**params)
|
|
46
|
+
@voice_pool.each do |voice|
|
|
47
|
+
params.each do |key, value|
|
|
48
|
+
writer = :"#{key}="
|
|
49
|
+
voice.public_send(writer, value) if voice.respond_to?(writer)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def release_all(time = nil)
|
|
56
|
+
scheduled_time = resolve_time(time)
|
|
57
|
+
@active_voices.each_value { |voice| voice.trigger_release(scheduled_time) }
|
|
58
|
+
@active_voices.clear
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def max_polyphony
|
|
63
|
+
@voice_pool.length
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def loaded?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
alias loaded loaded?
|
|
71
|
+
alias releaseAll release_all
|
|
72
|
+
|
|
73
|
+
def active?
|
|
74
|
+
@voice_pool.any?(&:active?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def allocate_voice(note)
|
|
80
|
+
return @active_voices[note] if @active_voices.key?(note)
|
|
81
|
+
|
|
82
|
+
available_voice = @voice_pool.find { |voice| !@active_voices.value?(voice) }
|
|
83
|
+
return available_voice if available_voice
|
|
84
|
+
|
|
85
|
+
_, stolen_voice = @active_voices.shift
|
|
86
|
+
stolen_voice
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_time(time)
|
|
90
|
+
return context.current_time if time.nil?
|
|
91
|
+
|
|
92
|
+
Deftones::Music::Time.parse(time)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deftones
|
|
4
|
+
module Instrument
|
|
5
|
+
class Sampler < Core::Instrument
|
|
6
|
+
attr_reader :samples, :voices
|
|
7
|
+
|
|
8
|
+
def initialize(samples:, max_voices: 8, context: Deftones.context)
|
|
9
|
+
super(context: context)
|
|
10
|
+
@samples = samples.transform_keys(&:to_s)
|
|
11
|
+
@max_voices = max_voices
|
|
12
|
+
@voices = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def play(notes, duration: "8n", at: nil, velocity: 1.0)
|
|
16
|
+
Array(notes).each do |note|
|
|
17
|
+
trigger_attack(note, at, velocity)
|
|
18
|
+
trigger_release(note, resolve_time(at) + Deftones::Music::Time.parse(duration))
|
|
19
|
+
end
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def trigger_attack(note, time = nil, velocity = 1.0)
|
|
24
|
+
buffer_note, buffer = closest_sample(note)
|
|
25
|
+
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)
|
|
27
|
+
gain = Core::Gain.new(gain: velocity, context: context)
|
|
28
|
+
player >> gain >> @output
|
|
29
|
+
player.start(resolve_time(time))
|
|
30
|
+
@voices << { note: note, player: player }
|
|
31
|
+
@voices.shift if @voices.length > @max_voices
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trigger_release(note, time = nil)
|
|
36
|
+
voice = @voices.find { |entry| entry[:note] == note }
|
|
37
|
+
voice&.fetch(:player)&.stop(resolve_time(time))
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def trigger_attack_release(note, duration, time = nil, velocity = 1.0)
|
|
42
|
+
scheduled_time = resolve_time(time)
|
|
43
|
+
trigger_attack(note, scheduled_time, velocity)
|
|
44
|
+
trigger_release(note, scheduled_time + Deftones::Music::Time.parse(duration))
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add(note, buffer)
|
|
49
|
+
@samples[note.to_s] = buffer.is_a?(Deftones::IO::Buffer) ? buffer : Deftones::IO::Buffer.load(buffer)
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get(note)
|
|
54
|
+
@samples[note.to_s]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def has?(note)
|
|
58
|
+
@samples.key?(note.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def release_all(time = nil)
|
|
62
|
+
scheduled_time = resolve_time(time)
|
|
63
|
+
@voices.each { |voice| voice[:player].stop(scheduled_time) }
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def loaded?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def dispose
|
|
72
|
+
release_all(context.current_time)
|
|
73
|
+
@voices.clear
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
alias loaded loaded?
|
|
78
|
+
alias triggerAttackRelease trigger_attack_release
|
|
79
|
+
alias releaseAll release_all
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def closest_sample(note)
|
|
84
|
+
target_midi = Deftones::Music::Note.to_midi(note)
|
|
85
|
+
@samples.min_by do |sample_note, _|
|
|
86
|
+
(Deftones::Music::Note.to_midi(sample_note) - target_midi).abs
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_time(time)
|
|
91
|
+
return context.current_time if time.nil?
|
|
92
|
+
|
|
93
|
+
Deftones::Music::Time.parse(time)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deftones
|
|
4
|
+
module Instrument
|
|
5
|
+
class Synth < Core::Instrument
|
|
6
|
+
attr_reader :oscillator, :envelope
|
|
7
|
+
|
|
8
|
+
def initialize(type: :triangle, attack: 0.005, decay: 0.1, sustain: 0.3, release: 1.0,
|
|
9
|
+
context: Deftones.context, &block)
|
|
10
|
+
super(context: context)
|
|
11
|
+
@oscillator = Source::Oscillator.new(type: type, context: context)
|
|
12
|
+
@envelope = Component::AmplitudeEnvelope.new(
|
|
13
|
+
attack: attack,
|
|
14
|
+
decay: decay,
|
|
15
|
+
sustain: sustain,
|
|
16
|
+
release: release,
|
|
17
|
+
context: context
|
|
18
|
+
)
|
|
19
|
+
@oscillator.start(0.0)
|
|
20
|
+
@oscillator >> @envelope >> @output
|
|
21
|
+
block&.call(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def play(note, duration: "8n", at: nil, velocity: 1.0)
|
|
25
|
+
trigger_attack_release(note, duration, at, velocity)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def trigger_attack(note, time = nil, velocity = 1.0)
|
|
29
|
+
scheduled_time = resolve_time(time)
|
|
30
|
+
@oscillator.frequency.set_value_at_time(note, scheduled_time)
|
|
31
|
+
@envelope.trigger_attack(scheduled_time, velocity)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trigger_release(time = nil)
|
|
36
|
+
@envelope.trigger_release(resolve_time(time))
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def trigger_attack_release(note, duration, time = nil, velocity = 1.0)
|
|
41
|
+
scheduled_time = resolve_time(time)
|
|
42
|
+
trigger_attack(note, scheduled_time, velocity)
|
|
43
|
+
trigger_release(scheduled_time + Deftones::Music::Time.parse(duration))
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def active?
|
|
48
|
+
@envelope.active?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def resolve_time(time)
|
|
54
|
+
return context.current_time if time.nil?
|
|
55
|
+
|
|
56
|
+
Deftones::Music::Time.parse(time)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module Deftones
|
|
7
|
+
module IO
|
|
8
|
+
class Buffer
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :samples, :channels, :sample_rate
|
|
12
|
+
|
|
13
|
+
COMPRESSED_EXTENSIONS = %w[.mp3 .ogg .oga].freeze
|
|
14
|
+
SAVEABLE_FORMATS = %i[wav mp3 ogg].freeze
|
|
15
|
+
|
|
16
|
+
def self.interleave(mono_samples, channels)
|
|
17
|
+
return mono_samples.dup if channels == 1
|
|
18
|
+
|
|
19
|
+
mono_samples.flat_map { |sample| Array.new(channels, sample) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_mono(samples, channels: 1, sample_rate: Context::DEFAULT_SAMPLE_RATE)
|
|
23
|
+
interleaved = channels == 1 ? samples : interleave(samples, channels)
|
|
24
|
+
new(interleaved, channels: channels, sample_rate: sample_rate)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_array(samples, sample_rate: Context::DEFAULT_SAMPLE_RATE, channels: nil)
|
|
28
|
+
if samples.first.is_a?(Array)
|
|
29
|
+
channel_count = channels || samples.length
|
|
30
|
+
frame_count = samples.map(&:length).max || 0
|
|
31
|
+
interleaved = Array.new(frame_count * channel_count, 0.0)
|
|
32
|
+
|
|
33
|
+
frame_count.times do |frame_index|
|
|
34
|
+
channel_count.times do |channel_index|
|
|
35
|
+
source_channel = samples[channel_index] || []
|
|
36
|
+
interleaved[(frame_index * channel_count) + channel_index] = source_channel[frame_index].to_f
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
new(interleaved, channels: channel_count, sample_rate: sample_rate)
|
|
41
|
+
else
|
|
42
|
+
from_mono(samples, channels: channels || 1, sample_rate: sample_rate)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.from_url(path)
|
|
47
|
+
load(path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.loaded
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
alias fromArray from_array
|
|
56
|
+
alias fromUrl from_url
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.load(path)
|
|
60
|
+
extension = File.extname(path).downcase
|
|
61
|
+
return load_wav(path) if extension == ".wav"
|
|
62
|
+
return load_compressed(path, extension) if COMPRESSED_EXTENSIONS.include?(extension)
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "Unsupported audio format: #{extension}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize(samples, channels:, sample_rate:)
|
|
68
|
+
@samples = samples.map(&:to_f)
|
|
69
|
+
@channels = channels
|
|
70
|
+
@sample_rate = sample_rate
|
|
71
|
+
@disposed = false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def each(&block)
|
|
75
|
+
return enum_for(:each) unless block
|
|
76
|
+
|
|
77
|
+
mono.each(&block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def each_frame
|
|
81
|
+
return enum_for(:each_frame) unless block_given?
|
|
82
|
+
|
|
83
|
+
frames.times do |frame_index|
|
|
84
|
+
yield frame(frame_index)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def frames
|
|
89
|
+
@samples.length / @channels
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def duration
|
|
93
|
+
frames.to_f / @sample_rate
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def length
|
|
97
|
+
frames
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def loaded?
|
|
101
|
+
!@disposed
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def mono
|
|
105
|
+
return @samples if @channels == 1
|
|
106
|
+
|
|
107
|
+
Array.new(frames) do |frame|
|
|
108
|
+
offset = frame * @channels
|
|
109
|
+
@samples[offset, @channels].sum / @channels.to_f
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def peak
|
|
114
|
+
@samples.map(&:abs).max || 0.0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rms
|
|
118
|
+
return 0.0 if @samples.empty?
|
|
119
|
+
|
|
120
|
+
Math.sqrt(@samples.sum { |sample| sample * sample } / @samples.length)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def [](frame_index, channel = nil)
|
|
124
|
+
return mono[frame_index] if channel.nil?
|
|
125
|
+
|
|
126
|
+
@samples[(frame_index * @channels) + channel]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def frame(frame_index)
|
|
130
|
+
offset = frame_index * @channels
|
|
131
|
+
@samples[offset, @channels]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def number_of_channels
|
|
135
|
+
@channels
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def get_channel_data(channel)
|
|
139
|
+
channel_index = channel.to_i
|
|
140
|
+
raise ArgumentError, "channel is out of range" if channel_index.negative? || channel_index >= @channels
|
|
141
|
+
|
|
142
|
+
Array.new(frames) { |frame_index| self[frame_index, channel_index] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def to_array
|
|
146
|
+
Array.new(@channels) { |channel_index| get_channel_data(channel_index) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def sample_at(frame_position, channel = 0)
|
|
150
|
+
return 0.0 if @samples.empty?
|
|
151
|
+
|
|
152
|
+
clamped_position = Deftones::DSP::Helpers.clamp(frame_position.to_f, 0.0, [frames - 1, 0].max)
|
|
153
|
+
lower = clamped_position.floor
|
|
154
|
+
upper = [lower + 1, frames - 1].min
|
|
155
|
+
fraction = clamped_position - lower
|
|
156
|
+
lower_sample = self[lower, [channel, @channels - 1].min]
|
|
157
|
+
upper_sample = self[upper, [channel, @channels - 1].min]
|
|
158
|
+
Deftones::DSP::Helpers.lerp(lower_sample, upper_sample, fraction)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def slice(start_frame, length)
|
|
162
|
+
frame_count = [length.to_i, 0].max
|
|
163
|
+
offset = start_frame.to_i * @channels
|
|
164
|
+
subset = @samples.slice(offset, frame_count * @channels) || []
|
|
165
|
+
self.class.new(subset, channels: @channels, sample_rate: @sample_rate)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def reverse
|
|
169
|
+
reversed_frames = each_frame.to_a.reverse.flatten
|
|
170
|
+
self.class.new(reversed_frames, channels: @channels, sample_rate: @sample_rate)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def normalize(target_peak = 0.99)
|
|
174
|
+
return self.class.new(@samples, channels: @channels, sample_rate: @sample_rate) if peak.zero?
|
|
175
|
+
|
|
176
|
+
scale = target_peak.to_f / peak
|
|
177
|
+
self.class.new(@samples.map { |sample| sample * scale }, channels: @channels, sample_rate: @sample_rate)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def mixdown
|
|
181
|
+
self.class.new(mono, channels: 1, sample_rate: @sample_rate)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def dispose
|
|
185
|
+
@samples = []
|
|
186
|
+
@disposed = true
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
alias numberOfChannels number_of_channels
|
|
191
|
+
alias getChannelData get_channel_data
|
|
192
|
+
alias toArray to_array
|
|
193
|
+
|
|
194
|
+
def save(path, format: nil)
|
|
195
|
+
resolved_format = self.class.send(:resolve_save_format, path, format)
|
|
196
|
+
raise ArgumentError, "Unsupported format: #{resolved_format}" unless SAVEABLE_FORMATS.include?(resolved_format)
|
|
197
|
+
|
|
198
|
+
case resolved_format
|
|
199
|
+
when :wav
|
|
200
|
+
save_wav(path)
|
|
201
|
+
when :mp3, :ogg
|
|
202
|
+
save_compressed(path, resolved_format)
|
|
203
|
+
end
|
|
204
|
+
path
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def save_wav(path)
|
|
210
|
+
sample_buffer = Wavify::Core::SampleBuffer.new(
|
|
211
|
+
@samples,
|
|
212
|
+
self.class.send(:wavify_work_format, @channels, @sample_rate)
|
|
213
|
+
)
|
|
214
|
+
Wavify::Codecs::Wav.write(
|
|
215
|
+
path,
|
|
216
|
+
sample_buffer,
|
|
217
|
+
format: self.class.send(:wavify_wav_format, @channels, @sample_rate)
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def save_compressed(path, format)
|
|
222
|
+
backend = self.class.send(:encoder_backend_for, format)
|
|
223
|
+
raise ArgumentError, self.class.send(:missing_encoder_message, format) unless backend
|
|
224
|
+
|
|
225
|
+
Tempfile.create(["deftones-buffer-export", ".wav"]) do |tempfile|
|
|
226
|
+
tempfile.close
|
|
227
|
+
save_wav(tempfile.path)
|
|
228
|
+
stdout, stderr, status = Open3.capture3(*self.class.send(:encoder_command, backend, tempfile.path, path, format))
|
|
229
|
+
return if status.success?
|
|
230
|
+
|
|
231
|
+
message = [stderr, stdout].map(&:strip).reject(&:empty?).first || "unknown encoder error"
|
|
232
|
+
raise ArgumentError, "Failed to encode #{format}: #{message}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class << self
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def load_wav(path)
|
|
240
|
+
sample_buffer = Wavify::Codecs::Wav.read(path)
|
|
241
|
+
float_buffer = sample_buffer.convert(wavify_work_format(sample_buffer.format.channels, sample_buffer.format.sample_rate))
|
|
242
|
+
new(float_buffer.samples, channels: float_buffer.format.channels, sample_rate: float_buffer.format.sample_rate)
|
|
243
|
+
rescue Wavify::Error => error
|
|
244
|
+
raise ArgumentError, "Failed to load WAV: #{error.message}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def load_compressed(path, extension)
|
|
248
|
+
backend = decoder_backend_for(extension)
|
|
249
|
+
raise ArgumentError, missing_decoder_message(extension) unless backend
|
|
250
|
+
|
|
251
|
+
Tempfile.create(["deftones-buffer", ".wav"]) do |tempfile|
|
|
252
|
+
tempfile.close
|
|
253
|
+
stdout, stderr, status = Open3.capture3(*decoder_command(backend, path, tempfile.path))
|
|
254
|
+
next load_wav(tempfile.path) if status.success?
|
|
255
|
+
|
|
256
|
+
message = [stderr, stdout].map(&:strip).reject(&:empty?).first || "unknown decoder error"
|
|
257
|
+
raise ArgumentError, "Failed to decode #{extension}: #{message}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def wavify_work_format(channels, sample_rate)
|
|
262
|
+
Wavify::Core::Format.new(
|
|
263
|
+
channels: channels,
|
|
264
|
+
sample_rate: sample_rate,
|
|
265
|
+
bit_depth: 32,
|
|
266
|
+
sample_format: :float
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def wavify_wav_format(channels, sample_rate)
|
|
271
|
+
Wavify::Core::Format.new(
|
|
272
|
+
channels: channels,
|
|
273
|
+
sample_rate: sample_rate,
|
|
274
|
+
bit_depth: 16,
|
|
275
|
+
sample_format: :pcm
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def decoder_backend_for(extension)
|
|
280
|
+
return :ffmpeg if executable_available?("ffmpeg")
|
|
281
|
+
return :afconvert if extension == ".mp3" && executable_available?("afconvert")
|
|
282
|
+
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def encoder_backend_for(format)
|
|
287
|
+
return :ffmpeg if executable_available?("ffmpeg")
|
|
288
|
+
return :afconvert if format == :mp3 && executable_available?("afconvert")
|
|
289
|
+
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def decoder_command(backend, input_path, output_path)
|
|
294
|
+
case backend
|
|
295
|
+
when :ffmpeg
|
|
296
|
+
["ffmpeg", "-v", "error", "-y", "-i", input_path, "-f", "wav", output_path]
|
|
297
|
+
when :afconvert
|
|
298
|
+
["afconvert", "-f", "WAVE", "-d", "LEI16", input_path, output_path]
|
|
299
|
+
else
|
|
300
|
+
raise ArgumentError, "Unknown decoder backend: #{backend}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def encoder_command(backend, input_path, output_path, format)
|
|
305
|
+
case backend
|
|
306
|
+
when :ffmpeg
|
|
307
|
+
container = format == :ogg ? "ogg" : format.to_s
|
|
308
|
+
["ffmpeg", "-v", "error", "-y", "-i", input_path, "-f", container, output_path]
|
|
309
|
+
when :afconvert
|
|
310
|
+
raise ArgumentError, "afconvert only supports mp3 export" unless format == :mp3
|
|
311
|
+
|
|
312
|
+
["afconvert", "-f", "MPG3", "-d", ".mp3", input_path, output_path]
|
|
313
|
+
else
|
|
314
|
+
raise ArgumentError, "Unknown encoder backend: #{backend}"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def executable_available?(name)
|
|
319
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
|
|
320
|
+
executable = File.join(directory, name)
|
|
321
|
+
File.file?(executable) && File.executable?(executable)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def missing_decoder_message(extension)
|
|
326
|
+
"No decoder available for #{extension}. Install ffmpeg to enable compressed audio loading."
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def missing_encoder_message(format)
|
|
330
|
+
"No encoder available for #{format}. Install ffmpeg to enable compressed audio export."
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def resolve_save_format(path, format)
|
|
334
|
+
return normalize_format(format) if format
|
|
335
|
+
|
|
336
|
+
extension = File.extname(path).downcase
|
|
337
|
+
return :mp3 if extension == ".mp3"
|
|
338
|
+
return :ogg if COMPRESSED_EXTENSIONS.include?(extension)
|
|
339
|
+
|
|
340
|
+
:wav
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def normalize_format(format)
|
|
344
|
+
normalized = format.to_sym
|
|
345
|
+
return :ogg if normalized == :oga
|
|
346
|
+
|
|
347
|
+
normalized
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|