Coverage for src / audioio / playaudio.py: 40%
828 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 09:39 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 09:39 +0000
1"""Play numpy arrays as audio.
3Accepted data for playback are 1-D or 2-D (frames, channels) numpy
4arrays with values ranging from -1 to 1.
5If necessary data are downsampled automatically to match supported
6sampling rates.
8## Class
10Use the `PlayAudio` class for audio output to a speaker:
12```
13with PlayAudio() as audio:
14 audio.beep()
15```
17or without context management:
19```
20audio = PlayAudio()
21audio.beep(1.0, 'a4')
22audio.close()
23```
25## Functions
27Alternatively, the globally defined functions `play()` and `beep()`
28use the global instance `handle` of the `PlayAudio` class to play a
29sound on the default audio output device.
31- `play()`: playback audio data.
32- `beep()`: playback a tone.
33- `close()`: close the global PlayAudio instance.
36## Helper functions
38- `speaker_devices()`: query available output devices.
39- `print_speaker_devices()`: print available output devices.
40- `fade_in()`: fade in a signal in place.
41- `fade_out()`: fade out a signal in place.
42- `fade()`: fade in and out a signal in place.
43- `note2freq()`: convert textual note to corresponding frequency.
46## Installation
48You might need to install additional packages for better audio output.
49See [installation](https://bendalab.github.io/audioio/installation)
50for further instructions.
53## Demo
55For a demo, run the script as:
56```
57python -m src.audioio.playaudio
58```
60"""
62import os
63import warnings
64import numpy as np
66from sys import platform
67from scipy.signal import decimate
68from time import sleep
69from io import BytesIO
70from multiprocessing import Process
72from .audiomodules import *
75handle = None
76"""Default audio device handler.
78Defaults to `None`. Will get a PlayAudio instance assigned via
79`play()` or `beep()`.
80"""
83def note2freq(note, a4freq=440.0):
84 """Convert textual note to corresponding frequency.
86 Parameters
87 ----------
88 note: string
89 A musical note like 'a4', 'f#3', 'eb5'.
90 The first character is the note, it can be
91 'a', 'b', 'c', 'd', 'e', 'f', or 'g'.
92 The optional second character is either a 'b'
93 or a '#' to decrease or increase by half a note.
94 The last character specifies the octave.
95 'a4' is defined by `a4freq`.
96 a4freq: float
97 The frequency of a4 in Hertz.
99 Returns
100 -------
101 freq: float
102 The frequency of the note in Hertz.
104 Raises
105 ------
106 ValueError
107 No or an invalid note was specified.
108 """
109 freq = a4freq
110 tone = 0
111 octave = 4
112 if not isinstance(note, str) or len(note) == 0:
113 raise ValueError('no note specified')
114 # note:
115 if note[0] < 'a' or note[0] > 'g':
116 raise ValueError('invalid note', note[0])
117 index = 0
118 tonemap = [0, 2, 3, 5, 7, 8, 10]
119 tone = tonemap[ord(note[index]) - ord('a')]
120 index += 1
121 # flat or sharp:
122 flat = False
123 sharp = False
124 if index < len(note):
125 if note[index] == 'b':
126 flat = True
127 tone -= 1
128 index += 1
129 elif note[index] == '#':
130 sharp = True
131 tone += 1
132 index += 1
133 # octave:
134 if index < len(note) and note[index] >= '0' and note[index] <= '9':
135 octave = 0
136 while index < len(note) and note[index] >= '0' and note[index] <= '9':
137 octave *= 10
138 octave += ord(note[index]) - ord('0')
139 index += 1
140 # remaining characters:
141 if index < len(note):
142 raise ValueError('invalid characters in note', note)
143 # compute frequency:
144 if (tone >= 3 and not sharp) or (tone == 2 and flat):
145 octave -= 1
146 tone += 12*(octave-4)
147 # frequency:
148 freq = a4freq * 2.0**(tone/12.0)
149 return freq
152def fade_in(data, rate, fadetime):
153 """Fade in a signal in place.
155 The first `fadetime` seconds of the data are multiplied with a
156 squared sine in place. If `fadetime` is larger than half the
157 duration of the data, then `fadetime` is reduced to half of the
158 duration.
160 Parameters
161 ----------
162 data: array
163 The data to be faded in, either 1-D array for single channel output,
164 or 2-D array with first axis time and second axis channel.
165 rate: float
166 The sampling rate in Hertz.
167 fadetime: float
168 Time for fading in in seconds.
169 """
170 if len(data) < 4:
171 return
172 nr = min(int(np.round(fadetime*rate)), len(data)//2)
173 x = np.arange(float(nr))/float(nr) # 0 to pi/2
174 y = np.sin(0.5*np.pi*x)**2.0
175 if data.ndim > 1:
176 data[:nr, :] *= y[:, None]
177 else:
178 data[:nr] *= y
181def fade_out(data, rate, fadetime):
182 """Fade out a signal in place.
184 The last `fadetime` seconds of the data are multiplied with a
185 squared sine in place. If `fadetime` is larger than half the
186 duration of the data, then `fadetime` is reduced to half of the
187 duration.
189 Parameters
190 ----------
191 data: array
192 The data to be faded out, either 1-D array for single channel output,
193 or 2-D array with first axis time and second axis channel.
194 rate: float
195 The sampling rate in Hertz.
196 fadetime: float
197 Time for fading out in seconds.
198 """
199 if len(data) < 4:
200 return
201 nr = min(int(np.round(fadetime*rate)), len(data)//2)
202 x = np.arange(float(nr))/float(nr) + 1.0 # pi/2 to pi
203 y = np.sin(0.5*np.pi*x)**2.0
204 if data.ndim > 1:
205 data[-nr:, :] *= y[:, None]
206 else:
207 data[-nr:] *= y
210def fade(data, rate, fadetime):
211 """Fade in and out a signal in place.
213 The first and last `fadetime` seconds of the data are multiplied
214 with a squared sine in place. If `fadetime` is larger than half the
215 duration of the data, then `fadetime` is reduced to half of the
216 duration.
218 Parameters
219 ----------
220 data: array
221 The data to be faded, either 1-D array for single channel output,
222 or 2-D array with first axis time and second axis channel.
223 rate: float
224 The sampling rate in Hertz.
225 fadetime: float
226 Time for fading in and out in seconds.
227 """
228 fade_in(data, rate, fadetime)
229 fade_out(data, rate, fadetime)
232class PlayAudio(object):
233 """ Audio playback.
235 Parameters
236 ----------
237 device_index: int or None
238 Index of the playback device to be used.
239 If None take the default device.
240 Use the speaker_devices() function to query available devices.
241 verbose: int
242 Verbosity level.
243 library: str or None
244 If specified, open a specific sound library.
247 Attributes
248 ----------
249 lib: string
250 The library used for playback.
251 verbose: int
252 Verbosity level.
254 Methods
255 -------
256 - `play(data, rate, scale=None, blocking=True)`: Playback audio data.
257 - `beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True)`: Playback a pure tone.
258 - `open()`: Initialize the PlayAudio class with the best module available.
259 - `close()`: Terminate module for playing audio.
260 - `stop()`: Stop any playback in progress.
261 - `active()`: Report whether playback is in progress.
263 Examples
264 --------
265 ```
266 from audioio import PlayAudio
268 with PlayAudio() as audio:
269 audio.beep()
270 ```
271 or without context management:
272 ```
273 audio = PlayAudio()
274 audio.beep(1.0, 'a4')
275 audio.close()
276 ```
277 """
279 def __init__(self, device_index=None, verbose=0, library=None):
280 self.verbose = verbose
281 self.handle = None
282 self._do_play = self._play
283 self.close = self._close
284 self.stop = self._stop
285 self.active = self._active
286 self.lib = None
287 self.open(device_index, library)
289 def _close(self):
290 """Terminate PlayAudio class for playing audio."""
291 self.handle = None
292 self._do_play = self._play
293 self.close = self._close
294 self.stop = self._stop
295 self.lib = None
297 def _stop(self):
298 """Stop any playback in progress."""
299 pass
301 def _active(self):
302 """Report whether playback is in progress.
304 Returns
305 -------
306 active: bool
307 True if audio output is still active.
308 Libraries that do not support this return False independent
309 of whether output is in progress or not.
310 """
311 return False
313 def _play(self, blocking=True):
314 """Default implementation of playing a sound: does nothing."""
315 pass
317 def play(self, data, rate, scale=None, blocking=True, device_index=None):
318 """Playback audio data.
320 Parameters
321 ----------
322 data: array
323 The data to be played, either 1-D array for single channel output,
324 or 2-D array with first axis time and second axis channel.
325 Data values range between -1 and 1.
326 rate: float
327 The sampling rate in Hertz.
328 scale: float
329 Multiply data with scale before playing.
330 If `None` scale it to the maximum value, if 1.0 do not scale.
331 blocking: boolean
332 If False do not block.
333 device_index: int or None
334 Index of the playback device to be used,
335 if not already openend via the constructor.
336 If None take the default device.
338 Raises
339 ------
340 ValueError
341 Invalid sampling rate (after some attemps of resampling).
342 FileNotFoundError
343 No audio device for playback.
344 """
345 if self.handle is None:
346 self.open(device_index)
347 else:
348 self.stop()
349 self.rate = rate
350 self.channels = 1
351 if data.ndim > 1:
352 self.channels = data.shape[1]
353 # convert data:
354 rawdata = data - np.mean(data, axis=0)
355 if scale is None:
356 scale = 1.0/np.max(np.abs(rawdata))
357 rawdata *= scale
358 self.data = np.floor(rawdata*(2**15-1)).astype(np.int16, order='C')
359 self.index = 0
360 self._do_play(blocking)
362 def beep(self, duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0,
363 fadetime=0.05, blocking=True, device_index=None):
364 """Playback a pure tone.
366 Parameters
367 ----------
368 duration: float
369 The duration of the tone in seconds.
370 frequency: float or string
371 If float, the frequency of the tone in Hertz.
372 If string, a musical note like 'f#5'.
373 See `note2freq()` for details.
374 amplitude: float
375 The ampliude (volume) of the tone in the range from 0.0 to 1.0.
376 rate: float
377 The sampling rate in Hertz.
378 fadetime: float
379 Time for fading in and out in seconds.
380 blocking: boolean
381 If False do not block.
382 device_index: int or None
383 Index of the playback device to be used,
384 if not already openend via the constructor.
385 If None take the default device.
387 Raises
388 ------
389 ValueError
390 Invalid sampling rate (after some attemps of resampling).
391 FileNotFoundError
392 No audio device for playback.
394 See also
395 --------
396 https://mail.python.org/pipermail/tutor/2012-September/091529.html
397 for fourier series based construction of waveforms.
398 """
399 # frequency
400 if isinstance(frequency, str):
401 frequency = note2freq(frequency)
402 # sine wave:
403 time = np.arange(0.0, duration, 1.0/rate)
404 data = amplitude*np.sin(2.0*np.pi*frequency*time)
405 # fade in and out:
406 fade(data, rate, fadetime)
407 # # final click for testing (mono only):
408 # data = np.hstack((data, np.sin(2.0*np.pi*1000.0*time[0:int(np.ceil(4.0*rate/1000.0))])))
409 # play:
410 self.play(data, rate, scale=1.0, blocking=blocking,
411 device_index=device_index)
413 def _down_sample(self, channels, scale=1):
414 """Sample the data down and adapt maximum channel number."""
415 iscale = 1
416 rscale = scale
417 if isinstance(scale, int):
418 iscale = scale
419 rscale = 1.0
420 elif scale > 2:
421 iscale = int(np.floor(scale))
422 rscale = scale/iscale
424 if iscale > 1:
425 data = decimate(self.data, iscale, axis=0)
426 if self.data.ndim > 1:
427 self.data = np.asarray(data[:,:channels],
428 dtype=np.int16, order='C')
429 else:
430 self.data = np.asarray(data, dtype=np.int16, order='C')
431 if self.verbose > 0:
432 print(f'decimated sampling rate from {self.rate:.1f}Hz down to {self.rate/iscale:.1f}Hz')
433 self.rate /= iscale
435 if rscale != 1.0:
436 dt0 = 1.0/self.rate
437 dt1 = rscale/self.rate
438 old_time = np.arange(len(self.data))*dt0
439 new_time = np.arange(0.0, old_time[-1]+0.5*dt0, dt1)
440 if self.data.ndim > 1:
441 data = np.zeros((len(new_time), channels), order='C')
442 for c in range(channels):
443 data[:,c] = np.interp(new_time, old_time, self.data[:,c])
444 else:
445 data = np.interp(new_time, old_time, self.data)
446 self.data = np.asarray(data, dtype=self.data.dtype, order='C')
447 if self.verbose > 0:
448 print(f'adapted sampling rate from {self.rate:.1f}Hz to {self.rate/rscale:.1f}Hz')
449 self.rate /= rscale
450 self.channels = channels
452 def __del__(self):
453 """Terminate the audio module."""
454 self.close()
456 def __enter__(self):
457 return self
459 def __exit__(self, type, value, tb):
460 self.__del__()
461 return value
464 def open_pyaudio(self, device_index=None):
465 """Initialize audio output via PyAudio module.
467 Parameters
468 ----------
469 device_index: int or None
470 Index of the playback device to be used.
471 If None take the default device.
473 Raises
474 ------
475 ImportError
476 PyAudio module is not available.
477 FileNotFoundError
478 Failed to open audio device.
480 Documentation
481 -------------
482 https://people.csail.mit.edu/hubert/pyaudio/
483 http://www.portaudio.com/
485 Installation
486 ------------
487 ```
488 sudo apt install -y libportaudio2 portaudio19-dev python-pyaudio python3-pyaudio
489 ```
491 On Windows, download an appropriate (latest version, 32 or 64 bit) wheel from
492 <https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio>. Install this file with pip,
493 that is go to the folder where the wheel file is downloaded and run
494 ```
495 pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
496 ```
497 replace the wheel file name by the one you downloaded.
498 """
499 if not audio_modules['pyaudio']:
500 raise ImportError
501 oldstderr = os.dup(2)
502 os.close(2)
503 tmpfile = 'tmpfile.tmp'
504 os.open(tmpfile, os.O_WRONLY | os.O_CREAT)
505 self.handle = pyaudio.PyAudio()
506 self.stream = None
507 os.close(2)
508 os.dup(oldstderr)
509 os.close(oldstderr)
510 os.remove(tmpfile)
511 try:
512 if device_index is None:
513 info = self.handle.get_default_output_device_info()
514 else:
515 info = self.handle.get_device_info_by_index(device_index)
516 self.max_channels = info['maxOutputChannels']
517 self.default_rate = info['defaultSampleRate']
518 self.device_index = info['index']
519 self.handle.is_format_supported(self.default_rate,
520 output_device=self.device_index,
521 output_channels=1,
522 output_format=pyaudio.paInt16)
523 except Exception as e:
524 if self.verbose > 0:
525 print(str(e))
526 self.handle.terminate()
527 self._close()
528 raise FileNotFoundError('failed to initialize audio device')
529 self.index = 0
530 self.data = None
531 self.close = self._close_pyaudio
532 self.stop = self._stop_pyaudio
533 self.active = self._active_pyaudio
534 self._do_play = self._play_pyaudio
535 self.lib = 'pyaudio'
536 return self
538 def _callback_pyaudio(self, in_data, frames, time_info, status):
539 """Callback for pyaudio for supplying output with data."""
540 flag = pyaudio.paContinue
541 if not self.run:
542 flag = pyaudio.paComplete
543 if self.index < len(self.data):
544 out_data = self.data[self.index:self.index+frames]
545 self.index += len(out_data)
546 # zero padding:
547 if len(out_data) < frames:
548 if self.data.ndim > 1:
549 out_data = np.vstack((out_data,
550 np.zeros((frames-len(out_data), self.channels), dtype=np.int16)))
551 else:
552 out_data = np.hstack((out_data, np.zeros(frames-len(out_data), dtype=np.int16)))
553 return (out_data, flag)
554 else:
555 # we need to play more to make sure everything is played!
556 # This is because of an ALSA bug and might be fixed in newer versions,
557 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html
558 out_data = np.zeros(frames*self.channels, dtype=np.int16)
559 self.index += frames
560 if self.index >= len(self.data) + 2*self.latency:
561 flag = pyaudio.paComplete
562 return (out_data, flag)
564 def _stop_pyaudio(self):
565 """Stop any ongoing activity of the pyaudio module."""
566 if self.stream is not None:
567 if self.stream.is_active():
568 # fade out:
569 fadetime = 0.1
570 nr = int(np.round(fadetime*self.rate))
571 index = self.index+nr
572 if nr > len(self.data) - index:
573 nr = len(self.data) - index
574 else:
575 self.data[index+nr:] = 0
576 if nr > 0:
577 for k in range(nr) :
578 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] *
579 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C')
580 try:
581 sleep(2*fadetime)
582 except SystemError:
583 # pyaudio interferes with sleep in python 3.10
584 pass
585 if self.stream.is_active():
586 self.run = False
587 while self.stream.is_active():
588 try:
589 sleep(0.01)
590 except SystemError:
591 # pyaudio interferes with sleep in python 3.10
592 pass
593 self.stream.stop_stream()
594 self.stream.close()
595 self.stream = None
597 def _active_pyaudio(self):
598 """Report whether playback is in progress.
600 Returns
601 -------
602 active: bool
603 True if audio output is still active.
604 """
605 return self.stream is not None and self.stream.is_active()
607 def _play_pyaudio(self, blocking=True):
608 """Play audio data using the pyaudio module.
610 Parameters
611 ----------
612 blocking: boolean
613 If False do not block.
615 Raises
616 ------
617 ValueError
618 Invalid sampling rate (after some attemps of resampling).
619 """
620 # check channel count:
621 channels = self.channels
622 if self.channels > self.max_channels:
623 channels = self.max_channels
624 # check sampling rate:
625 scale_fac = 1
626 scaled_rate = self.rate
627 max_rate = 48000.0
628 if self.rate > max_rate:
629 scale_fac = int(np.ceil(self.rate/max_rate))
630 scaled_rate = int(self.rate//scale_fac)
631 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate]
632 scales = [1, scale_fac, None, None, None, None]
633 success = False
634 for rate, scale in zip(rates, scales):
635 try:
636 if self.handle.is_format_supported(int(rate),
637 output_device=self.device_index,
638 output_channels=channels,
639 output_format=pyaudio.paInt16):
640 if scale is None:
641 scale = self.rate/float(rate)
642 success = True
643 break
644 except Exception as e:
645 if self.verbose > 0:
646 print(f'invalid sampling rate of {rate}Hz')
647 if e.args[1] != pyaudio.paInvalidSampleRate:
648 raise
649 if not success:
650 raise ValueError('No valid sampling rate found')
651 if channels != self.channels or scale != 1:
652 self._down_sample(channels, scale)
654 # play:
655 self.run = True
656 self.stream = self.handle.open(format=pyaudio.paInt16, channels=self.channels,
657 rate=int(self.rate), output=True,
658 stream_callback=self._callback_pyaudio)
659 self.latency = int(self.stream.get_output_latency()*self.rate)
660 self.stream.start_stream()
661 if blocking:
662 while self.stream.is_active():
663 try:
664 sleep(0.01)
665 except (ValueError, SystemError):
666 # pyaudio interferes with sleep in python 3.10
667 pass
668 self.run = False
669 self.stream.stop_stream()
670 self.stream.close()
671 self.stream = None
673 def _close_pyaudio(self):
674 """Terminate pyaudio module."""
675 self._stop_pyaudio()
676 if self.handle is not None:
677 self.handle.terminate()
678 self._close()
681 def open_sounddevice(self, device_index=None):
682 """Initialize audio output via sounddevice module.
684 Parameters
685 ----------
686 device_index: int or None
687 Index of the playback device to be used.
688 If None take the default device.
690 Raises
691 ------
692 ImportError
693 sounddevice module is not available.
694 FileNotFoundError
695 Failed to open audio device.
697 Documentation
698 -------------
699 https://python-sounddevice.readthedocs.io
701 Installation
702 ------------
703 ```
704 sudo apt install -y libportaudio2 portaudio19-dev
705 sudo pip install sounddevice
706 ```
707 """
708 if not audio_modules['sounddevice']:
709 raise ImportError
710 self.handle = True
711 self.index = 0
712 self.data = None
713 self.stream = None
714 try:
715 if device_index is None:
716 info_in = sounddevice.query_devices(kind='input')
717 info_out = sounddevice.query_devices(kind='output')
718 if info_in['index'] == info_out['index']:
719 info = info_out
720 else:
721 info = info_out
722 if info_in['max_output_channels'] > info_out['max_output_channels']:
723 info = info_in
724 else:
725 info = sounddevice.query_devices(device_index)
726 self.device_index = info['index']
727 self.max_channels = info['max_output_channels']
728 self.default_rate = info['default_samplerate']
729 sounddevice.check_output_settings(device=self.device_index,
730 channels=1, dtype=np.int16,
731 samplerate=48000)
732 except Exception as e:
733 if self.verbose > 0:
734 print(str(e))
735 self._close()
736 raise FileNotFoundError('failed to initialize audio device')
737 self.close = self._close_sounddevice
738 self.stop = self._stop_sounddevice
739 self.active = self._active_sounddevice
740 self._do_play = self._play_sounddevice
741 self.lib = 'sounddevice'
742 return self
744 def _callback_sounddevice(self, out_data, frames, time_info, status):
745 """Callback for sounddevice for supplying output with data."""
746 if status:
747 print(status)
748 if self.index < len(self.data):
749 ndata = len(self.data) - self.index
750 if ndata >= frames :
751 if self.data.ndim <= 1:
752 out_data[:,0] = self.data[self.index:self.index+frames]
753 else:
754 out_data[:, :] = self.data[self.index:self.index+frames, :]
755 self.index += frames
756 else:
757 if self.data.ndim <= 1:
758 out_data[:ndata, 0] = self.data[self.index:]
759 out_data[ndata:, 0] = np.zeros(frames-ndata, dtype=np.int16)
760 else:
761 out_data[:ndata, :] = self.data[self.index:, :]
762 out_data[ndata:, :] = np.zeros((frames-ndata, self.channels),
763 dtype=np.int16)
764 self.index += frames
765 else:
766 # we need to play more to make sure everything is played!
767 # This is because of an ALSA bug and might be fixed in newer versions,
768 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html
769 if self.data.ndim <= 1:
770 out_data[:, 0] = np.zeros(frames, dtype=np.int16)
771 else:
772 out_data[:, :] = np.zeros((frames, self.channels), dtype=np.int16)
773 self.index += frames
774 if self.index >= len(self.data) + 2*self.latency:
775 raise sounddevice.CallbackStop
776 if not self.run:
777 raise sounddevice.CallbackStop
779 def _stop_sounddevice(self):
780 """Stop any ongoing activity of the sounddevice module."""
781 if self.stream is not None:
782 if self.stream.active:
783 # fade out:
784 fadetime = 0.1
785 nr = int(np.round(fadetime*self.rate))
786 index = self.index+nr
787 if nr > len(self.data) - index:
788 nr = len(self.data) - index
789 else:
790 self.data[index+nr:] = 0
791 if nr > 0:
792 for k in range(nr) :
793 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] *
794 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C')
795 sounddevice.sleep(int(2000*fadetime))
796 if self.stream.active:
797 self.run = False
798 while self.stream.active:
799 sounddevice.sleep(10)
800 self.stream.stop()
801 self.stream.close()
802 self.stream = None
804 def _active_sounddevice(self):
805 """Report whether playback is in progress.
807 Returns
808 -------
809 active: bool
810 True if audio output is still active.
811 """
812 return self.stream is not None and self.stream.active
814 def _play_sounddevice(self, blocking=True):
815 """Play audio data using the sounddevice module.
817 Parameters
818 ----------
819 blocking: boolean
820 If False do not block.
822 Raises
823 ------
824 ValueError
825 Invalid sampling rate (after some attemps of resampling).
826 """
827 # check channel count:
828 channels = self.channels
829 if self.channels > self.max_channels:
830 channels = self.max_channels
831 # check sampling rate:
832 scale_fac = 1
833 scaled_rate = self.rate
834 max_rate = 48000.0
835 if self.rate > max_rate:
836 scale_fac = int(np.ceil(self.rate/max_rate))
837 scaled_rate = int(self.rate//scale_fac)
838 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate]
839 scales = [1, scale_fac, None, None, None, None]
840 success = False
841 for rate, scale in zip(rates, scales):
842 try:
843 sounddevice.check_output_settings(device=self.device_index,
844 channels=channels,
845 dtype=np.int16,
846 samplerate=rate)
847 if scale is None:
848 scale = self.rate/float(rate)
849 success = True
850 break
851 except sounddevice.PortAudioError as pae:
852 if pae.args[1] != -9997:
853 raise
854 elif self.verbose > 0:
855 print(f'invalid sampling rate of {rate}Hz')
856 if not success:
857 raise ValueError('No valid sampling rate found')
858 if channels != self.channels or scale != 1:
859 self._down_sample(channels, scale)
861 # play:
862 self.stream = sounddevice.OutputStream(samplerate=self.rate,
863 device=self.device_index,
864 channels=self.channels,
865 dtype=np.int16,
866 callback=self._callback_sounddevice)
867 self.latency = self.stream.latency*self.rate
868 self.run = True
869 self.stream.start()
870 if blocking:
871 while self.stream.active:
872 sounddevice.sleep(10)
873 self.run = False
874 self.stream.stop()
875 self.stream.close()
876 self.stream = None
878 def _close_sounddevice(self):
879 """Terminate sounddevice module."""
880 self._stop_sounddevice()
881 self._close()
884 def open_simpleaudio(self, device_index=None):
885 """Initialize audio output via simpleaudio package.
887 Parameters
888 ----------
889 device_index: int or None
890 Index of the playback device to be used.
891 If None take the default device.
892 Not supported by simpleaudio.
894 Raises
895 ------
896 ImportError
897 simpleaudio module is not available.
899 Documentation
900 -------------
901 https://simpleaudio.readthedocs.io
902 """
903 if not audio_modules['simpleaudio']:
904 raise ImportError
905 self.handle = True
906 self._do_play = self._play_simpleaudio
907 self.close = self._close_simpleaudio
908 self.stop = self._stop_simpleaudio
909 self.active = self._active_simpleaudio
910 self.lib = 'simpleaudio'
911 return self
913 def _stop_simpleaudio(self):
914 """Stop any ongoing activity of the simpleaudio package."""
915 if self.handle is not None and self.handle is not True:
916 self.handle.stop()
918 def _active_simpleaudio(self):
919 """Report whether playback is in progress.
921 Returns
922 -------
923 active: bool
924 True if audio output is still active.
925 """
926 return self.handle is not None and \
927 self.handle is not True and self.handle.is_playing()
929 def _play_simpleaudio(self, blocking=True):
930 """Play audio data using the simpleaudio package.
932 Parameters
933 ----------
934 blocking: boolean
935 If False do not block.
937 Raises
938 ------
939 ValueError
940 Invalid sampling rate (after some attemps of resampling).
941 FileNotFoundError
942 No audio device for playback.
943 """
944 rates = [self.rate, 44100, 48000, 22050]
945 scales = [1, None, None, None]
946 success = False
947 for rate, scale in zip(rates, scales):
948 if scale is None:
949 scale = self.rate/float(rate)
950 if scale != 1:
951 self._down_sample(self.channels, scale)
952 try:
953 self.handle = simpleaudio.play_buffer(self.data, self.channels,
954 2, int(self.rate))
955 success = True
956 break
957 except ValueError as e:
958 if self.verbose > 0:
959 print(f'invalid sampling rate of {rate}Hz')
960 except simpleaudio._simpleaudio.SimpleaudioError as e:
961 if self.verbose > 0:
962 print('simpleaudio SimpleaudioError:', str(e))
963 if 'Error opening' in str(e):
964 raise FileNotFoundError('No audio device found')
965 except Exception as e:
966 if self.verbose > 0:
967 print('simpleaudio Exception:', str(e))
968 if not success:
969 raise ValueError('No valid sampling rate found')
970 elif blocking:
971 self.handle.wait_done()
973 def _close_simpleaudio(self):
974 """Close audio output using simpleaudio package."""
975 self._stop_simpleaudio()
976 simpleaudio.stop_all()
977 self._close()
980 def open_soundcard(self, device_index=None):
981 """Initialize audio output via soundcard package.
983 Parameters
984 ----------
985 device_index: int or None
986 Index of the playback device to be used.
987 If None take the default device.
989 Raises
990 ------
991 ImportError
992 soundcard module is not available.
993 FileNotFoundError
994 Failed to open audio device.
996 Documentation
997 -------------
998 https://github.com/bastibe/SoundCard
999 """
1000 if not audio_modules['soundcard']:
1001 raise ImportError
1002 try:
1003 if device_index is None:
1004 self.handle = soundcard.default_speaker()
1005 else:
1006 self.handle = soundcard.all_speakers()[device_index]
1007 except IndexError:
1008 raise FileNotFoundError('No audio device found')
1009 except Exception as e:
1010 print('soundcard Exception:', type(e).__name__, str(e))
1011 if self.handle is None:
1012 raise FileNotFoundError('No audio device found')
1013 self._do_play = self._play_soundcard
1014 self.close = self._close_soundcard
1015 self.stop = self._stop_soundcard
1016 self.active = self._active # not supported ?
1017 self.lib = 'soundcard'
1018 return self
1020 def _stop_soundcard(self):
1021 """Stop any ongoing activity of the soundcard package."""
1022 pass
1024 def _play_soundcard(self, blocking=True):
1025 """Play audio data using the soundcard package.
1027 Parameters
1028 ----------
1029 blocking: boolean
1030 If False do not block.
1031 Non-blocking playback not supported by soundcard.
1032 Return immediately without playing sound.
1034 Raises
1035 ------
1036 ValueError
1037 Invalid sampling rate (after some attemps of resampling).
1038 """
1039 if not blocking:
1040 warnings.warn('soundcard module does not support non-blocking playback')
1041 return
1042 rates = [self.rate, 44100, 48000, 22050]
1043 scales = [1, None, None, None]
1044 success = False
1045 for rate, scale in zip(rates, scales):
1046 if scale is None:
1047 scale = self.rate/float(rate)
1048 if scale != 1:
1049 self._down_sample(self.channels, scale)
1050 try:
1051 self.handle.play(self.data, samplerate=int(self.rate))
1052 success = True
1053 break
1054 except RuntimeError as e:
1055 if 'invalid sample spec' in str(e):
1056 if self.verbose > 0:
1057 print(f'invalid sampling rate of {rate}Hz')
1058 else:
1059 if self.verbose > 0:
1060 print('soundcard error:', type(e).__name__, str(e))
1061 except Exception as e:
1062 if self.verbose > 0:
1063 print('soundcard error:', type(e).__name__, str(e))
1064 if not success:
1065 raise ValueError('No valid sampling rate found')
1067 def _close_soundcard(self):
1068 """Close audio output using soundcard package."""
1069 self._stop_soundcard()
1070 self._close()
1073 def open_ossaudiodev(self, device_index=None):
1074 """Initialize audio output via ossaudiodev module.
1076 The OSS audio module is part of the python standard library.
1078 Parameters
1079 ----------
1080 device_index: int or None
1081 Index of the playback device to be used.
1082 If None take the default device.
1083 There is only a single OSS audio device.
1085 Raises
1086 ------
1087 ImportError
1088 ossaudiodev module is not available.
1089 FileNotFoundError
1090 Failed to open audio device.
1092 Documentation
1093 -------------
1094 https://docs.python.org/2/library/ossaudiodev.html
1096 Installation
1097 ------------
1098 The ossaudiodev module needs an oss `/dev/dsp` device file.
1099 Enable an oss emulation via alsa by installing
1100 ```
1101 sudo apt install -y osspd
1102 ```
1103 """
1104 if not audio_modules['ossaudiodev']:
1105 raise ImportError
1106 self.handle = True
1107 self.osshandle = None
1108 self.run = False
1109 self.play_thread = None
1110 try:
1111 handle = ossaudiodev.open('w')
1112 handle.close()
1113 except Exception as e:
1114 if self.verbose > 0:
1115 print(str(e))
1116 self._close()
1117 raise FileNotFoundError('failed to initialize audio device')
1118 self.close = self._close_ossaudiodev
1119 self.stop = self._stop_ossaudiodev
1120 self.active = self._active_ossaudiodev
1121 self._do_play = self._play_ossaudiodev
1122 self.lib = 'ossaudiodev'
1123 return self
1125 def _stop_ossaudiodev(self):
1126 """Stop any ongoing activity of the ossaudiodev module."""
1127 if self.osshandle is not None:
1128 self.run = False
1129 self.osshandle.reset()
1130 if self.play_thread is not None:
1131 if self.play_thread.is_alive():
1132 self.play_thread.join()
1133 self.play_thread = None
1134 self.osshandle.close()
1135 self.osshandle = None
1137 def _active_ossaudiodev(self):
1138 """Report whether playback is in progress.
1140 Returns
1141 -------
1142 active: bool
1143 True if audio output is still active.
1144 """
1145 return self.run
1147 def _run_play_ossaudiodev(self):
1148 """Play the data using the ossaudiodev module."""
1149 self.osshandle.writeall(self.data)
1150 if self.run:
1151 sleep(0.5)
1152 self.osshandle.close()
1153 self.osshandle = None
1154 self.run = False
1156 def _play_ossaudiodev(self, blocking=True):
1157 """Play audio data using the ossaudiodev module.
1159 Raises
1160 ------
1161 ValueError
1162 Invalid sampling rate (after some attemps of resampling).
1164 Parameters
1165 ----------
1166 blocking: boolean
1167 If False do not block.
1168 """
1169 self.osshandle = ossaudiodev.open('w')
1170 self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE)
1171 # set and check channel count:
1172 channels = self.osshandle.channels(self.channels)
1173 # check sampling rate:
1174 scale_fac = 1
1175 scaled_rate = self.rate
1176 max_rate = 48000.0
1177 if self.rate > max_rate:
1178 scale_fac = int(np.ceil(self.rate/max_rate))
1179 scaled_rate = int(self.rate//scale_fac)
1180 rates = [self.rate, scaled_rate, 44100, 48000, 22050, 8000]
1181 scales = [1, scale_fac, None, None, None, None]
1182 success = False
1183 for rate, scale in zip(rates, scales):
1184 set_rate = self.osshandle.speed(int(rate))
1185 if abs(set_rate - rate) < 2:
1186 if scale is None:
1187 scale = self.rate/float(set_rate)
1188 success = True
1189 break
1190 else:
1191 if self.verbose > 0:
1192 print(f'invalid sampling rate of {rate}Hz')
1193 if not success:
1194 raise ValueError('No valid sampling rate found')
1195 if channels != self.channels or scale != 1:
1196 self._down_sample(channels, scale)
1197 if blocking:
1198 self.run = True
1199 self.osshandle.writeall(self.data)
1200 sleep(0.5)
1201 self.osshandle.close()
1202 self.run = False
1203 self.osshandle = None
1204 else:
1205 self.play_thread = Process(target=self._run_play_ossaudiodev)
1206 self.run = True
1207 self.play_thread.start()
1209 def _close_ossaudiodev(self):
1210 """Close audio output using ossaudiodev module."""
1211 self._stop_ossaudiodev()
1212 self._close()
1215 def open_winsound(self, device_index=None):
1216 """Initialize audio output via winsound module.
1218 The winsound module is part of the python standard library.
1220 Parameters
1221 ----------
1222 device_index: int or None
1223 Index of the playback device to be used.
1224 If None take the default device.
1225 Device selection is not supported by the winsound module.
1227 Raises
1228 ------
1229 ImportError
1230 winsound module is not available.
1232 Documentation
1233 -------------
1234 https://docs.python.org/3.6/library/winsound.html
1235 https://mail.python.org/pipermail/tutor/2012-September/091529.html
1236 """
1237 if not audio_modules['winsound'] or not audio_modules['wave']:
1238 raise ImportError
1239 self.handle = True
1240 self._do_play = self._play_winsound
1241 self.close = self._close_winsound
1242 self.stop = self._stop_winsound
1243 self.active = self._active # not supported
1244 self.audio_file = ''
1245 self.lib = 'winsound'
1246 return self
1248 def _stop_winsound(self):
1249 """Stop any ongoing activity of the winsound module."""
1250 try:
1251 winsound.PlaySound(None, winsound.SND_MEMORY)
1252 except Exception as e:
1253 pass
1255 def _play_winsound(self, blocking=True):
1256 """Play audio data using the winsound module.
1258 Parameters
1259 ----------
1260 blocking: boolean
1261 If False do not block.
1262 """
1263 # play file:
1264 if blocking:
1265 # write data as wav file to memory:
1266 self.data_buffer = BytesIO()
1267 w = wave.open(self.data_buffer, 'w')
1268 w.setnchannels(self.channels)
1269 w.setsampwidth(2)
1270 w.setframerate(int(self.rate))
1271 w.setnframes(len(self.data))
1272 try:
1273 w.writeframes(self.data.tobytes())
1274 except AttributeError:
1275 w.writeframes(self.data.tostring())
1276 w.close()
1277 try:
1278 winsound.PlaySound(self.data_buffer.getvalue(), winsound.SND_MEMORY)
1279 except Exception as e:
1280 if self.verbose > 0:
1281 print(str(e))
1282 return
1283 else:
1284 if self.verbose > 0:
1285 print('Warning: asynchronous playback is limited to playing wav files by the winsound module. Install an alternative package as recommended by the audiomodules script. ')
1286 # write data as wav file to file:
1287 self.audio_file = 'audioio-async_playback.wav'
1288 w = wave.open(self.audio_file, 'w')
1289 w.setnchannels(self.channels)
1290 w.setsampwidth(2)
1291 w.setframerate(int(self.rate))
1292 w.setnframes(len(self.data))
1293 try:
1294 w.writeframes(self.data.tobytes())
1295 except AttributeError:
1296 w.writeframes(self.data.tostring())
1297 w.close()
1298 try:
1299 winsound.PlaySound(self.audio_file, winsound.SND_ASYNC)
1300 except Exception as e:
1301 if self.verbose > 0:
1302 print(str(e))
1303 return
1305 def _close_winsound(self):
1306 """Close audio output using winsound module."""
1307 self._stop_winsound()
1308 self.handle = None
1309 if len(self.audio_file) > 0 and os.path.isfile(self.audio_file):
1310 os.remove(self.audio_file)
1311 self._close()
1314 def open(self, device_index=None, library=None):
1315 """Initialize the PlayAudio class with the best module available.
1317 Parameters
1318 ----------
1319 device_index: int or None
1320 Index of the playback device to be used.
1321 If None take the default device.
1322 library: str or None
1323 If specified, open a specific sound library.
1324 """
1325 # list of implemented play functions:
1326 audio_open = [
1327 ['sounddevice', self.open_sounddevice],
1328 ['pyaudio', self.open_pyaudio],
1329 ['simpleaudio', self.open_simpleaudio],
1330 ['soundcard', self.open_soundcard],
1331 ['ossaudiodev', self.open_ossaudiodev],
1332 ['winsound', self.open_winsound]
1333 ]
1334 if platform[0:3] == "win":
1335 sa = audio_open.pop(2)
1336 audio_open.insert(0, sa)
1337 # open audio device by trying various modules:
1338 success = False
1339 for lib, open_device in audio_open:
1340 if library and library != lib:
1341 continue
1342 if not audio_modules[lib]:
1343 if self.verbose > 0:
1344 print(f'module {lib} not available')
1345 continue
1346 try:
1347 open_device(device_index)
1348 success = True
1349 if self.verbose > 0:
1350 print(f'successfully opened {lib} module for playing')
1351 break
1352 except Exception as e:
1353 if self.verbose > 0:
1354 print(f'failed to open {lib} module for playing:',
1355 type(e).__name__, str(e))
1356 if not success:
1357 warnings.warn('cannot open any device for audio output')
1358 return self
1361def play(data, rate, scale=None, blocking=True, device_index=None, verbose=0):
1362 """Playback audio data.
1364 Create a `PlayAudio` instance on the global variable `handle`.
1366 Parameters
1367 ----------
1368 data: array
1369 The data to be played, either 1-D array for single channel output,
1370 or 2-D array with first axis time and second axis channel.
1371 Data values range between -1 and 1.
1372 rate: float
1373 The sampling rate in Hertz.
1374 scale: float
1375 Multiply data with scale before playing.
1376 If `None` scale it to the maximum value, if 1.0 do not scale.
1377 blocking: boolean
1378 If False do not block.
1379 device_index: int or None
1380 Index of the playback device to be used,
1381 if not already openend.
1382 If None take the default device.
1383 verbose: int
1384 Verbosity level.
1385 """
1386 global handle
1387 if handle is None:
1388 handle = PlayAudio(device_index, verbose)
1389 handle.verbose = verbose
1390 handle.play(data, rate, scale, blocking, device_index)
1393def beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0,
1394 fadetime=0.05, blocking=True, device_index=None, verbose=0):
1395 """Playback a tone.
1397 Create a `PlayAudio` instance on the global variable `handle`.
1399 Parameters
1400 ----------
1401 duration: float
1402 The duration of the tone in seconds.
1403 frequency: float or string
1404 If float the frequency of the tone in Hertz.
1405 If string, a musical note like 'f#5'.
1406 See `note2freq()` for details
1407 amplitude: float
1408 The ampliude (volume) of the tone from 0.0 to 1.0.
1409 rate: float
1410 The sampling rate in Hertz.
1411 fadetime: float
1412 Time for fading in and out in seconds.
1413 blocking: boolean
1414 If False do not block.
1415 device_index: int or None
1416 Index of the playback device to be used,
1417 if not already openend.
1418 If None take the default device.
1419 verbose: int
1420 Verbosity level.
1421 """
1422 global handle
1423 if handle is None:
1424 handle = PlayAudio(device_index, verbose)
1425 handle.verbose = verbose
1426 handle.beep(duration, frequency, amplitude, rate, fadetime, blocking,
1427 device_index)
1430def close():
1431 """Close the global PlayAudio instance.
1432 """
1433 global handle
1434 if handle is not None:
1435 handle.close()
1436 handle = None
1439def speaker_devices_pyaudio():
1440 """Query available output devices of the pyaudio module.
1442 Returns
1443 -------
1444 indices: list of int
1445 Device indices.
1446 devices: list of str
1447 Devices corresponding to `indices`.
1448 default_device: int
1449 Index of default device.
1450 -1 if no default output device is available.
1451 """
1452 if not audio_modules['pyaudio']:
1453 raise ImportError
1454 oldstderr = os.dup(2)
1455 os.close(2)
1456 tmpfile = 'tmpfile.tmp'
1457 os.open(tmpfile, os.O_WRONLY | os.O_CREAT)
1458 pa = pyaudio.PyAudio()
1459 os.close(2)
1460 os.dup(oldstderr)
1461 os.close(oldstderr)
1462 os.remove(tmpfile)
1463 indices = []
1464 devices = []
1465 for i in range(pa.get_device_count()):
1466 info = pa.get_device_info_by_index(i)
1467 if info['maxOutputChannels'] > 0:
1468 host = sounddevice.query_hostapis(info['hostApi'])['name']
1469 device = f'{info["name"]}, {host} ({info["maxInputChannels"]} in, {info["maxOutputChannels"]} out)'
1470 indices.append(info['index'])
1471 devices.append(device)
1472 try:
1473 default_device = pa.get_default_output_device_info()['index']
1474 except OSError:
1475 default_device = -1
1476 return indices, devices, default_device
1478def speaker_devices_sounddevice():
1479 """Query available output devices of the sounddevice module.
1481 Returns
1482 -------
1483 indices: list of int
1484 Device indices.
1485 devices: list of str
1486 Devices corresponding to `indices`.
1487 default_device: int
1488 Index of default device.
1489 """
1490 if not audio_modules['sounddevice']:
1491 raise ImportError
1492 indices = []
1493 devices = []
1494 infos = sounddevice.query_devices()
1495 for info in infos:
1496 if info['max_output_channels'] > 0:
1497 host = sounddevice.query_hostapis(info['hostapi'])['name']
1498 device = f'{info["name"]}, {host} ({info["max_input_channels"]} in, {info["max_output_channels"]} out)'
1499 indices.append(info['index'])
1500 devices.append(device)
1501 try:
1502 info_out = sounddevice.query_devices(kind='output')
1503 except sounddevice.PortAudioError:
1504 return indices, devices, -1
1505 try:
1506 info_in = sounddevice.query_devices(kind='input')
1507 if info_in['index'] != info_out['index'] and \
1508 info_in['max_output_channels'] > info_out['max_output_channels']:
1509 info_out = info_in
1510 except sounddevice.PortAudioError:
1511 pass
1512 return indices, devices, info_out['index']
1514def speaker_devices_soundcard():
1515 """Query available output devices of the soundcard module.
1517 Returns
1518 -------
1519 indices: list of int
1520 Device indices.
1521 devices: list of str
1522 Devices corresponding to `indices`.
1523 default_device: int
1524 Index of default device.
1525 """
1526 if not audio_modules['soundcard']:
1527 raise ImportError
1528 indices = []
1529 devices = []
1530 infos = soundcard.all_speakers()
1531 def_speaker = str(soundcard.default_speaker())
1532 default_device = -1
1533 for i, info in enumerate(infos):
1534 if str(info) == def_speaker:
1535 default_device = i
1536 indices.append(i)
1537 devices.append(str(info).lstrip('<').rstrip('>'))
1538 return indices, devices, default_device
1540def speaker_devices(library=None, verbose=0):
1541 """Query available output devices.
1543 Parameters
1544 ----------
1545 library: str or None
1546 If specified, use specific sound library.
1547 verbose: int
1548 Verbosity level.
1550 Returns
1551 -------
1552 indices: list of int
1553 Device indices.
1554 devices: list of str
1555 Devices corresponding to `indices`.
1556 default_device: int
1557 Index of default device.
1558 """
1559 # list of implemented list functions:
1560 audio_devices = [
1561 ['sounddevice', speaker_devices_sounddevice],
1562 ['pyaudio', speaker_devices_pyaudio],
1563 ['simpleaudio', None],
1564 ['soundcard', speaker_devices_soundcard],
1565 ['ossaudiodev', None],
1566 ['winsound', None]
1567 ]
1568 if platform[0:3] == "win":
1569 sa = audio_open.pop(2)
1570 audio_open.insert(0, sa)
1571 # query audio devices by trying various modules:
1572 success = False
1573 for lib, devices in audio_devices:
1574 if library and library != lib:
1575 continue
1576 if not audio_modules[lib]:
1577 if verbose > 0:
1578 print(f'module {lib} not available')
1579 continue
1580 if devices is None:
1581 return [0], ['default output device'], 0
1582 else:
1583 return devices()
1584 warnings.warn('no library for audio output available for devices')
1585 return [], [], -1
1588def print_speaker_devices(library=None):
1589 """Print available output devices.
1591 Parameters
1592 ----------
1593 library: str or None
1594 If specified, use specific sound library.
1595 """
1596 indices, devices, default_device = speaker_devices()
1597 for i, d in zip(indices, devices):
1598 if i == default_device:
1599 print(f'* {i:2d}: {d}')
1600 else:
1601 print(f' {i:2d}: {d}')
1604def demo(device_index=None):
1605 """ Demonstrate the playaudio module."""
1606 print('play mono beep 1')
1607 audio = PlayAudio(device_index, verbose=2)
1608 audio.beep(1.0, 440.0)
1609 audio.close()
1611 print('play mono beep 2')
1612 with PlayAudio(device_index) as audio:
1613 audio.beep(1.0, 'b4', 0.75, blocking=False)
1614 print(' done')
1615 sleep(0.3)
1616 sleep(0.5)
1618 print('play mono beep 3')
1619 beep(1.0, 'c5', 0.25, blocking=False, device_index=device_index)
1620 print(' done')
1621 sleep(0.5)
1623 print('play stereo beep')
1624 duration = 1.0
1625 rate = 44100.0
1626 t = np.arange(0.0, duration, 1.0/rate)
1627 data = np.zeros((len(t),2))
1628 data[:,0] = np.sin(2.0*np.pi*note2freq('a4')*t)
1629 data[:,1] = 0.25*np.sin(2.0*np.pi*note2freq('e5')*t)
1630 fade(data, rate, 0.1)
1631 play(data, rate, verbose=2, device_index=device_index)
1634def main(*args):
1635 """Call demo with command line arguments.
1637 Parameters
1638 ----------
1639 args: list of strings
1640 Command line arguments as provided by sys.argv[1:]
1641 """
1642 help = False
1643 mod = False
1644 dev = False
1645 ldev = False
1646 device_index = None
1647 for arg in args:
1648 if mod:
1649 if not select_module(arg):
1650 print(f'module {arg} not installed. Exit!')
1651 return
1652 mod = False
1653 elif dev:
1654 device_index = int(arg)
1655 dev = False
1656 elif arg == '-h':
1657 help = True
1658 break
1659 elif arg == '-l':
1660 ldev = True
1661 break
1662 elif arg == '-m':
1663 mod = True
1664 elif arg == '-d':
1665 dev = True
1666 else:
1667 break
1669 if help:
1670 print()
1671 print('Usage:')
1672 print(' python -m src.audioio.playaudio [-m <module>] [-l] [-d <device index>]')
1673 print(' -m: audio module to be used')
1674 print(' -l: list available audio output devices')
1675 print(' -d: set audio output device to be used')
1676 return
1678 if ldev:
1679 print_speaker_devices()
1680 return
1682 demo(device_index)
1685if __name__ == "__main__":
1686 import sys
1687 main(*sys.argv[1:])