How to…

This page provides a tour of audiomath’s main features. The examples all use the following conventions:

  • am is an alias for the audiomath package, created with import audiomath as am;
  • s is a Sound instance (as are s1, s2, etc.);
  • p is a Player instance;
  • r is a Recorder instance.

Many of the examples can be tried out easily using the built-in test sound:

s = am.TestSound()      # returns an 8-channel Sound
s = am.TestSound('12')  # returns just the first two channels

Read a sound from a file into memory

s = am.Sound('some_file.wav')  # uncompressed .wav files can be read using
                               # the standard Python library, under the hood

s = am.Sound('some_file.mp3')  # other formats require the third-party AVBin
                               # library---AVBin binaries for the most common
                               # OS platforms are included within audiomath

Write a sound from memory into a file

s.Write('some_file.wav')       # uncompressed .wav files can be written using
                               # the standard Python library, under the hood

s.Write('some_file.flac')      # other formats require the third-party
                               # command-line utility `ffmpeg` or `sox` to be
                               # installed on your system (you'll need to do
                               # this yourself---see the doc for the `am.ffmpeg`
                               # and `am.sox` classes for more info)

Create a sound from scratch, in memory

A specified number of seconds of silence:

s = am.Sound(5)                          # five seconds of silence
s = am.Sound(5, fs=44100, nChannels=2)   # the same, with sampling frequency and
                                         # number of channels specified explicitly

…or from a numpy array y which can be either one-dimensional (single-channel) or two-dimensional (samples-by-channels):

s = am.Sound(y, fs=44100)

The Sound instance’s property s.y will contain a reference to y (or, if y was one-dimensional, it will be a two-dimensional samples-by-channels view into y). You can also initialize from another Sound instance in the same way.

Another way to create a Sound from scratch is to create an instance and then use its GenerateWaveform() method:

s = am.Sound().GenerateWaveform(freq_hz=440, duration_msec=300)

This method is identical to the function of the same name, from the semi-independent audiomath.Signal submodule. Further useful methods duplicated from that submodule include ModulateAmplitude() and ApplyWindow().

Define a functionally-generated sound

You can use the Synth type (or even a pre-configured Synth instance) as a function decorator. That will turn the function into a Synth instance, which is a duck-typed replacement for a numpy array. It can be used to create a Sound either directly, or implicitly by passing it to the Player constructor:

import numpy as np
TWO_PI = 2.0 * np.pi

@am.Synth(fs=22050)
def tone440Hz(fs, sampleIndices, channelIndices):
    timeInSeconds = sampleIndices / fs
    return np.sin(TWO_PI * 440 * timeInSeconds)

s = am.Sound(tone440Hz)  # although it doesn't really support numeric manipulation...

# - or - #

p = am.Player(tone440Hz) # ... so this is the more likely use-case

The function should take three arguments: a scalar sampling frequency, an m x 1 array of sample indices, and a 1 x n array of channel indices. It should return either an m x 1 or an m x n array of sample values. More options are described in the documentation for the audiomath.Synth class.

Perform simple arithmetic

audiomath takes its name from the ability to do simple arithmetic with Sound objects. Sounds can be effortlessly added together (i.e. superimposed, mixed together):

s = s1 + s2

They can also be multiplied or divided—by scalars, to rescale the sound amplitude:

s /= 3
s2 = s * 2

—or by sequences of scalars, to rescale individual channels independently:

s *= [1, 0.5]

—or by other Sound objects (to perform amplitude modulation):

s = s_carrier * s_envelope

…although in that last case you might also want to check out the helper function audiomath.Signal.ModulateAmplitude().

Multiplication by a sequence is a good way to turn a monophonic Sound into a stereo or multi-channel Sound:

s2 = s.Copy().MixDownToMono() * [0, 1]   # mixes everything into the right channel,
                                         # and makes the left channel silent

