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