deftones 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -6
  3. data/README.md +5 -0
  4. data/Rakefile +50 -1
  5. data/lib/deftones/analysis/meter.rb +22 -2
  6. data/lib/deftones/component/channel.rb +1 -1
  7. data/lib/deftones/component/compressor.rb +127 -22
  8. data/lib/deftones/component/filter.rb +29 -19
  9. data/lib/deftones/component/merge.rb +14 -0
  10. data/lib/deftones/component/multiband_compressor.rb +1 -1
  11. data/lib/deftones/component/one_pole_filter.rb +10 -3
  12. data/lib/deftones/component/panner.rb +25 -2
  13. data/lib/deftones/component/panner3d.rb +0 -10
  14. data/lib/deftones/component/split.rb +14 -0
  15. data/lib/deftones/context.rb +90 -9
  16. data/lib/deftones/core/audio_block.rb +64 -5
  17. data/lib/deftones/core/audio_node.rb +98 -8
  18. data/lib/deftones/core/gain.rb +0 -8
  19. data/lib/deftones/core/instrument.rb +52 -10
  20. data/lib/deftones/core/param.rb +51 -1
  21. data/lib/deftones/core/signal.rb +79 -28
  22. data/lib/deftones/core/source.rb +71 -11
  23. data/lib/deftones/destination.rb +41 -17
  24. data/lib/deftones/draw.rb +6 -10
  25. data/lib/deftones/dsp/biquad.rb +9 -4
  26. data/lib/deftones/dsp/delay_line.rb +2 -2
  27. data/lib/deftones/dsp/helpers.rb +7 -0
  28. data/lib/deftones/effect/bit_crusher.rb +10 -2
  29. data/lib/deftones/effect/chebyshev.rb +7 -3
  30. data/lib/deftones/effect/distortion.rb +5 -3
  31. data/lib/deftones/effect/feedback_delay.rb +2 -1
  32. data/lib/deftones/effect/oversampling.rb +43 -0
  33. data/lib/deftones/effect/phaser.rb +2 -1
  34. data/lib/deftones/effect/pitch_shift.rb +1 -2
  35. data/lib/deftones/effect/reverb.rb +73 -5
  36. data/lib/deftones/event/callback_behavior.rb +7 -3
  37. data/lib/deftones/event/loop.rb +7 -2
  38. data/lib/deftones/event/part.rb +18 -3
  39. data/lib/deftones/event/pattern.rb +51 -6
  40. data/lib/deftones/event/sequence.rb +19 -5
  41. data/lib/deftones/event/tone_event.rb +7 -2
  42. data/lib/deftones/event/transport.rb +243 -21
  43. data/lib/deftones/instrument/poly_synth.rb +81 -15
  44. data/lib/deftones/instrument/sampler.rb +53 -10
  45. data/lib/deftones/io/buffer.rb +376 -55
  46. data/lib/deftones/io/buffers.rb +28 -4
  47. data/lib/deftones/io/recorder.rb +2 -1
  48. data/lib/deftones/music/frequency.rb +13 -8
  49. data/lib/deftones/music/midi.rb +132 -9
  50. data/lib/deftones/music/note.rb +13 -3
  51. data/lib/deftones/music/time.rb +42 -4
  52. data/lib/deftones/offline_context.rb +194 -17
  53. data/lib/deftones/portaudio_support.rb +68 -9
  54. data/lib/deftones/source/fat_oscillator.rb +28 -9
  55. data/lib/deftones/source/grain_player.rb +49 -2
  56. data/lib/deftones/source/noise.rb +42 -10
  57. data/lib/deftones/source/omni_oscillator.rb +1 -2
  58. data/lib/deftones/source/oscillator.rb +83 -19
  59. data/lib/deftones/source/player.rb +24 -6
  60. data/lib/deftones/source/players.rb +39 -6
  61. data/lib/deftones/source/tone_buffer_source.rb +12 -6
  62. data/lib/deftones/source/tone_oscillator_node.rb +4 -3
  63. data/lib/deftones/source/user_media.rb +83 -10
  64. data/lib/deftones/version.rb +1 -1
  65. data/lib/deftones.rb +108 -31
  66. metadata +3 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80388e6ab20e1b3a9affc4cca0a73812c482003d1ac124b80ccfb1714c94a4a5
