Zack Scholl

zack.scholl@gmail.com

THX theme in SuperCollider

 / #software 

An epic sound in just a few lines of code.

The THX “Deep Note” was composed by Lucasfilm sound engineer Dr. James Moorer and premiered in 1983 at the debut of “Star Wars: Episode VI.” The original was written in hundreds of lines of C code. However, as a demonstration of SuperCollider, I found it engaging to program in fewer than 100 lines.

the score

The score itself is a remarkably concise piece of music, shown below in its entirity.

THX score

It provides all necessary details for reconstruction:

  1. Use three detuned voices for high notes and two for low notes.
  2. Generate random pitches between 200 and 400 Hz.
  3. Oscillate pitches for approximately 10 seconds.
  4. Approach final pitch over approximately 6 seconds.
  5. Hold final pitches for 24 seconds.
  6. Gradually increase volume until the end.

Let’s try!

the final pitches

Lets start at the end, rather than the beginning to make things easier. The final notes, from the score, are D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6. We can gather all these notes as midi notes - [26, 38, 45, 50, 57, 62, 69, 74, 81, 86, 90] and create a little Routine in SuperCollider that will play a “thx” synth (to be determined). But here we are:

(
s.waitForBoot({  
  Routine {
    // sync the server
    s.sync;
    // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
    ~thxNotes = [26, 38, 45, 50, 57, 62, 69, 74, 81, 86, 90];
    // play all notes
    Synth.tail(s,"out");
    ~thxNotes.do({ arg v;
      if (v<60,{
        // 2 voices
        Synth("thx",[\noteFinal,v]);
        Synth("thx",[\noteFinal,v]);
      },{
        // 3 voices, slightly detuneds
        Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
        Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
        Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
      });
    });
  }.play;
});
)

For notes less than middle C (midi note “60”) there are two voices created, and for the higher notes there are three voices with randomization added to their noteFinal.

the basic synth

Now lets make a basic synth that plays these pitches. For now lets just make a super basic “thx” synth:

(
s.waitForBoot({
   SynthDef("thx",{
      arg noteFinal=72;
      var snd;

      // set random inital note
      var note = noteFinal;

      snd = Saw.ar(note.midicps);
      snd = snd * EnvGen.ar(Env.new([0,1],[1]));
      Out.ar(0,snd*12.neg.dbamp);
   }).add;

   Routine {
      // sync the server
      s.sync;
      // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
      ~thxNotes = [26,38,45,50,57,62,69,74,81,86,90];
      // play all notes
      Synth.tail(s,"out");
      ~thxNotes.do({ arg v;
         if (v<60,{
            // 2 voices
            Synth("thx",[\noteFinal,v]);
            Synth("thx",[\noteFinal,v]);
         },{
            // 3 voices, slightly detuneds
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
         });
      });

   }.play;
});
)

random pitches

To define some initial notes that transition into the final “THX” notes, you can generate random pitches and then smoothly transition them to the final notes. Here’s how you can do it in SuperCollider:

(
s.waitForBoot({
   SynthDef("thx",{
      arg noteFinal=72;
      var snd;
      
      // set random inital note
      var noteInitial = Rand(200,400).cpsmidi;      
      
      // note movement
      var noteMove = EnvGen.ar(Env.new([0,1],[8],curve:\sine));
      
      // setting up the note
      var note = noteInitial;
      // add the movement in the note
      note = note + (noteMove* (noteFinal-noteInitial));
      
      snd = Saw.ar(note.midicps);
      snd = snd * EnvGen.ar(Env.new([0,1],[1]));
      Out.ar(0,snd*12.neg.dbamp);
   }).add;
   
   Routine {
      // sync the server
      s.sync;
      // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
      ~thxNotes = [26,38,45,50,57,62,69,74,81,86,90];
      // play all notes
      Synth.tail(s,"out");
      ~thxNotes.do({ arg v;
         if (v<60,{
            // 2 voices
            Synth("thx",[\noteFinal,v]);
            Synth("thx",[\noteFinal,v]);
         },{
            // 3 voices, slightly detuneds
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
         });
      });
      
   }.play;
});
)

random pitch movement

the score is a little more subtle than this though - as each note seems to be able tow wave around a little bit before the actual movement begins to the final note. lets add this in too. we will introduce noteRandomization, which when it is finished will trigger the noteMove envelope.

(
s.waitForBoot({
   SynthDef("thx",{
      arg noteFinal=72;
      var snd;
      
      // set random inital note
      var noteInitial = Rand(200,400).cpsmidi;      
      
      // note randomization
      var noteRandomization = EnvGen.ar(Env.new([1,1,0],[11,1],curve:\welch));
      
      // note movement
      var noteMove = EnvGen.ar(Env.new([0,1],[8],curve:\sine),gate:noteRandomization<0.01);
      
      // setting up the note
      var note = noteInitial;
      // add the randomization of the note
      note = note + (noteRandomization * LFNoise2.kr(1).range(-1,1));
      // add the movement in the note
      note = note + (noteMove* (noteFinal-noteInitial));
      
      snd = Saw.ar(note.midicps);
      snd = snd * EnvGen.ar(Env.new([0,1],[1]));
      Out.ar(0,snd*12.neg.dbamp);
   }).add;
   
   Routine {
      // sync the server
      s.sync;
      // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
      ~thxNotes = [26,38,45,50,57,62,69,74,81,86,90];
      // play all notes
      Synth.tail(s,"out");
      ~thxNotes.do({ arg v;
         if (v<60,{
            // 2 voices
            Synth("thx",[\noteFinal,v]);
            Synth("thx",[\noteFinal,v]);
         },{
            // 3 voices, slightly detuneds
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
         });
      });
      
   }.play;
});
)

