Coverage for src/audioio/audioloader.py: 83%
948 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-01 19:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-01 19:42 +0000
1"""Loading data, metadata, and markers from audio files.
3- `load_audio()`: load a whole audio file at once.
4- `metadata()`: read metadata of an audio file.
5- `markers()`: read markers of an audio file.
6- class `AudioLoader`: read data from audio files in chunks.
8The read in data are always numpy arrays of floats ranging between -1 and 1.
9The arrays are 2-D ndarrays with first axis time and second axis channel,
10even for single channel data.
12If an audio file cannot be loaded, you might need to install
13additional packages. See
14[installation](https://bendalab.github.io/audioio/installation) for
15further instructions.
17For a demo run the module as:
18```
19python -m src.audioio.audioloader audiofile.wav
20```
21"""
23import os
24import gc
25import sys
26import ctypes
27import warnings
28import numpy as np
30from io import BytesIO
31from pathlib import Path
32from datetime import timedelta
34from .audiomodules import *
35from .bufferedarray import BufferedArray
36from .riffmetadata import metadata_riff, markers_riff
37from .audiometadata import update_gain, add_unwrap, get_datetime
38from .audiometadata import flatten_metadata, add_metadata, set_starttime
39from .audiotools import unwrap
42def load_wave(filepath):
43 """Load wav file using the wave module from pythons standard libray.
45 Documentation
46 -------------
47 https://docs.python.org/3.8/library/wave.html
49 Parameters
50 ----------
51 filepath: str or Path
52 The full path and name of the file to load.
54 Returns
55 -------
56 data: ndarray
57 All data traces as an 2-D ndarray, first dimension is time, second is channel
58 rate: float
59 The sampling rate of the data in Hertz.
61 Raises
62 ------
63 ImportError
64 The wave module is not installed
65 *
66 Loading of the data failed
67 """
68 if not audio_modules['wave']:
69 raise ImportError
71 wf = wave.open(os.fspath(filepath), 'r') # 'with' is not supported by wave
72 (nchannels, sampwidth, rate, nframes, comptype, compname) = wf.getparams()
73 buffer = wf.readframes(nframes)
74 factor = 2.0**(sampwidth*8-1)
75 if sampwidth == 1:
76 dtype = 'u1'
77 buffer = np.frombuffer(buffer, dtype=dtype).reshape(-1, nchannels)
78 data = buffer.astype('d')/factor - 1.0
79 else:
80 dtype = f'i{sampwidth}'
81 buffer = np.frombuffer(buffer, dtype=dtype).reshape(-1, nchannels)
82 data = buffer.astype('d')/factor
83 wf.close()
84 return data, float(rate)
87def load_ewave(filepath):
88 """Load wav file using ewave module.
90 Documentation
91 -------------
92 https://github.com/melizalab/py-ewave
94 Parameters
95 ----------
96 filepath: str or Path
97 The full path and name of the file to load.
99 Returns
100 -------
101 data: ndarray
102 All data traces as an 2-D ndarray, first dimension is time, second is channel.
103 rate: float
104 The sampling rate of the data in Hertz.
106 Raises
107 ------
108 ImportError
109 The ewave module is not installed
110 *
111 Loading of the data failed
112 """
113 if not audio_modules['ewave']:
114 raise ImportError
116 data = np.array([])
117 rate = 0.0
118 with ewave.open(os.fspath(filepath), 'r') as wf:
119 rate = wf.sampling_rate
120 buffer = wf.read()
121 data = ewave.rescale(buffer, 'float')
122 if len(data.shape) == 1:
123 data = np.reshape(data,(-1, 1))
124 return data, float(rate)
127def load_wavfile(filepath):
128 """Load wav file using scipy.io.wavfile.
130 Documentation
131 -------------
132 http://docs.scipy.org/doc/scipy/reference/io.html
133 Does not support blocked read.
135 Parameters
136 ----------
137 filepath: str or Path
138 The full path and name of the file to load.
140 Returns
141 -------
142 data: ndarray
143 All data traces as an 2-D ndarray, first dimension is time, second is channel.
144 rate: float
145 The sampling rate of the data in Hertz.
147 Raises
148 ------
149 ImportError
150 The scipy.io module is not installed
151 *
152 Loading of the data failed
153 """
154 if not audio_modules['scipy.io.wavfile']:
155 raise ImportError
157 warnings.filterwarnings("ignore")
158 rate, data = wavfile.read(filepath)
159 warnings.filterwarnings("always")
160 if data.dtype == np.uint8:
161 data = data / 128.0 - 1.0
162 elif np.issubdtype(data.dtype, np.signedinteger):
163 data = data / (2.0**(data.dtype.itemsize*8-1))
164 else:
165 data = data.astype(np.float64, copy=False)
166 if len(data.shape) == 1:
167 data = np.reshape(data,(-1, 1))
168 return data, float(rate)
171def load_soundfile(filepath):
172 """Load audio file using SoundFile (based on libsndfile).
174 Documentation
175 -------------
176 http://pysoundfile.readthedocs.org
177 http://www.mega-nerd.com/libsndfile
179 Parameters
180 ----------
181 filepath: str or Path
182 The full path and name of the file to load.
184 Returns
185 -------
186 data: ndarray
187 All data traces as an 2-D ndarray, first dimension is time, second is channel.
188 rate: float
189 The sampling rate of the data in Hertz.
191 Raises
192 ------
193 ImportError
194 The soundfile module is not installed.
195 *
196 Loading of the data failed.
197 """
198 if not audio_modules['soundfile']:
199 raise ImportError
201 data = np.array([])
202 rate = 0.0
203 with soundfile.SoundFile(filepath, 'r') as sf:
204 rate = sf.samplerate
205 data = sf.read(frames=-1, dtype='float64', always_2d=True)
206 return data, float(rate)
209def load_wavefile(filepath):
210 """Load audio file using wavefile (based on libsndfile).
212 Documentation
213 -------------
214 https://github.com/vokimon/python-wavefile
216 Parameters
217 ----------
218 filepath: str or Path
219 The full path and name of the file to load.
221 Returns
222 -------
223 data: ndarray
224 All data traces as an 2-D ndarray, first dimension is time, second is channel.
225 rate: float
226 The sampling rate of the data in Hertz.
228 Raises
229 ------
230 ImportError
231 The wavefile module is not installed.
232 *
233 Loading of the data failed.
234 """
235 if not audio_modules['wavefile']:
236 raise ImportError
238 rate, data = wavefile.load(os.fspath(filepath))
239 return data.astype(np.float64, copy=False).T, float(rate)
242def load_audioread(filepath):
243 """Load audio file using audioread.
245 Documentation
246 -------------
247 https://github.com/beetbox/audioread
249 Parameters
250 ----------
251 filepath: str or Path
252 The full path and name of the file to load.
254 Returns
255 -------
256 data: ndarray
257 All data traces as an 2-D ndarray, first dimension is time, second is channel.
258 rate: float
259 The sampling rate of the data in Hertz.
261 Raises
262 ------
263 ImportError
264 The audioread module is not installed.
265 *
266 Loading of the data failed.
267 """
268 if not audio_modules['audioread']:
269 raise ImportError
271 data = np.array([])
272 rate = 0.0
273 with audioread.audio_open(filepath) as af:
274 rate = af.samplerate
275 data = np.zeros((int(np.ceil(af.samplerate*af.duration)), af.channels),
276 dtype="<i2")
277 index = 0
278 for buffer in af:
279 fulldata = np.frombuffer(buffer, dtype='<i2').reshape(-1, af.channels)
280 n = fulldata.shape[0]
281 if index + n > len(data):
282 n = len(fulldata) - index
283 if n <= 0:
284 break
285 data[index:index+n,:] = fulldata[:n,:]
286 index += n
287 return data/(2.0**15-1.0), float(rate)
290wavpack_wrapper = 0x04
291""" Flag for WavpackOpenFileInput() to also read the header of the original file. """
293wavpack_float = 0x08
294""" Bit returned by WavpackGetMode() indicating floating point samples. """
297def open_wavpack_file(filepath, flags=0):
298 """Open a WavPack file using the wavpack library.
300 Parameters
301 ----------
302 filepath: str or Path
303 The full path and name of the file to open.
304 flags: int
305 Flags passed on to WavpackOpenFileInput().
307 Returns
308 -------
309 wpc: ctypes.c_void_p
310 Handle of the open WavPack file.
312 Raises
313 ------
314 ImportError
315 The wavpack library is not installed
316 IOError
317 Opening the file failed
318 """
319 if not audio_modules['wavpack']:
320 raise ImportError
322 error = ctypes.create_string_buffer(160)
323 wpc = wavpack.WavpackOpenFileInput(os.fsencode(filepath), error, flags, 0)
324 if not wpc:
325 raise IOError(f'{filepath}: {error.value.decode("latin-1")}')
326 return ctypes.c_void_p(wpc)
329def unpack_wavpack(wpc, nframes, channels):
330 """Read samples from an open WavPack file.
332 Parameters
333 ----------
334 wpc: ctypes.c_void_p
335 Handle of an open WavPack file.
336 nframes: int
337 Number of frames to be read.
338 channels: int
339 Number of channels of the file.
341 Returns
342 -------
343 buffer: 2-D ndarray of int32
344 The samples as signed integers, right justified. At the end of
345 the file this can be less than `nframes` frames.
346 """
347 buffer = np.zeros((nframes, channels), dtype=np.int32)
348 n = 0
349 while n < nframes:
350 p = buffer[n:,:].ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
351 m = wavpack.WavpackUnpackSamples(wpc, p, nframes - n)
352 if m == 0: # end of file
353 break
354 n += m
355 return buffer[:n,:]
358def riff_wavpack(filepath):
359 """Read the header and trailer of the original file from a WavPack file.
361 The wavpack program stores everything of the file it compresses
362 that is not a sample, so that metadata and markers of the original
363 wave file are still available. The samples are not part of the
364 returned stream, the size of its data chunk is set to zero.
366 Parameters
367 ----------
368 filepath: str or Path
369 The full path and name of the WavPack file.
371 Returns
372 -------
373 sf: BytesIO or None
374 Header and trailer of the original file as a stream,
375 None if the WavPack file does not contain a RIFF header.
376 """
377 try:
378 wpc = open_wavpack_file(filepath, wavpack_wrapper)
379 except (ImportError, IOError, TypeError):
380 return None
381 nheader = wavpack.WavpackGetWrapperBytes(wpc)
382 frames = wavpack.WavpackGetNumSamples64(wpc)
383 if frames > 1:
384 # the trailer is added to the wrapper only after unpacking
385 # ran into the end of the file:
386 wavpack.WavpackSeekSample64(wpc, frames - 1)
387 unpack_wavpack(wpc, 2, wavpack.WavpackGetNumChannels(wpc))
388 n = wavpack.WavpackGetWrapperBytes(wpc)
389 riff = ctypes.string_at(wavpack.WavpackGetWrapperData(wpc), n) if n > 0 else b''
390 wavpack.WavpackCloseFile(wpc)
391 if riff[:4] != b'RIFF':
392 return None
393 header = riff[:nheader]
394 trailer = riff[nheader:]
395 if header[-8:-4] == b'data':
396 header = header[:-4] + bytes(4) # no samples in this data chunk
397 riff = header + trailer
398 riff = riff[:4] + (len(riff) - 8).to_bytes(4, 'little') + riff[8:]
399 return BytesIO(riff)
402def load_wavpack(filepath):
403 """Load WavPack file using the wavpack library.
405 Documentation
406 -------------
407 https://www.wavpack.com
409 Parameters
410 ----------
411 filepath: str or Path
412 The full path and name of the file to load.
414 Returns
415 -------
416 data: ndarray
417 All data traces as an 2-D ndarray, first dimension is time, second is channel
418 rate: float
419 The sampling rate of the data in Hertz.
421 Raises
422 ------
423 ImportError
424 The wavpack library is not installed
425 *
426 Loading of the data failed
427 """
428 if not audio_modules['wavpack']:
429 raise ImportError
431 wpc = open_wavpack_file(filepath)
432 rate = wavpack.WavpackGetSampleRate(wpc)
433 channels = wavpack.WavpackGetNumChannels(wpc)
434 frames = wavpack.WavpackGetNumSamples64(wpc)
435 buffer = unpack_wavpack(wpc, frames, channels)
436 if wavpack.WavpackGetMode(wpc) & wavpack_float > 0:
437 data = buffer.view(np.float32).astype('d')
438 else:
439 bits = wavpack.WavpackGetBitsPerSample(wpc)
440 data = buffer.astype('d')/2.0**(bits-1)
441 wavpack.WavpackCloseFile(wpc)
442 return data, float(rate)
445audio_loader_funcs = (
446 ('soundfile', load_soundfile),
447 ('wave', load_wave),
448 ('wavefile', load_wavefile),
449 ('wavpack', load_wavpack),
450 ('ewave', load_ewave),
451 ('scipy.io.wavfile', load_wavfile),
452 ('audioread', load_audioread),
453 )
454"""List of implemented load() functions.
456Each element of the list is a tuple with the module's name and its
457load() function.
459"""
462def load_audio(filepath, verbose=0):
463 """Call this function to load all channels of audio data from a file.
465 This function tries different python modules to load the audio file.
467 Parameters
468 ----------
469 filepath: str or Path
470 The full path and name of the file to load.
471 verbose: int
472 If larger than zero show detailed error/warning messages.
474 Returns
475 -------
476 data: ndarray
477 All data traces as an 2-D ndarray, even for single channel data.
478 First dimension is time, second is channel.
479 Data values range maximally between -1 and 1.
480 rate: float
481 The sampling rate of the data in Hertz.
483 Raises
484 ------
485 FileNotFoundError
486 `filepath` is not an existing file.
487 EOFError
488 File size of `filepath` is zero.
489 IOError
490 Failed to load data.
492 Examples
493 --------
494 ```
495 import matplotlib.pyplot as plt
496 from audioio import load_audio
498 data, rate = load_audio('some/audio.wav')
499 plt.plot(np.arange(len(data))/rate, data[:,0])
500 plt.show()
501 ```
502 """
503 # check values:
504 filepath = Path(filepath)
505 if not filepath.is_file:
506 raise FileNotFoundError(f'file "{filepath}" not found')
507 if filepath.stat().st_size <= 0:
508 raise EOFError(f'file "{filepath}" is empty (size=0)!')
510 # load an audio file by trying various modules:
511 not_installed = []
512 errors = [f'failed to load data from file "{filepath}":']
513 for lib, load_file in audio_loader_funcs:
514 if not audio_modules[lib]:
515 if verbose > 1:
516 print(f'unable to load data from file "{filepath}" using {lib} module: module not available')
517 not_installed.append(lib)
518 continue
519 try:
520 data, rate = load_file(filepath)
521 if len(data) > 0:
522 if verbose > 0:
523 print(f'loaded data from file "{filepath}" using {lib} module')
524 if verbose > 1:
525 print(f' sampling rate: {rate:g} Hz')
526 print(f' channels : {data.shape[1]}')
527 print(f' frames : {len(data)}')
528 return data, rate
529 except Exception as e:
530 errors.append(f' {lib} failed: {str(e)}')
531 if verbose > 1:
532 print(errors[-1])
533 if len(not_installed) > 0:
534 errors.append('\n You may need to install one of the ' + \
535 ', '.join(not_installed) + ' packages.')
536 raise IOError('\n'.join(errors))
537 return np.zeros(0), 0.0
540def metadata(filepath, store_empty=False):
541 """Read metadata of an audio file.
543 Parameters
544 ----------
545 filepath: str or file handle
546 The audio file from which to read metadata.
547 store_empty: bool
548 If `False` do not return meta data with empty values.
550 Returns
551 -------
552 meta_data: nested dict
553 Meta data contained in the audio file. Keys of the nested
554 dictionaries are always strings. If the corresponding values
555 are dictionaries, then the key is the section name of the
556 metadata contained in the dictionary. All other types of
557 values are values for the respective key. In particular they
558 are strings. But other types like for example ints or floats
559 are also allowed. See `audioio.audiometadata` module for
560 available functions to work with such metadata.
562 Raises
563 ------
564 ValueError
565 Not a RIFF file.
567 Examples
568 --------
569 ```
570 from audioio import metadata, print_metadata
571 md = metadata('data.wav')
572 print_metadata(md)
573 ```
575 """
576 try:
577 return metadata_riff(filepath, store_empty)
578 except ValueError: # not a RIFF file, but maybe a WavPack file
579 sf = riff_wavpack(filepath)
580 if sf is None:
581 return {}
582 return metadata_riff(sf, store_empty)
585def markers(filepath):
586 """ Read markers of an audio file.
588 See `audioio.audiomarkers` module for available functions
589 to work with markers.
591 Parameters
592 ----------
593 filepath: str or file handle
594 The audio file.
596 Returns
597 -------
598 locs: 2-D ndarray of int
599 Marker positions (first column) and spans (second column)
600 for each marker (rows).
601 labels: 2-D ndarray of string objects
602 Labels (first column) and texts (second column)
603 for each marker (rows).
605 Raises
606 ------
607 ValueError
608 Not a RIFF file.
610 Examples
611 --------
612 ```
613 from audioio import markers, print_markers
614 locs, labels = markers('data.wav')
615 print_markers(locs, labels)
616 ```
617 """
618 try:
619 return markers_riff(filepath)
620 except ValueError: # not a RIFF file, but maybe a WavPack file
621 sf = riff_wavpack(filepath)
622 if sf is None:
623 return np.zeros((0, 2), dtype=int), np.zeros((0, 2), dtype=object)
624 return markers_riff(sf)
627class AudioLoader(BufferedArray):
628 """Buffered reading of audio data for random access of the data in the file.
630 The class allows for reading very large audio files or many
631 sequential audio files that do not fit into memory.
632 An AudioLoader instance can be used like a huge read-only numpy array, i.e.
633 ```
634 data = AudioLoader('path/to/audio/file.wav')
635 x = data[10000:20000,0]
636 ```
637 The first index specifies the frame, the second one the channel.
639 Behind the scenes, `AudioLoader` tries to open the audio file with
640 all available audio modules until it succeeds (first line). It
641 then reads data from the file as necessary for the requested data
642 (second line). Accesing the content of the audio files via a
643 buffer that holds only a part of the data is managed by the
644 `BufferedArray` class.
646 Reading sequentially through the file is always possible. Some
647 modules, however, (e.g. audioread, needed for mp3 files) can only
648 read forward. If previous data are requested, then the file is read
649 from the beginning again. This slows down access to previous data
650 considerably. Use the `backsize` argument of the open function to
651 make sure some data are loaded into the buffer before the requested
652 frame. Then a subsequent access to the data within `backsize` seconds
653 before that frame can still be handled without the need to reread
654 the file from the beginning.
656 Usage
657 -----
658 With context management:
659 ```
660 import audioio as aio
661 with aio.AudioLoader(filepath, 60.0, 10.0) as data:
662 # do something with the content of the file:
663 x = data[0:10000]
664 y = data[10000:20000]
665 z = x + y
666 ```
668 For using a specific audio module, here the audioread module:
669 ```
670 data = aio.AudioLoader()
671 with data.open_audioread(filepath, 60.0, 10.0):
672 # do something ...
673 ```
675 Use `blocks()` for sequential, blockwise reading and processing:
676 ```
677 from scipy.signal import spectrogram
678 nfft = 2048
679 with aio.AudioLoader('some/audio.wav') as data:
680 for x in data.blocks(100*nfft, nfft//2):
681 f, t, Sxx = spectrogram(x, fs=data.rate,
682 nperseg=nfft, noverlap=nfft//2)
683 ```
685 For loop iterates over single frames (1-D arrays containing samples for each channel):
686 ```
687 with aio.AudioLoader('some/audio.wav') as data:
688 for x in data:
689 print(x)
690 ```
692 Traditional open and close:
693 ```
694 data = aio.AudioLoader(filepath, 60.0)
695 x = data[:,:] # read the whole file
696 data.close()
697 ```
699 this is the same as:
700 ```
701 data = aio.AudioLoader()
702 data.open(filepath, 60.0)
703 ...
704 ```
706 Classes inheriting AudioLoader just need to implement
707 ```
708 self.load_audio_buffer(offset, nsamples, pbuffer)
709 ```
710 This function needs to load the supplied `pbuffer` with
711 `nframes` frames of data starting at frame `offset`.
713 In the constructor or some kind of opening function, you need to
714 set some member variables, as described for `BufferedArray`.
716 For loading metadata and markers, implement the functions
717 ```
718 self._load_metadata(filepath, **kwargs)
719 self._load_markers(filepath)
720 ```
722 Parameters
723 ----------
724 filepath: str or Path or list of str of list of Path
725 Name of the file or list of many file names that should be
726 made accessible as a single array.
727 buffersize: float
728 Size of internal buffer in seconds.
729 backsize: float
730 Part of the buffer to be loaded before the requested start index in seconds.
731 verbose: int
732 If larger than zero show detailed error/warning messages.
733 store_empty: bool
734 If `False` do not return meta data with empty values.
735 meta_kwargs: dict
736 Keyword arguments that are passed on to the _load_metadata()
737 function. For audio data the only recognized key is
738 `store_empty` - see the metadata() function for more infos.
739 **kwargs: dict
740 Further keyword arguments that are passed on to the
741 specific open() functions.
743 Attributes
744 ----------
745 filepath: Path
746 Full path of the opened file. In case of many files, the first one.
747 file_paths: list of Path
748 List of pathes of the opened files that are made accessible
749 as a single array.
750 file_indices: list of int
751 For each file the index of its first sample.
752 rate: float
753 The sampling rate of the data in seconds.
754 channels: int
755 The number of channels.
756 frames: int
757 The number of frames in the file. Same as `len()`.
758 format: str or None
759 Format of the audio file.
760 encoding: str or None
761 Encoding/subtype of the audio file.
762 shape: tuple
763 Frames and channels of the data.
764 ndim: int
765 Number of dimensions: always 2 (frames and channels).
766 offset: int
767 Index of first frame in the current buffer.
768 buffer: ndarray of floats
769 The curently available data from the file.
770 ampl_min: float
771 Minimum amplitude the file format supports.
772 Always -1.0 for audio data.
773 ampl_max: float
774 Maximum amplitude the file format supports.
775 Always +1.0 for audio data.
777 Methods
778 -------
779 - `len()`: Number of frames.
780 - `file_start_times()`: time of first frame of each file in seconds.
781 - `get_file_index()`: file path and index of frame contained by this file.
782 - `open()`: Open an audio file by trying available audio modules.
783 - `open_*()`: Open an audio file with the respective audio module.
784 - `__getitem__`: Access data of the audio file.
785 - `update_buffer()`: Update the internal buffer for a range of frames.
786 - `blocks()`: Generator for blockwise processing of AudioLoader data.
787 - `file_start_times()`: Time of first frame of each file in seconds.
788 - `get_file_index()`: File path and index of frame contained by this file.
789 - `basename()`: Base name of the audio data.
790 - `format_dict()`: technical infos about how the data are stored.
791 - `metadata()`: Metadata stored along with the audio data.
792 - `markers()`: Markers stored along with the audio data.
793 - `set_unwrap()`: Set parameters for unwrapping clipped data.
794 - `set_time_delta()`: Set maximum allowed time difference between successive files.
795 - `close()`: Close the file.
797 """
799 max_open_files = 5
800 """ Suggestion for maximum number of open file descriptors. """
802 max_open_loaders = 10
803 """ Suggestion for maximum number of AudioLoaders when opening multiple files. """
805 def __init__(self, filepath=None, buffersize=10.0, backsize=0.0,
806 verbose=0, meta_kwargs={}, **kwargs):
807 super().__init__(verbose=verbose)
808 self.format = None
809 self.encoding = None
810 self._metadata = None
811 self._locs = None
812 self._labels = None
813 self._load_metadata = metadata
814 self._load_markers = markers
815 self._metadata_kwargs = meta_kwargs
816 self.filepath = None
817 self.file_paths = None
818 self.file_indices = []
819 self._max_time_diff = 1
820 self.sf = None
821 self.close = self._close
822 self.load_buffer = self._load_buffer_unwrap
823 self.ampl_min = -1.0
824 self.ampl_max = +1.0
825 self.unwrap = False
826 self.unwrap_thresh = 0.0
827 self.unwrap_clips = False
828 self.unwrap_ampl = 1.0
829 self.unwrap_downscale = True
830 if filepath is not None:
831 self.open(filepath, buffersize, backsize, verbose, **kwargs)
833 numpy_encodings = {np.dtype(np.int64): 'PCM_64',
834 np.dtype(np.int32): 'PCM_32',
835 np.dtype(np.int16): 'PCM_16',
836 np.dtype(np.single): 'FLOAT',
837 np.dtype(np.double): 'DOUBLE',
838 np.dtype('>f4'): 'FLOAT',
839 np.dtype('>f8'): 'DOUBLE'}
840 """ Map numpy dtypes to encodings.
841 """
843 def _close(self):
844 pass
846 def __del__(self):
847 self.close()
849 def file_start_times(self):
850 """ Time of first frame of each file in seconds.
852 Returns
853 -------
854 times: array of float
855 Time of the first frame of each file relative to buffer start
856 in seconds.
857 """
858 times = []
859 for idx in self.file_indices:
860 times.append(idx/self.rate)
861 return np.array(times)
863 def get_file_index(self, frame):
864 """ File path and index of frame contained by this file.
866 Parameters
867 ----------
868 frame: int
869 Index of frame.
871 Returns
872 -------
873 filepath: Path
874 Path of file that contains the frame.
875 index: int
876 Index of the frame relative to the first frame
877 in the containing file.
879 Raises
880 ------
881 ValueError
882 Invalid frame index.
883 """
884 if frame < 0 or frame >= self.frames:
885 raise ValueError('invalid frame')
886 fname = self.file_paths[0]
887 index = self.file_indices[0]
888 for i in reversed(range(len(self.file_indices))):
889 if self.file_indices[i] <= frame:
890 fname = self.file_paths[i]
891 index = self.file_indices[i]
892 break
893 return fname, frame - index
895 def basename(self, path=None):
896 """ Base name of the audio data.
898 Parameters
899 ----------
900 path: str or Path or None
901 Path of the audio file from which a base name is generated.
902 If `None`, use `self.filepath`.
904 Returns
905 -------
906 s: str
907 The name. Defaults to the stem of `path`.
909 """
910 if path is None:
911 path = self.filepath
912 return Path(path).stem
914 def format_dict(self):
915 """ Technical infos about how the data are stored in the file.
917 Returns
918 -------
919 fmt: dict
920 Dictionary with filepath, format, encoding, samplingrate,
921 channels, frames, and duration of the audio file as strings.
923 """
924 fmt = dict(name=self.basename(), filepath=os.fsdecode(self.filepath))
925 if self.format is not None:
926 fmt['format'] = self.format
927 if self.encoding is not None:
928 fmt['encoding'] = self.encoding
929 fmt.update(dict(samplingrate=f'{self.rate:.0f}Hz',
930 channels=self.channels,
931 frames=self.frames,
932 duration=f'{self.frames/self.rate:.3f}s'))
933 return fmt
935 def metadata(self):
936 """Metadata of the audio file.
938 Parameters
939 ----------
940 store_empty: bool
941 If `False` do not add meta data with empty values.
943 Returns
944 -------
945 meta_data: nested dict
947 Meta data contained in the audio file. Keys of the nested
948 dictionaries are always strings. If the corresponding
949 values are dictionaries, then the key is the section name
950 of the metadata contained in the dictionary. All other
951 types of values are values for the respective key. In
952 particular they are strings. But other types like for
953 example ints or floats are also allowed. See
954 `audioio.audiometadata` module for available functions to
955 work with such metadata.
957 """
958 if self._metadata is None:
959 if self._load_metadata is None:
960 self._metadata = {}
961 else:
962 self._metadata = self._load_metadata(self.filepath,
963 **self._metadata_kwargs)
964 return self._metadata
966 def markers(self):
967 """Read markers of the audio file.
969 See `audioio.audiomarkers` module for available functions
970 to work with markers.
972 Returns
973 -------
974 locs: 2-D ndarray of int
975 Marker positions (first column) and spans (second column)
976 for each marker (rows).
977 labels: 2-D ndarray of str objects
978 Labels (first column) and texts (second column)
979 for each marker (rows).
980 """
981 if self._locs is None:
982 if self._load_markers is None:
983 self._locs = np.zeros((0, 2), dtype=int)
984 self._labels = np.zeros((0, 2), dtype=object)
985 else:
986 self._locs, self._labels = self._load_markers(self.filepath)
987 return self._locs, self._labels
989 def set_unwrap(self, thresh, clips=False, down_scale=True, unit=''):
990 """Set parameters for unwrapping clipped data.
992 See unwrap() function from the audioio package.
994 Parameters
995 ----------
996 thresh: float
997 Threshold for detecting wrapped data relative to self.unwrap_ampl
998 which is initially set to self.ampl_max.
999 If zero, do not unwrap.
1000 clips: bool
1001 If True, then clip the unwrapped data properly.
1002 Otherwise, unwrap the data and double the
1003 minimum and maximum data range
1004 (self.ampl_min and self.ampl_max).
1005 down_scale: bool
1006 If not `clips`, then downscale the signal by a factor of two,
1007 in order to keep the range between -1 and 1.
1008 unit: str
1009 Unit of the data.
1010 """
1011 self.unwrap_ampl = self.ampl_max
1012 self.unwrap_thresh = thresh
1013 self.unwrap_clips = clips
1014 self.unwrap_down_scale = down_scale
1015 self.unwrap = thresh > 1e-3
1016 if self.unwrap:
1017 if self.unwrap_clips:
1018 add_unwrap(self.metadata(),
1019 self.unwrap_thresh*self.unwrap_ampl,
1020 self.unwrap_ampl, unit)
1021 elif down_scale:
1022 update_gain(self.metadata(), 0.5)
1023 add_unwrap(self.metadata(),
1024 0.5*self.unwrap_thresh*self.unwrap_ampl,
1025 0.0, unit)
1026 else:
1027 self.ampl_min *= 2
1028 self.ampl_max *= 2
1029 add_unwrap(self.metadata(),
1030 self.unwrap_thresh*self.unwrap_ampl,
1031 0.0, unit)
1033 def _load_buffer_unwrap(self, r_offset, r_size, pbuffer):
1034 """Load new data and unwrap it.
1036 Parameters
1037 ----------
1038 r_offset: int
1039 First frame to be read from file.
1040 r_size: int
1041 Number of frames to be read from file.
1042 pbuffer: ndarray
1043 Buffer where to store the loaded data.
1044 """
1045 self.load_audio_buffer(r_offset, r_size, pbuffer)
1046 if self.unwrap:
1047 # TODO: handle edge effects!
1048 unwrap(pbuffer, self.unwrap_thresh, self.unwrap_ampl)
1049 if self.unwrap_clips:
1050 pbuffer[pbuffer > self.ampl_max] = self.ampl_max
1051 pbuffer[pbuffer < self.ampl_min] = self.ampl_min
1052 elif self.unwrap_down_scale:
1053 pbuffer *= 0.5
1055 def set_time_delta(time_delta):
1056 """ Set maximum allowed time difference between successive files.
1058 Parameters
1059 ----------
1060 time_delta: int
1061 Maximum number of seconds the start time of a recording file is allowed
1062 to differ from the end of the previous file.
1063 Default is one second.
1064 """
1065 self._max_time_diff = time_delta
1067 # wave interface:
1068 def open_wave(self, filepath, buffersize=10.0, backsize=0.0,
1069 verbose=0):
1070 """Open audio file for reading using the wave module.
1072 Note: we assume that setpos() and tell() use integer numbers!
1074 Parameters
1075 ----------
1076 filepath: str or Path
1077 Name of the file.
1078 buffersize: float
1079 Size of internal buffer in seconds.
1080 backsize: float
1081 Part of the buffer to be loaded before the requested start index in seconds.
1082 verbose: int
1083 If larger than zero show detailed error/warning messages.
1085 Raises
1086 ------
1087 ImportError
1088 The wave module is not installed
1089 """
1090 self.verbose = verbose
1091 if self.verbose > 0:
1092 print(f'open_wave(filepath) with filepath={filepath}')
1093 if not audio_modules['wave']:
1094 self.rate = 0.0
1095 self.channels = 0
1096 self.frames = 0
1097 self.size = 0
1098 self.shape = (0, 0)
1099 self.offset = 0
1100 raise ImportError
1101 if self.sf is not None:
1102 self._close_wave()
1103 self.sf = wave.open(os.fspath(filepath), 'r')
1104 self.filepath = Path(filepath)
1105 self.file_paths = [self.filepath]
1106 self.file_indices = [0]
1107 self.rate = float(self.sf.getframerate())
1108 self.format = 'WAV'
1109 sampwidth = self.sf.getsampwidth()
1110 if sampwidth == 1:
1111 self.dtype = 'u1'
1112 self.encoding = 'PCM_U8'
1113 else:
1114 self.dtype = f'i{sampwidth}'
1115 self.encoding = f'PCM_{sampwidth*8}'
1116 self.factor = 1.0/(2.0**(sampwidth*8-1))
1117 self.channels = self.sf.getnchannels()
1118 self.frames = self.sf.getnframes()
1119 self.shape = (self.frames, self.channels)
1120 self.size = self.frames * self.channels
1121 self.bufferframes = int(buffersize*self.rate)
1122 self.backframes = int(backsize*self.rate)
1123 self.init_buffer()
1124 self.close = self._close_wave
1125 self.load_audio_buffer = self._load_buffer_wave
1126 # read 1 frame to determine the unit of the position values:
1127 self.p0 = self.sf.tell()
1128 self.sf.readframes(1)
1129 self.pfac = self.sf.tell() - self.p0
1130 self.sf.setpos(self.p0)
1131 return self
1133 def _close_wave(self):
1134 """Close the audio file using the wave module. """
1135 if self.sf is not None:
1136 self.sf.close()
1137 self.sf = None
1139 def _load_buffer_wave(self, r_offset, r_size, buffer):
1140 """Load new data from file using the wave module.
1142 Parameters
1143 ----------
1144 r_offset: int
1145 First frame to be read from file.
1146 r_size: int
1147 Number of frames to be read from file.
1148 buffer: ndarray
1149 Buffer where to store the loaded data.
1150 """
1151 if self.sf is None:
1152 self.sf = wave.open(os.fspath(self.filepath), 'r')
1153 self.sf.setpos(r_offset*self.pfac + self.p0)
1154 fbuffer = self.sf.readframes(r_size)
1155 fbuffer = np.frombuffer(fbuffer, dtype=self.dtype).reshape((-1, self.channels))
1156 if self.dtype[0] == 'u':
1157 buffer[:, :] = fbuffer * self.factor - 1.0
1158 else:
1159 buffer[:, :] = fbuffer * self.factor
1162 # ewave interface:
1163 def open_ewave(self, filepath, buffersize=10.0, backsize=0.0,
1164 verbose=0):
1165 """Open audio file for reading using the ewave module.
1167 Parameters
1168 ----------
1169 filepath: str or Path
1170 Name of the file.
1171 buffersize: float
1172 Size of internal buffer in seconds.
1173 backsize: float
1174 Part of the buffer to be loaded before the requested start index in seconds.
1175 verbose: int
1176 If larger than zero show detailed error/warning messages.
1178 Raises
1179 ------
1180 ImportError
1181 The ewave module is not installed.
1182 """
1183 self.verbose = verbose
1184 if self.verbose > 0:
1185 print(f'open_ewave(filepath) with filepath={filepath}')
1186 if not audio_modules['ewave']:
1187 self.rate = 0.0
1188 self.channels = 0
1189 self.frames = 0
1190 self.shape = (0, 0)
1191 self.size = 0
1192 self.offset = 0
1193 raise ImportError
1194 if self.sf is not None:
1195 self._close_ewave()
1196 self.sf = ewave.open(os.fspath(filepath), 'r')
1197 self.filepath = Path(filepath)
1198 self.file_paths = [self.filepath]
1199 self.file_indices = [0]
1200 self.rate = float(self.sf.sampling_rate)
1201 self.channels = self.sf.nchannels
1202 self.frames = self.sf.nframes
1203 self.shape = (self.frames, self.channels)
1204 self.size = self.frames * self.channels
1205 self.format = 'WAV' # or WAVEX?
1206 self.encoding = self.numpy_encodings[self.sf.dtype]
1207 self.bufferframes = int(buffersize*self.rate)
1208 self.backframes = int(backsize*self.rate)
1209 self.init_buffer()
1210 self.close = self._close_ewave
1211 self.load_audio_buffer = self._load_buffer_ewave
1212 return self
1214 def _close_ewave(self):
1215 """Close the audio file using the ewave module. """
1216 if self.sf is not None:
1217 del self.sf
1218 self.sf = None
1220 def _load_buffer_ewave(self, r_offset, r_size, buffer):
1221 """Load new data from file using the ewave module.
1223 Parameters
1224 ----------
1225 r_offset: int
1226 First frame to be read from file.
1227 r_size: int
1228 Number of frames to be read from file.
1229 buffer: ndarray
1230 Buffer where to store the loaded data.
1231 """
1232 if self.sf is None:
1233 self.sf = ewave.open(os.fspath(self.filepath), 'r')
1234 fbuffer = self.sf.read(frames=r_size, offset=r_offset, memmap='r')
1235 fbuffer = ewave.rescale(fbuffer, 'float')
1236 if len(fbuffer.shape) == 1:
1237 fbuffer = np.reshape(fbuffer,(-1, 1))
1238 buffer[:,:] = fbuffer
1241 # soundfile interface:
1242 def open_soundfile(self, filepath, buffersize=10.0, backsize=0.0,
1243 verbose=0):
1244 """Open audio file for reading using the SoundFile module.
1246 Parameters
1247 ----------
1248 filepath: str or Path
1249 Name of the file.
1250 bufferframes: float
1251 Size of internal buffer in seconds.
1252 backsize: float
1253 Part of the buffer to be loaded before the requested start index in seconds.
1254 verbose: int
1255 If larger than zero show detailed error/warning messages.
1257 Raises
1258 ------
1259 ImportError
1260 The SoundFile module is not installed
1261 """
1262 self.verbose = verbose
1263 if self.verbose > 0:
1264 print(f'open_soundfile(filepath) with filepath={filepath}')
1265 if not audio_modules['soundfile']:
1266 self.rate = 0.0
1267 self.channels = 0
1268 self.frames = 0
1269 self.shape = (0, 0)
1270 self.size = 0
1271 self.offset = 0
1272 raise ImportError
1273 if self.sf is not None:
1274 self._close_soundfile()
1275 self.sf = soundfile.SoundFile(filepath, 'r')
1276 self.filepath = Path(filepath)
1277 self.file_paths = [self.filepath]
1278 self.file_indices = [0]
1279 self.rate = float(self.sf.samplerate)
1280 self.channels = self.sf.channels
1281 self.frames = 0
1282 self.size = 0
1283 if self.sf.seekable():
1284 self.frames = self.sf.seek(0, soundfile.SEEK_END)
1285 self.sf.seek(0, soundfile.SEEK_SET)
1286 # TODO: if not seekable, we cannot handle that file!
1287 self.shape = (self.frames, self.channels)
1288 self.size = self.frames * self.channels
1289 self.format = self.sf.format
1290 self.encoding = self.sf.subtype
1291 self.bufferframes = int(buffersize*self.rate)
1292 self.backframes = int(backsize*self.rate)
1293 self.init_buffer()
1294 self.close = self._close_soundfile
1295 self.load_audio_buffer = self._load_buffer_soundfile
1296 return self
1298 def _close_soundfile(self):
1299 """Close the audio file using the SoundFile module. """
1300 if self.sf is not None:
1301 self.sf.close()
1302 self.sf = None
1304 def _load_buffer_soundfile(self, r_offset, r_size, buffer):
1305 """Load new data from file using the SoundFile module.
1307 Parameters
1308 ----------
1309 r_offset: int
1310 First frame to be read from file.
1311 r_size: int
1312 Number of frames to be read from file.
1313 buffer: ndarray
1314 Buffer where to store the loaded data.
1315 """
1316 if self.sf is None:
1317 self.sf = soundfile.SoundFile(self.filepath, 'r')
1318 self.sf.seek(r_offset, soundfile.SEEK_SET)
1319 buffer[:, :] = self.sf.read(r_size, always_2d=True)
1322 # wavefile interface:
1323 def open_wavefile(self, filepath, buffersize=10.0, backsize=0.0,
1324 verbose=0):
1325 """Open audio file for reading using the wavefile module.
1327 Parameters
1328 ----------
1329 filepath: str or Path
1330 Name of the file.
1331 bufferframes: float
1332 Size of internal buffer in seconds.
1333 backsize: float
1334 Part of the buffer to be loaded before the requested start index in seconds.
1335 verbose: int
1336 If larger than zero show detailed error/warning messages.
1338 Raises
1339 ------
1340 ImportError
1341 The wavefile module is not installed
1342 """
1343 self.verbose = verbose
1344 if self.verbose > 0:
1345 print(f'open_wavefile(filepath) with filepath={filepath}')
1346 if not audio_modules['wavefile']:
1347 self.rate = 0.0
1348 self.channels = 0
1349 self.frames = 0
1350 self.shape = (0, 0)
1351 self.size = 0
1352 self.offset = 0
1353 raise ImportError
1354 if self.sf is not None:
1355 self._close_wavefile()
1356 self.sf = wavefile.WaveReader(os.fspath(filepath))
1357 self.filepath = Path(filepath)
1358 self.file_paths = [self.filepath]
1359 self.file_indices = [0]
1360 self.rate = float(self.sf.samplerate)
1361 self.channels = self.sf.channels
1362 self.frames = self.sf.frames
1363 self.shape = (self.frames, self.channels)
1364 self.size = self.frames * self.channels
1365 # get format and encoding:
1366 for attr in dir(wavefile.Format):
1367 v = getattr(wavefile.Format, attr)
1368 if isinstance(v, int):
1369 if v & wavefile.Format.TYPEMASK > 0 and \
1370 (self.sf.format & wavefile.Format.TYPEMASK) == v:
1371 self.format = attr
1372 if v & wavefile.Format.SUBMASK > 0 and \
1373 (self.sf.format & wavefile.Format.SUBMASK) == v:
1374 self.encoding = attr
1375 # init buffer:
1376 self.bufferframes = int(buffersize*self.rate)
1377 self.backframes = int(backsize*self.rate)
1378 self.init_buffer()
1379 self.close = self._close_wavefile
1380 self.load_audio_buffer = self._load_buffer_wavefile
1381 return self
1383 def _close_wavefile(self):
1384 """Close the audio file using the wavefile module. """
1385 if self.sf is not None:
1386 self.sf.close()
1387 self.sf = None
1389 def _load_buffer_wavefile(self, r_offset, r_size, buffer):
1390 """Load new data from file using the wavefile module.
1392 Parameters
1393 ----------
1394 r_offset: int
1395 First frame to be read from file.
1396 r_size: int
1397 Number of frames to be read from file.
1398 buffer: ndarray
1399 Buffer where to store the loaded data.
1400 """
1401 if self.sf is None:
1402 self.sf = wavefile.WaveReader(os.fspath(self.filepath))
1403 self.sf.seek(r_offset, wavefile.Seek.SET)
1404 fbuffer = self.sf.buffer(r_size, dtype=self.buffer.dtype)
1405 self.sf.read(fbuffer)
1406 buffer[:,:] = fbuffer.T
1409 # audioread interface:
1410 def open_audioread(self, filepath, buffersize=10.0, backsize=0.0,
1411 verbose=0):
1412 """Open audio file for reading using the audioread module.
1414 Note, that audioread can only read forward, therefore random and
1415 backward access is really slow.
1417 Parameters
1418 ----------
1419 filepath: str or Path
1420 Name of the file.
1421 bufferframes: float
1422 Size of internal buffer in seconds.
1423 backsize: float
1424 Part of the buffer to be loaded before the requested start index in seconds.
1425 verbose: int
1426 If larger than zero show detailed error/warning messages.
1428 Raises
1429 ------
1430 ImportError
1431 The audioread module is not installed
1432 """
1433 self.verbose = verbose
1434 if self.verbose > 0:
1435 print(f'open_audioread(filepath) with filepath={filepath}')
1436 if not audio_modules['audioread']:
1437 self.rate = 0.0
1438 self.channels = 0
1439 self.frames = 0
1440 self.shape = (0, 0)
1441 self.size = 0
1442 self.offset = 0
1443 raise ImportError
1444 if self.sf is not None:
1445 self._close_audioread()
1446 self.sf = audioread.audio_open(filepath)
1447 self.filepath = Path(filepath)
1448 self.file_paths = [self.filepath]
1449 self.file_indices = [0]
1450 self.rate = float(self.sf.samplerate)
1451 self.channels = self.sf.channels
1452 self.frames = int(np.ceil(self.rate*self.sf.duration))
1453 self.shape = (self.frames, self.channels)
1454 self.size = self.frames * self.channels
1455 self.bufferframes = int(buffersize*self.rate)
1456 self.backframes = int(backsize*self.rate)
1457 self.init_buffer()
1458 self.read_buffer = np.zeros((0,0))
1459 self.read_offset = 0
1460 self.close = self._close_audioread
1461 self.load_audio_buffer = self._load_buffer_audioread
1462 self.sf_iter = self.sf.__iter__()
1463 return self
1465 def _close_audioread(self):
1466 """Close the audio file using the audioread module. """
1467 if self.sf is not None:
1468 self.sf.__exit__(None, None, None)
1469 self.sf = None
1471 def _load_buffer_audioread(self, r_offset, r_size, buffer):
1472 """Load new data from file using the audioread module.
1474 audioread can only iterate through a file once and in blocksizes that are
1475 given by audioread. Therefore we keep yet another buffer: `self.read_buffer`
1476 at file offset `self.read_offset` containing whatever audioread returned.
1478 Parameters
1479 ----------
1480 r_offset: int
1481 First frame to be read from file.
1482 r_size: int
1483 Number of frames to be read from file.
1484 buffer: ndarray
1485 Buffer where to store the loaded data.
1486 """
1487 if self.sf is None:
1488 self.sf = audioread.audio_open(self.filepath)
1489 b_offset = 0
1490 if ( self.read_offset + self.read_buffer.shape[0] >= r_offset + r_size
1491 and self.read_offset < r_offset + r_size ):
1492 # read_buffer overlaps at the end of the requested interval:
1493 i = 0
1494 n = r_offset + r_size - self.read_offset
1495 if n > r_size:
1496 i += n - r_size
1497 n = r_size
1498 buffer[self.read_offset+i-r_offset:self.read_offset+i+n-r_offset,:] = self.read_buffer[i:i+n,:] / (2.0**15-1.0)
1499 if self.verbose > 2:
1500 print(f' recycle {n:6d} frames from the front of the read buffer at {self.read_offset}-{self.read_offset+n} ({self.read_offset-self.offset}-{self.read_offset-self.offset+n} in buffer)')
1501 r_size -= n
1502 if r_size <= 0:
1503 return
1504 # go back to beginning of file:
1505 if r_offset < self.read_offset:
1506 if self.verbose > 2:
1507 print(' rewind')
1508 self._close_audioread()
1509 self.sf = audioread.audio_open(self.filepath)
1510 self.sf_iter = self.sf.__iter__()
1511 self.read_buffer = np.zeros((0,0))
1512 self.read_offset = 0
1513 # read to position:
1514 while self.read_offset + self.read_buffer.shape[0] < r_offset:
1515 self.read_offset += self.read_buffer.shape[0]
1516 try:
1517 if hasattr(self.sf_iter, 'next'):
1518 fbuffer = self.sf_iter.next()
1519 else:
1520 fbuffer = next(self.sf_iter)
1521 except StopIteration:
1522 self.read_buffer = np.zeros((0,0))
1523 buffer[:,:] = 0.0
1524 if self.verbose > 1:
1525 print(f' caught StopIteration, padded buffer with {r_size} zeros')
1526 break
1527 self.read_buffer = np.frombuffer(fbuffer, dtype='<i2').reshape(-1, self.channels)
1528 if self.verbose > 2:
1529 print(f' read forward by {self.read_buffer.shape[0]} frames')
1530 # recycle file data:
1531 if ( self.read_offset + self.read_buffer.shape[0] > r_offset
1532 and self.read_offset <= r_offset ):
1533 i = r_offset - self.read_offset
1534 n = self.read_offset + self.read_buffer.shape[0] - r_offset
1535 if n > r_size:
1536 n = r_size
1537 buffer[:n,:] = self.read_buffer[i:i+n,:] / (2.0**15-1.0)
1538 if self.verbose > 2:
1539 print(f' recycle {n:6d} frames from the end of the read buffer at {self.read_offset}-{self.read_offset + self.read_buffer.shape[0]} to {r_offset}-{r_offset+n} ({r_offset-self.offset}-{r_offset+n-self.offset} in buffer)')
1540 b_offset += n
1541 r_offset += n
1542 r_size -= n
1543 # read data:
1544 if self.verbose > 2 and r_size > 0:
1545 print(f' read {r_size:6d} frames at {r_offset}-{r_offset+r_size} ({r_offset-self.offset}-{r_offset+r_size-self.offset} in buffer)')
1546 while r_size > 0:
1547 self.read_offset += self.read_buffer.shape[0]
1548 try:
1549 if hasattr(self.sf_iter, 'next'):
1550 fbuffer = self.sf_iter.next()
1551 else:
1552 fbuffer = next(self.sf_iter)
1553 except StopIteration:
1554 self.read_buffer = np.zeros((0,0))
1555 buffer[b_offset:,:] = 0.0
1556 if self.verbose > 1:
1557 print(f' caught StopIteration, padded buffer with {r_size} zeros')
1558 break
1559 self.read_buffer = np.frombuffer(fbuffer, dtype='<i2').reshape(-1, self.channels)
1560 n = self.read_buffer.shape[0]
1561 if n > r_size:
1562 n = r_size
1563 if n > 0:
1564 buffer[b_offset:b_offset+n,:] = self.read_buffer[:n,:] / (2.0**15-1.0)
1565 if self.verbose > 2:
1566 print(f' read {n:6d} frames to {r_offset}-{r_offset+n} ({r_offset-self.offset}-{r_offset+n-self.offset} in buffer)')
1567 b_offset += n
1568 r_offset += n
1569 r_size -= n
1572 # wavpack interface:
1573 def open_wavpack(self, filepath, buffersize=10.0, backsize=0.0,
1574 verbose=0):
1575 """Open WavPack file for reading using the wavpack library.
1577 Parameters
1578 ----------
1579 filepath: str or Path
1580 Name of the file.
1581 buffersize: float
1582 Size of internal buffer in seconds.
1583 backsize: float
1584 Part of the buffer to be loaded before the requested start index in seconds.
1585 verbose: int
1586 If larger than zero show detailed error/warning messages.
1588 Raises
1589 ------
1590 ImportError
1591 The wavpack library is not installed
1592 """
1593 self.verbose = verbose
1594 if self.verbose > 0:
1595 print(f'open_wavpack(filepath) with filepath={filepath}')
1596 if not audio_modules['wavpack']:
1597 self.rate = 0.0
1598 self.channels = 0
1599 self.frames = 0
1600 self.size = 0
1601 self.shape = (0, 0)
1602 self.offset = 0
1603 raise ImportError
1604 if self.sf is not None:
1605 self._close_wavpack()
1606 self.sf = open_wavpack_file(filepath)
1607 self.filepath = Path(filepath)
1608 self.file_paths = [self.filepath]
1609 self.file_indices = [0]
1610 self.rate = float(wavpack.WavpackGetSampleRate(self.sf))
1611 self.format = 'WAVPACK'
1612 if wavpack.WavpackGetMode(self.sf) & wavpack_float > 0:
1613 self.encoding = 'FLOAT'
1614 self.factor = 1.0
1615 else:
1616 bits = wavpack.WavpackGetBitsPerSample(self.sf)
1617 self.encoding = f'PCM_{bits}'
1618 self.factor = 1.0/(2.0**(bits-1))
1619 self.channels = wavpack.WavpackGetNumChannels(self.sf)
1620 self.frames = wavpack.WavpackGetNumSamples64(self.sf)
1621 self.shape = (self.frames, self.channels)
1622 self.size = self.frames * self.channels
1623 self.bufferframes = int(buffersize*self.rate)
1624 self.backframes = int(backsize*self.rate)
1625 self.init_buffer()
1626 self.close = self._close_wavpack
1627 self.load_audio_buffer = self._load_buffer_wavpack
1628 # the frame at which the file continues to unpack:
1629 self.wavpack_pos = 0
1630 return self
1632 def _close_wavpack(self):
1633 """Close the audio file using the wavpack library. """
1634 if self.sf is not None:
1635 wavpack.WavpackCloseFile(self.sf)
1636 self.sf = None
1638 def _load_buffer_wavpack(self, r_offset, r_size, buffer):
1639 """Load new data from file using the wavpack library.
1641 Parameters
1642 ----------
1643 r_offset: int
1644 First frame to be read from file.
1645 r_size: int
1646 Number of frames to be read from file.
1647 buffer: ndarray
1648 Buffer where to store the loaded data.
1649 """
1650 if self.sf is None:
1651 self.sf = open_wavpack_file(self.filepath)
1652 self.wavpack_pos = 0
1653 if r_offset != self.wavpack_pos: # reading on is cheaper than seeking
1654 if wavpack.WavpackSeekSample64(self.sf, r_offset) == 0:
1655 raise IOError(f'failed to seek to frame {r_offset} of file "{self.filepath}"')
1656 self.wavpack_pos = r_offset
1657 fbuffer = unpack_wavpack(self.sf, r_size, self.channels)
1658 self.wavpack_pos += len(fbuffer)
1659 if self.encoding == 'FLOAT':
1660 buffer[:len(fbuffer),:] = fbuffer.view(np.float32)
1661 else:
1662 buffer[:len(fbuffer),:] = fbuffer * self.factor
1665 # open multiple audio files as one:
1666 def open_multiple(self, filepaths, buffersize=10.0, backsize=0.0,
1667 verbose=0, mode='strict', rate=None, channels=None,
1668 end_indices=None):
1669 """Open multiple audio files as a single concatenated array.
1671 Parameters
1672 ----------
1673 filepaths: list of str or Path
1674 List of file paths of audio files.
1675 buffersize: float
1676 Size of internal buffer in seconds.
1677 backsize: float
1678 Part of the buffer to be loaded before the requested start index in seconds.
1679 verbose: int
1680 If larger than zero show detailed error/warning messages.
1681 mode: 'relaxed' or 'strict'
1682 If 'strict', only concatenate files if they contain
1683 a start time in their meta data.
1684 rate: float
1685 If provided, do a minimal initialization (no checking)
1686 using the provided sampling rate (in Hertz), channels,
1687 and end_indices.
1688 channels: int
1689 If provided, do a minimal initialization (no checking)
1690 using the provided rate, number of channels, and end_indices.
1691 end_indices: sequence of int
1692 If provided, do a minimal initialization (no checking)
1693 using the provided rate, channels, and end_indices.
1695 Raises
1696 ------
1697 TypeError
1698 `filepaths` must be a sequence.
1699 ValueError
1700 Empty `filepaths`.
1701 FileNotFoundError
1702 `filepaths` does not contain a single valid file.
1704 """
1705 if not isinstance(filepaths, (list, tuple, np.ndarray)):
1706 raise TypeError('input argument filepaths is not a sequence!')
1707 if len(filepaths) == 0:
1708 raise ValueError('input argument filepaths is empy sequence!')
1709 self.buffersize = buffersize
1710 self.backsize = backsize
1711 self.filepath = None
1712 self.file_paths = []
1713 self.open_files = []
1714 self.open_loaders = []
1715 self.audio_files = []
1716 self.collect_counter = 0
1717 self.frames = 0
1718 self.start_indices = []
1719 self.end_indices = []
1720 self.start_time = None
1721 start_time = None
1722 self._metadata = {}
1723 self._locs = np.zeros((0, 2), dtype=int)
1724 self._labels = np.zeros((0, 2), dtype=object)
1725 if end_indices is not None:
1726 self.filepath = Path(filepaths[0])
1727 self.file_paths = [Path(fp) for fp in filepaths]
1728 self.audio_files = [None] * len(filepaths)
1729 self.frames = end_indices[-1]
1730 self.start_indices = [0] + list(end_indices[:-1])
1731 self.end_indices = end_indices
1732 self.format = None
1733 self.encoding = None
1734 self.rate = rate
1735 self.channels = channels
1736 else:
1737 for filepath in filepaths:
1738 try:
1739 a = AudioLoader(filepath, buffersize, backsize, verbose)
1740 except Exception as e:
1741 if verbose > 0:
1742 print(e)
1743 continue
1744 # collect metadata:
1745 md = a.metadata()
1746 fmd = flatten_metadata(md, True)
1747 add_metadata(self._metadata, fmd)
1748 if self.filepath is None:
1749 # first file:
1750 self.filepath = a.filepath
1751 self.format = a.format
1752 self.encoding = a.encoding
1753 self.rate = a.rate
1754 self.channels = a.channels
1755 self.start_time = get_datetime(md)
1756 start_time = self.start_time
1757 stime = self.start_time
1758 else:
1759 # check channels and rate:
1760 error_str = None
1761 if a.channels != self.channels:
1762 error_str = f'number of channels differs: ' \
1763 f'{a.channels} in {a.filepath} versus ' \
1764 f'{self.channels} in {self.filepath}'
1765 if a.rate != self.rate:
1766 error_str = f'sampling rates differ: ' \
1767 f'{a.rate} in {a.filepath} versus ' \
1768 f'{self.rate} in {self.filepath}'
1769 # check start time of recording:
1770 stime = get_datetime(md)
1771 if mode == 'strict' and (start_time is None or stime is None):
1772 error_str = 'file does not contain a start time in its meta data'
1773 if start_time is not None and stime is not None and \
1774 abs(start_time - stime) > timedelta(seconds=self._max_time_diff):
1775 error_str = f'start time does not indicate continuous recording: ' \
1776 f'expected {start_time} instead of ' \
1777 f'{stime} in {a.filepath}'
1778 if error_str is not None:
1779 if verbose > 0:
1780 print(error_str)
1781 a.close()
1782 del a
1783 break
1784 # markers:
1785 locs, labels = a.markers()
1786 locs[:,0] += self.frames
1787 self._locs = np.vstack((self._locs, locs))
1788 self._labels = np.vstack((self._labels, labels))
1789 # indices:
1790 self.start_indices.append(self.frames)
1791 self.frames += a.frames
1792 self.end_indices.append(self.frames)
1793 if stime is not None:
1794 start_time = stime + timedelta(seconds=a.frames/a.rate)
1795 # add file to lists:
1796 self.file_paths.append(a.filepath)
1797 if len(self.open_files) < AudioLoader.max_open_files:
1798 self.open_files.append(a)
1799 else:
1800 a.close()
1801 if len(self.open_loaders) < AudioLoader.max_open_loaders:
1802 self.audio_files.append(a)
1803 self.open_loaders.append(a)
1804 else:
1805 a.close()
1806 del a
1807 self.audio_files.append(None)
1808 if len(self.audio_files) == 0:
1809 raise FileNotFoundError('input argument filepaths does not contain any valid audio file!')
1810 # set startime from first file:
1811 if self.start_time is not None:
1812 set_starttime(self._metadata, self.start_time)
1813 # setup infrastructure:
1814 self.file_indices = self.start_indices
1815 self.start_indices = np.array(self.start_indices)
1816 self.end_indices = np.array(self.end_indices)
1817 self.shape = (self.frames, self.channels)
1818 self.bufferframes = int(buffersize*self.rate)
1819 self.backframes = int(backsize*self.rate)
1820 self.init_buffer()
1821 self.close = self._close_multiple
1822 self.load_audio_buffer = self._load_buffer_multiple
1823 self._load_metadata = None
1824 self._load_markers = None
1825 return self
1827 def _close_multiple(self):
1828 """Close all the audio files. """
1829 self.open_files = []
1830 self.open_loaders = []
1831 if hasattr(self, 'audio_files'):
1832 for a in self.audio_files:
1833 if a is not None:
1834 a.close()
1835 self.audio_files = []
1836 self.filepath = None
1837 self.file_paths = []
1838 self.file_indices = []
1839 self.start_indices = []
1840 self.end_indices = []
1841 del self.audio_files
1842 del self.open_files
1843 del self.open_loaders
1844 del self.start_indices
1845 del self.end_indices
1847 def _load_buffer_multiple(self, r_offset, r_size, buffer):
1848 """Load new data from the underlying files.
1850 Parameters
1851 ----------
1852 r_offset: int
1853 First frame to be read from file.
1854 r_size: int
1855 Number of frames to be read from file.
1856 buffer: ndarray
1857 Buffer where to store the loaded data.
1858 """
1859 offs = r_offset
1860 size = r_size
1861 boffs = 0
1862 ai = np.searchsorted(self.end_indices, offs, side='right')
1863 while size > 0:
1864 if self.audio_files[ai] is None:
1865 a = AudioLoader(self.file_paths[ai],
1866 self.buffersize, self.backsize, 0)
1867 self.audio_files[ai] = a
1868 self.open_loaders.append(a)
1869 self.open_files.append(a)
1870 if len(self.open_files) > AudioLoader.max_open_files:
1871 a0 = self.open_files.pop(0)
1872 a0.close()
1873 if len(self.open_loaders) > AudioLoader.max_open_loaders:
1874 a0 = self.open_loaders.pop(0)
1875 self.audio_files[self.audio_files.index(a0)] = None
1876 a0.close()
1877 del a0
1878 self.collect_counter += 1
1879 if self.collect_counter > AudioLoader.max_open_loaders//2:
1880 gc.collect()
1881 self.collect_counter = 0
1882 else:
1883 self.open_loaders.pop(self.open_loaders.index(self.audio_files[ai]))
1884 self.open_loaders.append(self.audio_files[ai])
1885 ai0 = offs - self.start_indices[ai]
1886 ai1 = offs + size
1887 if ai1 > self.end_indices[ai]:
1888 ai1 = self.end_indices[ai]
1889 ai1 -= self.start_indices[ai]
1890 n = ai1 - ai0
1891 self.audio_files[ai].load_audio_buffer(ai0, n,
1892 buffer[boffs:boffs + n,:])
1893 if self.audio_files[ai] in self.open_files:
1894 self.open_files.pop(self.open_files.index(self.audio_files[ai]))
1895 self.open_files.append(self.audio_files[ai])
1896 if len(self.open_files) > AudioLoader.max_open_files:
1897 self.open_files[0].close()
1898 self.open_files.pop(0)
1899 boffs += n
1900 offs += n
1901 size -= n
1902 ai += 1
1905 def open(self, filepath, buffersize=10.0, backsize=0.0,
1906 verbose=0, **kwargs):
1907 """Open audio file for reading.
1909 Parameters
1910 ----------
1911 filepath: str or Path or list of str or Path
1912 Path of the file or list of many file paths that should be
1913 made accessible as a single array.
1914 buffersize: float
1915 Size of internal buffer in seconds.
1916 backsize: float
1917 Part of the buffer to be loaded before the requested start index in seconds.
1918 verbose: int
1919 If larger than zero show detailed error/warning messages.
1920 **kwargs: dict
1921 Further keyword arguments that are passed on to the
1922 specific opening functions. Only used by open_multiple() so far.
1924 Raises
1925 ------
1926 FileNotFoundError
1927 `filepath` is not an existing file.
1928 EOFError
1929 File size of `filepath` is zero.
1930 IOError
1931 Failed to load data.
1933 """
1934 self.buffer = np.array([])
1935 self.rate = 0.0
1936 if isinstance(filepath, (list, tuple, np.ndarray)):
1937 if len(filepath) > 1:
1938 self.open_multiple(filepath, buffersize, backsize,
1939 verbose - 1, **kwargs)
1940 if len(self.file_paths) > 1:
1941 return self
1942 filepath = self.file_paths[0]
1943 self.close()
1944 else:
1945 filepath = filepath[0]
1946 filepath = Path(filepath)
1947 if not filepath.is_file():
1948 raise FileNotFoundError(f'file "{filepath}" not found')
1949 if filepath.stat().st_size <= 0:
1950 raise EOFError(f'file "{filepath}" is empty (size=0)!')
1951 # list of implemented open functions:
1952 audio_open_funcs = (
1953 ('soundfile', self.open_soundfile),
1954 ('wave', self.open_wave),
1955 ('wavefile', self.open_wavefile),
1956 ('wavpack', self.open_wavpack),
1957 ('ewave', self.open_ewave),
1958 ('audioread', self.open_audioread),
1959 )
1960 # open an audio file by trying various modules:
1961 not_installed = []
1962 errors = [f'failed to load data from file "{filepath}":']
1963 for lib, open_file in audio_open_funcs:
1964 if not audio_modules[lib]:
1965 if verbose > 1:
1966 print(f'unable to load data from file "{filepath}" using {lib} module: module not available')
1967 not_installed.append(lib)
1968 continue
1969 try:
1970 open_file(filepath, buffersize, backsize,
1971 verbose - 1, **kwargs)
1972 if self.frames > 0:
1973 if verbose > 0:
1974 print(f'opened audio file "{filepath}" using {lib}')
1975 if verbose > 1:
1976 if self.format is not None:
1977 print(f' format : {self.format}')
1978 if self.encoding is not None:
1979 print(f' encoding : {self.encoding}')
1980 print(f' sampling rate: {self.rate} Hz')
1981 print(f' channels : {self.channels}')
1982 print(f' frames : {self.frames}')
1983 return self
1984 except Exception as e:
1985 errors.append(f' {lib} failed: {str(e)}')
1986 if verbose > 1:
1987 print(errors[-1])
1988 if len(not_installed) > 0:
1989 errors.append('\n You may need to install one of the ' + \
1990 ', '.join(not_installed) + ' packages.')
1991 raise IOError('\n'.join(errors))
1992 return self
1995def demo(file_path, plot):
1996 """Demo of the audioloader functions.
1998 Parameters
1999 ----------
2000 file_path: str
2001 File path of an audio file.
2002 plot: bool
2003 If True also plot the loaded data.
2004 """
2005 print('')
2006 print("try load_audio:")
2007 full_data, rate = load_audio(file_path, 1)
2008 if plot:
2009 plt.plot(np.arange(len(full_data))/rate, full_data[:,0])
2010 plt.show()
2012 if audio_modules['soundfile'] and audio_modules['audioread']:
2013 print('')
2014 print("cross check:")
2015 data1, rate1 = load_soundfile(file_path)
2016 data2, rate2 = load_audioread(file_path)
2017 n = min((len(data1), len(data2)))
2018 print(f"rms difference is {np.std(data1[:n]-data2[:n])}")
2019 if plot:
2020 plt.plot(np.arange(len(data1))/rate1, data1[:,0])
2021 plt.plot(np.arange(len(data2))/rate2, data2[:,0])
2022 plt.show()
2024 print('')
2025 print("try AudioLoader:")
2026 with AudioLoader(file_path, 4.0, 1.0, verbose=1) as data:
2027 print(f'samplerate: {data.rate:0f}Hz')
2028 print(f'channels: {data.channels} {data.shape[1]}')
2029 print(f'frames: {len(data)} {data.shape[0]}')
2030 nframes = int(1.5*data.rate)
2031 # check access:
2032 print('check random single frame access')
2033 for inx in np.random.randint(0, len(data), 1000):
2034 if np.any(np.abs(full_data[inx] - data[inx]) > 2.0**(-14)):
2035 print('single random frame access failed', inx, full_data[inx], data[inx])
2036 print('check random frame slice access')
2037 for inx in np.random.randint(0, len(data)-nframes, 1000):
2038 if np.any(np.abs(full_data[inx:inx+nframes] - data[inx:inx+nframes]) > 2.0**(-14)):
2039 print('random frame slice access failed', inx)
2040 print('check frame slice access forward')
2041 for inx in range(0, len(data)-nframes, 10):
2042 if np.any(np.abs(full_data[inx:inx+nframes] - data[inx:inx+nframes]) > 2.0**(-14)):
2043 print('frame slice access forward failed', inx)
2044 print('check frame slice access backward')
2045 for inx in range(len(data)-nframes, 0, -10):
2046 if np.any(np.abs(full_data[inx:inx+nframes] - data[inx:inx+nframes]) > 2.0**(-14)):
2047 print('frame slice access backward failed', inx)
2048 # forward:
2049 for i in range(0, len(data), nframes):
2050 print(f'forward {i}-{i+nframes}')
2051 x = data[i:i+nframes,0]
2052 if plot:
2053 plt.plot((i+np.arange(len(x)))/rate, x)
2054 plt.show()
2055 # and backwards:
2056 for i in reversed(range(0, len(data), nframes)):
2057 print(f'backward {i}-{i+nframes}')
2058 x = data[i:i+nframes,0]
2059 if plot:
2060 plt.plot((i+np.arange(len(x)))/rate, x)
2061 plt.show()
2064def main(*args):
2065 """Call demo with command line arguments.
2067 Parameters
2068 ----------
2069 args: list of str
2070 Command line arguments as provided by sys.argv[1:]
2071 """
2072 print("Checking audioloader module ...")
2074 help = False
2075 plot = False
2076 file_path = None
2077 mod = False
2078 for arg in args:
2079 if mod:
2080 if not select_module(arg):
2081 print(f'can not select module {arg} that is not installed')
2082 return
2083 mod = False
2084 elif arg == '-h':
2085 help = True
2086 break
2087 elif arg == '-p':
2088 plot = True
2089 elif arg == '-m':
2090 mod = True
2091 else:
2092 file_path = arg
2093 break
2095 if help:
2096 print('')
2097 print('Usage:')
2098 print(' python -m src.audioio.audioloader [-m <module>] [-p] <audio/file.wav>')
2099 print(' -m: audio module to be used')
2100 print(' -p: plot loaded data')
2101 return
2103 if plot:
2104 import matplotlib.pyplot as plt
2106 demo(file_path, plot)
2109if __name__ == "__main__":
2110 main(*sys.argv[1:])