Best way to rescale a buffer into one of a different size?

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

With cubic interpolation (recommended), aliasing should be minimal / undetectable. (The upsampled audio has the capacity to represent higher frequencies, but the source doesn’t contain any energy at those higher frequencies, so those higher partials will be 0 magnitude = nothing to alias.)

If you partition the upsampling operation, then multiple partitions could run at the same time.

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

c = Buffer.alloc(s, b.numFrames * 8, 1);

(
SynthDef(\segment, { |srcbuf, dstbuf, rate, startFrame, numFrames|
	var phase = Line.ar(
		0, numFrames,
		(numFrames * SampleDur.ir) * rate,  // longer dur = slower read
		doneAction: 2
	) + startFrame;
	var sig = BufRd.ar(1, srcbuf, phase, loop: 0, interpolation: 4);
	BufWr.ar(sig, dstbuf, phase * rate, loop: 0);
}).add;
)

(
(
	instrument: \segment,
	srcbuf: b, dstbuf: c,
	rate: 8,
	startFrame: (0, 2048 .. b.numFrames - 1),  // array --> multiple concurrent synths
	numFrames: 2048  // multiple of block size is probably a good idea
).play;
)

// original
a = { (PlayBuf.ar(1, b, rate: BufRateScale.ir(b), doneAction: 2) * 0.5).dup }.play;

// upsampled... if there are flaws, they aren't obvious
a = { (PlayBuf.ar(1, c, rate: BufRateScale.ir(b) * 8, doneAction: 2) * 0.5).dup }.play;

However, I haven’t tested carefully with a pure sine, for instance. With the a11 sound, I don’t hear blatant artifacts. If there were clicking, in that example, I’d hear ~20 clicks per second, and I don’t. EDIT: I did test with a sine and it sounds clean to me.

hjh

1 Like