Spaces:
Running
on
Zero
Running
on
Zero
| """ | |
| Audio format converter: m4a ↔ wav conversion, sample rate normalization. | |
| Converts between m4a (compressed) and wav (lossless) formats. | |
| Normalizes to 48kHz/24-bit for processing, outputs as m4a/192kbps for final. | |
| """ | |
| import logging | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| class FormatConversionError(Exception): | |
| """Custom exception for format conversion errors.""" | |
| pass | |
| def m4a_to_wav( | |
| input_path: str, | |
| output_path: Optional[str] = None, | |
| target_sr: int = 48000, | |
| target_bit_depth: int = 24, | |
| ) -> str: | |
| """ | |
| Convert m4a to wav format with normalization. | |
| Args: | |
| input_path: Path to input m4a file | |
| output_path: Path to output wav file (temp file if None) | |
| target_sr: Target sample rate in Hz (default: 48000) | |
| target_bit_depth: Target bit depth (default: 24) | |
| Returns: | |
| Path to output wav file | |
| Raises: | |
| FormatConversionError: If conversion fails | |
| """ | |
| try: | |
| from pydub import AudioSegment | |
| input_path = Path(input_path) | |
| if not input_path.exists(): | |
| raise FormatConversionError(f"Input file not found: {input_path}") | |
| # Create output path if not provided | |
| if output_path is None: | |
| temp_dir = tempfile.gettempdir() | |
| output_path = Path(temp_dir) / f"{input_path.stem}_temp.wav" | |
| else: | |
| output_path = Path(output_path) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| # Load m4a | |
| audio = AudioSegment.from_file(str(input_path), format="m4a") | |
| # Normalize to target format | |
| audio = audio.set_frame_rate(target_sr) | |
| audio = audio.set_channels(1) # Mono | |
| audio = audio.set_sample_width(target_bit_depth // 8) # Bytes (24-bit = 3 bytes) | |
| # Export as wav | |
| audio.export(str(output_path), format="wav") | |
| logger.debug(f"Converted m4a to wav: {input_path.name} -> {output_path.name}") | |
| return str(output_path) | |
| except Exception as e: | |
| if isinstance(e, FormatConversionError): | |
| raise | |
| raise FormatConversionError(f"Failed to convert m4a to wav: {str(e)}") | |
| def wav_to_m4a( | |
| input_path: str, output_path: str, bitrate: str = "192k", sample_rate: int = 48000 | |
| ) -> str: | |
| """ | |
| Convert wav to m4a format. | |
| Args: | |
| input_path: Path to input wav file | |
| output_path: Path to output m4a file | |
| bitrate: AAC bitrate (default: "192k") | |
| sample_rate: Sample rate in Hz (default: 48000) | |
| Returns: | |
| Path to output m4a file | |
| Raises: | |
| FormatConversionError: If conversion fails | |
| """ | |
| try: | |
| from pydub import AudioSegment | |
| input_path = Path(input_path) | |
| output_path = Path(output_path) | |
| if not input_path.exists(): | |
| raise FormatConversionError(f"Input file not found: {input_path}") | |
| # Create output directory if needed | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| # Load wav | |
| audio = AudioSegment.from_file(str(input_path), format="wav") | |
| # Normalize sample rate | |
| audio = audio.set_frame_rate(sample_rate) | |
| # Export as m4a with AAC codec | |
| audio.export( | |
| str(output_path), | |
| format="mp4", # m4a uses mp4 container | |
| codec="aac", | |
| bitrate=bitrate, | |
| parameters=["-profile:a", "aac_low"], | |
| ) | |
| logger.debug(f"Converted wav to m4a: {input_path.name} -> {output_path.name}") | |
| return str(output_path) | |
| except Exception as e: | |
| if isinstance(e, FormatConversionError): | |
| raise | |
| raise FormatConversionError(f"Failed to convert wav to m4a: {str(e)}") | |
| def normalize_to_intermediate(input_path: str, output_path: Optional[str] = None) -> str: | |
| """ | |
| Normalize any audio format to intermediate wav format (48kHz/24-bit/mono). | |
| This is the standard intermediate format for all processing. | |
| Args: | |
| input_path: Path to input audio file (m4a, wav, mp3, etc.) | |
| output_path: Path to output wav file (temp file if None) | |
| Returns: | |
| Path to normalized wav file | |
| Raises: | |
| FormatConversionError: If normalization fails | |
| """ | |
| try: | |
| from pydub import AudioSegment | |
| input_path = Path(input_path) | |
| if not input_path.exists(): | |
| raise FormatConversionError(f"Input file not found: {input_path}") | |
| # Create output path if not provided | |
| if output_path is None: | |
| temp_dir = tempfile.gettempdir() | |
| output_path = Path(temp_dir) / f"{input_path.stem}_normalized.wav" | |
| else: | |
| output_path = Path(output_path) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| # Detect input format | |
| input_format = input_path.suffix.lstrip(".") | |
| # Load audio | |
| audio = AudioSegment.from_file(str(input_path), format=input_format) | |
| # Normalize to intermediate format: 48kHz, 24-bit, mono | |
| audio = audio.set_frame_rate(48000) | |
| audio = audio.set_channels(1) | |
| audio = audio.set_sample_width(3) # 24-bit = 3 bytes | |
| # Export as wav | |
| audio.export(str(output_path), format="wav") | |
| logger.debug(f"Normalized to intermediate: {input_path.name} -> {output_path.name}") | |
| return str(output_path) | |
| except Exception as e: | |
| if isinstance(e, FormatConversionError): | |
| raise | |
| raise FormatConversionError(f"Failed to normalize audio: {str(e)}") | |
| def convert_to_final_output(input_path: str, output_path: str, format: str = "m4a") -> str: | |
| """ | |
| Convert intermediate wav to final output format. | |
| Final output is m4a with AAC 192kbps, 48kHz, mono. | |
| Args: | |
| input_path: Path to input wav file | |
| output_path: Path to output file | |
| format: Output format (default: "m4a") | |
| Returns: | |
| Path to output file | |
| Raises: | |
| FormatConversionError: If conversion fails | |
| """ | |
| if format == "m4a": | |
| return wav_to_m4a(input_path, output_path, bitrate="192k", sample_rate=48000) | |
| elif format == "wav": | |
| # Just copy if wav output requested | |
| import shutil | |
| output_path = Path(output_path) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy(input_path, output_path) | |
| return str(output_path) | |
| else: | |
| raise FormatConversionError(f"Unsupported output format: {format}") | |
| def batch_convert( | |
| input_files: list, output_dir: str, output_format: str = "m4a", progress_callback=None | |
| ) -> list: | |
| """ | |
| Convert multiple files to output format. | |
| Args: | |
| input_files: List of input file paths | |
| output_dir: Output directory | |
| output_format: Output format (default: "m4a") | |
| progress_callback: Optional callback(index, total, filename) | |
| Returns: | |
| List of output file paths | |
| Raises: | |
| FormatConversionError: If any conversion fails | |
| """ | |
| output_dir = Path(output_dir) | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| output_files = [] | |
| total = len(input_files) | |
| for i, input_file in enumerate(input_files): | |
| input_path = Path(input_file) | |
| # Generate output filename | |
| output_name = f"{input_path.stem}.{output_format}" | |
| output_path = output_dir / output_name | |
| if progress_callback: | |
| progress_callback(i + 1, total, input_path.name) | |
| # Convert to intermediate then to final | |
| intermediate = normalize_to_intermediate(str(input_path)) | |
| final = convert_to_final_output(intermediate, str(output_path), output_format) | |
| # Clean up intermediate file | |
| Path(intermediate).unlink(missing_ok=True) | |
| output_files.append(final) | |
| return output_files | |
| def get_conversion_info(input_path: str) -> dict: | |
| """ | |
| Get information about required conversion. | |
| Args: | |
| input_path: Path to input file | |
| Returns: | |
| Dictionary with conversion details | |
| """ | |
| try: | |
| from pydub import AudioSegment | |
| input_path = Path(input_path) | |
| if not input_path.exists(): | |
| return {"error": "File not found"} | |
| # Load audio to inspect properties | |
| input_format = input_path.suffix.lstrip(".") | |
| audio = AudioSegment.from_file(str(input_path), format=input_format) | |
| return { | |
| "current_format": input_format, | |
| "current_sample_rate": audio.frame_rate, | |
| "current_channels": audio.channels, | |
| "current_sample_width": audio.sample_width, | |
| "duration_seconds": len(audio) / 1000.0, | |
| "needs_conversion": ( | |
| audio.frame_rate != 48000 or audio.channels != 1 or audio.sample_width != 3 | |
| ), | |
| "target_format": "wav (intermediate) -> m4a (final)", | |
| "target_sample_rate": 48000, | |
| "target_channels": 1, | |
| "target_bit_depth": 24, | |
| } | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def estimate_output_size(input_path: str, output_format: str = "m4a") -> int: | |
| """ | |
| Estimate output file size in bytes. | |
| Args: | |
| input_path: Path to input file | |
| output_format: Output format | |
| Returns: | |
| Estimated file size in bytes | |
| """ | |
| try: | |
| info = get_conversion_info(input_path) | |
| if "error" in info: | |
| return 0 | |
| duration = info["duration_seconds"] | |
| if output_format == "m4a": | |
| # AAC 192kbps = 192 * 1000 / 8 bytes per second | |
| bitrate_bps = 192 * 1000 / 8 | |
| return int(duration * bitrate_bps) | |
| elif output_format == "wav": | |
| # 48kHz * 3 bytes (24-bit) * 1 channel | |
| return int(duration * 48000 * 3) | |
| else: | |
| return 0 | |
| except Exception: | |
| return 0 | |