In all cases, the Sound object is not overly fussy about matching dimensions: if two sounds of unequal duration are to be added or multiplied together, silence is automatically appended to the shorter data array so that the lengths match. Similarly, if one of the sounds has one channel and the other has more, the monophonic data array is automatically replicated up to the required number of channels. An exception is only raised if both Sound objects have more than one channel and the numbers of channels are unequal, or if they have different sampling frequencies.

The in-place versions the operators (+=, -=, *=, /=) modify the Sound instance (and where possible the numpy array s.y inside it) in place, whereas the corresponding infix operators (+, -, *, /) create a new instance, with a copy of the numpy array.

Extract a segment of a sound

Sound objects may be indexed using slice subscripts. The numeric start and stop values are interpreted as time in seconds:

s2 = s[1:3]   # returns a new `Sound` instance containing a "view" into a
              # two-second segment of `s`, starting 1 second from the beginning

s[-3:] *= 2   # doubles the amplitude of the last three seconds of `s` in-place

Start and stop times may also be expressed as strings with the format MM:SS.FFF or even HH:MM:SS.FFF:

s['02:01' : '02:03.4']   # slice from 121s to 123.4s

Extract selected channels from multi-channel sounds

An optional second subscript allows you to refer to specific channels:

s2 = s[:, 0]         # returns a new `Sound` instance containing a "view" into just
                     # the first channel of `s`

s2 = s[:, :2]        # returns a new `Sound` instance containing a "view" into just
                     # the first two channels of `s`

s2 = s[:, [0,1]]     # same as above, but returns a copy rather than a view (this is
                 # consistent with the behaviour of the underlying `numpy` array,
                 # when indexed the same way)

…and manipulate/assign to specific channels:

s[:, ::2] = 0        # silence every second channel of `s`, in-place

s[:, -1] = s[:, 0]   # make the last channel of `s` identical to the first

Channel indices may also be strings—if so, they are interpreted as 1-based indices rather than 0-based:

s2 = s[:, 0]         # \_ equivalent
s2 = s[:, "1"]       # /

s2 = s[:, [1,0]]     # \_ equivalent
s2 = s[:, "21"]      # /

You may, of course, slice out a time segment at the same time as extracting channels:

s[:0.25, :2] *= 2    # double the amplitude of the first quarter-second of `s`, in
                     # just the first two channels

You can also use the SplitChannels() method to obtain views into different channels or batches of channels.

Concatenate sounds in time (splicing)

The % operator can be used to concatenate Sound instances:

s = s1 % s2          # creates a new `Sound` instance
s %= s2              # changes instance `s` in-place

Numeric scalars stand for the corresponding number of seconds of silence:

s2 = 1.23 % s        # create a new instance, with 1.23s of silence prepended
s %= 3.45            # change instance `s` by appending 3.45s of silence

Equivalently, you can also use the Concatenate() method or global Concatenate() function. Both of these automatically delve into list or tuple instances included in their argument lists, which means you can use them with or without [] encasing the to-be-concatenated arguments:

s = am.Concatenate(s1, 5, s2)      # \_ equivalent (create new instance)
s = am.Concatenate([s1, 5, s2])    # /

s.Concatenate(5, s2)               # \_ equivalent (extend instance `s`)
s.Concatenate([5, s2])             # /

In all cases, single-channel data will get automatically replicated up to the required number of channels when you concatenate it with a stereo or multi-channel object. But an exception will be raised if you attempt to concatenate stereo or multi-channel objects that have different numbers of channels.

An exception is also raised if you try to concatenate Sound objects that have unequal sampling frequencies. You can use the Resample() method to equalize them—for example:

s  =  s1 % s2.Copy().Resample(s1)

Note that, if you want to append or prepend silence, with the goal of ensuring a particular final duration, this is a “padding” operation, which can be performed more easily as follows:

s.PadStartTo(2)  # prepend silence to ensure a total length of at least 2 seconds

or:

s.PadEndTo(3)  # append silence to `s` to ensure a total length of at least 3 seconds

