Saturday, 22 February 2014

Downloads

Welcome To The Sonic Field Download Page

News:

I have not kept up with this page at all. However, a quite check at github will show that Sonic Field has been coming along. The biggest piece of recent news is the new memory manager.  Now out of memory errors a thing of the past.

Check out this blog and github commit comments for news.

Blessed Be The Cheese Makers

License:

Sonic Field is licensed under AGPL3. The bundled Jython jar is licensed under its own license - seee www.jython.org

Those found abusing the license terms will be banished to Castle Anthrax (which might not be such a bad thing)

No - seriously - do not mess with copy right. If you are interested in other licensing terms for any of my code - just ask and we will see what we can do.

Releases And Downloads:

  • Latest development version tree is on GitHub. This is generally working as I do not push broken builds!
    http://github.com/nerds-central/SonicFieldRepo
  • In the past I also had snapshots for downloads, I have given up supporting this as checking out the github version should be very simple. If you as not a github user and would like to get your hands on the latest version - just drop me a comment and I'll sort something.

And never forget that he is not the messiah, he is a very naughty boy. 

Friday, 21 February 2014

The Code Behind Valley Of The Sythons

The entire patch and some description of how it works beneath that:

The key to understanding what comes next is that Sonic Field now works as a big extension to Python (Jython actually - Python running on the Java Vertual Machine). A Sonic Field patch is created using Python statements. However, the data being passed around is not Python Data it is hidden from the view of Python inside opaque Java objects. 

Valley Of The Sythons

The original idea was that Sonic Field sounds were 'signals' which were passed between 'processors'. Control of processing was also done via signals. The metaphor continues into Sython (Sonic Field Python). The syntax of Python makes the approach less obvious but it is still there.

sf.Multiply(sf.NumericShape((0,0),(len,1)),trem)

For example the above creates a signal which starts at 0 and works up to 1 at length len in milliseconds. The signal is then multiplied with another signal held in variable trem.  All Sonic Field processors are exposed to Python as methods on the sf object.

One last example before the code dump:

sf.Monitor(sf.SineWave(1000,440))

The above is a very simple Sython patch. It just makes an A4 tone for one second. However, the tone will have clicks at each end because it has no attack or release. So:

sf.Monitor(
    sf.Multiply(
        sf.SimpleShape((0,-90),(100,0),(900,0),(1000,-90)),
        sf.SineWave(1000,440)
    )
)

Now that does have an attack and release so it will sound much more like the beep one might expect.

Valley Of The Sythons:

import math
import random

execfile("patches/python/concurrent.py")

def randWalk(value,size,uBound):
    value  = float(value)
    size   = float(size)
    uBound = float(uBound)
    r=random.random()
    r=math.floor(r*size)-math.floor((size/2.0))    
    value+=r
    if value<1:
        value=2
    elif value>uBound:
        value=uBound-2
    return value

def randWalk3(value,uBound):
    return randWalk(value,3,uBound)

def fixSize(signal):
    mag=sf.MaxValue(signal)
    return sf.NumericVolume(signal,1.0/mag)
 
def fixSizeSat(signal):
     return fixSize(sf.Saturate(fixSize(signal)))
    
