Tuesday 3 February 2015

Working With An Analogue SynthThe

The Waldorf Pulse 2 analogue synthesiser

Most of my work in Sonic Field has been using the built in synth abilities of the program. But there is not reason it should to drive an external synth and post process the signal.

I recently bought a Pulse 2 and it is quite amazing. However, it is also a mono synth. I am completely spoilt generating sounds with Sonic Field as it has no upper limit to the number of notes which can be generated at once. Whilst the mono synth sounds has its place, it is also rather limited. So, I needed a solution to give multi-tracking.

The existing midi implementation in SF was just pathetic. I completely ripped it out and pretty much started over. The only piece remaining is the code which maps midi on/off messages into notes and disambiguated overlapping messages on the same track/channel/key combination.

I should go into great detail about how it all works, but I am exhausted after a long day working and evening making music so here is the dump of the patch I used to drive the synthesiser over midi. Yes - I drove the synth from Sonic Field directly!



from com.nerdscentral.audio.midi import MidiFunctions

class Midi(MidiFunctions):
    metaTypes={
            0x00:'SequenceNumber',
            0x01:'text',
            0x02:'copyright',
            0x03:'track_name',
            0x04:'instrument',
            0x05:'lyrics',
            0x06:'marker',
            0x07:'cue',
            0x20:'channel',
            0x2F:'end',
            0x51:'tempo',
            0x54:'smpte_offset',
            0x58:'time_signature',
            0x59:'key_signature',
            0x7f:'sequencer_specific'
        }
        
    timeTypes={
        0.0:  'PPQ',
        24.0: 'SMPTE_24',
        25.0: 'SMPTE_25',
        29.97:'SMPTE_30DROP',
        30.0: 'SMPTE_30'
    }
     
    @staticmethod
    def timeType(sequence):
        return Midi.timeTypes[sequence.getDivisionType()]

    @staticmethod
    def isNote(event):
        return event['command']=='note'

    @staticmethod
    def isMeta(event):
        return event['command']=='meta'

    @staticmethod
    def isCommand(event):
        return event['command']=='command'
        
    @staticmethod
    def isTempo(event):
        Midi.checkMeta(event)
        return event['type']==0x51

    @staticmethod
    def isTimeSignature(event):
        Midi.checkMeta(event)
        return event['type']==0x58

    @staticmethod
    def metaType(event):
        t=event['type']
        if t in Midi.metaTypes:
            return Midi.metaTypes[t]
        return 'unknown'

    @staticmethod
    def checkMeta(event):
        if not event['command']=='meta':
            raise Exception('Not meta message')

    @staticmethod
    def tempo(event):
        Midi.checkMeta(event)
        if event['type']!=0x51:
            raise Exception('not tempo message')
        data=event['data']
        if len(data)==0:
            raise Exception('no data')
        t=0
        for i in range(0,len(data)):
            if not i==0:
                t <<= 8
            t+=data[i]
        return t

    @staticmethod
    def timeSignature(event):
        Midi.checkMeta(event)
        if event['type']!=0x58:
            raise Exception('not tempo message')
        data=event['data']
        if not len(data)==4:
            raise Exception('wrong data')
        return {
            'numerator'  :data[0],
            'denominator':2**data[1],
            'metronome'  :data[2],
            '32nds/beat' :data[3]
        }
        
    @staticmethod
    def tickLength(denominator,microPerQuater,sequence):
        # if denom = 4 then 1 beat per quater note
        # if denom = 8 then 2 beats per quater note
        # there fore beats per quater note= denom/4
        beatsPerQuaterNote = denominator/4.0
        ticksPerBeat       = float(sequence.getResolution())
        microsPerBeat      = float(microPerQuater)/beatsPerQuaterNote
        return microsPerBeat/float(ticksPerBeat)

sequence=Midi.readMidiFile("temp/passac.mid")

print 'Sequence Time  Type:', Midi.timeType(sequence)
print 'Sequence Resolution:', sequence.getResolution()
print 'Initial tick length:',Midi.tickLength(4,500000,sequence)
otl=Midi.tickLength(4,500000,sequence)

midis=Midi.processSequence(sequence)

sout=Midi.blankSequence(sequence)

# Create the timing information track
tout=sout.createTrack()
for event in midis[0]:
    if Midi.isMeta(event):
        if Midi.isTempo(event) or Midi.isTimeSignature(event):
            tout.add(event['event'])

tout1=sout.createTrack()
tout2=sout.createTrack()
midi1=[]
midi2=[]
flip=True
minKey=999
maxKey=0

# Use 499 for 1 Done
# Use 496 for 2
# Use 497 for 3
# Use 497 for 4
# Use 001 for 5 Done
# Use 002 for 6
midiNo=6

for event in midis[midiNo]:
    if Midi.isNote(event):
        ev1=event['event']
        ev2=event['event-off']
        if event['key']>maxKey:
            maxKey=event['key']
        if event['key']<minKey:
            minKey=event['key']

for event in midis[midiNo]:
    if Midi.isNote(event):
        ev1=event['event']
        ev2=event['event-off']
        ev1.setTick(ev1.getTick()+600)
        ev2.setTick(ev2.getTick()+600)
        key=event['key']
        pan=127.0*float(key-minKey)/float(maxKey-minKey)
        pan=31+pan/2
        pan=int(pan)
        pan=Midi.makePan(1,ev1.getTick()-1,pan)
        if flip:
            midi1.append(pan)
            midi1.append(event['event'])
            midi1.append(event['event-off'])
            flip=False
        else:
            midi2.append(pan)
            midi2.append(event['event'])
            midi2.append(event['event-off'])
            flip=True

Midi.addPan(tout1,1,100,64)
Midi.addPan(tout2,2,100,64)

Midi.addNote(tout1,1,100,120,50,100)
Midi.addNote(tout2,2,100,120,50,100)
        
midi1=sorted(midi1,key=lambda event: event.getTick())
midi2=sorted(midi2,key=lambda event: event.getTick())

for event in midi1:
    Midi.setChannel(event,1)
    tout1.add(event)
#for event in midi2:
#    Midi.setChannel(event,2)
#    tout2.add(event)

Midi.writeMidiFile("temp/temp.midi",sout)

for dev in Midi.getMidiDeviceNames():
    print dev

player=Midi.getPlayer(3,2)
player.manual(sout)
player.waitFor()

And here is the post processing patch. I took each separately recorded voice from the synth and mixed them together in Audacity using the note I injected at a known point at the start of each to line them up. Once the mix sounded OK, I post processed with this patch:

def reverbInner(signal,convol,grainLength):
    def rii():
        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_)
            if newMag>0:
                signal_=sf.NumericVolume(signal_,mag/newMag)        
                # tail out clicks due to amplitude at end of signal 
                return sf.Realise(signal_)
            else:
                return sf.Silence(sf.Length(signal_))
        else:
            -convol
            return signal
    return sf_do(rii)
            
def reverberate(signal,convol):
    def revi():
        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
            out.append((reverbInner(signal_i,+convol_,grainLength),at))
        -convol_
        return sf.Clean(sf.FixSize(sf.MixAt(out)))
    return sf_do(revi)

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.Clean(sigh)
        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)

####################################
#
# Load the file and clean
#
####################################

(left,right)=sf.ReadFile("temp/pulse-passa-2.wav")

left =sf.Multiply(sf.NumericShape((0,0),(64,1),(sf.Length(+left ),1)),left )
right=sf.Multiply(sf.NumericShape((0,0),(64,1),(sf.Length(+right),1)),right)

left =sf.Concatenate(sf.Silence(1024),left)
right=sf.Concatenate(sf.Silence(1024),right)


####################################
#
# Room Size And Nature Controls
#
####################################

bright  = True
vBright = False
church  = False
ambient = False
post    = True
spring  = False
bboost  = False
  
if ambient:  
    (convoll,convolr)=sf.ReadFile("temp/v-grand-l.wav")
    (convorl,convorr)=sf.ReadFile("temp/v-grand-r.wav")
