Zack Scholl

zack.scholl@gmail.com

sampswap

 / #supercollider #music #tutorial 

Generative breakbeats with SuperCollider and sox.

sampswap is a Lua script utility/wrapper around sox that makes it easy to swap pieces within a sample while adding effect to them. sox is an incredible tool that can be employed to perform a lot of audio feats, including splicing, trimming, merging, adding effects, detecting silence, equalizing, amplifying, etc. After playing around with it for awhile I realized I could mix and match these operations randomly on audio loops to produce new audio loops, akin to breakbeat music.

The heavy-lifting is done by sox but I employed Lua to create pure functions that can perform the sox commands on audio files. For example, a function to trim the silence from both ends of an audio file is declared as:

1function audio.silence_trim(fname)
2  local fname2=string.random_filename()
3  os.cmd("sox "..fname.." "..fname2.." silence 1 0.1 0.025% reverse silence 1 0.1 0.025% reverse")
4  return fname2
5end

My idea to produce “generative” breakbeat music was to take a loop and perform operations randomly, in sequence, so that the loop itself has memory of each operation and thus the result can be more complex than utilizing the original file for each.

When copying and pasting audio its important to be aware of the boundaries. Often the boundaries are “merged” by crossfading some “excess” region on the outside of the regions you are pasting. For example, from the sox docs:

      length1   excess
    -----------><--->
    _________   :   :  _________________
             \  :   : :\     
              \ :   : : \     
               \:   : :  \     
                *   : :   *  
                 \  : :   :\    
                  \ : :   : \     
    _______________\: :   :  \_________
                      :   :   
                      <--->   
                      excess  

Its a bit like cutting and pasting two tape loops together. Practically speaking, this means that when copying a loop you actually need to copy a little bit extra for both sides and paste it at a position slightly before, so that it gets into the correct location and correctly crossfades. This bit is probably the most complicated operation as it requires multiple sox commands.

audio.copy_and_paste(...)
 1function audio.copy_and_paste(fname,copy_start,copy_stop,paste_start,crossfade)
 2	local copy_length=copy_stop-copy_start
 3	if copy_length==nil or copy_length<0.05 then 
 4		do return fname end 
 5	end
 6	local piece=string.random_filename()
 7	local part1=string.random_filename()
 8	local part2=string.random_filename()
 9	local fname2=string.random_filename()
10	local splice1=string.random_filename()
11	local e=crossfade or 0.1 
12	local l=0 -- no leeway
13	os.cmd(string.format("sox %s %s trim %f %f",fname,piece,copy_start-e,copy_length+2*e))
14	os.cmd(string.format("sox %s %s trim 0 %f",fname,part1,paste_start+e))
15	os.cmd(string.format("sox %s %s trim %f",fname,part2,paste_start+copy_length-e))
16	os.cmd(string.format("sox %s %s %s splice %f,%f,%f",part1,piece,splice1,paste_start+e,e,l))
17	os.cmd(string.format("sox %s %s %s splice %f,%f,%f",splice1,part2,fname2,paste_start+copy_length+e,e,l))
18	os.cmd(string.format("rm -f %s %s %s %s",piece,part1,part2,splice1))
19	return fname2
20end

(^ clicking that will expand the rest of the functions if you’d like to see them.)

Lets get to the music

I’ll go through some of the pure functions I’ve used for creating generative breakbeat music. First, here is the original audio loop that I will demonstrate with:

The original audio is great, but its short and can get repetitive so using a bunch of operations on it we can add repeats with variety.

Jumping

“Jumping” is what I call when you simply copy and paste one region to another region within the sample.

The code for “jumping” is that audio.copy_and_paste function that I posted previously. Running that function a few times on the original sample yields something quite interesting.

Reversing

Reversing is what I call when you take a region and reverse it. Reversing an audio file is one of the simplest things and it can sound great when pasted back into the original audio.

The reversing function is really simple.

audio.reverse(...)
1function audio.reverse(fname)
2  local fname2=string.random_filename()
3  os.cmd(string.format("sox %s %s reverse",fname,fname2))
4  return fname2
5end

But pasting it requires taking notice of the crossfade regions. I altered the audio.copy_and_paste function to allow pasting any piece of audio with crossfades. Without the crossfading it will inevitably produce “clipping” or “popping” sounds.

audio.paste(...)
 1function audio.paste(fname,piece,paste_start,crossfade)
 2	local copy_length=audio.length(piece)
 3  if copy_length==nil then 
 4    do return fname end 
 5  end
 6	local part1=string.random_filename()
 7	local part2=string.random_filename()
 8	local fname2=string.random_filename()
 9	local splice1=string.random_filename()
