2.1. Sampling period and rate#

The basic scheme for digitizing an analog signal is to measure the signal \(x(t)\) at a sequence of uniformly spaced time points. The sampling period is denoted by \(t_s\), which must be a positive number, measuring the number of seconds between samples. (Fractional values are allowed.) The resulting sequence of samples will be

\[ x(0), x(0 + t_s), x(0 + t_s + t_s), \dots \]

More generally, the \(n^\text{th}\) sample (for an arbitrary integer \(n = 0, 1, 2, \dots\)) represents the signal at time \(n\cdot t_s\). In a slight abuse of notation, we use square brackets with index \(n\) to indicate discrete signals \(x[n]\), and parentheses with time \(t\) to denote continuous signals:

(2.1)#\[x[n] = x(n \cdot t_s).\]

By convention, we use \(N\) to denote the total number of samples.

a continuous signal being sampled at uniformly spaced intervals

Fig. 2.1 A continuous signal (solid curve) sampled at t_s=0.2 [seconds / sample] (dots).#

Oftentimes, it is more convenient to work with the sampling rate, which we denote as

\[ f_s = \frac{1}{t_s} \quad \left[\frac{\text{samples}}{\text{second}}\right]. \]

Note that the sampling rate can always be converted to a sampling period (and vice versa) by taking reciprocals, resulting in the following (equivalent) form for discretely sampled signals:

(2.2)#\[x[n] = x\left(\frac{n}{f_s}\right).\]

2.1.1. Discrete signals and visualization#

A signal \(x[n]\) which has been sampled is said to be a discrete-time signal (or sometimes, just discrete signal), to distinguish it from continuous-time signals \(x(t)\).

Properly speaking, we do not have sample values at non-integer indices (e.g., \(x[n+1/2]\)), but it is often helpful for understanding to connect sample values in visualizations. When visualizing discrete-time signals, we adopt the convention of using step-plots rather than continuously varying curves (like \(x(t)\) in the figure above), to emphasize the fact that the signal has been discretized. Step plots, as demonstrated below, preserve the sample value \(x[n]\) up to the next sample position \(x[n+1]\).

A continuous signal sampled at different sampling rates.

Fig. 2.2 A continuous signal \(x(t)\) and the discrete signal \(x[n]\) obtained by sampling at 5 Hz (top), 25 Hz (middle), and 50 Hz (bottom). The discrete-time signal is illustrated as a step plot. At sufficiently high sampling rates, the continuous and discrete signals appear visually similar.#

For most practical applications, we tend to have sampling rates that are sufficiently high to guarantee that a step plot and a continuous plot look visually identical. However, throughout this text, we will often use examples generated at low sampling rates because they are easier to understand.

2.1.2. Tone generation#

We now have everything that we need to start making sounds. In this example, we’ll generate a pure tone at 220 Hz.

Recall from the previous chapter that a wave at frequency \(f_0\) is expressed as a function of time \(t\) by

\[x(t) = \cos\left(2\pi \cdot f_0 \cdot t\right).\]

(Here, we’ll ignore amplitude and phase to keep things simple.)

Even if we don’t have an existing signal to sample, we can still sample from this idealized signal by substituting \(t = n / f_s\) and computing the cosine values directly:

\[x[n] = \cos\left(2\pi \cdot f_0 \cdot \frac{n}{f_s}\right),\]

or, in code:

for n in range(N):  # n goes from 0, 1, 2, ..., N-1
    x[n] = np.cos(2 * np.pi * f_0 * n / f_s)

2.1.2.1. Vectorization#

In practice, generating a tone in this fashion would be rather slow – at least, when using the Python programming language. Instead, a much faster way to do it is to pre-allocate all the values of n as a vector

n = [0, 1, 2, ..., N-1].

In code, you can do this by using the np.arange function, like so:

n = np.arange(N)  # n is now an array containing values [0, 1, 2, ..., N-1]
x = np.cos(2 * np.pi * f_0 * n / f_s)

The result will be an array x with N total samples. Python (numpy) is smart enough to know that when you multiply, add, and call cosine on lists of numbers, it should apply these operations to each element of the list. This process is called vectorization, and it’s quite common in numerical computing. It might look a little strange and take some getting used to, but it does simplify and accelerate many of the types of operations we do in signal processing code.

# We use numpy for numeric computation
import numpy as np

# The Audio object allows us to play back sound
# directly in the web browser
from IPython.display import Audio


# We'll use an 8 KHz sampling rate, roughly
# equivalent to telephone quality
fs = 8000

# And generate 1.5 seconds of audio
duration = 1.5

# Total number of samples
# we round it down to a whole number by int(...)
N = int(duration * fs)

# Generate a pure tone at 220 Hz
f0 = 220

# Make an array of sample indices
n = np.arange(N)

# And make the tone, using (n / fs) in place of t
x = np.cos(2 * np.pi * f0 * n / fs)

# How's it sound?
Audio(data=x, rate=fs)