Coverage for src / audioio / playaudio.py: 40%
811 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-17 21:34 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-17 21:34 +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.
262 Examples
263 --------
264 ```
265 from audioio import PlayAudio
267 with PlayAudio() as audio:
268 audio.beep()
269 ```
270 or without context management:
271 ```
272 audio = PlayAudio()
273 audio.beep(1.0, 'a4')
274 audio.close()
275 ```
276 """
278 def __init__(self, device_index=None, verbose=0, library=None):
279 self.verbose = verbose
280 self.handle = None
281 self._do_play = self._play
282 self.close = self._close
283 self.stop = self._stop
284 self.lib = None
285 self.open(device_index, library)
287 def _close(self):
288 """Terminate PlayAudio class for playing audio."""
289 self.handle = None
290 self._do_play = self._play
291 self.close = self._close
292 self.stop = self._stop
293 self.lib = None
295 def _stop(self):
296 """Stop any playback in progress."""
297 pass
299 def _play(self, blocking=True):
300 """Default implementation of playing a sound: does nothing."""
301 pass
303 def play(self, data, rate, scale=None, blocking=True, device_index=None):
304 """Playback audio data.
306 Parameters
307 ----------
308 data: array
309 The data to be played, either 1-D array for single channel output,
310 or 2-D array with first axis time and second axis channel.
311 Data values range between -1 and 1.
312 rate: float
313 The sampling rate in Hertz.
314 scale: float
315 Multiply data with scale before playing.
316 If `None` scale it to the maximum value, if 1.0 do not scale.
317 blocking: boolean
318 If False do not block.
319 device_index: int or None
320 Index of the playback device to be used,
321 if not already openend via the constructor.
322 If None take the default device.
324 Raises
325 ------
326 ValueError
327 Invalid sampling rate (after some attemps of resampling).
328 FileNotFoundError
329 No audio device for playback.
330 """
331 if self.handle is None:
332 self.open(device_index)
333 else:
334 self.stop()
335 self.rate = rate
336 self.channels = 1
337 if data.ndim > 1:
338 self.channels = data.shape[1]
339 # convert data:
340 rawdata = data - np.mean(data, axis=0)
341 if scale is None:
342 scale = 1.0/np.max(np.abs(rawdata))
343 rawdata *= scale
344 self.data = np.floor(rawdata*(2**15-1)).astype(np.int16, order='C')
345 self.index = 0
346 self._do_play(blocking)
348 def beep(self, duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0,
349 fadetime=0.05, blocking=True, device_index=None):
350 """Playback a pure tone.
352 Parameters
353 ----------
354 duration: float
355 The duration of the tone in seconds.
356 frequency: float or string
357 If float, the frequency of the tone in Hertz.
358 If string, a musical note like 'f#5'.
359 See `note2freq()` for details.
360 amplitude: float
361 The ampliude (volume) of the tone in the range from 0.0 to 1.0.
362 rate: float
363 The sampling rate in Hertz.
364 fadetime: float
365 Time for fading in and out in seconds.
366 blocking: boolean
367 If False do not block.
368 device_index: int or None
369 Index of the playback device to be used,
370 if not already openend via the constructor.
371 If None take the default device.
373 Raises
374 ------
375 ValueError
376 Invalid sampling rate (after some attemps of resampling).
377 FileNotFoundError
378 No audio device for playback.
380 See also
381 --------
382 https://mail.python.org/pipermail/tutor/2012-September/091529.html
383 for fourier series based construction of waveforms.
384 """
385 # frequency
386 if isinstance(frequency, str):
387 frequency = note2freq(frequency)
388 # sine wave:
389 time = np.arange(0.0, duration, 1.0/rate)
390 data = amplitude*np.sin(2.0*np.pi*frequency*time)
391 # fade in and out:
392 fade(data, rate, fadetime)
393 # # final click for testing (mono only):
394 # data = np.hstack((data, np.sin(2.0*np.pi*1000.0*time[0:int(np.ceil(4.0*rate/1000.0))])))
395 # play:
396 self.play(data, rate, scale=1.0, blocking=blocking,
397 device_index=device_index)
399 def _down_sample(self, channels, scale=1):
400 """Sample the data down and adapt maximum channel number."""
401 iscale = 1
402 rscale = scale
403 if isinstance(scale, int):
404 iscale = scale
405 rscale = 1.0
406 elif scale > 2:
407 iscale = int(np.floor(scale))
408 rscale = scale/iscale
410 if iscale > 1:
411 data = decimate(self.data, iscale, axis=0)
412 if self.data.ndim > 1:
413 self.data = np.asarray(data[:,:channels],
414 dtype=np.int16, order='C')
415 else:
416 self.data = np.asarray(data, dtype=np.int16, order='C')
417 if self.verbose > 0:
418 print(f'decimated sampling rate from {self.rate:.1f}Hz down to {self.rate/iscale:.1f}Hz')
419 self.rate /= iscale
421 if rscale != 1.0:
422 dt0 = 1.0/self.rate
423 dt1 = rscale/self.rate
424 old_time = np.arange(len(self.data))*dt0
425 new_time = np.arange(0.0, old_time[-1]+0.5*dt0, dt1)
426 if self.data.ndim > 1:
427 data = np.zeros((len(new_time), channels), order='C')
428 for c in range(channels):
429 data[:,c] = np.interp(new_time, old_time, self.data[:,c])
430 else:
431 data = np.interp(new_time, old_time, self.data)
432 self.data = np.asarray(data, dtype=self.data.dtype, order='C')
433 if self.verbose > 0:
434 print(f'adapted sampling rate from {self.rate:.1f}Hz to {self.rate/rscale:.1f}Hz')
435 self.rate /= rscale
436 self.channels = channels
438 def __del__(self):
439 """Terminate the audio module."""
440 self.close()
442 def __enter__(self):
443 return self
445 def __exit__(self, type, value, tb):
446 self.__del__()
447 return value
450 def open_pyaudio(self, device_index=None):
451 """Initialize audio output via PyAudio module.
453 Parameters
454 ----------
455 device_index: int or None
456 Index of the playback device to be used.
457 If None take the default device.
459 Raises
460 ------
461 ImportError
462 PyAudio module is not available.
463 FileNotFoundError
464 Failed to open audio device.
466 Documentation
467 -------------
468 https://people.csail.mit.edu/hubert/pyaudio/
469 http://www.portaudio.com/
471 Installation
472 ------------
473 ```
474 sudo apt install -y libportaudio2 portaudio19-dev python-pyaudio python3-pyaudio
475 ```
477 On Windows, download an appropriate (latest version, 32 or 64 bit) wheel from
478 <https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio>. Install this file with pip,
479 that is go to the folder where the wheel file is downloaded and run
480 ```
481 pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
482 ```
483 replace the wheel file name by the one you downloaded.
484 """
485 if not audio_modules['pyaudio']:
486 raise ImportError
487 oldstderr = os.dup(2)
488 os.close(2)
489 tmpfile = 'tmpfile.tmp'
490 os.open(tmpfile, os.O_WRONLY | os.O_CREAT)
491 self.handle = pyaudio.PyAudio()
492 self.stream = None
493 os.close(2)
494 os.dup(oldstderr)
495 os.close(oldstderr)
496 os.remove(tmpfile)
497 try:
498 if device_index is None:
499 info = self.handle.get_default_output_device_info()
500 else:
501 info = self.handle.get_device_info_by_index(device_index)
502 self.max_channels = info['maxOutputChannels']
503 self.default_rate = info['defaultSampleRate']
504 self.device_index = info['index']
505 self.handle.is_format_supported(self.default_rate,
506 output_device=self.device_index,
507 output_channels=1,
508 output_format=pyaudio.paInt16)
509 except Exception as e:
510 if self.verbose > 0:
511 print(str(e))
512 self.handle.terminate()
513 self._close()
514 raise FileNotFoundError('failed to initialize audio device')
515 self.index = 0
516 self.data = None
517 self.close = self._close_pyaudio
518 self.stop = self._stop_pyaudio
519 self._do_play = self._play_pyaudio
520 self.lib = 'pyaudio'
521 return self
523 def _callback_pyaudio(self, in_data, frames, time_info, status):
524 """Callback for pyaudio for supplying output with data."""
525 flag = pyaudio.paContinue
526 if not self.run:
527 flag = pyaudio.paComplete
528 if self.index < len(self.data):
529 out_data = self.data[self.index:self.index+frames]
530 self.index += len(out_data)
531 # zero padding:
532 if len(out_data) < frames:
533 if self.data.ndim > 1:
534 out_data = np.vstack((out_data,
535 np.zeros((frames-len(out_data), self.channels), dtype=np.int16)))
536 else:
537 out_data = np.hstack((out_data, np.zeros(frames-len(out_data), dtype=np.int16)))
538 return (out_data, flag)
539 else:
540 # we need to play more to make sure everything is played!
541 # This is because of an ALSA bug and might be fixed in newer versions,
542 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html
543 out_data = np.zeros(frames*self.channels, dtype=np.int16)
544 self.index += frames
545 if self.index >= len(self.data) + 2*self.latency:
546 flag = pyaudio.paComplete
547 return (out_data, flag)
549 def _stop_pyaudio(self):
550 """Stop any ongoing activity of the pyaudio module."""
551 if self.stream is not None:
552 if self.stream.is_active():
553 # fade out:
554 fadetime = 0.1
555 nr = int(np.round(fadetime*self.rate))
556 index = self.index+nr
557 if nr > len(self.data) - index:
558 nr = len(self.data) - index
559 else:
560 self.data[index+nr:] = 0
561 if nr > 0:
562 for k in range(nr) :
563 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] *
564 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C')
565 try:
566 sleep(2*fadetime)
567 except SystemError:
568 # pyaudio interferes with sleep in python 3.10
569 pass
570 if self.stream.is_active():
571 self.run = False
572 while self.stream.is_active():
573 try:
574 sleep(0.01)
575 except SystemError:
576 # pyaudio interferes with sleep in python 3.10
577 pass
578 self.stream.stop_stream()
579 self.stream.close()
580 self.stream = None
582 def _play_pyaudio(self, blocking=True):
583 """Play audio data using the pyaudio module.
585 Parameters
586 ----------
587 blocking: boolean
588 If False do not block.
590 Raises
591 ------
592 ValueError
593 Invalid sampling rate (after some attemps of resampling).
594 """
595 # check channel count:
596 channels = self.channels
597 if self.channels > self.max_channels:
598 channels = self.max_channels
599 # check sampling rate:
600 scale_fac = 1
601 scaled_rate = self.rate
602 max_rate = 48000.0
603 if self.rate > max_rate:
604 scale_fac = int(np.ceil(self.rate/max_rate))
605 scaled_rate = int(self.rate//scale_fac)
606 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate]
607 scales = [1, scale_fac, None, None, None, None]
608 success = False
609 for rate, scale in zip(rates, scales):
610 try:
611 if self.handle.is_format_supported(int(rate),
612 output_device=self.device_index,
613 output_channels=channels,
614 output_format=pyaudio.paInt16):
615 if scale is None:
616 scale = self.rate/float(rate)
617 success = True
618 break
619 except Exception as e:
620 if self.verbose > 0:
621 print(f'invalid sampling rate of {rate}Hz')
622 if e.args[1] != pyaudio.paInvalidSampleRate:
623 raise
624 if not success:
625 raise ValueError('No valid sampling rate found')
626 if channels != self.channels or scale != 1:
627 self._down_sample(channels, scale)
629 # play:
630 self.run = True
631 self.stream = self.handle.open(format=pyaudio.paInt16, channels=self.channels,
632 rate=int(self.rate), output=True,
633 stream_callback=self._callback_pyaudio)
634 self.latency = int(self.stream.get_output_latency()*self.rate)
635 self.stream.start_stream()
636 if blocking:
637 while self.stream.is_active():
638 try:
639 sleep(0.01)
640 except (ValueError, SystemError):
641 # pyaudio interferes with sleep in python 3.10
642 pass
643 self.run = False
644 self.stream.stop_stream()
645 self.stream.close()
646 self.stream = None
648 def _close_pyaudio(self):
649 """Terminate pyaudio module."""
650 self._stop_pyaudio()
651 if self.handle is not None:
652 self.handle.terminate()
653 self._close()
656 def open_sounddevice(self, device_index=None):
657 """Initialize audio output via sounddevice module.
659 Parameters
660 ----------
661 device_index: int or None
662 Index of the playback device to be used.
663 If None take the default device.
665 Raises
666 ------
667 ImportError
668 sounddevice module is not available.
669 FileNotFoundError
670 Failed to open audio device.
672 Documentation
673 -------------
674 https://python-sounddevice.readthedocs.io
676 Installation
677 ------------
678 ```
679 sudo apt install -y libportaudio2 portaudio19-dev
680 sudo pip install sounddevice
681 ```
682 """
683 if not audio_modules['sounddevice']:
684 raise ImportError
685 self.handle = True
686 self.index = 0
687 self.data = None
688 self.stream = None
689 try:
690 if device_index is None:
691 info_in = sounddevice.query_devices(kind='input')
692 info_out = sounddevice.query_devices(kind='output')
693 if info_in['index'] == info_out['index']:
694 info = info_out
695 else:
696 info = info_out
697 if info_in['max_output_channels'] > info_out['max_output_channels']:
698 info = info_in
699 else:
700 info = sounddevice.query_devices(device_index)
701 self.device_index = info['index']
702 self.max_channels = info['max_output_channels']
703 self.default_rate = info['default_samplerate']
704 sounddevice.check_output_settings(device=self.device_index,
705 channels=1, dtype=np.int16,
706 samplerate=48000)
707 except Exception as e:
708 if self.verbose > 0:
709 print(str(e))
710 self._close()
711 raise FileNotFoundError('failed to initialize audio device')
712 self.close = self._close_sounddevice
713 self.stop = self._stop_sounddevice
714 self._do_play = self._play_sounddevice
715 self.lib = 'sounddevice'
716 return self
718 def _callback_sounddevice(self, out_data, frames, time_info, status):
719 """Callback for sounddevice for supplying output with data."""
720 if status:
721 print(status)
722 if self.index < len(self.data):
723 ndata = len(self.data) - self.index
724 if ndata >= frames :
725 if self.data.ndim <= 1:
726 out_data[:,0] = self.data[self.index:self.index+frames]
727 else:
728 out_data[:, :] = self.data[self.index:self.index+frames, :]
729 self.index += frames
730 else:
731 if self.data.ndim <= 1:
732 out_data[:ndata, 0] = self.data[self.index:]
733 out_data[ndata:, 0] = np.zeros(frames-ndata, dtype=np.int16)
734 else:
735 out_data[:ndata, :] = self.data[self.index:, :]
736 out_data[ndata:, :] = np.zeros((frames-ndata, self.channels),
737 dtype=np.int16)
738 self.index += frames
739 else:
740 # we need to play more to make sure everything is played!
741 # This is because of an ALSA bug and might be fixed in newer versions,
742 # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html
743 if self.data.ndim <= 1:
744 out_data[:, 0] = np.zeros(frames, dtype=np.int16)
745 else:
746 out_data[:, :] = np.zeros((frames, self.channels), dtype=np.int16)
747 self.index += frames
748 if self.index >= len(self.data) + 2*self.latency:
749 raise sounddevice.CallbackStop
750 if not self.run:
751 raise sounddevice.CallbackStop
753 def _stop_sounddevice(self):
754 """Stop any ongoing activity of the sounddevice module."""
755 if self.stream is not None:
756 if self.stream.active:
757 # fade out:
758 fadetime = 0.1
759 nr = int(np.round(fadetime*self.rate))
760 index = self.index+nr
761 if nr > len(self.data) - index:
762 nr = len(self.data) - index
763 else:
764 self.data[index+nr:] = 0
765 if nr > 0:
766 for k in range(nr) :
767 self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] *
768 np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C')
769 sounddevice.sleep(int(2000*fadetime))
770 if self.stream.active:
771 self.run = False
772 while self.stream.active:
773 sounddevice.sleep(10)
774 self.stream.stop()
775 self.stream.close()
776 self.stream = None
778 def _play_sounddevice(self, blocking=True):
779 """Play audio data using the sounddevice module.
781 Parameters
782 ----------
783 blocking: boolean
784 If False do not block.
786 Raises
787 ------
788 ValueError
789 Invalid sampling rate (after some attemps of resampling).
790 """
791 # check channel count:
792 channels = self.channels
793 if self.channels > self.max_channels:
794 channels = self.max_channels
795 # check sampling rate:
796 scale_fac = 1
797 scaled_rate = self.rate
798 max_rate = 48000.0
799 if self.rate > max_rate:
800 scale_fac = int(np.ceil(self.rate/max_rate))
801 scaled_rate = int(self.rate//scale_fac)
802 rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate]
803 scales = [1, scale_fac, None, None, None, None]
804 success = False
805 for rate, scale in zip(rates, scales):
806 try:
807 sounddevice.check_output_settings(device=self.device_index,
808 channels=channels,
809 dtype=np.int16,
810 samplerate=rate)
811 if scale is None:
812 scale = self.rate/float(rate)
813 success = True
814 break
815 except sounddevice.PortAudioError as pae:
816 if pae.args[1] != -9997:
817 raise
818 elif self.verbose > 0:
819 print(f'invalid sampling rate of {rate}Hz')
820 if not success:
821 raise ValueError('No valid sampling rate found')
822 if channels != self.channels or scale != 1:
823 self._down_sample(channels, scale)
825 # play:
826 self.stream = sounddevice.OutputStream(samplerate=self.rate,
827 device=self.device_index,
828 channels=self.channels,
829 dtype=np.int16,
830 callback=self._callback_sounddevice)
831 self.latency = self.stream.latency*self.rate
832 self.run = True
833 self.stream.start()
834 if blocking:
835 while self.stream.active:
836 sounddevice.sleep(10)
837 self.run = False
838 self.stream.stop()
839 self.stream.close()
840 self.stream = None
842 def _close_sounddevice(self):
843 """Terminate sounddevice module."""
844 self._stop_sounddevice()
845 self._close()
848 def open_simpleaudio(self, device_index=None):
849 """Initialize audio output via simpleaudio package.
851 Parameters
852 ----------
853 device_index: int or None
854 Index of the playback device to be used.
855 If None take the default device.
856 Not supported by simpleaudio.
858 Raises
859 ------
860 ImportError
861 simpleaudio module is not available.
863 Documentation
864 -------------
865 https://simpleaudio.readthedocs.io
866 """
867 if not audio_modules['simpleaudio']:
868 raise ImportError
869 self.handle = True
870 self._do_play = self._play_simpleaudio
871 self.close = self._close_simpleaudio
872 self.stop = self._stop_simpleaudio
873 self.lib = 'simpleaudio'
874 return self
876 def _stop_simpleaudio(self):
877 """Stop any ongoing activity of the simpleaudio package."""
878 if self.handle is not None and self.handle is not True:
879 self.handle.stop()
881 def _play_simpleaudio(self, blocking=True):
882 """Play audio data using the simpleaudio package.
884 Parameters
885 ----------
886 blocking: boolean
887 If False do not block.
889 Raises
890 ------
891 ValueError
892 Invalid sampling rate (after some attemps of resampling).
893 FileNotFoundError
894 No audio device for playback.
895 """
896 rates = [self.rate, 44100, 48000, 22050]
897 scales = [1, None, None, None]
898 success = False
899 for rate, scale in zip(rates, scales):
900 if scale is None:
901 scale = self.rate/float(rate)
902 if scale != 1:
903 self._down_sample(self.channels, scale)
904 try:
905 self.handle = simpleaudio.play_buffer(self.data, self.channels,
906 2, int(self.rate))
907 success = True
908 break
909 except ValueError as e:
910 if self.verbose > 0:
911 print(f'invalid sampling rate of {rate}Hz')
912 except simpleaudio._simpleaudio.SimpleaudioError as e:
913 if self.verbose > 0:
914 print('simpleaudio SimpleaudioError:', str(e))
915 if 'Error opening' in str(e):
916 raise FileNotFoundError('No audio device found')
917 except Exception as e:
918 if self.verbose > 0:
919 print('simpleaudio Exception:', str(e))
920 if not success:
921 raise ValueError('No valid sampling rate found')
922 elif blocking:
923 self.handle.wait_done()
925 def _close_simpleaudio(self):
926 """Close audio output using simpleaudio package."""
927 self._stop_simpleaudio()
928 simpleaudio.stop_all()
929 self._close()
932 def open_soundcard(self, device_index=None):
933 """Initialize audio output via soundcard package.
935 Parameters
936 ----------
937 device_index: int or None
938 Index of the playback device to be used.
939 If None take the default device.
941 Raises
942 ------
943 ImportError
944 soundcard module is not available.
945 FileNotFoundError
946 Failed to open audio device.
948 Documentation
949 -------------
950 https://github.com/bastibe/SoundCard
951 """
952 if not audio_modules['soundcard']:
953 raise ImportError
954 try:
955 if device_index is None:
956 self.handle = soundcard.default_speaker()
957 else:
958 self.handle = soundcard.all_speakers()[device_index]
959 except IndexError:
960 raise FileNotFoundError('No audio device found')
961 except Exception as e:
962 print('soundcard Exception:', type(e).__name__, str(e))
963 if self.handle is None:
964 raise FileNotFoundError('No audio device found')
965 self._do_play = self._play_soundcard
966 self.close = self._close_soundcard
967 self.stop = self._stop_soundcard
968 self.lib = 'soundcard'
969 return self
971 def _stop_soundcard(self):
972 """Stop any ongoing activity of the soundcard package."""
973 pass
975 def _play_soundcard(self, blocking=True):
976 """Play audio data using the soundcard package.
978 Parameters
979 ----------
980 blocking: boolean
981 If False do not block.
982 Non-blocking playback not supported by soundcard.
983 Return immediately without playing sound.
985 Raises
986 ------
987 ValueError
988 Invalid sampling rate (after some attemps of resampling).
989 """
990 if not blocking:
991 warnings.warn('soundcard module does not support non-blocking playback')
992 return
993 rates = [self.rate, 44100, 48000, 22050]
994 scales = [1, None, None, None]
995 success = False
996 for rate, scale in zip(rates, scales):
997 if scale is None:
998 scale = self.rate/float(rate)
999 if scale != 1:
1000 self._down_sample(self.channels, scale)
1001 try:
1002 self.handle.play(self.data, samplerate=int(self.rate))
1003 success = True
1004 break
1005 except RuntimeError as e:
1006 if 'invalid sample spec' in str(e):
1007 if self.verbose > 0:
1008 print(f'invalid sampling rate of {rate}Hz')
1009 else:
1010 if self.verbose > 0:
1011 print('soundcard error:', type(e).__name__, str(e))
1012 except Exception as e:
1013 if self.verbose > 0:
1014 print('soundcard error:', type(e).__name__, str(e))
1015 if not success:
1016 raise ValueError('No valid sampling rate found')
1018 def _close_soundcard(self):
1019 """Close audio output using soundcard package."""
1020 self._stop_soundcard()
1021 self._close()
1024 def open_ossaudiodev(self, device_index=None):
1025 """Initialize audio output via ossaudiodev module.
1027 The OSS audio module is part of the python standard library.
1029 Parameters
1030 ----------
1031 device_index: int or None
1032 Index of the playback device to be used.
1033 If None take the default device.
1034 There is only a single OSS audio device.
1036 Raises
1037 ------
1038 ImportError
1039 ossaudiodev module is not available.
1040 FileNotFoundError
1041 Failed to open audio device.
1043 Documentation
1044 -------------
1045 https://docs.python.org/2/library/ossaudiodev.html
1047 Installation
1048 ------------
1049 The ossaudiodev module needs an oss `/dev/dsp` device file.
1050 Enable an oss emulation via alsa by installing
1051 ```
1052 sudo apt install -y osspd
1053 ```
1054 """
1055 if not audio_modules['ossaudiodev']:
1056 raise ImportError
1057 self.handle = True
1058 self.osshandle = None
1059 self.run = False
1060 self.play_thread = None
1061 try:
1062 handle = ossaudiodev.open('w')
1063 handle.close()
1064 except Exception as e:
1065 if self.verbose > 0:
1066 print(str(e))
1067 self._close()
1068 raise FileNotFoundError('failed to initialize audio device')
1069 self.close = self._close_ossaudiodev
1070 self.stop = self._stop_ossaudiodev
1071 self._do_play = self._play_ossaudiodev
1072 self.lib = 'ossaudiodev'
1073 return self
1075 def _stop_ossaudiodev(self):
1076 """Stop any ongoing activity of the ossaudiodev module."""
1077 if self.osshandle is not None:
1078 self.run = False
1079 self.osshandle.reset()
1080 if self.play_thread is not None:
1081 if self.play_thread.is_alive():
1082 self.play_thread.join()
1083 self.play_thread = None
1084 self.osshandle.close()
1085 self.osshandle = None
1087 def _run_play_ossaudiodev(self):
1088 """Play the data using the ossaudiodev module."""
1089 self.osshandle.writeall(self.data)
1090 if self.run:
1091 sleep(0.5)
1092 self.osshandle.close()
1093 self.osshandle = None
1094 self.run = False
1096 def _play_ossaudiodev(self, blocking=True):
1097 """Play audio data using the ossaudiodev module.
1099 Raises
1100 ------
1101 ValueError
1102 Invalid sampling rate (after some attemps of resampling).
1104 Parameters
1105 ----------
1106 blocking: boolean
1107 If False do not block.
1108 """
1109 self.osshandle = ossaudiodev.open('w')
1110 self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE)
1111 # set and check channel count:
1112 channels = self.osshandle.channels(self.channels)
1113 # check sampling rate:
1114 scale_fac = 1
1115 scaled_rate = self.rate
1116 max_rate = 48000.0
1117 if self.rate > max_rate:
1118 scale_fac = int(np.ceil(self.rate/max_rate))
1119 scaled_rate = int(self.rate//scale_fac)
1120 rates = [self.rate, scaled_rate, 44100, 48000, 22050, 8000]
1121 scales = [1, scale_fac, None, None, None, None]
1122 success = False
1123 for rate, scale in zip(rates, scales):
1124 set_rate = self.osshandle.speed(int(rate))
1125 if abs(set_rate - rate) < 2:
1126 if scale is None:
1127 scale = self.rate/float(set_rate)
1128 success = True
1129 break
1130 else:
1131 if self.verbose > 0:
1132 print(f'invalid sampling rate of {rate}Hz')
1133 if not success:
1134 raise ValueError('No valid sampling rate found')
1135 if channels != self.channels or scale != 1:
1136 self._down_sample(channels, scale)
1137 if blocking:
1138 self.run = True
1139 self.osshandle.writeall(self.data)
1140 sleep(0.5)
1141 self.osshandle.close()
1142 self.run = False
1143 self.osshandle = None
1144 else:
1145 self.play_thread = Process(target=self._run_play_ossaudiodev)
1146 self.run = True
1147 self.play_thread.start()
1149 def _close_ossaudiodev(self):
1150 """Close audio output using ossaudiodev module."""
1151 self._stop_ossaudiodev()
1152 self._close()
1155 def open_winsound(self, device_index=None):
1156 """Initialize audio output via winsound module.
1158 The winsound module is part of the python standard library.
1160 Parameters
1161 ----------
1162 device_index: int or None
1163 Index of the playback device to be used.
1164 If None take the default device.
1165 Device selection is not supported by the winsound module.
1167 Raises
1168 ------
1169 ImportError
1170 winsound module is not available.
1172 Documentation
1173 -------------
1174 https://docs.python.org/3.6/library/winsound.html
1175 https://mail.python.org/pipermail/tutor/2012-September/091529.html
1176 """
1177 if not audio_modules['winsound'] or not audio_modules['wave']:
1178 raise ImportError
1179 self.handle = True
1180 self._do_play = self._play_winsound
1181 self.close = self._close_winsound
1182 self.stop = self._stop_winsound
1183 self.audio_file = ''
1184 self.lib = 'winsound'
1185 return self
1187 def _stop_winsound(self):
1188 """Stop any ongoing activity of the winsound module."""
1189 try:
1190 winsound.PlaySound(None, winsound.SND_MEMORY)
1191 except Exception as e:
1192 pass
1194 def _play_winsound(self, blocking=True):
1195 """Play audio data using the winsound module.
1197 Parameters
1198 ----------
1199 blocking: boolean
1200 If False do not block.
1201 """
1202 # play file:
1203 if blocking:
1204 # write data as wav file to memory:
1205 self.data_buffer = BytesIO()
1206 w = wave.open(self.data_buffer, 'w')
1207 w.setnchannels(self.channels)
1208 w.setsampwidth(2)
1209 w.setframerate(int(self.rate))
1210 w.setnframes(len(self.data))
1211 try:
1212 w.writeframes(self.data.tobytes())
1213 except AttributeError:
1214 w.writeframes(self.data.tostring())
1215 w.close()
1216 try:
1217 winsound.PlaySound(self.data_buffer.getvalue(), winsound.SND_MEMORY)
1218 except Exception as e:
1219 if self.verbose > 0:
1220 print(str(e))
1221 return
1222 else:
1223 if self.verbose > 0:
1224 print('Warning: asynchronous playback is limited to playing wav files by the winsound module. Install an alternative package as recommended by the audiomodules script. ')
1225 # write data as wav file to file:
1226 self.audio_file = 'audioio-async_playback.wav'
1227 w = wave.open(self.audio_file, 'w')
1228 w.setnchannels(self.channels)
1229 w.setsampwidth(2)
1230 w.setframerate(int(self.rate))
1231 w.setnframes(len(self.data))
1232 try:
1233 w.writeframes(self.data.tobytes())
1234 except AttributeError:
1235 w.writeframes(self.data.tostring())
1236 w.close()
1237 try:
1238 winsound.PlaySound(self.audio_file, winsound.SND_ASYNC)
1239 except Exception as e:
1240 if self.verbose > 0:
1241 print(str(e))
1242 return
1244 def _close_winsound(self):
1245 """Close audio output using winsound module."""
1246 self._stop_winsound()
1247 self.handle = None
1248 if len(self.audio_file) > 0 and os.path.isfile(self.audio_file):
1249 os.remove(self.audio_file)
1250 self._close()
1253 def open(self, device_index=None, library=None):
1254 """Initialize the PlayAudio class with the best module available.
1256 Parameters
1257 ----------
1258 device_index: int or None
1259 Index of the playback device to be used.
1260 If None take the default device.
1261 library: str or None
1262 If specified, open a specific sound library.
1263 """
1264 # list of implemented play functions:
1265 audio_open = [
1266 ['sounddevice', self.open_sounddevice],
1267 ['pyaudio', self.open_pyaudio],
1268 ['simpleaudio', self.open_simpleaudio],
1269 ['soundcard', self.open_soundcard],
1270 ['ossaudiodev', self.open_ossaudiodev],
1271 ['winsound', self.open_winsound]
1272 ]
1273 if platform[0:3] == "win":
1274 sa = audio_open.pop(2)
1275 audio_open.insert(0, sa)
1276 # open audio device by trying various modules:
1277 success = False
1278 for lib, open_device in audio_open:
1279 if library and library != lib:
1280 continue
1281 if not audio_modules[lib]:
1282 if self.verbose > 0:
1283 print(f'module {lib} not available')
1284 continue
1285 try:
1286 open_device(device_index)
1287 success = True
1288 if self.verbose > 0:
1289 print(f'successfully opened {lib} module for playing')
1290 break
1291 except Exception as e:
1292 if self.verbose > 0:
1293 print(f'failed to open {lib} module for playing:',
1294 type(e).__name__, str(e))
1295 if not success:
1296 warnings.warn('cannot open any device for audio output')
1297 return self
1300def play(data, rate, scale=None, blocking=True, device_index=None, verbose=0):
1301 """Playback audio data.
1303 Create a `PlayAudio` instance on the global variable `handle`.
1305 Parameters
1306 ----------
1307 data: array
1308 The data to be played, either 1-D array for single channel output,
1309 or 2-D array with first axis time and second axis channel.
1310 Data values range between -1 and 1.
1311 rate: float
1312 The sampling rate in Hertz.
1313 scale: float
1314 Multiply data with scale before playing.
1315 If `None` scale it to the maximum value, if 1.0 do not scale.
1316 blocking: boolean
1317 If False do not block.
1318 device_index: int or None
1319 Index of the playback device to be used,
1320 if not already openend.
1321 If None take the default device.
1322 verbose: int
1323 Verbosity level.
1324 """
1325 global handle
1326 if handle is None:
1327 handle = PlayAudio(device_index, verbose)
1328 handle.verbose = verbose
1329 handle.play(data, rate, scale, blocking, device_index)
1332def beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0,
1333 fadetime=0.05, blocking=True, device_index=None, verbose=0):
1334 """Playback a tone.
1336 Create a `PlayAudio` instance on the global variable `handle`.
1338 Parameters
1339 ----------
1340 duration: float
1341 The duration of the tone in seconds.
1342 frequency: float or string
1343 If float the frequency of the tone in Hertz.
1344 If string, a musical note like 'f#5'.
1345 See `note2freq()` for details
1346 amplitude: float
1347 The ampliude (volume) of the tone from 0.0 to 1.0.
1348 rate: float
1349 The sampling rate in Hertz.
1350 fadetime: float
1351 Time for fading in and out in seconds.
1352 blocking: boolean
1353 If False do not block.
1354 device_index: int or None
1355 Index of the playback device to be used,
1356 if not already openend.
1357 If None take the default device.
1358 verbose: int
1359 Verbosity level.
1360 """
1361 global handle
1362 if handle is None:
1363 handle = PlayAudio(device_index, verbose)
1364 handle.verbose = verbose
1365 handle.beep(duration, frequency, amplitude, rate, fadetime, blocking,
1366 device_index)
1369def close():
1370 """Close the global PlayAudio instance.
1371 """
1372 global handle
1373 if handle is not None:
1374 handle.close()
1375 handle = None
1378def speaker_devices_pyaudio():
1379 """Query available output devices of the pyaudio module.
1381 Returns
1382 -------
1383 indices: list of int
1384 Device indices.
1385 devices: list of str
1386 Devices corresponding to `indices`.
1387 default_device: int
1388 Index of default device.
1389 -1 if no default output device is available.
1390 """
1391 if not audio_modules['pyaudio']:
1392 raise ImportError
1393 oldstderr = os.dup(2)
1394 os.close(2)
1395 tmpfile = 'tmpfile.tmp'
1396 os.open(tmpfile, os.O_WRONLY | os.O_CREAT)
1397 pa = pyaudio.PyAudio()
1398 os.close(2)
1399 os.dup(oldstderr)
1400 os.close(oldstderr)
1401 os.remove(tmpfile)
1402 indices = []
1403 devices = []
1404 for i in range(pa.get_device_count()):
1405 info = pa.get_device_info_by_index(i)
1406 if info['maxOutputChannels'] > 0:
1407 host = sounddevice.query_hostapis(info['hostApi'])['name']
1408 device = f'{info["name"]}, {host} ({info["maxInputChannels"]} in, {info["maxOutputChannels"]} out)'
1409 indices.append(info['index'])
1410 devices.append(device)
1411 try:
1412 default_device = pa.get_default_output_device_info()['index']
1413 except OSError:
1414 default_device = -1
1415 return indices, devices, default_device
1417def speaker_devices_sounddevice():
1418 """Query available output devices of the sounddevice module.
1420 Returns
1421 -------
1422 indices: list of int
1423 Device indices.
1424 devices: list of str
1425 Devices corresponding to `indices`.
1426 default_device: int
1427 Index of default device.
1428 """
1429 if not audio_modules['sounddevice']:
1430 raise ImportError
1431 indices = []
1432 devices = []
1433 infos = sounddevice.query_devices()
1434 for info in infos:
1435 if info['max_output_channels'] > 0:
1436 host = sounddevice.query_hostapis(info['hostapi'])['name']
1437 device = f'{info["name"]}, {host} ({info["max_input_channels"]} in, {info["max_output_channels"]} out)'
1438 indices.append(info['index'])
1439 devices.append(device)
1440 try:
1441 info_out = sounddevice.query_devices(kind='output')
1442 except sounddevice.PortAudioError:
1443 return indices, devices, -1
1444 try:
1445 info_in = sounddevice.query_devices(kind='input')
1446 if info_in['index'] != info_out['index'] and \
1447 info_in['max_output_channels'] > info_out['max_output_channels']:
1448 info_out = info_in
1449 except sounddevice.PortAudioError:
1450 pass
1451 return indices, devices, info_out['index']
1453def speaker_devices_soundcard():
1454 """Query available output devices of the soundcard module.
1456 Returns
1457 -------
1458 indices: list of int
1459 Device indices.
1460 devices: list of str
1461 Devices corresponding to `indices`.
1462 default_device: int
1463 Index of default device.
1464 """
1465 if not audio_modules['soundcard']:
1466 raise ImportError
1467 indices = []
1468 devices = []
1469 infos = soundcard.all_speakers()
1470 def_speaker = str(soundcard.default_speaker())
1471 default_device = -1
1472 for i, info in enumerate(infos):
1473 if str(info) == def_speaker:
1474 default_device = i
1475 indices.append(i)
1476 devices.append(str(info).lstrip('<').rstrip('>'))
1477 return indices, devices, default_device
1479def speaker_devices(library=None, verbose=0):
1480 """Query available output devices.
1482 Parameters
1483 ----------
1484 library: str or None
1485 If specified, use specific sound library.
1486 verbose: int
1487 Verbosity level.
1489 Returns
1490 -------
1491 indices: list of int
1492 Device indices.
1493 devices: list of str
1494 Devices corresponding to `indices`.
1495 default_device: int
1496 Index of default device.
1497 """
1498 # list of implemented list functions:
1499 audio_devices = [
1500 ['sounddevice', speaker_devices_sounddevice],
1501 ['pyaudio', speaker_devices_pyaudio],
1502 ['simpleaudio', None],
1503 ['soundcard', speaker_devices_soundcard],
1504 ['ossaudiodev', None],
1505 ['winsound', None]
1506 ]
1507 if platform[0:3] == "win":
1508 sa = audio_open.pop(2)
1509 audio_open.insert(0, sa)
1510 # query audio devices by trying various modules:
1511 success = False
1512 for lib, devices in audio_devices:
1513 if library and library != lib:
1514 continue
1515 if not audio_modules[lib]:
1516 if verbose > 0:
1517 print(f'module {lib} not available')
1518 continue
1519 if devices is None:
1520 return [0], ['default output device'], 0
1521 else:
1522 return devices()
1523 warnings.warn('no library for audio output available for devices')
1524 return [], [], -1
1527def print_speaker_devices(library=None):
1528 """Print available output devices.
1530 Parameters
1531 ----------
1532 library: str or None
1533 If specified, use specific sound library.
1534 """
1535 indices, devices, default_device = speaker_devices()
1536 for i, d in zip(indices, devices):
1537 if i == default_device:
1538 print(f'* {i:2d}: {d}')
1539 else:
1540 print(f' {i:2d}: {d}')
1543def demo(device_index=None):
1544 """ Demonstrate the playaudio module."""
1545 print('play mono beep 1')
1546 audio = PlayAudio(device_index, verbose=2)
1547 audio.beep(1.0, 440.0)
1548 audio.close()
1550 print('play mono beep 2')
1551 with PlayAudio(device_index) as audio:
1552 audio.beep(1.0, 'b4', 0.75, blocking=False)
1553 print(' done')
1554 sleep(0.3)
1555 sleep(0.5)
1557 print('play mono beep 3')
1558 beep(1.0, 'c5', 0.25, blocking=False, device_index=device_index)
1559 print(' done')
1560 sleep(0.5)
1562 print('play stereo beep')
1563 duration = 1.0
1564 rate = 44100.0
1565 t = np.arange(0.0, duration, 1.0/rate)
1566 data = np.zeros((len(t),2))
1567 data[:,0] = np.sin(2.0*np.pi*note2freq('a4')*t)
1568 data[:,1] = 0.25*np.sin(2.0*np.pi*note2freq('e5')*t)
1569 fade(data, rate, 0.1)
1570 play(data, rate, verbose=2, device_index=device_index)
1573def main(*args):
1574 """Call demo with command line arguments.
1576 Parameters
1577 ----------
1578 args: list of strings
1579 Command line arguments as provided by sys.argv[1:]
1580 """
1581 help = False
1582 mod = False
1583 dev = False
1584 ldev = False
1585 device_index = None
1586 for arg in args:
1587 if mod:
1588 if not select_module(arg):
1589 print(f'module {arg} not installed. Exit!')
1590 return
1591 mod = False
1592 elif dev:
1593 device_index = int(arg)
1594 dev = False
1595 elif arg == '-h':
1596 help = True
1597 break
1598 elif arg == '-l':
1599 ldev = True
1600 break
1601 elif arg == '-m':
1602 mod = True
1603 elif arg == '-d':
1604 dev = True
1605 else:
1606 break
1608 if help:
1609 print()
1610 print('Usage:')
1611 print(' python -m src.audioio.playaudio [-m <module>] [-l] [-d <device index>]')
1612 print(' -m: audio module to be used')
1613 print(' -l: list available audio output devices')
1614 print(' -d: set audio output device to be used')
1615 return
1617 if ldev:
1618 print_speaker_devices()
1619 return
1621 demo(device_index)
1624if __name__ == "__main__":
1625 import sys
1626 main(*sys.argv[1:])