Saturday, 5 December 2015

Sythonic Unit Tests

I have finally started adding unit tests to Sython. The approach I am taking is to add a test for each not features I add or to test the fix to each new bug. I hope to add a bunch of other tests as I go along as well. It is just overwhelming to try and add good coverage tests from day one so I am going to take this bit by bit approach.

Also, I hope implement these tests in the unittest framework from standard Python. Right now I want a few additions to what that provides and have not had the time to mix the two things. But - at leasts tests are happening!

The tests so far have largely been motivated by my work on fast off heap memory access and the work stealing schedular.

Here is the current, brief test suit:

def assertEqual(msg,a,b,sig=None):
    if a!=b:
        print "Fail: ",msg,a,b
        if sig is not None:
            sf.WriteFile32([sig],"temp/"+msg+".wav")
    else:
        print "Pass: ",msg,a,b
        if sig is not None:
            -sig

def assertClose(msg,a,b,sig=None,ratio=1.0e-10):
    if (b==0 and abs(a)>ratio) or (b!=0 and abs(1.0-abs(a/b))>ratio):
        print "Fail: ",msg,a,b
        if sig is not None:
            sf.WriteFile32([sig],"temp/"+msg+".wav")
    else:
        print "Pass: ",msg,a,b
        if sig is not None:
            -sig

def simpleMix():
    signal=sf.Silence(10000)
    signal=sf.Realise(signal)
    other=+signal
    other=sf.DirectMix(1,other)
    signal=sf.Mix(other,signal)
    other=+signal
    other=sf.DirectMix(-2,other)
    signal=sf.Mix(other,signal)
    assertEqual("Simple-mix-magnitude",sf.Magnitude(+signal),0,signal)

def simpleGranulate():
    signalA=sf.SineWave(10000,1000)
    signalB=sf.MixAt(sf.Granulate(+signalA,128,0))
    assertClose("Simple-granulate-magnitude-a",sf.Magnitude(+signalB),sf.Magnitude(+signalA),+signalB)    
    signalB=sf.NumericVolume(signalB,-1)
    signal=sf.Mix(signalA,signalB)  
    assertClose("Simple-granulate-magnitude-b",sf.Magnitude(+signal),0,signal)

def realisedGranulate():
    signalA=sf.SineWave(10000,1000)
    all=[]
    for sig,at in sf.Granulate(+signalA,128,0):
        all.append((sf.Realise(sig),at))
    signalB=sf.MixAt(all)
    assertClose("Realised-granulate-magnitude-a",sf.Magnitude(+signalB),sf.Magnitude(+signalA),+signalB)    
    signalB=sf.Realise(sf.NumericVolume(signalB,-1))
    signal=sf.Mix(signalA,signalB)
    signal=sf.Realise(signal)
    assertClose("Realised-granulate-magnitude-b",sf.Magnitude(+signal),0,signal)

def simpleFFT():
    signalA=sf.SineWave(10000,1000)
    oldMag=sf.Magnitude(+signalA)
    signalB=sf.FrequencyDomain(+signalA)
    signalB=sf.TimeDomain(signalB)
    newMag=sf.Magnitude(+signalB)
    assertClose("Simple-FFT-magnitude-a",newMag,346.410161514,+signalB,1.0e-8);
    signalB=sf.NumericVolume(signalB,oldMag/newMag)
    signalB=sf.NumericVolume(signalB,-1)
    signalB=sf.Mix(signalA,signalB)  
    assertClose("Simple-FFT-magnitude",sf.Magnitude(+signalB),0,signalB)    

def realisedFFT():
    signalA=sf.SineWave(10000,1000)
    oldMag=sf.Magnitude(+signalA)
    signalB=sf.Realise(sf.FrequencyDomain(sf.Realise(+signalA)))
    interMag=sf.Magnitude(+signalB)
    assertClose("Realised-FFT-magnitude-i",interMag,489.897948556,+signalB,1.0e-8);
    signalB=sf.Realise(sf.TimeDomain(signalB))
    newMag=sf.Magnitude(+signalB)
    assertClose("Realised-FFT-magnitude-a",newMag,346.410161514,+signalB,1.0e-8);
    signalB=sf.NumericVolume(signalB,oldMag/newMag)
    signalB=sf.NumericVolume(signalB,-1)
    signalB=sf.Mix(signalA,signalB)  
    assertClose("Realised-FFT-magnitude",sf.Magnitude(+signalB),0,signalB,1.0e-8)

def parallel():
    @sf_parallel
    def p1():
       return sf.SineWave(1000,1000)
    t=sf.Mix(p1(),p1(),p1(),p1())
    assertEqual("Parallel-mix-length",sf.Length(+t),1000,+t)
    s=sf.SineWave(1000,1000)
    assertClose("Parallel-mix-magnitude",sf.Magnitude(s)*4,sf.Magnitude(+t),t)
    
    @sf_parallel
    def p2(x):
       return sf.SineWave(1000,x)
    t=sf.Mix(p2(1000),p2(1000),p2(1000),p2(1000))
    s=sf.SineWave(1000,1000)
    assertClose("Parallel-mix-magnitude-1-arg",sf.Magnitude(s)*4,sf.Magnitude(+t),t)
    
    @sf_parallel
    def p3(x,y):
       return sf.SineWave(y,x)
    t=sf.Mix(p3(1000,1000),p3(1000,1000),p3(1000,1000),p3(1000,1000))
    s=sf.SineWave(1000,1000)
    assertClose("Parallel-mix-magnitude-2-args",sf.Magnitude(s)*4,sf.Magnitude(+t),t)
    
    t=sf.Mix(p3(x=1000,y=1000),p3(x=1000,y=1000),p3(x=1000,y=1000),p3(x=1000,y=1000))
    s=sf.SineWave(1000,1000)
    assertClose("Parallel-mix-magnitude-2-kargs",sf.Magnitude(s)*4,sf.Magnitude(+t),t)
    
    @sf_parallel
    def p4(x,y):
       return sf.SineWave(y,x),sf.SineWave(y,x)
    t,r=p4(1000,1000)
    assertClose("Parallel-mix-magnitude-2-itter",sf.Magnitude(r),sf.Magnitude(t))
    
    # Check we can plus and minute on a future.
    t=p3(1000,1000)
    +t
    -t
    assertEqual("Reference-count",t.getReferenceCount(),1)

