Recreating the THX Deep Note

If you’ve ever watched a movie in a movie theater, chances are that you are familiar with the Deep Note, the audio logo of THX. That sound is one of the first sounds we hear at the beginning of movie trailers in a THX-certified venue. I’ve always been fascinated with that great distinctive crescendo, starting from an eerie cluster of tones and ending with a full range bright and grand finale. What an ear treat!

Yesterday, (probably) out of nowhere, the origins of that sound tickled my curiosity and I did some little research. I’m deeply moved by the history behind it, and I want to share what I’ve learned with you. Then we will move on to create that sound ourselves, get your scissors ready, and some glue!

The best source of information I could find about the sound, which I think is a complete electro-acoustic composition, is from the great Music Thing Blog. It is from a blog post from 2005. The link to the post is here.

So here is some trivia:

“I like to say that the THX sound is the most widely-recognized piece of computer-generated music in the world. This may or may not be true, but it sounds cool!”

http://www.uspto.gov/go/kids/soundex/74309951.mp3

Be sure to listen the sound because we will be referring to that particular recording when we have a go at recreating the Deep Note. You may also listen other instances of this piece: http://www.thx.com/cinema/trailers.html

Here is some technical/theoretical trivia before we start synthesizing:

So let’s get going. SuperCollider is my tool of choice here. I start with a simple waveform. I want to use a sawtooth wave as the oscillator source, it has a rich and harmonic spectrum consisting of even and odd partials. I’ll want to filter the upper partials later on. Here is some beginning code:

//30 oscillators together, distributed across the stereo field
(
{
    var numVoices = 30;
    //generating initial random fundamentals:
    var fundamentals = {rrand(200.0, 400.0)}!numVoices;
    Mix
    ({|numTone|
        var freq = fundamentals[numTone];
        Pan2.ar
        (
            Saw.ar(freq),
            rrand(-0.5, 0.5), //stereo placement of voices
            numVoices.reciprocal //scale the amplitude of each voice
        )
    }!numVoices);
}.play;
)

I chose to have 30 oscillators for sound generation, congruent with the capabilities of the ASP computer as reported by Dr. Moorer. I’ve created an array of 30 random frequencies between 200Hz and 400Hz, distributed them randomly across the stereo field with Pan2.ar and with the argument rrand(-0.5, 0.5), assigned the freqs to the sawtooth oscillators (30 instances). Here is how it sounds:

Now if we examine the info provided by Dr. Moorer, and/or listen closely to the original piece, we can hear that the pitches of the oscillators drift up and down randomly. We want to add this for a more organic feel. The frequency scale is logarithmic, so lower frequencies should have narrower wobbling ranges than higher frequencies. We can implement it by sorting our randomly generated frequency values, and assigning LFNoise2 (which generates quadratically interpolated random values) mul arguments in order inside our Mix macro. And I also added a lowpass filter for the oscillators whose cutoff frequencies are 5 * freq of oscilator with moderate 1/q:

//adding random wobbling to freqs, sorting randoms, lowpassing
(
{
    var numVoices = 30;
    //sorting to get high freqs at top
    var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
	Mix
    ({|numTone|
		//fundamentals are sorted, so higher frequencies drift more.
        var freq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
        Pan2.ar
        (
            BLowPass.ar(Saw.ar(freq), freq * 5, 0.5),
            rrand(-0.5, 0.5),
            numVoices.reciprocal
        )
    }!numVoices);
}.play;
)

Here is how it sounds with the latest tweaks:

This sounds like a good starting point, so let’s start implementing our sweep, initially in a very crude way. To implement the sweep, we first need to define our final landing pitches for each of the oscillator. This is not very straightforward, but not very hard either. The fundamental tone should be the pitch that is right in between low D and Eb, so the midi pitch for that tone would be 14.5 (0 is C, count up chromatically, I’m skipping the first octave). So we need to map our freq arguments for 30 oscillators from random frequencies between 200Hz and 400Hz to 14.5 and to its octaves. By ear, I’ve chosen to use the first 6 octaves. So our final array of destination frequencies will be:

(numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;

We’ll be using a sweep that goes from 0 to 1. The random frequencies will be multiplied by (1 – sweep), and the destination frequencies will be multiplied by sweep itself. So when sweep is 0 (beginning) freq will be the random one, when it is 0.5, it will be ((random + destination) / 2), and when it is 1, the freq will be our destination value. Here is our modified code:

//creating the initial sweep (crude), creating final pitches
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var sweepEnv = EnvGen.kr(Env([0, 1], [13]));
Mix
({|numTone|
	var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
	var destinationFreq = finalPitches[numTone];
	var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
	Pan2.ar
	(
		BLowPass.ar(Saw.ar(freq), freq * 5, 0.5),
		rrand(-0.5, 0.5),
		numVoices.reciprocal //scale the amplitude of each voice
	)
}!numVoices);
}.play;
)

Here is the sound:

As I said earlier, this is a very crude sweep. It goes linearly from 0 to 1, which is not congruent with the original composition. Also you should have noticed that the final octaves sounds AWFUL because they are tuned to perfect octaves, and fuse into each other, having fundamental-overtone relationships between them. We will fix this by adding random wobbling to the final pitches, just as we did with the initial random pitches, and it will sound much much more organic.

So we should fix the frequency sweep envelope first. The earlier envelope was just for trying the formulas (and the final landing) out. If we observe the original piece, we can see that there is very little change in organization for the first 5-6 seconds. After that there is a fast and exponential sweep that lands the oscillators to the final octave spaced destinations. Here is the envelope I’ve chosen:

sweepEnv = EnvGen.kr(Env([0, 0.1, 1], [5, 8], [2, 5]));

It takes 5 seconds to go from 0 to 0.1, and 8 seconds to go from 0.1 to 1. The curvatures for the segments are 2 and 5. We’ll see how that worked out, but we also need to fix the final sound spacings. Just as we did with the random frequencies, we will add random wobbles with LFNoise2 whose range will be proportional to the final frequency of the oscillator. This will make the finale sound much more organic. Here is the modified code:

//tweaking the envelope, detuning the final chord
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var sweepEnv = EnvGen.kr(Env([0, 0.1, 1], [5, 8], [2, 5]));
Mix
({|numTone|
	var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
	var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 4));
	var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
	Pan2.ar
	(
		BLowPass.ar(Saw.ar(freq), freq * 8, 0.5),
		rrand(-0.5, 0.5),
		numVoices.reciprocal
	)
}!numVoices);
}.play;
)

Here, I’ve also tweaked the cutoff frequency of the lowpass filter to my liking. I like tweaking stuff, until it alienates me from what I’ve been working on… Anyway. Here is the resulting sound:

I’m not really happy with this envelope either. It needs a longer initialization and faster finish. Or wait… Do I have to have the same envelope for every oscillator? Absolutely not! Each oscillator should have its own envelope with slightly different time and curve values, and I bet it will be more interesting. And the high frequency overtones of the random sawtooth cluster is a bit annoying, so I’m adding a lowpass to the sum, whose cutoff is controlled by a global “outer” envelope that has nothing to do with the envelopes of the oscillators. Here is the modified code:

//custom envelopes. lowpass at end
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var outerEnv = EnvGen.kr(Env([0, 0.1, 1], [8, 4], [2, 4]));
var snd = Mix
({|numTone|
	var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 3 * (numTone + 1));
	var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 4));
	var sweepEnv =
		EnvGen.kr(
			Env([0, rrand(0.1, 0.2), 1], [rrand(5.0, 6), rrand(8.0, 9)],
				[rrand(2.0, 3.0), rrand(4.0, 5.0)]));
	var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
	Pan2.ar
	(
		BLowPass.ar(Saw.ar(freq), freq * 8, 0.5),
		rrand(-0.5, 0.5),
		numVoices.reciprocal
	)
}!numVoices);
BLowPass.ar(snd, 2000 + (outerEnv * 18000), 0.5);
}.play;
)

The slightly out of phase envelopes rendered the sweep slightly more interesting. Lowpass at 2000Hz at the beginning helps to tame the initial cluster. Here is what it sounds like:

I have one more thing that will make the process sound more interesting. You remember we’ve sorted the random oscillators at the beginning right? Well we can now reverse-sort them and make sure oscillators running in higher random frequencies will end up in bottom voices after the crescendo and vice versa. This will add more “movement” to the crescendo and is quite congruent with the way the original piece is structured. I’m not sure if Dr. Moorer programmed it specifically in this way, but at least, the chosen recording demonstrates this process and it sounds cool, be it a random product of the generative process itself, or a compositional choice (oh, did I say that? If the process covers it, it IS a choice… or is it?). So I’ll reverse the sorted values and the way we structured our code will make sure that the higher pitched sawtooths will end up in the lower voices in the finale and vice versa.

Another thing: We need a louder bass. In the way it is now, all voices have equal amplitude. I want to have the lower voices to have slightly higher amplitude and decay proportionally as the frequency goes up. So I’ll change the mul argument of Pan2 to take this into account. I’ll re-tweak the cutoff frequencies of the lowpass filters governing the individual oscillators. And I am going to add a global amplitude scaling envelope that will fade the piece in, and fade out when the piece ends, and free the synth from scserver. Also some more numeric tweaks here and there, here is our final code:

//inverting init sort, louder bass, final volume envelope, some little tweaks
(
{
var numVoices = 30;
var fundamentals = ({rrand(200.0, 400.0)}!numVoices).sort.reverse;
var finalPitches = (numVoices.collect({|nv| (nv/(numVoices/6)).round * 12; }) + 14.5).midicps;
var outerEnv = EnvGen.kr(Env([0, 0.1, 1], [8, 4], [2, 4]));
var ampEnvelope = EnvGen.kr(Env([0, 1, 1, 0], [3, 21, 3], [2, 0, -4]), doneAction: 2);
var snd = Mix
({|numTone|
	var initRandomFreq = fundamentals[numTone] + LFNoise2.kr(0.5, 6 * (numVoices - (numTone + 1)));
	var destinationFreq = finalPitches[numTone] + LFNoise2.kr(0.1, (numTone / 3));
	var sweepEnv =
		EnvGen.kr(
			Env([0, rrand(0.1, 0.2), 1], [rrand(5.5, 6), rrand(8.5, 9)],
				[rrand(2.0, 3.0), rrand(4.0, 5.0)]));
	var freq = ((1 - sweepEnv) * initRandomFreq) + (sweepEnv * destinationFreq);
	Pan2.ar
	(
		BLowPass.ar(Saw.ar(freq), freq * 6, 0.6),
		rrand(-0.5, 0.5),
		(1 - (1/(numTone + 1))) * 1.5
	) / numVoices
}!numVoices);
Limiter.ar(BLowPass.ar(snd, 2000 + (outerEnv * 18000), 0.5, (2 + outerEnv) * ampEnvelope));
}.play;
)

And here is the final recording of the piece:

You may want to compare it with this original one:

http://www.uspto.gov/go/kids/soundex/74309951.mp3

So this is my rendition. Of course it can be further tweaked to death, envelopes, frequencies, distribution, everything… Nevertheless, I think mine is a decent attempt at keeping the legacy alive. And I’d love to hear your comments and/or hear your shots at interpreting this piece.

——————–

Oh and here is one more thing I did for fun. You know, I told you about how it took 20000 lines of C code to generate the original piece. I’m pretty sure Dr. Moorer had to create almost everything by hand so that is not very awkward. But you know, we’ve been sctweeting for some time, trying to fill stuff into 140 characters of code. So for the fun of it, I tried to replicate the essential elements of the composition in 140 characters of code. I think it still sounds cool, here is the code (this one uses an F/E fundamental):

play{Mix({|k|k=k+1/2;2/k*Mix({|i|i=i+1;Blip.ar(i*XLine.kr(rand(2e2,4e2),87+LFNoise2.kr(2)*k,15),2,1/(i/a=XLine.kr(0.3,1,9))/9)}!9)}!40)!2*a}

And here is the sound this version generates:

All the code in this page is in this document for you to experiment: - get from here –

Happy sweeping…

This post’s social cover image is derivative work from this CC licensed image by eflon.


Privacy Policy