s.PadEndTo(s2) # append silence to `s` to ensure total length is at least that of `s2`

Stack channels (multiplexing)

Sound objects can be assembled by “stacking”, to create objects with larger numbers of channels. The & operator is the easiest way of doing this:

s = s1 & s2   # stack two `Sound` instances to create a third instance

s &= s1       # stack the channels of `s1` onto the channels of `s` in-place

An exception is raised if you try to stack Sound objects that have unequal sampling frequencies. You can use the Resample() method to equalize them—for example:

s  =  s1 & s2.Copy().Resample(s1)

Mismatched durations are not a problem—Sound data are simply padded with silence at the end to ensure the required length. This means that, by default, content in different channels is aligned in time at the start. To align differently, you can combine your stacking operation with concatenation or padding:

s = s1 & (2 % s2)   # stack `s1` with a 2-second-delayed version of `s2`

s &= s1.Copy().PadStartTo(s.duration)  # stack extra channels onto `s`,
                                       # aligned at the end

The functional form of the stacking operator is the Stack() method (for in-place modification of a Sound instance) or global Stack() function (for creating a new instance). Both of these automatically delve into list or tuple instances included in their argument lists, so you can use them with or without [] encasing the arguments:

s = am.Stack(s1, s2, s3)      # \_ equivalent (create new instance)
s = am.Stack([s1, s2, s3])    # /

s.Stack(s1, s2)               # \_ equivalent (modify instance `s`)
s.Stack([s1, s2])             # /

Pitch-shift or time-stretch a sound

audiomath does not itself contain a phase vocoder implementation, but it provides convenience wrappers around the implementation in the optional third-party Python package librosa (which you must install separately, for example by saying python -m pip install librosa from your shell command prompt).

To use this, you must explicitly import audiomath.StretchAndShift. This will automatically import librosa and will then give you access to two new Sound instance methods:

s.TimeStretch(speed=0.5)    # slows down `s`, doubling its duration without
                            # changing its pitch

s.PitchShift(semitones=-12) # pitch-shifts `s` down by 1 octave, without changing
                            # its speed or duration

Preprocess a sound using SoX

The sox class (and similarly the ffmpeg class) can be used in two ways. The first way is to create an instance—this is an open connection to a running instance of the corresponding sox (or ffmpeg) command-line binary, good for processing chunks of sound data sequentially and (typically) streaming it out to a file. The second way, which is often simpler, is to use the class method Process()—this creates a temporary instance, streams to a temporary file, then loads the resulting file contents back into memory. This allows you to transform a Sound instance according to the effects provided by SoX, and immediately examine or further manipulate the results—for example:

s = am.TestSound('12')
s2 = am.sox.Process(s, effects='loudness -10')
# The `effects` arguments are as they would appear on
# the `sox` command line (see the SoX dox)

To take the same example one stage further, the following will generally equalize the perceived loudness (according to ISO 226) across all channels of a Sound s:

s = am.TestSound().AutoScale()  # eight-channel Sound
s2 = am.Stack(
    am.sox.Process(eachChannel, effects='loudness -10')
    for eachChannel in s.SplitChannels()
).AutoScale() # finally rescale all channels together
              # according to their collective maximum

Manipulate a sound in other miscellaneous ways

The following methods are also useful for tailoring sound objects:

  • AutoScale() removes DC offsets from each channel, and rescales the sound to standardize its maximum amplitude.
  • Center() removes DC offsets from each channel.
  • Fade() fades a sound in at the beginning and/or out at the end.
  • MixDownToMono() converts the data to mono (single-channel) by averaging across all channels.
  • Resample() changes the sampling frequency.
  • Reverse() reverses the sound samples in time.
  • Trim() removes data below a specified amplitude from the beginning and/or end of a sound.

All methods that modify a Sound instance in-place also return a reference to the Sound instance itself. This means methods can be chained—for example:

s.AutoScale().Trim().Resample(44100)                # `s` is modified in-place

This architecture, in combination with the Copy() method, also makes it easy to operate on a copy of a Sound instance without modifying the original instance in-place, and assign the result to a new variable name at the end of the chain:

s2 = s.Copy().AutoScale().Trim().Resample(44100)    # `s` is unchanged

Plot a sound

The third-party package matplotlib is used for plotting. Unlike numpy (a hard dependency without which audiomath can do nothing), matplotlib is an optional part of audiomath, so it is not installed automatically. If you want to be able to plot, you must install it yourself—for example by typing python -m pip install matplotlib at your shell command prompt. Once you have that, plotting is as easy as:

s.Plot()

Note that, if there are multiple channels, they are vertically separated from each other on the plot so that they can be easily distinguished. If you want a more conventional plot in which they are superimposed (so then the y-axis is more directly interpretable) you can use audiomath.Signal.PlotSignal instead:

from audiomath.Signal import PlotSignal
s = am.TestSound('12')
PlotSignal(s, hold=False)

You can also visualize the amplitude spectrum of the sound:

from audiomath.Signal import PlotSpectrum
s = am.TestSound('1')
PlotSpectrum(s, hold=False, dB=True, xlim=[0,1000], ylim=[-120, 0], xlabel='Frequency (Hz)', ylabel='Power (dB)')

Play sounds

The programmers’ interface to playback and recording is implemented as a generic pure-Python “front end”, and the default “back end” is the third-party PortAudio library, binaries for which are included with audiomath for the most common operating systems. Other back ends may be added in future, with the aim of preserving the programmers’ interface unchanged.

The quickest/dirtiest way to hear a preview of a Sound instance is to call its Play() method:

s.Play()

Note that this is synchronous—the method returns only when the Sound ends, or when you press ctrl-C. Note also that it is verbose, as a visible reminder that it is also inefficient: on the console, you will see that a Player object is being constructed (which takes a certain amount of time) and then destroyed at the end. For asynchronous, lower-latency playback you will want to create a Player instance p yourself in advance, and then call p.Play() at the time-critical moment. You can construct a Player using an existing Sound instance:

p = am.Player(s)

or indeed from anything that would also be a valid input to the Sound constructor, such as a filename:

p = am.Player('some_file.wav')

or a number of seconds of silence:

p = am.Player(6)

or a numpy array. In all cases the current Sound instance is then addressable as p.sound.

The seconds-of-silence example may seem useless until you know that a Player actually contains a Queue of Sound instances, each of which can be thought of as one “track”. When you have multiple tracks, silences then provide one easy way to create gaps between them. Multiple Sound instances can be loaded into a Player as a list or tuple:

p = am.Player([s1, s2, s3])               # a list of `Sound` instances...
p = am.Player([s, 0.5, 'some_file.wav'])  # or any valid `Sound`-constructor argument

…and even from a file glob pattern:

p = am.Player('my_sounds/*.wav')

In all cases, all tracks’ sound data are fully represented in memory (all files get loaded and decoded from disk into memory, and all numeric values get fully expanded into arrays of zeros in memory). The attribute p.queue is an instance of type Queue, which is a container for the tracks’ corresponding Sound instances. The Queue provides a list-like interface—for example, you can insert(), append(), del or pop() elements:

p = am.Player([])      # no tracks
p.queue.append(s)
p.queue.append(0.5)
p.queue.append('some_file.wav')
print(p.queue)   # pretty-prints indices and labels, and marks which track is current

