import os import tempfile import numpy as np import soundfile as sf from pydub import AudioSegment from app.core.dsp_utils import find_nearest_zero_crossing_file, apply_micro_fade def edit_audio_file(config: dict, input_path: str, output_path: str): """ Applies editing commands on the audio file based on config: - cut_start_ms, cut_end_ms (with optional zero_crossing_align) - loop_count - fade_in_ms, fade_out_ms - volume_change_db """ # Load original audio audio = AudioSegment.from_file(input_path) # Calculate start and end positions in ms cut_start_ms = config.get("cut_start_ms") cut_end_ms = config.get("cut_end_ms") total_len = len(audio) start_ms = float(cut_start_ms) if cut_start_ms is not None else 0.0 end_ms = float(cut_end_ms) if cut_end_ms is not None else float(total_len) # Bound start and end start_ms = max(0.0, min(start_ms, float(total_len))) end_ms = max(start_ms, min(end_ms, float(total_len))) # Align to Zero-crossing if requested if config.get("zero_crossing_align", True): # Align start if cut_start_ms is not None: start_sec = start_ms / 1000.0 aligned_start_sec = find_nearest_zero_crossing_file(input_path, start_sec) start_ms = aligned_start_sec * 1000.0 # Align end if cut_end_ms is not None: end_sec = end_ms / 1000.0 aligned_end_sec = find_nearest_zero_crossing_file(input_path, end_sec) end_ms = aligned_end_sec * 1000.0 # Crop the segment segment = audio[start_ms:end_ms] # Apply micro-fade (50ms) to ensure smooth boundaries segment = apply_micro_fade(segment, fade_duration_ms=50) # Loop the segment loop_count = int(config.get("loop_count", 1)) if loop_count > 1: segment = segment * loop_count # Apply volume change volume_change_db = float(config.get("volume_change_db", 0.0)) if volume_change_db != 0.0: segment = segment + volume_change_db # Apply custom fades fade_in_ms = float(config.get("fade_in_ms", 0.0)) if fade_in_ms > 0: segment = segment.fade_in(int(fade_in_ms)) # Apply fade out fade_out_ms = float(config.get("fade_out_ms", 0.0)) if fade_out_ms > 0: segment = segment.fade_out(int(fade_out_ms)) # Ensure parent output directory exists os.makedirs(os.path.dirname(output_path), exist_ok=True) # Export as WAV segment.export(output_path, format="wav") return { "success": True, "output_path": output_path, "duration_ms": len(segment), "aligned_start_ms": round(start_ms, 2), "aligned_end_ms": round(end_ms, 2) } def cut_and_loop_segment( file_path: str, start_sec: float, end_sec: float, loop_count: int = 1, fade_in_ms: int = 50, fade_out_ms: int = 50, volume_db_change: float = 0.0 ) -> AudioSegment: """ Cắt đoạn âm thanh, áp dụng gain, fade-in/out và lặp lại đoạn đó. pydub sử dụng mili-giây (ms) làm đơn vị chuẩn. Args: file_path: Đường dẫn tệp âm thanh start_sec: Thời điểm bắt đầu (giây) end_sec: Thời điểm kết thúc (giây) loop_count: Số lần lặp fade_in_ms: Thời gian fade-in (ms) fade_out_ms: Thời gian fade-out (ms) volume_db_change: Thay đổi âm lượng (dB) Returns: AudioSegment: Đoạn âm thanh đã xử lý """ # Tải tệp tin âm thanh gốc sound = AudioSegment.from_file(file_path) # Chuyển đổi giây sang mili-giây start_ms = int(start_sec * 1000) end_ms = int(end_sec * 1000) # Trích xuất phân đoạn (Slicing) clip = sound[start_ms:end_ms] # Thay đổi âm lượng nếu có yêu cầu if volume_db_change != 0.0: clip = clip + volume_db_change # Áp dụng hiệu ứng mờ đầu và mờ cuối (Fading) if fade_in_ms > 0: clip = clip.fade_in(fade_in_ms) if fade_out_ms > 0: clip = clip.fade_out(fade_out_ms) # Tạo chuỗi lặp (Looping) looped_clip = clip * loop_count return looped_clip def mix_multitrack_session(tracks_meta: list, output_path: str, sample_rate: int = 44100, bit_depth: int = 16): """ Hòa âm đa kênh thống nhất (Multitrack Mixdown). Args: tracks_meta: Danh sách cấu hình của từng track: [{"file_path": "...", "volume": 0.8, "muted": False}, ...] output_path: Đường dẫn file xuất sample_rate: Tần số lấy mẫu (Hz) bit_depth: Độ sâu bit (8, 16, hoặc 24) Returns: dict: Thông tin kết quả """ master_mix = None for track in tracks_meta: if track.get("muted", False): continue # Đọc tệp âm thanh của track sound = AudioSegment.from_file(track["file_path"]) # Áp dụng Gain (chuyển đổi từ tỷ lệ 0.0 -> 1.0 sang decibels dB) gain_raw = track.get("volume", 1.0) if gain_raw <= 0.001: continue # Xem như tắt tiếng hoàn toàn gain_db = 20 * np.log10(gain_raw) sound = sound + gain_db # Gộp vào Master Mix if master_mix is None: master_mix = sound else: # overlay tự động đồng bộ thời điểm bắt đầu tại mốc 0ms master_mix = master_mix.overlay(sound, position=0) if master_mix is not None: # Đảm bảo thư mục output tồn tại out_dir = os.path.dirname(output_path) if out_dir: os.makedirs(out_dir, exist_ok=True) # Xác định format từ đuôi file output_ext = os.path.splitext(output_path)[1].lower().lstrip(".") if output_ext in ("mp3", "ogg"): # Xuất trực tiếp qua pydub/FFmpeg cho MP3, OGG master_mix.export(output_path, format=output_ext) else: # Cho WAV: sử dụng soundfile để kiểm soát bit-depth chính xác # Dùng tempfile an toàn thay vì đường dẫn tương đối with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: temp_wav = tmp.name try: master_mix.export(temp_wav, format="wav") y, sr_read = sf.read(temp_wav) # Xác định subtype mã hóa bit-depth subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"} selected_subtype = subtype_map.get(bit_depth, "PCM_16") # Ghi tệp WAV chất lượng cao sf.write(output_path, y, sample_rate, subtype=selected_subtype) finally: # Dọn dẹp tệp tạm (luôn thực hiện) if os.path.exists(temp_wav): os.remove(temp_wav) return { "success": True, "output_path": output_path, "duration_ms": len(master_mix), "format": output_ext if output_ext else "wav", "tracks_processed": len([t for t in tracks_meta if not t.get("muted", False)]) } else: return { "success": False, "error": "No active tracks to mix" } def export_audio(input_path: str, output_path: str, format: str = "wav", sample_rate: int = 44100, bit_depth: int = 16): """ Xuất tệp âm thanh sang nhiều định dạng (WAV/MP3/OGG). Args: input_path: Đường dẫn file nguồn output_path: Đường dẫn file đích format: Định dạng xuất (wav, mp3, ogg) sample_rate: Tần số lấy mẫu bit_depth: Độ sâu bit (chỉ cho WAV) Returns: dict: Thông tin kết quả """ # Guard: prevent overwriting source file if os.path.abspath(input_path) == os.path.abspath(output_path): raise ValueError("output_path must not be the same as input_path") sound = AudioSegment.from_file(input_path) out_dir = os.path.dirname(output_path) if out_dir: os.makedirs(out_dir, exist_ok=True) if format in ("mp3", "ogg"): # Xuất qua pydub/FFmpeg sound.export(output_path, format=format) else: # WAV: dùng soundfile cho bit-depth chính xác with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: temp_wav = tmp.name try: sound.export(temp_wav, format="wav") y, sr_read = sf.read(temp_wav) subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"} selected_subtype = subtype_map.get(bit_depth, "PCM_16") sf.write(output_path, y, sample_rate, subtype=selected_subtype) finally: if os.path.exists(temp_wav): os.remove(temp_wav) return { "success": True, "output_path": output_path, "format": format, "duration_ms": len(sound) }