simpleMix()
simpleGranulate()
realisedGranulate()
simpleFFT()
realisedFFT()
parallel()

Wednesday, 2 December 2015

Parallel Decorator and Jython 2.7

Announcement:


Sonic Field now works with Jython 2.7.

Alongside this is the new work stealing schedular.

Also, today I have pushed an enhancement which allows parallel execution via an annotation/decorator (which required Jython 2.7):
    
Here is the decorator:

# An experimental decorator approach equivalent to sf_do
class sf_parallel(object):

    def __init__(self,func):
        self.func=func

    def __call__(self,*args, **kwargs):
        def closure():
            return self.func(*args, **kwargs
        return sf_superFuture(closure)

For example - what would have required sf_do being called on a manually created closure can now look like this:

@sf_parallel
def granularReverb(signal,ratio,delay,density,length=50,stretch=1,vol=1):
    print "Granular reverb: ratio:",ratio," delay:",delay," density",density," length:",length," stretch:",stretch," volume:",vol
    out=[]
    for grain in sf.Granulate(signal,length,10):
        (signal_i,at)=grain
        signal_i=sf.Realise(signal_i)
        signal_i=sf.Realise(sf.DirectRelength(signal_i,ratio-0.01+(0.02*random.random())))
        for x in range(0,density):
            out.append(
                (
                    +signal_i,
                    int((at + (random.random()+random.random())*delay)*stretch)
                )
            )
        -signal_i
  
    out=sf.Realise(sf.MixAt(out))
    out=sf.Realise(sf.NumericVolume(out,vol))
    return out

Just annotate a method sf_parallel and it will happen in parallel!

Not that parallel methods should have no side effects other than the manipulation of resource counts.  I must write this up in greater detail sometime.

Sunday, 29 November 2015

Work Steeling, Lazy, Parallel Task Schedular in Jython

Performance, and we are cooking.
Sonic Field is heavily parallel, but I have always wanted to make the used of parallelism really easy. 

The schedular has gone though a lot of changes over the years; this new work steeling feature, I believe, really fixes a lot of the deficiencies in the previous design without adding and complexity to the user experience.

So let's start by an explanation of the schedular in general. This is a task based lazy schedular. We create a closure around a 'task' and then return a 'SuperFuture' for it. The thing about SuperFuture is that it does not do anything until it has to. Unlike some Future based task modules, this one does not compute stuff as it turns up; rather it computes stuff as it is needed. 

When I restarted work on this a few days again I realised I had forgotten how the previous, simpler version worked. To avoid this mistake again, I have very, very heavily commented the code. So, rather than duplicate everything, I leave the code and comments to explain the rest.

Note - this code is all AGPL 3 - please respect copyright.

# For Copyright and License see LICENSE.txt and COPYING.txt in the root directory
import threading
import time
from java.util.concurrent import Callable, Future, ConcurrentLinkedQueue, \
                                 ConcurrentHashMap, Executors, TimeUnit
from java.util.concurrent.locks import ReentrantLock

from java.lang import System
from java.lang import ThreadLocal
from java.lang import Thread
from java.util import Collections
from java.util.concurrent import TimeUnit
from java.util.concurrent.atomic import AtomicLong, AtomicBoolean

"""
Work Stealing Lazy Task Scheduler By Dr Alexander J Turner

Basic Concepts:
===============

- Tasks are closures. 
- The computation of the task is called 'execution'.
- They can be executed in any order* on any thread.
- The data they contain is immutable.
- Making a task able to be executed is called submission.
- Execution is lazy**.
- Tasks are executed via a pool of threads and optionally one 'main' thread.
- Ensuring the work of a task is complete and acquiring the result (if any)
  is called 'getting' the task.

Scheduling Overview:
====================

* Any order, means that subitted tasks can be executed in any order though there
can be an order implicit in their dependencies.

IE taskA can depend on the result of taskB. Therefore.
- taskA submits taskA for execution.
- taskB submits taskC, taskD and taskE for execution.
- taskB now 'gets' the results of taskC, taskD and taskE

In the above case it does not matter which order taskC, taskD and taskE are
executed.

** Lazy execution means that executing a task is not done always at task 
submission time. Tasks are submitted if one of the following conditions is 
met.
- the maximum permitted number of non executing tasks has been reached. See
  'the embrace of meh' below.
- the thread submitting the task 'gets' and of the tasks which have not yet
  been submitted.
- a thread would is in the process of 'getting' a task but would block waiting
  for the task to finish executing. In this results in 'work stealing' (see
  below) where any other pending tasks for any threads any be executed by the
  thread which would otherwise block.

Embrace of meh
==============

Meh, as in a turm for not caring or giving up. This is a form of deadlock in
pooled future based systems where the deadlock is causes by a circular 
dependency involving the maximum number of executors in the pool rather than a
classic mutex deadlock. Consider this scenario:

- There are only two executors X and Y
- taskA executes on X
- taskA sumbits and then 'gets' taskB
- taskB executes on Y
- taskB submits and then 'gets' taskC
- taskC cannot execute because X and Y are blocked
- taskB cannot progress because it is waiting for taskC
- taskA cannot progress because it is waiting for taskB
- the executors cannt free to execute task as they are blocked by taskA and 
  taskB
- Meh

The solution used here is a soft upper limit to the number of tasks submitted
to the pool of executors. When that upper limit is reached, new tasks are 
not submitted for asynchronous execution but are executed immediately by the
thread which submits them. This prevents exhaustion of the available executors
and therefore prevents the embrace of meh.

Exact computation of the number of running executors is non tricky with the
code used here (java thread pools etc). Therefore, the upper limit is 'soft'.
In other words, sometimes more executors are used than the specified limit. This
is not a large issue here because the OS scheduler simply time shares between 
the executors - which are in fact native threads.

The alternative of an unbounded upper limit to the number of executors in not
viable; every simple granular synthesis or parallel FFT work can easily 
exhaust the maximum number of available native threads on modern machines. Also,
current operating systems are not optimised for time sharing between huge
numbers of threads. Finally, there is a direct link between the number of
threads running and the amount of memory used. For all these reasons, a soft
limited thread pool with direct execution works well.

Work Stealing
=============
Consider:
- taskA threadX submits taskB
- taskA threadX gets taskB
- taskB threadY submits taskC and taskD
- taskB threadY gets taskC
- there are no more executors so taskC is executed on threadY
- at this point taskC is pending execution and taskA threadX is waiting for the
- result of taskB on threadY.
- threadX can the stop waiting for taskB and 'steal' taskC.

Note that tasks can be executed in any order on any thread.

"""

# CONSTANTS AND CONTROL VARIABLES
# ===============================

# The maximum number of executors. Note that in general the system
# gets started by some 'main' thread which is then used for work as 
# well, so the total number of executors is often one more than this 
# number. Also note that this is a soft limit, synchronisation between
# submissions for execution is weak so it is possible for more executors
# to scheduled.
SF_MAX_CONCURRENT = int(System.getProperty("sython.threads"))

# Tracks the number of current queue but not executing tasks
SF_QUEUED         = AtomicLong()

# Tracks the number of executors which are sleeping because they are blocked
# and are not currently stealing work
SF_ASLEEP         = AtomicLong()

# Marks when the concurrent system came up to make logging more human readable
SF_STARTED        = System.currentTimeMillis()

# Causes scheduler operation to be logged
TRACE             = str(System.getProperty("sython.trace")).lower()=="true"

# A thread pool used for the executors 
SF_POOL    = Executors.newCachedThreadPool()

# A set of tasks which might be available for stealing. Use a concurrent set so
# that it shares information between threads in a stable and relatively 
# efficient way. Note that a task being in this set does not guarantee it is
# not being executed. A locking flag on the 'superFuture' task management
# objects disambiguates this to prevent double execution. 
SF_PENDING = Collections.newSetFromMap(ConcurrentHashMap(SF_MAX_CONCURRENT*128,0.75,SF_MAX_CONCURRENT))

# EXECUTION
# =========

# Define the logger method as more than pass only is tracing is turned on
if TRACE:
    # Force 'nice' interleaving when logging from multiple threads
    SF_LOG_LOCK=ReentrantLock()
    print "Thread\tQueue\tAsleep\tTime\tMessage..."
    def cLog(*args):
        SF_LOG_LOCK.lock()
        print "\t".join(str(x) for x in [Thread.currentThread().getId(),SF_QUEUED.get(),SF_ASLEEP.get(),(System.currentTimeMillis()-SF_STARTED)] + list(args))
        SF_LOG_LOCK.unlock()
else:
    def cLog(*args):
        pass

cLog( "Concurrent Threads: " + SF_MAX_CONCURRENT.__str__())
    
# Decorates ConcurrentLinkedQueue with tracking of total (global) number of
# queued elements. Also remaps the method names to be closer to python lists
class sf_safeQueue(ConcurrentLinkedQueue):
    # Note that this is actually the reverse of a python pop, this is actually
    # equivalent to [1,2,3,4,5].pop(0).
    def pop(self):
        SF_QUEUED.getAndDecrement()
        r = self.poll()
        SF_PENDING.remove(r)
        return r
    
    def append(self, what):
        SF_QUEUED.getAndIncrement()
        SF_PENDING.add(what)
        self.add(what)

# Python implements Callable to alow Python closers to be executed in Java
# thread pools
class sf_callable(Callable):
    def __init__(self,toDo):
        self.toDo=toDo
    
    # This method is that which causes a task to be executed. It actually
    # executes the Python closure which defines the work to be done
    def call(self):
        cLog("FStart",self.toDo)
        ret=self.toDo()
        cLog("FDone")
        return ret

# Holds the Future created by submitting a sf_callable to the SF_POOL for
# execution. Note that this is also a Future for consistency, but its work
# is delegated to the wrapped future. 
class sf_futureWrapper(Future):
    def __init__(self,toDo):
        self.toDo   = toDo

    def __iter__(self):
        return iter(self.get())
    
    def isDone(self):
        return self.toDo.isDone()
    
    def get(self):
        return self.toDo.get()

# Also a Future (see sf_futureWrapper) but these execute the python closer
# in the thread which calls the constructor. Therefore, the results is available
# when the constructor exits. These are the primary mechanism for preventing
# The Embrace Of Meh.
class sf_getter(Future):
    def __init__(self,toDo):
        self.toDo=toDo
        cLog("GStart",self.toDo)
        self.result=self.toDo()
        cLog("GDone")

    def isDone(self):
        return True

    def get(self):
        return self.result

# Queues of tasks which have not yet been submitted are thread local. It is only
# when a executor thread would become blocked that we go to work stealing. This
# class managed that thread locallity.
# TODO, should this, can this, go to using Python lists rather than concurrent
# linked queues.
class sf_taskQueue(ThreadLocal):
    def initialValue(self):
        return sf_safeQueue()

# The thread local queue of tasks which have not yet been submitted for
# execution
SF_TASK_QUEUE=sf_taskQueue()

# The main coordination class for the schedular. Whilst it is a future
# it actually deligates execution to sf_futureWrapper and sf_getter objects
# for synchronous and asynchronous operation respectively
class sf_superFuture(Future):

    # - Wrap the closure (toDo) which is the actual task (work to do)
    # - Add that task to the thread local queue by adding self to the queue
    #   thus this object is a proxy for the task.
    # - Initialise a simple mutual exclusion lock.
    # - Mark this super future as not having been submitted for execution. This
    #   is part of the mechanism which prevents work stealing resulting in a
    #   task being executed twice.
    def __init__(self,toDo):
        self.toDo=toDo
        queue=SF_TASK_QUEUE.get()
        queue.append(self)
        self.mutex=ReentrantLock()
        self.submitted=False

    # Used by work stealing to submit this task for immediate execution on the
    # the executing thread. The actual execution is delegated to an sf_getter
    # which executes the task in its constructor. This (along with submit) use
    # the mutex to manage the self.submitted field in a thread safe way. No
    # two threads can execute submit a super future more than once because
    # self.submitted is either true or false atomically across all threads. 
    # The lock has the other effect of synchronising memory state across cores
    # etc.
    def directSubmit(self):
        # Ensure this cannot be executed twice
        self.mutex.lock()
        if self.submitted:
            self.mutex.unlock()
            return
        self.submitted=True
        self.mutex.unlock()
        # Execute
        self.future=sf_getter(self.toDo)
    
    # Normal (non work stealing) submition of this task. This might or might not
    # result in immediate execution. If the total number of active executors is
    # at the limit then the task will execute in the calling thread via a
    # sf_getter (see directSubmit for more details). Otherwise, the task is 
    # submitted to the execution pool for asynchronous execution.
    #
    # It is important to understand that this method is not called directly
    # but is called via submitAll. submitAll is the method which subits tasks
    # from the thread local queue of pending tasks.
    def submit(self):
        # Ensure this cannot be submitted twice
        self.mutex.lock()
        if self.submitted:
            self.mutex.unlock()
            return
        self.submitted=True
        self.mutex.unlock()
        
        # See if we have reached the parallel execution soft limit
        count=SF_POOL.getActiveCount()
        cLog("Submit")
        if count<SF_MAX_CONCURRENT:
            # No, so submit to the thread pool for execution
            task=sf_callable(self.toDo)
            self.future=sf_futureWrapper(SF_POOL.submit(task))
        else:
            # Yes, execute in the current thread
            self.future=sf_getter(self.toDo)
        cLog("Submitted")

    # Submit all the tasks in the current thread local queue of tasks. This is
    # lazy executor. This gets called when we need results. 
    def submitAll(self):
        queue=SF_TASK_QUEUE.get()
        while(len(queue)):
            queue.pop().submit()

    # The point of execution in the lazy model. This method is what consumers
    # of tasks call to get the task executed and retrieve the result (if any).
    # This therefore acts as the point of synchronisation. This method will not
    # return until the task wrapped by this super future has finished executing.
    #
    # A note on stealing. Consider that we steal taskA. TaskA then invokes
    # get() on taskB. taskB is not completed. The stealing can chain here; 
    # whilst waiting for taskB to complete the thread can just steal another
    # task and so on. This is why we can use the directSubmit for stolen tasks.  
    def get(self):
        cLog( "Submit All")
        # Submit the current thread local task queue
        self.submitAll()
        cLog( "Submitted All")
        t=System.currentTimeMillis()
        c=1
        # There is a race condition where by the submitAll has been called
        # which recursively causes another instance of get on this super future
        # which results in there being no tasks to submit but we get to this 
        # point before self.future has been set. This tends to resolve itself
        # very quickly as the empty calls to submit all do not do very much work 
        # so the origianl progresses and sets the future. Rather than a complex
        # and potentially brittle locking system, we just spin waiting for the
        # future to be set. This works fine on my Mac as it only ever seems to 
        # spin once, so the cost is pretty much the same as a locking approach
        # basically, one quantum. If this starts to spin a lot in the future
        # a promise/future approach could be used. 
        while not hasattr(self,"future"):
            c+=1
            Thread.yield()
        t=System.currentTimeMillis()-t
        cLog( "Raced: ", c ,t)
        
        # This is where the work stealing logic starts
        # isDone() tells us if the thread would block if get() were called on
        # the future. We will try to work steal if the thread would block so as
        # not to 'waste' the thread. This if block is setting up the
        # log/tracking information
        if not self.future.isDone():
            SF_ASLEEP.getAndIncrement();
            cLog("Sleep")
            nap=True
        else:
            nap=False
            cLog("Get")
        # back control increasing backoff of the thread trying to work steal.
        # This is not the most efficient solution but as Sonic Field tasks are
        # generally large, this approach is OK for the current use. We back of
        # between 1 and 100 milliseconds. At 100 milliseconds we just stick at
        # polling every 100 milliseconds.
        back=1
        # This look steal work until the get() on the current task will 
        # not block
        while not self.future.isDone():
            # Iterate over the global set of pending super futures
            # Note that the locking logic in the super futures ensure 
            # no double execute.
            it=SF_PENDING.iterator()
            while it.hasNext():
                try:
                    # reset back (the back-off sleep time)
                    back=1
                    toSteal=it.next()
                    it.remove()
                    cLog("Steal",toSteal.toDo)
                    # Track number of tasks available to steal for logging
                    SF_ASLEEP.getAndDecrement();
                    # The stollen task must be performed in this thread as 
                    # this is the thread which would otherwise block so is
                    # availible for further execution
                    toSteal.directSubmit()
                    # Now we manage state of 'nap' which is used for logging
                    # based on if we can steal more (i.e. would still block)
                    # Nap also controls the thread back off
                    if self.future.isDone():
                        nap=False
                    else:
                        nap=True
                        SF_ASLEEP.getAndIncrement();
                except Exception, e:
                    # All bets are off
                    cLog("Failed to Steal",e.getMessage())
                    # Just raise and give up
                    raise
            # If the thread would block again or we are not able to steal as
            # nothing pending then we back off. 
            if nap==True:
                if back==1:
                    cLog("Non Pending")
                Thread.sleep(back)
                back+=1
                if back>100:
                    back=100
        if nap:
            SF_ASLEEP.getAndDecrement();
        # To get here we know this get will not block
        r = self.future.get()
        cLog("Wake")
        # Return the result if any
        return r

    # If the return of the get is iterable then we delegate to it so that 
    # this super future appears to be its embedded task
    def __iter__(self):
        return iter(self.get())
            
    # Similarly for resource control for the + and - reference counting 
    # semantics for SF_Signal objects
    def __pos__(self):
        obj=self.get()
        return +obj

    def __neg__(self):
        obj=self.get()
        return -obj

# Wrap a closure in a super future which automatically 
# queues it for future lazy execution
def sf_do(toDo):
    return sf_superFuture(toDo)
    
# An experimental decorator approach equivalent to sf_do
def sf_parallel(func):
    def inner(*args, **kwargs): #1
        return func(*args, **kwargs) #2
    return sf_do(inner)

# Shut the execution pool down. This waits for it to shut down
# but if the shutdown takes longer than timeout then it is 
# forced down.
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:
        pool.shutdownNow()
        Thread.currentThread().interrupt()

# The default shutdown for the main pool  
def shutdownConcurrnt():
    shutdown_and_await_termination(SF_POOL, 5)

Sunday, 1 November 2015

Scary Halloween From Sonic Field

I am a very lucky person! My piece of music 'Fever' was plaid at a a Danish Halloween event for thousands of people to hear.

Many, many thanks to Henrik Graham who, most politely, asked permission to use my piece Fever for the music for a presentation at Halloween in Måløv which is a Danish halloween festival help at a park in Måløv Denmark.

There is the rather excited email I received from Henrik last night (the night of halloween).

" Hi Alexander Just want to drop you a note thanking you for the opportunity to share your music with 5.000 guest at Halloween in Måløv. It created an awesome ‘sound-installation’ in the park. Everyone I talked to thought it was spot on and provided the experience of parading through the park a new spooky dimension.

I’ll let you know once the pictures of the 2015 event is posted on the website: http://www.halloweenimaaloev.dk/ TV2 (one of 2 major Danish broadcasting companies) visited and made a TV spot that was broadcasts yesterday – you can actually hear your music at the beginning and at the very end of the clip 43-45 sec. just before the end at 45 sec. See and listen here: http://www.tv2lorry.dk/artikel/285660

If you interested in the parade here is the link to the programme: http://www.halloweenimaaloev.dk/assets/halloweenkort_ruten_2015_web.pdfthe The PA was setup to cover post 24 to 32. Below I’ve marked the PA setup with red dots app. 4kw Turbosound setup.

The blue dots maks the route through the park.
In a sec I’ll be emailing the organizer asking them to pay tribute to you by posting a thank you note on their website. Would you like anything special or will something like “Thank you to Dr. Alexander J Turner…” do? All the Best Henrik Graham "


Here is Fever on youtube:


Also see see it and music I have made on my 'Greatest Hits' page (note the irony).

Friday, 3 April 2015

Midi processing for fretted string and keyboard

Just because Sonic Field has not been advancing in the synthesis systems recently does not imply it is standing still. My current efforts have all been in midi (just about).

Working with the Waldorf Pulse 2 has been altogether too much fun! However, it is a mono synth and I want to make polyphonic, multi instrument sounds. The simple solution to this is over dubbing. Here we play each sound separately through the synth and then mix the result up. That is a very well known and understood technique. However, there is more to it than we might think.

Consider just one instrument, which we might think of as monophonic. The flute is a good example. We might believe that the flute plays one note at once. The truth is that the sounds it makes are more intermixed and as the player changes notes on a flute the pitch does not smoothly move from one note to the next like glide on a synthesiser. Even if we are not going for a perfect flute synthesis, to play flute music can still require overdubbing. So far I have found that playing alternate notes and overdubbing the result seems to work very nicely.


This piece synthesises flute and guitar using
 the midi techniques discussed here.

Then we have those instruments which can indeed play more than one note at once. The guitar and harpsichord (piano etc) are all interesting examples. To keep things simple, let us imagine we have a single manual harpsichord. If we play this sequence:

A4 G4 C5 G3 A4

Each note consists, in a simple minded way, of an ADSR envelope where A is almost infinitely short (actually, a tiny bit of attack slop can sound a bit better as a quill pluck is not really a click).

Something quite piano like:

|   /\ 
|   | \
|   |  ------------------
|   |                    \
|   |                     \
+---------------------------
    ADDSSSSSSSSSSSSSSSSSSRR

More of a harpsichord or classical guitar

|   /\ 
|   | -
|   |  \
|   |   -   
|   |    --\
+------------
    ADDDDDDR

Even with the best efforts to damp a string, there is still some release. The body, sounding board and reverberation of the rest of the instrument will sound the string a little after release. So we have two different ways in which notes will overlap. These instruments (with the exception of the guitar - which I am still working on) can play any notes overlapping apart from the same note. However, we only have so many figures or strings so for the harpsichord or piano we can play a maximum of 10 notes at once and the guitar 6.

Now we have the basis for our overdub approach, we can rotate through up to 10 different recording. If two notes are not overlapping, we do not need to rotate the second note to a new recording. But, what the midi says is overlapping is not quite true due to the release phase, we need more overlap than we might think. This means we might need more recordings. However, we can reuse recordings more efficiently when the same note is played twice in close succession.

This is what this piece of Sonic Field code is attempting to do. I used it with 8 recordings to perform the guitar int eh Albinoni Sonata For Guitar and Flute above. As I said, I am still working on the guitar because both the sound and overdub techniques are still some what closer to the harpsichord than guitar.

# Full polyphony
# Release sets the minimum time between notes on a
# particular voice unless those notes are the same
# key, which alows for the sound to go through its
# release phase. Voices sets the absoulte maximum
# number of voices which will be used before falling
# back to round robbin. 12 is a popular 'as big as you 
# need' value for polyphonic synths so I have picked
# that as the default. Release is in ticks.
#
# At the moment it is not clever enough to take account
# of tempo change events
def playNotes(release=120,sameRelease=60,voices=12,pan=True):
    chans=[]
    for v in range(0,voices):
        chans.append([])
    
    voice=0
    vs=[]
    rota=0
    for event in midis[midiNo]:
        if Midi.isNote(event):
            notFound=True
            for v in range(0,2*(voices+1)):
                second=v>voices                
                v+=rota
                v=v%voices
                chan=chans[v]
                if chan:
                    top=chan[-1]['tick-off']
                    next=event['tick']
                    if chan[-1]['key']==event['key']:
                        if top+sameRelease<next:
                            notFound=False
                            rota+=1                        
                            break
                    if top+release<next and second:
                        rota+=1
                        notFound=False
                        break
                else:
                    notFound=False
                    break

            if notFound:
                v=rota
                v=v%voices
                rota+=1
            vs.append(v)
            chans[v].append(event)
    print "Voicing:",vs

    for chan in chans:
        print chan    

    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'])
    for chan in chans:
        if chan:

            events=[]
            for event in chan:
                ev1=event['event']
                ev2=event['event-off']
                key=event['key']
                panev=127.0*float(key-minKey)/float(maxKey-minKey)
                panev=31+pan/2
                panev=int(pan)
                panev=Midi.makePan(1,ev1.getTick()-1,pan)
                if pan:
                    events.append(pan)
                events.append(event['event'])
                events.append(event['event-off'])

            events=sorted(events,key=lambda event: event.getTick())

            # Create note track
            tout=sout.createTrack()
            Midi.addPan(tout,1,100,64)
            Midi.addNote(tout,1,offset/2,(offset/2)+2000,50,100)

            for event in events:
                Midi.setChannel(event,1)
                tout.add(event)
    Midi.writeMidiFile("temp/temp.midi",sout)


    nChan=0
    for chan in chans:
        if chan:
            nChan+=1
            print "Performing Channel: :",chan
            events=[]
            for event in chan:
                ev1=event['event']
                ev2=event['event-off']
                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)
                events.append(pan)
                events.append(event['event'])
                events.append(event['event-off'])

            events=sorted(events,key=lambda event: event.getTick())
                    
            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'])

            # Create note track
            tout=sout.createTrack()
            Midi.addPan(tout,1,100,64)
            Midi.addNote(tout,1,offset/2,(offset/2)+20,50,100)

            for event in events:
                Midi.setChannel(event,1)
                tout.add(event)
            player=Midi.getPlayer(sequencer,synthesiser)
            player.play(sout)
            player.waitFor()
    

