Some Simple Examples of Modeling Inharmonic Metals

Chris Bailey, your loving TA



This week I spoke about modelling the sounds of metals, specifically, bicycle spokes and gongs. We began with gongs. I recalled that about year ago, I wanted to get a HUGE GONG SMASH sound in one of my pieces. I thought about it for a while: gongs are usually quite inharmonic, (that is, the partials don't seem to be systematically related, by ratio, et al). Also, the durations of the partials don't seem to be related either: you always hear a few random partials "hanging on" at the end of a gong sound's decay. So, I thought that perhaps synthesizing a sound with whole bunch of randomly pitched sine-wave partials, all attacking at once, decaying for random durations, and with random amplitudes; might just give me the result I was looking for.

Hence, my first CMIX gong score was born:

        /*basic set-up stuff*/
rtsetparams(44100, 2)
rtoutput("gong0.aiff")

        /*use sine-wave as the basic building block
        * for partials  */
makegen(1, 10, 10000, 1)


        /*the envelope is basically a sharp attack, 
         *followed by quick drop, longer, slightly 
         *decaying steady-state, and then a drop to 0*/
makegen(2, 24, 10000, 0,1, 1,.5, 8,.4, 10,0)


                 /*there will be 30 partials */
for (partials=0; partials < 30; partials=partials+1)
        {
                 /*they all begin within .02" of the beginning*/
        start=random()*.02
                 /*pitch varies from 30 to 3000 */
        pitch=30+(random()*2970)
                 /*duration varies from 5 to 15 seconds */
        dur=5+(random()*10)
                 /*  amplitude, from 150 to 250---I keep it 
                 * pretty soft to make sure that all 30
                 * partials added together will never exceed 32768
                 * max-amp
        amp=(150+(random()*100))
                 /* stereo is just anywhere 'twixt left and right */
        stereo=random()
                 /* write the partial for this loop-pass */
        WAVETABLE(start, dur, amp, pitch, stereo)
        }




Well, the result of that is not bad, but to get a more gong-like sound, I'm going to try something a little tricky: scaling amplitude and duration to be inversely proportional to pitch. That is, I want the lower partials (harmonics) of the sound to be the strongest and longest, and I want the upper ones to be softer and to die off quickly-er. That way, hopefully, we'll get a more boomy oomph to our gong sound.


What I'm going to do to achieve this is to take the equations for amp and dur that we used earlier, and multiply them by a scaling factor which I'll call pitchfactor. pitchfactor will vary from 1 to 0, depending on how far above 150 Hz the frequency of the partial is. Thus, I allow partials below 150 Hz to be as long as they want; but if a partial is above 150Hz, it's going to be shorter and softer the closer it is to the upper limit of 3000Hz.

the following diagram illustrates:

We want to make an equation out of this. Given a pitch (of an individual partial), we want to find pitchfactor. So, from our diagram, we see that the "distance-from-the-top" is directly proportional to pitchfactor. As "distance-from-the-top" goes up (the actual pitch is going down), pitchfactor gets larger as well. So, let us say pitchfactor=dist_from_top/range_of_pitches. Why? Because then if we're at the top, dist_from_top will be 0, and 0/range_of_pitches=0. So if our partial lies at the very top of the pitch range, its amp and dur will be multiplied by, and thereby reduced to, 0. If on the other hand, the pitch of our partial is at the "bottom" of the range, dist_from_top will be equal to the range_of_pitches, and thus dist_from_top/range_of_pitches will equal 1. So pitchfactor will equal 1. And that's what we want. So, continuing to refine our equation, dist_from_top=top-pitch or 3000-pitch. Thus, our final equation for pitchfactor is


                pitchfactor=(3000-pitch)/2850     

In the score, we first assume the pitch is below 150 Hz, and then decide if that's not true, in which case pitchfactor gets re-calculated. Later, amp and dur are multiplied by it to get new values.


                /* assume the pitch is lower than 150 */
        pitchfactor=1
                /* check if it really is */
        if (pitch > 150)
                {
                pitchfactor=(3000-pitch)/2850     
                }         


                /* later on . . . . */      
        dur=(5+(random()*10))*pitchfactor
        amp=(150+(random()*100))*pitchfactor

So that's how we deal with the upper partial issue.


The other change we make in this new version of the gong score, is to add a little bit of random variation to the previously invariant amplitude envelope. I do this by inserting variables for the numbers in the makegen definition. Basically the old values (0,1, 1,.5, 8,.2, 10,0) are still what's more or less going to happen, but we add a bit of random jitter to them:



 
           /*makegen variables*/
        b=.9+(random()*.2)
           /*b is going to vary around 1*/
        c=.4+(random()*.2)
           /*c around .5  */
        d=7.5+random()
           /* d around 8, and so on . . . . */
        e=.1+(random()*.2)
        f=10+random()
        /*later*/

        makegen(2, 24, 10000, 0,1, b,c, d,e, f,0)





So, here's the whole score:




rtsetparams(44100, 2)
rtoutput("gong1.aiff")


makegen(1, 10, 10000, 1)

/* added: makegen variables
*         and factors for pitch height
*/

for (notes=0; notes < 30; notes=notes+1)
        {
             /*makegen variables*/
        b=.9+(random()*.2)
        c=.4+(random()*.2)
        d=7.5+random()
        e=.1+(random()*.2)
        f=10+random()
             /*our usual wavetable variables*/
        start=random()*.02
        pitch=30+(random()*2970)
             /*default pf to 1, check if
              * that's right, re-calculate */ 
        pitchfactor=1
        if (pitch > 150)
                {
                pitchfactor=(3000-pitch)/2850     
                }               
             /* scale amplitude and duration */
        dur=(5+(random()*10))*pitchfactor
        amp=(150+(random()*100))*pitchfactor
        stereo=random()
        makegen(2, 24, 10000, 0,1, b,c, d,e, f,0)
        WAVETABLE(start, dur, amp, pitch, stereo)
        }



Well, this is somewhat of an improvement, but still a little lifeless. What about those famous tremolating partials that you get with tam-tams?




In the next score, we have a decision-variable that we use to decide whether or not a partial will be doubled by another copy of itself, which will be exactly the same, except its frequency will be a few Hz off to produce beating (tremolo.)

Hence:


                /* this variable is used to "decide"  */
        trem=random()
                /* based on whether it's less than
                 *  or greater than  .5  */
        if (trem < .5) WAVETABLE(start, dur, amp, pitch, stereo)  



Also, I add another loop that simply makes 15 additional partials below 150 Hz, to add more "beef" to the sound.

So, here's the whole thing:





rtsetparams(44100, 2)
rtoutput("gong2.aiff")

/* with tremolo
* and, with more lower partials,
* more BEEF!!
*/


makegen(1, 10, 10000, 1)


/* loop for extra lower partials
*   Note that there's no pitchfactor stuff
*  here, as these partials vary only from
* 20-120 Hz, and thus are too low to involve
* pitchfactor */

for (notes=0; notes < 15; notes=notes+1)
        {
                /*makegen variables*/
        b=.9+(random()*.2)
        c=.4+(random()*.2)
        d=7.5+random()
        e=.1+(random()*.2)
        f=10+random()
                /*wavetable variables*/
        start=random()*.02
        pitch=20+(random()*100)
        dur=(5+(random()*10))
        amp=(150+(random()*100))
        stereo=random()
        makegen(2, 24, 10000, 0,1, b,c, d,e, f,0)
        WAVETABLE(start, dur, amp, pitch, stereo)
              /*below, is where a "tremolo" is created.
              * Note: 50% of the time; i.e. "if (trem<.5)"   */
        trem=random()
        pitch=pitch+(.5+(random()*5))
        if (trem < .5) WAVETABLE(start, dur, amp, pitch, stereo)  
        }



/* loop for upper partials
*  same as last score, but with
*  the tremolo thang added*/

for (notes=0; notes < 30; notes=notes+1)
        {
                /*makegen variables*/
        b=.9+(random()*.2)
        c=.4+(random()*.2)
        d=7.5+random()
        e=.1+(random()*.2)
        f=10+random()
                /*wavetable variables*/
        start=random()*.02
        pitch=150+(random()*2850)
        pitchfactor=(3000-pitch)/2850     
        dur=(5+(random()*10))*pitchfactor
        amp=(150+(random()*100))*pitchfactor
        stereo=random()
        makegen(2, 24, 10000, 0,1, b,c, d,e, f,0)
        WAVETABLE(start, dur, amp, pitch, stereo)
                /* tremolo stuff */
        trem=random()
        if (trem < .5) WAVETABLE(start, dur, amp, pitch+(.5+(random()*5)), stereo)        
        }



the result of this sounds pretty cool, if I don't say so myself. . . .

So, now onto modelling the sound of twigs jouncing softly against bicycle spokes . . . .



We begin by creating a single spoke. We treat it simply as a high gong, with all partials very short.

Hence the first score:




rtsetparams(44100, 2)
reset(44100)
rtoutput("pingk.aiff")


srand(29348723)
makegen(1, 10, 10000, 1)

start=0
dur=0
pitch=400
stereo=.5

for (x=0; x < 100; x=x+1)
        {
        start=random()*.02
        dur=random()*.2+.05
           /* this sound is gonna be soft, like
            * any self-respecting bike-spoke  */
        amp=random()*100+50
           /* note the pitch range is quite high  */
        pitch=random()*4000+1800
           /* choose 'tween two slightly different
            * envelopes.  */
        chooseenv=random()
           if (chooseenv < .5)
                {
                a=random()*3+2
                b=random()*.5+.3
                c=a+random()*3+2
                d=b/(random()*1+2)
                e=c+random()*3+2
                makegen(2, 24, 10000, 0,1, a,b, c,d, e,0)
                }
           if (chooseenv > .5)
                {
                a=random()*2+1
                b=a+random()*4+5
                c=random()*.5+.3
                d=b+random()*3+2
                makegen(2, 24, 10000, 0,0, a,1, b,c, d,0)
                }
        WAVETABLE(start, dur, amp, pitch, stereo)
        }

The result of this is nice, but perhaps a bit too "noisy." We want a sound that's a bit more "pitchy." Also, let's start turning the wheel 'round---i.e. generating more than one spoke-hit.


So, what we do in the next score is twofold:

  1. to make a stream of spoke-hits, we simply build an outer loop around the inner one. Things are a bit tricky with start-time, as we still want that .02 second deviation of when the different partials start. Thus, we make a variable emperor-start which controls the start-time for a given spoke hit. Then, we calculate each partial's individual start with a .02 second range of deviation from emperor-start.
    Hence:

    
    
    
                    /* the outer loop: 10 spoke-hits */
    for (i=0; i < 10; i=i+1)
            {
                    /* the inner loop: 10 partials per hit */
            for (x=0; x < 10; x=x+1)
                    {
                          /* emperor-start controls when all of the
                          * partials of a given spoke-hit happen,
                          * an individual partial's attack-time
                          * may vary slightly, however.  */
                    start=emperor_start+random()*.02
     
                            /*  lots of commands and stuff */
                    }
                            /* update emperor_start before next spoke-hit */
            emperor_start=emperor_start+.2
            }
    
    
    
    
    

  2. As for making the sound less "noisy," I first opt for a simple solution, which is to simply lower the number of partials to 10.

So, here it is:




rtsetparams(44100, 2)
reset(44100)
rtoutput("pingk2.aiff")


srand(29723)
makegen(1, 10, 10000, 1)

emperor_start=0
start=0
dur=0
pitch=400
stereo=0.5



    /* 10 spoke-hits  */
for (i=0; i < 10; i=i+1)
        {
            /*fewer partials*/ 
        for (x=0; x < 10; x=x+1)
                {
                      /* emperor-start controls when all of the
                      * partials of a given spoke-hit happen,
                      * an individual partial's attack-time
                      * may vary slightly, however.  */
                start=emperor_start+random()*.02
                dur=random()*.2+.05
                amp=random()*100+50
                pitch=random()*4000+2500
                a=random()*3+2
                b=random()*.5+.3
                c=a+random()*3+2
                d=b/(random()*1+2)
                e=c+random()*3+2
                makegen(2, 24, 10000, 0,1, a,b, c,d, e,0)
                WAVETABLE(start, dur, amp, pitch, stereo)
                }
        emperor_start=emperor_start+.2
        }


This one definitely sounds more "pitchy." Perhaps a bit too high though.


The solution to that is simple enough, just lower the pitch range.


rtsetparams(44100, 2)
reset(44100)
rtoutput("pingk3.aiff")


srand(29723)
makegen(1, 10, 10000, 1)

emperor_start=0
start=0
dur=0
pitch=400
stereo=0.5


/*lowered the pitch range*/

for (i=0; i < 10; i=i+1)
        {
        for (x=0; x < 10; x=x+1)
                {
                start=emperor_start+random()*.02
                dur=random()*.2+.05
                amp=random()*100+50
                pitch=random()*3000+500
                a=random()*3+2
                b=random()*.5+.3
                c=a+random()*3+2
                d=b/(random()*1+2)
                e=c+random()*3+2
                makegen(2, 24, 10000, 0,1, a,b, c,d, e,0)
                WAVETABLE(start, dur, amp, pitch, stereo)
                }
        emperor_start=emperor_start+.2
        }


Aaah, yes, that sounds much better.


Finally, just for experimentation's sake, I decide to try a different method of getting that slightly "pitchy" sound that I'm after. I've seen "gaussian distribution" on the makegen page, and I know that means I've got a makegen that writes a bunch of points, mostly around a "central value" (0.5 in the case of Gaussian distribution). If I can get those points to represent pitch values, then using those frequencies as my spoke-partials might then give me the sound I'm looking for.

So, each time a "spoke-hit" is written, we generate a new Gaussian distribution of its partials. This kind of distribution ranges from 0 to 1, "centering" around .5. So .5 is going to "equal" our central pitch, which I'm going to call "dukepitch." The mapping of makegen points onto pitch values is best illustrated by the following diagram:

To find an individual partial's pitch, we need to know first the range of pitches available. I decide, for no particular reason, to have the pitches vary from dukepitch to +/- 45% of dukepitch. Hence the rangebase will be (dukepitch-(dukepitch*.45)) Then we map the values obtained from our Gaussian makegen via a sampfunc call onto pitch values with the following equation:


        pitch=rangebase+(sampfunc(10, x)*range)

So, here's the whole score for the "gaussian" bicycle spokes:



rtsetparams(44100, 2)
reset(44100)
rtoutput("pingk4.aiff")


srand(286723)
makegen(1, 10, 10000, 1)

emperor_start=0
start=0
dur=0
pitch=400
stereo=0.5


/* employ gaussian distribution around a "center pitch"*/
/*  more partials, higher amplitude */


for (i=0; i < 15; i=i+1)
        {
               /* for each spoke-hit, generate a
                * a new Gaussian makegen.  Note
                * 15 points corresponds to 15 partials.
                * Also, generate a new dukepitch.  */
        makegen(10, 20, 15, 4)
        dukepitch=random()*1500+900
                     /*now, the loop for a given spoke-hit.
                      * 15 partials for each  */
        for (x=0; x < 15; x=x+1)
                {
                rangebase=(dukepitch-(dukepitch*.45))
                range=(dukepitch*.45)*2
                pitch=rangebase+(sampfunc(10, x)*range)
                start=emperor_start+random()*.02
                dur=random()*.2+.05
                amp=random()*200+150
                a=random()*3+2
                b=random()*.5+.3
                c=a+random()*3+2
                d=b/(random()*1+2)
                e=c+random()*3+2
                makegen(2, 24, 10000, 0,1, a,b, c,d, e,0)
                WAVETABLE(start, dur, amp, pitch, stereo)
                }
        emperor_start=emperor_start+.2
        }


This wasn't necessarily a better result than I was getting before, but it's kind of interesting.




Boy, I love horizontal rules

possible assignments: