diff --git a/omnivoice/utils/audio.py b/omnivoice/utils/audio.py index 6b0ed317..75fa3307 100644 --- a/omnivoice/utils/audio.py +++ b/omnivoice/utils/audio.py @@ -65,18 +65,7 @@ def load_audio(audio_path: str, sampling_rate: int): # libraries are absent. The ``backend="soundfile"`` argument above # does NOT avoid that — 2.9 accepts and ignores it. aseg = AudioSegment.from_file(audio_path) - # Scale by the decoded sample width instead of a hardcoded 16-bit - # divisor. pydub reports 8-bit as sample_width 1 and widens 24-bit to - # a full-range int32 (sample_width 4), so /32768 makes 24- and 32-bit - # references 32768x too loud and 8-bit ones 256x too quiet. - audio_data = ( - np.array(aseg.get_array_of_samples()).astype(np.float32) - / aseg.max_possible_amplitude - ) - if aseg.channels == 1: - waveform = torch.from_numpy(audio_data).unsqueeze(0) - else: - waveform = torch.from_numpy(audio_data.reshape(-1, aseg.channels).T) + waveform = audiosegment_to_tensor(aseg) prompt_sampling_rate = aseg.frame_rate if prompt_sampling_rate != sampling_rate: @@ -255,7 +244,7 @@ def audiosegment_to_tensor(aseg): audio_data = np.array(aseg.get_array_of_samples()) # Convert to float32 and normalize to [-1, 1] range - audio_data = audio_data.astype(np.float32) / 32768.0 + audio_data = audio_data.astype(np.float32) / aseg.max_possible_amplitude # Handle channels if aseg.channels == 1: diff --git a/tests/test_load_audio_torchcodec_fallback.py b/tests/test_load_audio_torchcodec_fallback.py index f1ebf211..d4360fd5 100644 --- a/tests/test_load_audio_torchcodec_fallback.py +++ b/tests/test_load_audio_torchcodec_fallback.py @@ -23,7 +23,6 @@ import pytest import soundfile as sf import torch -from omnivoice.utils.audio import load_audio def _write_sine_wav(path, *, seconds: float = 0.5, sample_rate: int = 24000): @@ -43,6 +42,7 @@ def _torchcodec_missing(*_a, **_kw): def test_load_audio_falls_back_when_torchcodec_missing(tmp_path, monkeypatch): """Without the ImportError catch this raises instead of returning audio.""" + from omnivoice.utils.audio import load_audio import torchaudio ref = tmp_path / "ref.wav" @@ -60,6 +60,7 @@ def test_load_audio_falls_back_when_torchcodec_missing(tmp_path, monkeypatch): def test_load_audio_fallback_resamples_to_target(tmp_path, monkeypatch): """The fallback path must still honour the requested sampling rate.""" + from omnivoice.utils.audio import load_audio import torchaudio ref = tmp_path / "ref_16k.wav" @@ -90,6 +91,7 @@ def test_load_audio_fallback_amplitude_matches_bit_depth( without TorchCodec it is the only path, which is what makes it a bug worth fixing here. """ + from omnivoice.utils.audio import load_audio import torchaudio sample_rate = 24000 @@ -106,3 +108,14 @@ def test_load_audio_fallback_amplitude_matches_bit_depth( f"{subtype} decoded at the wrong scale: peak " f"{waveform.abs().max().item():.6f}, expected ~{peak}" ) + + +@pytest.mark.parametrize("subtype", ["PCM_U8", "PCM_16", "PCM_24", "PCM_32"]) +def test_audiosegment_conversion_preserves_amplitude(tmp_path, subtype): + from pydub import AudioSegment + from omnivoice.utils.audio import audiosegment_to_tensor + path = tmp_path / "stereo.wav" + samples = np.tile([0.5, -0.25], (100, 1)) + sf.write(path, samples, 24000, subtype=subtype) + converted = audiosegment_to_tensor(AudioSegment.from_file(path)).numpy() + np.testing.assert_allclose(converted, samples.T, atol=0.01)