playNotes(release=60,sameRelease=0,voices=8,pan=False)



Thursday, 12 February 2015

Surprising Success With The Waldorf

I'll be honest, today I tried and failed to synthesis Well Tempered Clavier part 2. But, as a consolation prise I got a lovely deep 'cello'.


Now, instruments of the violin family cannot be synthesised in any realistic way; they are just too complex (if someone does it - they are probably using samples directly - convolution with a sample also works). However, one can (I believe) capture their essence using first principles synthesis. The key ingredients are:

  1. Body resonance - if you think you have too much - you probably don't.
  2. Pitch variation due to bowing (as the string in stretched by the bow its pitch varies).
  3. A shockingly rattle at the top end which somehow works out because it is unstable.
  4. A shimmering stereo field cause but the sound being sent in all directions by the body of the instrument.
  5. A strong variation of the notes as they move in and out of resonance with the body.
  6. A very carefully tailored envelope.
  7. Vibrato and tremolo which is not just slapped on but moves and is subtle.

This is the Sonic Field blog so which of these came from the Waldorf Pulse 2 and which from Sonic Field?

2 and 3 were definitely aided by Sonic Field. The reverb had a chunk of excitation to it which built on top of chorusing. This along with the alive nature of the synth (being analogue) helped create this piece. It is not a cello but people tell me it sounds nice :)


