Coverage for src/blob_dict/blob/audio.py: 0%
51 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-04 23:47 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-06-04 23:47 -0700
1from __future__ import annotations
3from io import BytesIO
4from pathlib import Path
5from typing import NamedTuple, Self, override
7import numpy
8import soundfile
9from moviepy.audio.AudioClip import AudioClip
10from moviepy.audio.io.AudioFileClip import AudioFileClip
12from . import BytesBlob
13from .audio_video import read_from_clip
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__(
25 self,
26 blob: bytes | AudioClip | AudioData,
27 *,
28 delete_temp_clip_file: bool = False,
29 ) -> None:
30 if isinstance(blob, AudioClip):
31 blob = read_from_clip(
32 blob,
33 ".mp3",
34 delete_temp_clip_file=delete_temp_clip_file,
35 )
36 elif isinstance(blob, AudioData):
37 bio = BytesIO()
38 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
39 soundfile.write(bio, AudioData.data, AudioData.sample_rate)
40 blob = bio.getvalue()
42 super().__init__(blob)
44 def as_audio(self, filename: str) -> AudioFileClip:
45 Path(filename).write_bytes(self._blob_bytes)
47 return AudioFileClip(filename)
49 def as_audio_data(self) -> AudioData:
50 bio = BytesIO(self._blob_bytes)
51 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
52 return AudioData(*soundfile.read(bio))
54 @override
55 def __repr__(self) -> str:
56 return f"{self.__class__.__name__}(...)"
58 @classmethod
59 @override
60 def load(cls: type[Self], f: Path | str) -> Self:
61 f = Path(f).expanduser()
63 if f.suffix.lower() == ".mp3":
64 return cls(f.read_bytes())
66 clip = AudioFileClip(str(f))
67 blob = cls(clip)
68 clip.close()
70 return blob
72 @override
73 def dump(self, f: Path | str) -> None:
74 f = Path(f).expanduser()
75 if f.suffix.lower() != ".mp3":
76 msg = "Only MP3 file is supported."
77 raise ValueError(msg)
79 f.write_bytes(self.as_bytes())