How low does the organ go?
The pipe organ in Hans Zimmer’s Interstellar soundtrack goes deep. Really deep. But how deep exactly? I wanted a number and not just a feeling in my stomach, so I wrote a small Python script to find the lowest frequency in a recording that still carries a relevant level.
Getting the audio
I recorded about a minute of the stream on my Mac with Audacity via BlackHole, a virtual audio device that loops the system output back as an input. Then export it uncompressed:
File -> Export -> Export as WAV (16 or 24 bit)
The script
The whole thing is one file and three small functions: load the WAV and mix it down to mono, compute the spectrum, find the lowest frequency that is still “relevant”.
For the spectrum I did not use a single big FFT but Welch’s method. It cuts the recording into overlapping segments, computes an FFT for each one and averages them. That is much more robust against a single loud moment:
from scipy.signal import welch
frequencies, power = welch(
data,
fs=samplerate,
nperseg=8192,
window="hann",
scaling="spectrum",
)
level_db = 10 * np.log10(power + 1e-12)
“Relevant” needs a definition. I went for: everything that is not more than 20 dB below the loudest peak in the spectrum. Everything below 10 Hz is ignored, that is DC offset and other stuff nobody can hear anyway:
peak_level = np.max(level_db)
limit = peak_level + threshold_db # threshold_db is negative, e.g. -20
valid = frequencies >= 10
above_threshold = valid & (level_db >= limit)
lowest_freq = frequencies[above_threshold][0]
The first result
Peak level in spectrum: 65.3 dB
--> Lowest relevant frequency: 10.8 Hz
10.8 Hz? From a pipe organ? That would be nearly an octave below the lowest pipe of a really big organ. And it sits suspiciously close to my own 10 Hz cutoff. The plot did not help either, it showed a nice smooth rolloff and no obvious spike. Looked plausible. But it wasn’t…
The culprit: frequency resolution
The resolution of the spectrum is simply the samplerate divided by the segment size:
44100 Hz / 8192 = 5.38 Hz per bin
So every bin is about 5.4 Hz wide. Up at 1 kHz nobody cares. Down at 10 to 20 Hz that is a disaster: there are only two or three bins for the whole region, and the one near 10 Hz happily collects the leakage of the real and strong signal just above 20 Hz. The energy is there, just not where the script says.
Easy to check: run the same file again with bigger segments and see if the result stays where it is.
nperseg |
resolution | lowest relevant frequency |
|---|---|---|
| 8192 | 5.38 Hz | 10.77 Hz |
| 32768 | 1.35 Hz | 20.19 Hz |
| 65536 | 0.67 Hz | 20.86 Hz |
| 131072 | 0.34 Hz | 21.53 Hz |
| 262144 | 0.17 Hz | 21.70 Hz |
It does not stay. It walks upwards and settles somewhere around 21.5 Hz. With the fine resolution you can also see what is really going on down there: narrow peaks at about 21.9 Hz, 24.6 Hz and 27.6 Hz. That is pretty much F0, G0 and A0, so real pedal notes and not some rumble smeared down to 10 Hz.
The fix
One number:
nperseg=65536, # FFT window size -> frequency resolution approx. samplerate/65536
That gives 0.67 Hz per bin instead of 5.4 Hz. Why not go even bigger? Because bigger segments mean fewer of them. My recording is 65 seconds long, with 65536 samples per segment there are still more than 80 segments to average over. With 262144 it would be only around 20, and then one loud moment starts to dominate again. 65536 is a good trade-off.
$ python analyse.py Zimmer_interstellar-Analyse.wav --plot spectrum.png
File loaded: Zimmer_interstellar-Analyse.wav
Samplerate: 44100 Hz
Duration: 65.4 seconds
Peak level in spectrum: 64.5 dB
Threshold: -20.0 dB relative to the peak level
--> Lowest relevant frequency: 20.9 Hz
So the answer is: about 21 Hz. Right at the edge of what you can hear, and way below what most speakers can reproduce. No wonder it is more a feeling than a sound.
Lesson learned
If a result sits right next to the width of one FFT bin, do not trust it. Run it again with a much finer resolution and check if it holds or if it drifts. Mine drifted by a whole octave.