Open an interactive online version by clicking the badge Binder badge or download the notebook.

Arithmetic Operations#

Many algorithms require arithmetic operations on signals, such as adding or multiplying two signals, or performing a matrix multiplication between an array and a multi-channel signal. To this end, pyfar implements the operations add, subtract, multiply, divide, power, and matrix_multiplication in top level functions. The following examples illustrate how to perform arithmetic operations using these functions and corresponding operators +, -, *, /, **, and @. It is best to learn about pyfar audio objects before continuing with these examples.

[1]:
import pyfar as pf

Arithmetic operations using two or more audio objects#

Consider the two signals below for illustrating the use of arithmetic operations

[2]:
signal_1 = pf.Signal([1, 0, .5, 0], 44100)
signal_2 = pf.Signal([2, -2, 0, 0], 44100)

They can be added using pyfar.add in the time and frequency domain

[3]:
result = pf.add((signal_1, signal_2), domain='time')
print(f'Time domain addition:\nresult.time = {result.time}\n')
Time domain addition:
result.time = [[ 3.  -2.   0.5  0. ]]

[4]:
result = pf.add((signal_1, signal_2), domain='freq')
print(f'Frequency domain addition:\nresult.time = {result.time}')
Frequency domain addition:
result.time = [[ 3.  -2.   0.5  0. ]]

In this case it does not matter if the operation is performed in the time or frequency domain, because the FFT and the addition are linear and time invariant operations. But this is not true for all arithmetic operations as will become clear later. All arithmetic operations are performed in the frequency domain by default and pyfar overloads the standard arithmetic operators. The same addition operation can thus be done with

[5]:
result = signal_1 + signal_2
print(f'Frequency domain addition:\nresult.time = {result.time}')
Frequency domain addition:
result.time = [[ 3.  -2.   0.5  0. ]]

If using the same signals for a multiplication, it becomes clear that the domain makes a difference

[6]:
result = pf.multiply((signal_1, signal_2), domain='time')
print(f'Time domain multiplication:\nresult.time = {result.time}\n')
Time domain multiplication:
result.time = [[ 2. -0.  0.  0.]]

[7]:
result = pf.multiply((signal_1, signal_2), domain='freq')
print(f'Frequency domain multiplication:\nresult.time = {result.time}')
Frequency domain multiplication:
result.time = [[ 2. -2.  1. -1.]]

In the time domain, the result is an element wise multiplication of the time signals, but the frequency domain multiplication equals a cyclic convolution of the time signals.

The examples above used only two signals but it worth noting that all operations work for an arbitrary number of audio objects as long as all audio objects are of the same type, that is all audio objects are Signals, TimeData, or FrequencyData objects but not mixtures thereof. The following example illustrates this for three TimeData objects

[8]:
time_1 = pf.TimeData([1, 0, 0], [0, 1, 3])
time_2 = pf.TimeData([0, 1, 0], [0, 1, 3])
time_3 = pf.TimeData([0, 0, 1], [0, 1, 3])

result = time_1 + time_2 + time_3
print(f'result.time={result.time}')
result.time=[[1. 1. 1.]]

Of course all operations on TimeData are always performed in the time domain and all operations on FrequencyData are always performed in the frequency domain.

Arithmetic operations also work with multichannel signals, as long as the cshapes of all signals can be broadcasted. For example a single channel and a two channel FrequencyData object can be added

[9]:
freq_1 = pf.FrequencyData([1, 1, 1], [125, 250, 500])    # single channel
freq_2 = pf.FrequencyData([[1, 1, 1],
                           [2, 2, 2]], [125, 250, 500])  # two channels

result = freq_1 + freq_2
print(f'result.freq=\n{result.freq}')
result.freq=
[[2. 2. 2.]
 [3. 3. 3.]]

DFT normalization and arithmetic operations#

All frequency domain operations on pyfar.Signal objects work on signal.freq_raw, that is, the signal spectrum without normalization of the Discrete Fourier Transform. The arithmetic operations are implemented in a way that only physically meaningful operations are allowed with respect to the FFT normalizations. These rules are motivated by the fact that the normalization denotes if a signal is an energy signal (e.g., an impulse response) or a power signals (e.g., a finite block samples from an otherwise infinite noise process). While addition and subtraction are equivalent in the time and frequency domain, this is not the case for multiplication and division. Nevertheless, the same rules apply regardless of the domain for convenience:

Addition, subtraction, multiplication, and power

  • If one signal has the FFT normalization 'none', the results gets the normalization of the other signal.

  • If both signals have the same FFT normalization, the results gets the same normalization.

  • Other combinations raise an error.

Division

  • If the denominator signal has the FFT normalization 'none', the result gets the normalization of the numerator signal.

  • If both signals have the same FFT normalization, the results gets the normalization 'none'.

  • Other combinations raise an error.

Arithmetic operations using audio objects and arrays#

All arithmetic operations also work between audio objects and scalars, and audio objects and arrays if the shape of the array can be broadcasted to the cshape of the audio object. This means that the same operation is applied to all time or frequency values of an audio object. The example below performes a matrix multiplication between an array of shape=(4, 3) and a signal of cshape(3, ), which results in a signal of cshape=(4, ). Because the same matrix multiplication is done for all time or frequency values, in the example below, it does not matter whether the operation is done in the time or frequency domain. Hence, we directly used the shorthand @ that is equivalent to using pf.matrix_multiplication with the default parameters.

[10]:
signal = pf.Signal([[2, -2, 2, -2],
                    [1, -1, 1, -1],
                    [1,  0, -1, 0]], 44100)

matrix = [[1,  0,  1],
          [1,  1,  0],
          [1,  0, -1],
          [1, -1,  0]]

result = matrix @ signal

print('result.time=')
print(result.time)
result.time=
[[[ 3. -2.  1. -2.]]

 [[ 3. -3.  3. -3.]]

 [[ 1. -2.  3. -2.]]

 [[ 1. -1.  1. -1.]]]

In praxis the signal could be a 2D Ambisonics signal and that is decoded to a four channel loudspeaker array by multiplication with the decoder matrix. The matrix multiplication is special compared to all other arithmetic operations in the sense that it has an additional parameter to specify the axes along which the operation is performed. The example above, however, worked as expected with the default parameters.

License notice#

This notebook © 2024 by the pyfar developers is licensed under CC BY 4.0

CC BY Large

Watermark#

[11]:
%load_ext watermark
%watermark -v -m -iv
Python implementation: CPython
Python version       : 3.10.13
IPython version      : 8.23.0

Compiler    : GCC 11.4.0
OS          : Linux
Release     : 5.19.0-1028-aws
Machine     : x86_64
Processor   : x86_64
CPU cores   : 2
Architecture: 64bit

pyfar: 0.6.5