Source code for diffFx_pytorch.processors.modulation.phaser

import torch 
import torch.nn as nn
import torch.nn.functional as F
import numpy as np 
from typing import Dict, List, Tuple, Union
from ..base import ProcessorsBase, EffectParam
from ..base_utils import check_params
from torchlpc import sample_wise_lpc

from typing import Optional

import torch
from torch import Tensor as T

# Phaser 
# > Depth 
# > Width 
# > Feedback 
# > LFO Frequency 
def time_varying_fir(x: T, b: T, zi: Optional[T] = None) -> T:
    assert x.ndim == 2
    assert b.ndim == 3
    assert x.size(0) == b.size(0)
    assert x.size(1) == b.size(1)
    order = b.size(2) - 1
    x_padded = F.pad(x, (order, 0))
    if zi is not None:
        assert zi.shape == (x.size(0), order)
        x_padded[:, :order] = zi
    x_unfolded = x_padded.unfold(dimension=1, size=order + 1, step=1)
    x_unfolded = x_unfolded.unsqueeze(3)
    b = b.flip(2).unsqueeze(2)
    y = b @ x_unfolded
    y = y.squeeze(3)
    y = y.squeeze(2)
    return y

def fourth_order_ap_coeffs(p):
    b = torch.stack([p**4, -4 * p**3, 6 * p**2, -4 * p, torch.ones_like(p)], dim=p.ndim)
    a = b.flip(-1)
    return a, b

