Skip to content

Commit

Permalink
lichen-community-systemsgh-46: Adds floored division fmodf function.
Browse files Browse the repository at this point in the history
lichen-community-systemsgh-22: Adds unit tests for sig_dsp_List and fixes its implementation.
  • Loading branch information
colinbdclark committed Mar 21, 2023
1 parent ed182c0 commit 1a6b823
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 26 deletions.
18 changes: 18 additions & 0 deletions libsignaletic/include/libsignaletic.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ float sig_fmaxf(float a, float b);
*/
float sig_clamp(float value, float min, float max);

/**
* @brief Computes the remainder of the floored division of two arguments.
* This is useful when implementing "through zero" wrap arounds.
* See https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition
* for more information.
*
* @param num the numerator
* @param denom the denominator
* @return float the remainder of the floored division operation
*/
float sig_flooredfmodf(float num, float denom);

/**
* Generates a random float between 0.0 and 1.0.
*
Expand Down Expand Up @@ -1527,9 +1539,15 @@ void sig_dsp_List_Outputs_newAudioBlocks(struct sig_Allocator* allocator,
void sig_dsp_List_Outputs_destroyAudioBlocks(struct sig_Allocator* allocator,
struct sig_dsp_List_Outputs* outputs);


struct sig_dsp_List_Parameters {
float wrap;
};

struct sig_dsp_List {
struct sig_dsp_Signal signal;
struct sig_dsp_List_Inputs inputs;
struct sig_dsp_List_Parameters parameters;
struct sig_dsp_List_Outputs outputs;
struct sig_Buffer* list;
};
Expand Down
74 changes: 48 additions & 26 deletions libsignaletic/src/libsignaletic.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ inline void sig_Status_reportResult(struct sig_Status* status,
}
}

float sig_fminf(float a, float b) {
inline float sig_fminf(float a, float b) {
float r;
#ifdef __arm__
asm("vminnm.f32 %[d], %[n], %[m]" : [d] "=t"(r) : [n] "t"(a), [m] "t"(b) :);
Expand All @@ -29,7 +29,7 @@ float sig_fminf(float a, float b) {
return r;
}

float sig_fmaxf(float a, float b) {
inline float sig_fmaxf(float a, float b) {
float r;
#ifdef __arm__
asm("vmaxnm.f32 %[d], %[n], %[m]" : [d] "=t"(r) : [n] "t"(a), [m] "t"(b) :);
Expand All @@ -40,10 +40,21 @@ float sig_fmaxf(float a, float b) {
}

// TODO: Unit tests
float sig_clamp(float value, float min, float max) {
inline float sig_clamp(float value, float min, float max) {
return sig_fminf(sig_fmaxf(value, min), max);
}

// TODO: Unit tests
inline float sig_flooredfmodf(float numer, float denom) {
float remain = fmodf(numer, denom);
if ((remain > 0.0f && denom < 0.0f) ||
(remain < 0.0f && denom > 0.0f)) {
remain = remain + denom;
}

return remain;
}

// TODO: Replace this with an object that implements
// the quick and dirty LCR method from Numerical Recipes:
// unsigned long jran = seed,
Expand Down Expand Up @@ -1176,7 +1187,10 @@ inline void sig_dsp_Oscillator_accumulatePhase(
float phaseStep = FLOAT_ARRAY(self->inputs.freq)[i] /
self->signal.audioSettings->sampleRate;
float phase = self->phaseAccumulator + phaseStep;
self->phaseAccumulator = sig_dsp_Oscillator_wrapPhase(phase);

// TODO: Benchmark this against wrapPhase() above,
// which is currently unused.
self->phaseAccumulator = sig_flooredfmodf(phase, 1.0f);
}

void sig_dsp_Sine_init(struct sig_dsp_Oscillator* self,
Expand Down Expand Up @@ -2039,35 +2053,43 @@ struct sig_dsp_List* sig_dsp_List_new(
void sig_dsp_List_init(struct sig_dsp_List* self,
struct sig_SignalContext* context) {
sig_dsp_Signal_init(self, context, *sig_dsp_List_generate);
self->parameters.wrap = 1.0f;
sig_CONNECT_TO_SILENCE(self, index, context);
}

void sig_dsp_List_generate(void* signal) {
struct sig_dsp_List* self = (struct sig_dsp_List*) signal;
struct sig_Buffer* list = self->list;

for (size_t i = 0; i < self->signal.audioSettings->blockSize; i++) {
float index = FLOAT_ARRAY(self->inputs.index)[i];
float listLength = (float) list->length;
float sample = 0.0f;

if (listLength > 0.0f) {
float scaledIndex = index * (float) (listLength - 1);
while (scaledIndex < 0.0f) {
scaledIndex += listLength;
}

while (scaledIndex > listLength) {
scaledIndex -= listLength;
}

size_t roundedIndex = (size_t) roundf(scaledIndex);

sample = FLOAT_ARRAY(list->samples)[roundedIndex];
size_t blockSize = self->signal.audioSettings->blockSize;

// List buffers can only be updated at control rate.
// TODO: Cache these and only recalculate when
// the list buffer changes.
size_t listLength = self->list != NULL ? list->length : 0;
float listLengthF = (float) listLength;
size_t lastIndex = listLength - 1;
float lastIndexF = (float) lastIndex;
bool shouldWrap = self->parameters.wrap > 0.0f;

if (listLength < 1) {
// There's nothing in the list; just output silence.
for (size_t i = 0; i < blockSize; i++) {
FLOAT_ARRAY(self->outputs.main)[i] = 0.0f;
FLOAT_ARRAY(self->outputs.length)[i] = listLengthF;
}
} else {
for (size_t i = 0; i < blockSize; i++) {
float index = FLOAT_ARRAY(self->inputs.index)[i];
float scaledIndex = index * lastIndexF;
float roundedIndex = roundf(scaledIndex);
float wrappedIndex = shouldWrap ?
sig_flooredfmodf(roundedIndex, listLengthF) :
sig_clamp(roundedIndex, 0.0f, lastIndexF);

float sample = FLOAT_ARRAY(list->samples)[(size_t) wrappedIndex];
FLOAT_ARRAY(self->outputs.main)[i] = sample;
FLOAT_ARRAY(self->outputs.length)[i] = listLengthF;
}

FLOAT_ARRAY(self->outputs.main)[i] = sample;
FLOAT_ARRAY(self->outputs.length)[i] = (float) listLength;
}
}

Expand Down
157 changes: 157 additions & 0 deletions libsignaletic/tests/test-libsignaletic.c
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,159 @@ void test_sig_dsp_TimedGate_bipolar(void) {
thirdGate->samples, thirdGate->length);
}

void generateAndTestListIndex(struct sig_dsp_Value* idx, float idxValue,
struct sig_dsp_List* list, float expectedListValue) {
idx->parameters.value = idxValue;
idx->signal.generate(idx);
list->signal.generate(list);
testAssertBufferContainsValueOnly(&allocator, expectedListValue,
list->outputs.main, audioSettings->blockSize);
}

void test_sig_dsp_List_wrapping(void) {
struct sig_dsp_Value* idx = sig_dsp_Value_new(&allocator,
context);

float listItems[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
struct sig_Buffer listBuffer = {
.length = 5,
.samples = listItems
};

struct sig_dsp_List* list = sig_dsp_List_new(&allocator, context);
list->list = &listBuffer;
list->inputs.index = idx->outputs.main;

// No index rounding required.
generateAndTestListIndex(idx, 0.0f, list, 1.0f);
testAssertBufferContainsValueOnly(&allocator, 5.0f,
list->outputs.length, audioSettings->blockSize);

// Index is normalized between 0.0->1.0f,
// so there are values at 0.0, 0.25 0.5 0.75, 1.0
// Fractional indexes less than halfway should be rounded down.
generateAndTestListIndex(idx, 0.12f, list, 1.0f);

// Fractional indexes >= half should be rounded up.
generateAndTestListIndex(idx, 0.125f, list, 2.0f);

// Another even index.
generateAndTestListIndex(idx, 0.5f, list, 3.0f);

// Nearly the last index.
generateAndTestListIndex(idx, 0.99f, list, 5.0f);

// Just past the first index.
generateAndTestListIndex(idx, 0.00001f, list, 1.0f);

// Exactly the last index.
generateAndTestListIndex(idx, 1.0f, list, 5.0f);

// A slightly out of bounds index should round down to the last index.
generateAndTestListIndex(idx, 1.1f, list, 5.0f);

// But a larger index should round up and
// wrap around to return the first value.
generateAndTestListIndex(idx, 1.25f, list, 1.0f);

// Larger indices should wrap around past the beginning.
generateAndTestListIndex(idx, 1.5f, list, 2.0f);

// Very large indices should wrap around the beginning again.
generateAndTestListIndex(idx, 3.0f, list, 3.0f);

// Negative indices should wrap around the end.
// Note the index "shift" here: -0.25 should point
// to the last value in the list.
generateAndTestListIndex(idx, -0.25f, list, 5.0f);

// But not if they're not negative enough.
generateAndTestListIndex(idx, -0.12f, list, 1.0f);

// Very negative indices should keep wrapping around.
generateAndTestListIndex(idx, -1.5f, list, 5.0f);

// Small lists should work.
float small[2] = {1.0f, 2.0f};
listBuffer.length = 2;
listBuffer.samples = small;
generateAndTestListIndex(idx, 0.0f, list, 1.0f);
generateAndTestListIndex(idx, 1.0f, list, 2.0f);
generateAndTestListIndex(idx, 0.5f, list, 2.0f);
generateAndTestListIndex(idx, 0.25f, list, 1.0f);
generateAndTestListIndex(idx, -1.0f, list, 2.0f);
generateAndTestListIndex(idx, -0.25f, list, 1.0f);
generateAndTestListIndex(idx, -0.5f, list, 2.0f);

// Very small lists should work.
float verySmall[1] = {1.0f};
listBuffer.length = 1;
listBuffer.samples = verySmall;
generateAndTestListIndex(idx, 1.0f, list, 1.0f);
generateAndTestListIndex(idx, 0.0f, list, 1.0f);
generateAndTestListIndex(idx, 0.5f, list, 1.0f);
generateAndTestListIndex(idx, -0.5f, list, 1.0f);
generateAndTestListIndex(idx, -1.15f, list, 1.0f);
}

void test_sig_dsp_List_clamping(void) {
struct sig_dsp_Value* idx = sig_dsp_Value_new(&allocator,
context);

float listItems[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
struct sig_Buffer listBuffer = {
.length = 5,
.samples = listItems
};

struct sig_dsp_List* list = sig_dsp_List_new(&allocator, context);
list->parameters.wrap = 0.0f;
list->list = &listBuffer;
list->inputs.index = idx->outputs.main;

generateAndTestListIndex(idx, 0.0f, list, 1.0f);
generateAndTestListIndex(idx, 0.00001f, list, 1.0f);
generateAndTestListIndex(idx, 0.99f, list, 5.0f);
generateAndTestListIndex(idx, 1.0f, list, 5.0f);
generateAndTestListIndex(idx, 1.001f, list, 5.0f);
generateAndTestListIndex(idx, 1.125f, list, 5.0f);
generateAndTestListIndex(idx, 1.25f, list, 5.0f);
generateAndTestListIndex(idx, 3.0f, list, 5.0f);
generateAndTestListIndex(idx, -0.00001f, list, 1.0f);
generateAndTestListIndex(idx, -0.99f, list, 1.0f);
generateAndTestListIndex(idx, -1.5f, list, 1.0f);
}

void test_sig_dsp_List_noList(void) {
struct sig_dsp_Value* idx = sig_dsp_Value_new(&allocator,
context);

float empty[0] = {};
struct sig_Buffer listBuffer = {
.length = 0,
.samples = empty
};

struct sig_dsp_List* list = sig_dsp_List_new(&allocator, context);
list->inputs.index = idx->outputs.main;

// A NULL list should return silence.
list->signal.generate(list);
testAssertBufferIsSilent(&allocator, list->outputs.main,
audioSettings->blockSize);
testAssertBufferIsSilent(&allocator, list->outputs.length,
audioSettings->blockSize);

// An empty list should return silence.
list->list = &listBuffer;
list->signal.generate(list);
testAssertBufferIsSilent(&allocator, list->outputs.main,
audioSettings->blockSize);
testAssertBufferIsSilent(&allocator, list->outputs.length,
audioSettings->blockSize);

}

int main(void) {
UNITY_BEGIN();

Expand Down Expand Up @@ -1012,5 +1165,9 @@ int main(void) {
RUN_TEST(test_sig_dsp_TimedGate_unipolar);
RUN_TEST(test_sig_dsp_TimedGate_resetOnTrigger);
RUN_TEST(test_sig_dsp_TimedGate_bipolar);
RUN_TEST(test_sig_dsp_List_wrapping);
RUN_TEST(test_sig_dsp_List_clamping);
RUN_TEST(test_sig_dsp_List_noList);

return UNITY_END();
}

0 comments on commit 1a6b823

Please sign in to comment.