def saturatedNode(beat,pPitch,pitch,a,d,s,r,v):
    def saturateNode_():
        l=a+d+s+r
        if l>beat*2:
            iPitch=(pitch+pPitch)/2.0
            pos=beat/8
            signal1=sf.Slide((0,iPitch),(pos,pitch),(l,pitch))
            signal2=sf.Slide((0,iPitch),(pos,pitch*2),(l,pitch*2.02))
            signal3=sf.Slide((0,iPitch),(pos,pitch*3),(l,pitch*3.03))
        else:
            signal1=sf.SineWave(l,pitch)
            signal2=sf.SineWave(l,2*pitch*1.003)
            signal3=sf.SineWave(l,3*pitch*1.005)
            
        envelope= sf.NumericShape(
                 (0,0),
                 (a,1),
                 (a+d,0.75),
                 (a+d+s,0.25),
                 (a+d+s+r,0)
        )
        
        sat=(20-pitch/1000)
        if sat<1:
            sat=1
                        
        def doSat(sigIn):
            temp=sf.NumericVolume(sf.Multiply(sigIn,envelope),sat)
            return sf.Normalise(sf.Clean(sf.Saturate(temp)))

        signal=sf.Mix(
            doSat(signal1),
            sf.DB_6(doSat(signal2)),
            sf.DB_15(doSat(signal3))
        )
        
        envelope= sf.NumericShape(
                 (0,0),
                 (a,0.1),
                 (a+d,0),
                 (a+d+s,0.1),
                 (a+d+s+r,0)
        )
        
        signal=sf.Mix(
            sf.Multiply(
                sf.ButterworthLowPass(sf.WhiteNoise(l),pitch*5,1),
                envelope),
            signal
        )
        
        signal=fixSize(signal)
        
        hf=sf.Clip(sf.NumericVolume(signal,3))
    
        r1=fixSizeSat(sf.RBJPeaking(hf,pitch*1.3,0.5,85))
        r2=fixSizeSat(sf.RBJPeaking(hf,pitch*2.1,0.5,85))
        r3=fixSizeSat(sf.RBJPeaking(hf,pitch*2.9,0.5,85))
    
        signal=sf.Mix(
            sf.DB_6(signal),
            sf.DB_1(r1),
            sf.DB_4(r2),
            sf.DB_6(r3)
        )
        
        signal=sf.Clean(sf.NumericVolume(signal,v))

        signal=sf.BesselLowPass(signal,pitch*2,4)

        envelope= sf.NumericShape(
                 (0,1),
                 (a+d+s+r-125,1),
                 (a+d+s+r,0)
        )
        signal=sf.Multiply(envelope,signal)
        
        trem=sf.Slide((0,6*random.random()),(l,0.5*random.random()))
        trem=sf.Multiply(sf.NumericShape((0,0),(l,1)),trem)
        trem=sf.Mix(
            sf.NumericShape((0,1),(l,1)),
            trem
        )
        return sf.Multiply(signal,trem)
        
    return sf_do(saturateNode_)

def run(pitch,beat,minutes,startP,initial,overV):
    notesL=[]
    notesR=[]
    oPitch=float(pitch)
    pitchScaleDenom = 1.0
    pitchScaleNume  = float(startP)
    lengthScale     = 4.0
    volumeScale     = 4.0
    oVolume         = 4.0
    at=beat*float(initial)
    pPitch=float(pitch)

    while at/60000 < minutes:
        pitchScale = pitchScaleNume/pitchScaleDenom
        rvs        = 1.0/volumeScale
        volume     = rvs*oVolume
        pitch      = pitchScale*oPitch
        length     = lengthScale*beat

        
        # Create a consistent envelope
        a          = length*0.25
        d          = length*0.5
        s          = length*1.0
        r          = length*2.0
        if a<50:
            a=50
        if d<50:
            d=50
        if a>d-50:
           a=d/2
        
        r=r-s-d-a
        s=s-d-a
        d=d-a       
        
        vCorrection = 1/pitchScale
        
        # Do not over correct very & v high low frequencies 
        #  or very quiet notes. This aim it to stop loud highs
        #  dominating (psycho-acoustics)
        if rvs<0.2:
            if vCorrection<1:
                vCorrection=1
        
        if vCorrection>4:
            vCorrection=4
                                  
        print (
            at,
            "PitchNume: ",  pitchScaleNume,
            "PitchDenom: ", pitchScaleDenom,
            "Volume: ",     volumeScale,
            "Pitch: ",      pitch,
            "Length: ",     length,
            "Rvs: ",        rvs,
            "VCorr: ",      vCorrection
        ).__str__()    
            
        signal = saturatedNode(
            beat,
            pPitch,
            pitch,
            a,
            d,
            s,
            r,
            volume * vCorrection
        )

        lr=random.random()
        rl=1.0-lr
        notesL.append([sf.NumericVolume(signal,lr),at+30*rl])
        notesR.append([sf.NumericVolume(signal,rl),at+30*lr])

        at+=length
        
        pitchScaleDenom = randWalk3(pitchScaleDenom,10)

        pitchScaleNume  = randWalk3(pitchScaleNume,16)
        
        lengthScale     = randWalk3(lengthScale,8)

        volumeScale     = randWalk3(volumeScale,8)
        
        pPitch          = pitch

    return (
        sf.NumericVolume(sf.Normalise(sf.Clean(sf.MixAt(notesL))),overV),
        sf.NumericVolume(sf.Normalise(sf.Clean(sf.MixAt(notesR))),overV)
    )

