I’m working on an idea I’ve had for a precise and neurotic sample mangler. I’m starting out by stretching the buffer to the nearest greater power of 2 with interpolation.
Doing it in sclang makes more sense since it just needs to compute the new buffer and not play it. However, there’s not really a fast/obvious way to interpolate one array to another AFAIK. So, I’m handling it from the server.
//rescaling synth: given two existing buffers,
// scale the data from one to
// the other w/ interpolation
(
SynthDef(\scaleBuf, {|inBuf, outBuf|
//read the two bufs for the same duration
var inBufRd = Line.ar(0, BufFrames.kr(inBuf), BufDur.kr(outBuf));
var outBufRd = Line.ar(0, BufFrames.kr(outBuf), BufDur.kr(outBuf));
var trig = Done.kr(outBufRd);
//write into the outBuf with the interpolated
//data from the inBuf
BufWr.ar(BufRd.ar(1, inBuf, inBufRd, 1, 4), outBuf, outBufRd);
//tell the server that we're done!
SendReply.kr(trig, '/scaledone', trig);
}).load(s);
)
This is works just fine, but this is not exactly efficient (has to play the files in realtime), and I believe it’s doing something evil. What I’m doing is playing a shorter buf slowly into a larger buf at the same samplerate. Then, when it comes time for playback, I’m playing the larger buf faster in proportion so that the operation roughly cancels out. I’m assuming that all the artifacts are interpolation and not aliasing. That said, I’ve been clicking between the two buffers and they sound just slightly different in the high end.
The problem with this is that it’s linked to the samplerate. If I try to do it faster, it’ll undersample the original buffer. It’d be best if this can be done off the server to save time and have the same results.
Here’s the stupid playback synthdef I use:
(
SynthDef(\dumbplay, {|bufnum, outbus, rate|
Out.ar(outbus, PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum)*rate, doneAction:2).dup);
}).load(s);
)
and here’s a dummy version of my app with the same kind of routine structure as the real version, except meant to just demonstrate what I’m trying to do.
(
s.waitForBoot({
//set up conditionals and gui variables
var fileReceived = Condition.new();
var synthDone = Condition.new();
var guiFinished = false;
var filepath;
var p1, p2;
var f1, f2;
var st1, st2;
var l1, l2;
var lt;
var b1, b2;
//make a basic GUI for loading a file
Window.closeAll();
w = Window.new("File Rescale Test", Rect(Window.availableBounds.width / 2 - 500, Window.availableBounds.height / 2 - 300, 1000, 600)).front;
x = DragSink().receiveDragHandler_({filepath = View.currentDrag; fileReceived.unhang; x.value = View.currentDrag.split.last}).value_("drag file here!");
p1 = SoundFileView();
p2 = SoundFileView();
b1 = Button().string_("play");
b2 = Button().string_("play");
l1 = StaticText().string_("File 1");
l2 = StaticText().string_("File 2");
st1 = StaticText().string_("0 samples");
st2 = StaticText().string_("0 samples");
lt = StaticText().string_("loading....").visible_(false);
w.layout_(VLayout(HLayout(x, lt), HLayout(b1, l1, st1),p1, HLayout(b2, l2, st2),p2)).front;
//begin the program:
Routine({
fileReceived.hang(); //wait until a file is received
lt.visible_(true);
//load file to buffer
~buf = Buffer.readChannel(s, filepath, channels: [0]);
s.sync;
//alloc the new larger buffer
~newBuf = Buffer.alloc(s, (2 ** (ceil(log2(~buf.numFrames)))), 1);
s.sync;
//make an action s.t. sclang gets a done message
o = OSCdef(\beepResponder, {|msg| msg[0].postln; synthDone.unhang}, '/scaledone');
//run our rescaling synth
a = Synth(\scaleBuf, [\inBuf, ~buf, \outBuf, ~newBuf]);
//wait for the writing to be done
synthDone.hang;
a.free; //free the writing synth
lt.string_("done!");
//load the buffers into the GUI
p1.alloc(~buf.numFrames.asInteger, 1);
p2.alloc(~newBuf.numFrames.asInteger, 1);
f1 = ~buf.loadToFloatArray(action:{|arr| p1.data_(arr)});
f2 = ~newBuf.loadToFloatArray(action:{|arr| p2.data_(arr)});
s.sync; //idk if loadToFloatArray needs this, but just to be safe
st1.string_("no. of samples: " ++ ~buf.numFrames.asInteger.asString);
st2.string_("no. of samples: " ++ ~newBuf.numFrames.asInteger.asString);
b1.action_({
Synth(\dumbplay, [\bufnum, ~buf, \outbus, 0, \rate, 1]);
});
b2.action_({
Synth(\dumbplay, [\bufnum, ~newBuf, \outbus, 0, \rate, ~newBuf.duration / ~buf.duration]);
});
~buf.sampleRate;
~newBuf.sampleRate;
guiFinished = true;
}).play(AppClock);
});
)
Run the synthdefs, then this code. Drag and drop some audio in and you’ll see and hear what this does.
The question is, is there a better way of doing what I’m doing in terms of rescaling buffers?
This is all to get ahead of the interpolation that would happen if I tried to split, say, a 101 sample buffer into quarters. My brain is screaming at me to normalize the buffer lengths and then read explicit chunks out without further interpolation for sample step mangling.
On the a different note, if any of this async handling code has a better implementation, that is also something I’m trying to get better at!
Thanks,
Hamish