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