Sunday, 28 September 2014

Reverb's So Real You Cannot Hear It

If I said this myself - then I would be boasting but this is a paraphrase of what someone else said about this double reverb' effect.

What (if I remember correctly) he said was that he was expecting the effect but was confused because he did not hear it. Then he realised it must be there because the music would not sound like that if there was no reverb'; "Yes, it sounds like is it played in a room, but not like a reverb' effect".

My contention is that we have gotten very used to the sound of reverberation effects. We are used to them to such and extent that we expect that artificial sound of early reverberation units and forget that the original idea was to mimic real spaces. Indeed, even impulse response reverberation does not sound real.

How can impulse response reverberation not sound real?

I have a theory as to how this happens. Consider that you are going to make an impulse response reverberation for cathedral. One way of doing this is to set up your equipment in the nave or choir and send out your frequency sweeps. You measure the returned sound and compute the impulse response.

What actually happens when music is created in such a space is very different. The sound is typically made near a wall and other reflective surfaces and then heard by the listener some distance away. My guess is that the upper frequencies of the early reflections are more highlighted this way. Indeed, some early reflections will be sound bright they will interfere with the original signal and make the upper frequencies dance.

What could be more this way than a pipe organ in a church or cathedral? The instrument is always near a wall; what is more that pipes themselves are reflectors of other pipes and the instrument is highly distributed through space. Somehow we must make the sound shine an vibrate (emotive words, not technical) in a way that a simple impulse response cannot.

It try and mimc the effect of listening to a large organ in a large space I have worked on this patch. As Joerg Sprave would say - "let me show you its features"

def excite(sig_,mix,power):
    def exciteInner():
        sig=sig_
        m=sf.Magnitude(+sig)
        sigh=sf.BesselHighPass(+sig,500,2)
        mh=sf.Magnitude(+sigh)
        sigh=sf.Power(sigh,power)
        sigh=sf.BesselHighPass(sigh,1000,2)
        nh=sf.Magnitude(+sigh)
        sigh=sf.NumericVolume(sigh,mh/nh)
        sig=sf.Mix(sf.NumericVolume(sigh,mix),sf.NumericVolume(sig,1.0-mix))
        n=sf.Magnitude(+sig)
        return sf.Realise(sf.NumericVolume(sig,m/n))
    return sf_do(exciteInner)
    
(convoll,convolr)=sf.ReadFile("temp/bh-l.wav")
(convorl,convorr)=sf.ReadFile("temp/bh-r.wav")

convoll=excite(convoll,0.75,2.0)
convolr=excite(convolr,0.75,2.0)
convorl=excite(convorl,0.75,2.0)
convorr=excite(convorr,0.75,2.0)

ll  = reverberate(+left ,convoll)
lr  = reverberate(+left ,convolr)
rl  = reverberate(+right,convorl)
rr  = reverberate(+right,convorr)
wleft =sf.FixSize(sf.Mix(ll,rl))
wright=sf.FixSize(sf.Mix(rr,lr))

wright = excite(wright,0.15,1.11)
wleft  = excite(wleft ,0.15,1.11)

sf.WriteFile32((sf.FixSize(+wleft),sf.FixSize(+wright)),"temp/wet.wav")

wleft =sf.FixSize(sf.Mix(sf.Pcnt15(+left),sf.Pcnt85(wleft)))
wright =sf.FixSize(sf.Mix(sf.Pcnt15(+right),sf.Pcnt85(wright)))

sf.WriteFile32((+wleft,+wright),"temp/mix.wav")

(convoll,convolr)=sf.ReadFile("temp/v-grand-l.wav")
(convorl,convorr)=sf.ReadFile("temp/v-grand-r.wav")
left  = sf.BesselLowPass(left  ,392,1)
right = sf.BesselLowPass(right,392,1)
ll  = reverberate(+left ,convoll)
lr  = reverberate( left ,convolr)
rl  = reverberate(+right,convorl)
rr  = reverberate( right,convorr)
vwleft =sf.FixSize(sf.Mix(ll,rl))
vwright=sf.FixSize(sf.Mix(rr,lr))
wleft =sf.FixSize(sf.Mix(wleft ,sf.Pcnt20(vwleft )))
wright=sf.FixSize(sf.Mix(wright,sf.Pcnt20(vwright)))