def compressInner(signal,amount):
    def compressInnerDo():
        if sf.MaxValue(signal)<0.001:
            return signal
        signal_=sf.Normalise(signal)
        stf=sf.Normalise(sf.ButterworthLowPass(signal_,128,2))
    
        offset=1.0-amount    
        sr=sf.Reverse(sf.Follow(sf.Reverse(stf),1,1024))
        sw=sf.Follow(stf,1,1024)
        shape=sf.Mix(sr,sw)
        div=1.0/sf.MaxValue(shape)
        shape=sf.NumericVolume(shape,div)
        shape=sf.DirectMix(offset,sf.NumericVolume(shape,amount))
        return sf.Normalise(sf.Divide(signal_,shape))
    return sf_do(compressInnerDo)

def compress(signal,amount):
    def compressDo():
        cpo=amount
        signalM=sf.BesselBandPass(signal,200,2000,4)
        signalH=sf.BesselHighPass(signal    ,2000,4)
        signalL=sf.BesselLowPass( signal    , 200,4)
        amount_=cpo*cpo 
        
        signalM=compressInner(signalM, amount_)
        signalH=compressInner(signalH, amount_)
        signalL=compressInner(signalL, amount_)
    
        return sf.Normalise(sf.MixAt(
            (sf.Pcnt40(signalL),3.5),
            (sf.Pcnt20(signalM),0.0),
            (sf.Pcnt40(signalH),0.0)
        ))
    return sf_do(compressDo)
    

def reverbInner(signal,convol,grainLength):
    def reverbInnerDo():
        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 
            l=sf.Length(signal_)
            sf.Multiply(
                sf.NumericShape(
                    (0,1),
                    (l-100,1),
                    (1,0)
                ),
                signal_
            )
            return signal_
        else:
            return signal
            
    return sf_do(reverbInnerDo)

def reverberate(signal,convol):
    def reverberateDo():
        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_,at)=grain
            out.append((reverbInner(signal_,convol_,grainLength),at))
        return sf.Normalise(sf.MixAt(out))
    return sf_do(reverberateDo)

def doRun1():
   return run(128,1024          ,6,1,0,1.0)
def doRun2():
   return run(128.0*4.0/3.0,1024,6,2,1,1.0)
def doRun3():
   return run(256.0*3.0/2.0,1024,6,1,5,0.5)
def doRun4():
   return run(512.0*5.0/4.0,1024,6,1,9,0.25)

random.seed(0.128)

x1=sf_do(doRun1)
x2=sf_do(doRun2)
(left1,right1) = x1.get()
sf.WriteSignal(left1,"temp/l1")
sf.WriteSignal(right1,"temp/r1")

(left2,right2) = x2.get()
sf.WriteSignal(left2,"temp/l2")
sf.WriteSignal(right2,"temp/r2")

x3=sf_do(doRun3)
x4=sf_do(doRun4)

(left3,right3) = x3.get()
sf.WriteSignal(left3,"temp/l3")
sf.WriteSignal(right3,"temp/r3")

(left4,right4) = x4.get()
sf.WriteSignal(left4,"temp/l4")
sf.WriteSignal(right4,"temp/r4")


left1=sf.ReadSignal("temp/l1")
left2=sf.ReadSignal("temp/l2")
left3=sf.ReadSignal("temp/l3")
left4=sf.ReadSignal("temp/l4")
left  = sf.Normalise(sf.Clean(fixSize(sf.Mix(left1,left2,left3,left4))))
left  = compress(left,0.33)
sf.WriteSignal(left,"temp/l")
left=""

right1=sf.ReadSignal("temp/r1")
right2=sf.ReadSignal("temp/r2")
right3=sf.ReadSignal("temp/r3")
right4=sf.ReadSignal("temp/r4")
right = sf.Normalise(sf.Clean(fixSize(sf.Mix(right1,right2,right3,right4))))

right = compress(right,0.33)
sf.WriteSignal(right,"temp/r")
right=""

sf.WriteFile32((sf.ReadSignal("temp/l"),sf.ReadSignal("temp/r")),"temp/temp.wav")

(left,right)=sf.ReadFile("temp/temp.wav")

(convoll,convolr)=sf.ReadFile("temp/terrys_warehouse_stereo_short.wav")
convoll=sf.Mix(
    convoll,
    sf.Pcnt15(sf.DirectRelength(convoll,0.2)),
    sf.Pcnt15(sf.Raise(sf.DirectRelength(convolr,0.2),2))
)
convolr=sf.Mix(
    convolr,
    sf.Pcnt15(sf.DirectRelength(convolr,0.2)),
    sf.Pcnt15(sf.Raise(sf.DirectRelength(convolr,0.2),2))
)
convoll=sf.Normalise(sf.Saturate(sf.Normalise(convoll)))
convolr=sf.Normalise(sf.Saturate(sf.Normalise(convolr)))

wleft =reverberate(left,convoll)
wright=reverberate(right,convolr)

left=sf.Normalise(sf.MixAt(
    (sf.Pcnt70(wleft),10),
    (sf.Pcnt10(wright),40),
    (sf.Pcnt20(left),0)
))

right=sf.Normalise(sf.MixAt(
    (sf.Pcnt70(wright),10),
    (sf.Pcnt10(wleft),40),
    (sf.Pcnt20(right),0)
))

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

(left,right)=sf.ReadFile("temp/temp_post.wav")

left  = compress(left,0.95)
right = compress(right,0.95)

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

shutdownConcurrnt()