4
- data.tar.gz: 30a59df05c0faec27f12794bb167d80b5ab0ce61c6273a3c2d77df72106a11fc
3
+ metadata.gz: 6215833a8d30fe3a76382b1928335e8a22b94df1e1649f7fb0b028f5836d9b3e
4
+ data.tar.gz: 943cdc21e72614d018ca04709763cf8e38d1c646c071a53311c3a0e5aaef97ef
5
5
  SHA512:
6
- metadata.gz: 4fed0062263092487ce098080789d29f754151f6991fbd6479fe6cfa35f8c3f248a2e5c2f8d4c06206cfbdc3447ea0569d46052f72f4763aa6a9617a0a3549b1
7
- data.tar.gz: 611b45acf4df1b5621a137892ff04ecb286634c96185a29d34b41e21e8bfec57181f0e296b24e6fab634815a036d95e9716786d40093e2fb49d12176cf0bbac3
6
+ metadata.gz: 07dd0a8c79f2ed19114d2af15ed16a7a622a9dcbe816cf2573940af20e028d0fac81c37c6f162a9ea8529d0b635aa61a0da0d066cd424e069f56b9f8015c63e6
7
+ data.tar.gz: 494465a98c4407ac72999dbc9070351a2b39cdc013af4f957129f50cdcae524a47efd9f16b6a31723da62a0d5178fbdcda53175e3a88e831a057afb95d000cc2
data/CHANGELOG.md CHANGED
@@ -2,11 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- - Added realtime PortAudio stream integration with lazy context startup.
6
- - Added `UserMedia` live capture support through pluggable capture backends.
7
- - Added `Buffers`, compressed audio import, MIDI I/O helpers, and lower-level routing primitives.
8
- - Added MP3 export support for offline rendering when `ffmpeg` or `afconvert` is available.
5
+ ## 1.0.0 - 2026-05-25
9
6
 
10
- ## 0.1.0
7
+ - Expanded the public API surface across sources, instruments, effects, analysis, transport/event helpers, music-unit wrappers, and top-level compatibility helpers.
8
+ - Improved transport and event scheduling with context-scoped schedulers, scheduler-window offline rendering, tempo/state automation, richer pattern scheduling, and duplicate-safe realtime lookahead.
9
+ - Expanded sample, buffer, and render workflows with explicit resampling, interpolation modes, LUFS normalization, slice metadata, codec capability reporting, streamed compressed renders, codec path/format validation, and WAV bit depth/dither options.
10
+ - Added more synthesis and effects controls, including grain jitter/window options, selectable panner pan laws, reverb damping controls, compressor detector controls, nonlinear effect oversampling, sampler playback policies, instrument loaded state, and packed audio block access.
11
+ - Improved audio correctness and failure modes with stricter graph routing, voice state, source type, automation, param, event, codec, and music-value validation; clearer music value errors; feedback delay clamping; triangle oscillator fixes; filter state resets; and denormal DSP handling.
12
+ - Improved realtime and MIDI workflows with selectable realtime output devices, explicit realtime stream error policy, sample rate mismatch detection, metering diagnostics, MIDI device session handling, and MIDI-to-transport/target routing.
13
+ - Optional realtime, MIDI, and codec backends now load lazily and expose capability queries, so offline rendering can run without native realtime, MIDI, or codec dependencies.
11
14
 
12
- - Initial public MVP release.
15
+ ## 0.1.0 - 2026-03-27
16
+
17
+ - Initial release.
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Deftones
2
2
 