sf.WriteFile32((wleft,wright),"temp/grand.wav")

First notice the simple harmonic exciter. I have discussed this elsewhere. However, the first trick to getting a bright reverb' is to excite the initial impulse response:

(convoll,convolr)=sf.ReadFile("temp/bh-l.wav")
(convorl,convorr)=sf.ReadFile("temp/bh-r.wav")

convoll=excite(convoll,0.75,2.0)
convolr=excite(convolr,0.75,2.0)
convorl=excite(convorl,0.75,2.0)
convorr=excite(convorr,0.75,2.0)

Then the resulting 'wet' (i.e. reverberated) signal is excited again (just a little):

ll  = reverberate(+left ,convoll)
lr  = reverberate(+left ,convolr)
rl  = reverberate(+right,convorl)
rr  = reverberate(+right,convorr)
wleft =sf.FixSize(sf.Mix(ll,rl))
wright=sf.FixSize(sf.Mix(rr,lr))

wright = excite(wright,0.15,1.11)
wleft  = excite(wleft ,0.15,1.11)

This makes a bright, vibrant sound. The impulse I am using is a lot like a large concert hall. With excitation effects added it starts to sound very bright and close and looses some of its size. I guess it is a bit like an ornate church.

I wanted to add much more size. To do this, I used my imagination rather than just mathematics. Consider listening to a organ with the nave of a huge cathedral behind you and the aisles to the sides. The bright sound will come from the organ and its surround but, somewhat delayed and at lower frequencies, will come the long, slow reverberation from those huge stone enclosed spaces. To get that effect I created a completely separate reverberation with a much longer, denser reverberation which and pre-filtered the signal to remove some of the high frequencies.

(convoll,convolr)=sf.ReadFile("temp/v-grand-l.wav")
(convorl,convorr)=sf.ReadFile("temp/v-grand-r.wav")
left  = sf.BesselLowPass(left  ,392,1)
right = sf.BesselLowPass(right,392,1)
ll  = reverberate(+left ,convoll)
lr  = reverberate( left ,convolr)
rl  = reverberate(+right,convorl)
rr  = reverberate( right,convorr)
vwleft =sf.FixSize(sf.Mix(ll,rl))
vwright=sf.FixSize(sf.Mix(rr,lr))
wleft =sf.FixSize(sf.Mix(wleft ,sf.Pcnt20(vwleft )))
wright=sf.FixSize(sf.Mix(wright,sf.Pcnt20(vwright)))

sf.WriteFile32((wleft,wright),"temp/grand.wav")

It is the mixture of a little of this very long low reverberation with the bright faster reverberation which makes the final result.

Here is an example:





Saturday, 27 September 2014

Keeping Aliasing Under Control

I have been working on a reed pipe sound. Something like a oboe but for a pipe organ. Aliasing made it really hard!

The problems stem from wanting to use saw tooth waveforms and then distortion synthesis on top. I could have used additive synthesis to create the saw tooth wave forms. This works (actually, just after writing this I did start using some additive synthesis so have added that to the bottom of the write up) but it only helps a little because of the distortion synthesis. The basic problem is that my sawtooth has aliasing in it and then adding distortion add more aliasing.

What do I mean by aliasing? I am not talking about that 8 bit effect what the waveform as squelchy noise in it. The aliasing I am talking about is frequency aliasing. 

You see, we work with twice the sample rate of the maximum frequency we want to convey. The snag is that mathematical transformations of a wave form or a simple generation of a waveform make frequencies which are above that half way point. Say that we are making a signal at 5000Hz at 96ksps (96 000 samples per second). If that signal is a saw tooth then the 6th harmonic will have energy (1/6 of the fundamental) and be of 30kHz for example. Not a problem, but what about the 10th harmonic at 1/10 the fundamental energy (only 10db down); it is at 50kHz. It is not possible to represent 50KHz at 96ksps. The 'Nyquist Frequency' is 48kHz. What happens is that the frequency 'folds' around the Nyquist frequency and we get not 50kHz but 46 (48-2).  The higher the harmonics after that, the lower they actually come out in the eventual output until the fold again at 0Hz and start coming back up.