To start playing, call p.Play() or set p.playing = True. To pause, call p.Pause() or set p.playing = False. During a call to p.Play() or p.Pause(), or alternatively during the Player() constructor call or p.Set(), you can use optional keyword arguments to set properties of p at the same time. Properties that affect playback behavior are as follows:

  • loop (bool): whether or not to play the current track repeatedly on a (seamless, indefinite) loop;
  • repeatQueue (bool): how to handle attempts to advance beyond the end, or go back earlier than the beginning, of p.queue (wrap around, or raise an exception);
  • autoAdvance (False, True, 'play' or 'pause'): how to behave when the current track p.sound finishes playing (True and 'play' are synonymous);
  • track (int, str): the integer index of the current track p.sound within the queue p.queue (when assigning this, you can also refer to Sound instances by their label property, e.g.: p.track = 'test12345678');
  • head (float): current playback position, in seconds, within the current track (negative numbers are counted backwards from the end of the track);
  • speed (float): a multiplier for playback speed;
  • pan (float): one way of panning the audio from left (-1.0) to right (+1.0);
  • levels (list): a list of per-channel amplitude scaling factors, each in the range 0.0 to 1.0 (independent of pan and volume);
  • volume (float): over-all amplitude scaling factor (0.0 to 1.0)
  • playing (bool): reflects and determines the current play/pause status;

So for example, to play a Sound instance s asynchronously on an endless seamless loop, the following are all equivalent:

p = am.Player(s);  p.Play(loop=True)

p = am.Player(s, loop=True);  p.Play()

p = am.Player(s);  p.loop = True;  p.Play()

p = am.Player(s, loop=True, playing=True)

By contrast, the following options let you play through a whole queue repeatedly on an endless loop (note that this will not be precisely seamless—there is sometimes an unavoidable millisecond or so of silence between tracks):

p = am.Player('tracks/*.mp3', loop=False, repeatQueue=True, autoAdvance=True)
p.Play()

By default, playback is asynchronous—i.e., p.Play() returns immediately. However if you want synchronous playback, you can say:

p.Play(wait=True)    # synchronous playback

You can skip to the beginning of the next, or to the beginning of the previous, track in p.queue as follows:

p.NextTrack()     #  \_ equivalent
p.track += 1      #  /

p.PreviousTrack() #  \_ equivalent
p.track -= 1      #  /

Most Player properties support dynamic assignment—that means, you can assign a callable function to them, which specifies the property value as a function of time. The following example uses dynamic assignment to the pan property, to pan a sound smoothly and automatically from left to right:

p = am.Player(am.TestSound('12').MixDownToMono())
import math
periodInSeconds = 4.0
p.Play(loop=True, pan=lambda t: math.sin(2 * math.pi * t / periodInSeconds))

Querying p.pan will give you the instantaneous numeric value. The callable itself can be retrieved by p.GetDynamic('pan') or by examining the p.dynamics property. More information can be obtained with help(am.Player.dynamics).

The Play() method has low computational overhead: once you have pre-initialized a Player instance p, you can therefore call p.Play() in time-critical parts of your code. If for any reason you need to go one step further, and construct new Player instances at time-critical moments, you can reduce the initialization overhead by pre-initializing a single Stream instance and then passing it as the stream argument in all new Player initializations:

s = am.TestSound('12') # sound pre-loaded into memory
m = Stream()           # connection to audio hardware pre-initialized
                       # (use the `device` keyword here to target a particular device)

p = am.Player(s, stream=m)  # this constructor call is now faster

Multiple Player instances can (and do, by default) all use the same Stream instance if one has already been created. If you want different Player instances to target different sound devices simultaneously, you will have to create a Stream instance explicitly for each device (see below for instructions on how to target a particular input or output device) and assign the streams explicitly when creating the respective Player instances.

Play sounds with more-precise latency, via PsychPortAudio

The Psychophysics Toolbox (a.k.a. PsychToolbox) has long been a mainstay of (Matlab-based) stimulus presentation in neuroscience. In 2019 its audio library PsychPortAudio was ported to Python as part of the psychtoolbox Python package. This is a specially tuned version of the PortAudio library, optimized for low latency and low jitter, with extra functionality such the ability to pre-schedule a sound. audiomath has a separate “back-end” that can take advantage of this. First, you will need to install the psychtoolbox package, for example by typing python -m pip install psychtoolbox at your command prompt. Then, in your Python program, you will need to tell audiomath to use the corresponding back-end implementation:

import audiomath as am
am.BackEnd.Load('PsychToolboxInterface')
# as opposed to the default 'PortAudioInterface'

This sacrifices some functionality—most of the real-time manipulation capabilities, such as dynamic properties and on-the-fly resampling—in return for latency improvements. In January–February 2020 our tests found that the absolute latencies improved a little (macOS, Linux, some Windows devices) or not at all (most of our Windows devices) relative to the best available settings of the default PortAudioInterface back-end. However, what really improved was the jitter, when taking advantage of the unique “pre-scheduling” functionality PsychPortAudio provides. This is implemented via the new when property of the Player class:

am.BackEnd.Load('PsychToolboxInterface')
p = am.Player(am.TestSound('12'))
offsetSeconds = 0.025
t0 = am.Seconds()
p.Play(when=t0 + offsetSeconds)

offsetSeconds needs to be comfortably larger than the minimal absolute latency PsychPortAudio can achieve on your hardware and OS—we found 25ms worked for most of our Windows devices, and 5ms for our Macs. Provided this is the case, we found the time between assignment to t0 and physical delivery of the sound was accurate to within a millisecond or two (depending on the hardware and OS) and very precise: on any given machine, the standard deviation might be of the order of 0.1 milliseconds.

Record a sound into memory

To record sound data from an input device into memory, the most useful tool is probably the synchronous global Record() function:

s = am.Record()   # records for up to 60 seconds (less if ctrl-C
                  # is pressed) and returns a `Sound` instance
                  # containing the recorded data.

The length of the buffer can be specified by a numeric first argument, or you can record directly into a pre-prepared Sound. For example, to record for just 3 seconds:

s = am.Record(3, nChannnels=1)                    # \_ equivalent
s = am.Sound(3, nChannnels=1); am.Record(s)       # /

Under the hood, the Record() function creates an instance of the Recorder class. If you want to perform asynchronous recording, you can use this class yourself:

r = am.Recorder(10, start=False)

# wait until you are ready to start, then:

r.Record()
while r.recording:
    time.sleep(0.001)  # or use the time however you like...
r.sound.Plot()

Record a sound directly to file

The filename argument to Record() or Recorder() allows you to stream recorded data directly to a file. This requires the command-line utility ffmpeg to be separately installed on your system—see the audiomath.ffmpeg doc for more details.

To record indefinitely into a file without filling up memory, you can combine this with the loop=True option, which lets you record into a circular buffer. The following example records indefinitely into a 3-second circular buffer, streaming the recorded data all the while into the file blah.mp3:

s2 = am.Record(3, loop=True, filename='blah.mp3')

When you press ctrl-C it stops, cleans up, and returns the last 3 seconds as a Sound instance.

Target a particular input or output device

You can have audiomath list your devices right from the shell command prompt—for example, on a Mac:

$ python -m audiomath --devices

hostApi.name  index     name                 #in  #out  defaultSampleRate
Core Audio    0      <  Built-in Microphone   2    0    44100
Core Audio    1      >  Built-in Output       0    2    44100
Core Audio    2         ZoomAudioDevice       2    2    48000

or on Windows:

> python -m audiomath --devices

