Coverage for src/audioio/audiowriter.py: 81%
434 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"""Write numpy arrays of floats to audio files.
3- `write_audio()`: write audio data to file.
4- `available_formats()`: audio file formats supported by any of the installed audio modules.
5- `available_encodings()`: encodings of an audio file format supported by any of the installed audio modules.
6- `format_from_extension()`: deduce audio file format from file extension.
8The data to be written are 1-D or 2-D numpy arrays of floats ranging
9between -1 and 1 with first axis time and (optional) second axis channel.
11For support of more audio formats, you might need to install
12additional packages.
13See [installation](https://bendalab.github.io/audioio/installation)
14for further instructions.
16For a demo, run the script as:
17```
18python -m src.audioio.audiowriter
19```
21"""
23import os
24import sys
25import subprocess
26import numpy as np
28from io import BytesIO
29from pathlib import Path
30from .audiomodules import *
31from .riffmetadata import write_wave as audioio_write_wave
32from .riffmetadata import append_riff, write_riff_chunk
33from .riffmetadata import append_metadata_riff, append_markers_riff
36def format_from_extension(filepath):
37 """Deduce audio file format from file extension.
39 Parameters
40 ----------
41 filepath: str or Path or None
42 Name of the audio file.
44 Returns
45 -------
46 format: str
47 Audio format deduced from file extension.
48 """
49 if filepath is None:
50 return None
51 filepath = Path(filepath)
52 ext = filepath.suffix
53 if not ext:
54 return None
55 if ext[0] == '.':
56 ext = ext[1:]
57 if not ext:
58 return None
59 ext = ext.upper()
60 if ext == 'WAVE':
61 return 'WAV'
62 ext = ext.replace('MPEG' , 'MP')
63 return ext
66def formats_wave():
67 """Audio file formats supported by the wave module.
69 Returns
70 -------
71 formats: list of str
72 List of supported file formats as strings.
73 """
74 if not audio_modules['wave']:
75 return []
76 else:
77 return ['WAV']
80def encodings_wave(format):
81 """Encodings of an audio file format supported by the wave module.
83 Parameters
84 ----------
85 format: str
86 The file format.
88 Returns
89 -------
90 encodings: list of str
91 List of supported encodings as strings.
92 """
93 if not audio_modules['wave']:
94 return []
95 elif format.upper() != 'WAV':
96 return []
97 else:
98 return ['PCM_32', 'PCM_16', 'PCM_U8']
101def write_wave(filepath, data, rate, metadata=None, locs=None,
102 labels=None, format=None, encoding=None,
103 marker_hint='cue'):
104 """Write audio data using the wave module from pythons standard libray.
106 Documentation
107 -------------
108 https://docs.python.org/3.8/library/wave.html
110 Parameters
111 ----------
112 filepath: str or Path
113 Full path and name of the file to write.
114 data: 1-D or 2-D array of floats
115 Array with the data (first index time, second index channel,
116 values within -1.0 and 1.0).
117 rate: float
118 Sampling rate of the data in Hertz.
119 metadata: None or nested dict
120 Metadata as key-value pairs. Values can be strings, integers,
121 or dictionaries.
122 locs: None or 1-D or 2-D array of ints
123 Marker positions (first column) and spans (optional second column)
124 for each marker (rows).
125 labels: None or 2-D array of string objects
126 Labels (first column) and texts (optional second column)
127 for each marker (rows).
128 format: str or None
129 File format, only 'WAV' is supported.
130 encoding: str or None
131 Encoding of the data: 'PCM_32', 'PCM_16', or 'PCM_U8'.
132 If None or empty string use 'PCM_16'.
133 marker_hint: str
134 - 'cue': store markers in cue and and adtl chunks.
135 - 'lbl': store markers in avisoft lbl chunk.
137 Raises
138 ------
139 ImportError
140 The wave module is not installed.
141 *
142 Writing of the data failed.
143 ValueError
144 File format or encoding not supported.
145 """
146 if not audio_modules['wave']:
147 raise ImportError
148 if not format:
149 format = format_from_extension(filepath)
150 if format and format.upper() != 'WAV':
151 raise ValueError('file format %s not supported by wave module' % format)
153 wave_encodings = {'PCM_32': [4, 'i4'],
154 'PCM_16': [2, 'i2'],
155 'PCM_U8': [1, 'u1'] }
156 if not encoding:
157 encoding = 'PCM_16'
158 encoding = encoding.upper()
159 if encoding not in wave_encodings:
160 raise ValueError('file encoding %s not supported by wave module' % encoding)
161 sampwidth = wave_encodings[encoding][0]
162 dtype = wave_encodings[encoding][1]
164 wf = wave.open(os.fspath(filepath), 'w') # 'with' is not supported by wave
165 channels = 1
166 if len(data.shape) > 1:
167 channels = data.shape[1]
168 wf.setnchannels(channels)
169 wf.setnframes(len(data))
170 wf.setframerate(int(rate))
171 wf.setsampwidth(sampwidth)
172 factor = 2**(sampwidth*8-1)
173 if sampwidth == 1:
174 buffer = np.floor((data+1.0) * factor).astype(dtype)
175 buffer[data >= 1.0] = 2*factor - 1
176 else:
177 buffer = np.floor(data * factor).astype(dtype)
178 buffer[data >= 1.0] = factor - 1
179 wf.writeframes(buffer.tobytes())
180 wf.close()
181 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
184def formats_ewave():
185 """Audio file formats supported by the ewave module.
187 Returns
188 -------
189 formats: list of str
190 List of supported file formats as strings.
191 """
192 if not audio_modules['ewave']:
193 return []
194 else:
195 return ['WAV', 'WAVEX']
198def encodings_ewave(format):
199 """Encodings of an audio file format supported by the ewave module.
201 Parameters
202 ----------
203 format: str
204 The file format.
206 Returns
207 -------
208 encodings: list of str
209 List of supported encodings as strings.
210 """
211 if not audio_modules['ewave']:
212 return []
213 elif format.upper() != 'WAV' and format.upper() != 'WAVEX':
214 return []
215 else:
216 return ['PCM_64', 'PCM_32', 'PCM_16', 'FLOAT', 'DOUBLE']
219def write_ewave(filepath, data, rate, metadata=None, locs=None,
220 labels=None, format=None, encoding=None,
221 marker_hint='cue'):
222 """Write audio data using the ewave module from pythons standard libray.
224 Documentation
225 -------------
226 https://github.com/melizalab/py-ewave
228 Parameters
229 ----------
230 filepath: str
231 Full path and name of the file to write.
232 data: 1-D or 2-D array of floats
233 Array with the data (first index time, second index channel,
234 values within -1.0 and 1.0).
235 rate: float
236 Sampling rate of the data in Hertz.
237 metadata: None or nested dict
238 Metadata as key-value pairs. Values can be strings, integers,
239 or dictionaries.
240 locs: None or 1-D or 2-D array of ints
241 Marker positions (first column) and spans (optional second column)
242 for each marker (rows).
243 labels: None or 2-D array of string objects
244 Labels (first column) and texts (optional second column)
245 for each marker (rows).
246 format: str or None
247 File format, only 'WAV' and 'WAVEX' are supported.
248 encoding: str or None
249 Encoding of the data: 'PCM_64', 'PCM_32', PCM_16', 'FLOAT', 'DOUBLE'
250 If None or empty string use 'PCM_16'.
251 marker_hint: str
252 - 'cue': store markers in cue and and adtl chunks.
253 - 'lbl': store markers in avisoft lbl chunk.
255 Raises
256 ------
257 ImportError
258 The ewave module is not installed.
259 *
260 Writing of the data failed.
261 ValueError
262 File format or encoding not supported.
263 """
264 if not audio_modules['ewave']:
265 raise ImportError
267 if not format:
268 format = format_from_extension(filepath)
269 if format and format.upper() != 'WAV' and format.upper() != 'WAVEX':
270 raise ValueError('file format %s not supported by ewave module' % format)
272 ewave_encodings = {'PCM_64': 'l',
273 'PCM_32': 'i',
274 'PCM_16': 'h',
275 'FLOAT': 'f',
276 'DOUBLE': 'd' }
277 if not encoding:
278 encoding = 'PCM_16'
279 encoding = encoding.upper()
280 if encoding not in ewave_encodings:
281 raise ValueError('file encoding %s not supported by ewave module' % encoding)
283 channels = 1
284 if len(data.shape) > 1:
285 channels = data.shape[1]
287 with ewave.open(filepath, 'w', sampling_rate=int(rate),
288 dtype=ewave_encodings[encoding], nchannels=channels) as wf:
289 wf.write(data, scale=True)
290 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
293def formats_wavfile():
294 """Audio file formats supported by the scipy.io.wavfile module.
296 Returns
297 -------
298 formats: list of str
299 List of supported file formats as strings.
300 """
301 if not audio_modules['scipy.io.wavfile']:
302 return []
303 else:
304 return ['WAV']
307def encodings_wavfile(format):
308 """Encodings of an audio file format supported by the scipy.io.wavfile module.
310 Parameters
311 ----------
312 format: str
313 The file format.
315 Returns
316 -------
317 encodings: list of str
318 List of supported encodings as strings.
319 """
320 if not audio_modules['scipy.io.wavfile']:
321 return []
322 elif format.upper() != 'WAV':
323 return []
324 else:
325 return ['PCM_U8', 'PCM_16', 'PCM_32', 'PCM_64', 'FLOAT', 'DOUBLE']
328def write_wavfile(filepath, data, rate, metadata=None, locs=None,
329 labels=None, format=None, encoding=None,
330 marker_hint='cue'):
331 """Write audio data using the scipy.io.wavfile module.
333 Documentation
334 -------------
335 http://docs.scipy.org/doc/scipy/reference/io.html
337 Parameters
338 ----------
339 filepath: str or Path
340 Full path and name of the file to write.
341 data: 1-D or 2-D array of floats
342 Array with the data (first index time, second index channel,
343 values within -1.0 and 1.0).
344 rate: float
345 Sampling rate of the data in Hertz.
346 metadata: None or nested dict
347 Metadata as key-value pairs. Values can be strings, integers,
348 or dictionaries.
349 locs: None or 1-D or 2-D array of ints
350 Marker positions (first column) and spans (optional second column)
351 for each marker (rows).
352 labels: None or 2-D array of string objects
353 Labels (first column) and texts (optional second column)
354 for each marker (rows).
355 format: str or None
356 File format, only 'WAV' is supported.
357 encoding: str or None
358 Encoding of the data: 'PCM_64', 'PCM_32', PCM_16', 'PCM_U8', 'FLOAT', 'DOUBLE'
359 If None or empty string use 'PCM_16'.
360 marker_hint: str
361 - 'cue': store markers in cue and and adtl chunks.
362 - 'lbl': store markers in avisoft lbl chunk.
364 Raises
365 ------
366 ImportError
367 The wavfile module is not installed.
368 ValueError
369 File format or encoding not supported.
370 *
371 Writing of the data failed.
372 """
373 if not audio_modules['scipy.io.wavfile']:
374 raise ImportError
376 if not format:
377 format = format_from_extension(filepath)
378 if format and format.upper() != 'WAV':
379 raise ValueError('file format %s not supported by scipy.io.wavfile module' % format)
381 wave_encodings = {'PCM_U8': [1, 'u1'],
382 'PCM_16': [2, 'i2'],
383 'PCM_32': [4, 'i4'],
384 'PCM_64': [8, 'i8'],
385 'FLOAT': [4, 'f'],
386 'DOUBLE': [8, 'd']}
387 if not encoding:
388 encoding = 'PCM_16'
389 encoding = encoding.upper()
390 if encoding not in wave_encodings:
391 raise ValueError('file encoding %s not supported by scipy.io.wavfile module' % encoding)
392 sampwidth = wave_encodings[encoding][0]
393 dtype = wave_encodings[encoding][1]
394 if sampwidth == 1:
395 factor = 2**(sampwidth*8-1)
396 buffer = np.floor((data+1.0) * factor).astype(dtype)
397 buffer[data >= 1.0] = 2*factor - 1
398 elif dtype[0] == 'i':
399 factor = 2**(sampwidth*8-1)
400 buffer = np.floor(data * factor).astype(dtype)
401 buffer[data >= 1.0] = factor - 1
402 else:
403 buffer = data.astype(dtype, copy=False)
404 wavfile.write(filepath, int(rate), buffer)
405 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
408def formats_soundfile():
409 """Audio file formats supported by the SoundFile module.
411 Returns
412 -------
413 formats: list of str
414 List of supported file formats as strings.
415 """
416 if not audio_modules['soundfile']:
417 return []
418 else:
419 return sorted(list(soundfile.available_formats()))
422def encodings_soundfile(format):
423 """Encodings of an audio file format supported by the SoundFile module.
425 Parameters
426 ----------
427 format: str
428 The file format.
430 Returns
431 -------
432 encodings: list of str
433 List of supported encodings as strings.
434 """
435 if not audio_modules['soundfile']:
436 return []
437 else:
438 return sorted(list(soundfile.available_subtypes(format)))
441def write_soundfile(filepath, data, rate, metadata=None, locs=None,
442 labels=None, format=None, encoding=None,
443 marker_hint='cue'):
444 """Write audio data using the SoundFile module (based on libsndfile).
446 Documentation
447 -------------
448 http://pysoundfile.readthedocs.org
450 Parameters
451 ----------
452 filepath: str or Path
453 Full path and name of the file to write.
454 data: 1-D or 2-D array of floats
455 Array with the data (first index time, second index channel,
456 values within -1.0 and 1.0).
457 rate: float
458 Sampling rate of the data in Hertz.
459 metadata: None or nested dict
460 Metadata as key-value pairs. Values can be strings, integers,
461 or dictionaries.
462 locs: None or 1-D or 2-D array of ints
463 Marker positions (first column) and spans (optional second column)
464 for each marker (rows).
465 labels: None or 2-D array of string objects
466 Labels (first column) and texts (optional second column)
467 for each marker (rows).
468 format: str or None
469 File format.
470 encoding: str or None
471 Encoding of the data.
472 If None or empty string use 'PCM_16'.
473 marker_hint: str
474 - 'cue': store markers in cue and and adtl chunks.
475 - 'lbl': store markers in avisoft lbl chunk.
477 Raises
478 ------
479 ImportError
480 The SoundFile module is not installed.
481 *
482 Writing of the data failed.
483 """
484 if not audio_modules['soundfile']:
485 raise ImportError
487 if not format:
488 format = format_from_extension(filepath)
489 if format:
490 format = format.upper()
492 if not encoding:
493 encoding = 'PCM_16'
494 encoding = encoding.upper()
496 soundfile.write(filepath, data, int(rate), format=format,
497 subtype=encoding)
498 try:
499 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
500 except ValueError:
501 pass
504def formats_wavefile():
505 """Audio file formats supported by the wavefile module.
507 Returns
508 -------
509 formats: list of str
510 List of supported file formats as strings.
511 """
512 if not audio_modules['wavefile']:
513 return []
514 formats = []
515 for attr in dir(wavefile.Format):
516 v = getattr(wavefile.Format, attr)
517 if ( isinstance(v, int)
518 and v & wavefile.Format.TYPEMASK > 0
519 and v != wavefile.Format.TYPEMASK ):
520 formats.append(attr)
521 return sorted(formats)
524def encodings_wavefile(format):
525 """Encodings supported by the wavefile module.
527 Parameters
528 ----------
529 format: str
530 The file format (ignored).
532 Returns
533 -------
534 encodings: list of str
535 List of supported encodings as strings.
536 """
537 if not audio_modules['wavefile']:
538 return []
539 if not format.upper() in formats_wavefile():
540 return []
541 encodings = []
542 for attr in dir(wavefile.Format):
543 v = getattr(wavefile.Format, attr)
544 if ( isinstance(v, int)
545 and v & wavefile.Format.SUBMASK > 0
546 and v != wavefile.Format.SUBMASK ):
547 encodings.append(attr)
548 return sorted(encodings)
551def write_wavefile(filepath, data, rate, metadata=None, locs=None,
552 labels=None, format=None, encoding=None,
553 marker_hint='cue'):
554 """Write audio data using the wavefile module (based on libsndfile).
556 Documentation
557 -------------
558 https://github.com/vokimon/python-wavefile
560 Parameters
561 ----------
562 filepath: str or Path
563 Full path and name of the file to write.
564 data: 1-D or 2-D array of floats
565 Array with the data (first index time, second index channel,
566 values within -1.0 and 1.0).
567 rate: float
568 Sampling rate of the data in Hertz.
569 metadata: None or nested dict
570 Metadata as key-value pairs. Values can be strings, integers,
571 or dictionaries.
572 locs: None or 1-D or 2-D array of ints
573 Marker positions (first column) and spans (optional second column)
574 for each marker (rows).
575 labels: None or 2-D array of string objects
576 Labels (first column) and texts (optional second column)
577 for each marker (rows).
578 format: str or None
579 File format as in wavefile.Format.
580 encoding: str or None
581 Encoding of the data as in wavefile.Format.
582 If None or empty string use 'PCM_16'.
583 marker_hint: str
584 - 'cue': store markers in cue and and adtl chunks.
585 - 'lbl': store markers in avisoft lbl chunk.
587 Raises
588 ------
589 ImportError
590 The wavefile module is not installed.
591 ValueError
592 File format or encoding not supported.
593 *
594 Writing of the data failed.
595 """
596 if not audio_modules['wavefile']:
597 raise ImportError
599 if not format:
600 format = format_from_extension(filepath)
601 format = format.upper()
602 try:
603 format_value = getattr(wavefile.Format, format)
604 except AttributeError:
605 raise ValueError('file format %s not supported by wavefile module' % format)
607 if not encoding:
608 encodings = encodings_wavefile(format)
609 encoding = encodings[0]
610 if 'PCM_16' in encodings:
611 encoding = 'PCM_16'
612 encoding = encoding.upper()
613 try:
614 encoding_value = getattr(wavefile.Format, encoding)
615 except AttributeError:
616 raise ValueError('file encoding %s not supported by wavefile module' % encoding)
618 channels = 1
619 if len(data.shape) > 1:
620 channels = data.shape[1]
621 else:
622 data = data.reshape((-1, 1))
623 with wavefile.WaveWriter(os.fspath(filepath), channels=channels,
624 samplerate=int(rate),
625 format=format_value|encoding_value) as w:
626 w.write(data.T)
627 try:
628 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
629 except ValueError:
630 pass
633def formats_wavpack():
634 """Audio file formats supported by the wavpack module.
636 Returns
637 -------
638 formats: list of str
639 List of supported file formats as strings.
640 """
641 if not audio_modules['wavpack']:
642 return []
643 else:
644 return ['WV']
646def encodings_wavpack(format):
647 """Encodings of an audio file format supported by the wavpack module.
649 Parameters
650 ----------
651 format: str
652 The file format.
654 Returns
655 -------
656 encodings: list of str
657 List of supported encodings as strings.
658 """
659 if not audio_modules['wavpack']:
660 return []
661 elif format.upper() != 'WV':
662 return []
663 else:
664 return ['PCM_FLOAT']
666def write_wavpack(filepath, data, rate, metadata=None, locs=None,
667 labels=None, format=None, encoding=None,
668 marker_hint='cue'):
669 """Write audio data using the wavpack module.
671 Documentation
672 -------------
673 https://www.wavpack.com
675 Parameters
676 ----------
677 filepath: str or Path
678 Full path and name of the file to write.
679 data: 1-D or 2-D array of floats
680 Array with the data (first index time, second index channel,
681 values within -1.0 and 1.0).
682 rate: float
683 Sampling rate of the data in Hertz.
684 metadata: None or nested dict
685 Metadata as key-value pairs. Values can be strings, integers,
686 or dictionaries.
687 locs: None or 1-D or 2-D array of ints
688 Marker positions (first column) and spans (optional second column)
689 for each marker (rows).
690 labels: None or 2-D array of string objects
691 Labels (first column) and texts (optional second column)
692 for each marker (rows).
693 format: str or None
694 File format, only 'WAV' is supported.
695 encoding: str or None
696 Encoding of the data: 'PCM_32', 'PCM_16', or 'PCM_U8'.
697 If None or empty string use 'PCM_16'.
698 marker_hint: str
699 - 'cue': store markers in cue and and adtl chunks.
700 - 'lbl': store markers in avisoft lbl chunk.
702 Raises
703 ------
704 ImportError
705 The wavpack library is not installed.
706 *
707 Writing of the data failed.
708 ValueError
709 File format or encoding not supported.
710 """
711 if not audio_modules['wavpack']:
712 raise ImportError
714 @wavpack.WavpackBlockOutput
715 def _wavpack_write_block(out_file, data_ptr, bcount):
716 if not out_file or not data_ptr or bcount <= 0:
717 return 0
718 try:
719 out_file.write(ctypes.string_at(data_ptr, bcount))
720 return 1
721 except Exception:
722 return 0
724 if not format:
725 format = format_from_extension(filepath)
726 if format and format.upper() != 'WV':
727 raise ValueError(f'file format {format} not supported by wavpack module')
728 if not encoding:
729 encoding = 'PCM_FLOAT'
730 encoding = encoding.upper()
731 if encoding != 'PCM_FLOAT':
732 raise ValueError(f'file encoding {encoding} not supported by wavpack module')
733 nchannels = 1
734 if len(data.shape) > 1:
735 nchannels = data.shape[1]
736 else:
737 data = data.reshape((-1, 1))
738 # WavPack float mode expects the raw IEEE-754 bit patterns as int32:
739 data = np.clip(data, -1.0, 1.0).astype(np.float32)
740 interleaved = np.ascontiguousarray(data.reshape(-1))
741 buffer = interleaved.view(np.int32)
742 # metadata:
743 meta_buffer = BytesIO()
744 write_riff_chunk(meta_buffer)
745 append_metadata_riff(meta_buffer, metadata)
746 append_markers_riff(meta_buffer, locs, labels, marker_hint)
747 meta_bytes = meta_buffer.getvalue()
748 # write wavpack file:
749 with open(filepath, 'wb') as dest:
750 wpc = wavpack.WavpackOpenFileOutput(_wavpack_write_block, dest, None)
751 if not wpc:
752 raise IOError(f'failed to open "{filepath}" for writing wavpack file')
753 config = WavpackConfig()
754 config.bits_per_sample = 32
755 config.bytes_per_sample = 4
756 config.num_channels = nchannels
757 config.sample_rate = int(rate)
758 config.flags = wavpack.CONFIG_FLOAT_DATA
759 config.float_norm_exp = 127 # floats normalized to +/-1
761 if not wavpack.WavpackSetConfiguration64(wpc, ctypes.byref(config),
762 len(data), None):
763 raise RuntimeError("wavpack: WavPackSetConfiguration() failed.")
764 if not wavpack.WavpackAddWrapper(wpc, meta_bytes, len(meta_bytes)):
765 raise RuntimeError("wavpack: WavpackAddWrapper() failed.")
766 if not wavpack.WavpackPackInit(wpc):
767 raise RuntimeError("wavpack: WavpackPackInit() failed.")
768 if not wavpack.WavpackPackSamples(wpc,
769 buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)),
770 len(data)):
771 raise RuntimeError("wavpack: WavpackPackSamples() failed.")
772 wavpack.WavpackFlushSamples(wpc)
773 wavpack.WavpackCloseFile(wpc)
776pydub_formats = None
778def formats_pydub():
779 """Audio file formats supported by the Pydub module.
781 Returns
782 -------
783 formats: list of str
784 List of supported file formats as strings.
785 """
786 if not audio_modules['pydub']:
787 return []
788 global pydub_formats
789 if pydub_formats is not None:
790 return pydub_formats
791 formats = []
792 command = [pydub.AudioSegment.converter, '-formats']
793 with open(os.devnull, 'rb') as devnull:
794 p = subprocess.Popen(command, stdin=devnull, stdout=subprocess.PIPE,
795 stderr=subprocess.PIPE, universal_newlines=True)
796 skip = True
797 for line in p.communicate()[0].split('\n'):
798 if '--' in line[:3]:
799 skip = False
800 continue
801 if skip:
802 continue
803 cols = line.split()
804 if len(cols) > 2 and 'E' in cols[0]:
805 formats.append(cols[1].upper())
806 pydub_formats = formats
807 return formats
809pydub_encodings = None
811def encodings_pydub(format):
812 """Encodings of an audio file format supported by the Pydub module.
814 Parameters
815 ----------
816 format: str
817 The file format.
819 Returns
820 -------
821 encodings: list of str
822 List of supported encodings as strings.
823 """
824 pydub_encodings = {'pcm_s16le': 'PCM_16',
825 'pcm_s24le': 'PCM_24',
826 'pcm_s32le': 'PCM_32',
827 'pcm_f32le': 'FLOAT',
828 'pcm_f64le': 'DOUBLE',
829 }
830 if not audio_modules['pydub']:
831 return []
832 if format.upper() not in formats_pydub():
833 return []
834 global pydup_encodings
835 if pydub_encodings is not None:
836 return pydub_encodings
837 encodings = []
838 command = [pydub.AudioSegment.converter, '-encoders']
839 with open(os.devnull, 'rb') as devnull:
840 p = subprocess.Popen(command, stdin=devnull, stdout=subprocess.PIPE,
841 stderr=subprocess.PIPE, universal_newlines=True)
842 skip = True
843 for line in p.communicate()[0].split('\n'):
844 if '--' in line[:3]:
845 skip = False
846 continue
847 if skip:
848 continue
849 cols = line.split()
850 if len(cols) > 2 and cols[0][0] == 'A':
851 encoding = cols[1]
852 if encoding in pydub_encodings:
853 encoding = pydub_encodings[encoding]
854 encodings.append(encoding.upper())
855 pydup_encodings = encodings
856 return encodings
858def write_pydub(filepath, data, rate, metadata=None, locs=None,
859 labels=None, format=None, encoding=None,
860 marker_hint='cue'):
861 """Write audio data using the Pydub module.
863 Documentation
864 -------------
865 https://github.com/jiaaro/pydub
867 Parameters
868 ----------
869 filepath: str or Path
870 Full path and name of the file to write.
871 data: 1-D or 2-D array of floats
872 Array with the data (first index time, second index channel,
873 values within -1.0 and 1.0).
874 rate: float
875 Sampling rate of the data in Hertz.
876 metadata: None or nested dict
877 Metadata as key-value pairs. Values can be strings, integers,
878 or dictionaries.
879 locs: None or 1-D or 2-D array of ints
880 Marker positions (first column) and spans (optional second column)
881 for each marker (rows).
882 labels: None or 2-D array of string objects
883 Labels (first column) and texts (optional second column)
884 for each marker (rows).
885 format: str or None
886 File format, everything ffmpeg or avtools are supporting.
887 encoding: str or None
888 Encoding of the data.
889 If None or empty string use 'PCM_16'.
890 marker_hint: str
891 - 'cue': store markers in cue and and adtl chunks.
892 - 'lbl': store markers in avisoft lbl chunk.
894 Raises
895 ------
896 ImportError
897 The Pydub module is not installed.
898 *
899 Writing of the data failed.
900 ValueError
901 File format or encoding not supported.
902 """
903 if not audio_modules['pydub']:
904 raise ImportError
906 if not format:
907 format = format_from_extension(filepath)
908 if format and format.upper() not in formats_pydub():
909 raise ValueError('file format %s not supported by Pydub module' % format)
911 pydub_encodings = {'PCM_16': 'pcm_s16le',
912 'PCM_24': 'pcm_s24le',
913 'PCM_32': 'pcm_s32le',
914 'DOUBLE': 'pcm_f32le',
915 'FLOAT': 'pcm_f64le',
916 }
917 if encoding:
918 encoding = encoding.upper()
919 if encoding in pydub_encodings:
920 encoding = pydub_encodings[encoding]
921 if encoding not in encodings_pydub(format):
922 raise ValueError('file encoding %s not supported by Pydub module' % encoding)
923 encoding = encoding.lower()
924 else:
925 encoding = None
927 channels = 1
928 if len(data.shape) > 1:
929 channels = data.shape[1]
930 int_data = (data*(2**31-1)).astype(np.int32)
931 sound = pydub.AudioSegment(int_data.ravel(), sample_width=4,
932 frame_rate=rate, channels=channels)
933 sound.export(filepath, format=format.lower(), codec=encoding)
934 try:
935 append_riff(filepath, metadata, locs, labels, rate, marker_hint)
936 except ValueError:
937 pass
940audio_formats_funcs = (
941 ('soundfile', formats_soundfile),
942 ('wavpack', formats_wavpack),
943 ('wavefile', formats_wavefile),
944 ('wave', formats_wave),
945 ('ewave', formats_ewave),
946 ('scipy.io.wavfile', formats_wavfile),
947 ('pydub', formats_pydub)
948 )
949""" List of implemented formats() functions.
951Each element of the list is a tuple with the module's name and the formats() function.
952"""
955def available_formats():
956 """Audio file formats supported by any of the installed audio modules.
958 Returns
959 -------
960 formats: list of str
961 List of supported file formats as strings.
963 Examples
964 --------
965 ```
966 >>> from audioio import available_formats
967 >>> f = available_formats()
968 >>> printf(f)
969 ['3G2', '3GP', 'A64', 'AC3', 'ADTS', 'ADX', 'AIFF', ..., 'WAV', 'WAVEX', 'WEBM', 'WEBM_CHUNK', 'WEBM_DASH_MANIFEST', 'WEBP', 'WEBVTT', 'WTV', 'WV', 'WVE', 'XI', 'XV', 'YUV4MPEGPIPE']
970 ```
971 """
972 formats = set()
973 for module, formats_func in audio_formats_funcs:
974 formats |= set(formats_func())
975 return sorted(list(formats))
978audio_encodings_funcs = (
979 ('soundfile', encodings_soundfile),
980 ('wavpack', encodings_wavpack),
981 ('wavefile', encodings_wavefile),
982 ('wave', encodings_wave),
983 ('ewave', encodings_ewave),
984 ('scipy.io.wavfile', encodings_wavfile),
985 ('pydub', encodings_pydub)
986 )
987""" List of implemented encodings() functions.
989Each element of the list is a tuple with the module's name and the encodings() function.
990"""
993def available_encodings(format):
994 """Encodings of an audio file format supported by any of the installed audio modules.
996 Parameters
997 ----------
998 format: str
999 The file format.
1001 Returns
1002 -------
1003 encodings: list of str
1004 List of supported encodings as strings.
1006 Example
1007 -------
1008 ```
1009 >>> from audioio import available_encodings
1010 >>> e = available_encodings('WAV')
1011 >>> printf(e)
1012 ['ALAW', 'DOUBLE', 'FLOAT', 'G721_32', 'GSM610', 'IMA_ADPCM', 'MS_ADPCM', 'PCM_16', 'PCM_24', 'PCM_32', 'PCM_U8', 'ULAW']
1013 ```
1014 """
1015 for module, encodings_func in audio_encodings_funcs:
1016 encs = encodings_func(format)
1017 if len(encs) > 0:
1018 return encs
1019 return []
1022audio_writer_funcs = (
1023 ('soundfile', write_soundfile),
1024 ('wavpack', write_wavpack),
1025 ('wavefile', write_wavefile),
1026 ('wave', write_wave),
1027 ('ewave', write_ewave),
1028 ('scipy.io.wavfile', write_wavfile),
1029 ('pydub', write_pydub)
1030 )
1031""" List of implemented write() functions.
1033Each element of the list is a tuple with the module's name and the write() function.
1034"""
1037def write_audio(filepath, data, rate, metadata=None, locs=None,
1038 labels=None, format=None, encoding=None,
1039 marker_hint='cue', verbose=0):
1040 """Write audio data, metadata, and marker to file.
1042 Parameters
1043 ----------
1044 filepath: str or Path
1045 Full path and name of the file to write.
1046 data: 1-D or 2-D array of floats
1047 Array with the data (first index time, second index channel,
1048 values within -1.0 and 1.0).
1049 rate: float
1050 Sampling rate of the data in Hertz.
1051 metadata: None or nested dict
1052 Metadata as key-value pairs. Values can be strings, integers,
1053 or dictionaries.
1054 locs: None or 1-D or 2-D array of ints
1055 Marker positions (first column) and spans (optional second column)
1056 for each marker (rows).
1057 labels: None or 2-D array of string objects
1058 Labels (first column) and texts (optional second column)
1059 for each marker (rows).
1060 format: str or None
1061 File format. If None deduce file format from filepath.
1062 See `available_formats()` for possible values.
1063 encoding: str or None
1064 Encoding of the data. See `available_encodings()` for possible values.
1065 If None or empty string use 'PCM_16'.
1066 marker_hint: str
1067 - 'cue': store markers in cue and and adtl chunks.
1068 - 'lbl': store markers in avisoft lbl chunk.
1069 verbose: int
1070 If >0 show detailed error/warning messages.
1072 Raises
1073 ------
1074 ValueError
1075 `filepath` is empty string.
1076 IOError
1077 Writing of the data failed.
1079 Examples
1080 --------
1081 ```
1082 import numpy as np
1083 from audioio import write_audio
1085 rate = 28000.0
1086 freq = 800.0
1087 time = np.arange(0.0, 1.0, 1/rate) # one second
1088 data = np.sin(2.0*np.p*freq*time) # 800Hz sine wave
1089 md = dict(Artist='underscore_') # metadata
1091 write_audio('audio/file.wav', data, rate, md)
1092 ```
1093 """
1094 # write audio with metadata and markers:
1095 if not format:
1096 format = format_from_extension(filepath)
1097 if format == 'WAV' and (metadata is not None or locs is not None):
1098 try:
1099 audioio_write_wave(filepath, data, rate, metadata,
1100 locs, labels, encoding, marker_hint)
1101 return
1102 except ValueError:
1103 pass
1104 # write audio file by trying available modules:
1105 errors = [f'failed to write data to file "{filepath}":']
1106 for lib, write_file in audio_writer_funcs:
1107 if not audio_modules[lib]:
1108 continue
1109 try:
1110 write_file(filepath, data, rate, metadata, locs,
1111 labels, format, encoding, marker_hint)
1112 success = True
1113 if verbose > 0:
1114 print('wrote data to file "%s" using %s module' %
1115 (filepath, lib))
1116 if verbose > 1:
1117 print(' sampling rate: %g Hz' % rate)
1118 print(' channels : %d' % (data.shape[1] if len(data.shape) > 1 else 1))
1119 print(' frames : %d' % len(data))
1120 return
1121 except Exception as e:
1122 errors.append(f' {lib} failed: {str(e)}')
1123 raise IOError('\n'.join(errors))
1126def demo(file_path, channels=2, encoding=''):
1127 """Demo of the audiowriter functions.
1129 Parameters
1130 ----------
1131 file_path: str
1132 File path of an audio file.
1133 encoding: str
1134 Encoding to be used.
1135 """
1136 print('generate data ...')
1137 rate = 44100.0
1138 t = np.arange(0.0, 1.0, 1.0/rate)
1139 data = np.zeros((len(t), channels))
1140 for c in range(channels):
1141 data[:,c] = (1 - c*0.1)*np.sin(2.0*np.pi*(440.0+c*110.0)*t)
1143 print("write_audio(%s) ..." % file_path)
1144 write_audio(file_path, data, rate, encoding=encoding, verbose=2)
1146 print('done.')
1149def main(*args):
1150 """Call demo with command line arguments.
1152 Parameters
1153 ----------
1154 args: list of str
1155 Command line arguments as provided by sys.argv[1:]
1156 """
1157 help = False
1158 file_path = None
1159 encoding = ''
1160 mod = False
1161 nchan = False
1162 channels = 2
1163 for arg in args:
1164 if mod:
1165 select_module(arg)
1166 mod = False
1167 elif nchan:
1168 channels = int(arg)
1169 nchan = False
1170 elif arg == '-h' or arg == '--help':
1171 help = True
1172 break
1173 elif arg == '-m':
1174 mod = True
1175 elif arg == '-n':
1176 nchan = True
1177 elif file_path is None:
1178 file_path = arg
1179 else:
1180 encoding = arg
1181 break
1182 if file_path is None:
1183 file_path = 'test.wav'
1185 if help:
1186 print('')
1187 print('Usage:')
1188 print(' python -m src.audioio.audiowriter [-m module] [-n channels] [<filename>] [<encoding>]')
1189 return
1191 demo(file_path, channels=channels, encoding=encoding)
1194if __name__ == "__main__":
1195 main(*sys.argv[1:])