There is another aliasing issue which occurs. Some people might not call it aliasing, but it sort of its. It is caused by negative frequencies. When we frequency modulate one signal by another we make side bands. Ring and amplitude modulation do the same thing but to a lesser extent. If the side bands are wide enough they can end up being negative. Consider a 1kHz signal modulated by a 1.1kHz signal. There will be sidebands all over the place, but the first side bands will be at 2.1 and 0.1kHz. The 0.1 is actually an alias of -0.1kHz. 

As we distort a signal using distortion synthesis we add harmonics. These can easily start to push up past the Nyquist frequency and cause problems. Also, sample wise manipulations can actually place a form of frequency modulation on a signal where by the signal is frequency modulated by the sample rate. This latter effect is very noticeable in the rather over simplistic MakeSawTooth function in Sonic Field.

The way to control these effects is to use an anti-aliasing filter sf.Clean and a high pass filter. The former stops harmonics from escaping above 22kHz so they have no chance of then being distorted into creating harmonics over the Nyquist frequency. The latter removes the low frequencies which build up due to negative frequency aliases. These low frequencies can be of very low energy but we still hear them distinctly as a metallic sound. They are very characteristic of digital synthesis and that head crushing effect harshness that can, sadly, sometime completely ruin it.

def simpleOboe(length,freq):
    sig=sf.FixSize(
        sf.Power(
            sf.Clean(
                sf.Mix(
                    sf.Clean(
                        sf.MakeSawTooth(
                            sf.PhasedSineWave(length,freq,random.random())
                        )
                    ),
                    sf.PhasedSineWave(length,freq,random.random())
                )
            )
            ,
            1.5
        )
    )
    sig=sf.ButterworthHighPass(sig,freq*0.66,6)
    sig=sf.Clean(sig)
    sig=sf.FixSize(sf.Power(sig,1.5))
    sig=sf.Clean(sig)
    sig=sf.ButterworthHighPass(sig,freq*0.66,6)
    sig=sf.FixSize(sf.Power(sig,1.5))
    sig=sf.Clean(sig)
    sig=sf.ButterworthHighPass(sig,freq*0.66,6)
    sig=sf.FixSize(sig)
      

    sig=sf.RBJPeaking(sig,freq*5,0.5,5)
    sig=sf.RBJPeaking(sig,freq*7,1,5)
    sig=sf.RBJNotch  (sig,freq*2,0.5,1)
    sig=sf.Clean(sig)
    
    sig=sf.Mix(
        sf.FixSize(sig),
        sf.Multiply(
            cleanNoise(length,freq*9.0),
            sf.SimpleShape((0,-60),(64,-20),(128,-24),(length,-24))
        )
    )

    sig=sf.ButterworthLowPass (sig,freq*9,4)
    sig=sf.ButterworthHighPass(sig,freq*0.66,6)

    return sf.FixSize(sf.Clean(sig))

In above patch which makes a Oboe like sound (to be honest, more of a reed organ pipe) I have highlighted the repeated anti-aliasing and high pass filters which make this sound usable. Without them it becomes overwhelmingly metallic and harsh.

Finally, a bit of additive synthesis helps. Rather than going a fully additive approach, I stuck with the distortion and subtractive approach buy replaced MakeSawTooth with a very simple additive sawtooth generator:

def niceSaw(length,frequency):
    p=random.random()
    if frequency>4000:
        sig=sf.Mix(
            sf.PhasedSineWave(length,frequency,p),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*2.0,p),1.0/2.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*3.0,p),1.0/3.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*4.0,p),1.0/4.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*5.0,p),1.0/5.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*6.0,p),1.0/6.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*7.0,p),1.0/7.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*8.0,p),1.0/8.0)
            )
    else:
        sig=sf.Mix(
            sf.PhasedSineWave(length,frequency,p),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*2.0,p),1.0/2.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*3.0,p),1.0/3.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*4.0,p),1.0/4.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*5.0,p),1.0/5.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*6.0,p),1.0/6.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*7.0,p),1.0/7.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*8.0,p),1.0/8.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*9.0,p),1.0/9.0),
            sf.NumericVolume(sf.PhasedSineWave(length,frequency*10.0,p),1.0/10.0)
        )

    return sf.FixSize(sig)


Tuesday, 23 September 2014

Making A Clarion Pipe