Tuesday, 10 February 2015

Waldorf Pulse 2: A Bit Of Random - A Lot Of Fun

Right now I am completely loving working with the Waldorf - the harpsichord sound it can make is beyond belief. 

A regular old fashioned ring modulated synth-harpsichord is all very well; but the pulse width modulated effect which is possible with the Waldorf is something else again. The raw sounds from the little synth is a bit rough and a bit electronic but when passed through a touch of harmonic excitation and reverb in Sonic Field the result is stunning. Now, I might be blowing my own trumpet, but I can honestly say everyone who has listened to this live has praised the sound:


To be completely honest, I was a bit lucky. I just tried adding a bit of spring reverberation (using a spring impulse response) and I think that was the final trick to make the sound come to life. The reverb' you can here is a mixture of a few room/hall impulse responses reverbs mixed with a bit of spring.

But, the true secret is the way the signal path of a true analogue synth works. The sounds are all coupled and constantly changing. An electronic audio circuit 'wants' to make audio because the values of the components are set up that way. The circuits in an analogue synth' then interact with one another in music ways. This is distinctly different from pure digital synthesis where nice sounding audio is something one has to force from the algorithms. I am enjoying the mix where the analogue makes amazing feed stuff for digital post processing.

Sunday, 8 February 2015

Spatial Chorus

