The second edition of Think DSP is not for sale yet, but if you would like to support this project, you can buy me a coffee.

Phase#

Click here to run this notebook on Colab.

Hide code cell content

import importlib, sys
sys.modules["imp"] = importlib

%load_ext autoreload
%autoreload 2

Hide code cell content

# download thinkdsp.py

from os.path import basename, exists

def download(url):
    filename = basename(url)
    if not exists(filename):
        from urllib.request import urlretrieve
        local, _ = urlretrieve(url, filename)
        print('Downloaded ' + local)
        
download("https://github.com/AllenDowney/ThinkDSP2/raw/main/soln/thinkdsp.py")
import numpy as np
import matplotlib.pyplot as plt

from thinkdsp import decorate

Do we hear phase?#

This notebook investigates what effect, if any, changes in phase have on our perception of sound.

I’ll start with a simple waveform, a sawtooth, and move on to more natural sounds.

from thinkdsp import SawtoothSignal

signal = SawtoothSignal(freq=500, offset=0)
wave = signal.make_wave(duration=0.5, framerate=40000)
wave.make_audio()
wave.segment(duration=0.01).plot()
decorate(xlabel='Time (s)')
_images/8ca826c8720ca6ec0b8dd2020be87603da2b57b25332081a0517ed7d9b5d1ef9.png
spectrum = wave.make_spectrum()
spectrum.plot()
decorate(xlabel='Frequency (Hz)',
         ylabel='Amplitude')
_images/8f425df9a549258d3ce2e15e44a294388cf163a532500081d2a45c97ba976a05.png

The following function plots the angle part of the spectrum.

def plot_angle(spectrum, thresh=1):
    angles = spectrum.angles
    angles[spectrum.amps < thresh] = np.nan
    plt.plot(spectrum.fs, angles, 'x')
    decorate(xlabel='Frequency (Hz)', 
             ylabel='Phase (radian)')

At most frequencies, the amplitude is small and the angle is pretty much a random number. So if we plot all of the angles, it’s a bit of a mess.

plot_angle(spectrum, thresh=0)
_images/8926f07bf8045ac4cc141cd02a54d46ef99008db12d3cbbb2f4af710072a86d9.png

But if we select only the frequencies where the magnitude exceeds a threshold, we see that there is a structure in the angles. Each harmonic is offset from the previous one by a fraction of a radian.

plot_angle(spectrum, thresh=1)
_images/8273f32575293d8b9916100d593af04cd0dca852581c7b866dd1d436f99b2ddd.png

The following function plots the amplitudes, angles, and waveform for a given spectrum.

def plot_three(spectrum, thresh=1):
    """Plot amplitude, phase, and waveform.
    
    spectrum: Spectrum object
    thresh: threshold passed to plot_angle
    """
    plt.figure(figsize=(10, 4))
    plt.subplot(1,3,1)
    spectrum.plot()
    plt.subplot(1,3,2)
    plot_angle(spectrum, thresh=thresh)
    plt.subplot(1,3,3)
    wave = spectrum.make_wave()
    wave.unbias()
    wave.normalize()
    wave.segment(duration=0.01).plot()
    display(wave.make_audio())

So we can visualize the unmodified spectrum:

plot_three(spectrum)
_images/a69952c5c75a885813a89bb5ce4ac02f28de50791b87618b5eb085d6ae5c3518.png

Now let’s see what happens if we set all the angles to zero.

def zero_angle(spectrum):
    res = spectrum.copy()
    res.hs = res.amps
    return res

The amplitudes are unchanged, the angles are all zero, and the waveform looks very different. But the wave sounds pretty much the same.

spectrum2 = zero_angle(spectrum)
plot_three(spectrum2)
_images/fccf9d465e2b365284fd55d9fc61bd282a97545a22f5f548b8fbeaf5222182da.png

You might notice that the volume is lower, but that’s because of the way the wave gets normalized; that’s not because of the changes in the phase structure.

If we multiply the complex components by \(\exp(i\phi)\), it has the effect of adding \(\phi\) to the angles:

def rotate_angle(spectrum, offset):
    res = spectrum.copy()
    res.hs *= np.exp(1j * offset)
    return res

We can see the effect in the figure below. Again, the wave form is different, but it sounds pretty much the same.

spectrum3 = rotate_angle(spectrum, 1)
plot_three(spectrum3)
_images/6a181bf101b11ff308c55b2291d4ed4c38ee0efd83310c23de27321e9b73ef10.png

Finally, let’s see what happens if we set the angles to random values.

PI2 = np.pi * 2

def random_angle(spectrum):
    res = spectrum.copy()
    angles = np.random.uniform(0, PI2, len(spectrum))
    res.hs *= np.exp(1j * angles)
    return res

The effect on the waveform is profound, but the perceived sound is the same.

spectrum4 = random_angle(spectrum)
plot_three(spectrum4)
_images/ba278c3eedbad9873d85eb7387f837120fa338dabe17da53956216f349b6fadd.png

Oboe#

Let’s see what happens with more natural sounds. Here’s recording of an oboe.

