Tuesday, December 25, 2018

Got latency issues with Samplerbox?

If you are wondering what is wrong with the latency issues in you Samplerbox build, follow this easy 1-step tutorial:

1) Stop using the "1GrandPiano" sample set.

Just look at this:

Sound of silence

The samples in the "1GrandPiano" set have varying lengths of silence before the sample starts. The silence is often just 10ms but on some samples it is up to 50ms.

If you have fought with latency issues before, you know that 50ms is a lot. It will make playing really hard. But what's even worse than a stable 50ms latency is that these samples have different lengths of silence before them. That makes the latency seem random and is really awful to play.

This sample set was one of the first introduced on the Samplerbox site. I think it was there from the beginning. So when I read complaints about latency with Samplerbox, I get the feeling that the cause is actually this sample set in most cases.

If you really want to use this sample set, you could trim the silence out of the samples. Some years ago I made a python script to do just this.

Disclaimer: I was just learning python at the time, so the code is probably not very pythonic. But it worked for me.

Note that it will cut silence and also a part of the attack! The higher the threshold value, the more you will cut from the beginning of the sample.

It will also cut any silence after the sample. That will make the sample set much smaller and thus faster to load.

Have fun.


# Sample trimmer for Python 3.x
# by Juho-Eric
# 24.10.2016
#
# This script trims silence out from the beginning of wave files.
# It makes them respond better when played live (reduces latency) and
# makes the file size smaller by removing the unnecessary silence.
#
# The script looks for .wav files in this directory and creates
# a subdirectory "trimmed" that contains the trimmed version of each
# sample.
#
import glob
import os
import wave
import struct
import timeit

def getframe(frames, i):
    framelong = frames[4*i]+(frames[4*i+1]<<8)+(frames[4*i+2]<<16)+(frames[4*i+3]<<24)
    framebs = struct.pack('L', framelong)
    return struct.unpack("<hh",framebs)

def setframe(frames, f, l, r):
    framebs = struct.pack('<hh', int(l), int(r))
    a,b,c,d = struct.unpack('BBBB',framebs)
    frames[f*4] = a
    frames[f*4+1] = b
    frames[f*4+2] = c
    frames[f*4+3] = d
    return 

# Just for checking how much time it takes to run
scriptstart = timeit.default_timer()

startthreshold = 100
endthreshold = 10
fadeinlength = 4
fadeoutlength = 4
wavefiles = glob.glob("*.wav")

totalcutframes = 0
totalcutsecs = 0

# Make output directory if not made yet
try:
    os.stat("trimmed")
except:
    os.mkdir("trimmed")
    
# Go through all wave files in this directory
for file in wavefiles:
    win = wave.open(file, 'r')
    params = win.getparams()
    #nbits = win.getsampwidth()*8
    #nchannels = win.getnchannels()
    nframes = win.getnframes()
    framerate = win.getframerate()

    # File is opened for reading.
    #print("Opening "+file+": "+str(nchannels)+" channels "+str(nbits)+"-bit "+str(framerate)+" Hz")
    
    # Get all frames as bytearray.
    frames = bytearray(win.readframes(nframes))

    # Find the first point where threshold is broken (trim starting silence)
    framewas = 0
    for i in range(nframes-1):
        l,r = getframe(frames, i)
        if (abs(l) > startthreshold or abs(r) > startthreshold):
            framewas = i
            break
    
    startframe = framewas
    startsecs = startframe / framerate
    totalcutsecs += startsecs
    totalcutframes += startframe
    print(file +" cut from start "+str(startframe)+" frames / "+str(startsecs)+" s")
    
    # Find the frame where the silence starts again (trim ending silence)
    framewas = nframes
    for i in range(nframes-1, 0, -1):
        l,r = getframe(frames, i)
        if (abs(l) > endthreshold or abs(r) > endthreshold):
            framewas = i
            break
    
    endframe = framewas
    cutendframes = nframes - endframe
    cutendsecs = cutendframes / framerate
    totalcutsecs += cutendsecs
    totalcutframes += cutendframes
    print(file+" cut from end "+str(cutendframes)+" frames / "+str(cutendsecs)+" s")
    
    # Create fadein
    for i,f in enumerate(range(startframe,startframe+fadeinlength)):
        l,r = getframe(frames,f)
        multiplier = ((i+1)/(fadeinlength+1))
        setframe(frames, f, l*multiplier, r*multiplier)
    
    # Create fadeout
    for i,f in enumerate(range(endframe-fadeoutlength,endframe)):
        l,r = getframe(frames,f)
        multiplier = ((fadeoutlength-i)/(fadeoutlength+1))
        setframe(frames, f, l*multiplier, r*multiplier)
    
    # write output file
    wout = wave.open("trimmed/"+file, 'w')
    wout.setparams(params)
    wout.writeframes(frames[startframe*4:endframe*4])
    wout.close()
    win.close()
    
print("Total: cut "+str(totalcutframes)+" frames / "+str(totalcutsecs)+" s")

scriptstop = timeit.default_timer()
print("Operation took "+str(scriptstop-scriptstart)+" seconds")