envelopes!

envelopes can control the volume of the sound, and that is the last thing that we are missing. we need an envelope to increase the volume over time, so we can use EnvGen with something like:

snd = snd * EnvGen.ar(Env.new([-36,-16,-4,-4,-96],[8,8,8,2]),doneAction:2,timeScale:timeScale).dbamp;

which will increase and then decrease at the end.

(
s.waitForBoot({
   SynthDef("thx",{
      arg noteFinal=72;
      var snd;
      
      // set random inital note
      var noteInitial = Rand(200,400).cpsmidi;      
      
      // note randomization
      var noteRandomization = EnvGen.ar(Env.new([1,1,0],[11,1],curve:\welch));
      
      // note movement
      var noteMove = EnvGen.ar(Env.new([0,1],[8],curve:\sine),gate:noteRandomization<0.01);
      
      // setting up the note
      var note = noteInitial;
      // add the randomization of the note
      note = note + (noteRandomization * LFNoise2.kr(1).range(-1,1));
      // add the movement in the note
      note = note + (noteMove* (noteFinal-noteInitial));
         
      snd = Saw.ar(note.midicps);
            
      // slowly louder
      snd = snd * EnvGen.ar(Env.new([-36,-16,-4,-4,-96],[8,8,8,2]),doneAction:2).dbamp;
      
      snd = snd * EnvGen.ar(Env.new([0,1],[1]));
      Out.ar(0,snd*12.neg.dbamp);
   }).add;
   
   Routine {
      // sync the server
      s.sync;
      // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
      ~thxNotes = [26,38,45,50,57,62,69,74,81,86,90];
      // play all notes
      Synth.tail(s,"out");
      ~thxNotes.do({ arg v;
         if (v<60,{
            // 2 voices
            Synth("thx",[\noteFinal,v]);
            Synth("thx",[\noteFinal,v]);
         },{
            // 3 voices, slightly detuneds
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
         });
      });
      
   }.play;
});
)

thats about it!

and that’s about it! now its about just adding a few elements to spruce things up a bit. we can add a reverb stage at the end and we can also add some random panning to the final code and get something like this:

(
s.waitForBoot({
   SynthDef("thx",{
      arg noteFinal=72;
      var timeScale = 1;
      var snd;

      // set random inital note
      var noteInitial = Rand(200,400).cpsmidi;

      // note randomization
      var noteRandomization = EnvGen.ar(Env.new([1,1,0],[11,1],curve:\welch),timeScale:timeScale);

      // note movement
      var noteMove = EnvGen.ar(Env.new([0,1],[8],curve:\sine),gate:noteRandomization<0.01,timeScale:timeScale);

      // setting up the note
      var note = noteInitial;
      // add the randomization of the note
      note = note + (noteRandomization * LFNoise2.kr(1).range(-1,1));
      // add the movement in the note
      note = note + (noteMove* (noteFinal-noteInitial));

      // sawtooth oscillator
      snd = Saw.ar(note.midicps);

      // some low-pass filtering
      snd = LPF.ar(snd,Rand(90,130).midicps);

      // random panning
      snd = Pan2.ar(snd,Rand(-0.5,0.5));

      // slowly louder
      snd = snd * EnvGen.ar(Env.new([-36,-16,-4,-4,-96],[8,8,8,2]),doneAction:2,timeScale:timeScale).dbamp;

      snd = snd * EnvGen.ar(Env.new([0,1],[1]));
      Out.ar(0,snd*12.neg.dbamp);
   }).add;


   // output
   SynthDef("out",{
      var snd = In.ar(0,2);
      var sndr;
      var snd2 = snd;
      // reverb
      snd2 = DelayN.ar(snd2, 0.03, 0.03);
      snd2 = DelayN.ar(snd2, 0.15, 0.15);
      snd2 = CombN.ar(snd2, 0.1, {Rand(0.01,0.099)}!32, 4);
      snd2 = SplayAz.ar(2, snd2);
      snd2 = LPF.ar(snd2, LinExp.kr(LFNoise2.kr(1),-1,1,2500,3000));
      5.do{snd2 = AllpassN.ar(snd2, 0.1, {Rand(0.01,0.099)}!2, 3)};
      snd2 = LPF.ar(snd2, LinExp.kr(LFNoise2.kr(1),-1,1,2500,3000));
      snd2 = LeakDC.ar(snd2);
      snd = SelectX.ar(0.6,[snd,snd2]);
      ReplaceOut.ar(0,snd * 6.neg.dbamp);
   }).add;

   Routine {
      // sync the server
      s.sync;
      // https://www.johndcook.com/blog/2018/06/12/mathematics-of-deep-note/
      // D1, D2, A2, D3, A3, D4, A4, D5, A5, D6, and F#6
      ~thxNotes = [26,38,45,50,57,62,69,74,81,86,90];
      // play all notes
      Synth.tail(s,"out");
      ~thxNotes.do({ arg v;
         if (v<60,{
            // 2 voices
            Synth("thx",[\noteFinal,v]);
            Synth("thx",[\noteFinal,v]);
         },{
            // 3 voices, slightly detuneds
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
            Synth("thx",[\noteFinal,v+rrand(-0.1,0.1)]);
         });
      });


   }.play;
});
)

best results if you have a subwoofer :)

tinker / #software 

go and zig      zeptocore