For Bach's Passacaglia and Fugue in C Minor I wanted another pipe sound to help differentiate the music. The midi I have of this (sorry – I cannot find who encoded it) has 4 manuals and the pedals. I have allocated a new sound to the 4th manual. The sound is one I call a 'clarion pipe'. It is brighter and brassier than the others. This was done by mixing a a very high frequency sawtooth with the original ( so far so normal ) but then passing the resulting mixture through substantial distortion.

def vocalRaw5(length,freq):
    s1=sf.MakeTriangle(sf.PhasedSineWave(length,freq,random.random()))
    s2=sf.MakeTriangle(sf.PhasedSineWave(length,freq*2.001,random.random()))
    s3=sf.MakeSawTooth(sf.PhasedSineWave(length,freq*4.002,random.random()))

    sig=sf.Mix(
        sf.Pcnt70(s1),
        sf.Pcnt20(s2),
        sf.Pcnt10(s3),
        sf.Multiply(
            cleanNoise(length,freq),
            sf.SimpleShape((0,-60),(64,-20),(128,-36),(length,-36))
        )
    )
    sig=sf.FixSize(sf.Power(sig,10.0))
    sig=sf.BesselLowPass(sig,freq*6.0,2)

Above we can see the three signal generators. The base note is just a triangle wave (or randomised phase). Then another triangle if placed one octave up and a sawtooth two octaves up. The upper waves are very slightly detune; this along with the random phases helps liven up the sound a great deal.

Nevertheless, it sounds very normal and quite like the other sounds but with an annoying sawtooth buzz until sf.Power(sig,10.0). This is a massive distortion. It is almost exactly the opposite of what we normally think of as distortion. 'Normal' distortion squashes the signal as it approaches maximum excursion. This adds an ordered set of diminishing harmonics. sf.Power does the opposite and stretches the signal. This adds a mass of higher harmonics which burst forth. If a sine wave is treated this way it starts to turn into a sound not unlike a Saxophone due to the build up of harmonics, both even and odd at similar intensities which only die away slowly.

The effect is far to strong and produces masses of harmonics up to and above the limit of human hearing. To get it back under control I use a Bessel filter. This has low phase distortion and a nice smooth rolloff. The combination makes the distinct clarion sound which can be heard in the piece.



To really accent the new pipe I added a 'stop' of a flute (almost diapason) pipe sound 2 octaves up. Due to the stereo positioning logic in the patch, this sound comes out in a different place to the clarion and so the two do not merge but rather follow one another much as a similar flute sound follows the pedals. Here is the patch for the flute pipe:

def vocalRaw4(length,freq):
    return sf.FixSize(sf.Power(sf.MakeTriangle(sf.SineWave(length,freq)),2.0))

Again, we can see that I have used a bit of sf.Power to brighten the sound. Here the amount is much less so I get something between a flute and a diapason.

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.

Saturday, 20 September 2014

Mimicking Reale Note Evolution

Humans seem to like listening to real things, tubes, strings and such. 

These real things don't make one tone in a sound, their tone evolves. Here I discuss a way of using resampling (FM if you like) to achieve that effect.

Here is part of the patch I used to create Contrapunctus III

        if tp==0:
            env=sf.NumericShape((0,0),(96,1),(192,0.5),(length,0))
        elif tp==1:
            env=sf.NumericShape((0,0),(96,0.75),(length-128,1.0),(length,0))
        elif tp==2:
            env=sf.NumericShape((0,0),(96,0.75),(length-256,1.0),(length,0))
        elif tp==3:
            if length<1280:
                env=sf.NumericShape((0,0),(64,0.5),(256,1),(512,0.75),(length-256,0.5),(length,0))
            else:
                env=sf.NumericShape((0,0),(64,0.5),(256,1),(512,0.75),(length-512,.75),(length,0))
        else:
            env=sf.NumericShape((0,0),(64,0.25),(512,1),(length/2,0.75),(length,0))

        sig=sf.Multiply(sig,+env)
        sig=sf.FixSize(sig)

        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)

Here is the outcome:

So, let's go through the patch and see how it works:
        if tp==0:
            env=sf.NumericShape((0,0),(96,1),(192,0.5),(length,0))
        elif tp==1:
            env=sf.NumericShape((0,0),(96,0.75),(length-128,1.0),(length,0))
        elif tp==2:
            env=sf.NumericShape((0,0),(96,0.75),(length-256,1.0),(length,0))
        elif tp==3:
            if length<1280:
                env=sf.NumericShape((0,0),(64,0.5),(256,1),(512,0.75),(length-256,0.5),(length,0))
            else:
                env=sf.NumericShape((0,0),(64,0.5),(256,1),(512,0.75),(length-512,.75),(length,0))
        else:
            env=sf.NumericShape((0,0),(64,0.25),(512,1),(length/2,0.75),(length,0))

        sig=sf.Multiply(sig,+env)