First the dirty! Why 'execfile("patches/python/concurrent.py")' The answer is that I could not be bothered to set up sys.path or the class path correctly - me bad :( [I have fixed it in later patches]

Now for the Random Walk code:


def randWalk(value,size,uBound):
    value  = float(value)
    size   = float(size)
    uBound = float(uBound)
    r=random.random()
    r=math.floor(r*size)-math.floor((size/2.0))    
    value+=r
    if value<1:
        value=2
    elif value>uBound:
        value=uBound-2
    return value

def randWalk3(value,uBound):
    return randWalk(value,3,uBound)

The core concept behind the piece is constraining randomness to give patters which shift around slowly forming shape and movement in the piece. Here we see a few key points. Working with Random numbers requires a close interaction between integer and non integer numbers. randWalk take an number (assumed to be an integer) and moves it randomly up or down. However, the maximum distance it can move is fixed by the size parameter. The maximum value it can reach is fixed by the uBound parameter and the minimum is 1. This causes the 'random walk' effect that the music is based upon. randWalk3 is simply a helper function (I prefer this to default parameters in some cases as it is more explicit).

        if l>beat*2:
            iPitch=(pitch+pPitch)/2.0
            pos=beat/8
            signal1=sf.Slide((0,iPitch),(pos,pitch),(l,pitch))
            signal2=sf.Slide((0,iPitch),(pos,pitch*2),(l,pitch*2.02))
            signal3=sf.Slide((0,iPitch),(pos,pitch*3),(l,pitch*3.03))
        else:
            signal1=sf.SineWave(l,pitch)
            signal2=sf.SineWave(l,2*pitch*1.003)
            signal3=sf.SineWave(l,3*pitch*1.005)

The above piece of code is interesting as it alters note articulation based on note length. Short notes will have the same pitch throughout. However, longer notes will have a short 'slur' or 'slide' between them by bending the start of the next note to the average to the two.

We can also see here that each note it made from 3 tones. However, what we hear in Valley is very much more harmonically rich than that.

        envelope= sf.NumericShape(
                 (0,0),
                 (a,1),
                 (a+d,0.75),
                 (a+d+s,0.25),
                 (a+d+s+r,0)
        )
        
        sat=(20-pitch/1000)
        if sat<1:
            sat=1
                        
        def doSat(sigIn):
            temp=sf.NumericVolume(sf.Multiply(sigIn,envelope),sat)
            return sf.Normalise(sf.Clean(sf.Saturate(temp)))

        signal=sf.Mix(
            doSat(signal1),
            sf.DB_6(doSat(signal2)),
            sf.DB_15(doSat(signal3))
        )

The addition of harmonic complexity is done with the above code. First we create a standard ADSR envelope. Then we work out an number related to pitch which will be used to control the amount of harmonic richness to add. The reason to base it on pitch is that physical instruments tend to have more harmonic in their lower registers and so mimicking this mathematically produces sounds which are more interesting to listen to. 

        @Override
        public double getSample(int index)
        {
            double x = getInputSample(index);
            double y = x >= 0 ? x / (x + 1) : x / (1 - x);
            return y;

        }

The above is the Java (remember that audio processing heavy work in Sonic Field is done in Java not Python). It is a rather magical formula because it is so simple and yet so effective. It simply forces any value in the incoming signal to fit between 1 and -1. It does this by asymptotically crushing the signal as it approaches 1 or -1.
X and X/(X+1)
I came up with the idea of using this as a audio processor (strictly a wave shaper) one evening whilst working in Cambridge a couple of years ago. It is so simple and yet so effective, I could not believe my luck in thinking of it (I was dreaming of complex polynomials and logs and things). We can see that to begin with (X near 0) X and X(X+1) are similar but as X grows the processed wave bends over to approach 1 (and -1 for the X/(1-X) version for negative numbers). As a result, the wave form is distorted to become closer to a square wave. This add odd harmonics. The larger the amplitude of the incoming wave the more the distortion and the greater the addition of harmonics. A sine wave a large magnitude entering the wave shaper will come out as a rounded square wave.

The effect of greater amplitude -> greater harmonic content also mimics natural instruments. By using the saturate processor after the application of an envelope we make the harmonic content follow the envelope just as it does with - for example - a Sax where the louder the note the 'brighter' it sounds. 

The link between pitch and harmonic content is performed the same way:

        def doSat(sigIn):
            temp=sf.NumericVolume(sf.Multiply(sigIn,envelope),sat)
            return sf.Normalise(sf.Clean(sf.Saturate(temp)))

We use the saturation processor on the output result of setting the over all volume (amplitude) of the signal by the variable sat. sat is bigger for lower notes and so amplitude will be bigger and so the harmonic content larger.

Note:

  1. sf.Clean removes higher frequencies using a special finite impulse response filter to avoid build up of those frequencies. This prevents further processing causing harmonics of high frequencies getting so high the alias.
  2. sf.Normalise removes any DC from the signal and sets the maximum excursion to 1 by scaling the whole signal. By DC I mean, the sum of all the samples in the signal is the DC component. Build up of DC is a constant problem in digital processing which does not happen in analogue as the capacitors used to link circuits automatically remove all DC.

Finally for this section: why three signals? I leave that up to you to think about.

Next - resonance and body sounds


        hf=sf.Clip(sf.NumericVolume(signal,3))
    
        r1=fixSizeSat(sf.RBJPeaking(hf,pitch*1.3,0.5,85))
        r2=fixSizeSat(sf.RBJPeaking(hf,pitch*2.1,0.5,85))
        r3=fixSizeSat(sf.RBJPeaking(hf,pitch*2.9,0.5,85))

When a real instrument is played it shakes. For strings the shanking in part of the projecting of the sound. For brass, it produces a percussive timbre on top of the fundamental sound of the instrument. In the patch fragment above, I am attempting to mimc the effect of such shaking. This is done by passing the signal into infinite impulse response filters which are set to near resonance. Any signal passed into them which contains frequencies near to their resonant frequency will cause them to ring.

The 'near to their resonant frequency' is important. They will not resonate if signal is passed in which does not contain the required references. We can see here that I have not set their resonant frequencies to those of the notes so how will they resonate? The trick is in the sf.Clip. This hard limits signals so that if a sample goes above 1 it is set to 1 and if it goes below -1 it is set to -1. That hard limiting sprays frequencies all over the spectrum (think electric guitar fuzz). The resonators can pick up some of that sprayed frequency and resonate form it. Because the clipping will be dependant of amplitude of the signal the resonance will as well, which again, is the way physical instruments tend to work.

In my next post I will discuss compression, reverberation and well the Sonic Field - that for which Sonic Field was first created.

Tuesday, 18 February 2014

Valley Of The Sythons - Complex Music Generated From Python

And so it continues - Python has transformed Sonic Field.

SFPL was deliberately simple to try and force Sonic Field to be declarative. However, I have become ever more interested in the details of sound when declaration no longer cuts it. Valley Of The Sythons  breaks new ground for me because the articulation of each of the notes is dynamically altered to fit in the context of the note before it.

The approach is simple in this case as it is a first step in with approach. However, the logic which add a slur (sorry for the language, but it started pout as a wind player so that is the word for me) from the direction of the previous note is a note is beyond two beats in length.

In a similar vein, the constrained random walk logic in Valley Of The Sythons is more sophisticated than in my previous pieces (Hall Of Chimes for example).

An unexpected benefit of using Jython is the control language for Sonic Field has been better memory management and more control over the memory/thread balance. Because garbage collection of references in Python is reference countered, and thus deterministic in execution graph space,  Synthon tends to clean up after it's self better than SFPL did. This also tends to mean restricting the pool size of the executor reduces memory pressure in a predictable way.

Here is Valley Of The Sythons:

Valley Of The Sythons

And here for comparison is Hall Of Chimes:


Hall Of Chimes

Sunday, 16 February 2014

Parallel Processing In Sython

Pythons are water creatures (or is that Anacondas?) Oh well - here is a random picture of a river.
CPython is not great at multi-threading - but Jython on the other hand is BRILLIANT at multi-threading.

This all goes back to Sonic Field which before porting to Jython used a command called Do. This took a closure, executed it and forwarded a future. A Do task would execute in parallel to the rest of the program until such time as it's result was required. By making retrieving the result from a future part of the dereferencing of variables, the Do semantics approach was dead easy to use.
  1. Define a task just like any other piece of code, just wrap it in a closure
  2. Pass the closure to the executer
  3. Return a future
  4. Automatically wait for the future to return its result when we need it, not when we created it
Well, pretty much the same thing can be done in Jython using the standard Executor/Future features of the JDK.  Here is the guts of the required Python code (which heavily borrows form the Jython docs - thanks).

import threading
import time
from java.util.concurrent import Executors, TimeUnit
from java.util.concurrent import Callable

SF_MAX_CONCURRENT = 16
SF_POOL = Executors.newFixedThreadPool(SF_MAX_CONCURRENT)

class sf_callable(Callable):
    def __init__(self,toDo):
        self.toDo=toDo
        
    def call(self):
        return self.toDo()

def sf_do(toDo):
    task=sf_callable(toDo)
    return SF_POOL.submit(task)

from java.util.concurrent import TimeUnit

def shutdown_and_await_termination(pool, timeout):
    pool.shutdown()
    try:
        if not pool.awaitTermination(timeout, TimeUnit.SECONDS):
            pool.shutdownNow()
            if (not pool.awaitTermination(timeout, TimeUnit.SECONDS)):
                print >> sys.stderr, "Pool did not terminate"
    except InterruptedException, ex:
        # (Re-)Cancel if current thread also interrupted
        pool.shutdownNow()
        # Preserve interrupt status
        Thread.currentThread().interrupt()
        
def shutdownConcurrnt():
    shutdown_and_await_termination(SF_POOL, 5)

The key is that all functions in Jython are actually closures which can be executed. To take advantage of this we wrap the closure in a Callable object. If we just execute the closure directly it is interpreted as a Runnable and the .get of the Future returns null. Wrapping in Callable solves this.

class sf_callable(Callable):
    def __init__(self,toDo):
        self.toDo=toDo
        
    def call(self):
        return self.toDo()

Now we make a simple function which does the wrapping and creates the future:

def sf_do(toDo):
    task=sf_callable(toDo)
    return SF_POOL.submit(task)

sf_do will return a Future object. To get the result of the task being executed we need to call .get() on the Future. That can be done by hand in Python. For the Sonic Field code, all objects coming in from Python into the Java audio processor code go through a specialise casting class. This simply check to Future objects and calls .get() on them. Thus, the conversion from Futures to their returned values is transparent. This is also the place that the transparent swapping in of swapped out audio signals happens.

    public static Object checkAutoTranslation(Object o) throws SFPL_RuntimeException
    {
        if (o instanceof SFMemoryManager) try
        {
            return ((SFMemoryManager) o).readInObject();
        }
        catch (ClassNotFoundException | IOException e)
        {
            throw new SFPL_RuntimeException(Messages.getString("Caster.6"), e); //$NON-NLS-1$
        }

        if (o instanceof FutureTask)
        {
            FutureTask doer = (FutureTask) o;
            try
            {
                Object d = doer.get();
                return checkAutoTranslation(d);
            }
            catch (Throwable t)
            {
                throw new SFPL_RuntimeException(t);
            }
        }

        if (o == null)
        {
            throw new SFPL_RuntimeException(Messages.getString("Caster.12"));             //$NON-NLS-1$
        }

        return o;
    }

So code can pass around values which might be signals, futures which produce signals, swapped out signals or even futures which will produce swapped out signals and all the necessary dereferencing happens transparently. OK - so how do we use this technique? Here is an example Sython script:

import random

execfile("patches/python/concurrent.py")

def fixSize(signal):
    mag=sf.MaxValue(signal)
    return sf.NumericVolume(signal,1.0/mag)
 
def fixSizeSat(signal):
     return fixSize(sf.Saturate(fixSize(signal)))
    
def saturatedNode(pitch,a,d,s,r,v):
    def saturateNode_():
        l=a+d+s+r
        signal1=sf.SineWave(l,pitch)
        signal2=sf.SineWave(l,2*pitch*1.003)
        signal3=sf.SineWave(l,3*pitch*1.005)
        envelope= sf.NumericShape(
                 (0,0),
                 (a,1),
                 (a+d,0.75),
                 (a+d+s,0.25),
                 (a+d+s+r,0)
        )
        sat=(20-pitch/1000)
        if sat<1:
            sat=1
            
        def doSat(sigIn):
            temp=sf.NumericVolume(sf.Multiply(sigIn,envelope),sat)
            return sf.Normalise(sf.Clean(sf.Saturate(temp)))
        
        signal=sf.Mix(
            doSat(signal1),
            sf.DB_6(doSat(signal2)),
            sf.DB_15(doSat(signal3))
        )
        signal=fixSize(signal)
        hf=sf.Clip(sf.NumericVolume(signal,3))
    
        r1=fixSizeSat(sf.RBJPeaking(hf,pitch*1.3,0.5,75))
        r2=fixSizeSat(sf.RBJPeaking(hf,pitch*2.1,0.5,75))
        r3=fixSizeSat(sf.RBJPeaking(hf,pitch*2.9,0.5,75))
    
        signal=sf.Mix(
            sf.DB_6(signal),
            sf.DB_1(r1),
            sf.DB_4(r2),
            sf.DB_6(r3)
        )
        signal=sf.Clean(sf.NumericVolume(signal,v))
        envelope= sf.NumericShape(
                 (0,1),
                 (a+d+s+r-125,1),
                 (a+d+s+r,0)
        )
        print "Returing from do task"   
        return sf.Multiply(envelope,signal)
    return sf_do(saturateNode_)

all=[sf.Silence(100)]

for x in range(12, 32):
    signal=saturatedNode(math.pow(2,x*0.325),125,500,1000,2500,1)
    print "All:" + all.__str__() + " , " + "Signal: " + signal.__str__()
    all.append(signal)

all=sf.Concatenate(all)
random.seed(0.128)
all=sf.Normalise(all)
sf.WriteFile32((all,all),"temp/temp.wav")
shutdownConcurrnt()
Let's have a look at the definition of saturateNode
def saturatedNode(pitch,a,d,s,r,v):
    def saturateNode_():

I create a closure immediately which closes around the contents of the function. This means the closure can do exactly what the function would do with exactly all the same names; however, it can do it anywhere in time or space. We have taken the function and detached its work into a separate, self contained task.

return sf.Multiply(envelope,signal)
    return sf_do(saturateNode_)

At the return end of the function, I return from the closure as though it were the outer function and return sf_do(<>) from the outer function. This makes writing code to be executed asynchronously near effortless (just as it was in SFPL) but still using standard Python code. Warnings! There is a problem. Python has a floppy approach to closure. It does not quite close around values and it does not quite close around references and it certainly does not let you choose which. Like most things in Python, it does not seem possible to properly declare your intension or express it in the code, you just have to hack around it. No this case we need to realise that Python closes around names.

So, if we do anything which would cause the setting of a local variable, the variable will be reified.
n=4 def y():
    n=5
z=y
z()
print n

 z has the value of the closure y. We execute it and set n. In this case the result is 4 because n has been reified to a local variable. If we just dereference the variable and update the thing it references then we are accessing the external scope:

n=[]
def y():
    n.append("dog")
z=y
z()
print n[0]

This will yield dog because we are not creating a local variable it is accessing from the outer scope. This means we MUST NOT DO THIS in closures used for tasks. Because Python has no way of enforcing stuff at compile time (no compile time) we just need to be careful to reify all local variables and be patient if we get bugs by forgetting to do so :(

Saturday, 15 February 2014

Sython - sound from Python

A quick note that SFPL has been replaced with Python! 

All the processors are the same but the syntax is simply adding new classes and methods to Python via the Jython Java port.   As of just now - I have even gotten the Do (thing Execute/Future) semantics for multi-threading running

Whilst SF will never be 'main stream' I hope that it being based on Python will make it more approachable. It also opens up the possibility of very much more complex synthesis and processing options.

Note: how easy it is to perform processing in parallel:

sf_do(saturateNode_)

I will discuss closures and do semantics in the next post.

Here is an example of Sython:

import math
import random

execfile("patches/python/concurrent.py")

def fixSize(signal):
    mag=sf.MaxValue(signal)
    return sf.NumericVolume(signal,1.0/mag)
 
def fixSizeSat(signal):
     return fixSize(sf.Saturate(fixSize(signal)))
    
def saturatedNode(pitch,a,d,s,r,v):
    def saturateNode_():
        l=a+d+s+r
        signal1=sf.SineWave(l,pitch)
        signal2=sf.SineWave(l,2*pitch*1.003)
        signal3=sf.SineWave(l,3*pitch*1.005)
        envelope= sf.NumericShape(
                 (0,0),
                 (a,1),
                 (a+d,0.75),
                 (a+d+s,0.25),
                 (a+d+s+r,0)
        )
        sat=(20-pitch/1000)
        if sat<1:
            sat=1
            
        def doSat(sigIn):
            temp=sf.NumericVolume(sf.Multiply(sigIn,envelope),sat)
            return sf.Normalise(sf.Clean(sf.Saturate(temp)))
        
        signal=sf.Mix(
            doSat(signal1),
            sf.DB_6(doSat(signal2)),
            sf.DB_15(doSat(signal3))
        )
        signal=fixSize(signal)
        hf=sf.Clip(sf.NumericVolume(signal,3))
    
        r1=fixSizeSat(sf.RBJPeaking(hf,pitch*1.3,0.5,75))
        r2=fixSizeSat(sf.RBJPeaking(hf,pitch*2.1,0.5,75))
        r3=fixSizeSat(sf.RBJPeaking(hf,pitch*2.9,0.5,75))
    
        signal=sf.Mix(
            sf.DB_6(signal),
            sf.DB_1(r1),
            sf.DB_4(r2),
            sf.DB_6(r3)
        )
        signal=sf.Clean(sf.NumericVolume(signal,v))
        envelope= sf.NumericShape(
                 (0,1),
                 (a+d+s+r-125,1),
                 (a+d+s+r,0)
        )
        print "Returing from do task"   
        return sf.Multiply(envelope,signal)
    return sf_do(saturateNode_)

all=[sf.Silence(100)]

for x in range(12, 32):
    signal=saturatedNode(math.pow(2,x*0.325),125,500,1000,2500,1)
    print "All:" + all.__str__() + " , " + "Signal: " + signal.__str__()
    all.append(signal)

all=sf.Concatenate(all)
random.seed(0.128)
all=sf.Normalise(all)
sf.WriteFile32((all,all),"temp/temp.wav")
shutdownConcurrnt()

The above produces this waveform: