Class: Wavify::DSP::Filter

Inherits:
Object
  • Object
show all
Defined in:
lib/wavify/dsp/filter.rb,
sig/dsp.rbs

Overview

Stateful biquad filter with common factory constructors.

Constant Summary collapse

DEFAULT_TAIL_SECONDS =

Minimum tail window used to drain an IIR filter.

0.05
MAX_TAIL_SECONDS =

Hard limit on automatic filter tail rendering.

10.0
TAIL_AMPLITUDE =

Amplitude below which an IIR tail is treated as silent.

1.0e-6

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type, cutoff:, q: 0.707, gain_db: 0.0) ⇒ Filter

Returns a new instance of Filter.

Parameters:

  • type (Symbol)
  • cutoff: (Numeric)
  • q: (Numeric) (defaults to: 0.707)
  • gain_db: (Numeric) (defaults to: 0.0)


72
73
74
75
76
77
78
79
80
81
82
# File 'lib/wavify/dsp/filter.rb', line 72

def initialize(type, cutoff:, q: 0.707, gain_db: 0.0)
  @type = validate_type!(type)
  @cutoff = validate_cutoff!(cutoff)
  @q = validate_q!(q)
  @gain_db = validate_gain!(gain_db)

  @coefficients = nil
  @coeff_sample_rate = nil
  @channel_states = []
  @runtime_channels = nil
end

Instance Attribute Details

#cutoffFloat (readonly)

Returns the value of attribute cutoff.

Returns:

  • (Float)


15
16
17
# File 'lib/wavify/dsp/filter.rb', line 15

def cutoff
  @cutoff
end

#gain_dbFloat (readonly)

Returns the value of attribute gain_db.

Returns:

  • (Float)


15
16
17
# File 'lib/wavify/dsp/filter.rb', line 15

def gain_db
  @gain_db
end

#qFloat (readonly)

Returns the value of attribute q.

Returns:

  • (Float)


15
16
17
# File 'lib/wavify/dsp/filter.rb', line 15

def q
  @q
end

#typeSymbol (readonly)

Returns the value of attribute type.

Returns:

  • (Symbol)


15
16
17
# File 'lib/wavify/dsp/filter.rb', line 15

def type
  @type
end

Class Method Details

.bandpass(center: nil, bandwidth: nil, cutoff: nil, q: nil) ⇒ Filter

Parameters:

  • center: (Numeric, nil) (defaults to: nil)
  • bandwidth: (Numeric, nil) (defaults to: nil)
  • cutoff: (Numeric, nil) (defaults to: nil)
  • q: (Numeric, nil) (defaults to: nil)

Returns:



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/wavify/dsp/filter.rb', line 31

def self.bandpass(center: nil, bandwidth: nil, cutoff: nil, q: nil)
  return new(:bandpass, cutoff: cutoff, q: q || 0.707) if cutoff

  unless center.is_a?(Numeric) && center.respond_to?(:finite?) && center.finite? && center.positive?
    raise InvalidParameterError, "center must be a positive finite Numeric"
  end
  unless bandwidth.is_a?(Numeric) && bandwidth.respond_to?(:finite?) && bandwidth.finite? && bandwidth.positive?
    raise InvalidParameterError, "bandwidth must be a positive finite Numeric"
  end

  new(:bandpass, cutoff: center, q: center.to_f / bandwidth)
end

.highpass(cutoff:, q: 0.707) ⇒ Filter

Builds a high-pass filter.

Parameters:

  • cutoff: (Numeric)
  • q: (Numeric) (defaults to: 0.707)

Returns:



27
28
29
# File 'lib/wavify/dsp/filter.rb', line 27

def self.highpass(cutoff:, q: 0.707)
  new(:highpass, cutoff: cutoff, q: q)
end

.highshelf(cutoff:, gain_db:) ⇒ Filter

Builds a high-shelf EQ filter.

Parameters:

  • cutoff: (Numeric)
  • gain_db: (Numeric)

Returns:



68
69
70
# File 'lib/wavify/dsp/filter.rb', line 68

def self.highshelf(cutoff:, gain_db:)
  new(:highshelf, cutoff: cutoff, gain_db: gain_db)
end

.lowpass(cutoff:, q: 0.707) ⇒ Filter

Builds a low-pass filter.

Parameters:

  • cutoff: (Numeric)
  • q: (Numeric) (defaults to: 0.707)

Returns:



20
21
22
# File 'lib/wavify/dsp/filter.rb', line 20

def self.lowpass(cutoff:, q: 0.707)
  new(:lowpass, cutoff: cutoff, q: q)
end

.lowshelf(cutoff:, gain_db:) ⇒ Filter

Builds a low-shelf EQ filter.

Parameters:

  • cutoff: (Numeric)
  • gain_db: (Numeric)

Returns:



61
62
63
# File 'lib/wavify/dsp/filter.rb', line 61

def self.lowshelf(cutoff:, gain_db:)
  new(:lowshelf, cutoff: cutoff, gain_db: gain_db)
end

.notch(cutoff:, q: 0.707) ⇒ Filter

Builds a notch filter.

Parameters:

  • cutoff: (Numeric)
  • q: (Numeric) (defaults to: 0.707)

Returns:



47
48
49
# File 'lib/wavify/dsp/filter.rb', line 47

def self.notch(cutoff:, q: 0.707)
  new(:notch, cutoff: cutoff, q: q)
end

.peaking(cutoff:, q: 1.0, gain_db: 0.0) ⇒ Filter

Builds a peaking EQ filter.

Parameters:

  • cutoff: (Numeric)
  • q: (Numeric) (defaults to: 1.0)
  • gain_db: (Numeric) (defaults to: 0.0)

Returns:



54
55
56
# File 'lib/wavify/dsp/filter.rb', line 54

def self.peaking(cutoff:, q: 1.0, gain_db: 0.0)
  new(:peaking, cutoff: cutoff, q: q, gain_db: gain_db)
end

Instance Method Details

#apply(buffer) ⇒ Core::SampleBuffer

Parameters:

Returns:



84
85
86
# File 'lib/wavify/dsp/filter.rb', line 84

def apply(buffer)
  DSP::Processor.render(self, buffer)
end

#build_runtimeFilter

Returns:



170
171
172
# File 'lib/wavify/dsp/filter.rb', line 170

def build_runtime
  dup.reset
end

#flush(format: nil) ⇒ Enumerator<Core::SampleBuffer>?

Emits the decaying IIR state after the input ends.

Parameters:

  • format (Core::Format, nil) (defaults to: nil)

    optional target format

  • format: (Core::Format, nil) (defaults to: nil)

Returns:



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/wavify/dsp/filter.rb', line 124

def flush(format: nil)
  return nil unless @coeff_sample_rate && filter_state_active?
  if format && !format.is_a?(Core::Format)
    raise InvalidParameterError, "format must be Core::Format"
  end

  channels = @channel_states.length
  runtime_format = Core::Format.new(
    channels: channels,
    sample_rate: @coeff_sample_rate,
    bit_depth: 32,
    sample_format: :float
  )
  remaining = tail_frame_count(@coeff_sample_rate)
  target_format = format || runtime_format
  Enumerator.new do |yielder|
    while remaining.positive?
      frames = [remaining, DSP::Processor::TAIL_CHUNK_FRAMES].min
      silence = Array.new(frames * channels, 0.0)
      processed = process_interleaved(silence, sample_rate: @coeff_sample_rate, channels: channels)
      yielder << Core::SampleBuffer.new(processed, runtime_format).convert(target_format)
      remaining -= frames
    end
  ensure
    reset
  end
end

#latencyFloat

Returns:

  • (Float)


174
175
176
# File 'lib/wavify/dsp/filter.rb', line 174

def latency
  0.0
end

#lookaheadFloat

Returns:

  • (Float)


178
179
180
# File 'lib/wavify/dsp/filter.rb', line 178

def lookahead
  0.0
end

#process(buffer) ⇒ Core::SampleBuffer

Processes a streaming chunk while preserving filter state.

Parameters:

Returns:

Raises:



89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/wavify/dsp/filter.rb', line 89

def process(buffer)
  raise InvalidParameterError, "buffer must be Core::SampleBuffer" unless buffer.is_a?(Core::SampleBuffer)

  float_format = buffer.format.with(sample_format: :float, bit_depth: 32)
  float_buffer = buffer.convert(float_format)
  prepare_runtime_format!(sample_rate: float_format.sample_rate, channels: float_format.channels)
  processed = process_interleaved(
    float_buffer.samples,
    sample_rate: float_format.sample_rate,
    channels: float_format.channels
  )

  Core::SampleBuffer.new(processed, float_format).convert(buffer.format)
end

#process_sample(sample, sample_rate:, channel: 0) ⇒ Float

Parameters:

  • sample (Numeric)
  • sample_rate: (Integer)
  • channel: (Integer) (defaults to: 0)
  • channels: (Integer)

Returns:

  • (Float)

Raises:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/wavify/dsp/filter.rb', line 104

def process_sample(sample, sample_rate:, channel: 0)
  unless sample.is_a?(Numeric) && sample.respond_to?(:finite?) && sample.finite?
    raise InvalidParameterError, "sample must be a finite Numeric"
  end
  raise InvalidParameterError, "channel must be a non-negative Integer" unless channel.is_a?(Integer) && channel >= 0
  raise InvalidParameterError, "sample_rate must be a positive Integer" unless sample_rate.is_a?(Integer) && sample_rate.positive?

  update_coefficients!(sample_rate)
  ensure_sample_channel_state!(channel)
  state = @channel_states[channel]
  x = sample.to_f
  y = compute_biquad(state, x)
  update_state!(state, x, y)
  y
end

#resetvoid

This method returns an undefined value.

Clears internal filter state for all channels.



162
163
164
165
166
167
168
# File 'lib/wavify/dsp/filter.rb', line 162

def reset
  @coefficients = nil
  @coeff_sample_rate = nil
  @channel_states = []
  @runtime_channels = nil
  self
end

#tail_durationFloat

Estimated time for the biquad poles to decay below -120 dB.

Returns:

  • (Float)


153
154
155
156
157
# File 'lib/wavify/dsp/filter.rb', line 153

def tail_duration
  return DEFAULT_TAIL_SECONDS unless @coeff_sample_rate && @coefficients

  tail_frame_count(@coeff_sample_rate).to_f / @coeff_sample_rate
end