hostApi.name         index     name                                       #in  #out  defaultSampleRate
MME                  0         Microsoft Sound Mapper - Input              2    0    44100
MME                  1      <  Microphone (High Definition Aud             2    0    44100
MME                  2         Microphone (High Definition Aud             2    0    44100
MME                  3         Microsoft Sound Mapper - Output             0    2    44100
MME                  4      >  Speakers (High Definition Audio             0    2    44100
Windows DirectSound  5      <  Primary Sound Capture Driver                2    0    44100
Windows DirectSound  6         Microphone (High Definition Audio Device)   2    0    44100
Windows DirectSound  7         Microphone (High Definition Audio Device)   2    0    44100
Windows DirectSound  8      >  Primary Sound Driver                        0    2    44100
Windows DirectSound  9         Speakers (High Definition Audio Device)     0    2    44100
ASIO                 10     *  ASIO4ALL v2                                 2    2    44100
ASIO                 11        Realtek ASIO                                2    2    44100
Windows WASAPI       12     >  Speakers (High Definition Audio Device)     0    2    48000
Windows WASAPI       13        Microphone (High Definition Audio Device)   2    0    44100
Windows WASAPI       14     <  Microphone (High Definition Audio Device)   2    0    44100
Windows WDM-KS       15     <  Microphone (HD Audio Microphone)            2    0    44100
Windows WDM-KS       16     >  Speakers (HD Audio Speaker)                 0    2    44100
Windows WDM-KS       17        Microphone (HD Audio Microphone 2)          2    0    44100

The equivalent Python code is:

list_of_devices = am.GetDeviceInfo()
print(list_of_devices)     # pretty-prints the list of device records as a table

A device record from this list, or even just the integer device index from it, can be passed as the optional device= argument to the Player(), Recorder() and Stream() constructors, or to the Record() global function.

You can also use FindDevice() and FindDevices() to narrow down the list according to a partial device name and/or support for a specified number of input and output channels, and to reorder the devices according to your preferred order of host APIs:

list_of_devices = am.FindDevices(mode='oo')  # match all devices that provide at
                                             # least two output channels

device = am.FindDevice(mode='oo')   # find the preferred stereo output device (raise
                                    # an exception if no matching device is found)

FindDevice is called implicitly if you use a string argument as your device argument, during Player, Recorder or Stream creation, and it is able to match partial device names, partial host-API names, or a combination of both delimited by '//':

p = am.Player(s, device='Speakers') # matches the first device with 'Speakers' in
                                    # its name (host APIs are prioritized according to
                                    # `am.PORTAUDIO.DEFAULT_OUTPUT_API_PREFERENCE_ORDER`)
# - or - #

p = am.Player(s, device='WDM-KS//Speakers')   # Matches host API and device explicitly

# - or - #

p = am.Player(s, device='WDM-KS//')  # default output device of the WDM-KS host API

If p is the first Player instance to be created, it will implicitly create a Stream instance, which it will store as p.stream. Subsequent newly-constructed Player instances will re-use the same Stream by default (unless a Stream instance is explicitly supplied in the Player() constructor) so they will ignore the device string. This will continue to be the case until p.stream is garbage-collected (i.e. after deletion of p and all the other Player instances that use the same Stream).

Manipulate the operating-system’s overall volume settings

The recommended way to manipulate the system’s overall volume is with the audiomath.SystemVolume.SystemVolumeSetting context manager:

p = am.Player(am.TestSound())

from audiomath.SystemVolume import SystemVolumeSetting
# NB: the sub-module must be imported explicitly

with SystemVolumeSetting(0.1, mute=False):
    p.Play(wait=True)

Also, MAX_VOLUME is a ready-made SystemVolumeSetting instance that can be used to turn the volume up to 1.0 and ensure the output is not muted:

from audiomath.SystemVolume import MAX_VOLUME

with MAX_VOLUME:
    p.Play(wait=True)

On Windows, this functionality requires the third-party packages comtypes and psutil, which should have been installed automatically as part of the python -m pip install audiomath procedure.

You can also use the lower-level functions audiomath.SystemVolume.SetVolume() and audiomath.SystemVolume.GetVolume(). However, the context-manager strategy is more highly recommended, as it ensures that the system volume settings are restored to their previous state when the context ends.

Measure audio latency

For this, you will need the full audiomath source-code repository, as described in the Advanced Installation section. You will also need to build or adapt the appropriate hardware, described in python/developer-tests/PlayerLatency-TeensySketch within the repository. Then you will need to run the script python/developer-tests/PlayerLatency.py (run it with --help first, for an overview).