I have been working on the chorus effect.

By introducing a new operator into Sonic Field, it has been possible to produce a very stable chorus effect. One could consider chorus to be an FM effect. However, the numeric stability of FM is very poor for sampled sounds (or so I have found). What I have come up with instead is a time shift. Rather than altering the sample rate based on a signal, I shift the point of samples. Thus, minute errors do not accumulate as they do in FM.

The pitch shift then becomes the first differential of the time shift. In chorusing I make the time shift a sine wave to the pitch shift if also a sine wave (or a cosine if you want to be pedantic).

Here is the new operator:


/* For Copyright and License see LICENSE.txt and COPYING.txt in the root directory */
package com.nerdscentral.audio.time;

import java.util.List;

import com.nerdscentral.audio.SFConstants;
import com.nerdscentral.audio.SFSignal;
import com.nerdscentral.sython.Caster;
import com.nerdscentral.sython.SFPL_Context;
import com.nerdscentral.sython.SFPL_Operator;
import com.nerdscentral.sython.SFPL_RuntimeException;

public class SF_TimeShift implements SFPL_Operator
{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    public Object Interpret(final Object input, final SFPL_Context context) throws SFPL_RuntimeException
    {
        List<Object> lin = Caster.makeBunch(input);
        try (SFSignal in = Caster.makeSFSignal(lin.get(0)); SFSignal shift = Caster.makeSFSignal(lin.get(1)))
        {
            try (SFSignal y = in.replicateEmpty())
            {

                int length = y.getLength();
                if (shift.getLength() < length) length = shift.getLength();
                for (int index = 0; index < length; ++index)
                {
                    double pos = index + SFConstants.SAMPLE_RATE_MS * shift.getSample(index);
                    y.setSample(index, in.getSampleCubic(pos));
                }
                length = y.getLength();
                for (int index = shift.getLength(); index < length; ++length)
                {
                    y.setSample(index, in.getSample(index));
                }
                return Caster.prep4Ret(y);
            }
        }
    }