First we have a dynamic envelope generator; it chooses the envelope based on the original length of the node (encoded in tp) and the current length (after some modification). The final line applies the envelope to the signal by simply multiplying one with the other. Note that NumericShape creates linear envelopes. Whilst Sonic Field can make exponential envelopes (via SimpleShape) and combination envelopes (by multiplying exponential and linear ones) I personally find a it easier to control linear envelopes and to add detail to them to mimic exponential behaviour.

So far so standard. This approach is pretty much text book synthesis plus some smarts over envelope shape. We need to make the shape 'full size' again so that we don't get unwanted volume effects. IE I tend to keep bringing signals back up to a deviation max of 1 so that when I do apply a volume to them, I get exactly what I want.

        sig=sf.FixSize(sig)

Does the the deviation correction. In the past I used Normalise a lot instead. This is actually more powerful because it removes any over all 'DC" component. By DC (an old fashion term to say the least) I mean an over all offset from zero. A pure tone (say a sine wave) should, when all the samples are added together, have a absolute offset of 0. Normalise makes this happen; in so doing it sometimes move the beginning or end sample of a signal away from zero. When the signals are mixed this puts clicks and pops into the sound. Now I try to use FixSize throughout and only use Normalise at the last step.

        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)

Finally we get tot the point of the post. I create an envelope which moves from 0.995 to 1.005. I then add to that the volume envelope which was used. However, I scale the volume envelop right down to 1% of its original size. What this gives me is a shape which moves over all up a bit but with more up when the volume is high. 


        sig=sf.FrequencyModulate(sig,mod)  

FrequencyModulate then 'bends' the note to the envelope. This mimics the pitch rising throughout the note (a little) and being higher when the volume is higher. That sort of bend - to my ears - is quite typical of a bowed instrument or a blown tube (flute or diapason).

Friday, 19 September 2014

Simple Harmonic Exciter

Harmonic Excitation is a technique to add harmonics which are missing or generally give more depth and energy to a top end - here is a patch I having quite a bit of success with:

def excite(sig_,mix,power):
    def exciteInner():
        sig=sig_
        m=sf.Magnitude(+sig)
        sigh=sf.BesselHighPass(+sig,500,2)
        mh=sf.Magnitude(+sigh)
        sigh=sf.Power(sigh,power)
        sigh=sf.BesselHighPass(sigh,1000,2)
        nh=sf.Magnitude(+sigh)
        sigh=sf.NumericVolume(sigh,mh/nh)
        sig=sf.Mix(sf.NumericVolume(sigh,mix),sf.NumericVolume(sig,1.0-mix))
        n=sf.Magnitude(+sig)
        return sf.Realise(sf.NumericVolume(sig,m/n))

    return sf_do(exciteInner)

The trick is to separate the top end using Bessel filters to avoid too much phase distortion. Then I use the 'power' filter which takes the signal and raises its instantaneous magnitude to a power. This adds distortion which add harmonics and strengthens the existing ones. The result is mixed back into the original after a second filter helps remove some of the original signal so we here a boost to the new harmonics.

I use Magnitude to make the effect not change the over all magnitude of the signal and to over come the volume reducing effect of power to the low magnitude filtered signal.

Simple - but it works nicely as part of the reverberation post processing for this track:

Bach Contrapunctus III

Tuesday, 16 September 2014

Bach Toccata Fugue In D Minor Full Patch


import math
import random

def granularReverb(signal,ratio,delay,density,length=50,stretch=1,vol=1):
    out=[]
    for grain in sf.Granulate(signal,length,10):
        (signal_i,at)=grain
        signal_i=sf.Realise(signal_i)
        signal_i=sf.DirectRelength(signal_i,ratio)
        for x in range(0,density):
            out.append(
                (
                    +signal_i,
                    int((at + (random.random()+random.random())*delay)*stretch)
                )
            )
        -signal_i
  
    out=sf.Collapse(out)
    out=sf.Realise(sf.MixAt(out))
    out=sf.Clean(sf.NumericVolume(out,vol))
    return out

