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

1from __future__ import annotations 

2 

3from io import BytesIO 

4from pathlib import Path 

5from typing import NamedTuple, Self, override 

6 

7import numpy 

8import soundfile 

9from moviepy.audio.AudioClip import AudioClip 

10from moviepy.audio.io.AudioFileClip import AudioFileClip 

11 

12from . import BytesBlob 

13from .audio_video import read_from_clip 

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

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

41 

42 super().__init__(blob) 

43 

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

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

46 

47 return AudioFileClip(filename) 

48 

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

53 

54 @override 

55 def __repr__(self) -> str: 

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

57 

58 @classmethod 

59 @override 

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

61 f = Path(f).expanduser() 

62 

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

64 return cls(f.read_bytes()) 

65 

66 clip = AudioFileClip(str(f)) 

67 blob = cls(clip) 

68 clip.close() 

69 

70 return blob 

71 

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) 

78 

79 f.write_bytes(self.as_bytes())