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