Tuesday, 23 September 2014

Dynamic Envelope Tweaking Using Note Context

Some time ago I started to play with dynamically altering the envelope settings for a note based on its length. This is not just a matter of shortening the decay or sustain; no I mean changing the whole shape a feel of the envelope based on the note length. However, when rendering Bach's organ music, this has prove insufficient. Where there are trills and very short notes mixed in with longer notes the envelope for the trill nots should be, i.e. sounds much better if it is, more abrupt than for a short note is isolation. The abrupt envelope (very short attack and decay) means the note has more energy. This must be further compensated for by reducing its volume (velocity in MIDI talk).

I have made some progress with this technique. I spit the renderer into two stages. The first reads the MIDI file and the second plays notes. The player (see below called sing) does not 'see' the previous or next note but it is passed a hint from the MIDI reader. 

Hits come as a two letter code. If the previous note is very close to the current one and was short and next to (very little inter-note delay) to the current one then the first letter is T; otherwise it is S for start or N for normal. Similarly, if the current note is short and next to the next note the second letter is T; again N for the second letter is normal and E is for end.

def sing(hint,pitch,lengthIn,beat,v,vl,vr,voice,vCorrect_):
    def singInner():
        vCorrect=vCorrect_
        length=lengthIn
        tp=0
        
        # minimum play time
        if length<192:
            length=192
            tp=0  
        elif length<363:
            length+=128
            tp=1
        elif length<512:
            length+=256
            tp=2
        elif length<1024:
            length+=128
            tp=3
        else:
            tp=4
    
        sig=[]
        if pitch<330:
            x=5
        elif pitch<880:
            x=6
        else:
            x=3
        for x in range(0,x):
            vc=voice(length,pitch*(1.0+random.random()*0.005))
            vc=sf.Multiply(
                safeEnv(
                    vc,
                    [
                        (0,0),
                        (24,1),
                        (sf.Length(+vc)-24,1),
                        (sf.Length(+vc),0)
                    ]
                ),
                vc
            )
            sig.append(
                sf.NumericVolume(
                    sf.Concatenate(
                        sf.Silence(24*random.random()),
                        vc
                    )
                    ,random.random()+0.25
                )
             )
        sig=sf.Realise(sf.Mix(sig))
        
        sig = sf.FixSize(sig)
        length=sf.Length(+sig)
        
        pHint=hint[0]
        nHint=hint[1]
        shine=False
        if tp==0:
            if pHint=="T":
                q=32
            else:
                q=64
            if nHint=="T":
                p=32
            else:
                p=64
            env=safeEnv(sig,[(0,0),(q,1),(192-p,0.5),(length,0)])
            if hint=="TT":
                vCorrect*=0.8
            elif hint=="NN" and pitch>660:
                shine=True
                vCorrect*=0.5        
        elif tp==1:
            if pHint=="T":
                q=48
            else:
                q=96
            if nHint=="T":
                p=64
            else:
                p=128
            env=safeEnv(sig,[(0,0),(q,0.75),(length-p,1.0),(length,0)])
            if hint=="TT":
                vCorrect*=0.8            
            if hint=="TT":
                vCorrect*=0.8
            elif hint=="NN" and pitch>880:
                shine=True
                vCorrect*=0.6
        elif tp==2:
            env=safeEnv(sig,[(0,0),(96,0.75),(length-256,1.0),(length,0)])
        elif tp==3:
            if length<1280:
                env=safeEnv(sig,[(0,0),(64,0.5),(256,1),(512,0.75),((length-512)/2.0+512,0.5),(length,0)])
            else:
                env=safeEnv(sig,[(0,0),(64,0.5),(256,1),(512,0.75),(length-512,0.75),(length,0)])
        else:
            env=safeEnv(sig,[(0,0),(64,0.25),(512,1),(length/2,0.75),(length,0)])

        mod=sf.NumericShape((0,0.995),(length,1.005))
        mod=sf.Mix(mod,sf.NumericVolume(+env,0.01))
        sig=sf.FrequencyModulate(sig,mod)  
        sig=sf.FixSize(sig)
     
        if pitch<256:
            if pitch < 128:
                sig=sf.Mix(
                    granularReverb(+sig,ratio=0.501 ,delay=256,density=32,length=256,stretch=1,vol=0.20),
                    granularReverb(+sig,ratio=0.2495,delay=256,density=32,length=256,stretch=1,vol=0.10),
                    sig
                )
            elif pitch < 192:
                sig=sf.Mix(
                    granularReverb(+sig,ratio=0.501,delay=256,density=32,length=256,stretch=1,vol=0.25),
                    sig
                )
            else:
                sig=sf.Mix(
                    granularReverb(+sig,ratio=0.501,delay=256,density=32,length=256,stretch=1,vol=0.15),
                    sig
                )
            sig=sf.BesselLowPass(sig,pitch*8.0,2)
        if pitch<392:
            sig=sf.BesselLowPass(sig,pitch*6.0,2)
        elif pitch<512:
            sig=sf.Mix(
                sf.BesselLowPass(+sig,pitch*6.0, 2),
                sf.BesselLowPass( sig,pitch*3.0, 2)
            )                
        elif pitch<640:
            sig=sf.BesselLowPass(sig,pitch*3.5, 2)
        elif pitch<1280:
            sig=sf.Mix(
                sf.BesselLowPass(+sig,pitch*3.5, 2),
                sf.BesselLowPass( sig,pitch*5.0, 2)
            )                
        else:
            sig=sf.Mix(
                sf.BesselLowPass(+sig,pitch*5, 2),
                sf.BesselLowPass( sig,5000,    1)
            )

        sig=sf.Multiply(sig,env)                     
        sig=sf.FixSize(sig)
        
        cnv=sf.WhiteNoise(10240)
        cnv=sf.ButterworthHighPass(cnv,32,4)
        if shine:
            q=640
            print "Shine"
        else:
            q=256
        cnv=sf.Cut(5000,5000+q,cnv)
        cnv=sf.Multiply(cnv,sf.NumericShape((0,0),(32,1),(q,0)))
        sigr=reverberate(+sig,cnv)
        sigr=sf.Multiply(
            safeEnv(sigr,[(0,0),(256,1),(sf.Length(+sigr),1.5)]),
            sigr
        )
        sig=sf.Mix(
            sf.Pcnt20(sigr),
            sf.Pcnt80(sig)
        )
        
        note=sf.NumericVolume(sf.FixSize(sig),v)
        notel=sf.Realise(sf.NumericVolume(+note,vl*vCorrect))
        noter=sf.Swap(sf.NumericVolume( note,vr*vCorrect))
        return (notel,noter)
    return sf_do(singInner)

