aboutsummaryrefslogtreecommitdiff
path: root/tests/test_audio.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_audio.py')
-rw-r--r--tests/test_audio.py67
1 files changed, 66 insertions, 1 deletions
diff --git a/tests/test_audio.py b/tests/test_audio.py
index fb448f8..c043766 100644
--- a/tests/test_audio.py
+++ b/tests/test_audio.py
@@ -1,9 +1,11 @@
"""Tests for audio helpers: speed parameters, chunk cleanup, encoding,
-command construction, and duration verification."""
+command construction, duration verification, and audio concatenation."""
import tempfile
import unittest
+import wave
from pathlib import Path
+from unittest.mock import patch
from converter import audio
from converter import config
@@ -17,6 +19,7 @@ from converter.audio import (
build_ffmetadata,
build_m4b_chapters_command,
cleanup_chunks,
+ concat_audio_files,
speed_export_params,
verify_output_duration,
)
@@ -422,5 +425,67 @@ class BuildM4bChaptersCommandMetaTests(unittest.TestCase):
self.assertNotIn("-metadata", cmd)
+class ConcatAudioFilesTests(unittest.TestCase):
+ """Concatenation of sub-request audio into one chunk file."""
+
+ @staticmethod
+ def _write_wav(path: Path, frames: bytes, framerate: int = 24000) -> Path:
+ with wave.open(str(path), "wb") as wav_file:
+ wav_file.setnchannels(1)
+ wav_file.setsampwidth(2)
+ wav_file.setframerate(framerate)
+ wav_file.writeframes(frames)
+ return path
+
+ def test_wav_files_are_merged_in_order(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ first = self._write_wav(Path(tmp) / "a.wav", b"\x01\x00" * 10)
+ second = self._write_wav(Path(tmp) / "b.wav", b"\x02\x00" * 20)
+ destination = Path(tmp) / "out.wav"
+ concat_audio_files([first, second], destination)
+ with wave.open(str(destination), "rb") as wav_file:
+ self.assertEqual(wav_file.getframerate(), 24000)
+ self.assertEqual(wav_file.getnchannels(), 1)
+ self.assertEqual(wav_file.getsampwidth(), 2)
+ frames = wav_file.readframes(wav_file.getnframes())
+ self.assertEqual(frames, b"\x01\x00" * 10 + b"\x02\x00" * 20)
+
+ def test_single_wav_file_is_copied(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ source = self._write_wav(Path(tmp) / "a.wav", b"\x03\x00" * 15)
+ destination = Path(tmp) / "out.wav"
+ concat_audio_files([source], destination)
+ with wave.open(str(destination), "rb") as wav_file:
+ self.assertEqual(wav_file.readframes(wav_file.getnframes()),
+ b"\x03\x00" * 15)
+
+ def test_empty_source_list_raises(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ with self.assertRaises(ValueError):
+ concat_audio_files([], Path(tmp) / "out.wav")
+
+ def test_mismatched_wav_parameters_fall_back_to_ffmpeg(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ first = self._write_wav(Path(tmp) / "a.wav", b"\x01\x00" * 10, framerate=24000)
+ second = self._write_wav(Path(tmp) / "b.wav", b"\x02\x00" * 10, framerate=16000)
+ destination = Path(tmp) / "out.wav"
+ with patch("converter.audio.shutil.which", return_value=None), \
+ self.assertRaises(RuntimeError) as ctx:
+ concat_audio_files([first, second], destination)
+ self.assertIn("ffmpeg", str(ctx.exception))
+ # The wave-module path must not have written a partial output.
+ self.assertFalse(destination.exists())
+
+ def test_non_wav_input_falls_back_to_ffmpeg(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ source = Path(tmp) / "part.mp3"
+ source.write_bytes(b"not a wav file")
+ destination = Path(tmp) / "out.wav"
+ with patch("converter.audio.shutil.which", return_value=None), \
+ self.assertRaises(RuntimeError) as ctx:
+ concat_audio_files([source], destination)
+ self.assertIn("ffmpeg", str(ctx.exception))
+
+
if __name__ == "__main__":
unittest.main()