3
+ [![CI](https://github.com/ydah/deftones/actions/workflows/main.yml/badge.svg)](https://github.com/ydah/deftones/actions/workflows/main.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/deftones.svg)](https://rubygems.org/gems/deftones)
5
+ [![Required Ruby](https://img.shields.io/gem/ruby-version/deftones.svg)](https://rubygems.org/gems/deftones)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
7
+
3
8
  Deftones is a Ruby audio synthesis library with a flexible node graph, oscillator and synth variants, effects, transport/event scheduling, sample playback, analysis utilities, offline rendering, and an optional PortAudio-backed realtime context.
4
9
 
5
10
  ## Features
data/Rakefile CHANGED
@@ -1,8 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rbconfig"
4
5
  require "rspec/core/rake_task"
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
7
8
 
8
- task default: :spec
9
+ namespace :quality do
10
+ desc "Verify gem package file boundaries"
11
+ task :gem_files do
12
+ spec = Gem::Specification.load("deftones.gemspec")
13
+ forbidden_prefixes = %w[.github/ spec/ build/ pkg/]
14
+ forbidden_files = spec.files.select { |file| forbidden_prefixes.any? { |prefix| file.start_with?(prefix) } }
15
+ raise "Unexpected files in gem: #{forbidden_files.join(', ')}" unless forbidden_files.empty?
16
+
17
+ large_files = spec.files.select { |file| File.file?(file) && File.size(file) > 1_000_000 }
18
+ raise "Large files in gem: #{large_files.join(', ')}" unless large_files.empty?
19
+ end
20
+
21
+ desc "Verify optional compressed audio backend detection without PATH tools"
22
+ task :optional_backends do
23
+ require_relative "lib/deftones"
24
+
25
+ previous_path = ENV["PATH"]
26
+ previous_backend = Deftones::IO::Buffer.codec_backend
27
+ ENV["PATH"] = ""
28
+ Deftones::IO::Buffer.codec_backend = nil
29
+ raise "Compressed audio backend should be unavailable without PATH tools" if Deftones.compressed_audio_available?
30
+ ensure
31
+ Deftones::IO::Buffer.codec_backend = previous_backend if defined?(Deftones::IO::Buffer)
32
+ ENV["PATH"] = previous_path
33
+ end
34
+
35
+ desc "Verify the library loads cleanly with Ruby warnings enabled"
36
+ task :require_warnings do
37
+ ruby = RbConfig.ruby
38
+ sh ruby, "-w", "-Ilib", "-e", "require 'deftones'; puts Deftones.version"
39
+ end
40
+ end
41
+
42
+ namespace :release do
43
+ desc "Verify release tag and changelog metadata"
44
+ task :verify do
45
+ require_relative "lib/deftones/version"
46
+
47
+ tag = ENV["GITHUB_REF_NAME"] || `git tag --points-at HEAD`.lines.first&.strip
48
+ expected_tag = "v#{Deftones::VERSION}"
49
+ raise "Release tag #{tag.inspect} does not match #{expected_tag}" if tag && !tag.empty? && tag != expected_tag
50
+
51
+ changelog = File.read("CHANGELOG.md")
52
+ raise "CHANGELOG.md is missing #{Deftones::VERSION}" unless changelog.include?("## #{Deftones::VERSION}")
53
+ end
54
+ end
55
+
56
+ task quality: ["quality:gem_files", "quality:optional_backends", "quality:require_warnings"]
57
+ task default: %i[spec quality]
@@ -3,14 +3,16 @@
3
3
  module Deftones
4
4
  module Analysis
5
5
  class Meter < Core::AudioNode
6
- attr_accessor :normal_range
6
+ attr_accessor :normal_range, :clip_threshold
7
7
 
8
- def initialize(smoothing: 0.8, normal_range: false, channels: 1, context: Deftones.context)
8
+ def initialize(smoothing: 0.8, normal_range: false, channels: 1, clip_threshold: 1.0, context: Deftones.context)
9
9
  super(context: context)
10
10
  @channels = [channels.to_i, 1].max
11
11
  @peak_values = Array.new(@channels, 0.0)
12
12
  @rms_values = Array.new(@channels, 0.0)
13
+ @clip_counts = Array.new(@channels, 0)
13
14
  @normal_range = !!normal_range
15
+ @clip_threshold = clip_threshold.to_f
14
16
  self.smoothing = smoothing
15
17
  end
16
18
 
@@ -26,6 +28,17 @@ module Deftones
26
28
  @rms_values.length == 1 ? @rms_values.first : @rms_values.dup
27
29
  end
28
30
 
31
+ def clip_count
32
+ @clip_counts.length == 1 ? @clip_counts.first : @clip_counts.dup
33
+ end
34
+
35
+ def reset
36
+ @peak_values.fill(0.0)
37
+ @rms_values.fill(0.0)
38
+ @clip_counts.fill(0)
39
+ self
40
+ end
41
+
29
42
  def smoothing
30
43
  @smoothing
31
44
  end
@@ -57,6 +70,7 @@ module Deftones
57
70
  segment = analysis_block.channel_data[channel_index].first(num_frames)
58
71
  instantaneous_peak = segment.map(&:abs).max || 0.0
59
72
  instantaneous_rms = Math.sqrt(segment.sum { |sample| sample * sample } / [segment.length, 1].max)
73
+ @clip_counts[channel_index] += segment.count { |sample| sample.abs >= @clip_threshold }
60
74
  @peak_values[channel_index] = smooth(@peak_values[channel_index], instantaneous_peak)
61
75
  @rms_values[channel_index] = smooth(@rms_values[channel_index], instantaneous_rms)
62
76
  end
@@ -66,11 +80,17 @@ module Deftones
66
80
 
67
81
  alias getValue get_value
68
82
  alias normalRange normal_range
83
+ alias clipCount clip_count
84
+ alias clipThreshold clip_threshold
69
85
 
70
86
  def normalRange=(value)
71
87
  self.normal_range = value
72
88
  end
73
89
 
90
+ def clipThreshold=(value)
91
+ self.clip_threshold = value
92
+ end
93
+
74
94
  private
75
95
 
76
96
  def smooth(previous, current)
@@ -9,7 +9,7 @@ module Deftones
9
9
  attr_reader :buses
10
10
  end
11
11
 
12
- attr_reader :input, :output, :pan_vol, :solo
12
+ attr_reader :input, :output, :pan_vol
13
13
 
14
14
  def initialize(pan: 0.0, volume: 0.0, solo: false, muted: false, mute: nil, context: Deftones.context)
15
15
  super(context: context)
@@ -3,15 +3,69 @@
3
3
  module Deftones
4
4
  module Component
5
5
  class Compressor < Core::AudioNode
6
- attr_accessor :threshold, :ratio, :attack, :release
6
+ DETECTORS = %i[peak rms].freeze
7
7
 
8
- def initialize(threshold: -18.0, ratio: 4.0, attack: 0.01, release: 0.1, context: Deftones.context)
8
+ attr_reader :threshold, :ratio, :attack, :release, :detector, :knee, :lookahead, :lookahead_samples,
9
+ :rms_window, :true_peak
10
+
11
+ def initialize(threshold: -18.0, ratio: 4.0, attack: 0.01, release: 0.1, detector: :peak,
12
+ knee: 0.0, lookahead: 0.0, rms_window: 0.01, true_peak: false,
13
+ context: Deftones.context)
9
14
  super(context: context)
10
- @threshold = threshold.to_f
11
- @ratio = ratio.to_f
12
- @attack = attack.to_f
13
- @release = release.to_f
14
15
  @gain_db = []
16
+ @rms_energy = []
17
+ @lookahead_buffers = []
18
+ @previous_detector_samples = []
19
+ self.threshold = threshold
20
+ self.ratio = ratio
21
+ self.attack = attack
22
+ self.release = release
23
+ self.detector = detector
24
+ self.knee = knee
25
+ self.lookahead = lookahead
26
+ self.rms_window = rms_window
27
+ self.true_peak = true_peak
28
+ end
29
+
30
+ def threshold=(value)
31
+ @threshold = value.to_f
32
+ end
33
+
34
+ def ratio=(value)
35
+ @ratio = value.to_f
36
+ end
37
+
38
+ def attack=(value)
39
+ @attack = value.to_f
40
+ @attack_smoothing = smoothing_for(@attack)
41
+ end
42
+
43
+ def release=(value)
44
+ @release = value.to_f
45
+ @release_smoothing = smoothing_for(@release)
46
+ end
47
+
48
+ def detector=(value)
49
+ @detector = normalize_detector(value)
50
+ end
51
+
52
+ def knee=(value)
53
+ @knee = [value.to_f, 0.0].max
54
+ end
55
+
56
+ def lookahead=(value)
57
+ @lookahead = [value.to_f, 0.0].max
58
+ @lookahead_samples = (@lookahead * context.sample_rate).round
59
+ @lookahead_buffers = []
60
+ end
61
+
62
+ def rms_window=(value)
63
+ @rms_window = [value.to_f, 0.0].max
64
+ @rms_smoothing = smoothing_for(@rms_window)
65
+ end
66
+
67
+ def true_peak=(value)
68
+ @true_peak = !!value
15
69
  end
16
70
 
17
71
  def multichannel_process?
@@ -19,7 +73,7 @@ module Deftones
19
73
  end
20
74
 
21
75
  def process(input_block, num_frames, _start_frame, _cache)
22
- ensure_gain_state(input_block.channels)
76
+ ensure_channel_state(input_block.channels)
23
77
  Core::AudioBlock.from_channel_data(
24
78
  input_block.channel_data.each_with_index.map do |channel, channel_index|
25
79
  Array.new(num_frames) { |index| compress(channel[index], channel_index) }
@@ -30,34 +84,85 @@ module Deftones
30
84
  private
31
85
 
32
86
  def compress(sample, channel_index)
33
- level = [sample.abs, 1.0e-9].max
87
+ level = [detector_level(sample, channel_index), 1.0e-9].max
34
88
  level_db = 20.0 * Math.log10(level)
35
- target_gain_db =
36
- if level_db > @threshold
37
- compressed_db = @threshold + ((level_db - @threshold) / [@ratio, 1.0].max)
38
- compressed_db - level_db
39
- else
40
- 0.0
41
- end
89
+ target_gain_db = gain_reduction_db(level_db)
42
90
 
43
91
  current_gain_db = @gain_db[channel_index]
44
- smoothing = target_gain_db < current_gain_db ? attack_smoothing : release_smoothing
92
+ smoothing = target_gain_db < current_gain_db ? @attack_smoothing : @release_smoothing
45
93
  current_gain_db += (target_gain_db - current_gain_db) * smoothing
46
94
  @gain_db[channel_index] = current_gain_db
47
- sample * (10.0**(current_gain_db / 20.0))
95
+ lookahead_sample(sample, channel_index) * (10.0**(current_gain_db / 20.0))
96
+ end
97
+
98
+ def detector_level(sample, channel_index)
99
+ level =
100
+ case @detector
101
+ when :rms then rms_level(sample, channel_index)
102
+ else sample.abs
103
+ end
104
+ return level unless @true_peak
105
+
106
+ previous_sample = @previous_detector_samples[channel_index] || sample
107
+ @previous_detector_samples[channel_index] = sample
108
+ [level, sample.abs, previous_sample.abs, ((previous_sample + sample) * 0.5).abs].max
48
109
  end
49
110
 
50
- def attack_smoothing
51
- 1.0 / [(@attack * context.sample_rate), 1.0].max
111
+ def rms_level(sample, channel_index)
112
+ energy = @rms_energy[channel_index]
113
+ @rms_energy[channel_index] = ((1.0 - @rms_smoothing) * energy) + (@rms_smoothing * sample * sample)
114
+ Math.sqrt(@rms_energy[channel_index])
52
115
  end
53
116
 
54
- def release_smoothing
55
- 1.0 / [(@release * context.sample_rate), 1.0].max
117
+ def gain_reduction_db(level_db)
118
+ ratio = [@ratio, 1.0].max
119
+ return 0.0 if ratio <= 1.0
120
+ return hard_knee_gain_reduction_db(level_db, ratio) if @knee.zero?
121
+
122
+ over_threshold = level_db - @threshold
123
+ half_knee = @knee * 0.5
124
+ return 0.0 if over_threshold <= -half_knee
125
+ return hard_knee_gain_reduction_db(level_db, ratio) if over_threshold >= half_knee
126
+
127
+ ((1.0 / ratio) - 1.0) * ((over_threshold + half_knee)**2.0) / (2.0 * @knee)
128
+ end
129
+
130
+ def hard_knee_gain_reduction_db(level_db, ratio)
131
+ return 0.0 unless level_db > @threshold
132
+
133
+ compressed_db = @threshold + ((level_db - @threshold) / ratio)
134
+ compressed_db - level_db
56
135
  end
57
136
 
58
- def ensure_gain_state(channels)
137
+ def lookahead_sample(sample, channel_index)
138
+ return sample if @lookahead_samples.zero?
139
+
140
+ buffer = @lookahead_buffers[channel_index]
141
+ buffer << sample
142
+ buffer.shift || 0.0
143
+ end
144
+
145
+ def smoothing_for(seconds)
146
+ 1.0 / [(seconds.to_f * context.sample_rate), 1.0].max
147
+ end
148
+
149
+ def ensure_channel_state(channels)
59
150
  required = [channels.to_i, 1].max
60
151
  @gain_db.fill(0.0, @gain_db.length...required)
152
+ @rms_energy.fill(0.0, @rms_energy.length...required)
153
+ @previous_detector_samples.fill(0.0, @previous_detector_samples.length...required)
154
+ required.times do |channel_index|
155
+ next if @lookahead_buffers[channel_index]&.length == @lookahead_samples
156
+
157
+ @lookahead_buffers[channel_index] = Array.new(@lookahead_samples, 0.0)
158
+ end
159
+ end
160
+
161
+ def normalize_detector(value)
162
+ normalized = value.to_sym
163
+ return normalized if DETECTORS.include?(normalized)
164
+
165
+ raise ArgumentError, "Unsupported compressor detector: #{value}"
61
166
  end
62
167
  end
63
168
  end
@@ -5,33 +5,48 @@ module Deftones
5
5
  class Filter < Core::AudioNode
6
6
  TYPES = DSP::Biquad::TYPES
7
7
 
8
- attr_reader :detune, :frequency, :q, :gain
9
- attr_accessor :type
8
+ attr_reader :detune, :frequency, :gain, :q, :type
10
9
 
11
10
  def initialize(type: :lowpass, frequency: 350.0, q: 1.0, gain: 0.0, detune: 0.0, context: Deftones.context)
12
11
  super(context: context)
13
- @type = normalize_type(type)
12
+ @biquads = []
13
+ self.type = type
14
14
  @frequency = Core::Signal.new(value: frequency, units: :frequency, context: context)
15
15
  @q = Core::Signal.new(value: q, units: :number, context: context)
16
16
  @gain = Core::Signal.new(value: gain, units: :number, context: context)
17
17
  @detune = Core::Signal.new(value: detune, units: :number, context: context)
18
- @biquads = []
19
18
  end
20
19
 
21
20
  def detune=(value)
22
21
  @detune.value = value
23
22
  end
24
23
 
24
+ def type=(value)
25
+ normalized = normalize_type(value)
26
+ return @type = normalized if @type == normalized
27
+
28
+ @type = normalized
29
+ reset!
30
+ end
31
+
25
32
  def multichannel_process?
26
33
  true
27
34
  end
28
35
 
29
36
  def process(input_block, num_frames, start_frame, _cache)
30
- update_filters(input_block.channels, start_frame)
37
+ ensure_biquads(input_block.channels)
38
+ frequencies = @frequency.process(num_frames, start_frame)
39
+ detunes = @detune.process(num_frames, start_frame)
40
+ q_values = @q.process(num_frames, start_frame)
41
+ gain_values = @gain.process(num_frames, start_frame)
42
+
31
43
  Core::AudioBlock.from_channel_data(
32
44
  input_block.channel_data.each_with_index.map do |channel, channel_index|
33
45
  biquad = @biquads[channel_index]
34
- Array.new(num_frames) { |index| biquad.process_sample(channel[index]) }
46
+ Array.new(num_frames) do |index|
47
+ update_filter(biquad, frequencies[index], detunes[index], q_values[index], gain_values[index])
48
+ biquad.process_sample(channel[index])
49
+ end
35
50
  end
36
51
  )
37
52
  end
@@ -43,19 +58,14 @@ module Deftones
43
58
 
44
59
  private
45
60
 
46
- def update_filters(channels, start_frame)
47
- ensure_biquads(channels)
48
- frequency = @frequency.process(1, start_frame).first
49
- detune = @detune.process(1, start_frame).first
50
- @biquads.each do |biquad|
51
- biquad.update(
52
- type: normalize_type(@type),
53
- frequency: frequency * (2.0**(detune / 1200.0)),
54
- q: @q.process(1, start_frame).first,
55
- gain_db: @gain.process(1, start_frame).first * 24.0,
56
- sample_rate: context.sample_rate
57
- )
58
- end
61
+ def update_filter(biquad, frequency, detune, q, gain)
62
+ biquad.update(
63
+ type: normalize_type(@type),
64
+ frequency: frequency * (2.0**(detune / 1200.0)),
65
+ q: q,
66
+ gain_db: gain * 24.0,
67
+ sample_rate: context.sample_rate
68
+ )
59
69
  end
60
70
 
61
71
  def ensure_biquads(channels)
@@ -13,6 +13,20 @@ module Deftones
13
13
  @output = self
14
14
  end
15
15
 
16
+ def number_of_inputs
17
+ 2
18
+ end
19
+
20
+ alias numberOfInputs number_of_inputs
21
+
22
+ def input_for_index(index)
23
+ case index
24
+ when 0 then @left
25
+ when 1 then @right
26
+ else raise_connection_index_error!(:input_index, index, number_of_inputs)
27
+ end
28
+ end
29
+
16
30
  def render(num_frames, start_frame = 0, cache = {})
17
31
  cache_key = [object_id, start_frame, num_frames]
18
32
  return cache.fetch(cache_key).dup if cache.key?(cache_key)
@@ -3,7 +3,7 @@
3
3
  module Deftones
4
4
  module Component
5
5
  class MultibandCompressor < Core::AudioNode
6
- attr_reader :high, :high_frequency, :input, :low, :low_frequency, :mid, :output, :q, :split
6
+ attr_reader :high, :input, :low, :mid, :output, :split
7
7
 
8
8
  def initialize(low_frequency: 400.0, high_frequency: 2_500.0, q: 1.0, low: {}, mid: {}, high: {},
9
9
  context: Deftones.context)
@@ -5,20 +5,27 @@ module Deftones
5
5
  class OnePoleFilter < Core::AudioNode
6
6
  TYPES = %i[lowpass highpass].freeze
7
7
 
8
- attr_reader :frequency
9
- attr_accessor :type
8
+ attr_reader :frequency, :type
10
9
 
11
10
  def initialize(frequency: 880.0, type: :lowpass, context: Deftones.context)
12
11
  super(context: context)
13
12
  @frequency = Core::Signal.new(value: frequency, units: :frequency, context: context)
14
- @type = normalize_type(type)
15
13
  @lowpass_state = []
14
+ self.type = type
16
15
  end
17
16
 
18
17
  def frequency=(value)
19
18
  @frequency.value = value
20
19
  end
21
20
 
21
+ def type=(value)
22
+ normalized = normalize_type(value)
23
+ return @type = normalized if @type == normalized
24
+
25
+ @type = normalized
26
+ reset!
27
+ end
28
+
22
29
  def multichannel_process?
23
30
  true
24
31
  end
@@ -3,17 +3,24 @@
3
3
  module Deftones
4
4
  module Component
5
5
  class Panner < Core::AudioNode
6
- attr_reader :pan
6
+ PAN_LAWS = %i[equal_power linear].freeze
7
7
 
8
- def initialize(pan: 0.0, context: Deftones.context)
8
+ attr_reader :pan, :pan_law
9
+
10
+ def initialize(pan: 0.0, pan_law: :equal_power, context: Deftones.context)
9
11
  super(context: context)
10
12
  @pan = Core::Signal.new(value: pan, units: :number, context: context)
13
+ @pan_law = normalize_pan_law(pan_law)
11
14
  end
12
15
 
13
16
  def pan=(value)
14
17
  @pan.value = value
15
18
  end
16
19
 
20
+ def pan_law=(value)
21
+ @pan_law = normalize_pan_law(value)
22
+ end
23
+
17
24
  def multichannel_process?
18
25
  true
19
26
  end
@@ -47,6 +54,7 @@ module Deftones
47
54
 
48
55
  def stereo_left_gain(pan)
49
56
  normalized = pan.to_f.clamp(-1.0, 1.0)
57
+ return [1.0 - normalized, 0.0].max if @pan_law == :linear && normalized.positive?
50
58
  return 1.0 if normalized <= 0.0
51
59
 
52
60
  Math.cos(normalized * Math::PI * 0.5)
@@ -54,22 +62,37 @@ module Deftones
54
62
 
55
63
  def stereo_right_gain(pan)
56
64
  normalized = pan.to_f.clamp(-1.0, 1.0)
65
+ return [1.0 + normalized, 0.0].max if @pan_law == :linear && normalized.negative?
57
66
  return 1.0 if normalized >= 0.0
58
67
 
59
68
  Math.cos(normalized.abs * Math::PI * 0.5)
60
69
  end
61
70
 
62
71
  def left_gain(pan)
72
+ return (1.0 - pan.to_f.clamp(-1.0, 1.0)) * 0.5 if @pan_law == :linear
73
+
63
74
  Math.cos(angle_for(pan))
64
75
  end
65
76
 
66
77
  def right_gain(pan)
78
+ return (1.0 + pan.to_f.clamp(-1.0, 1.0)) * 0.5 if @pan_law == :linear
79
+
67
80
  Math.sin(angle_for(pan))
68
81
  end
69
82
 
70
83
  def angle_for(pan)
71
84
  ((pan.to_f.clamp(-1.0, 1.0) + 1.0) * Math::PI) * 0.25
72
85
  end
86
+
87
+ def normalize_pan_law(value)
88
+ normalized = value.to_sym
89
+ return normalized if PAN_LAWS.include?(normalized)
90
+
91
+ raise ArgumentError, "Unsupported pan law: #{value}"
92
+ end
93
+
94
+ alias panLaw pan_law
95
+ alias panLaw= pan_law=
73
96
  end
74
97
  end
75
98
  end
@@ -178,16 +178,6 @@ module Deftones
178
178
  @listener.position_y.value,
179
179
  @listener.position_z.value
180
180
  ]
181
- listener_forward = normalize_vector([
182
- @listener.forward_x.value,
183
- @listener.forward_y.value,
184
- @listener.forward_z.value
185
- ])
186
- listener_up = normalize_vector([
187
- @listener.up_x.value,
188
- @listener.up_y.value,
189
- @listener.up_z.value
190
- ])
191
181
  mono_input = send(:mix_source_blocks, num_frames, start_frame, cache).mono
192
182
 
193
183
  Array.new(num_frames) do |index|
@@ -11,6 +11,20 @@ module Deftones
11
11
  @right = OutputTap.new(parent: self, channel: 1, context: context)
12
12
  end
13
13
 
14
+ def number_of_outputs
15
+ 2
16
+ end
17
+
18
+ alias numberOfOutputs number_of_outputs
19
+
20
+ def output_for_index(index)
21
+ case index
22
+ when 0 then @left
23
+ when 1 then @right
24
+ else raise_connection_index_error!(:output_index, index, number_of_outputs)
25
+ end
26
+ end
27
+
14
28
  def render_channel(_channel, num_frames, start_frame = 0, cache = {})
15
29
  render_channel_block(_channel, num_frames, start_frame, cache).mono
16
30
  end