elif church:    
    (convoll,convolr)=sf.ReadFile("temp/bh-l.wav")
    (convorl,convorr)=sf.ReadFile("temp/bh-r.wav")
else:
    (convoll,convolr)=sf.ReadFile("temp/Vocal-Chamber-L.wav")
    (convorl,convorr)=sf.ReadFile("temp/Vocal-Chamber-R.wav")

if spring:
    spring=sf.ReadFile("temp/classic-fs2a.wav")[0]
    convoll=sf.Mix(
        convoll,
        +spring
    )
    
    convorr=sf.Mix(
        convorr,
        sf.Invert(spring)
    )

if bboost:
    left =sf.RBJLowShelf(left,256,1,6)
    right=sf.RBJLowShelf(right,256,1,6)
    
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)

if bright:
    right  = excite(right,0.15,1.05)
    left   = excite(left ,0.15,1.05)
if vBright:
    right  = excite(right,0.25,1.15)
    left   = excite(left ,0.25,1.15)

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")

if ambient:
    (convoll,convolr)=sf.ReadFile("temp/ultra-l.wav")
    (convorl,convorr)=sf.ReadFile("temp/ultra-r.wav")
elif church:
    (convoll,convolr)=sf.ReadFile("temp/v-grand-l.wav")
    (convorl,convorr)=sf.ReadFile("temp/v-grand-r.wav")
else:
    (convoll,convolr)=sf.ReadFile("temp/bh-l.wav")
    (convorl,convorr)=sf.ReadFile("temp/bh-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))
sf.WriteFile32((sf.FixSize(+vwleft),sf.FixSize(+vwright)),"temp/vwet.wav")
wleft =sf.FixSize(sf.Mix(wleft ,sf.Pcnt20(vwleft )))
wright=sf.FixSize(sf.Mix(wright,sf.Pcnt20(vwright)))
sf.WriteSignal(+wleft ,"temp/grand-l.sig")
sf.WriteSignal(+wright,"temp/grand-r.sig")
wleft  = sf.Normalise(wleft)
wright = sf.Normalise(wright)
sf.WriteFile32((wleft,wright),"temp/grand.wav")

if post:
    print "Warming"
    
    left  = sf.ReadSignal("temp/grand-l.sig")
    right = sf.ReadSignal("temp/grand-r.sig")
    
    def highDamp(sig,freq,fact):
        hfq=sf.BesselHighPass(+sig,freq,4)
        ctr=sf.FixSize(sf.Follow(sf.FixSize(+hfq),0.25,0.5))
        ctr=sf.Clean(ctr)
        ctr=sf.RBJLowPass(ctr,8,1)
        ctr=sf.DirectMix(
            1,
            sf.NumericVolume(
                sf.FixSize(sf.Invert(ctr)),
                fact
            )
        )
        hfq=sf.Multiply(hfq,ctr)
        return sf.Mix(hfq,sf.BesselLowPass(sig,freq,4))
    
    def filter(sig_):
        def filterInner():
            sig=sig_
            q=0.5
            sig=sf.Mix(
                sf.Pcnt10(sf.FixSize(sf.WaveShaper(-0.03*q,0.2*q,0,-1.0*q,0.2*q,2.0*q,+sig))),
                sig
            )
            sig=sf.RBJPeaking(sig,64,2,2)
            damp=sf.BesselLowPass(+sig,2000,1)
            sig=sf.FixSize(sf.Mix(damp,sig))
            low=sf.BesselLowPass(+sig,256,4)
            m1=sf.Magnitude(+low)
            low=sf.FixSize(low)
            low=sf.Saturate(low)
            m2=sf.Magnitude(+low)
            low=sf.NumericVolume(low,m1/m2)
            sig=sf.BesselHighPass(sig,256,4)
            sig=sf.Mix(low,sig)
            sig=highDamp(sig,5000,0.66)
            return sf.FixSize(sf.Clean(sig))
        return sf_do(filterInner)
    
    left  = filter(left)
    right = filter(right)
    sf.WriteFile32((left,right),"temp/proc.wav")


No comments:

Post a Comment