Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

softcut: routines for copying and grabbing content from buffer regions #1161

Merged
merged 12 commits into from Jul 27, 2020
@@ -12,6 +12,7 @@

#include <sndfile.hh>
#include <array>
#include <cmath>
#include <utility>

#include "BufDiskWorker.h"
@@ -51,6 +52,13 @@ void BufDiskWorker::requestClear(size_t idx, float start, float dur) {
requestJob(job);
}

void BufDiskWorker::requestCopy(size_t srcIdx, size_t dstIdx,
float srcStart, float dstStart, float dur,
float fadeTime, float preserve, bool reverse) {
BufDiskWorker::Job job{BufDiskWorker::JobType::Copy, {srcIdx, dstIdx}, "", srcStart, dstStart, dur, 0, fadeTime, preserve, reverse};
requestJob(job);
}

void
BufDiskWorker::requestReadMono(size_t idx, std::string path, float startSrc, float startDst, float dur, int chanSrc) {
BufDiskWorker::Job job{BufDiskWorker::JobType::ReadMono, {idx, 0}, std::move(path), startSrc, startDst, dur,
@@ -76,6 +84,11 @@ void BufDiskWorker::requestWriteStereo(size_t idx0, size_t idx1, std::string pat
requestJob(job);
}

void BufDiskWorker::requestRender(size_t idx, float start, float dur, int samples, RenderCallback callback) {
BufDiskWorker::Job job{BufDiskWorker::JobType::Render, {idx, 0}, "", start, start, dur, 0, 0.f, 0.f, false, samples, callback};
requestJob(job);
}

void BufDiskWorker::workLoop() {
while (!shouldQuit) {
// FIXME: use condvar to wait here instead of sleeping...
@@ -89,6 +102,9 @@ void BufDiskWorker::workLoop() {
case JobType::Clear:
clearBuffer(bufs[job.bufIdx[0]], job.startDst, job.dur);
break;
case JobType::Copy:
copyBuffer(bufs[job.bufIdx[0]], bufs[job.bufIdx[1]], job.startSrc, job.startDst, job.dur, job.fadeTime, job.preserve, job.reverse);
break;
case JobType::ReadMono:
readBufferMono(job.path, bufs[job.bufIdx[0]], job.startSrc, job.startDst, job.dur, job.chan);
break;
@@ -102,6 +118,9 @@ void BufDiskWorker::workLoop() {
case JobType::WriteStereo:
writeBufferStereo(job.path, bufs[job.bufIdx[0]], bufs[job.bufIdx[1]], job.startSrc, job.dur);
break;
case JobType::Render:
render(bufs[job.bufIdx[0]], job.startSrc, job.dur, (size_t)job.samples, job.renderCallback);
break;
}
#if 0 // debug, timing
auto ms_now = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
@@ -128,6 +147,14 @@ int BufDiskWorker::secToFrame(float seconds) {
return static_cast<int>(seconds * (float) sampleRate);
}

float BufDiskWorker::raisedCosFade(float unitphase) {
return 0.5f * (cosf(M_PI * (1.f + unitphase)) + 1.f);
}

float BufDiskWorker::mixFade(float x, float y, float a, float b) {
return x * sinf(a * (float)M_PI_2) + y * sinf(b * (float) M_PI_2);
}

//------------------------
//---- private buffer routines

@@ -146,6 +173,94 @@ void BufDiskWorker::clearBuffer(BufDesc &buf, float start, float dur) {
}
}

void BufDiskWorker::copyBuffer(BufDesc &buf0, BufDesc &buf1,
float srcStart, float dstStart, float dur,
float fadeTime, float preserve, bool reverse) {
size_t frSrcStart = secToFrame(srcStart);
clamp(frSrcStart, buf0.frames - 1);
size_t frDstStart = secToFrame(dstStart);
clamp(frDstStart, buf1.frames - 1);

size_t frDur;
if (dur < 0) {
frDur = buf0.frames - frSrcStart;
} else {
frDur = secToFrame(dur);
}
clamp(frDur, buf1.frames - frDstStart);

if (preserve > 1.f) { preserve = 1.f; }
if (preserve < 0.f) { preserve = 0.f; }

float x;
float phi;
size_t frFadeTime = secToFrame(fadeTime);
if (frFadeTime > 0) {
x = 0.f;
phi = 1.f / frFadeTime;
clamp(frFadeTime, frDur);
} else {
frFadeTime = 0;
x = 1.f;
phi = 0.f;
}

This comment has been minimized.

@catfact

catfact Jul 14, 2020
Collaborator

equal-power crossfade also an option, using x as phase:

        static float raisedCosFadeIn(float unitphase) {
            return 0.5f * (cosf(M_PI * (1.f + unitphase)) + 1.f);
        };

        static float raisedCosFadeOut(float unitphase) {
            return 0.5f * (cosf(M_PI * unitphase) + 1.f);
        };
if (reverse) {
// reversing contents while copying into an overlapping region
// is not possible without additional storage the size of the
// overlap, so we don't handle this
copyLoop(buf1.data + frDstStart,
buf0.data + frSrcStart + frDur - 1,
frDur, frFadeTime,
preserve, x, phi,
[](float*& d, const float*& s) { d++; s--; });
} else {
// source and destination regions might overlap, so we need to
// imitate std::memmove - when src < dst start from the end and
// move backwards
if (frDstStart < frSrcStart) {
copyLoop(buf1.data + frDstStart,
buf0.data + frSrcStart,
frDur, frFadeTime,
preserve, x, phi,
[](float*& d, const float*& s) { d++; s++; });
} else {
copyLoop(buf1.data + frDstStart + frDur - 1,
buf0.data + frSrcStart + frDur - 1,
frDur, frFadeTime,
preserve, x, phi,
[](float*& d, const float*& s) { d--; s--; });
}
}
}

template <typename Step>
void BufDiskWorker::copyLoop(float* dst, const float* src,
size_t frDur, size_t frFadeTime,
float preserve, float x, float phi,
Step&& update) {
size_t i;
float lambda;
for (i = 0; i < frFadeTime; i++) {
lambda = raisedCosFade(x);
*dst = mixFade(*dst, *src,
1.f - lambda * (1.f - preserve), lambda);
x += phi;
update(dst, src);
}
for ( ; i < frDur - frFadeTime; i++) {
*dst = preserve * *dst + *src;
update(dst, src);
}
for ( ; i < frDur; i++) {
lambda = raisedCosFade(x);
*dst = mixFade(*dst, *src,
1.f - lambda * (1.f - preserve), lambda);
x -= phi;
update(dst, src);
}
}

void BufDiskWorker::readBufferMono(const std::string &path, BufDesc &buf,
float startSrc, float startDst, float dur, int chanSrc)
noexcept {
@@ -392,3 +507,54 @@ noexcept {
cleanup:
delete[] ioBuf;
}

void BufDiskWorker::render(BufDesc &buf, float start, float dur, size_t samples, RenderCallback callback) {
if (samples == 0) { return; }

size_t frStart = secToFrame(start);
if (frStart < 0) { frStart = 0; }
clamp(frStart, buf.frames - 1);

size_t frDur;
if (dur < 0) {
frDur = buf.frames - frStart;
} else {
frDur = secToFrame(dur);
}
if (frDur < 1) { return; }
clamp(frDur, buf.frames - frStart);
clamp(samples, frDur);
dur = frDur / (float)sampleRate;
float window = (float)dur / samples;

auto *sampleBuf = new float[samples];

size_t m;
if (frDur <= samples) {
// no peak finding
for (m = 0; m < samples; m++) {
sampleBuf[m] = buf.data[frStart + m];
}
} else {
size_t w, wStart, wEnd;
float peak;

// FIXME -- sloppy heuristic for how many frames to skip when peak finding
int stride = (int)std::log2f(dur / 4);
if (stride < 1) { stride = 1; }
for (m = 1; m <= samples; m++) {
wStart = secToFrame(start + (m - 1) * window);
wEnd = secToFrame(start + m * window);
peak = 0.f;
for (w = wStart; w < wEnd; w += stride) {
if (std::fabs(buf.data[w]) > std::fabs(peak)) {
peak = buf.data[w];
}
}
sampleBuf[m - 1] = peak;
}
}

callback(window, start, samples, sampleBuf);
delete[] sampleBuf;
}
@@ -7,7 +7,7 @@
* it requires users to _register_ buffers (returns numerical index for registered buf)
* disk read/write work can be requested for registered buffers, executed in background thread
*
* TODO:
* TODO:
* - callback for request completion?
* - use condvar for signaling, instead of sleep+poll
*/
@@ -20,16 +20,20 @@
#include <mutex>
#include <queue>
#include <memory>
#include <functional>

namespace crone {

// class for asynchronous management of mono audio buffers
class BufDiskWorker {
public:
typedef std::function<void(float secPerSample, float start, size_t count, float* samples)> RenderCallback;

private:
enum class JobType {
Clear,
Clear, Copy,
ReadMono, ReadStereo,
WriteMono, WriteStereo
WriteMono, WriteStereo,
Render,
};
struct Job {
JobType type;
@@ -39,6 +43,11 @@ namespace crone {
float startDst;
float dur;
int chan;
float fadeTime;
float preserve;
bool reverse;
int samples;
RenderCallback renderCallback;
};
struct BufDesc {
float *data;
@@ -56,8 +65,15 @@ namespace crone {
static constexpr int ioBufFrames = 1024;

static int secToFrame(float seconds);
static float raisedCosFade(float unitphase);
static float mixFade(float x, float y, float a, float b);

template <typename Step>
static void copyLoop(float* dst, const float* src,
size_t frDur, size_t frFadeTime,
float preserve, float x, float phi,
Step&& step);

private:
static void requestJob(Job &job);

public:
@@ -71,6 +87,10 @@ namespace crone {
// clear a portion of a mono buffer
static void requestClear(size_t idx, float start = 0, float dur = -1);

static void requestCopy(size_t srcIdx, size_t dstIdx,
float srcStart = 0, float dstStart = 0, float dur = -1,
float fadeTime = 0, float preserve = 0, bool reverse = false);

// read mono soundfile to mono buffer
static void
requestReadMono(size_t idx, std::string path, float startSrc = 0, float startDst = 0, float dur = -1,
@@ -87,11 +107,17 @@ namespace crone {
// write and interleave two mono buffers to one stereo file
static void requestWriteStereo(size_t idx0, size_t idx1, std::string path, float start = 0, float dur = -1);

static void requestRender(size_t idx, float start, float dur, int count, RenderCallback callback);

private:
static void workLoop();

static void clearBuffer(BufDesc &buf, float start = 0, float dur = -1);

static void copyBuffer(BufDesc &buf0, BufDesc &buf1,
float srcStart = 0, float dstStart = 0, float dur = -1,
float fadeTime = 0, float preserve = 0, bool reverse = false);

static void readBufferMono(const std::string &path, BufDesc &buf,
float startSrc = 0, float startDst = 0, float dur = -1, int chanSrc = 0) noexcept;

@@ -104,6 +130,7 @@ namespace crone {
static void writeBufferStereo(const std::string &path, BufDesc &buf0, BufDesc &buf1,
float start = 0, float dur = -1) noexcept;

static void render(BufDesc &buf, float start, float dur, size_t samples, RenderCallback callback);
};

}
@@ -681,6 +681,73 @@ void OscInterface::addServerMethods() {
softCutClient->clearBuffer(argv[0]->i, argv[1]->f, argv[2]->f);
});

addServerMethod("/softcut/buffer/copy_mono", "iifffffi", [](lo_arg **argv, int argc) {
float dur = -1.f;
float fadeTime = 0.f;
float preserve = 0.f;
bool reverse = false;
if (argc < 4) {
return;
}
if (argc > 4) {
dur = argv[4]->f;
}
if (argc > 5) {
fadeTime = argv[5]->f;
}
if (argc > 6) {
preserve = argv[6]->f;
}
if (argc > 7) {
reverse = argv[7]->i != 0;
}

softCutClient->copyBuffer(argv[0]->i, argv[1]->i, argv[2]->f, argv[3]->f, dur, fadeTime, preserve, reverse);
});

addServerMethod("/softcut/buffer/copy_stereo", "fffffi", [](lo_arg **argv, int argc) {
float dur = -1.f;
float fadeTime = 0.f;
float preserve = 0.f;
bool reverse = false;
if (argc < 2) {
return;
}
if (argc > 2) {
dur = argv[2]->f;
}
if (argc > 3) {
fadeTime = argv[3]->f;
}
if (argc > 4) {
preserve = argv[4]->f;
}
if (argc > 5) {
reverse = argv[5]->i != 0;
}

softCutClient->copyBuffer(0, 0, argv[0]->f, argv[1]->f, dur, fadeTime, preserve, reverse);
softCutClient->copyBuffer(1, 1, argv[0]->f, argv[1]->f, dur, fadeTime, preserve, reverse);
});

addServerMethod("/softcut/buffer/render", "iffi", [](lo_arg **argv, int argc) {
int sampleCt = 128;
if (argc < 3) {
return;
}

int ch = argv[0]->i;
if (argc > 3) {
sampleCt = argv[3]->i;
}

softCutClient->renderSamples(ch, argv[1]->f, argv[2]->f, sampleCt,
[=](float secPerSample, float start, size_t count, float* samples) {

This comment has been minimized.

@csboling

csboling Jul 12, 2020
Author Contributor

Here and here I demonstrate that I only pretend to know how to do Modern C++ things. All the other callbacks use regular function pointers and I understand that std::function is more costly, but I could not get this to compile with a function pointer when this lambda needs to value-capture the ch index. Other places in this file use addServerMethod with some capturing going on (I think?) but the compiler is not happy about this -- maybe because this is a lambda defined inside another lambda?

This comment has been minimized.

@catfact

catfact Jul 14, 2020
Collaborator

i think the overhead of the lambda is totally fine here. i don't think there are any cases of capturing elsewhere - there is just a lot of static storage.

lo_blob bl = lo_blob_new(count * sizeof(float), samples);
lo_send(matronAddress, "/softcut/buffer/render_callback", "iffb", ch, secPerSample, start, bl);
});
});

addServerMethod("/softcut/reset", "", [](lo_arg **argv, int argc) {
(void) argv;
(void) argc;
@@ -782,4 +849,3 @@ void OscInterface::printServerMethods() {
void OscInterface::deinit() {
lo_address_free(matronAddress);
}

ProTip! Use n and p to navigate between commits in a pull request.