From a0f9f6bff8001cd0c433aca79ad21f79f4d6dfd6 Mon Sep 17 00:00:00 2001 From: Daksha1611 Date: Fri, 11 Sep 2026 21:02:44 +0530 Subject: [PATCH] Down-mix stereo MP3 to mono in readMp3 readMp3() is documented in audio_utils.hpp as returning mono float32 PCM samples, and readWav() down-mixes stereo accordingly. readMp3() did not: it appended framesRead * mp3.channels floats per chunk, so a 2-channel file came back as an interleaved L/R stream twice as long as the frame count. Downstream that buffer is consumed as a mono waveform - the chat completions input_audio path copies it straight into a 1-D f32 tensor - so stereo MP3 input produced garbage audio at twice the real duration. The resampling path was affected too: outputLength was computed from the interleaved sample count and resample_audio() interpolated between adjacent L and R samples. Down-mix per decode chunk so the existing size guard keeps measuring the final buffer, and pass 1 channel to the metadata-based size validation to match readWav(). Dropping the drmp3_uninit() call before the overflow throw also removes a double-uninit: the enclosing catch(...) already calls it before rethrowing. Tests: the two existing max-file-size tests used a joint-stereo frame and encoded the interleaved count (2304); they now expect the mono count (1152). Adds mp3StereoIsDownmixedToMono covering the contract directly. --- src/audio/audio_utils.cpp | 16 +++++++++++++--- src/test/audio/audio_utils_test.cpp | 27 +++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/audio/audio_utils.cpp b/src/audio/audio_utils.cpp index 5c0136c1a6..78d2d084b4 100644 --- a/src/audio/audio_utils.cpp +++ b/src/audio/audio_utils.cpp @@ -169,7 +169,7 @@ std::vector readMp3(const std::string_view& mp3Data, uint32_t targetSampl if (mp3.totalPCMFrameCount != std::numeric_limits::max()) { try { if (targetSampleRate > 0) { - validateAudioFileSize(mp3.totalPCMFrameCount, mp3.sampleRate, targetSampleRate, mp3.channels, sizeof(float)); + validateAudioFileSize(mp3.totalPCMFrameCount, mp3.sampleRate, targetSampleRate, /*will be downmixed to mono*/ 1, sizeof(float)); } } catch (...) { drmp3_uninit(&mp3); @@ -188,10 +188,20 @@ std::vector readMp3(const std::string_view& mp3Data, uint32_t targetSampl break; } if (pcmf32.size() > AUDIO_BUFFER_SIZE_LIMIT) { - drmp3_uninit(&mp3); throw std::overflow_error("Decoded audio buffer size overflow"); } - pcmf32.insert(pcmf32.end(), tempBuffer, tempBuffer + framesRead * mp3.channels); + if (mp3.channels == 1) { + pcmf32.insert(pcmf32.end(), tempBuffer, tempBuffer + framesRead); + } else { + // Down-mix interleaved stereo to mono so that readMp3 honours the same + // "mono float32 PCM samples" contract as readWav. Done per chunk so the + // size guard below keeps measuring the final buffer. + const size_t writeOffset = pcmf32.size(); + pcmf32.resize(writeOffset + framesRead); + for (drmp3_uint64 frame = 0; frame < framesRead; frame++) { + pcmf32[writeOffset + frame] = (tempBuffer[2 * frame] + tempBuffer[2 * frame + 1]) * 0.5f; + } + } validateAudioFileSizeAgainstMaxValue(pcmf32.size() * sizeof(float)); } } catch (...) { diff --git a/src/test/audio/audio_utils_test.cpp b/src/test/audio/audio_utils_test.cpp index ede322830d..3fa7804a88 100644 --- a/src/test/audio/audio_utils_test.cpp +++ b/src/test/audio/audio_utils_test.cpp @@ -170,8 +170,8 @@ TEST_F(AudioUtilsSampleRateTest, mp3FileRejectedWhenExceedsMaxFileSizeEnv) { mp3.push_back(static_cast(0x40)); mp3.append(413, '\0'); std::string_view view(mp3); - // For this frame, actual decoded size is 2304 samples (stereo or decoder output) - size_t expectedDecodedSize = 2304 * sizeof(float); + // This joint-stereo frame decodes to 1152 PCM frames, returned as 1152 mono samples. + size_t expectedDecodedSize = 1152 * sizeof(float); SetEnvironmentVar("OVMS_AUDIO_MAX_FILE_SIZE_BYTES", std::to_string(expectedDecodedSize - 1)); std::vector decoded; EXPECT_THROW({ decoded = readMp3(view); }, std::runtime_error); @@ -187,14 +187,33 @@ TEST_F(AudioUtilsSampleRateTest, mp3FileAcceptedWhenAtMaxFileSizeEnv) { mp3.push_back(static_cast(0x40)); mp3.append(413, '\0'); std::string_view view(mp3); - // For this frame, actual decoded size is 2304 samples (stereo or decoder output) - size_t expectedDecodedSize = 2304 * sizeof(float); + // This joint-stereo frame decodes to 1152 PCM frames, returned as 1152 mono samples. + size_t expectedDecodedSize = 1152 * sizeof(float); SetEnvironmentVar("OVMS_AUDIO_MAX_FILE_SIZE_BYTES", std::to_string(expectedDecodedSize)); std::vector decoded; EXPECT_NO_THROW({ decoded = readMp3(view); }); UnSetEnvironmentVar("OVMS_AUDIO_MAX_FILE_SIZE_BYTES"); } +// readMp3 is documented to return mono float32 PCM samples, and readWav already +// down-mixes stereo. A 2-channel MP3 must therefore yield one sample per PCM frame, +// not one per channel - otherwise the interleaved stream is read downstream as a +// mono waveform of twice the real duration. +TEST_F(AudioUtilsSampleRateTest, mp3StereoIsDownmixedToMono) { + std::string mp3; + mp3.reserve(417); + mp3.push_back(static_cast(0xFF)); + mp3.push_back(static_cast(0xFB)); + mp3.push_back(static_cast(0x90)); + mp3.push_back(static_cast(0x40)); // channel mode 01: joint stereo + mp3.append(413, '\0'); + std::string_view view(mp3); + std::vector decoded; + ASSERT_NO_THROW({ decoded = readMp3(view, DISABLED_RESAMPLING_SAMPLE_RATE); }); + // 1152 PCM frames from a single MPEG-1 Layer III frame, down-mixed to 1152 samples. + EXPECT_EQ(decoded.size(), 1152u); +} + // Validates that validateAudioFileSize correctly rejects when inputSamples * targetRate // would overflow size_t. This guards against the case where dr_mp3 provides an inflated // totalPCMFrameCount (e.g. from a malicious Xing tag or UINT64_MAX sentinel).