def reverbInner(signal,convol,grainLength):
    mag=sf.Magnitude(+signal)
    if mag>0:
        signal_=sf.Concatenate(signal,sf.Silence(grainLength))
        signal_=sf.FrequencyDomain(signal_)
        signal_=sf.CrossMultiply(convol,signal_)
        signal_=sf.TimeDomain(signal_)
        newMag=sf.Magnitude(+signal_)
        signal_=sf.NumericVolume(signal_,mag/newMag)        
        # tail out clicks due to amplitude at end of signal 
        return signal_
    else:
        -convol
        -signal
        return sf.Silence(0)

def reverberate(signal,convol):
    def reverbI():
        grainLength = sf.Length(+convol)
        convol_=sf.FrequencyDomain(sf.Concatenate(convol,sf.Silence(grainLength)))
        signal_=sf.Concatenate(signal,sf.Silence(grainLength))
        out=[]
        for grain in sf.Granulate(signal_,grainLength):
            (signal_i,at)=grain
            inp=reverbInner(signal_i,+convol_,grainLength)
            if sf.Length(+inp)>0:
                out.append((inp,at))
            else:
                print "Skipping"
                -inp
        -convol_
        return sf.Clean(sf.Normalise(sf.MixAt(out)))
    return sf_do(reverbI)

def cleanNoise(length,freq):
    return sf.Normalise(
        sf.ButterworthHighPass(
            sf.Clean(sf.WhiteNoise(length)),
            freq*0.25,
            4
        )
    )

def vocalRaw1(length,freq):
    sig=sf.Mix(
        sf.Power(sf.MakeTriangle(sf.SineWave(length,freq)),1.25),
        sf.Multiply(
            cleanNoise(length,freq),
            sf.SimpleShape((0,-3),(128,-26),(length,-26))
        )
    )
    move=sf.NumericShape(
        (0,0.995+random.random()*0.01),
        (sf.Length(+sig),0.995+random.random()*0.01)
    )
    return sf.Resample(move,sig)

def vocalRaw2(length,freq):
    sig=sf.Mix(
        sf.Power(sf.MakeTriangle(sf.SineWave(length,freq)),2.0),
        sf.Multiply(
            cleanNoise(length,freq),
            sf.SimpleShape((0,-6),(128,-26),(length,-26))
        )
    )
    move=sf.NumericShape(
        (0,0.995+random.random()*0.01),
        (sf.Length(+sig),0.995+random.random()*0.01)
    )
    return sf.Resample(move,sig)

def vocalRaw3(length,freq):
    sig=sf.Mix(
        sf.Power(sf.MakeSquare(sf.SineWave(length,freq)),2.0),
        sf.Multiply(
            cleanNoise(length,freq),
            sf.SimpleShape((0,0),(128,-20),(length,-20))
        )
    )
    move=sf.NumericShape(
        (0,0.995+random.random()*0.01),
        (sf.Length(+sig),0.995+random.random()*0.01)
    )
    return sf.Resample(move,sig)


def sing(pitch,lengthIn,beat,v,vl,vr,voice):
    def singInner():
        length=lengthIn
        tp=0
        if length<250*beat:
            length=250*beat
        if length<500*beat:
            length*=1.25
            tp=1
        elif length<800*beat:
            if pitch<220:
                length*=1.4
            elif pitch<440:
                length*=1.25
            else:
                length*=1.1
            tp=2
        else:
            length*=1.0
            tp=3
    
        sig=[]
        if pitch<330:
            x=5
        else:
            x=3
        for x in range(0,x):
            sig.append(sf.NumericVolume(voice(length,pitch+random.random()),random.random()+0.25))
        sig=sf.Realise(sf.Mix(sig))
        
        sig = sf.FixSize(sig)
        length=sf.Length(+sig)
        
        print "Type: ",tp
        if tp==1:
            env=sf.NumericShape((0,0),(32,0.5),(64,0.75),(128,1),(length-128,1.0),(length,0))
        elif tp==2:
            env=sf.NumericShape((0,0),(64,0.5),(256,1),(512,0.75),(length-128,0.75),(length,0))
        else:
            env=sf.NumericShape((0,0),(64,0.25),(512,1),(length/2,0.75),(length,0))

        sig=sf.Multiply(sig,+env)
        sig=sf.Normalise(sig)

        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<220:
            sig=sf.Mix(
                granularReverb(+sig,ratio=0.501,delay=256,density=32,length=256,stretch=1,vol=0.25),
                sig
            )
            sig=sf.BesselLowPass(sig,pitch*5.0,2)
        elif pitch<440:
            sig=sf.BesselLowPass(sig,pitch*3.5, 1)
        else:
            sig=sf.Mix(
                granularReverb(+sig,ratio=2.0,delay=128,density=32,length=128,stretch=1,vol=0.1),
                sig
            )
            sig=sf.BesselLowPass(sig,pitch*2.5, 1)
          
        note=sf.NumericVolume(sf.Normalise(sig),v)
        notel=sf.Realise(sf.NumericVolume(+note,vl))
        noter=sf.Realise(sf.NumericVolume( note,vr))
        return (notel,noter)
    return sf_do(singInner)

