Coverage for src/blob_dict/blob/audio.py: 0%
56 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-29 23:07 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-29 23:07 -0700
1from __future__ import annotations
3from io import BytesIO
4from pathlib import Path
5from tempfile import NamedTemporaryFile
6from typing import NamedTuple, Self, override
8import numpy
9import soundfile
10from moviepy.audio.AudioClip import AudioClip
11from moviepy.audio.io.AudioFileClip import AudioFileClip
13from . import BytesBlob
16class AudioData(NamedTuple):
17 data: numpy.ndarray
18 sample_rate: int
21class AudioBlob(BytesBlob):
22 __IN_MEMORY_FILE_NAME: str = "file.mp3"
24 def __init__(self, blob: bytes | AudioClip | AudioData) -> None:
25 if isinstance(blob, AudioClip):
26 if (
27 isinstance(blob, AudioFileClip)
28 # Need to check whether `filename` is `str`` as it can also be sound array
29 and isinstance(blob.filename, str)
30 and blob.filename.lower().endswith(".mp3")
31 ):
32 blob = Path(blob.filename).read_bytes()
33 else:
34 with NamedTemporaryFile(suffix=".mp3", delete_on_close=False) as f:
35 blob.write_audiofile(f.name)
37 f.close()
39 blob = Path(f.name).read_bytes()
40 elif isinstance(blob, AudioData):
41 bio = BytesIO()
42 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
43 soundfile.write(bio, AudioData.data, AudioData.sample_rate)
44 blob = bio.getvalue()
46 super().__init__(blob)
48 def as_audio(self, filename: str) -> AudioFileClip:
49 Path(filename).write_bytes(self._blob_bytes)
51 return AudioFileClip(filename)
53 def as_audio_data(self) -> AudioData:
54 bio = BytesIO(self._blob_bytes)
55 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
56 return AudioData(*soundfile.read(bio))
58 @override
59 def __repr__(self) -> str:
60 return f"{self.__class__.__name__}(...)"
62 @classmethod
63 @override
64 def load(cls: type[Self], f: Path | str) -> Self:
65 f = Path(f).expanduser()
67 if f.suffix.lower() == ".mp3":
68 return cls(f.read_bytes())
70 clip = AudioFileClip(str(f))
71 blob = cls(clip)
72 clip.close()
74 return blob
76 @override
77 def dump(self, f: Path | str) -> None:
78 f = Path(f).expanduser()
79 if f.suffix.lower() != ".mp3":
80 msg = "Only MP3 file is supported."
81 raise ValueError(msg)
83 f.write_bytes(self.as_bytes())