[docs]class Phaser(ProcessorsBase): """Differentiable implementation of a high-order allpass phaser effect. Implementation is based on: .. [1] Reiss, Joshua D., and Andrew McPherson. Audio effects: theory, implementation and application. CRC Press, 2014. .. [2] Yu, Chin-Yun, et al. "Differentiable all-pole filters for time-varying audio systems." arXiv preprint arXiv:2404.07970 (2024). This processor implements a sophisticated phaser effect using allpass filters with time-varying coefficients, creating complex phase-shifting modulations in the audio signal. The implementation provides precise control over the phasing effect through multiple parameters. Processing Chain: 1. Generate low-frequency oscillator (LFO) modulation 2. Create time-varying allpass filter coefficients 3. Apply fourth-order allpass filtering 4. Mix processed and original signals The phaser effect works by creating phase shifts across different frequency bands, resulting in a sweeping, swirling sound characteristic of classic analog phasers. Unlike simple single-stage phasers, this implementation uses a fourth-order allpass filter for richer, more complex modulations. Mathematical Concept: The core of the phaser is a time-varying allpass filter where the transfer function is dynamically modified by a low-frequency oscillator (LFO). The phase shift is controlled by the allpass coefficient p(t), which is derived from the LFO and frequency range parameters. Args: sample_rate (int): Audio sample rate in Hz. Defaults to 44100. Attributes: sample_rate (int): Audio sample rate in Hz prev_states (torch.Tensor, optional): Previous filter states for stateful processing osc (callable): Oscillator function (defaults to torch.sin) lpc_func (callable): Linear prediction coding function for filtering Parameters Details: f0: LFO Frequency - Range: 0.1 to 5.0 Hz - Controls speed of phase modulation - Determines sweeping rate of the effect f_min: Minimum Cutoff Frequency - Range: 100.0 to 2000.0 Hz - Lower bound of frequency modulation - Defines the start of the phase-shifting range f_max: Maximum Cutoff Frequency - Range: 2000.0 to 10000.0 Hz - Upper bound of frequency modulation - Defines the end of the phase-shifting range feedback: Feedback Amount - Range: 0.0 to 0.9 - Controls resonance and intensity of the phasing effect - Higher values create more pronounced peaks and notches wet_mix: Wet/Dry Mix - Range: 0.0 to 1.0 - 0.0: Only clean (dry) signal - 1.0: Only processed (wet) signal Note: The processor supports the following features: - Fourth-order allpass filtering - Dynamic frequency range modulation - Precise feedback control - Efficient batch processing - Neural network compatible Warning: When using with neural networks: - Ensure norm_params are in range [0, 1] - Parameters will be automatically mapped to ranges - Network output should be properly normalized - Input must be mono or stereo - Parameter order must match _register_default_parameters() Examples: Basic DSP Usage: >>> # Create a phaser >>> phaser = Phaser(sample_rate=44100) >>> # Process with musical settings >>> output = phaser(input_audio, dsp_params={ ... 'f0': 0.5, # 0.5 Hz LFO ... 'f_min': 500.0, # Start at 500 Hz ... 'f_max': 5000.0, # End at 5000 Hz ... 'feedback': 0.6, # Moderate feedback ... 'wet_mix': 0.7 # 70% processed signal ... }) Neural Network Control: >>> # Simple parameter prediction network >>> class PhaserController(nn.Module): ... def __init__(self, input_size, num_params): ... super().__init__() ... self.net = nn.Sequential( ... nn.Linear(input_size, 32), ... nn.ReLU(), ... nn.Linear(32, num_params), ... nn.Sigmoid() # Ensures output is in [0,1] range ... ) ... ... def forward(self, x): ... return self.net(x) """
[docs] def __init__(self, sample_rate, param_range=None): super().__init__(sample_rate, param_range) self.prev_states = None self.osc = torch.sin self.lpc_func = sample_wise_lpc
[docs] def _register_default_parameters(self): """Register default parameters for the phaser effect. Sets up: f0: LFO frequency (0.1 to 5.0 Hz) f_min: Minimum cutoff frequency (100.0 to 2000.0 Hz) f_max: Maximum cutoff frequency (2000.0 to 10000.0 Hz) feedback: Feedback amount (0.0 to 0.9) wet_mix: Wet/dry balance (0.0 to 1.0) """ self.params = { 'f0': EffectParam(min_val=0.1, max_val=5.0), # LFO frequency (Hz) 'f_min': EffectParam(min_val=100.0, max_val=2000.0), # Min cutoff freq (Hz) 'f_max': EffectParam(min_val=2000.0, max_val=10000.0), # Max cutoff freq (Hz) 'feedback': EffectParam(min_val=0.0, max_val=0.9), # Feedback amount 'wet_mix': EffectParam(min_val=0.0, max_val=1.0), # Wet/dry mix }
[docs] def process(self, x: torch.Tensor, norm_params: Union[Dict[str, torch.Tensor], None] = None, dsp_params: Union[Dict[str, torch.Tensor], None] = None): """Process input signal through the phaser effect. Args: x (torch.Tensor): Input audio tensor. Shape: (batch, channels, samples) Supports mono or stereo inputs norm_params (Dict[str, torch.Tensor]): Normalized parameters (0 to 1) Must contain keys: - 'f0': LFO frequency - 'f_min': Minimum cutoff frequency - 'f_max': Maximum cutoff frequency - 'feedback': Feedback amount - 'wet_mix': Wet/dry mix Each value should be a tensor of shape (batch_size,) dsp_params (Dict[str, Union[float, torch.Tensor]], optional): Direct DSP parameters. Can specify as: - float/int: Single value applied to entire batch - 0D tensor: Single value applied to entire batch - 1D tensor: Batch of values matching input batch size If provided, norm_params must be None. Returns: torch.Tensor: Processed audio tensor Shape: (batch, channels, samples) Raises: AssertionError: If input tensor dimensions are incorrect """ batch_size, chs, num_samples = x.shape device = x.device # Get parameters exactly as in original code # Get parameters check_params(norm_params, dsp_params) # Set proper configuration if norm_params is not None: params = self.map_parameters(norm_params) else: params = dsp_params f0 = params['f0'] f_min = params['f_min'] f_max = params['f_max'] feedback = params['feedback'] mix = params['wet_mix'] # Calculate normalized frequencies d_min = 2.0 * f_min / self.sample_rate d_max = 2.0 * f_max / self.sample_rate depth = (d_max - d_min) * 0.5 # Generate time vector t = torch.arange(num_samples, device=device) / self.sample_rate # input_f0 = 2 * torch.pi * f0.view(-1, 1) * t # input_f0 = input_f0.unsqueeze(-1) input_f0 = 2 * torch.pi * f0.view(-1, 1) * t # Generate LFO (using sawtooth wave as in original) lfo = d_min.view(-1, 1) + depth.view(-1, 1) * (1.0 + self.osc(input_f0)) # Calculate allpass coefficient p = (1.0 - torch.tan(lfo)) / (1.0 + torch.tan(lfo)) # Process each channel x = x.squeeze(1) combine_a, combine_b = fourth_order_ap_coeffs(p) combine_denom = combine_a - feedback.view(-1, 1, 1).abs() * combine_b combine_b = combine_b / combine_denom[..., :1] combine_denom = combine_denom / combine_denom[..., :1] y_ch = time_varying_fir(x, combine_b) y_ch = self.lpc_func(y_ch, combine_denom[..., 1:], None) # Mix wet and dry signals y = mix.view(-1, 1) * y_ch + (1.0 - mix.view(-1, 1)) * x return y.unsqueeze(1)