def doMidi(count,notesStart,notesEnd,notes,midi,voice):
    for tickOn,tickOff,note,key,velocity in midi:
        if count>notesEnd:
            break
        at=tickOn*beat
        if count<notesStart:
            count+=1
            continue
        length=(tickOff-tickOn)*beat
        if key==0:
            pitch=base
        else:
            key=float(key)
            pitch= (sf.Semitone(0)**key) * base
        velocity=velocity/100
        #length*=1.5
        v=velocity
        lr=random.random()*0.5+0.25
        rl=1.0-lr
        print "C",count,"P",pitch,"@",at,"L",length,"V",velocity
        note = sing(pitch, length,beat,v,lr,rl,voice)
        dl=30*rl
        dr=38*lr
        notes.append((note,at+dl,at+dr))
        count+=1

def my_so(what):
    return what()

midis=sf.ReadMidiFile("temp/tfdm.mid")
beat        =     4.5
base        =     8.1757989156
notesl=[]
notesr=[]

notes=[]
count       =     0.0
notesStart  =     0.0
notesEnd    =999999.0
print "Channel 0"
midi=midis[1]
doMidi(count,notesStart,notesEnd,notes,midi,vocalRaw1)

count=0
for note in notes:
    nlr,atl,atr=note
    print "Done: ",count,atl,atr
    notel,noter=nlr
    notesl.append([notel,atl])
    notesr.append([noter,atr])
    count+=1

notes=[]
count       =     0.0
print "Channel 1"
midi=midis[2]
doMidi(count,notesStart,notesEnd,notes,midi,vocalRaw2)
count=0
for note in notes:
    nlr,atl,atr=note
    print "Done: ",count,atl,atr
    notel,noter=nlr
    notesl.append([sf.Pcnt65(notel),atl])
    notesr.append([sf.Pcnt65(noter),atr])
    count+=1

notes=[]
count       =     0.0
print "Channel 2"
midi=midis[3]
doMidi(count,notesStart,notesEnd,notes,midi,vocalRaw3)

count=0
for note in notes:
    nlr,atl,atr=note
    print "Done: ",count,atl,atr
    notel,noter=nlr
    notesl.append([notel,atl])
    notesr.append([noter,atr])
    count+=1

def mix(notes):
    def mixInner():
        return sf.Realise(sf.Trim(sf.Normalise(sf.Clean(sf.MixAt(notes)))))
    return sf_do(mixInner)

print "Mix"
left =mix(notesl)
right=mix(notesr)

sf.WriteFile32((+left,+right),"temp/dry.wav")
print "Reverb"

(convoll,convolr)=sf.ReadFile("temp/revl.wav")
(convorl,convorr)=sf.ReadFile("temp/revr.wav")

ll  = reverberate(+left ,convoll)
lr  = reverberate(+left ,convolr)
rl  = reverberate(+right,convorl)
rr  = reverberate(+right,convorr)
wleft =sf.FixSize(sf.Mix(ll,rl))
wright=sf.FixSize(sf.Mix(rr,lr))
left =sf.Normalise(sf.Mix(sf.Pcnt35(left ),sf.Pcnt65(wleft )))
right=sf.Normalise(sf.Mix(sf.Pcnt35(right),sf.Pcnt65(wright)))

sf.WriteFile32((left,right),"temp/wet.wav")