cysox 0.1.4__cp311-cp311-macosx_11_0_arm64.whl
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.
- cysox/__init__.py +61 -0
- cysox/__init__.pyi +223 -0
- cysox/__main__.py +102 -0
- cysox/audio.py +528 -0
- cysox/fx/__init__.py +113 -0
- cysox/fx/base.py +168 -0
- cysox/fx/convert.py +128 -0
- cysox/fx/eq.py +94 -0
- cysox/fx/filter.py +131 -0
- cysox/fx/reverb.py +215 -0
- cysox/fx/time.py +248 -0
- cysox/fx/volume.py +98 -0
- cysox/sox.cpython-311-darwin.so +0 -0
- cysox/utils.py +47 -0
- cysox/utils.pyi +21 -0
- cysox-0.1.4.dist-info/METADATA +339 -0
- cysox-0.1.4.dist-info/RECORD +21 -0
- cysox-0.1.4.dist-info/WHEEL +6 -0
- cysox-0.1.4.dist-info/entry_points.txt +2 -0
- cysox-0.1.4.dist-info/licenses/LICENSE +21 -0
- cysox-0.1.4.dist-info/top_level.txt +1 -0
cysox/fx/base.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Base classes for typed audio effects."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, List
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Effect(ABC):
|
|
11
|
+
"""Base class for all typed effects.
|
|
12
|
+
|
|
13
|
+
Subclasses must implement:
|
|
14
|
+
- name: The sox effect name
|
|
15
|
+
- to_args(): Convert parameters to sox argument list
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
"""The sox effect name."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def to_args(self) -> List[str]:
|
|
26
|
+
"""Convert effect parameters to sox argument list."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def _repr_args(self) -> str:
|
|
30
|
+
"""Return string representation of arguments for __repr__."""
|
|
31
|
+
args = []
|
|
32
|
+
for key, value in self.__dict__.items():
|
|
33
|
+
if not key.startswith('_'):
|
|
34
|
+
args.append(f"{key}={value!r}")
|
|
35
|
+
return ", ".join(args)
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
return f"{self.__class__.__name__}({self._repr_args()})"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CompositeEffect(Effect):
|
|
42
|
+
"""Base class for effects that combine multiple sox effects.
|
|
43
|
+
|
|
44
|
+
Subclasses must implement the `effects` property returning a list
|
|
45
|
+
of Effect instances to be applied in sequence.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
class WarmReverb(CompositeEffect):
|
|
49
|
+
def __init__(self, decay=60):
|
|
50
|
+
self.decay = decay
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def effects(self):
|
|
54
|
+
return [
|
|
55
|
+
HighPass(frequency=80),
|
|
56
|
+
Reverb(reverberance=self.decay),
|
|
57
|
+
Volume(db=-2),
|
|
58
|
+
]
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def effects(self) -> List[Effect]:
|
|
64
|
+
"""Return list of constituent effects."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def name(self) -> str:
|
|
69
|
+
return self.__class__.__name__
|
|
70
|
+
|
|
71
|
+
def to_args(self) -> List[str]:
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"CompositeEffect '{self.name}' must be expanded, not converted to args. "
|
|
74
|
+
"Use expand_effects() to get the list of constituent effects."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PythonEffect(Effect):
|
|
79
|
+
"""Base class for custom Python-based sample processing.
|
|
80
|
+
|
|
81
|
+
Subclasses must implement the `process()` method which receives
|
|
82
|
+
samples as a numpy array and returns processed samples.
|
|
83
|
+
|
|
84
|
+
Note: Python effects require numpy and run outside the sox pipeline,
|
|
85
|
+
making them slower than native sox effects. Use for custom DSP that
|
|
86
|
+
sox doesn't support.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
class BitCrusher(PythonEffect):
|
|
90
|
+
def __init__(self, bits=8):
|
|
91
|
+
self.bits = bits
|
|
92
|
+
|
|
93
|
+
def process(self, samples, sample_rate, channels):
|
|
94
|
+
levels = 2 ** self.bits
|
|
95
|
+
return np.round(samples * levels) / levels
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def name(self) -> str:
|
|
100
|
+
return self.__class__.__name__
|
|
101
|
+
|
|
102
|
+
def to_args(self) -> List[str]:
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"PythonEffect '{self.name}' cannot be converted to sox args. "
|
|
105
|
+
"It will be processed separately using the process() method."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def process(
|
|
110
|
+
self,
|
|
111
|
+
samples: "np.ndarray",
|
|
112
|
+
sample_rate: int,
|
|
113
|
+
channels: int
|
|
114
|
+
) -> "np.ndarray":
|
|
115
|
+
"""Process audio samples.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
samples: Input samples as numpy array of shape (n_samples,) for mono
|
|
119
|
+
or (n_samples, channels) for multi-channel.
|
|
120
|
+
sample_rate: Sample rate in Hz.
|
|
121
|
+
channels: Number of audio channels.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Processed samples as numpy array with same shape as input.
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CEffect(Effect):
|
|
130
|
+
"""Base class for custom C-level effects.
|
|
131
|
+
|
|
132
|
+
For advanced users who implement effects in C or Cython and need
|
|
133
|
+
to register them with sox.
|
|
134
|
+
|
|
135
|
+
Subclasses should:
|
|
136
|
+
1. Set _handler_ptr to the pointer returned by their Cython module
|
|
137
|
+
2. Call register() once at startup before using the effect
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
_handler_ptr: int = None
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def register(cls) -> None:
|
|
144
|
+
"""Register this effect's handler with sox.
|
|
145
|
+
|
|
146
|
+
Must be called once before using the effect.
|
|
147
|
+
"""
|
|
148
|
+
if cls._handler_ptr is None:
|
|
149
|
+
raise ValueError(f"{cls.__name__}._handler_ptr is not set")
|
|
150
|
+
|
|
151
|
+
from cysox import sox
|
|
152
|
+
if hasattr(sox, 'register_effect_handler'):
|
|
153
|
+
sox.register_effect_handler(cls._handler_ptr)
|
|
154
|
+
else:
|
|
155
|
+
raise NotImplementedError(
|
|
156
|
+
"Custom C effect registration not yet implemented in low-level API"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
@abstractmethod
|
|
161
|
+
def name(self) -> str:
|
|
162
|
+
"""The registered effect name."""
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def to_args(self) -> List[str]:
|
|
167
|
+
"""Convert parameters to sox argument list."""
|
|
168
|
+
pass
|
cysox/fx/convert.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Format conversion effects (rate, channels, bits)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from .base import Effect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Rate(Effect):
|
|
9
|
+
"""Resample to a different sample rate.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
sample_rate: Target sample rate in Hz.
|
|
13
|
+
quality: Resampling quality - 'quick', 'low', 'medium', 'high',
|
|
14
|
+
or 'very-high' (default: 'high').
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> fx.Rate(sample_rate=48000)
|
|
18
|
+
>>> fx.Rate(sample_rate=44100, quality='very-high')
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
QUALITY_FLAGS = {
|
|
22
|
+
"quick": "-q",
|
|
23
|
+
"low": "-l",
|
|
24
|
+
"medium": "-m",
|
|
25
|
+
"high": "-h",
|
|
26
|
+
"very-high": "-v",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __init__(self, sample_rate: int, *, quality: str = "high"):
|
|
30
|
+
if quality not in self.QUALITY_FLAGS:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"quality must be one of: {', '.join(self.QUALITY_FLAGS.keys())}"
|
|
33
|
+
)
|
|
34
|
+
self.sample_rate = sample_rate
|
|
35
|
+
self.quality = quality
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
return "rate"
|
|
40
|
+
|
|
41
|
+
def to_args(self) -> List[str]:
|
|
42
|
+
return [self.QUALITY_FLAGS[self.quality], str(self.sample_rate)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Channels(Effect):
|
|
46
|
+
"""Change the number of audio channels.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
channels: Target number of channels (1=mono, 2=stereo, etc.).
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> fx.Channels(channels=1) # Convert to mono
|
|
53
|
+
>>> fx.Channels(channels=2) # Convert to stereo
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, channels: int):
|
|
57
|
+
if channels < 1:
|
|
58
|
+
raise ValueError("channels must be at least 1")
|
|
59
|
+
self.channels = channels
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def name(self) -> str:
|
|
63
|
+
return "channels"
|
|
64
|
+
|
|
65
|
+
def to_args(self) -> List[str]:
|
|
66
|
+
return [str(self.channels)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Remix(Effect):
|
|
70
|
+
"""Remix channels with custom mix specification.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
mix: Channel mix specification. Each output channel is specified
|
|
74
|
+
as a string like "1,2" (mix channels 1 and 2) or "1v0.5,2v0.5"
|
|
75
|
+
(mix with volume adjustment). Use "-" for silence.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> fx.Remix(mix=["1,2"]) # Mono mixdown
|
|
79
|
+
>>> fx.Remix(mix=["1", "2"]) # Keep stereo as-is
|
|
80
|
+
>>> fx.Remix(mix=["2", "1"]) # Swap L/R
|
|
81
|
+
>>> fx.Remix(mix=["1v0.5,2v0.5"]) # Mono with equal mix
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, mix: List[str]):
|
|
85
|
+
self.mix = mix
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def name(self) -> str:
|
|
89
|
+
return "remix"
|
|
90
|
+
|
|
91
|
+
def to_args(self) -> List[str]:
|
|
92
|
+
return self.mix
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Dither(Effect):
|
|
96
|
+
"""Apply dithering for bit-depth reduction.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
type: Dither type - 'rectangular', 'triangular', 'gaussian',
|
|
100
|
+
or 'shaped' (default: 'shaped').
|
|
101
|
+
precision: Target precision in bits (optional).
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> fx.Dither() # Default shaped dither
|
|
105
|
+
>>> fx.Dither(type='triangular') # TPDF dither
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
type: str = "shaped",
|
|
112
|
+
precision: Optional[int] = None
|
|
113
|
+
):
|
|
114
|
+
valid_types = ("rectangular", "triangular", "gaussian", "shaped")
|
|
115
|
+
if type not in valid_types:
|
|
116
|
+
raise ValueError(f"type must be one of: {', '.join(valid_types)}")
|
|
117
|
+
self.type = type
|
|
118
|
+
self.precision = precision
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def name(self) -> str:
|
|
122
|
+
return "dither"
|
|
123
|
+
|
|
124
|
+
def to_args(self) -> List[str]:
|
|
125
|
+
args = [f"-{self.type[0]}"] # First letter
|
|
126
|
+
if self.precision is not None:
|
|
127
|
+
args.extend(["-p", str(self.precision)])
|
|
128
|
+
return args
|
cysox/fx/eq.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Equalization effects (bass, treble, equalizer)."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from .base import Effect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Bass(Effect):
|
|
9
|
+
"""Boost or cut bass frequencies.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
gain: Amount to boost (positive) or cut (negative) in dB.
|
|
13
|
+
frequency: Center frequency in Hz (default: 100).
|
|
14
|
+
width: Filter width in octaves (default: 0.5).
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> fx.Bass(gain=5) # Boost bass by 5dB
|
|
18
|
+
>>> fx.Bass(gain=-3, frequency=80) # Cut at 80Hz
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
gain: float,
|
|
24
|
+
*,
|
|
25
|
+
frequency: float = 100,
|
|
26
|
+
width: float = 0.5
|
|
27
|
+
):
|
|
28
|
+
self.gain = gain
|
|
29
|
+
self.frequency = frequency
|
|
30
|
+
self.width = width
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def name(self) -> str:
|
|
34
|
+
return "bass"
|
|
35
|
+
|
|
36
|
+
def to_args(self) -> List[str]:
|
|
37
|
+
return [str(self.gain), str(self.frequency), str(self.width)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Treble(Effect):
|
|
41
|
+
"""Boost or cut treble frequencies.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
gain: Amount to boost (positive) or cut (negative) in dB.
|
|
45
|
+
frequency: Center frequency in Hz (default: 3000).
|
|
46
|
+
width: Filter width in octaves (default: 0.5).
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> fx.Treble(gain=3) # Boost treble by 3dB
|
|
50
|
+
>>> fx.Treble(gain=-2, frequency=4000) # Cut at 4kHz
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
gain: float,
|
|
56
|
+
*,
|
|
57
|
+
frequency: float = 3000,
|
|
58
|
+
width: float = 0.5
|
|
59
|
+
):
|
|
60
|
+
self.gain = gain
|
|
61
|
+
self.frequency = frequency
|
|
62
|
+
self.width = width
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str:
|
|
66
|
+
return "treble"
|
|
67
|
+
|
|
68
|
+
def to_args(self) -> List[str]:
|
|
69
|
+
return [str(self.gain), str(self.frequency), str(self.width)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Equalizer(Effect):
|
|
73
|
+
"""Peaking EQ filter at a specific frequency.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
frequency: Center frequency in Hz.
|
|
77
|
+
width: Filter width (Q factor or bandwidth in Hz).
|
|
78
|
+
gain: Amount to boost (positive) or cut (negative) in dB.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> fx.Equalizer(frequency=1000, width=1, gain=3) # Boost 1kHz
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, frequency: float, width: float, gain: float):
|
|
85
|
+
self.frequency = frequency
|
|
86
|
+
self.width = width
|
|
87
|
+
self.gain = gain
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def name(self) -> str:
|
|
91
|
+
return "equalizer"
|
|
92
|
+
|
|
93
|
+
def to_args(self) -> List[str]:
|
|
94
|
+
return [str(self.frequency), str(self.width), str(self.gain)]
|
cysox/fx/filter.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Filter effects (highpass, lowpass, bandpass, etc.)."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from .base import Effect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HighPass(Effect):
|
|
9
|
+
"""High-pass filter (removes low frequencies).
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
frequency: Cutoff frequency in Hz.
|
|
13
|
+
poles: Filter order, 1 or 2 (default: 2). Higher = steeper rolloff.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> fx.HighPass(frequency=80) # Remove rumble below 80Hz
|
|
17
|
+
>>> fx.HighPass(frequency=200, poles=1) # Gentle rolloff
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, frequency: float, *, poles: int = 2):
|
|
21
|
+
if poles not in (1, 2):
|
|
22
|
+
raise ValueError("poles must be 1 or 2")
|
|
23
|
+
self.frequency = frequency
|
|
24
|
+
self.poles = poles
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return "highpass"
|
|
29
|
+
|
|
30
|
+
def to_args(self) -> List[str]:
|
|
31
|
+
return [f"-{self.poles}", str(self.frequency)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LowPass(Effect):
|
|
35
|
+
"""Low-pass filter (removes high frequencies).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
frequency: Cutoff frequency in Hz.
|
|
39
|
+
poles: Filter order, 1 or 2 (default: 2). Higher = steeper rolloff.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> fx.LowPass(frequency=8000) # Remove highs above 8kHz
|
|
43
|
+
>>> fx.LowPass(frequency=4000, poles=1) # Gentle rolloff
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, frequency: float, *, poles: int = 2):
|
|
47
|
+
if poles not in (1, 2):
|
|
48
|
+
raise ValueError("poles must be 1 or 2")
|
|
49
|
+
self.frequency = frequency
|
|
50
|
+
self.poles = poles
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self) -> str:
|
|
54
|
+
return "lowpass"
|
|
55
|
+
|
|
56
|
+
def to_args(self) -> List[str]:
|
|
57
|
+
return [f"-{self.poles}", str(self.frequency)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BandPass(Effect):
|
|
61
|
+
"""Band-pass filter (passes frequencies within a range).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
frequency: Center frequency in Hz.
|
|
65
|
+
width: Filter width. Interpretation depends on width_type.
|
|
66
|
+
width_type: 'q' for Q-factor, 'h' for Hz, 'o' for octaves (default: 'q').
|
|
67
|
+
constant_skirt: Use constant skirt gain (default: False).
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> fx.BandPass(frequency=1000, width=2) # Q=2 bandpass at 1kHz
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
frequency: float,
|
|
76
|
+
width: float,
|
|
77
|
+
*,
|
|
78
|
+
width_type: str = "q",
|
|
79
|
+
constant_skirt: bool = False
|
|
80
|
+
):
|
|
81
|
+
if width_type not in ("q", "h", "o"):
|
|
82
|
+
raise ValueError("width_type must be 'q', 'h', or 'o'")
|
|
83
|
+
self.frequency = frequency
|
|
84
|
+
self.width = width
|
|
85
|
+
self.width_type = width_type
|
|
86
|
+
self.constant_skirt = constant_skirt
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def name(self) -> str:
|
|
90
|
+
return "bandpass"
|
|
91
|
+
|
|
92
|
+
def to_args(self) -> List[str]:
|
|
93
|
+
args = []
|
|
94
|
+
if self.constant_skirt:
|
|
95
|
+
args.append("-c")
|
|
96
|
+
args.append(str(self.frequency))
|
|
97
|
+
args.append(f"{self.width}{self.width_type}")
|
|
98
|
+
return args
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BandReject(Effect):
|
|
102
|
+
"""Band-reject (notch) filter.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
frequency: Center frequency in Hz.
|
|
106
|
+
width: Filter width. Interpretation depends on width_type.
|
|
107
|
+
width_type: 'q' for Q-factor, 'h' for Hz, 'o' for octaves (default: 'q').
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
>>> fx.BandReject(frequency=60, width=10) # Remove 60Hz hum
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
frequency: float,
|
|
116
|
+
width: float,
|
|
117
|
+
*,
|
|
118
|
+
width_type: str = "q"
|
|
119
|
+
):
|
|
120
|
+
if width_type not in ("q", "h", "o"):
|
|
121
|
+
raise ValueError("width_type must be 'q', 'h', or 'o'")
|
|
122
|
+
self.frequency = frequency
|
|
123
|
+
self.width = width
|
|
124
|
+
self.width_type = width_type
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def name(self) -> str:
|
|
128
|
+
return "bandreject"
|
|
129
|
+
|
|
130
|
+
def to_args(self) -> List[str]:
|
|
131
|
+
return [str(self.frequency), f"{self.width}{self.width_type}"]
|