I have highlighted the parts of the code which use the hint information. Let's first look at the envelope generators: 

        length=sf.Length(+sig)
        
        pHint=hint[0]
        nHint=hint[1]
        shine=False
        if tp==0:
            if pHint=="T":
                q=32
            else:
                q=64
            if nHint=="T":
                p=32
            else:
                p=64
            env=safeEnv(sig,[(0,0),(q,1),(192-p,0.5),(length,0)])
            if hint=="TT":
                vCorrect*=0.8
            elif hint=="NN" and pitch>660:
                shine=True
                vCorrect*=0.5        
        elif tp==1:
            if pHint=="T":
                q=48
            else:
                q=96
            if nHint=="T":
                p=64
            else:
                p=128
            env=safeEnv(sig,[(0,0),(q,0.75),(length-p,1.0),(length,0)])
            if hint=="TT":
                vCorrect*=0.8            
            if hint=="TT":
                vCorrect*=0.8
            elif hint=="NN" and pitch>880:
                shine=True
                vCorrect*=0.6
        elif tp==2:
            env=safeEnv(sig,[(0,0),(96,0.75),(length-256,1.0),(length,0)])
        elif tp==3:
            if length<1280:
                env=safeEnv(sig,[(0,0),(64,0.5),(256,1),(512,0.75),((length-512)/2.0+512,0.5),(length,0)])
            else:
                env=safeEnv(sig,[(0,0),(64,0.5),(256,1),(512,0.75),(length-512,0.75),(length,0)])
        else:
            env=safeEnv(sig,[(0,0),(64,0.25),(512,1),(length/2,0.75),(length,0)])

Here we can see how (for example) the attack for a very short note is 32 milliseconds for a note with a T initial hint. Similarly the release is also 32 for a very short note with a T second hint. Both of these are 64 milliseconds in the N,S or E hint state. Further, a TT hint cause a reduction in the correction volume for the note.  For slightly longer notes I have made the effect asymmetrical between attack and release with the attack times at 48 and 96 milliseconds and the release 64 and 128. Longer notes do not take notice of the hints.

So what is 'shine'? This is set for high pitched short notes which have an NN hint? This is to help control and smooth out some notes which stand out too much and sound harsh. When shine is set to true code later on does something strange with it.

        cnv=sf.WhiteNoise(10240)
        cnv=sf.ButterworthHighPass(cnv,32,4)
        if shine:
            q=640
            print "Shine"
        else:
            q=256
        cnv=sf.Cut(5000,5000+q,cnv)
        cnv=sf.Multiply(cnv,sf.NumericShape((0,0),(32,1),(q,0)))
        sigr=reverberate(+sig,cnv)
        sigr=sf.Multiply(
            safeEnv(sigr,[(0,0),(256,1),(sf.Length(+sigr),1.5)]),
            sigr
        )
        sig=sf.Mix(
            sf.Pcnt20(sigr),
            sf.Pcnt80(sig)
        )

This code 'smudges' the notes out using convolution with white noise. If we consider a real world instrument, especially a pipe organ, the sound does not come from one place but the whole pipe; once the sound as escaped the pipe it will reflect of the pipes around it. These reflections form a very dense, localised, short reverberation. The 'situatedness' of the pipes takes a lot of the potential harshness of the sounds from them (think of a penny whistle or recorder being played very loud). The convolution with filtered (spot the Butterworth filter) helps reproduce this effect in the synthetic sound. It cannot be placed in the sound generators, it must come after the application of the envelope to sound authentic.

Anyhow, when shine is set to true the amount of this effect is increased substantially. This makes single, high notes radiate out from themselves rather than being a harsh 'peep'. It is not realistic in any way but it really helps with the sound.

No comments:

Post a Comment