--- jupyter: jupytext: notebook_metadata_filter: nbsphinx,-kernelspec text_representation: extension: .md format_name: markdown format_version: '1.3' jupytext_version: 1.13.8 nbsphinx: execute: never --- .. meta:: :description: Topic: Audio Processing, Category: Exercises :keywords: sound wave, pressure, audio basics, temporal waveform # Exercises: Basics of Sound Waves ```python import matplotlib.pyplot as plt import numpy as np %matplotlib notebook ``` (1.1.1) Create a Python function that describes a pressure wave impinging on a microphone. Assume that the sound wave is a sustained, pure tone of frequency $f$ and amplitude $A$, and that $p(0) = 0$. Note that this function represents our *temporal waveform*: the function that you create is defined on a continuous domain. While this represents a continuous mathematical function, we must work with concrete numbers when plotting and analyzing these functions on a computer. Thus we will evaluate this function at a discrete set of times. Note that the following function signature makes use of [type hints](https://www.pythonlikeyoumeanit.com/Module5_OddsAndEnds/Writing_Good_Code.html#Type-Hinting). Furthermore, the arguments `amp` and `freq` come after the `*` character in the signature, which means that they are keyword-only arguments in our function. This is to prevent us from accidentally swapping numbers when we pass our numbers into it. ```python def pressure(times: np.ndarray, *, amp: float, freq: float) -> np.ndarray: """Describes the temporal waveform of a pure tone impinging on a microphone at times `times` (an array of times). The wave has an amplitude `amp`, measured in Pascals, and a frequency `freq`, measured in Hz. Parameters ---------- times : numpy.ndarray, shape=(N,) The times at which we want to evaluate the sound wave amp : float The wave's amplitude (measured in Pascals - force per unit area) freq : float The wave's frequency (measured in Hz - oscillations per second) Returns ------- numpy.ndarray, shape=(N,) The pressure at the microphone at times `t` Notes ----- We only care about the wave at a fixed location, at the microphone, which is why we do not have any spatial component to our wave. """ # # return amp * np.sin(2 * np.pi * freq * times) # ``` (1.1.2) As stated above, the function that you just wrote can be thought of as a representation of the temporal waveform that is recorded by our microphone: it represents the continuous fluctuations in air density associated with a sound wave. We can "sample" this function by evaluating the function at specific times. Evaluate the temporal waveform for a $C_{4}$-note ($261.63 \:\mathrm{Hz}$) played for $3$ seconds with an amplitude of $0.06\:\mathrm{Pascals}$ **using the sampling rate 44100 Hz (samples per second)**. That is, evaluate your function at evenly-spaced times according to this sampling rate for a time duration of $3$ seconds. You can compute the times at which you will evaluate your function using: ```python duration = 3 # seconds sampling_rate = 44100 # Hz n_samples = int(duration * sampling_rate) + 1 # the times at which you should sample your temporal waveform times = np.arange(n_samples) / sampling_rate # seconds ``` You should ultimately produce an array, `c_4`, of pressure values associated with this pure tone. Include comments where appropriate to indicate the physical units of measurements associated with the quantities involved. ```python # amplitude = 0.01 # Pascals duration = 3 # seconds sampling_rate = 44100 # Hz n_samples = int(duration * sampling_rate) + 1 times = np.arange(n_samples) / sampling_rate # seconds freq = 261.63 # Hz c_4 = pressure(times, amp=amplitude, freq=freq) # Pascals # ``` Play the $3$-second audio using ```python from IPython.display import Audio Audio(c_4, rate=44100) ``` Note that `Audio` automatically normalized the volume according to its slider, so the amplitude that we set will have no effect. Adjusting the amplitude would typically manifest as a change in volume! ```python # from IPython.display import Audio Audio(c_4, rate=sampling_rate) # ``` (1.1.3) Using `pressure(...)`, plot **4 periods (repetitions) of the sound wave**. Label the $x$- and $y$-axes, including the units of the values being plotted. Use enough points to make the plot look smooth. Here is some pseudocode for plotting: ```python fig, ax = plt.subplots() t = # array/sequence of times pressures = # array/sequence of pressure samples ax.plot(t, pressures) ax.set_ylabel("Pressure [Pa]") ax.set_xlabel("Time [s]"); ``` The time required for one repetition is $T = \frac{1}{f}$, and the number of samples that we take per second is $f_s$, thus \begin{equation} N_{samples} = T \times f_s = \frac{f_{s}}{f} \end{equation} is the number of samples associated with *one* period of oscillation. ```python # fig, ax = plt.subplots() # number of samples associated with 4 repetitions duration = 4 / 261.63 # seconds sampling_rate = 44100 # Hz n_samples = int(duration * sampling_rate) + 1 times = np.arange(n_samples) / sampling_rate # seconds ax.plot(times, pressure(times, amp=amplitude, freq=freq)) ax.set_xlabel("t (seconds)") ax.grid("True") ax.set_ylabel("Pressure [Pa]") ax.set_xlabel("Time [s]"); # ``` (1.1.4) **Leveraging the principle of superposition**, plot the waveform of the C-major triad for $0.64$ seconds. This should combine three pure tones of equal amplitudes ($0.01 \;\mathrm{Pa}$) of the following respective frequencies: - 523.25 Hz (C) - 659.25 Hz (E) - 783.99 Hz (G) Use the same sampling rate of $44,100\; \mathrm{Hz}$ to determine the times at which you will evaluate this temporal waveform. ```python # amp = 0.01 # Pascals duration = 0.64 sampling_rate = 44100 # Hz n_samples = int(duration * sampling_rate) + 1 times = np.arange(n_samples) / sampling_rate # seconds # the principle of super positions simply states that we combine individual # components of a sound wave by adding them chord = ( pressure(times, amp=amp, freq=523.25) + pressure(times, amp=amp, freq=659.25) + pressure(times, amp=amp, freq=783.99) ) fig, ax = plt.subplots() ax.plot(times, chord) ax.set_ylabel("Pressure [Pa]") ax.set_xlabel("Time [s]") ax.set_title("Major Triad"); # ``` Play the major triad audio clip for $3$ seconds. ```python # You might want to turn down the volume on your computer a bit # amp = 0.01 # Pascals duration = 3 # seconds times = np.arange(0, int(sampling_rate * duration) + 1) / sampling_rate # seconds chord = ( pressure(times, amp=amp, freq=523.25) + pressure(times, amp=amp, freq=659.25) + pressure(times, amp=amp, freq=783.99) ) # Pascals Audio(chord, rate=sampling_rate) # ``` Isn't it beautiful? Notice how messy looking the waveform is. It is wholly unintuitive to look at the data in this way, even though it is only comprised of $3$ simple notes. In an upcoming section, we will see that we can convert this *amplitude-time* data into *amplitude-frequency* data, which is much more useful for us! This conversion process is known as a **Fourier Transform**. (1.1.5) Lastly, define a function that describes a pressure wave for **noise**. That is, use `numpy.random.rand` ([docs](https://numpy.org/doc/stable/reference/random/generated/numpy.random.rand.html)) to generate samples randomly between $0$ and $1$). Plot some of its temporal waveform for a duration of $0.05$ seconds, using a sampling rate of $44,100$ Hertz. ```python # def noise(t): return np.random.rand(*t.shape) fig, ax = plt.subplots() duration = 0.05 # seconds times = np.arange(0, int(sampling_rate * duration) + 1)/ sampling_rate # seconds ax.plot(times, noise(times)) ax.set_ylabel("Pressure [Pa]") ax.set_xlabel("Time [s]") ax.set_title("Noise!"); # ``` Now play 3 seconds of noise! ```python # duration = 3 times = np.arange(0, int(sampling_rate * duration) + 1) / sampling_rate Audio(noise(times), rate=sampling_rate) # ```