10	local e=crossfade or 0.1 
11	local l=0 -- no leeway
12	os.cmd(string.format("sox %s %s trim 0 %f",fname,part1,paste_start+e))
13	os.cmd(string.format("sox %s %s trim %f",fname,part2,paste_start+copy_length-e*3))
14	os.cmd(string.format("sox %s %s %s splice %f,%f,%f",part1,piece,splice1,paste_start+e,e,l))
15	os.cmd(string.format("sox %s %s %s splice %f,%f,%f",splice1,part2,fname2,paste_start+copy_length+e,e,l))
16  os.cmd(string.format("rm -f %s %s %s",part1,part2,splice1))
17	return fname2
18end

Combining the audio.reverse and audio.paste on the original file creates the following:

Stutter

The stutter is my favorite effect. It is where you clip a piece of audio and then paste it many times at 1/16th note apart. At each new piece its fun to increase the volume or open up a filter.

audio.stutter(...)
 1function audio.stutter(fname,stutter_length,pos_start,count,crossfade_piece,crossfade_stutter,gain_amt)
 2  crossfade_piece=0.1 or crossfade_piece
 3  crossfade_stutter=0.005 or crossfade_stutter
 4  local partFirst=string.random_filename()
 5  local partMiddle=string.random_filename()
 6  local partLast=string.random_filename()
 7  os.cmd(string.format("sox %s %s trim %f %f",fname,partFirst,pos_start-crossfade_piece,stutter_length+crossfade_piece+crossfade_stutter))
 8  os.cmd(string.format("sox %s %s trim %f %f",fname,partMiddle,pos_start-crossfade_stutter,stutter_length+crossfade_stutter+crossfade_stutter))
 9  os.cmd(string.format("sox %s %s trim %f %f",fname,partLast,pos_start-crossfade_stutter,stutter_length+crossfade_piece+crossfade_stutter))
10  gain_amt=gain_amt or (count>8 and -1.5 or -2)
11  for i=1,count do 
12    local fnameNext=""
13    if i==1 then 
14      fnameNext=audio.gain(partFirst,gain_amt*(count-i))
15    else
16      fnameNext=string.random_filename()
17    local fnameMid=i<count and partMiddle or partLast 
18    if gain_amt~=0 then 
19      fnameMid=audio.gain(fnameMid,gain_amt*(count-i))
20    end
21      os.cmd(string.format("sox %s %s %s splice %f,%f,0",fname2,fnameMid,fnameNext,audio.length(fname2),crossfade_stutter))
22    end
23    fname2=fnameNext
24  end
25  return fname2
26end

This effect is also the most complicated because each little piece is crossfaded with each other little piece, but then all the pieces together must be pasted in with crossfades on either side. So I wrote the function to encompass the left, middle, and right sides so you can have shorter crossfades in the middle and longer crossfades to paste it into the original audio.

Reverse reverb

Reversing a reverb is another one of my favorite effects. This is one that is useful to have resampling (all these other effects could be done in realtime using a good sample player).

Reversing a reverb is pretty much how it sounds - take a slice and add reverb. Render the reverb ringing out and then reverse the whole thing.

While sox has a reverb built-in as well, I decided to use SuperCollider instead. SuperCollider can actually be run in “non-realtime” mode in which case you can use the SuperCollider toolkit and immediately (and pretty quickly) render audio with any of its building blocks.

SuperCollider NRT server
(
var oscScore;
var mainServer;
var nrtServer;
var serverOptions;
var scoreFn;

mainServer = Server(\sampswap_nrt, NetAddr("127.0.0.1", 47112));
serverOptions=ServerOptions.new.numOutputBusChannels_(2);
serverOptions.sampleRate=48000;
nrtServer = Server(\nrt, NetAddr("127.0.0.1", 47114), options:serverOptions);
SynthDef("lpf_rampup", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd=LPF.ar(snd,XLine.kr(200,20000,duration));
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("lpf_rampdown", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd=LPF.ar(snd,XLine.kr(20000,200,duration));
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("dec_ramp", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd=SelectX.ar(Line.kr(0,1,duration/4),[snd,Decimator.ar(snd,8000,8)]);
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("dec", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd=Decimator.ar(snd,8000,8);
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("reverberate", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd=SelectX.ar(XLine.kr(0,1,duration/4),[snd,Greyhole.ar(snd* EnvGen.ar(Env.new([0, 1, 1, 0], [0.1,dur-0.2,0.1]), doneAction:2))]);
    snd=LeakDC.ar(snd);
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.1,dur-0.2,0.1]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("filter_in_out", {
    arg out=0,  dur=30, f1,f2,f3,f4;
    var duration=BufDur.ir(0);
    var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
    snd = RLPF.ar(snd,
        LinExp.kr(EnvGen.kr(Env.new([0.1, 1, 1, 0.1], [f1,dur-f1-f2,f2])),0.1,1,100,20000),
        0.6);
    snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
    Out.ar(out, snd);
}).load(nrtServer);
SynthDef("tapedeck", {
	arg out=0,  dur=30,f1,f2,f3,f4,
	amp=0.9,tape_wet=0.95,tape_bias=0.9,saturation=0.9,drive=0.9,
	tape_oversample=1,mode=0,
	dist_wet=0.07,drivegain=0.5,dist_bias=0.5,lowgain=0.1,highgain=0.1,
	shelvingfreq=600,dist_oversample=1,
	hpf=60,hpfqr=0.6,
	lpf=18000,lpfqr=0.6;
	var duration=BufDur.ir(0);
	var snd = PlayBuf.ar(2,0,BufRateScale.kr(0));
	snd=snd*amp;
	snd=SelectX.ar(Lag.kr(tape_wet,1),[snd,AnalogTape.ar(snd,tape_bias,saturation,drive,tape_oversample,mode)]);	
	snd=SelectX.ar(Lag.kr(dist_wet/10,1),[snd,AnalogVintageDistortion.ar(snd,drivegain,dist_bias,lowgain,highgain,shelvingfreq,dist_oversample)]);				
	snd=RHPF.ar(snd,hpf,hpfqr);
	snd=RLPF.ar(snd,lpf,lpfqr);
	snd = snd * EnvGen.ar(Env.new([0, 1, 1, 0], [0.005,dur-0.01,0.005]), doneAction:2);
	Out.ar(out, snd);
}).load(nrtServer);

scoreFn={
    arg inFile,outFile,synthDefinition,durationScaling,oscCallbackPort,f1,f2,f3,f4;
    Buffer.read(mainServer,inFile,action:{
        arg buf;
        Routine {
            var buffer;
            var score;
            var duration=buf.duration*durationScaling;

            "defining score".postln;
            score = [
                [0.0, ['/s_new', synthDefinition, 1000, 0, 0, \dur,duration,\f1,f1,\f2,f2,\f3,f3,\f4,f4]],
                [0.0, ['/b_allocRead', 0, inFile]],
                [duration, [\c_set, 0, 0]] // dummy to end
            ];

            "recording score".postln;
            Score(score).recordNRT(
                outputFilePath: outFile,
                sampleRate: 48000,
                headerFormat: "wav",
                sampleFormat: "int24",
                options: nrtServer.options,
                duration: duration,
                action: {
                    Routine {
                        postln("done rendering: " ++ outFile);
                        0.2.wait;
                        NetAddr.new("localhost",oscCallbackPort).sendMsg("/quit");
                    }.play;
                }
            );
        }.play;
    });
};
mainServer.waitForBoot({
    Routine {
        "registring osc for score".postln;
        oscScore = OSCFunc({ arg msg, time, addr, recvPort;
            var inFile=msg[1].asString;
            var outFile=msg[2].asString;
            var synthDefinition=msg[3].asSymbol;
            var durationScaling=msg[4].asFloat;
            var oscCallbackPort=msg[5].asInteger;
            var f1=msg[6].asFloat;
            var f2=msg[7].asFloat;
            var f3=msg[8].asFloat;
            var f4=msg[9].asFloat;
            [msg, time, addr, recvPort].postln;
            scoreFn.value(inFile,outFile,synthDefinition,durationScaling,oscCallbackPort,f1,f2,f3,f4);
            "finished".postln;
        }, '/score',recvPort:47113);
        1.wait;
        "writing ready file".postln;
        File.new("/tmp/nrt-scready", "w");
        "ready".postln;
    }.play;
});
)

I can still write a pure function to render the audio file, but I’ll be using OSC to communicate with the NRT SuperCollider server to produce the result.

audio.supercollider_effect(...)
 1function audio.supercollider_effect(fname,effect,f1,f2,f3,f4)
 2  local fname2=string.random_filename()
 3  local durationScaling=1 
 4  if effect=="reverberate" then 
 5    durationScaling=4
 6  end
 7  f1=f1 or 0
 8  f2=f2 or 0
 9  f3=f3 or 0
10  f4=f4 or 0
11  os.cmd(string.format(SENDOSC..' --host 127.0.0.1 --addr "/score" --port 47113 --recv-port 47888 -s %s -s %s -s %s -s %s -s 47888 -s %f -s %f -s %f -s %f',fname,fname2,effect,durationScaling,f1,f2,f3,f4))
12  return fname2
13end

Filter in/out

Speaking of SuperCollider effects…since I’m already using the NRT server I opted to add some other fancy effects that sox can’t quite do. For example - adding a filter to the beginning opening and a closing filter at the end.

Its quite easy to add these effects using the same function above, just specifying which effect it is.

All together now

Each of those effects works by itself, but then their combination can be quite cool. By adding each effect with a given probability, at a random position you can quickly get a beat with variety and lots of movement.

Usage

You can find the sources for all these files here: https://github.com/schollz/sampswap

There is also more usage information there.