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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6215833a8d30fe3a76382b1928335e8a22b94df1e1649f7fb0b028f5836d9b3e
|
|
4
|
+
data.tar.gz: 943cdc21e72614d018ca04709763cf8e38d1c646c071a53311c3a0e5aaef97ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
[](https://github.com/ydah/deftones/actions/workflows/main.yml)
|
|
4
|
+
[](https://rubygems.org/gems/deftones)
|
|
5
|
+
[](https://rubygems.org/gems/deftones)
|
|
6
|
+
[](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
|
-
|
|
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
|
|
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
|
-
|
|
6
|
+
DETECTORS = %i[peak rms].freeze
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
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
|
|
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, :
|
|
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
|
-
@
|
|
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
|
-
|
|
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)
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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, :
|
|
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
|
-
|
|
6
|
+
PAN_LAWS = %i[equal_power linear].freeze
|
|
7
7
|
|
|
8
|
-
|
|
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
|