    @Override
    public String Word()
    {
        return Messages.getString("SF_TimeShift.0"); //$NON-NLS-1$
    }

}


Here is a piece demonstrating the effect:

And here is a chorus patch which was used:


sf.SetSampleRate(96000)
if not 'random' in dir():
    import random
    random.seed(System.currentTimeMillis())

def bandRand(min,max):
    min=float(min)
    max=float(max)
    r1=random.random()
    r2=random.random()
    r=float(r1+r2)*0.5
    r=r*(max-min)
    r=r+min
    return r

def chorus(
    left,
    right,
    minDepth = 10.0,
    maxDepth = 50.0,
    maxRate  =  0.1,
    minRate  =  0.05,
    nChorus  =  4.0,
    minVol   =  0.7,
    maxVol   =  1.0):    
    def inner(signal_):
        def inner_():
            signal=sf.Clean(signal_)
            sigs=[]
            l=sf.Length(+signal)
            for inst in range(0,nChorus):
                def in_inner():
                    print "Do"
                    lfo=sf.PhasedSineWave(l,bandRand(minRate,maxRate),random.random())
                    lfo=sf.NumericVolume(lfo,bandRand(minDepth,maxDepth))
                    nsg=sf.TimeShift(+signal,lfo)
                    lfo=sf.PhasedSineWave(l,bandRand(minRate,maxRate),random.random())
                    lfo=sf.NumericVolume(lfo,bandRand(minVol,maxVol))
                    lfo=sf.DirectMix(1,lfo)
                    nsg=sf.Multiply(lfo,nsg)
                    print "Done"
                    return sf.Finalise(nsg)
                sigs.append(sf_do(in_inner))
            ret=sf.Finalise(sf.Mix(sigs))
            -signal
            return ret
        return sf_do(inner_)
    
    return inner(left),inner(right)
    
