import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
11.0 Sampling#
We first define precisely what we mean by a sample: a contiguous chunk of audio samples extracted from some source recording.
Definition 33 (Sample)
Let \(\mathbf{x} = [x[0], x[1], \ldots, x[N-1]]\) be source digital audio at sample rate \(f_s\) of length \(N\) samples. A sample of \(\mathbf{x}\) is a contiguous chunk
of length \(M \leq N\) audio samples, starting at offset \(m \in \{0, 1, \ldots, N - M\}\).
In units of seconds, the sample starts \(\frac{m}{f_s}\) seconds into the recording and lasts \(\frac{M}{f_s}\) seconds.
We have seen operations like this before. When we extracted frames for frame-based processing and granular synthesis in Chapter 10, each frame was also a contiguous chunk of audio samples.
The key difference here is how we choose \(m\) and \(M\). In frame-based processing, they are determined procedurally: the offsets are multiples of the hop length \(N_H\), and every frame has the same frame length \(N_F\). When sampling, by contrast, a person chooses \(m\) and \(M\) (the start time and duration of the sample) to capture something musically meaningful, like a single drum hit or a memorable phrase. They typically make this choice by ear, or visually by looking at the waveform.
Try it yourself below, using one of the most famous samples of all time: the Amen break. Choose an offset \(\frac{m}{f_s}\) and a duration \(\frac{M}{f_s}\) (both in seconds), and use the zoom to inspect the waveform up close. Can you isolate a single snare drum or cymbal hit?
Drag the sliders to choose a sample: its offset \(m / f_s\) and its duration \(M / f_s\), both in seconds. The sample is highlighted in blue, and the zoom slider magnifies the waveform around it. The audio card underneath always plays the current sample.
Extracting samples in Pyquist#
In pyquist, extracting a sample is a one-liner. We can work in units of seconds with pq.Audio.segment, whose offset and duration arguments correspond to \(\frac{m}{f_s}\) and \(\frac{M}{f_s}\). Or we can work in units of audio samples by slicing an pq.Audio directly with audio[m:m + M], just like a NumPy array.
Below, we extract the second bar of the Amen break both ways. Each beat of the break lasts about \(0.435\) seconds, so the second bar starts \(4 \times 0.435 = 1.74\) seconds into the recording and lasts just as long:
amen = pq.Audio.from_file("./assets/amen.wav")
f_s = amen.sample_rate
print(f"N = {len(amen):,} samples ({amen.duration:.2f} seconds at {f_s} Hz)")
pq.play(amen)
pq.plot(amen)
N = 307,411 samples (6.97 seconds at 44100 Hz)
<Axes: xlabel='Time (s)', ylabel='Amplitude'>
beat = 0.435 # approximately the duration of one beat of the break
offset = beat * 4
duration = beat * 4
# Extract the second bar (four beats) in units of seconds...
bar = amen.segment(offset=offset, duration=duration)
pq.play(bar)
# ...or, equivalently, in units of samples
m, M = int(offset * f_s), int(duration * f_s)
same_bar = amen[m:m + M]
print(f"m = {m:,}, M = {M:,}, same samples: {np.array_equal(bar, same_bar)}")
m = 76,734, M = 76,734, same samples: True
Both approaches extract the same \(M = 76{,}734\) audio samples. Use whichever units are more natural for the task at hand: seconds when working by ear, and audio samples when you need exact control.
Note that a sample extracted in either fashion does not allocate any new memory. It simply points to those audio samples in their original memory. Accordingly, any manipulations done to the sample will also apply upstream. Use pq.Audio.copy to explicitly create a copy of the memory.