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.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +5 -0
  3. data/CHANGELOG.md +12 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +197 -0
  6. data/Rakefile +8 -0
  7. data/examples/poly_chord.rb +14 -0
  8. data/examples/render_sampler.rb +16 -0
  9. data/examples/render_sequence.rb +13 -0
  10. data/examples/render_synth.rb +18 -0
  11. data/lib/deftones/analysis/analyser.rb +162 -0
  12. data/lib/deftones/analysis/dc_meter.rb +56 -0
  13. data/lib/deftones/analysis/fft.rb +128 -0
  14. data/lib/deftones/analysis/meter.rb +83 -0
  15. data/lib/deftones/analysis/waveform.rb +93 -0
  16. data/lib/deftones/component/amplitude_envelope.rb +8 -0
  17. data/lib/deftones/component/biquad_filter.rb +7 -0
  18. data/lib/deftones/component/channel.rb +109 -0
  19. data/lib/deftones/component/compressor.rb +64 -0
  20. data/lib/deftones/component/convolver.rb +99 -0
  21. data/lib/deftones/component/cross_fade.rb +67 -0
  22. data/lib/deftones/component/envelope.rb +160 -0
  23. data/lib/deftones/component/eq3.rb +73 -0
  24. data/lib/deftones/component/feedback_comb_filter.rb +75 -0
  25. data/lib/deftones/component/filter.rb +75 -0
  26. data/lib/deftones/component/follower.rb +55 -0
  27. data/lib/deftones/component/frequency_envelope.rb +24 -0
  28. data/lib/deftones/component/gate.rb +46 -0
  29. data/lib/deftones/component/lfo.rb +88 -0
  30. data/lib/deftones/component/limiter.rb +11 -0
  31. data/lib/deftones/component/lowpass_comb_filter.rb +43 -0
  32. data/lib/deftones/component/merge.rb +45 -0
  33. data/lib/deftones/component/mid_side_compressor.rb +43 -0
  34. data/lib/deftones/component/mid_side_merge.rb +53 -0
  35. data/lib/deftones/component/mid_side_split.rb +54 -0
  36. data/lib/deftones/component/mono.rb +8 -0
  37. data/lib/deftones/component/multiband_compressor.rb +70 -0
  38. data/lib/deftones/component/multiband_split.rb +133 -0
  39. data/lib/deftones/component/one_pole_filter.rb +71 -0
  40. data/lib/deftones/component/pan_vol.rb +26 -0
  41. data/lib/deftones/component/panner.rb +75 -0
  42. data/lib/deftones/component/panner3d.rb +322 -0
  43. data/lib/deftones/component/solo.rb +56 -0
  44. data/lib/deftones/component/split.rb +47 -0
  45. data/lib/deftones/component/volume.rb +31 -0
  46. data/lib/deftones/context.rb +213 -0
  47. data/lib/deftones/core/audio_block.rb +82 -0
  48. data/lib/deftones/core/audio_node.rb +262 -0
  49. data/lib/deftones/core/clock.rb +91 -0
  50. data/lib/deftones/core/computed_signal.rb +69 -0
  51. data/lib/deftones/core/delay.rb +44 -0
  52. data/lib/deftones/core/effect.rb +66 -0
  53. data/lib/deftones/core/emitter.rb +51 -0
  54. data/lib/deftones/core/gain.rb +39 -0
  55. data/lib/deftones/core/instrument.rb +109 -0
  56. data/lib/deftones/core/param.rb +31 -0
  57. data/lib/deftones/core/signal.rb +452 -0
  58. data/lib/deftones/core/signal_operator_methods.rb +73 -0
  59. data/lib/deftones/core/signal_operators.rb +138 -0
  60. data/lib/deftones/core/signal_shapers.rb +83 -0
  61. data/lib/deftones/core/source.rb +213 -0
  62. data/lib/deftones/core/synced_signal.rb +88 -0
  63. data/lib/deftones/destination.rb +132 -0
  64. data/lib/deftones/draw.rb +100 -0
  65. data/lib/deftones/dsp/biquad.rb +129 -0
  66. data/lib/deftones/dsp/delay_line.rb +41 -0
  67. data/lib/deftones/dsp/helpers.rb +25 -0
  68. data/lib/deftones/effect/auto_filter.rb +92 -0
  69. data/lib/deftones/effect/auto_panner.rb +57 -0
  70. data/lib/deftones/effect/auto_wah.rb +98 -0
  71. data/lib/deftones/effect/bit_crusher.rb +38 -0
  72. data/lib/deftones/effect/chebyshev.rb +36 -0
  73. data/lib/deftones/effect/chorus.rb +73 -0
  74. data/lib/deftones/effect/distortion.rb +22 -0
  75. data/lib/deftones/effect/feedback_delay.rb +38 -0
  76. data/lib/deftones/effect/freeverb.rb +11 -0
  77. data/lib/deftones/effect/frequency_shifter.rb +89 -0
  78. data/lib/deftones/effect/jc_reverb.rb +11 -0
  79. data/lib/deftones/effect/modulation_control.rb +159 -0
  80. data/lib/deftones/effect/phaser.rb +72 -0
  81. data/lib/deftones/effect/ping_pong_delay.rb +40 -0
  82. data/lib/deftones/effect/pitch_shift.rb +156 -0
  83. data/lib/deftones/effect/reverb.rb +71 -0
  84. data/lib/deftones/effect/stereo_widener.rb +34 -0
  85. data/lib/deftones/effect/tremolo.rb +52 -0
  86. data/lib/deftones/effect/vibrato.rb +47 -0
  87. data/lib/deftones/event/callback_behavior.rb +61 -0
  88. data/lib/deftones/event/loop.rb +53 -0
  89. data/lib/deftones/event/part.rb +51 -0
  90. data/lib/deftones/event/pattern.rb +94 -0
  91. data/lib/deftones/event/sequence.rb +87 -0
  92. data/lib/deftones/event/tone_event.rb +77 -0
  93. data/lib/deftones/event/transport.rb +276 -0
  94. data/lib/deftones/instrument/am_synth.rb +56 -0
  95. data/lib/deftones/instrument/duo_synth.rb +68 -0
  96. data/lib/deftones/instrument/fm_synth.rb +60 -0
  97. data/lib/deftones/instrument/membrane_synth.rb +60 -0
  98. data/lib/deftones/instrument/metal_synth.rb +61 -0
  99. data/lib/deftones/instrument/mono_synth.rb +88 -0
  100. data/lib/deftones/instrument/noise_synth.rb +56 -0
  101. data/lib/deftones/instrument/pluck_synth.rb +41 -0
  102. data/lib/deftones/instrument/poly_synth.rb +96 -0
  103. data/lib/deftones/instrument/sampler.rb +97 -0
  104. data/lib/deftones/instrument/synth.rb +60 -0
  105. data/lib/deftones/io/buffer.rb +352 -0
  106. data/lib/deftones/io/buffers.rb +77 -0
  107. data/lib/deftones/io/recorder.rb +89 -0
  108. data/lib/deftones/listener.rb +120 -0
  109. data/lib/deftones/music/frequency.rb +128 -0
  110. data/lib/deftones/music/midi.rb +206 -0
  111. data/lib/deftones/music/note.rb +58 -0
  112. data/lib/deftones/music/ticks.rb +106 -0
  113. data/lib/deftones/music/time.rb +209 -0
  114. data/lib/deftones/music/transport_time.rb +94 -0
  115. data/lib/deftones/music/unit_helpers.rb +30 -0
  116. data/lib/deftones/offline_context.rb +46 -0
  117. data/lib/deftones/portaudio_support.rb +112 -0
  118. data/lib/deftones/source/am_oscillator.rb +42 -0
  119. data/lib/deftones/source/fat_oscillator.rb +49 -0
  120. data/lib/deftones/source/fm_oscillator.rb +47 -0
  121. data/lib/deftones/source/grain_player.rb +198 -0
  122. data/lib/deftones/source/karplus_strong.rb +51 -0
  123. data/lib/deftones/source/noise.rb +99 -0
  124. data/lib/deftones/source/omni_oscillator.rb +175 -0
  125. data/lib/deftones/source/oscillator.rb +74 -0
  126. data/lib/deftones/source/player.rb +228 -0
  127. data/lib/deftones/source/players.rb +133 -0
  128. data/lib/deftones/source/pulse_oscillator.rb +38 -0
  129. data/lib/deftones/source/pwm_oscillator.rb +49 -0
  130. data/lib/deftones/source/tone_buffer_source.rb +136 -0
  131. data/lib/deftones/source/tone_oscillator_node.rb +65 -0
  132. data/lib/deftones/source/user_media.rb +519 -0
  133. data/lib/deftones/version.rb +5 -0
  134. data/lib/deftones.rb +542 -0
  135. 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