|
|
|
|
|
|
|
|
""" |
|
|
Ultimate Audio Ensemble Processor v4.0 |
|
|
- Tüm ensemble yöntemlerini destekler (avg_wave, median_wave, max_wave, min_wave, max_fft, min_fft, median_fft) |
|
|
- Özel karakterli ve uzun dosya yollarını destekler |
|
|
- Büyük dosyaları verimli şekilde işler |
|
|
- Detaylı hata yönetimi ve loglama |
|
|
""" |
|
|
|
|
|
import os |
|
|
import sys |
|
|
import argparse |
|
|
import numpy as np |
|
|
import soundfile as sf |
|
|
import librosa |
|
|
import psutil |
|
|
import gc |
|
|
import traceback |
|
|
from scipy.signal import stft, istft |
|
|
from pathlib import Path |
|
|
import tempfile |
|
|
import shutil |
|
|
import json |
|
|
from tqdm import tqdm |
|
|
import time |
|
|
|
|
|
class AudioEnsembleEngine: |
|
|
def __init__(self): |
|
|
self.temp_dir = None |
|
|
self.log_file = "ensemble_processor.log" |
|
|
|
|
|
def __enter__(self): |
|
|
self.temp_dir = tempfile.mkdtemp(prefix='audio_ensemble_') |
|
|
self.setup_logging() |
|
|
return self |
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb): |
|
|
if self.temp_dir and os.path.exists(self.temp_dir): |
|
|
shutil.rmtree(self.temp_dir, ignore_errors=True) |
|
|
|
|
|
def setup_logging(self): |
|
|
"""Initialize detailed logging system.""" |
|
|
with open(self.log_file, 'w') as f: |
|
|
f.write("Audio Ensemble Processor Log\n") |
|
|
f.write("="*50 + "\n") |
|
|
f.write(f"System Memory: {psutil.virtual_memory().total/(1024**3):.2f} GB\n") |
|
|
f.write(f"Python Version: {sys.version}\n\n") |
|
|
|
|
|
def log_message(self, message): |
|
|
"""Log messages with timestamp.""" |
|
|
with open(self.log_file, 'a') as f: |
|
|
f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}\n") |
|
|
|
|
|
def normalize_path(self, path): |
|
|
"""Handle all path-related issues comprehensively.""" |
|
|
try: |
|
|
|
|
|
path = str(Path(path).absolute().resolve()) |
|
|
|
|
|
|
|
|
if any(char in path for char in '[]()|&; '): |
|
|
base, ext = os.path.splitext(path) |
|
|
safe_name = f"{hash(base)}{ext}" |
|
|
temp_path = os.path.join(self.temp_dir, safe_name) |
|
|
|
|
|
if not os.path.exists(temp_path): |
|
|
data, sr = librosa.load(path, sr=None, mono=False) |
|
|
sf.write(temp_path, data.T, sr) |
|
|
|
|
|
return temp_path |
|
|
|
|
|
return path |
|
|
except Exception as e: |
|
|
self.log_message(f"Path normalization failed: {str(e)}") |
|
|
return path |
|
|
|
|
|
def validate_inputs(self, files, method, output_path): |
|
|
"""Comprehensive input validation with detailed error reporting.""" |
|
|
errors = [] |
|
|
valid_methods = [ |
|
|
'avg_wave', 'median_wave', 'max_wave', 'min_wave', |
|
|
'max_fft', 'min_fft', 'median_fft' |
|
|
] |
|
|
|
|
|
|
|
|
if method not in valid_methods: |
|
|
errors.append(f"Invalid method '{method}'. Available: {valid_methods}") |
|
|
|
|
|
|
|
|
valid_files = [] |
|
|
sample_rates = set() |
|
|
durations = [] |
|
|
channels_set = set() |
|
|
|
|
|
for f in files: |
|
|
try: |
|
|
f_normalized = self.normalize_path(f) |
|
|
|
|
|
|
|
|
if not os.path.exists(f_normalized): |
|
|
errors.append(f"File not found: {f_normalized}") |
|
|
continue |
|
|
|
|
|
if os.path.getsize(f_normalized) == 0: |
|
|
errors.append(f"Empty file: {f_normalized}") |
|
|
continue |
|
|
|
|
|
|
|
|
try: |
|
|
with sf.SoundFile(f_normalized) as sf_file: |
|
|
sr = sf_file.samplerate |
|
|
frames = sf_file.frames |
|
|
channels = sf_file.channels |
|
|
except Exception as e: |
|
|
errors.append(f"Invalid audio file {f_normalized}: {str(e)}") |
|
|
continue |
|
|
|
|
|
|
|
|
if channels != 2: |
|
|
errors.append(f"File must be stereo (has {channels} channels): {f_normalized}") |
|
|
continue |
|
|
|
|
|
sample_rates.add(sr) |
|
|
durations.append(frames / sr) |
|
|
channels_set.add(channels) |
|
|
valid_files.append(f_normalized) |
|
|
|
|
|
except Exception as e: |
|
|
errors.append(f"Error processing {f}: {str(e)}") |
|
|
continue |
|
|
|
|
|
|
|
|
if len(valid_files) < 2: |
|
|
errors.append("At least 2 valid files required") |
|
|
|
|
|
if len(sample_rates) > 1: |
|
|
errors.append(f"Sample rate mismatch: {sample_rates}") |
|
|
|
|
|
if len(channels_set) > 1: |
|
|
errors.append(f"Channel count mismatch: {channels_set}") |
|
|
|
|
|
|
|
|
try: |
|
|
output_path = self.normalize_path(output_path) |
|
|
output_dir = os.path.dirname(output_path) or '.' |
|
|
|
|
|
if not os.path.exists(output_dir): |
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
|
|
if not os.access(output_dir, os.W_OK): |
|
|
errors.append(f"No write permission for output directory: {output_dir}") |
|
|
except Exception as e: |
|
|
errors.append(f"Output path error: {str(e)}") |
|
|
|
|
|
if errors: |
|
|
error_msg = "\n".join(errors) |
|
|
self.log_message(f"Validation failed:\n{error_msg}") |
|
|
raise ValueError(error_msg) |
|
|
|
|
|
target_sr = sample_rates.pop() if sample_rates else 44100 |
|
|
return valid_files, target_sr, min(durations) if durations else None |
|
|
|
|
|
def process_waveform(self, chunks, method, weights=None): |
|
|
"""All waveform domain processing methods.""" |
|
|
if method == 'avg_wave': |
|
|
if weights is not None: |
|
|
return np.average(chunks, axis=0, weights=weights) |
|
|
return np.mean(chunks, axis=0) |
|
|
elif method == 'median_wave': |
|
|
return np.median(chunks, axis=0) |
|
|
elif method == 'max_wave': |
|
|
return np.max(chunks, axis=0) |
|
|
elif method == 'min_wave': |
|
|
return np.min(chunks, axis=0) |
|
|
|
|
|
def process_spectral(self, chunks, method): |
|
|
"""All frequency domain processing methods.""" |
|
|
specs = [] |
|
|
min_samples = min(chunk.shape[1] for chunk in chunks) |
|
|
nperseg = min(1024, min_samples) |
|
|
noverlap = nperseg // 2 |
|
|
self.log_message(f"STFT parameters: nperseg={nperseg}, noverlap={noverlap}, min_samples={min_samples}") |
|
|
|
|
|
for c in chunks: |
|
|
|
|
|
c = c[:, :min_samples] |
|
|
channel_specs = [] |
|
|
for channel in range(c.shape[0]): |
|
|
if c.shape[1] < 256: |
|
|
self.log_message(f"Warning: Chunk too short ({c.shape[1]} samples) for STFT. Skipping.") |
|
|
return None |
|
|
try: |
|
|
freqs, times, Zxx = stft( |
|
|
c[channel], |
|
|
nperseg=nperseg, |
|
|
noverlap=noverlap, |
|
|
window='hann' |
|
|
) |
|
|
channel_specs.append(Zxx) |
|
|
except Exception as e: |
|
|
self.log_message(f"STFT failed for channel: {str(e)}") |
|
|
return None |
|
|
specs.append(np.array(channel_specs)) |
|
|
|
|
|
if not specs: |
|
|
self.log_message("No valid STFTs computed.") |
|
|
return None |
|
|
|
|
|
specs = np.array(specs) |
|
|
self.log_message(f"STFT shapes: {[spec.shape for spec in specs]}") |
|
|
|
|
|
|
|
|
min_freqs = min(spec.shape[1] for spec in specs) |
|
|
min_times = min(spec.shape[2] for spec in specs) |
|
|
specs = np.array([spec[:, :min_freqs, :min_times] for spec in specs]) |
|
|
|
|
|
mag = np.abs(specs) |
|
|
|
|
|
if method == 'max_fft': |
|
|
combined_mag = np.max(mag, axis=0) |
|
|
elif method == 'min_fft': |
|
|
combined_mag = np.min(mag, axis=0) |
|
|
elif method == 'median_fft': |
|
|
combined_mag = np.median(mag, axis=0) |
|
|
|
|
|
|
|
|
combined_spec = combined_mag * np.exp(1j * np.angle(specs[0])) |
|
|
|
|
|
|
|
|
reconstructed = np.zeros((combined_spec.shape[0], chunks[0].shape[1])) |
|
|
for channel in range(combined_spec.shape[0]): |
|
|
try: |
|
|
_, xrec = istft( |
|
|
combined_spec[channel], |
|
|
nperseg=nperseg, |
|
|
noverlap=noverlap, |
|
|
window='hann' |
|
|
) |
|
|
|
|
|
if xrec.shape[0] < chunks[0].shape[1]: |
|
|
xrec = np.pad(xrec, (0, chunks[0].shape[1] - xrec.shape[0]), mode='constant') |
|
|
reconstructed[channel] = xrec[:chunks[0].shape[1]] |
|
|
except Exception as e: |
|
|
self.log_message(f"ISTFT failed for channel: {str(e)}") |
|
|
return None |
|
|
|
|
|
return reconstructed |
|
|
|
|
|
def run_ensemble(self, files, method, output_path, weights=None, buffer_size=32768): |
|
|
"""Core ensemble processing with maximum robustness.""" |
|
|
try: |
|
|
|
|
|
valid_files, target_sr, duration = self.validate_inputs(files, method, output_path) |
|
|
output_path = self.normalize_path(output_path) |
|
|
|
|
|
self.log_message(f"Starting ensemble with method: {method}") |
|
|
self.log_message(f"Input files: {json.dumps(valid_files, indent=2)}") |
|
|
self.log_message(f"Target sample rate: {target_sr}Hz") |
|
|
self.log_message(f"Output path: {output_path}") |
|
|
|
|
|
|
|
|
output_dir = os.path.dirname(output_path) or '.' |
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
self.log_message(f"Output directory created/verified: {output_dir}") |
|
|
|
|
|
|
|
|
try: |
|
|
test_file = os.path.join(output_dir, "test_write.txt") |
|
|
with open(test_file, "w") as f: |
|
|
f.write("Test") |
|
|
os.remove(test_file) |
|
|
self.log_message(f"Write permissions verified for: {output_dir}") |
|
|
except Exception as e: |
|
|
self.log_message(f"Write permission error for {output_dir}: {str(e)}") |
|
|
raise ValueError(f"Cannot write to output directory {output_dir}: {str(e)}") |
|
|
|
|
|
|
|
|
if weights and len(weights) == len(valid_files): |
|
|
weights = np.array(weights, dtype=np.float32) |
|
|
weights /= weights.sum() |
|
|
self.log_message(f"Using weights: {weights}") |
|
|
else: |
|
|
weights = None |
|
|
|
|
|
|
|
|
readers = [] |
|
|
try: |
|
|
readers = [sf.SoundFile(f) for f in valid_files] |
|
|
shortest_frames = min(int(duration * r.samplerate) for r in readers) |
|
|
|
|
|
|
|
|
self.log_message(f"Opening output file for writing: {output_path}") |
|
|
with sf.SoundFile(output_path, 'w', target_sr, 2, 'PCM_24') as outfile: |
|
|
|
|
|
progress = tqdm(total=shortest_frames, unit='samples', desc='Processing') |
|
|
|
|
|
for pos in range(0, shortest_frames, buffer_size): |
|
|
chunk_size = min(buffer_size, shortest_frames - pos) |
|
|
|
|
|
|
|
|
chunks = [] |
|
|
for r in readers: |
|
|
r.seek(pos) |
|
|
data = r.read(chunk_size) |
|
|
if data.size == 0: |
|
|
data = np.zeros((chunk_size, 2)) |
|
|
chunks.append(data.T) |
|
|
|
|
|
chunks = np.array(chunks) |
|
|
self.log_message(f"Chunk shape: {chunks.shape}, pos={pos}") |
|
|
|
|
|
|
|
|
if method.endswith('_fft'): |
|
|
result = self.process_spectral(chunks, method) |
|
|
if result is None: |
|
|
self.log_message("Spectral processing failed, falling back to avg_wave") |
|
|
result = self.process_waveform(chunks, 'avg_wave', weights) |
|
|
else: |
|
|
result = self.process_waveform(chunks, method, weights) |
|
|
|
|
|
|
|
|
outfile.write(result.T) |
|
|
|
|
|
|
|
|
del chunks, result |
|
|
if pos % (5 * buffer_size) == 0: |
|
|
gc.collect() |
|
|
|
|
|
progress.update(chunk_size) |
|
|
|
|
|
progress.close() |
|
|
|
|
|
self.log_message(f"Successfully created output: {output_path}") |
|
|
print(f"\nEnsemble completed successfully: {output_path}") |
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
self.log_message(f"Processing error: {str(e)}\n{traceback.format_exc()}") |
|
|
raise |
|
|
finally: |
|
|
for r in readers: |
|
|
try: |
|
|
r.close() |
|
|
except: |
|
|
pass |
|
|
|
|
|
except Exception as e: |
|
|
self.log_message(f"Fatal error: {str(e)}\n{traceback.format_exc()}") |
|
|
print(f"\nError during processing: {str(e)}", file=sys.stderr) |
|
|
return False |
|
|
|
|
|
def main(): |
|
|
parser = argparse.ArgumentParser( |
|
|
description='Ultimate Audio Ensemble Processor - Supports all ensemble methods', |
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter |
|
|
) |
|
|
parser.add_argument('--files', nargs='+', required=True, |
|
|
help='Input audio files (supports special characters)') |
|
|
parser.add_argument('--type', required=True, |
|
|
choices=['avg_wave', 'median_wave', 'max_wave', 'min_wave', |
|
|
'max_fft', 'min_fft', 'median_fft'], |
|
|
help='Ensemble method to use') |
|
|
parser.add_argument('--weights', nargs='+', type=float, |
|
|
help='Relative weights for each input file') |
|
|
parser.add_argument('--output', required=True, |
|
|
help='Output file path') |
|
|
parser.add_argument('--buffer', type=int, default=32768, |
|
|
help='Buffer size in samples (larger=faster but uses more memory)') |
|
|
|
|
|
args = parser.parse_args() |
|
|
|
|
|
with AudioEnsembleEngine() as engine: |
|
|
success = engine.run_ensemble( |
|
|
files=args.files, |
|
|
method=args.type, |
|
|
output_path=args.output, |
|
|
weights=args.weights, |
|
|
buffer_size=args.buffer |
|
|
) |
|
|
|
|
|
sys.exit(0 if success else 1) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
import time |
|
|
main() |
|
|
|