@@ -127,12 +127,16 @@ def load_audio_text_image_video(
127127 data_or_path_or_list = data_or_path_or_list .mean (0 )
128128 except :
129129 try :
130+ if hasattr (data_or_path_or_list , "seek" ):
131+ data_or_path_or_list .seek (0 )
130132 import soundfile as sf
131133 data_np , audio_fs = sf .read (data_or_path_or_list , dtype = "float32" )
132134 data_or_path_or_list = torch .from_numpy (data_np ).squeeze ()
133135 if data_or_path_or_list .ndim > 1 and kwargs .get ("reduce_channels" , True ):
134136 data_or_path_or_list = data_or_path_or_list .mean (- 1 )
135137 except :
138+ if hasattr (data_or_path_or_list , "seek" ):
139+ data_or_path_or_list .seek (0 )
136140 data_or_path_or_list = _load_audio_ffmpeg (data_or_path_or_list , sr = fs )
137141 data_or_path_or_list = torch .from_numpy (
138142 data_or_path_or_list
@@ -175,6 +179,96 @@ def load_audio_text_image_video(
175179 return data_or_path_or_list
176180
177181
182+ def _mp3_header_fields (data : bytes , offset : int ):
183+ """Return MPEG audio header fields, including free-format bitrate index zero."""
184+ if offset + 4 > len (data ):
185+ return None
186+
187+ header = int .from_bytes (data [offset : offset + 4 ], "big" )
188+ if header & 0xFFE00000 != 0xFFE00000 :
189+ return None
190+
191+ version_id = (header >> 19 ) & 0x3
192+ layer_id = (header >> 17 ) & 0x3
193+ bitrate_index = (header >> 12 ) & 0xF
194+ sample_rate_index = (header >> 10 ) & 0x3
195+ padding = (header >> 9 ) & 0x1
196+ if (
197+ version_id == 1
198+ or layer_id == 0
199+ or bitrate_index == 15
200+ or sample_rate_index == 3
201+ ):
202+ return None
203+ return version_id , layer_id , bitrate_index , sample_rate_index , padding
204+
205+
206+ def _mp3_frame_length (data : bytes , offset : int ) -> int :
207+ """Return a fixed-bitrate MPEG audio frame length, or zero if unavailable."""
208+ fields = _mp3_header_fields (data , offset )
209+ if fields is None :
210+ return 0
211+ version_id , layer_id , bitrate_index , sample_rate_index , padding = fields
212+ if bitrate_index == 0 :
213+ return 0
214+
215+ mpeg1_bitrates = {
216+ 3 : (32 , 64 , 96 , 128 , 160 , 192 , 224 , 256 , 288 , 320 , 352 , 384 , 416 , 448 ),
217+ 2 : (32 , 48 , 56 , 64 , 80 , 96 , 112 , 128 , 160 , 192 , 224 , 256 , 320 , 384 ),
218+ 1 : (32 , 40 , 48 , 56 , 64 , 80 , 96 , 112 , 128 , 160 , 192 , 224 , 256 , 320 ),
219+ }
220+ mpeg2_bitrates = {
221+ 3 : (32 , 48 , 56 , 64 , 80 , 96 , 112 , 128 , 144 , 160 , 176 , 192 , 224 , 256 ),
222+ 2 : (8 , 16 , 24 , 32 , 40 , 48 , 56 , 64 , 80 , 96 , 112 , 128 , 144 , 160 ),
223+ 1 : (8 , 16 , 24 , 32 , 40 , 48 , 56 , 64 , 80 , 96 , 112 , 128 , 144 , 160 ),
224+ }
225+ sample_rates = {
226+ 3 : (44100 , 48000 , 32000 ),
227+ 2 : (22050 , 24000 , 16000 ),
228+ 0 : (11025 , 12000 , 8000 ),
229+ }
230+ bitrate_table = mpeg1_bitrates if version_id == 3 else mpeg2_bitrates
231+ bitrate = bitrate_table [layer_id ][bitrate_index - 1 ] * 1000
232+ sample_rate = sample_rates [version_id ][sample_rate_index ]
233+
234+ if layer_id == 3 :
235+ return (12 * bitrate // sample_rate + padding ) * 4
236+ coefficient = 144 if version_id == 3 or layer_id == 2 else 72
237+ return coefficient * bitrate // sample_rate + padding
238+
239+
240+ def _has_consecutive_mp3_frames (data : bytes ) -> bool :
241+ """Avoid mistaking raw PCM that starts with one sync-like sample for MP3."""
242+ fields = _mp3_header_fields (data , 0 )
243+ if fields is None :
244+ return False
245+ version_id , layer_id , bitrate_index , sample_rate_index , _ = fields
246+ if bitrate_index == 0 :
247+ signature = version_id , layer_id , sample_rate_index
248+ padding_slot = 4 if layer_id == 3 else 1
249+ first_padding = fields [4 ] * padding_slot
250+ for second_offset in range (24 , min (len (data ) - 3 , 8192 )):
251+ next_fields = _mp3_header_fields (data , second_offset )
252+ if next_fields is not None and (
253+ next_fields [0 ], next_fields [1 ], next_fields [3 ]
254+ ) == signature and next_fields [2 ] == 0 :
255+ base_frame_length = second_offset - first_padding
256+ third_offset = (
257+ second_offset
258+ + base_frame_length
259+ + next_fields [4 ] * padding_slot
260+ )
261+ third_fields = _mp3_header_fields (data , third_offset )
262+ if third_fields is not None and (
263+ third_fields [0 ], third_fields [1 ], third_fields [3 ]
264+ ) == signature and third_fields [2 ] == 0 :
265+ return True
266+ return False
267+
268+ first_frame_length = _mp3_frame_length (data , 0 )
269+ return first_frame_length > 0 and _mp3_frame_length (data , first_frame_length ) > 0
270+
271+
178272def _is_audio_container (data : bytes ) -> bool :
179273 """Return True if *data* starts with a recognised container-format magic header.
180274
@@ -184,10 +278,15 @@ def _is_audio_container(data: bytes) -> bool:
184278 if len (data ) < 4 :
185279 return False
186280 # WAV – RIFF....WAVE
187- if data [:4 ] == b"RIFF" :
281+ if (
282+ len (data ) >= 12
283+ and data [:4 ] in (b"RIFF" , b"RIFX" , b"RF64" , b"BW64" )
284+ and data [8 :12 ] == b"WAVE"
285+ ):
188286 return True
189- # MP3 – ID3 tag or sync word (0xFF 0xEx)
190- if data [:3 ] == b"ID3" or (data [0 ] == 0xFF and (data [1 ] & 0xE0 ) == 0xE0 ):
287+ # MP3 – ID3 tag or at least two structurally valid MPEG audio frames
288+ has_mpeg_sync = data [0 ] == 0xFF and (data [1 ] & 0xE0 ) == 0xE0
289+ if data [:3 ] == b"ID3" or (has_mpeg_sync and _has_consecutive_mp3_frames (data )):
191290 return True
192291 # OGG
193292 if data [:4 ] == b"OggS" :
@@ -205,23 +304,28 @@ def _is_audio_container(data: bytes) -> bool:
205304
206305
207306def load_bytes (input ):
208- """Convert audio bytes to numpy array .
307+ """Convert raw PCM or container-formatted audio bytes to a waveform .
209308
210309 Args:
211- input (bytes): Raw audio bytes.
310+ input (bytes): Raw int16 PCM or encoded audio-file bytes.
212311
213312 Returns:
214- numpy.ndarray: Decoded audio samples.
313+ numpy.ndarray: Mono float32 samples at 16 kHz .
215314 """
216- # Only run the (expensive) frame-rate validation when the payload is an
217- # actual audio container (WAV, MP3, OGG, …). Raw PCM buffers have no
218- # recognisable header and would cause pydub to spend ~200 ms before
219- # raising an exception that is then silently swallowed anyway.
220315 if _is_audio_container (input ):
221316 try :
222- input = validate_frame_rate (input )
223- except Exception :
224- pass
317+ waveform = load_audio_text_image_video (BytesIO (input ), fs = 16000 )
318+ except Exception as exc :
319+ raise RuntimeError (
320+ "Failed to decode container-formatted audio bytes. Verify that the input is "
321+ "a complete supported audio file and that torchaudio, soundfile, or ffmpeg "
322+ "is available."
323+ ) from exc
324+ else :
325+ if isinstance (waveform , torch .Tensor ):
326+ waveform = waveform .detach ().cpu ().numpy ()
327+ return np .asarray (waveform , dtype = np .float32 )
328+
225329 middle_data = np .frombuffer (input , dtype = np .int16 )
226330 middle_data = np .asarray (middle_data )
227331 if middle_data .dtype .kind not in "iu" :
@@ -315,14 +419,14 @@ def extract_fbank(data, data_len=None, data_type: str = "sound", frontend=None,
315419 return data .to (torch .float32 ), data_len .to (torch .int32 )
316420
317421
318- def _load_audio_ffmpeg (file : str , sr : int = 16000 ):
422+ def _load_audio_ffmpeg (file , sr : int = 16000 ):
319423 """
320424 Open an audio file and read as mono waveform, resampling as necessary
321425
322426 Parameters
323427 ----------
324- file: str
325- The audio file to open
428+ file: str or file-like object
429+ The audio file or byte stream to open
326430
327431 sr: int
328432 The sample rate to resample the audio if necessary
@@ -336,7 +440,15 @@ def _load_audio_ffmpeg(file: str, sr: int = 16000):
336440 # and resampling as necessary. Requires the ffmpeg CLI in PATH.
337441 # fmt: off
338442 pcm_params = []
339- if file .lower ().endswith ('.pcm' ):
443+ stdin_data = None
444+ if hasattr (file , "read" ):
445+ if hasattr (file , "seek" ):
446+ file .seek (0 )
447+ stdin_data = file .read ()
448+ input_source = "pipe:0"
449+ else :
450+ input_source = os .fspath (file )
451+ if isinstance (input_source , str ) and input_source .lower ().endswith ('.pcm' ):
340452 pcm_params = [
341453 "-f" , "s16le" ,
342454 "-ar" , str (sr ),
@@ -348,7 +460,7 @@ def _load_audio_ffmpeg(file: str, sr: int = 16000):
348460 "-nostdin" ,
349461 "-threads" , "0" ,
350462 * pcm_params , # PCM files need input format specified before -i since PCM is raw data without headers
351- "-i" , file ,
463+ "-i" , input_source ,
352464 "-f" , "s16le" ,
353465 "-ac" , "1" ,
354466 "-acodec" , "pcm_s16le" ,
@@ -357,7 +469,7 @@ def _load_audio_ffmpeg(file: str, sr: int = 16000):
357469 ]
358470 # fmt: on
359471 try :
360- out = run (cmd , capture_output = True , check = True ).stdout
472+ out = run (cmd , input = stdin_data , capture_output = True , check = True ).stdout
361473 except CalledProcessError as e :
362474 raise RuntimeError (f"Failed to load audio: { e .stderr .decode ()} " ) from e
363475
0 commit comments