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 :ref:`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).