From 7a1f640b056cbdcb872d0af0fc93f596e6ad08d3 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 10:41:58 +0100 Subject: [PATCH 01/11] fix: display original format name for ffmpeg files (#46) --- src/sounddiff/formats.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index b2b9292..1b8c152 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -15,6 +15,15 @@ # Formats that require ffmpeg FFMPEG_FORMATS = {".mp3", ".aac", ".m4a", ".wma", ".opus"} +# Mapping for original format display names in metadata +FORMAT_DISPLAY_NAMES = { + ".mp3": "MP3", + ".aac": "AAC", + ".m4a": "AAC", + ".wma": "WMA", + ".opus": "Opus", +} + def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: """Load an audio file and return the signal and metadata. @@ -57,13 +66,19 @@ def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: data, sample_rate = sf.read(str(filepath), dtype="float64", always_2d=True) + if original_filepath != read_filepath: + ext = original_filepath.suffix.lower() + display_format = FORMAT_DISPLAY_NAMES.get(ext, ext.lstrip('.').upper()) + else: + display_format = info.format + metadata = AudioMetadata( path=str(filepath), duration=len(data) / sample_rate, sample_rate=sample_rate, channels=data.shape[1], bit_depth=_subtype_to_bits(info.subtype), - format_name=info.format, + format_name=display_format, frames=len(data), ) @@ -97,4 +112,4 @@ def format_channels(n: int) -> str: return "mono" if n == 2: return "stereo" - return f"{n}ch" + return f"{n}ch" \ No newline at end of file From 062e7662d4e85898770d78466290de2123ae4123 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 11:25:11 +0100 Subject: [PATCH 02/11] style: add trailing newline --- src/sounddiff/formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index 1b8c152..573ab19 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -112,4 +112,4 @@ def format_channels(n: int) -> str: return "mono" if n == 2: return "stereo" - return f"{n}ch" \ No newline at end of file + return f"{n}ch" From ede69afae89feeefe414ded7b00111b68ed214e0 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 11:27:34 +0100 Subject: [PATCH 03/11] style: apply ruff formatting --- src/sounddiff/formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index 573ab19..b9e6907 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -68,7 +68,7 @@ def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: if original_filepath != read_filepath: ext = original_filepath.suffix.lower() - display_format = FORMAT_DISPLAY_NAMES.get(ext, ext.lstrip('.').upper()) + display_format = FORMAT_DISPLAY_NAMES.get(ext, ext.lstrip(".").upper()) else: display_format = info.format From 47d69ceaa54d100cfbcec7c6dd1c59429f648a16 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 11:33:31 +0100 Subject: [PATCH 04/11] fix: restore missing ffmpeg block and resolve undefined variables --- src/sounddiff/formats.py | 73 +++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index b9e6907..b8c8418 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -2,6 +2,12 @@ from __future__ import annotations +import atexit +import contextlib +import os +import shutil +import subprocess +import tempfile from pathlib import Path import numpy as np # noqa: TC002 (used at runtime in return type) @@ -25,6 +31,12 @@ } +def _remove_if_exists(path: str) -> None: + """Remove a file if it exists, silently ignoring missing files.""" + with contextlib.suppress(FileNotFoundError): + os.remove(path) + + def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: """Load an audio file and return the signal and metadata. @@ -40,31 +52,62 @@ def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: ValueError: If the format is unsupported or requires ffmpeg. RuntimeError: If the file cannot be read. """ - filepath = Path(path) + original_filepath = Path(path) + read_filepath = original_filepath - if not filepath.exists(): - raise FileNotFoundError(f"File not found: {filepath}") + if not original_filepath.exists(): + raise FileNotFoundError(f"File not found: {original_filepath}") - suffix = filepath.suffix.lower() + suffix = original_filepath.suffix.lower() if suffix in FFMPEG_FORMATS: - raise ValueError( - f"Format '{suffix}' requires ffmpeg, which is not installed or not supported yet. " - f"Supported formats without ffmpeg: {', '.join(sorted(NATIVE_FORMATS))}" - ) - - if suffix not in NATIVE_FORMATS: + if not shutil.which("ffmpeg"): + raise ValueError( + f"Format '{suffix}' requires ffmpeg, but it is not installed on your system. " + "Please install ffmpeg to analyze compressed audio files." + ) + + # Create a temporary WAV file for ffmpeg to write into + fd, temp_wav_path = tempfile.mkstemp(suffix=".wav", prefix="sounddiff_") + os.close(fd) + + # Schedule cleanup on exit so we never leave temp files behind + atexit.register(_remove_if_exists, temp_wav_path) + + try: + # Transcode silently to WAV, dropping video streams (like album art) with -vn + subprocess.run( + [ + "ffmpeg", + "-y", + "-i", + str(original_filepath), + "-vn", + "-loglevel", + "error", + temp_wav_path, + ], + check=True, + capture_output=True, + ) + # We will read from the temp file, but keep the original path for metadata + read_filepath = Path(temp_wav_path) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"FFmpeg failed to transcode '{path}': {e.stderr.decode('utf-8', errors='replace').strip()}" + ) from e + + if suffix not in NATIVE_FORMATS and suffix not in FFMPEG_FORMATS: raise ValueError( f"Unsupported audio format: '{suffix}'. " f"Supported: {', '.join(sorted(NATIVE_FORMATS | FFMPEG_FORMATS))}" ) try: - info = sf.info(str(filepath)) + info = sf.info(str(read_filepath)) + data, sample_rate = sf.read(str(read_filepath), dtype="float64", always_2d=True) except RuntimeError as e: - raise RuntimeError(f"Cannot read audio file: {filepath} ({e})") from e - - data, sample_rate = sf.read(str(filepath), dtype="float64", always_2d=True) + raise RuntimeError(f"Cannot read audio file: {original_filepath} ({e})") from e if original_filepath != read_filepath: ext = original_filepath.suffix.lower() @@ -73,7 +116,7 @@ def load_audio(path: str | Path) -> tuple[np.ndarray, AudioMetadata]: display_format = info.format metadata = AudioMetadata( - path=str(filepath), + path=str(original_filepath), duration=len(data) / sample_rate, sample_rate=sample_rate, channels=data.shape[1], From d50f8dc5d9f0d39ded4f65252e972f0bf982d6af Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 11:40:37 +0100 Subject: [PATCH 05/11] style: fix trailing newline for good --- src/sounddiff/formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index 8609ba6..b8c8418 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -155,4 +155,4 @@ def format_channels(n: int) -> str: return "mono" if n == 2: return "stereo" - return f"{n}ch" \ No newline at end of file + return f"{n}ch" From c003cd7ce9a1f0f0ddcc4c7c221730a17f9fcd4e Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:03:53 +0100 Subject: [PATCH 06/11] test: add format name test and fix typing --- src/sounddiff/formats.py | 2 +- tests/test_formats.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index b8c8418..61df3cc 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -22,7 +22,7 @@ FFMPEG_FORMATS = {".mp3", ".aac", ".m4a", ".wma", ".opus"} # Mapping for original format display names in metadata -FORMAT_DISPLAY_NAMES = { +FORMAT_DISPLAY_NAMES: dict[str, str] = { ".mp3": "MP3", ".aac": "AAC", ".m4a": "AAC", diff --git a/tests/test_formats.py b/tests/test_formats.py index 792030d..d6fe824 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -53,6 +53,34 @@ def test_frames_matches_data_length(self) -> None: data, meta = load_audio(FIXTURES_DIR / "sine_a.wav") assert meta.frames == len(data) + def test_transcoded_format_name_is_correct(self, tmp_path: Path) -> None: + """Verifies that transcoded files show their original format name, not WAV.""" + from unittest.mock import patch + + # 1. Creamos un archivo MP3 falso para pasar la validación de path.exists() + fake_mp3 = tmp_path / "test.mp3" + fake_mp3.write_text("fake audio content") + + # 2. Simulamos que ffmpeg existe y que la lectura del WAV temporal funciona + with patch("sounddiff.formats.shutil.which", return_value="ffmpeg"), \ + patch("sounddiff.formats.subprocess.run"), \ + patch("sounddiff.formats.sf.info") as mock_info, \ + patch("sounddiff.formats.sf.read") as mock_read: + + # Configuramos el mock para simular lo que devolvería el WAV temporal + class MockInfo: + format = "WAV" + subtype = "PCM_16" + + mock_info.return_value = MockInfo() + mock_read.return_value = (np.zeros((100, 2), dtype=np.float64), 44100) + + # 3. Llamamos a tu función + _, meta = load_audio(fake_mp3) + + # 4. LA COMPROBACIÓN FINAL: Debe decir MP3 y no WAV + assert meta.format_name == "MP3" + class TestFormatDuration: def test_zero(self) -> None: @@ -76,4 +104,4 @@ def test_stereo(self) -> None: assert format_channels(2) == "stereo" def test_multichannel(self) -> None: - assert format_channels(6) == "6ch" + assert format_channels(6) == "6ch" \ No newline at end of file From ab69fb94dbfdbcf4fdd02ac5a6bb661bfbf7cf47 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:11:43 +0100 Subject: [PATCH 07/11] style: add missing trailing newline --- tests/test_formats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index d6fe824..9b037a7 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -104,4 +104,5 @@ def test_stereo(self) -> None: assert format_channels(2) == "stereo" def test_multichannel(self) -> None: - assert format_channels(6) == "6ch" \ No newline at end of file + assert format_channels(6) == "6ch" + \ No newline at end of file From f398dceeb489dcd9dc4a65c0c4bf7126287a733f Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:16:32 +0100 Subject: [PATCH 08/11] style: remove trailing whitespace at EOF --- tests/test_formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 9b037a7..8c8ce49 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -105,4 +105,4 @@ def test_stereo(self) -> None: def test_multichannel(self) -> None: assert format_channels(6) == "6ch" - \ No newline at end of file + \ No newline at end of file From a6da74ef3ca964c637e7f06095ea784e5e9052cd Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:17:54 +0100 Subject: [PATCH 09/11] style: remove trailing whitespace at EOF --- tests/test_formats.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 8c8ce49..8b29d3c 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -105,4 +105,3 @@ def test_stereo(self) -> None: def test_multichannel(self) -> None: assert format_channels(6) == "6ch" - \ No newline at end of file From 9660a0e0cd01b6cb5f399859f9b8421e4c6a20d0 Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:22:14 +0100 Subject: [PATCH 10/11] style: fix all formatting and linting issues --- tests/test_formats.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 8b29d3c..b7f017a 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -62,11 +62,12 @@ def test_transcoded_format_name_is_correct(self, tmp_path: Path) -> None: fake_mp3.write_text("fake audio content") # 2. Simulamos que ffmpeg existe y que la lectura del WAV temporal funciona - with patch("sounddiff.formats.shutil.which", return_value="ffmpeg"), \ - patch("sounddiff.formats.subprocess.run"), \ - patch("sounddiff.formats.sf.info") as mock_info, \ - patch("sounddiff.formats.sf.read") as mock_read: - + with ( + patch("sounddiff.formats.shutil.which", return_value="ffmpeg"), + patch("sounddiff.formats.subprocess.run"), + patch("sounddiff.formats.sf.info") as mock_info, + patch("sounddiff.formats.sf.read") as mock_read, + ): # Configuramos el mock para simular lo que devolvería el WAV temporal class MockInfo: format = "WAV" From dda935bb5073e2d9d6a02a9e07965aa7edff510a Mon Sep 17 00:00:00 2001 From: jaimefg1888 Date: Fri, 27 Mar 2026 20:37:18 +0100 Subject: [PATCH 11/11] fix: manually add trailing newlines --- src/sounddiff/formats.py | 2 +- tests/test_formats.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sounddiff/formats.py b/src/sounddiff/formats.py index 763f5d7..2e03462 100644 --- a/src/sounddiff/formats.py +++ b/src/sounddiff/formats.py @@ -172,4 +172,4 @@ def format_channels(n: int) -> str: return "mono" if n == 2: return "stereo" - return f"{n}ch" \ No newline at end of file + return f"{n}ch" diff --git a/tests/test_formats.py b/tests/test_formats.py index 57a11e2..f87fb84 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -127,4 +127,4 @@ def test_stereo(self) -> None: assert format_channels(2) == "stereo" def test_multichannel(self) -> None: - assert format_channels(6) == "6ch" \ No newline at end of file + assert format_channels(6) == "6ch"