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/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}"]