cysox 0.1.5__cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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-39-x86_64-linux-gnu.so +0 -0
- cysox/utils.py +47 -0
- cysox/utils.pyi +21 -0
- cysox-0.1.5.dist-info/METADATA +339 -0
- cysox-0.1.5.dist-info/RECORD +27 -0
- cysox-0.1.5.dist-info/WHEEL +6 -0
- cysox-0.1.5.dist-info/entry_points.txt +2 -0
- cysox-0.1.5.dist-info/licenses/LICENSE +21 -0
- cysox-0.1.5.dist-info/sboms/auditwheel.cdx.json +1 -0
- cysox-0.1.5.dist-info/top_level.txt +1 -0
- cysox.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
- cysox.libs/libgsm-6a620b60.so.1.0.17 +0 -0
- cysox.libs/libltdl-dfe8b62f.so.7 +0 -0
- cysox.libs/libpng16-2bf8e833.so.16.34.0 +0 -0
- cysox.libs/libsox-36efe91a.so.3.0.0 +0 -0
cysox/audio.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""High-level audio processing API for cysox.
|
|
2
|
+
|
|
3
|
+
This module provides a simplified, Pythonic interface for audio processing.
|
|
4
|
+
It handles initialization automatically and provides convenient functions
|
|
5
|
+
for common operations.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> import cysox
|
|
9
|
+
>>> from cysox import fx
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Get file info
|
|
12
|
+
>>> info = cysox.info('audio.wav')
|
|
13
|
+
>>> print(f"Duration: {info['duration']:.2f}s")
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Convert with effects
|
|
16
|
+
>>> cysox.convert('input.wav', 'output.mp3', effects=[
|
|
17
|
+
... fx.Normalize(),
|
|
18
|
+
... fx.Fade(fade_in=0.5),
|
|
19
|
+
... ])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import atexit
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, Iterator, List, Optional, Union
|
|
25
|
+
|
|
26
|
+
from . import sox
|
|
27
|
+
from .fx.base import Effect, CompositeEffect, PythonEffect
|
|
28
|
+
|
|
29
|
+
# Module state
|
|
30
|
+
_initialized = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ensure_init() -> None:
|
|
34
|
+
"""Ensure sox is initialized (called automatically)."""
|
|
35
|
+
global _initialized
|
|
36
|
+
if not _initialized:
|
|
37
|
+
sox.init()
|
|
38
|
+
atexit.register(_cleanup)
|
|
39
|
+
_initialized = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cleanup() -> None:
|
|
43
|
+
"""Cleanup sox on exit (called automatically via atexit)."""
|
|
44
|
+
global _initialized
|
|
45
|
+
if _initialized:
|
|
46
|
+
try:
|
|
47
|
+
sox._force_quit() # Use internal function for actual cleanup
|
|
48
|
+
except Exception:
|
|
49
|
+
pass # Ignore errors during cleanup
|
|
50
|
+
_initialized = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _expand_effects(effects: List[Effect]) -> List[Effect]:
|
|
54
|
+
"""Expand CompositeEffects into their constituent effects."""
|
|
55
|
+
expanded = []
|
|
56
|
+
for effect in effects:
|
|
57
|
+
if isinstance(effect, CompositeEffect):
|
|
58
|
+
expanded.extend(_expand_effects(effect.effects))
|
|
59
|
+
else:
|
|
60
|
+
expanded.append(effect)
|
|
61
|
+
return expanded
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def info(path: Union[str, Path]) -> Dict:
|
|
65
|
+
"""Get audio file metadata.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
path: Path to audio file.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with file information:
|
|
72
|
+
- path: Original path
|
|
73
|
+
- format: File format (e.g., 'wav', 'mp3')
|
|
74
|
+
- duration: Duration in seconds
|
|
75
|
+
- sample_rate: Sample rate in Hz
|
|
76
|
+
- channels: Number of channels
|
|
77
|
+
- bits_per_sample: Bits per sample
|
|
78
|
+
- samples: Total number of samples
|
|
79
|
+
- encoding: Encoding type
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> info = cysox.info('audio.wav')
|
|
83
|
+
>>> print(f"Duration: {info['duration']:.2f}s")
|
|
84
|
+
>>> print(f"Sample rate: {info['sample_rate']} Hz")
|
|
85
|
+
"""
|
|
86
|
+
_ensure_init()
|
|
87
|
+
|
|
88
|
+
path = str(path)
|
|
89
|
+
with sox.Format(path) as f:
|
|
90
|
+
signal = f.signal
|
|
91
|
+
encoding = f.encoding
|
|
92
|
+
|
|
93
|
+
# Calculate duration
|
|
94
|
+
if signal.length and signal.rate and signal.channels:
|
|
95
|
+
duration = signal.length / (signal.rate * signal.channels)
|
|
96
|
+
else:
|
|
97
|
+
duration = 0.0
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"path": path,
|
|
101
|
+
"format": f.filetype or "",
|
|
102
|
+
"duration": duration,
|
|
103
|
+
"sample_rate": int(signal.rate) if signal.rate else 0,
|
|
104
|
+
"channels": signal.channels or 0,
|
|
105
|
+
"bits_per_sample": encoding.bits_per_sample if encoding else 0,
|
|
106
|
+
"samples": signal.length or 0,
|
|
107
|
+
"encoding": _encoding_name(encoding.encoding) if encoding else "",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _encoding_name(encoding_type: int) -> str:
|
|
112
|
+
"""Convert encoding type constant to string."""
|
|
113
|
+
encoding_names = {
|
|
114
|
+
0: "unknown",
|
|
115
|
+
1: "signed-integer",
|
|
116
|
+
2: "unsigned-integer",
|
|
117
|
+
3: "float",
|
|
118
|
+
4: "float-text",
|
|
119
|
+
5: "flac",
|
|
120
|
+
6: "hcom",
|
|
121
|
+
7: "wavpack",
|
|
122
|
+
8: "wavpackf",
|
|
123
|
+
9: "ulaw",
|
|
124
|
+
10: "alaw",
|
|
125
|
+
11: "g721",
|
|
126
|
+
12: "g723",
|
|
127
|
+
13: "cl-adpcm",
|
|
128
|
+
14: "cl-adpcm16",
|
|
129
|
+
15: "ms-adpcm",
|
|
130
|
+
16: "ima-adpcm",
|
|
131
|
+
17: "oki-adpcm",
|
|
132
|
+
18: "dpcm",
|
|
133
|
+
19: "dwvw",
|
|
134
|
+
20: "dwvwn",
|
|
135
|
+
21: "gsm",
|
|
136
|
+
22: "mp3",
|
|
137
|
+
23: "vorbis",
|
|
138
|
+
24: "amr-wb",
|
|
139
|
+
25: "amr-nb",
|
|
140
|
+
26: "cvsd",
|
|
141
|
+
27: "lpc10",
|
|
142
|
+
28: "opus",
|
|
143
|
+
}
|
|
144
|
+
return encoding_names.get(encoding_type, "unknown")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def convert(
|
|
148
|
+
input_path: Union[str, Path],
|
|
149
|
+
output_path: Union[str, Path],
|
|
150
|
+
effects: Optional[List[Effect]] = None,
|
|
151
|
+
*,
|
|
152
|
+
sample_rate: Optional[int] = None,
|
|
153
|
+
channels: Optional[int] = None,
|
|
154
|
+
bits: Optional[int] = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Convert audio file with optional effects.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
input_path: Path to input audio file.
|
|
160
|
+
output_path: Path for output audio file. Format determined by extension.
|
|
161
|
+
effects: List of effect objects to apply (from cysox.fx).
|
|
162
|
+
sample_rate: Target sample rate in Hz (optional).
|
|
163
|
+
channels: Target number of channels (optional).
|
|
164
|
+
bits: Target bits per sample (optional).
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> # Simple conversion
|
|
168
|
+
>>> cysox.convert('input.wav', 'output.mp3')
|
|
169
|
+
>>>
|
|
170
|
+
>>> # With effects
|
|
171
|
+
>>> cysox.convert('input.wav', 'output.wav', effects=[
|
|
172
|
+
... fx.Volume(db=3),
|
|
173
|
+
... fx.Reverb(),
|
|
174
|
+
... ])
|
|
175
|
+
>>>
|
|
176
|
+
>>> # With format options
|
|
177
|
+
>>> cysox.convert('input.wav', 'output.wav',
|
|
178
|
+
... sample_rate=48000,
|
|
179
|
+
... channels=1,
|
|
180
|
+
... )
|
|
181
|
+
"""
|
|
182
|
+
_ensure_init()
|
|
183
|
+
|
|
184
|
+
input_path = str(input_path)
|
|
185
|
+
output_path = str(output_path)
|
|
186
|
+
|
|
187
|
+
# Open input
|
|
188
|
+
input_fmt = sox.Format(input_path)
|
|
189
|
+
|
|
190
|
+
# Build output signal info
|
|
191
|
+
out_signal = sox.SignalInfo(
|
|
192
|
+
rate=sample_rate or input_fmt.signal.rate,
|
|
193
|
+
channels=channels or input_fmt.signal.channels,
|
|
194
|
+
precision=bits or input_fmt.signal.precision,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Open output
|
|
198
|
+
output_fmt = sox.Format(output_path, signal=out_signal, mode='w')
|
|
199
|
+
|
|
200
|
+
# Create effects chain
|
|
201
|
+
chain = sox.EffectsChain(input_fmt.encoding, output_fmt.encoding)
|
|
202
|
+
|
|
203
|
+
# Save original input properties (before any mutation)
|
|
204
|
+
original_rate = input_fmt.signal.rate
|
|
205
|
+
|
|
206
|
+
# Track current signal - use same object pattern to allow libsox in-place updates
|
|
207
|
+
current_signal = input_fmt.signal
|
|
208
|
+
|
|
209
|
+
# Target output rate
|
|
210
|
+
target_rate = sample_rate or original_rate
|
|
211
|
+
|
|
212
|
+
# Add input effect
|
|
213
|
+
e = sox.Effect(sox.find_effect("input"))
|
|
214
|
+
e.set_options([input_fmt])
|
|
215
|
+
chain.add_effect(e, current_signal, current_signal)
|
|
216
|
+
|
|
217
|
+
# Process effects
|
|
218
|
+
if effects:
|
|
219
|
+
expanded = _expand_effects(effects)
|
|
220
|
+
|
|
221
|
+
for effect in expanded:
|
|
222
|
+
if isinstance(effect, PythonEffect):
|
|
223
|
+
raise NotImplementedError(
|
|
224
|
+
"PythonEffect not yet supported in convert(). "
|
|
225
|
+
"Use stream() for custom Python processing."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
handler = sox.find_effect(effect.name)
|
|
229
|
+
if handler is None:
|
|
230
|
+
raise ValueError(f"Unknown effect: {effect.name}")
|
|
231
|
+
|
|
232
|
+
e = sox.Effect(handler)
|
|
233
|
+
e.set_options(effect.to_args())
|
|
234
|
+
|
|
235
|
+
# Handle effects that explicitly change signal properties
|
|
236
|
+
if effect.name == "rate":
|
|
237
|
+
new_signal = sox.SignalInfo(
|
|
238
|
+
rate=float(effect.to_args()[-1]),
|
|
239
|
+
channels=current_signal.channels,
|
|
240
|
+
precision=current_signal.precision,
|
|
241
|
+
)
|
|
242
|
+
chain.add_effect(e, current_signal, new_signal)
|
|
243
|
+
current_signal = new_signal
|
|
244
|
+
elif effect.name == "channels":
|
|
245
|
+
new_signal = sox.SignalInfo(
|
|
246
|
+
rate=current_signal.rate,
|
|
247
|
+
channels=int(effect.to_args()[0]),
|
|
248
|
+
precision=current_signal.precision,
|
|
249
|
+
)
|
|
250
|
+
chain.add_effect(e, current_signal, new_signal)
|
|
251
|
+
current_signal = new_signal
|
|
252
|
+
else:
|
|
253
|
+
# For other effects, pass same signal (allows libsox in-place updates)
|
|
254
|
+
chain.add_effect(e, current_signal, current_signal)
|
|
255
|
+
|
|
256
|
+
# After add_effect, current_signal may have been mutated
|
|
257
|
+
# Check if rate changed (pitch, speed, tempo, etc.)
|
|
258
|
+
# Always create fresh signal for next effect to avoid stale state
|
|
259
|
+
if e.out_signal.rate > 0 and e.out_signal.rate != original_rate:
|
|
260
|
+
current_signal = sox.SignalInfo(
|
|
261
|
+
rate=e.out_signal.rate,
|
|
262
|
+
channels=e.out_signal.channels,
|
|
263
|
+
precision=e.out_signal.precision,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Add rate conversion if current rate differs from target
|
|
267
|
+
if current_signal.rate != target_rate:
|
|
268
|
+
new_signal = sox.SignalInfo(
|
|
269
|
+
rate=target_rate,
|
|
270
|
+
channels=current_signal.channels,
|
|
271
|
+
precision=current_signal.precision,
|
|
272
|
+
)
|
|
273
|
+
e = sox.Effect(sox.find_effect("rate"))
|
|
274
|
+
e.set_options(["-q", str(int(target_rate))]) # -q for quick to avoid FFT issues
|
|
275
|
+
chain.add_effect(e, current_signal, new_signal)
|
|
276
|
+
current_signal = new_signal
|
|
277
|
+
|
|
278
|
+
# Add channel conversion if needed
|
|
279
|
+
target_channels = channels or input_fmt.signal.channels
|
|
280
|
+
if current_signal.channels != target_channels:
|
|
281
|
+
new_signal = sox.SignalInfo(
|
|
282
|
+
rate=current_signal.rate,
|
|
283
|
+
channels=target_channels,
|
|
284
|
+
precision=current_signal.precision,
|
|
285
|
+
)
|
|
286
|
+
e = sox.Effect(sox.find_effect("channels"))
|
|
287
|
+
e.set_options([str(target_channels)])
|
|
288
|
+
chain.add_effect(e, current_signal, new_signal)
|
|
289
|
+
current_signal = new_signal
|
|
290
|
+
|
|
291
|
+
# Add output effect
|
|
292
|
+
e = sox.Effect(sox.find_effect("output"))
|
|
293
|
+
e.set_options([output_fmt])
|
|
294
|
+
chain.add_effect(e, current_signal, out_signal)
|
|
295
|
+
|
|
296
|
+
# Process
|
|
297
|
+
result = chain.flow_effects()
|
|
298
|
+
|
|
299
|
+
# Cleanup
|
|
300
|
+
input_fmt.close()
|
|
301
|
+
output_fmt.close()
|
|
302
|
+
|
|
303
|
+
if result != sox.SUCCESS:
|
|
304
|
+
raise RuntimeError(f"Effects processing failed with code {result}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def stream(
|
|
308
|
+
path: Union[str, Path],
|
|
309
|
+
chunk_size: int = 8192,
|
|
310
|
+
) -> Iterator[memoryview]:
|
|
311
|
+
"""Stream audio samples from a file.
|
|
312
|
+
|
|
313
|
+
Yields chunks of samples as memoryview objects that can be used
|
|
314
|
+
with numpy, array.array, or any buffer protocol consumer.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
path: Path to audio file.
|
|
318
|
+
chunk_size: Number of samples per chunk (default: 8192).
|
|
319
|
+
|
|
320
|
+
Yields:
|
|
321
|
+
memoryview of samples (int32 format).
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> import numpy as np
|
|
325
|
+
>>> for chunk in cysox.stream('audio.wav'):
|
|
326
|
+
... arr = np.frombuffer(chunk, dtype=np.int32)
|
|
327
|
+
... process(arr)
|
|
328
|
+
"""
|
|
329
|
+
_ensure_init()
|
|
330
|
+
|
|
331
|
+
path = str(path)
|
|
332
|
+
with sox.Format(path) as f:
|
|
333
|
+
remaining = f.signal.length
|
|
334
|
+
while remaining > 0:
|
|
335
|
+
to_read = min(chunk_size, remaining)
|
|
336
|
+
buf = f.read_buffer(to_read)
|
|
337
|
+
if len(buf) == 0:
|
|
338
|
+
break
|
|
339
|
+
remaining -= len(buf)
|
|
340
|
+
yield memoryview(buf)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def play(
|
|
344
|
+
path: Union[str, Path],
|
|
345
|
+
effects: Optional[List[Effect]] = None,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Play audio to the default audio device.
|
|
348
|
+
|
|
349
|
+
Uses libsox's audio output handlers (coreaudio on macOS,
|
|
350
|
+
alsa/pulseaudio on Linux).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
path: Path to audio file.
|
|
354
|
+
effects: Optional list of effects to apply during playback.
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
>>> cysox.play('audio.wav')
|
|
358
|
+
>>> cysox.play('audio.wav', effects=[fx.Volume(db=-6)])
|
|
359
|
+
|
|
360
|
+
Note:
|
|
361
|
+
This function blocks until playback is complete.
|
|
362
|
+
"""
|
|
363
|
+
_ensure_init()
|
|
364
|
+
|
|
365
|
+
import platform
|
|
366
|
+
|
|
367
|
+
path = str(path)
|
|
368
|
+
|
|
369
|
+
# Determine audio output type based on platform
|
|
370
|
+
system = platform.system()
|
|
371
|
+
if system == "Darwin":
|
|
372
|
+
output_type = "coreaudio"
|
|
373
|
+
elif system == "Linux":
|
|
374
|
+
# Try pulseaudio first, fall back to alsa
|
|
375
|
+
output_type = "pulseaudio"
|
|
376
|
+
else:
|
|
377
|
+
raise NotImplementedError(f"Playback not supported on {system}")
|
|
378
|
+
|
|
379
|
+
# Open input
|
|
380
|
+
input_fmt = sox.Format(path)
|
|
381
|
+
|
|
382
|
+
# Open audio output
|
|
383
|
+
try:
|
|
384
|
+
output_fmt = sox.Format(
|
|
385
|
+
"default",
|
|
386
|
+
signal=input_fmt.signal,
|
|
387
|
+
filetype=output_type,
|
|
388
|
+
mode='w'
|
|
389
|
+
)
|
|
390
|
+
except Exception:
|
|
391
|
+
# Try alsa as fallback on Linux
|
|
392
|
+
if system == "Linux":
|
|
393
|
+
output_type = "alsa"
|
|
394
|
+
output_fmt = sox.Format(
|
|
395
|
+
"default",
|
|
396
|
+
signal=input_fmt.signal,
|
|
397
|
+
filetype=output_type,
|
|
398
|
+
mode='w'
|
|
399
|
+
)
|
|
400
|
+
else:
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
# Create effects chain
|
|
404
|
+
chain = sox.EffectsChain(input_fmt.encoding, output_fmt.encoding)
|
|
405
|
+
current_signal = input_fmt.signal
|
|
406
|
+
|
|
407
|
+
# Add input effect
|
|
408
|
+
e = sox.Effect(sox.find_effect("input"))
|
|
409
|
+
e.set_options([input_fmt])
|
|
410
|
+
chain.add_effect(e, current_signal, current_signal)
|
|
411
|
+
|
|
412
|
+
# Add user effects
|
|
413
|
+
if effects:
|
|
414
|
+
expanded = _expand_effects(effects)
|
|
415
|
+
for effect in expanded:
|
|
416
|
+
if isinstance(effect, PythonEffect):
|
|
417
|
+
raise NotImplementedError(
|
|
418
|
+
"PythonEffect not supported in play()"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
handler = sox.find_effect(effect.name)
|
|
422
|
+
if handler is None:
|
|
423
|
+
raise ValueError(f"Unknown effect: {effect.name}")
|
|
424
|
+
|
|
425
|
+
e = sox.Effect(handler)
|
|
426
|
+
e.set_options(effect.to_args())
|
|
427
|
+
chain.add_effect(e, current_signal, current_signal)
|
|
428
|
+
|
|
429
|
+
# Add output effect
|
|
430
|
+
e = sox.Effect(sox.find_effect("output"))
|
|
431
|
+
e.set_options([output_fmt])
|
|
432
|
+
chain.add_effect(e, current_signal, current_signal)
|
|
433
|
+
|
|
434
|
+
# Play (blocks until complete)
|
|
435
|
+
result = chain.flow_effects()
|
|
436
|
+
|
|
437
|
+
# Cleanup
|
|
438
|
+
input_fmt.close()
|
|
439
|
+
output_fmt.close()
|
|
440
|
+
|
|
441
|
+
if result != sox.SUCCESS:
|
|
442
|
+
raise RuntimeError(f"Playback failed with code {result}")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def concat(
|
|
446
|
+
inputs: List[Union[str, Path]],
|
|
447
|
+
output_path: Union[str, Path],
|
|
448
|
+
*,
|
|
449
|
+
chunk_size: int = 8192,
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Concatenate multiple audio files into one.
|
|
452
|
+
|
|
453
|
+
All input files must have the same sample rate and number of channels.
|
|
454
|
+
The output format is determined by the output file extension.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
inputs: List of paths to input audio files (minimum 2).
|
|
458
|
+
output_path: Path for the concatenated output file.
|
|
459
|
+
chunk_size: Number of samples to read/write at a time (default: 8192).
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: If fewer than 2 input files provided.
|
|
463
|
+
ValueError: If input files have mismatched sample rates or channels.
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> cysox.concat(['intro.wav', 'main.wav', 'outro.wav'], 'full.wav')
|
|
467
|
+
"""
|
|
468
|
+
_ensure_init()
|
|
469
|
+
|
|
470
|
+
if len(inputs) < 2:
|
|
471
|
+
raise ValueError("concat() requires at least 2 input files")
|
|
472
|
+
|
|
473
|
+
inputs = [str(p) for p in inputs]
|
|
474
|
+
output_path = str(output_path)
|
|
475
|
+
|
|
476
|
+
output_fmt = None
|
|
477
|
+
reference_rate = None
|
|
478
|
+
reference_channels = None
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
for i, input_path in enumerate(inputs):
|
|
482
|
+
input_fmt = sox.Format(input_path)
|
|
483
|
+
|
|
484
|
+
if i == 0:
|
|
485
|
+
# First file: capture reference signal and open output
|
|
486
|
+
reference_rate = input_fmt.signal.rate
|
|
487
|
+
reference_channels = input_fmt.signal.channels
|
|
488
|
+
|
|
489
|
+
out_signal = sox.SignalInfo(
|
|
490
|
+
rate=reference_rate,
|
|
491
|
+
channels=reference_channels,
|
|
492
|
+
precision=input_fmt.signal.precision,
|
|
493
|
+
)
|
|
494
|
+
out_encoding = sox.EncodingInfo(
|
|
495
|
+
encoding=int(input_fmt.encoding.encoding),
|
|
496
|
+
bits_per_sample=input_fmt.encoding.bits_per_sample,
|
|
497
|
+
)
|
|
498
|
+
output_fmt = sox.Format(
|
|
499
|
+
output_path, signal=out_signal, encoding=out_encoding, mode='w'
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
# Subsequent files: verify compatibility
|
|
503
|
+
if input_fmt.signal.rate != reference_rate:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"Sample rate mismatch: {input_path} has {input_fmt.signal.rate}Hz, "
|
|
506
|
+
f"expected {reference_rate}Hz"
|
|
507
|
+
)
|
|
508
|
+
if input_fmt.signal.channels != reference_channels:
|
|
509
|
+
raise ValueError(
|
|
510
|
+
f"Channel count mismatch: {input_path} has {input_fmt.signal.channels} channels, "
|
|
511
|
+
f"expected {reference_channels}"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Copy all samples from this input to output
|
|
515
|
+
while True:
|
|
516
|
+
samples = input_fmt.read(chunk_size)
|
|
517
|
+
if len(samples) == 0:
|
|
518
|
+
break
|
|
519
|
+
output_fmt.write(samples)
|
|
520
|
+
|
|
521
|
+
input_fmt.close()
|
|
522
|
+
|
|
523
|
+
output_fmt.close()
|
|
524
|
+
|
|
525
|
+
except Exception:
|
|
526
|
+
if output_fmt is not None:
|
|
527
|
+
output_fmt.close()
|
|
528
|
+
raise
|
cysox/fx/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Typed audio effect classes for cysox.
|
|
2
|
+
|
|
3
|
+
This module provides typed effect classes that can be used with
|
|
4
|
+
cysox.convert() and other high-level functions.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> import cysox
|
|
8
|
+
>>> from cysox import fx
|
|
9
|
+
>>>
|
|
10
|
+
>>> cysox.convert('input.wav', 'output.mp3', effects=[
|
|
11
|
+
... fx.Volume(db=3),
|
|
12
|
+
... fx.Bass(gain=5),
|
|
13
|
+
... fx.Reverb(),
|
|
14
|
+
... ])
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Base classes
|
|
18
|
+
from .base import (
|
|
19
|
+
Effect,
|
|
20
|
+
CompositeEffect,
|
|
21
|
+
PythonEffect,
|
|
22
|
+
CEffect,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Volume and gain effects
|
|
26
|
+
from .volume import (
|
|
27
|
+
Volume,
|
|
28
|
+
Gain,
|
|
29
|
+
Normalize,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Equalization effects
|
|
33
|
+
from .eq import (
|
|
34
|
+
Bass,
|
|
35
|
+
Treble,
|
|
36
|
+
Equalizer,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Filter effects
|
|
40
|
+
from .filter import (
|
|
41
|
+
HighPass,
|
|
42
|
+
LowPass,
|
|
43
|
+
BandPass,
|
|
44
|
+
BandReject,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Reverb and spatial effects
|
|
48
|
+
from .reverb import (
|
|
49
|
+
Reverb,
|
|
50
|
+
Echo,
|
|
51
|
+
Chorus,
|
|
52
|
+
Flanger,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Time-based effects
|
|
56
|
+
from .time import (
|
|
57
|
+
Trim,
|
|
58
|
+
Pad,
|
|
59
|
+
Speed,
|
|
60
|
+
Tempo,
|
|
61
|
+
Pitch,
|
|
62
|
+
Reverse,
|
|
63
|
+
Fade,
|
|
64
|
+
Repeat,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Conversion effects
|
|
68
|
+
from .convert import (
|
|
69
|
+
Rate,
|
|
70
|
+
Channels,
|
|
71
|
+
Remix,
|
|
72
|
+
Dither,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
# Base
|
|
77
|
+
"Effect",
|
|
78
|
+
"CompositeEffect",
|
|
79
|
+
"PythonEffect",
|
|
80
|
+
"CEffect",
|
|
81
|
+
# Volume
|
|
82
|
+
"Volume",
|
|
83
|
+
"Gain",
|
|
84
|
+
"Normalize",
|
|
85
|
+
# EQ
|
|
86
|
+
"Bass",
|
|
87
|
+
"Treble",
|
|
88
|
+
"Equalizer",
|
|
89
|
+
# Filter
|
|
90
|
+
"HighPass",
|
|
91
|
+
"LowPass",
|
|
92
|
+
"BandPass",
|
|
93
|
+
"BandReject",
|
|
94
|
+
# Reverb/Spatial
|
|
95
|
+
"Reverb",
|
|
96
|
+
"Echo",
|
|
97
|
+
"Chorus",
|
|
98
|
+
"Flanger",
|
|
99
|
+
# Time
|
|
100
|
+
"Trim",
|
|
101
|
+
"Pad",
|
|
102
|
+
"Speed",
|
|
103
|
+
"Tempo",
|
|
104
|
+
"Pitch",
|
|
105
|
+
"Reverse",
|
|
106
|
+
"Fade",
|
|
107
|
+
"Repeat",
|
|
108
|
+
# Convert
|
|
109
|
+
"Rate",
|
|
110
|
+
"Channels",
|
|
111
|
+
"Remix",
|
|
112
|
+
"Dither",
|
|
113
|
+
]
|