Hide code cell content

# download oboe.wav
        
download("https://github.com/AllenDowney/ThinkDSP/raw/master/code/120994__thirsk__120-oboe.wav")
Downloaded 120994__thirsk__120-oboe.wav
from thinkdsp import read_wave

wave = read_wave('120994__thirsk__120-oboe.wav')
wave.make_audio()

I’ll select a segment where the pitch is constant.

segment = wave.segment(start=0.05, duration=0.9)

Here’s what the original looks like.

spectrum = segment.make_spectrum()
plot_three(spectrum, thresh=50)
_images/cc51537be8d86a5228a049db1440b60de65df29b4e68a2937f9db24f41c99d42.png

Here it is with all angles set to zero.

spectrum2 = zero_angle(spectrum)
plot_three(spectrum2, thresh=50)
_images/ae10e25caff9f6a042db2852434b81e23d57e00a0e0ee92075bc8ca496609ba3.png

Changing the phase structure seems to create a “ringing” effect, where volume varies over time.

Here it is with the angles rotated by 1 radian.

spectrum3 = rotate_angle(spectrum, 1)
plot_three(spectrum3, thresh=50)
_images/4b119c8fffea5b3b5dc67fd7e132244d02a8def70d0143d81990071de63bb9af.png

Rotating the angles doesn’t seem to cause ringing.

And here it is with randomized angles.

spectrum4 = random_angle(spectrum)
plot_three(spectrum4, thresh=50)
_images/e7e5fbbd594412c6acb03b43275ca1165144eefb53cf564e4449160ec15b5e25.png

Randomizing the angles seems to cause some ringing, and adds a breathy quality to the sound.

Saxophone#

Let’s try the same thing with a segment from a recording of a saxophone.

Hide code cell content

# download saxophone.wav

download("https://github.com/AllenDowney/ThinkDSP/raw/master/code/100475__iluppai__saxophone-weep.wav")
Downloaded 100475__iluppai__saxophone-weep.wav
wave = read_wave('100475__iluppai__saxophone-weep.wav')
wave.make_audio()
segment = wave.segment(start=1.9, duration=0.6)

The original:

spectrum = segment.make_spectrum()
plot_three(spectrum, thresh=50)
_images/ec4cd96ff479133756670c0191d38c7b4920bd6fa455133266a4a89f8ddcd4ca.png

Set angles to 0.

spectrum2 = zero_angle(spectrum)
plot_three(spectrum2, thresh=50)
_images/8d34a8d6f01ef74f01d56abe512318f895b320d67acf2aa85766f348672e92eb.png

Rotate angles by 1 radian.

spectrum3 = rotate_angle(spectrum, 1)
plot_three(spectrum3, thresh=50)
_images/d52eeddaa2c5f554fa98ad48691dd64c992f25ac83b93d9122376cc62abacc67.png

Randomize the angles.

spectrum4 = random_angle(spectrum)
plot_three(spectrum4, thresh=50)
_images/340cbdda0a4779b82ac5238dec8579e22db9c29a3aa9d9b80c86d27a37e36ca3.png

Again, zeroing seems to create ringing, rotating has little effect, and randomizing adds a breathy quality.

One way the saxophone differs from the other sounds is that the fundamental component is not dominant. For sounds like that, I conjecture that the ear uses something like autocorrelation in addition to spectral analysis, and it’s possible that this secondary mode of analysis is more sensitive to phase structure.

If so, the effect should be more profound when the fundamental is missing altogether.

Saxophone with missing fundamental#

Let’s run these steps one more time after filtering out the fundamental.

spectrum.high_pass(600)
spectrum.plot(high=4000)
_images/37586f9227bb4f2a581dc87135cfc08ede3a7af05ee90525a39b902987442600.png
plot_three(spectrum2, thresh=50)
_images/8d34a8d6f01ef74f01d56abe512318f895b320d67acf2aa85766f348672e92eb.png

Zeroing

spectrum2 = zero_angle(spectrum)
plot_three(spectrum2, thresh=50)
_images/a42757928dbab434c00f704d645fc14c91c500bc7051f1135ba068532610db6b.png

Rotating

spectrum3 = rotate_angle(spectrum, 1)
plot_three(spectrum3, thresh=50)
_images/b9815d838010b142f26698f34035596bbf95d117f0e438634bb1596806808b68.png

Randomizing

spectrum4 = random_angle(spectrum)
plot_three(spectrum4, thresh=50)
_images/71f5f3f205e40fa8bce903db8f87c1e29564cb9fc6583c5367f2a7a45cddb12b.png

In summary:

  1. At least for sounds that have simple harmonic structure, it seems like we are mostly “phase blind”; that is, we don’t hear changes in the phase structure, provided that the harmonic structure is unchanged.

  2. A possible exception is sounds with low amplitude at the fundamental frequency. In that case we might use something autocorrelation-like to perceive pitch, and there are hints that this analysis might be more sensitive to the phase structure.

Think DSP: Digital Signal Processing in Python, 2nd Edition

Copyright 2024 Allen B. Downey

License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International