Module audioio.playaudio
Play numpy arrays as audio.
Accepted data for playback are 1-D or 2-D (frames, channels) numpy arrays with values ranging from -1 to 1. If necessary data are downsampled automatically to match supported sampling rates.
Class
Use the PlayAudio
class for audio output to a speaker:
with PlayAudio() as audio:
audio.beep()
or without context management:
audio = PlayAudio()
audio.beep(1.0, 'a4')
audio.close()
Functions
Alternatively, the globally defined functions play()
and beep()
use the global instance handle
of the PlayAudio
class to play a
sound on the default audio output device.
Helper functions
speaker_devices()
: query available output devices.fade_in()
: fade in a signal in place.fade_out()
: fade out a signal in place.fade()
: fade in and out a signal in place.note2freq()
: convert textual note to corresponding frequency.
Installation
You might need to install additional packages for better audio output. See installation for further instructions.
Demo
For a demo, run the script as:
python -m src.audioio.playaudio
Global variables
var handle
Functions
def main(*args)
-
Call demo with command line arguments.
Parameters
args
:list
ofstrings
- Command line arguments as provided by sys.argv[1:]
def note2freq(note, a4freq=440.0)
-
Convert textual note to corresponding frequency.
Parameters
note
:string
- A musical note like 'a4', 'f#3', 'eb5'.
The first character is the note, it can be
'a', 'b', 'c', 'd', 'e', 'f', or 'g'.
The optional second character is either a 'b'
or a '#' to decrease or increase by half a note.
The last character specifies the octave.
'a4' is defined by
a4freq
. a4freq
:float
- The frequency of a4 in Hertz.
Returns
freq
:float
- The frequency of the note in Hertz.
Raises
Valueerror
No or an invalid note was specified.
def fade_in(data, rate, fadetime)
-
Fade in a signal in place.
The first
fadetime
seconds of the data are multiplied with a squared sine in place. Iffadetime
is larger than half the duration of the data, thenfadetime
is reduced to half of the duration.Parameters
data
:array
- The data to be faded in, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel.
rate
:float
- The sampling rate in Hertz.
fadetime
:float
- Time for fading in in seconds.
def fade_out(data, rate, fadetime)
-
Fade out a signal in place.
The last
fadetime
seconds of the data are multiplied with a squared sine in place. Iffadetime
is larger than half the duration of the data, thenfadetime
is reduced to half of the duration.Parameters
data
:array
- The data to be faded out, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel.
rate
:float
- The sampling rate in Hertz.
fadetime
:float
- Time for fading out in seconds.
def fade(data, rate, fadetime)
-
Fade in and out a signal in place.
The first and last
fadetime
seconds of the data are multiplied with a squared sine in place. Iffadetime
is larger than half the duration of the data, thenfadetime
is reduced to half of the duration.Parameters
data
:array
- The data to be faded, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel.
rate
:float
- The sampling rate in Hertz.
fadetime
:float
- Time for fading in and out in seconds.
def play(data, rate, scale=None, blocking=True, device_index=None, verbose=0)
-
Playback audio data.
Create a
PlayAudio
instance on the global variablehandle
.Parameters
data
:array
- The data to be played, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel. Data values range between -1 and 1.
rate
:float
- The sampling rate in Hertz.
scale
:float
- Multiply data with scale before playing.
If
None
scale it to the maximum value, if 1.0 do not scale. blocking
:boolean
- If False do not block.
device_index
:int
orNone
- Index of the playback device to be used, if not already openend. If None take the default device.
verbose
:int
- Verbosity level.
def beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True, device_index=None, verbose=0)
-
Playback a tone.
Create a
PlayAudio
instance on the global variablehandle
.Parameters
duration
:float
- The duration of the tone in seconds.
frequency
:float
orstring
- If float the frequency of the tone in Hertz.
If string, a musical note like 'f#5'.
See
note2freq()
for details amplitude
:float
- The ampliude (volume) of the tone from 0.0 to 1.0.
rate
:float
- The sampling rate in Hertz.
fadetime
:float
- Time for fading in and out in seconds.
blocking
:boolean
- If False do not block.
device_index
:int
orNone
- Index of the playback device to be used, if not already openend. If None take the default device.
verbose
:int
- Verbosity level.
def close()
-
Close the global PlayAudio instance.
def speaker_devices_pyaudio()
-
Query available output devices of the pyaudio module.
Returns
indices
:list
ofint
- Device indices.
devices
:list
ofstr
- Devices corresponding to
indices
.
def speaker_devices_sounddevice()
-
Query available output devices of the sounddevice module.
Returns
indices
:list
ofint
- Device indices.
devices
:list
ofstr
- Devices corresponding to
indices
.
def speaker_devices_soundcard()
-
Query available output devices of the soundcard module.
Returns
indices
:list
ofint
- Device indices.
devices
:list
ofstr
- Devices corresponding to
indices
.
def speaker_devices(library=None, verbose=0)
-
Query available output devices.
Parameters
library
:str
orNone
- If specified, use specific sound library.
verbose
:int
- Verbosity level.
Returns
indices
:list
ofint
- Device indices.
devices
:list
ofstr
- Devices corresponding to
indices
.
def demo(device_index=None)
-
Demonstrate the playaudio module.
Classes
class PlayAudio (device_index=None, verbose=0, library=None)
-
Audio playback.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device. Use the speaker_devices() function to query available devices.
verbose
:int
- Verbosity level.
library
:str
orNone
- If specified, open a specific sound library.
Attributes
lib
:string
- The library used for playback.
verbose
:int
- Verbosity level.
Methods
play(data, rate, scale=None, blocking=True)
: Playback audio data.beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True)
: Playback a pure tone.open()
: Initialize the PlayAudio class with the best module available.close()
: Terminate module for playing audio.stop()
: Stop any playback in progress.
Examples
from audioio import PlayAudio with PlayAudio() as audio: audio.beep()
or without context management:
audio = PlayAudio() audio.beep(1.0, 'a4') audio.close()
Expand source code
class PlayAudio(object): """ Audio playback. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Use the speaker_devices() function to query available devices. verbose: int Verbosity level. library: str or None If specified, open a specific sound library. Attributes ---------- lib: string The library used for playback. verbose: int Verbosity level. Methods ------- - `play(data, rate, scale=None, blocking=True)`: Playback audio data. - `beep(duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True)`: Playback a pure tone. - `open()`: Initialize the PlayAudio class with the best module available. - `close()`: Terminate module for playing audio. - `stop()`: Stop any playback in progress. Examples -------- ``` from audioio import PlayAudio with PlayAudio() as audio: audio.beep() ``` or without context management: ``` audio = PlayAudio() audio.beep(1.0, 'a4') audio.close() ``` """ def __init__(self, device_index=None, verbose=0, library=None): self.verbose = verbose self.handle = None self._do_play = self._play self.close = self._close self.stop = self._stop self.lib = None self.open(device_index, library) def _close(self): """Terminate PlayAudio class for playing audio.""" self.handle = None self._do_play = self._play self.close = self._close self.stop = self._stop self.lib = None def _stop(self): """Stop any playback in progress.""" pass def _play(self, blocking=True): """Default implementation of playing a sound: does nothing.""" pass def play(self, data, rate, scale=None, blocking=True, device_index=None): """Playback audio data. Parameters ---------- data: array The data to be played, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel. Data values range between -1 and 1. rate: float The sampling rate in Hertz. scale: float Multiply data with scale before playing. If `None` scale it to the maximum value, if 1.0 do not scale. blocking: boolean If False do not block. device_index: int or None Index of the playback device to be used, if not already openend via the constructor. If None take the default device. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). FileNotFoundError No audio device for playback. """ if self.handle is None: self.open(device_index) else: self.stop() self.rate = rate self.channels = 1 if data.ndim > 1: self.channels = data.shape[1] # convert data: rawdata = data - np.mean(data, axis=0) if scale is None: scale = 1.0/np.max(np.abs(rawdata)) rawdata *= scale self.data = np.floor(rawdata*(2**15-1)).astype(np.int16, order='C') self.index = 0 self._do_play(blocking) def beep(self, duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True, device_index=None): """Playback a pure tone. Parameters ---------- duration: float The duration of the tone in seconds. frequency: float or string If float, the frequency of the tone in Hertz. If string, a musical note like 'f#5'. See `note2freq()` for details. amplitude: float The ampliude (volume) of the tone in the range from 0.0 to 1.0. rate: float The sampling rate in Hertz. fadetime: float Time for fading in and out in seconds. blocking: boolean If False do not block. device_index: int or None Index of the playback device to be used, if not already openend via the constructor. If None take the default device. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). FileNotFoundError No audio device for playback. See also -------- https://mail.python.org/pipermail/tutor/2012-September/091529.html for fourier series based construction of waveforms. """ # frequency if isinstance(frequency, str): frequency = note2freq(frequency) # sine wave: time = np.arange(0.0, duration, 1.0/rate) data = amplitude*np.sin(2.0*np.pi*frequency*time) # fade in and out: fade(data, rate, fadetime) # # final click for testing (mono only): # data = np.hstack((data, np.sin(2.0*np.pi*1000.0*time[0:int(np.ceil(4.0*rate/1000.0))]))) # play: self.play(data, rate, scale=1.0, blocking=blocking, device_index=device_index) def _down_sample(self, channels, scale=1): """Sample the data down and adapt maximum channel number.""" iscale = 1 rscale = scale if isinstance(scale, int): iscale = scale rscale = 1.0 elif scale > 2: iscale = int(np.floor(scale)) rscale = scale/iscale if iscale > 1: data = decimate(self.data, iscale, axis=0) if self.data.ndim > 1: self.data = np.asarray(data[:,:channels], dtype=np.int16, order='C') else: self.data = np.asarray(data, dtype=np.int16, order='C') if self.verbose > 0: print(f'decimated sampling rate from {self.rate:.1f}Hz down to {self.rate/iscale:.1f}Hz') self.rate /= iscale if rscale != 1.0: dt0 = 1.0/self.rate dt1 = rscale/self.rate old_time = np.arange(len(self.data))*dt0 new_time = np.arange(0.0, old_time[-1]+0.5*dt0, dt1) if self.data.ndim > 1: data = np.zeros((len(new_time), channels), order='C') for c in range(channels): data[:,c] = np.interp(new_time, old_time, self.data[:,c]) else: data = np.interp(new_time, old_time, self.data) self.data = np.asarray(data, dtype=self.data.dtype, order='C') if self.verbose > 0: print(f'adapted sampling rate from {self.rate:.1f}Hz to {self.rate/rscale:.1f}Hz') self.rate /= rscale self.channels = channels def __del__(self): """Terminate the audio module.""" self.close() def __enter__(self): return self def __exit__(self, type, value, tb): self.__del__() return value def open_pyaudio(self, device_index=None): """Initialize audio output via PyAudio module. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Raises ------ ImportError PyAudio module is not available. FileNotFoundError Failed to open audio device. Documentation ------------- https://people.csail.mit.edu/hubert/pyaudio/ http://www.portaudio.com/ Installation ------------ ``` sudo apt install -y libportaudio2 portaudio19-dev python-pyaudio python3-pyaudio ``` On Windows, download an appropriate (latest version, 32 or 64 bit) wheel from <https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio>. Install this file with pip, that is go to the folder where the wheel file is downloaded and run ``` pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl ``` replace the wheel file name by the one you downloaded. """ if not audio_modules['pyaudio']: raise ImportError oldstderr = os.dup(2) os.close(2) tmpfile = 'tmpfile.tmp' os.open(tmpfile, os.O_WRONLY | os.O_CREAT) self.handle = pyaudio.PyAudio() self.stream = None os.close(2) os.dup(oldstderr) os.close(oldstderr) os.remove(tmpfile) try: if device_index is None: info = self.handle.get_default_output_device_info() else: info = self.handle.get_device_info_by_index(device_index) self.max_channels = info['maxOutputChannels'] self.default_rate = info['defaultSampleRate'] self.device_index = info['index'] self.handle.is_format_supported(self.default_rate, output_device=self.device_index, output_channels=1, output_format=pyaudio.paInt16) except Exception as e: if self.verbose > 0: print(str(e)) self.handle.terminate() self._close() raise FileNotFoundError('failed to initialize audio device') self.index = 0 self.data = None self.close = self._close_pyaudio self.stop = self._stop_pyaudio self._do_play = self._play_pyaudio self.lib = 'pyaudio' return self def _callback_pyaudio(self, in_data, frames, time_info, status): """Callback for pyaudio for supplying output with data.""" flag = pyaudio.paContinue if not self.run: flag = pyaudio.paComplete if self.index < len(self.data): out_data = self.data[self.index:self.index+frames] self.index += len(out_data) # zero padding: if len(out_data) < frames: if self.data.ndim > 1: out_data = np.vstack((out_data, np.zeros((frames-len(out_data), self.channels), dtype=np.int16))) else: out_data = np.hstack((out_data, np.zeros(frames-len(out_data), dtype=np.int16))) return (out_data, flag) else: # we need to play more to make sure everything is played! # This is because of an ALSA bug and might be fixed in newer versions, # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html out_data = np.zeros(frames*self.channels, dtype=np.int16) self.index += frames if self.index >= len(self.data) + 2*self.latency: flag = pyaudio.paComplete return (out_data, flag) def _stop_pyaudio(self): """Stop any ongoing activity of the pyaudio module.""" if self.stream is not None: if self.stream.is_active(): # fade out: fadetime = 0.1 nr = int(np.round(fadetime*self.rate)) index = self.index+nr if nr > len(self.data) - index: nr = len(self.data) - index else: self.data[index+nr:] = 0 if nr > 0: for k in range(nr) : self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] * np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C') try: sleep(2*fadetime) except SystemError: # pyaudio interferes with sleep in python 3.10 pass if self.stream.is_active(): self.run = False while self.stream.is_active(): try: sleep(0.01) except SystemError: # pyaudio interferes with sleep in python 3.10 pass self.stream.stop_stream() self.stream.close() self.stream = None def _play_pyaudio(self, blocking=True): """Play audio data using the pyaudio module. Parameters ---------- blocking: boolean If False do not block. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). """ # check channel count: channels = self.channels if self.channels > self.max_channels: channels = self.max_channels # check sampling rate: scale_fac = 1 scaled_rate = self.rate max_rate = 48000.0 if self.rate > max_rate: scale_fac = int(np.ceil(self.rate/max_rate)) scaled_rate = int(self.rate//scale_fac) rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate] scales = [1, scale_fac, None, None, None, None] success = False for rate, scale in zip(rates, scales): try: if self.handle.is_format_supported(int(rate), output_device=self.device_index, output_channels=channels, output_format=pyaudio.paInt16): if scale is None: scale = self.rate/float(rate) success = True break except Exception as e: if self.verbose > 0: print(f'invalid sampling rate of {rate}Hz') if e.args[1] != pyaudio.paInvalidSampleRate: raise if not success: raise ValueError('No valid sampling rate found') if channels != self.channels or scale != 1: self._down_sample(channels, scale) # play: self.run = True self.stream = self.handle.open(format=pyaudio.paInt16, channels=self.channels, rate=int(self.rate), output=True, stream_callback=self._callback_pyaudio) self.latency = int(self.stream.get_output_latency()*self.rate) self.stream.start_stream() if blocking: while self.stream.is_active(): try: sleep(0.01) except (ValueError, SystemError): # pyaudio interferes with sleep in python 3.10 pass self.run = False self.stream.stop_stream() self.stream.close() self.stream = None def _close_pyaudio(self): """Terminate pyaudio module.""" self._stop_pyaudio() if self.handle is not None: self.handle.terminate() self._close() def open_sounddevice(self, device_index=None): """Initialize audio output via sounddevice module. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Raises ------ ImportError sounddevice module is not available. FileNotFoundError Failed to open audio device. Documentation ------------- https://python-sounddevice.readthedocs.io Installation ------------ ``` sudo apt install -y libportaudio2 portaudio19-dev sudo pip install sounddevice ``` """ if not audio_modules['sounddevice']: raise ImportError self.handle = True self.index = 0 self.data = None self.stream = None try: if device_index is None: info_in = sounddevice.query_devices(kind='input') info_out = sounddevice.query_devices(kind='output') if info_in['index'] == info_out['index']: info = info_out else: info = info_out if info_in['max_output_channels'] > info_out['max_output_channels']: info = info_in else: info = sounddevice.query_devices(device_index) self.device_index = info['index'] self.max_channels = info['max_output_channels'] self.default_rate = info['default_samplerate'] sounddevice.check_output_settings(device=self.device_index, channels=1, dtype=np.int16, samplerate=48000) except Exception as e: if self.verbose > 0: print(str(e)) self._close() raise FileNotFoundError('failed to initialize audio device') self.close = self._close_sounddevice self.stop = self._stop_sounddevice self._do_play = self._play_sounddevice self.lib = 'sounddevice' return self def _callback_sounddevice(self, out_data, frames, time_info, status): """Callback for sounddevice for supplying output with data.""" if status: print(status) if self.index < len(self.data): ndata = len(self.data) - self.index if ndata >= frames : if self.data.ndim <= 1: out_data[:,0] = self.data[self.index:self.index+frames] else: out_data[:, :] = self.data[self.index:self.index+frames, :] self.index += frames else: if self.data.ndim <= 1: out_data[:ndata, 0] = self.data[self.index:] out_data[ndata:, 0] = np.zeros(frames-ndata, dtype=np.int16) else: out_data[:ndata, :] = self.data[self.index:, :] out_data[ndata:, :] = np.zeros((frames-ndata, self.channels), dtype=np.int16) self.index += frames else: # we need to play more to make sure everything is played! # This is because of an ALSA bug and might be fixed in newer versions, # see http://music.columbia.edu/pipermail/portaudio/2012-May/013959.html if self.data.ndim <= 1: out_data[:, 0] = np.zeros(frames, dtype=np.int16) else: out_data[:, :] = np.zeros((frames, self.channels), dtype=np.int16) self.index += frames if self.index >= len(self.data) + 2*self.latency: raise sounddevice.CallbackStop if not self.run: raise sounddevice.CallbackStop def _stop_sounddevice(self): """Stop any ongoing activity of the sounddevice module.""" if self.stream is not None: if self.stream.active: # fade out: fadetime = 0.1 nr = int(np.round(fadetime*self.rate)) index = self.index+nr if nr > len(self.data) - index: nr = len(self.data) - index else: self.data[index+nr:] = 0 if nr > 0: for k in range(nr) : self.data[index+(nr-k-1)] = np.array(self.data[index+(nr-k-1)] * np.sin(0.5*np.pi*float(k)/float(nr))**2.0, np.int16, order='C') sounddevice.sleep(int(2000*fadetime)) if self.stream.active: self.run = False while self.stream.active: sounddevice.sleep(10) self.stream.stop() self.stream.close() self.stream = None def _play_sounddevice(self, blocking=True): """Play audio data using the sounddevice module. Parameters ---------- blocking: boolean If False do not block. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). """ # check channel count: channels = self.channels if self.channels > self.max_channels: channels = self.max_channels # check sampling rate: scale_fac = 1 scaled_rate = self.rate max_rate = 48000.0 if self.rate > max_rate: scale_fac = int(np.ceil(self.rate/max_rate)) scaled_rate = int(self.rate//scale_fac) rates = [self.rate, scaled_rate, 44100, 48000, 22050, self.default_rate] scales = [1, scale_fac, None, None, None, None] success = False for rate, scale in zip(rates, scales): try: sounddevice.check_output_settings(device=self.device_index, channels=channels, dtype=np.int16, samplerate=rate) if scale is None: scale = self.rate/float(rate) success = True break except sounddevice.PortAudioError as pae: if pae.args[1] != -9997: raise elif self.verbose > 0: print(f'invalid sampling rate of {rate}Hz') if not success: raise ValueError('No valid sampling rate found') if channels != self.channels or scale != 1: self._down_sample(channels, scale) # play: self.stream = sounddevice.OutputStream(samplerate=self.rate, device=self.device_index, channels=self.channels, dtype=np.int16, callback=self._callback_sounddevice) self.latency = self.stream.latency*self.rate self.run = True self.stream.start() if blocking: while self.stream.active: sounddevice.sleep(10) self.run = False self.stream.stop() self.stream.close() self.stream = None def _close_sounddevice(self): """Terminate sounddevice module.""" self._stop_sounddevice() self._close() def open_simpleaudio(self, device_index=None): """Initialize audio output via simpleaudio package. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Not supported by simpleaudio. Raises ------ ImportError simpleaudio module is not available. Documentation ------------- https://simpleaudio.readthedocs.io """ if not audio_modules['simpleaudio']: raise ImportError self.handle = True self._do_play = self._play_simpleaudio self.close = self._close_simpleaudio self.stop = self._stop_simpleaudio self.lib = 'simpleaudio' return self def _stop_simpleaudio(self): """Stop any ongoing activity of the simpleaudio package.""" if self.handle is not None and self.handle is not True: self.handle.stop() def _play_simpleaudio(self, blocking=True): """Play audio data using the simpleaudio package. Parameters ---------- blocking: boolean If False do not block. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). FileNotFoundError No audio device for playback. """ rates = [self.rate, 44100, 48000, 22050] scales = [1, None, None, None] success = False for rate, scale in zip(rates, scales): if scale is None: scale = self.rate/float(rate) if scale != 1: self._down_sample(self.channels, scale) try: self.handle = simpleaudio.play_buffer(self.data, self.channels, 2, int(self.rate)) success = True break except ValueError as e: if self.verbose > 0: print(f'invalid sampling rate of {rate}Hz') except simpleaudio._simpleaudio.SimpleaudioError as e: if self.verbose > 0: print('simpleaudio SimpleaudioError:', str(e)) if 'Error opening' in str(e): raise FileNotFoundError('No audio device found') except Exception as e: if self.verbose > 0: print('simpleaudio Exception:', str(e)) if not success: raise ValueError('No valid sampling rate found') elif blocking: self.handle.wait_done() def _close_simpleaudio(self): """Close audio output using simpleaudio package.""" self._stop_simpleaudio() simpleaudio.stop_all() self._close() def open_soundcard(self, device_index=None): """Initialize audio output via soundcard package. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Raises ------ ImportError soundcard module is not available. FileNotFoundError Failed to open audio device. Documentation ------------- https://github.com/bastibe/SoundCard """ if not audio_modules['soundcard']: raise ImportError try: if device_index is None: self.handle = soundcard.default_speaker() else: self.handle = soundcard.all_speakers()[device_index] except IndexError: raise FileNotFoundError('No audio device found') except Exception as e: print('soundcard Exception:', type(e).__name__, str(e)) if self.handle is None: raise FileNotFoundError('No audio device found') self._do_play = self._play_soundcard self.close = self._close_soundcard self.stop = self._stop_soundcard self.lib = 'soundcard' return self def _stop_soundcard(self): """Stop any ongoing activity of the soundcard package.""" pass def _play_soundcard(self, blocking=True): """Play audio data using the soundcard package. Parameters ---------- blocking: boolean If False do not block. Non-blocking playback not supported by soundcard. Return immediately without playing sound. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). """ if not blocking: warnings.warn('soundcard module does not support non-blocking playback') return rates = [self.rate, 44100, 48000, 22050] scales = [1, None, None, None] success = False for rate, scale in zip(rates, scales): if scale is None: scale = self.rate/float(rate) if scale != 1: self._down_sample(self.channels, scale) try: self.handle.play(self.data, samplerate=int(self.rate)) success = True break except RuntimeError as e: if 'invalid sample spec' in str(e): if self.verbose > 0: print(f'invalid sampling rate of {rate}Hz') else: if self.verbose > 0: print('soundcard error:', type(e).__name__, str(e)) except Exception as e: if self.verbose > 0: print('soundcard error:', type(e).__name__, str(e)) if not success: raise ValueError('No valid sampling rate found') def _close_soundcard(self): """Close audio output using soundcard package.""" self._stop_soundcard() self._close() def open_ossaudiodev(self, device_index=None): """Initialize audio output via ossaudiodev module. The OSS audio module is part of the python standard library. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. There is only a single OSS audio device. Raises ------ ImportError ossaudiodev module is not available. FileNotFoundError Failed to open audio device. Documentation ------------- https://docs.python.org/2/library/ossaudiodev.html Installation ------------ The ossaudiodev module needs an oss `/dev/dsp` device file. Enable an oss emulation via alsa by installing ``` sudo apt install -y osspd ``` """ if not audio_modules['ossaudiodev']: raise ImportError self.handle = True self.osshandle = None self.run = False self.play_thread = None try: handle = ossaudiodev.open('w') handle.close() except Exception as e: if self.verbose > 0: print(str(e)) self._close() raise FileNotFoundError('failed to initialize audio device') self.close = self._close_ossaudiodev self.stop = self._stop_ossaudiodev self._do_play = self._play_ossaudiodev self.lib = 'ossaudiodev' return self def _stop_ossaudiodev(self): """Stop any ongoing activity of the ossaudiodev module.""" if self.osshandle is not None: self.run = False self.osshandle.reset() if self.play_thread is not None: if self.play_thread.is_alive(): self.play_thread.join() self.play_thread = None self.osshandle.close() self.osshandle = None def _run_play_ossaudiodev(self): """Play the data using the ossaudiodev module.""" self.osshandle.writeall(self.data) if self.run: sleep(0.5) self.osshandle.close() self.osshandle = None self.run = False def _play_ossaudiodev(self, blocking=True): """Play audio data using the ossaudiodev module. Raises ------ ValueError Invalid sampling rate (after some attemps of resampling). Parameters ---------- blocking: boolean If False do not block. """ self.osshandle = ossaudiodev.open('w') self.osshandle.setfmt(ossaudiodev.AFMT_S16_LE) # set and check channel count: channels = self.osshandle.channels(self.channels) # check sampling rate: scale_fac = 1 scaled_rate = self.rate max_rate = 48000.0 if self.rate > max_rate: scale_fac = int(np.ceil(self.rate/max_rate)) scaled_rate = int(self.rate//scale_fac) rates = [self.rate, scaled_rate, 44100, 48000, 22050, 8000] scales = [1, scale_fac, None, None, None, None] success = False for rate, scale in zip(rates, scales): set_rate = self.osshandle.speed(int(rate)) if abs(set_rate - rate) < 2: if scale is None: scale = self.rate/float(set_rate) success = True break else: if self.verbose > 0: print(f'invalid sampling rate of {rate}Hz') if not success: raise ValueError('No valid sampling rate found') if channels != self.channels or scale != 1: self._down_sample(channels, scale) if blocking: self.run = True self.osshandle.writeall(self.data) sleep(0.5) self.osshandle.close() self.run = False self.osshandle = None else: self.play_thread = Process(target=self._run_play_ossaudiodev) self.run = True self.play_thread.start() def _close_ossaudiodev(self): """Close audio output using ossaudiodev module.""" self._stop_ossaudiodev() self._close() def open_winsound(self, device_index=None): """Initialize audio output via winsound module. The winsound module is part of the python standard library. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. Device selection is not supported by the winsound module. Raises ------ ImportError winsound module is not available. Documentation ------------- https://docs.python.org/3.6/library/winsound.html https://mail.python.org/pipermail/tutor/2012-September/091529.html """ if not audio_modules['winsound'] or not audio_modules['wave']: raise ImportError self.handle = True self._do_play = self._play_winsound self.close = self._close_winsound self.stop = self._stop_winsound self.audio_file = '' self.lib = 'winsound' return self def _stop_winsound(self): """Stop any ongoing activity of the winsound module.""" try: winsound.PlaySound(None, winsound.SND_MEMORY) except Exception as e: pass def _play_winsound(self, blocking=True): """Play audio data using the winsound module. Parameters ---------- blocking: boolean If False do not block. """ # play file: if blocking: # write data as wav file to memory: self.data_buffer = BytesIO() w = wave.open(self.data_buffer, 'w') w.setnchannels(self.channels) w.setsampwidth(2) w.setframerate(int(self.rate)) w.setnframes(len(self.data)) try: w.writeframes(self.data.tobytes()) except AttributeError: w.writeframes(self.data.tostring()) w.close() try: winsound.PlaySound(self.data_buffer.getvalue(), winsound.SND_MEMORY) except Exception as e: if self.verbose > 0: print(str(e)) return else: if self.verbose > 0: print('Warning: asynchronous playback is limited to playing wav files by the winsound module. Install an alternative package as recommended by the audiomodules script. ') # write data as wav file to file: self.audio_file = 'audioio-async_playback.wav' w = wave.open(self.audio_file, 'w') w.setnchannels(self.channels) w.setsampwidth(2) w.setframerate(int(self.rate)) w.setnframes(len(self.data)) try: w.writeframes(self.data.tobytes()) except AttributeError: w.writeframes(self.data.tostring()) w.close() try: winsound.PlaySound(self.audio_file, winsound.SND_ASYNC) except Exception as e: if self.verbose > 0: print(str(e)) return def _close_winsound(self): """Close audio output using winsound module.""" self._stop_winsound() self.handle = None if len(self.audio_file) > 0 and os.path.isfile(self.audio_file): os.remove(self.audio_file) self._close() def open(self, device_index=None, library=None): """Initialize the PlayAudio class with the best module available. Parameters ---------- device_index: int or None Index of the playback device to be used. If None take the default device. library: str or None If specified, open a specific sound library. """ # list of implemented play functions: audio_open = [ ['sounddevice', self.open_sounddevice], ['pyaudio', self.open_pyaudio], ['simpleaudio', self.open_simpleaudio], ['soundcard', self.open_soundcard], ['ossaudiodev', self.open_ossaudiodev], ['winsound', self.open_winsound] ] if platform[0:3] == "win": sa = audio_open.pop(2) audio_open.insert(0, sa) # open audio device by trying various modules: success = False for lib, open_device in audio_open: if library and library != lib: continue if not audio_modules[lib]: if self.verbose > 0: print(f'module {lib} not available') continue try: open_device(device_index) success = True if self.verbose > 0: print(f'successfully opened {lib} module for playing') break except Exception as e: if self.verbose > 0: print(f'failed to open {lib} module for playing:', type(e).__name__, str(e)) if not success: warnings.warn('cannot open any device for audio output') return self
Methods
def play(self, data, rate, scale=None, blocking=True, device_index=None)
-
Playback audio data.
Parameters
data
:array
- The data to be played, either 1-D array for single channel output, or 2-D array with first axis time and second axis channel. Data values range between -1 and 1.
rate
:float
- The sampling rate in Hertz.
scale
:float
- Multiply data with scale before playing.
If
None
scale it to the maximum value, if 1.0 do not scale. blocking
:boolean
- If False do not block.
device_index
:int
orNone
- Index of the playback device to be used, if not already openend via the constructor. If None take the default device.
Raises
ValueError
- Invalid sampling rate (after some attemps of resampling).
FileNotFoundError
- No audio device for playback.
def beep(self, duration=0.5, frequency=880.0, amplitude=0.5, rate=44100.0, fadetime=0.05, blocking=True, device_index=None)
-
Playback a pure tone.
Parameters
duration
:float
- The duration of the tone in seconds.
frequency
:float
orstring
- If float, the frequency of the tone in Hertz.
If string, a musical note like 'f#5'.
See
note2freq()
for details. amplitude
:float
- The ampliude (volume) of the tone in the range from 0.0 to 1.0.
rate
:float
- The sampling rate in Hertz.
fadetime
:float
- Time for fading in and out in seconds.
blocking
:boolean
- If False do not block.
device_index
:int
orNone
- Index of the playback device to be used, if not already openend via the constructor. If None take the default device.
Raises
ValueError
- Invalid sampling rate (after some attemps of resampling).
FileNotFoundError
- No audio device for playback.
See Also
<https://mail.python.org/pipermail/tutor/2012-September/091529.html>
for fourier series based construction of waveforms.
def open_pyaudio(self, device_index=None)
-
Initialize audio output via PyAudio module.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device.
Raises
ImportError
- PyAudio module is not available.
FileNotFoundError
- Failed to open audio device.
Documentation
https://people.csail.mit.edu/hubert/pyaudio/ http://www.portaudio.com/
Installation
sudo apt install -y libportaudio2 portaudio19-dev python-pyaudio python3-pyaudio
On Windows, download an appropriate (latest version, 32 or 64 bit) wheel from https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio. Install this file with pip, that is go to the folder where the wheel file is downloaded and run
pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
replace the wheel file name by the one you downloaded.
def open_sounddevice(self, device_index=None)
-
Initialize audio output via sounddevice module.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device.
Raises
ImportError
- sounddevice module is not available.
FileNotFoundError
- Failed to open audio device.
Documentation
https://python-sounddevice.readthedocs.io
Installation
sudo apt install -y libportaudio2 portaudio19-dev sudo pip install sounddevice
def open_simpleaudio(self, device_index=None)
-
Initialize audio output via simpleaudio package.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device. Not supported by simpleaudio.
Raises
ImportError
- simpleaudio module is not available.
Documentation
def open_soundcard(self, device_index=None)
-
Initialize audio output via soundcard package.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device.
Raises
ImportError
- soundcard module is not available.
FileNotFoundError
- Failed to open audio device.
Documentation
def open_ossaudiodev(self, device_index=None)
-
Initialize audio output via ossaudiodev module.
The OSS audio module is part of the python standard library.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device. There is only a single OSS audio device.
Raises
ImportError
- ossaudiodev module is not available.
FileNotFoundError
- Failed to open audio device.
Documentation
https://docs.python.org/2/library/ossaudiodev.html
Installation
The ossaudiodev module needs an oss
/dev/dsp
device file. Enable an oss emulation via alsa by installingsudo apt install -y osspd
def open_winsound(self, device_index=None)
-
Initialize audio output via winsound module.
The winsound module is part of the python standard library.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device. Device selection is not supported by the winsound module.
Raises
ImportError
- winsound module is not available.
Documentation
https://docs.python.org/3.6/library/winsound.html https://mail.python.org/pipermail/tutor/2012-September/091529.html
def open(self, device_index=None, library=None)
-
Initialize the PlayAudio class with the best module available.
Parameters
device_index
:int
orNone
- Index of the playback device to be used. If None take the default device.
library
:str
orNone
- If specified, open a specific sound library.