# BPM-Synced Delay Effects
Learn how to create musically-timed delay effects using TorchFX's BPM synchronization system. This tutorial covers musical time divisions, tempo-synced delays, and rhythmic echo patterns.
## Overview
In music production, delays are often synchronized to the tempo (BPM) rather than specified in milliseconds. This ensures delays stay musically coherent even when the tempo changes. TorchFX's {class}`~torchfx.effect.Delay` effect supports:
- **Musical time notation**: Quarter notes (`1/4`), eighth notes (`1/8`), etc.
- **BPM synchronization**: Automatic calculation from tempo
- **Dotted and triplet notes**: `1/4d`, `1/8t`, etc.
- **Multiple taps**: Create rhythmic echo patterns
- **Delay strategies**: Mono and ping-pong stereo effects
```{mermaid}
graph LR
Input[User Input] --> MT[Musical Time
e.g., 1/8]
BPM[BPM
e.g., 120] --> Calc[Calculate
Duration]
MT --> Calc
FS[Sample Rate
fs=44100] --> Samples[Convert to
Samples]
Calc --> Samples
Samples --> Delay[Apply Delay
with Taps]
Delay --> Output[Delayed Audio]
style Input fill:#e1f5ff
style Output fill:#e1f5ff
style MT fill:#fff5e1
style BPM fill:#ffe1e1
style Calc fill:#e8f5e1
```
## Musical Time Notation
### Basic Time Divisions
Musical time in TorchFX uses the format `n/d[modifier]`:
| Notation | Name | Bar Fraction | At 120 BPM (4/4) |
|----------|------|--------------|------------------|
| `1/1` | Whole note | 1.0 bars | 2.0 seconds |
| `1/2` | Half note | 0.5 bars | 1.0 seconds |
| `1/4` | Quarter note | 0.25 bars | 0.5 seconds |
| `1/8` | Eighth note | 0.125 bars | 0.25 seconds |
| `1/16` | Sixteenth note | 0.0625 bars | 0.125 seconds |
| `3/16` | Three sixteenths | 0.1875 bars | 0.375 seconds |
### Modifiers
Add modifiers for dotted and triplet notes:
| Modifier | Multiplier | Example | Bar Fraction | At 120 BPM (4/4) |
|----------|------------|---------|--------------|------------------|
| ` ` (none) | ×1.0 | `1/4` | 0.25 bars | 0.5 seconds |
| `d` (dotted) | ×1.5 | `1/4d` | 0.375 bars | 0.75 seconds |
| `t` (triplet) | ×1/3 | `1/4t` | 0.0833 bars | 0.167 seconds |
**Dotted notes**: Add half the original duration (1.5×)
**Triplet notes**: Fit three notes in the space of two (1/3×)
```python
from torchfx.typing import MusicalTime
# Parse musical time notation
quarter = MusicalTime.from_string("1/4")
print(quarter.fraction()) # 0.25 bars
dotted_quarter = MusicalTime.from_string("1/4d")
print(dotted_quarter.fraction()) # 0.375 bars
eighth_triplet = MusicalTime.from_string("1/8t")
print(eighth_triplet.fraction()) # 0.0417 bars (1/24)
# Convert to seconds at specific BPM
duration = quarter.duration_seconds(bpm=120, beats_per_bar=4)
print(f"Duration: {duration} seconds") # 0.5 seconds
```
```{seealso}
{doc}`/guides/core-concepts/type-system` - Complete type system documentation
```
## BPM-to-Samples Conversion
Understanding how musical time converts to sample-accurate delays:
```{mermaid}
graph TB
Start[Input: BPM=120
delay_time=1/4
fs=44100]
Parse[Parse Musical Time
MusicalTime.from_string]
Fraction[Calculate Bar Fraction
fraction = 0.25]
BeatDur[Calculate Beat Duration
60 / 120 = 0.5s]
BarDur[Calculate Bar Duration
0.5s × 4 beats = 2.0s]
NoteDur[Calculate Note Duration
0.25 bars × 2.0s = 0.5s]
Samples[Convert to Samples
0.5s × 44100 Hz = 22050]
Start --> Parse
Parse --> Fraction
Start --> BeatDur
BeatDur --> BarDur
Fraction --> NoteDur
BarDur --> NoteDur
NoteDur --> Samples
style Start fill:#e1f5ff
style Samples fill:#e1f5ff
```
**Formula**:
$$
\text{samples} = \frac{n}{d} \times m \times \frac{60}{BPM} \times \text{beats\_per\_bar} \times f_s
$$
Where:
- $n/d$ is the note division (e.g., 1/4)
- $m$ is the modifier (1.0, 1.5, or 1/3)
- $BPM$ is beats per minute
- $\text{beats\_per\_bar}$ is typically 4 (for 4/4 time)
- $f_s$ is the sample rate
## Basic Usage
### With Wave Pipeline (Automatic fs)
The recommended way—let {class}`~torchfx.Wave` configure the sample rate:
```python
import torchfx as fx
# Load audio
wave = fx.Wave.from_file("audio.wav") # fs = 44100
# Create BPM-synced delay (fs auto-configured)
delay = fx.effect.Delay(
bpm=128, # 128 beats per minute
delay_time="1/8", # Eighth note delay
feedback=0.3, # 30% feedback for echoes
mix=0.2, # 20% wet/dry mix
taps=3 # 3 delay taps
)
# Apply using pipeline operator
delayed = wave | delay # fs automatically set to 44100
delayed.save("delayed.wav")
```
### With Explicit Sample Rate
When working directly with tensors:
```python
import torch
import torchfx as fx
# Create or load waveform
waveform = torch.randn(2, 44100) # Stereo, 1 second
# Create delay with explicit fs
delay = fx.effect.Delay(
bpm=128,
delay_time="1/8",
fs=44100, # Explicit sample rate
feedback=0.3,
mix=0.2
)
# Apply to tensor
delayed = delay(waveform)
```
### Direct Sample Specification
For non-musical delays or precise control:
```python
import torchfx as fx
# Delay by exact number of samples
delay = fx.effect.Delay(
delay_samples=2205, # 50ms at 44.1kHz
feedback=0.4,
mix=0.3,
taps=4
)
# No BPM or fs needed when using delay_samples
delayed = delay(waveform)
```
## Taps and Feedback
### Understanding Taps
**Taps** create multiple echoes of the original signal. Each tap is delayed by `tap_number × delay_time`:
```python
import torchfx as fx
wave = fx.Wave.from_file("audio.wav")
# Create delay with 4 taps
delay = fx.effect.Delay(
bpm=120,
delay_time="1/4", # 0.5s at 120 BPM
feedback=0.6,
taps=4
)
delayed = wave | delay
# Results in echoes at:
# Tap 1: 0.5s (amplitude = 1.0)
# Tap 2: 1.0s (amplitude = 0.6^1 = 0.6)
# Tap 3: 1.5s (amplitude = 0.6^2 = 0.36)
# Tap 4: 2.0s (amplitude = 0.6^3 = 0.216)
```
### Feedback Decay Pattern
The first tap always has amplitude 1.0. Subsequent taps decay exponentially:
$$
\text{amplitude}_n = \begin{cases}
1.0 & \text{if } n = 1 \\
\text{feedback}^{n-1} & \text{if } n > 1
\end{cases}
$$
**Example visualization**:
```python
import matplotlib.pyplot as plt
import numpy as np
feedback = 0.6
taps = 6
tap_numbers = np.arange(1, taps + 1)
amplitudes = np.array([1.0] + [feedback**(n-1) for n in range(2, taps + 1)])
plt.stem(tap_numbers, amplitudes)
plt.xlabel("Tap Number")
plt.ylabel("Amplitude")
plt.title(f"Delay Tap Amplitudes (feedback={feedback})")
plt.grid(True)
plt.show()
```
### Output Length Extension
The output is extended to accommodate all delay taps:
```python
original_length = 44100 # 1 second at 44.1kHz
delay_samples = 22050 # 0.5 second delay
taps = 3
output_length = original_length + (delay_samples * taps)
# output_length = 44100 + (22050 * 3) = 110250 samples (~2.5 seconds)
```
## Delay Strategies
### MonoDelayStrategy (Default)
Applies identical delay to all channels:
```python
import torchfx as fx
from torchfx.effect import Delay, MonoDelayStrategy
wave = fx.Wave.from_file("stereo.wav")
# Mono strategy (default)
delay = Delay(
bpm=120,
delay_time="1/8",
feedback=0.4,
mix=0.3,
strategy=MonoDelayStrategy() # Optional, this is default
)
delayed = wave | delay
```
**Behavior**: Each channel receives the same delay pattern independently.
### PingPongDelayStrategy
Creates alternating delays between left and right stereo channels:
```python
import torchfx as fx
from torchfx.effect import Delay, PingPongDelayStrategy
wave = fx.Wave.from_file("stereo.wav") # Must be stereo
# Ping-pong delay
delay = Delay(
bpm=120,
delay_time="1/8",
feedback=0.5,
mix=0.4,
taps=6,
strategy=PingPongDelayStrategy()
)
delayed = wave | delay
```
**Ping-pong pattern**:
- **Odd taps (1, 3, 5)**: Left → Right
- **Even taps (2, 4, 6)**: Right → Left
```{mermaid}
sequenceDiagram
participant L as Left Channel
participant R as Right Channel
Note over L,R: Original
rect rgb(200, 220, 255)
L->>L: Original left
R->>R: Original right
end
Note over L,R: Tap 1 (amp=1.0)
rect rgb(255, 220, 200)
L->>R: L→R
end
Note over L,R: Tap 2 (amp=feedback)
rect rgb(200, 255, 220)
R->>L: R→L
end
Note over L,R: Tap 3 (amp=feedback²)
rect rgb(255, 220, 200)
L->>R: L→R
end
Note over L,R: Result: Bouncing effect
```
**Fallback**: If input is not stereo, automatically uses {class}`~torchfx.effect.MonoDelayStrategy`.
## Musical Applications
### Common Genre Patterns
| Genre | Typical BPM | Common Delays | Use Case |
|-------|-------------|---------------|----------|
| House | 120-130 | `1/8`, `1/16` | Rhythmic vocal delays |
| Techno | 125-135 | `1/16`, `1/32` | Fast percussion echoes |
| Dubstep | 140 | `1/4d`, `1/8t` | Syncopated delays |
| Hip-Hop | 80-110 | `1/4`, `1/8` | Vocal doubling |
| Ambient | 60-90 | `1/2`, `1/4d` | Long atmospheric delays |
### Dubstep Delay Example
Classic dubstep delay using dotted eighth notes:
```python
import torchfx as fx
from torchfx.effect import Delay, PingPongDelayStrategy
wave = fx.Wave.from_file("synth.wav")
# Dotted eighth creates syncopated rhythm
delay = Delay(
bpm=140,
delay_time="1/8d", # Dotted eighth = 0.1875 bars
feedback=0.5,
mix=0.4,
taps=3,
strategy=PingPongDelayStrategy()
)
delayed = wave | delay
delayed.save("synth_delayed.wav")
```
**Why dotted eighth?** At 140 BPM, a dotted eighth note creates a syncopated rhythm that's iconic in dubstep.
### Vocal Doubling
Subtle delay for thickening vocals:
```python
import torchfx as fx
wave = fx.Wave.from_file("vocal.wav")
# Short delay for doubling effect
doubler = fx.effect.Delay(
bpm=100,
delay_time="1/16", # Very short delay
feedback=0.0, # No feedback (single tap)
mix=0.3, # Subtle blend
taps=1 # Just one echo
)
doubled = wave | doubler
```
### Ambient Atmosphere
Long delays with high feedback:
```python
import torchfx as fx
wave = fx.Wave.from_file("pad.wav")
# Long atmospheric delay
ambient_delay = fx.effect.Delay(
bpm=70,
delay_time="1/2", # Half note = long delay
feedback=0.7, # High feedback for many echoes
mix=0.5, # Prominent effect
taps=8 # Many echoes
)
atmospheric = wave | ambient_delay
```
## Complex Processing Chains
### Combining with Filters
Create frequency-dependent delays:
```python
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("audio.wav")
# Pre-filter the delay
processed = (
wave
| iir.HiButterworth(cutoff=100, order=2) # Remove rumble
| fx.effect.Delay(
bpm=128,
delay_time="1/4",
feedback=0.5,
mix=0.3
)
| iir.LoButterworth(cutoff=8000, order=4) # Soften delays
)
```
### Multi-Effect Chain
Complete production-ready chain:
```python
import torchfx as fx
from torchfx.filter import iir
wave = fx.Wave.from_file("vocal.wav")
# Professional vocal processing
processed = (
wave
# EQ
| iir.HiButterworth(cutoff=80, order=2)
| iir.PeakingEQ(freq=3000, gain_db=3, q=1.0)
# Dynamics
| fx.effect.Compressor(threshold=0.5, ratio=4.0)
# Delay (quarter note at 120 BPM)
| fx.effect.Delay(
bpm=120,
delay_time="1/4",
feedback=0.4,
mix=0.25,
taps=3
)
# Reverb
| fx.effect.Reverb(delay=4410, decay=0.5, mix=0.2)
# Final normalization
| fx.effect.Normalize(peak=0.95)
)
processed.save("vocal_processed.wav")
```
## Advanced Techniques
### Tempo Automation
Apply different delays for different sections:
```python
import torchfx as fx
wave = fx.Wave.from_file("full_track.wav")
# Process intro (slow tempo)
intro_delay = fx.effect.Delay(bpm=90, delay_time="1/4", feedback=0.6, mix=0.4)
# Process verse (faster tempo)
verse_delay = fx.effect.Delay(bpm=120, delay_time="1/8", feedback=0.4, mix=0.3)
# Split audio and apply different delays
# (Manual splitting required)
intro = wave.ys[:, :44100*8] # First 8 seconds
verse = wave.ys[:, 44100*8:] # Rest
intro_processed = intro_delay(intro)
verse_processed = verse_delay(verse)
```
### Creative Feedback Patterns
Use extreme feedback for creative effects:
```python
import torchfx as fx
wave = fx.Wave.from_file("drum_loop.wav")
# High feedback creates infinite delays (carefully!)
infinite_delay = fx.effect.Delay(
bpm=128,
delay_time="1/16",
feedback=0.95, # Very high feedback (max allowed)
mix=0.3,
taps=16 # Many taps
)
# Result: Long, dense echo tail
infinite = wave | infinite_delay
```
⚠️ **Warning**: Feedback > 0.95 can cause numerical instability and is clamped.
### Layered Delays
Stack multiple delays for complex patterns:
```python
import torchfx as fx
wave = fx.Wave.from_file("audio.wav")
# Quarter note and dotted eighth
delay1 = fx.effect.Delay(bpm=128, delay_time="1/4", feedback=0.4, mix=0.2)
delay2 = fx.effect.Delay(bpm=128, delay_time="1/8d", feedback=0.3, mix=0.15)
# Apply both
layered = wave | delay1 | delay2
```
## Lazy vs Eager Calculation
The {class}`~torchfx.effect.Delay` effect supports two calculation modes:
### Eager Calculation
When `fs` is provided at initialization:
```python
delay = fx.effect.Delay(
bpm=120,
delay_time="1/4",
fs=44100, # fs provided: calculate immediately
feedback=0.4
)
# Delay samples already calculated: 22050
print(delay.delay_samples) # 22050
```
### Lazy Calculation
When `fs` is `None` (deferred until `forward()`):
```python
delay = fx.effect.Delay(
bpm=120,
delay_time="1/4",
# fs=None: defer calculation
feedback=0.4
)
print(delay._needs_calculation) # True
# Wave sets fs automatically
wave = fx.Wave.from_file("audio.wav")
delayed = wave | delay
print(delay._needs_calculation) # False (calculated during forward)
print(delay.delay_samples) # 22050 (at fs=44100)
```
**Use lazy calculation** when using the {term}`pipeline operator` with {class}`~torchfx.Wave`.
## Error Handling
### Common Errors
```python
import torchfx as fx
# ❌ ERROR: BPM required without delay_samples
delay = fx.effect.Delay(delay_time="1/4")
# AssertionError: BPM must be provided
# ❌ ERROR: fs required for lazy calculation
delay = fx.effect.Delay(bpm=120, delay_time="1/4")
waveform = torch.randn(2, 44100)
delayed = delay(waveform)
# AssertionError: Sample rate (fs) is required
# ❌ ERROR: Invalid musical time format
delay = fx.effect.Delay(bpm=120, delay_time="invalid", fs=44100)
# ValueError: Invalid musical time string
# ❌ ERROR: Feedback out of range
delay = fx.effect.Delay(delay_samples=1000, feedback=1.2)
# AssertionError: Feedback must be between 0 and 0.95
```
### Validation Checklist
Before applying delay, ensure:
- ✅ `bpm` is provided (if using musical time)
- ✅ `fs` is set (either explicitly or via Wave)
- ✅ `delay_time` matches pattern `n/d[d|t]`
- ✅ `feedback` is in [0, 0.95]
- ✅ `mix` is in [0, 1]
- ✅ `taps` >= 1
## Best Practices
### Choose Appropriate Mix Levels
```python
# ✅ GOOD: Subtle delay (20-30% mix)
subtle = fx.effect.Delay(bpm=120, delay_time="1/8", mix=0.25)
# ✅ GOOD: Prominent delay (40-60% mix)
prominent = fx.effect.Delay(bpm=120, delay_time="1/4", mix=0.5)
# ⚠️ USE CAREFULLY: Heavy delay (70-100% mix)
heavy = fx.effect.Delay(bpm=120, delay_time="1/2", mix=0.8)
```
### Match Delay to Tempo
```python
# ✅ GOOD: Delay matches song tempo
song_bpm = 128
delay = fx.effect.Delay(bpm=song_bpm, delay_time="1/8")
# ❌ BAD: Hardcoded delay time (not tempo-aware)
delay = fx.effect.Delay(delay_samples=10000) # What tempo is this?
```
### Use Feedback Wisely
```python
# ✅ GOOD: Moderate feedback (0.3-0.6)
musical = fx.effect.Delay(bpm=120, delay_time="1/4", feedback=0.4)
# ⚠️ CAREFUL: High feedback (0.7-0.95)
dense = fx.effect.Delay(bpm=120, delay_time="1/16", feedback=0.9)
# Can create very dense, potentially muddy delays
```
## Related Concepts
- {doc}`/guides/core-concepts/type-system` - Musical time and type system
- {doc}`custom-effects` - Creating custom effects
- {doc}`multi-channel-effects` - Multi-channel processing
- {doc}`/guides/core-concepts/pipeline-operator` - Pipeline operator usage
## External Resources
- [Note Values on Wikipedia](https://en.wikipedia.org/wiki/Note_value) - Understanding musical note durations
- [Delay Effect on Wikipedia](https://en.wikipedia.org/wiki/Delay_(audio_effect)) - Delay effect theory
- [Tempo on Wikipedia](https://en.wikipedia.org/wiki/Tempo) - Understanding BPM and tempo
## References
```{bibliography}
:filter: docname in docnames
:style: alpha
```