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.

  • play(): playback audio data.
  • beep(): playback a tone.
  • close(): close the global PlayAudio instance.

Helper functions

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

Default audio device handler.

Defaults to None. Will get a PlayAudio instance assigned via play() or beep().

Functions

def main(*args)

Call demo with command line arguments.

Parameters

args : list of strings
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. If fadetime is larger than half the duration of the data, then fadetime 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. If fadetime is larger than half the duration of the data, then fadetime 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. If fadetime is larger than half the duration of the data, then fadetime 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 variable handle.

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. 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 variable handle.

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 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. 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 of int
Device indices.
devices : list of str
Devices corresponding to indices.
def speaker_devices_sounddevice()

Query available output devices of the sounddevice module.

Returns

indices : list of int
Device indices.
devices : list of str
Devices corresponding to indices.
def speaker_devices_soundcard()

Query available output devices of the soundcard module.

Returns

indices : list of int
Device indices.
devices : list of str
Devices corresponding to indices.
def speaker_devices(library=None, verbose=0)

Query available output devices.

Parameters

library : str or None
If specified, use specific sound library.
verbose : int
Verbosity level.

Returns

indices : list of int
Device indices.
devices : list of str
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 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()
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 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.
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.

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.

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
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

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

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
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

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.