Series and Parallel Filter Combinations#
Learn how to create complex filter networks by combining filters in series (sequential) and parallel (simultaneous) configurations. This tutorial demonstrates TorchFX’s intuitive operators for building sophisticated audio processing chains.
Overview#
TorchFX provides two fundamental ways to combine filters:
Configuration |
Operator |
Behavior |
Use Case |
|---|---|---|---|
Series |
|
Sequential processing |
Filter chains, cascaded stages |
Parallel |
|
Simultaneous processing with summed outputs |
Band-pass filters, multi-band processing |
Both can be combined to create complex signal processing networks with minimal code.
graph TB
subgraph "Series Configuration"
Input1[Input] --> F1[Filter 1]
F1 --> F2[Filter 2]
F2 --> F3[Filter 3]
F3 --> Output1[Output]
end
subgraph "Parallel Configuration"
Input2[Input] --> Split{Split}
Split --> FA[Filter A]
Split --> FB[Filter B]
Split --> FC[Filter C]
FA --> Sum((+))
FB --> Sum
FC --> Sum
Sum --> Output2[Output]
end
style Input1 fill:#e1f5ff
style Output1 fill:#e1f5ff
style Input2 fill:#e1f5ff
style Output2 fill:#e1f5ff
style F1 fill:#fff5e1
style F2 fill:#fff5e1
style F3 fill:#fff5e1
style FA fill:#fff5e1
style FB fill:#fff5e1
style FC fill:#fff5e1
Series Filter Chains#
Series combinations use the pipeline operator (|) to process audio sequentially through multiple filters.
Basic Series Chain#
import torchfx as fx
# Load audio
wave = fx.Wave.from_file("audio.wav")
# Apply filters in series
processed = (
wave
| fx.filter.iir.HiButterworth(cutoff=100, order=2) # Remove low frequencies
| fx.filter.iir.LoButterworth(cutoff=5000, order=4) # Remove high frequencies
| fx.effect.Normalize(peak=0.9) # Normalize
)
processed.save("filtered.wav")
Signal flow: Input → High-pass (100 Hz) → Low-pass (5000 Hz) → Normalize → Output
How Series Works#
When you use the pipe operator:
The
Waveobject flows through each filter sequentiallyEach filter’s
forward()method processes the audio tensorA new
Waveis returned at each stageSample rate (
fs) is automatically propagated through the pipeline
sequenceDiagram
participant Wave
participant Filter1
participant Filter2
participant Filter3
Wave->>Filter1: wave | filter1
Note over Filter1: fs auto-configured
Filter1->>Filter1: forward(wave.ys)
Filter1->>Wave: Return new Wave
Wave->>Filter2: result | filter2
Note over Filter2: fs auto-configured
Filter2->>Filter2: forward(result.ys)
Filter2->>Wave: Return new Wave
Wave->>Filter3: result | filter3
Note over Filter3: fs auto-configured
Filter3->>Filter3: forward(result.ys)
Filter3->>Wave: Return final Wave
Multi-Stage Processing#
Build complex filter chains with multiple stages:
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("vocal.wav")
# Multi-stage vocal processing chain
processed = (
wave
| iir.HiButterworth(cutoff=80, order=2) # Remove rumble
| iir.ParametricEQ(frequency=3000, gain=3, q=1.0) # Presence boost
| iir.ParametricEQ(frequency=200, gain=-2, q=0.7) # Reduce muddiness
| fx.effect.Gain(0.7, gain_type="amplitude") # Trim level
| fx.effect.Normalize(peak=0.95) # Final normalization
)
Each stage processes the output of the previous stage, allowing you to build sophisticated processing chains.
See also
Pipeline Operator - Functional Composition - Deep dive into the pipeline operator
Parallel Filter Combinations#
Parallel combinations use the addition operator (+) to apply multiple filters simultaneously and sum their outputs.
Basic Parallel Combination#
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("audio.wav")
# Create a band-pass filter using parallel high-pass and low-pass
bandpass = iir.HiButterworth(200, order=2) + iir.LoButterworth(2000, order=4)
# Apply the combined filter
filtered = wave | bandpass
Signal flow: Input → [High-pass (200 Hz) + Low-pass (2000 Hz)] → Output
The output is the sum: result = highpass(input) + lowpass(input)
The ParallelFilterCombination Class#
When you use the + operator with filters, TorchFX creates a ParallelFilterCombination instance:
import torchfx as fx
from torchfx.filter import iir
# These are equivalent
combination1 = iir.HiButterworth(200) + iir.LoButterworth(2000)
combination2 = fx.filter.ParallelFilterCombination(
iir.HiButterworth(200),
iir.LoButterworth(2000)
)
The ParallelFilterCombination class:
Stores all filters in a
filtersattributeAutomatically propagates sample rate (
fs) to all child filtersSums outputs:
result = sum(filter(input) for filter in filters)
classDiagram
class ParallelFilterCombination {
+Sequence~AbstractFilter~ filters
+int|None fs
+compute_coefficients() None
+forward(x) Tensor
}
class AbstractFilter {
<<abstract>>
+__add__(other) ParallelFilterCombination
+compute_coefficients() None
+forward(x) Tensor
}
class HiButterworth {
+float cutoff
+int order
+forward(x) Tensor
}
class LoButterworth {
+float cutoff
+int order
+forward(x) Tensor
}
AbstractFilter <|-- ParallelFilterCombination
AbstractFilter <|-- HiButterworth
AbstractFilter <|-- LoButterworth
ParallelFilterCombination o-- AbstractFilter : contains
note for ParallelFilterCombination "Sums outputs of all child filters"
How Parallel Works#
The forward() method of ParallelFilterCombination:
@torch.no_grad()
def forward(self, x: Tensor) -> Tensor:
# Apply each filter independently
outputs = [f.forward(x) for f in self.filters]
# Create result tensor on same device as input
results = torch.zeros_like(x)
# Sum all outputs
for output in outputs:
results += output
return results
This implementation:
Processes all filters in parallel (conceptually)
Uses
torch.zeros_like(x)to ensure device compatibilityAccumulates outputs via element-wise addition
Disables gradients with
@torch.no_grad()for efficiency
Multiple Parallel Filters#
Combine more than two filters:
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("audio.wav")
# Three-way parallel combination. There is no standalone bandpass filter
# class -- synthesize a band by chaining a high-pass and a low-pass.
mid_band = iir.HiButterworth(500, order=2) | iir.LoButterworth(2000, order=2)
multiband = (
iir.LoButterworth(500, order=4) + # Low band
mid_band + # Mid band
iir.HiButterworth(2000, order=4) # High band
)
processed = wave | multiband
Combining Series and Parallel#
The real power comes from combining both patterns in a single pipeline.
Complete Example#
Here’s a complete example demonstrating mixed series/parallel processing:
import torchfx as fx
from torchfx.filter import iir
import torch
# Load audio
wave = fx.Wave.from_file("audio.wav")
# Move to GPU if available
device = "cuda" if torch.cuda.is_available() else "cpu"
wave = wave.to(device)
# Complex processing chain
processed = (
wave
# Stage 1: Remove low-frequency rumble (series)
| iir.LoButterworth(cutoff=100, order=2)
# Stage 2: Parallel high-pass filters (parallel)
| iir.HiButterworth(2000, order=4) + iir.HiChebyshev1(2000, order=2)
# Stage 3: Reduce level (series)
| fx.effect.Gain(gain=0.5, gain_type="amplitude")
)
# Save result (move to CPU for I/O)
processed.to("cpu").save("processed.wav")
Signal Flow Visualization#
graph TB
Input[Input Signal<br/>from_file] --> Stage1[Stage 1: LoButterworth<br/>fc=100 Hz, order=2]
Stage1 --> Split{Split}
subgraph Stage2 [Stage 2: Parallel Combination]
Split --> HP1[HiButterworth<br/>fc=2000 Hz, order=4]
Split --> HP2[HiChebyshev1<br/>fc=2000 Hz, order=2]
HP1 --> Sum((+))
HP2 --> Sum
end
Sum --> Stage3[Stage 3: Gain<br/>amplitude=0.5]
Stage3 --> Output[Output Signal<br/>save to file]
style Input fill:#e1f5ff
style Output fill:#e1f5ff
style Stage1 fill:#fff5e1
style HP1 fill:#fff5e1
style HP2 fill:#fff5e1
style Stage3 fill:#fff5e1
style Stage2 fill:#f9f9f9
Operator Precedence#
Python’s operator precedence affects how series and parallel combine:
# Addition (+) has HIGHER precedence than pipe (|)
result = wave | filter1 + filter2 | filter3
# This is parsed as:
result = wave | (filter1 + filter2) | filter3
# For clarity, use explicit parentheses:
result = wave | (filter1 + filter2) | filter3
# Or break into variables:
parallel_section = filter1 + filter2
result = wave | parallel_section | filter3
Best practice: Use parentheses or intermediate variables for complex combinations.
Sample Rate Management#
Both series and parallel combinations handle sample rate automatically.
Automatic Propagation#
import torchfx as fx
from torchfx.filter import iir
# Load audio (fs automatically extracted from file)
wave = fx.Wave.from_file("audio.wav") # fs = 44100
# Filters auto-configured with fs=44100
processed = wave | (
iir.LoButterworth(100) + iir.HiButterworth(5000)
)
When you use the pipeline operator:
Wavesets each filter’sfsattributeFor
ParallelFilterCombination,fspropagates to all child filtersFilters compute coefficients using the correct sample rate
Manual Configuration#
You can also set sample rate explicitly:
from torchfx.filter import iir, ParallelFilterCombination
# Create parallel combination with explicit fs
parallel = ParallelFilterCombination(
iir.HiButterworth(2000, order=2),
iir.HiChebyshev1(2000, order=2),
fs=44100
)
# All child filters now have fs=44100
The fs property automatically propagates to child filters that have fs=None.
Performance Considerations#
GPU Acceleration#
Both series and parallel work seamlessly on GPU:
import torchfx as fx
import torch
wave = fx.Wave.from_file("audio.wav")
# Move to GPU
if torch.cuda.is_available():
wave = wave.to("cuda")
# All processing happens on GPU
processed = wave | (filter1 + filter2) | filter3
# Move back to CPU for saving
processed.to("cpu").save("output.wav")
The device is automatically propagated because:
Filters inherit from
torch.nn.ModuleFilter coefficients are stored as
torch.TensorThe
Wavetensor carries device information
See also
GPU Acceleration - GPU acceleration guide
Memory Efficiency#
For parallel combinations with many filters:
# Current implementation collects all outputs
outputs = [f.forward(x) for f in self.filters] # List of N tensors
# Then sums them
for output in outputs:
results += output
This holds all filter outputs in memory simultaneously. For very large signals or many filters, consider processing in chunks.
Mixing with PyTorch Modules#
Since filters inherit from torch.nn.Module, they work with standard PyTorch containers:
import torch.nn as nn
import torchfx as fx
from torchfx.filter import iir
# Use nn.Sequential
effect_chain = nn.Sequential(
iir.LoButterworth(100, order=2),
iir.HiButterworth(5000, order=4),
fx.effect.Normalize()
)
wave = fx.Wave.from_file("audio.wav")
processed = wave | effect_chain
You can also mix TorchFX filters with torch.nn.Module instances from any other PyTorch ecosystem library (e.g., torchaudio transforms, if installed separately). TorchFX’s pipe operator forwards the underlying tensor through any nn.Module:
from torchfx.filter import iir
processed = (
wave
| iir.LoButterworth(100, order=2)
| iir.HiButterworth(2000, order=2) + iir.HiChebyshev1(2000, order=2)
| fx.effect.Gain(0.5, gain_type="amplitude") # -6 dB
| fx.effect.Reverb()
)
Real-World Examples#
Vocal Processing Chain#
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("vocal.wav")
# Professional vocal processing
processed = (
wave
# Remove rumble and noise
| iir.HiButterworth(cutoff=80, order=2)
| iir.LoButterworth(cutoff=12000, order=4)
# Parallel EQ boosts for presence and air
| (
iir.ParametricEQ(frequency=3000, gain=3, q=1.0) + # Presence
iir.ParametricEQ(frequency=10000, gain=2, q=0.7) # Air
)
# Trim level and normalize
| fx.effect.Gain(0.7, gain_type="amplitude")
| fx.effect.Normalize(peak=0.95)
)
Multi-Band Processing#
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("master.wav")
# Three-band processing. There is no standalone bandpass filter --
# synthesize the mid band from a high-pass + low-pass cascade.
low = wave | iir.LoButterworth(200, order=4) | fx.effect.Gain(1.2)
mid = (
wave
| iir.HiButterworth(200, order=2)
| iir.LoButterworth(2000, order=2)
| fx.effect.Gain(0.8, gain_type="amplitude")
)
high = wave | iir.HiButterworth(2000, order=4) | fx.effect.Gain(1.1)
# Recombine bands
multiband = fx.Wave.merge([low, mid, high], split_channels=False)
Creative Parallel Effects#
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("guitar.wav")
# Parallel processing for thickness
processed = (
wave
# Three parallel delay lines
| (
fx.effect.Delay(delay_samples=1000, feedback=0.3) +
fx.effect.Delay(delay_samples=1500, feedback=0.25) +
fx.effect.Delay(delay_samples=2000, feedback=0.2)
)
# Final processing
| fx.effect.Normalize(peak=0.9)
)
Best Practices#
Use Multi-Line for Readability#
# ✅ GOOD: Clear, readable
processed = (
wave
| filter1
| filter2 + filter3
| filter4
)
# ❌ BAD: Hard to read
processed = wave | filter1 | filter2 + filter3 | filter4
Name Complex Sections#
# ✅ GOOD: Named sections
parallel_hp = iir.HiButterworth(2000, order=4) + iir.HiChebyshev1(2000, order=2)
processed = wave | iir.LoButterworth(100) | parallel_hp | fx.effect.Gain(0.5)
# ❌ LESS GOOD: Inline everything
processed = wave | iir.LoButterworth(100) | iir.HiButterworth(2000, order=4) + iir.HiChebyshev1(2000, order=2) | fx.effect.Gain(0.5)
Use Parentheses for Clarity#
# ✅ GOOD: Explicit grouping
result = wave | (lowpass + highpass) | gain
# ⚠️ WORKS: Relies on operator precedence
result = wave | lowpass + highpass | gain
Reuse Filter Combinations#
# ✅ GOOD: Reusable filter
mastering_chain = (
iir.HiButterworth(30, order=2) +
iir.LoButterworth(18000, order=4)
)
# Apply to multiple files
for file in audio_files:
wave = fx.Wave.from_file(file)
processed = wave | mastering_chain | fx.effect.Normalize()
processed.save(f"mastered_{file}")
Common Pitfalls#
Forgetting Device Management#
# ❌ WRONG: Mixing CPU and GPU
wave_gpu = fx.Wave.from_file("audio.wav").to("cuda")
processed = wave_gpu | filter_chain
processed.save("output.wav") # Error: can't save CUDA tensor
# ✅ CORRECT: Move to CPU before saving
processed.to("cpu").save("output.wav")
Misunderstanding Parallel Summation#
# Parallel combination SUMS outputs, not average
parallel = filter1 + filter2
# Output amplitude may be higher than input
# Consider adding gain reduction if needed
parallel_with_gain = (filter1 + filter2) | fx.effect.Gain(0.5)
Sample Rate Mismatch#
# ❌ WRONG: Manually created filters without fs
filter1 = iir.LoButterworth(100)
filter1.fs = 44100
filter2 = iir.HiButterworth(5000)
# filter2.fs is None!
parallel = ParallelFilterCombination(filter1, filter2)
# ✅ CORRECT: Let Wave set fs automatically
parallel = iir.LoButterworth(100) + iir.HiButterworth(5000)
wave = fx.Wave.from_file("audio.wav") # fs auto-configured
processed = wave | parallel
External Resources#
Digital Filter on Wikipedia - Filter theory and design
Series and Parallel Circuits - Electrical circuit analogy
PyTorch nn.Module - Module base class