(left,right)=sf.ReadFile("temp/a.wav")
left,right=chorus(
    left,
    right,
    minDepth =  0.0,
    maxDepth = 10.0,
    minVol   =  1.0,
    maxVol   =  1.0,
    nChorus  =  9.0)

left,right=chorus(left,right)
sf.WriteFile32((left,right),"temp/c.wav")

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


Monday, 19 January 2015

Ion Drive

I have started a completely new project - Space Craft Sounds!

Synthesising Bach has been a huge learning curve and very enjoyable indeed, what could possibly follow such a musical experience? Something completely non musical. Music makes an amazing background to work, concentration and relaxation; nevertheless, other sounds can be very effective and maybe less tiring. I remember the sound of a small stream outside on of the houses I lived in as  child was very relaxing. A few months ago, my wife and I were travelling across Belgium and I noticed how the throb of the Mercedes V8 and low rumble of the road noise helped her sleep over the motorway sections of our journey.

I have taken these ideas and scaled it up quite a bit! 'Ion Drive' is a super sized version of ambient car/road noise. With a nod to my faithful old Mercedes here is the 'back story':

"
Ion Drive:

At full power, slight fluctuations in the magnetic ion acceleration coils cause slow, ever changing throbbing to emerge from each bank of huge 1.6 Terra Watt engines. The battle rages outside in the silence of space as this flag ship ‘Europa’ yields burst after burst of withering anti-neutron fire on the retreating force. At these speeds, pulling against Epsilon Major’s gravity, the engines constantly boil off helium coolant which whistles on its super sonic journey to the refrigerator plant. Built on Epsilon IV with engines by Daimler Benz Space Ag and neutron cannons by Advanced Particle Weapons Inc, her prey knows the fight is already lost, no Thor class heavy cruiser has ever been defeated in battle.
"

