Coverage for src/blob_dict/blob/audio.py: 0%
51 statements
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-31 14:35 -0700
« prev ^ index » next coverage.py v7.8.1, created at 2025-05-31 14:35 -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 ) -> None:
28 if isinstance(blob, AudioClip):
29 blob = read_from_clip(
30 blob,
31 ".mp3",
32 delete_temp_clip_file=delete_temp_clip_file,
33 )
34 elif isinstance(blob, AudioData):
35 bio = BytesIO()
36 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
37 soundfile.write(bio, AudioData.data, AudioData.sample_rate)
38 blob = bio.getvalue()
40 super().__init__(blob)
42 def as_audio(self, filename: str) -> AudioFileClip:
43 Path(filename).write_bytes(self._blob_bytes)
45 return AudioFileClip(filename)
47 def as_audio_data(self) -> AudioData:
48 bio = BytesIO(self._blob_bytes)
49 bio.name = AudioBlob.__IN_MEMORY_FILE_NAME
50 return AudioData(*soundfile.read(bio))
52 @override
53 def __repr__(self) -> str:
54 return f"{self.__class__.__name__}(...)"
56 @classmethod
57 @override
58 def load(cls: type[Self], f: Path | str) -> Self:
59 f = Path(f).expanduser()
61 if f.suffix.lower() == ".mp3":
62 return cls(f.read_bytes())
64 clip = AudioFileClip(str(f))
65 blob = cls(clip)
66 clip.close()
68 return blob
70 @override
71 def dump(self, f: Path | str) -> None:
72 f = Path(f).expanduser()
73 if f.suffix.lower() != ".mp3":
74 msg = "Only MP3 file is supported."
75 raise ValueError(msg)
77 f.write_bytes(self.as_bytes())