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

1from __future__ import annotations 

2 

3from io import BytesIO 

4from pathlib import Path 

5from tempfile import NamedTemporaryFile 

6from typing import NamedTuple, Self, override 

7 

8import numpy 

9import soundfile 

10from moviepy.audio.AudioClip import AudioClip 

11from moviepy.audio.io.AudioFileClip import AudioFileClip 

12 

13from . import BytesBlob 

14 

15 

16class AudioData(NamedTuple): 

17 data: numpy.ndarray 

18 sample_rate: int 

19 

20 

21class AudioBlob(BytesBlob): 

22 __IN_MEMORY_FILE_NAME: str = "file.mp3" 

23 

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) 

36 

37 f.close() 

38 

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() 

45 

46 super().__init__(blob) 

47 

48 def as_audio(self, filename: str) -> AudioFileClip: 

49 Path(filename).write_bytes(self._blob_bytes) 

50 

51 return AudioFileClip(filename) 

52 

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)) 

57 

58 @override 

59 def __repr__(self) -> str: 

60 return f"{self.__class__.__name__}(...)" 

61 

62 @classmethod 

63 @override 

64 def load(cls: type[Self], f: Path | str) -> Self: 

65 f = Path(f).expanduser() 

66 

67 if f.suffix.lower() == ".mp3": 

68 return cls(f.read_bytes()) 

69 

70 clip = AudioFileClip(str(f)) 

71 blob = cls(clip) 

72 clip.close() 

73 

74 return blob 

75 

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) 

82 

83 f.write_bytes(self.as_bytes())