So, how was it done? How to make a never repeating sound which whistles and rumbles? The key to the whole effect is in makeEngine. I guess I started off thinking of a regular car engine hence the parameter 'rpm' which give a bass frequency to work with. As I fiddled with ideas (this was an evolution not a design) my imagination got carried away; starting with a few hundred horse power to a few thousand million!

So, the characteristic rumble sound is created not directly from a sine wave but from resonance with the upper harmonics of a distorted sine wave.

        sig=sf.SineWave(length,pitch*0.1+random()*0.05)
        mod=sf.SineWave(length,0.1+random()*0.05)
        mod=sf.DirectMix(1.0,sf.Pcnt50(mod))
        sig=sf.Multiply(
            sig,
            mod
        )
        sig=sf.Power(sig,10)
        sig=sf.RBJPeaking(sig,pitch,1,99)
        sig=sf.RBJPeaking(sig,pitch,1,99)

I take a sine wave and ring modulate it with a very low frequency sine wave and then distort the result using the Power function. This produces an modulated set of harmonics. The two RBJPeaking filters then resonate at around 10 times the frequency of the initial sine wave. This will produce an unstable set of ringing sounds at around the resonance frequency. An approach like this produces a much more real world, changing and unstable sound than trying to directly create the rumble.

However, this still was not unstable enough. When it comes to engine noise, is seems the more complex the sound the more believable it is; after all, engines are made of a lot of parts, pipe, pannels etc. All these individual things add together to make the real sound and if we do not add enough complexity to the synthesis, it will sound thin and unreal.

So, the next trick was to frequency modulate by a random but very slow signal. This is the thing which give the sound its unstable longitudinal nature.

        sig=sf.SineWave(length,pitch*0.1+random()*0.05)
        mod=sf.WhiteNoise(sLen)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.DirectRelength(mod,0.01)
        mod=sf.Finalise(mod)
        mod=sf.Pcnt50(mod)
        mod=sf.DirectMix(1.0,mod)
        mod=sf.Pcnt49(mod)
        mod=sf.Cut(0,sf.Length(+sig),mod)
        print sf.Length(+mod),sf.Length(+sig)
        sig=sf.FrequencyModulate(sig,mod)

The rest of the patch is a combination of excitation and convolution reverb. Here is the complete thing.

sf.SetSampleRate(64000)
from random import random

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)

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 makeEngine(length_,rpm):
    def inner():
        pitch=2.0*float(rpm)/60.0
        length=float(length_)
        sig=sf.SineWave(length,pitch*0.1+random()*0.05)
        mod=sf.SineWave(length,0.1+random()*0.05)
        mod=sf.DirectMix(1.0,sf.Pcnt50(mod))
        sig=sf.Multiply(
            sig,
            mod
        )
        sig=sf.Power(sig,10)
        sig=sf.RBJPeaking(sig,pitch,1,99)
        sig=sf.RBJPeaking(sig,pitch,1,99)
        sig=sf.Finalise(sig)
        noise=sf.WhiteNoise(length)
        noise=sf.Power(noise,5)
        noise=sf.FixSize(noise)
        noise=sf.ButterworthLowPass(noise,32,2)
        noise=sf.Finalise(noise)
        sig=sf.Mix(
            sf.Pcnt98(sig),
            sf.Pcnt2(noise)
        )
        sig=sf.Finalise(sig)
        sig2=sf.RBJPeaking(+sig,pitch*32,4,99)
        sig=sf.Mix(
            sf.Pcnt10(sf.FixSize(sig2)),
            sig
        )
        sig=sf.Cut(1,sf.Length(+sig)-1,sig)       
        sLen=sf.Length(+sig)*0.011
        mod=sf.WhiteNoise(sLen)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.RBJLowPass(mod,8,1.0)
        mod=sf.DirectRelength(mod,0.01)
        mod=sf.Finalise(mod)
        mod=sf.Pcnt50(mod)
        mod=sf.DirectMix(1.0,mod)
        mod=sf.Pcnt49(mod)
        mod=sf.Cut(0,sf.Length(+sig),mod)
        print sf.Length(+mod),sf.Length(+sig)
        sig=sf.FrequencyModulate(sig,mod)
        return sf.Realise(sig)
    return sf_do(inner)

length =  60*60000
chans  =        16
rpm    =      2200
sigs  = []
for x in range(0,chans):
    l=float(x)/float(chans)
    r=1.0-l
    sig=makeEngine(length,rpm)
    sigs.append(((l,r),sig))

def mix(sigs,pos,keep=True):
    def inner():
        toMix=[]
        for lr,sig in sigs:
            v=lr[pos]
            if keep:
                +sig
            p=30.0*v
            toMix.append((sf.NumericVolume(sig,v),p))
        sig=sf.Realise(sf.Finalise(sf.MixAt(toMix)))
        sig=sf.Power(sig,1.1)
        sig=sf.Cut(1,sf.Length(+sig)-1,sig)
        return sf.Finalise(sig)
    return sf_do(inner)

left  = mix(sigs,0)
right = mix(sigs,1,False)

print "Entering reverb"
(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)

right  = excite(right,0.15,1.05)
left   = excite(left ,0.15,1.05)

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