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