From 64fcd3bc588aeefe6b82e6d92b9bdadd435df42c Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 23 Feb 2021 23:21:51 -0600 Subject: [PATCH] feat: sample expression at 8khz and upsample --- README.md | 6 ++-- include/expression.hpp | 8 ++++- plugins/ByteBeat/ByteBeat.cpp | 39 ++++++++++++++++++---- plugins/ByteBeat/ByteBeat.hpp | 6 ++++ plugins/ByteBeat/ByteBeatController.sc | 4 +-- plugins/ByteBeat/ByteBeatController.schelp | 11 ++++-- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7564549..b8a8163 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ UGen to be parsed and evaluated. ``` ( -SynthDef.new(\bytebeat, { arg out; - Out.ar(out, ByteBeat.ar()) +SynthDef.new(\bytebeat, { + Out.ar(0, ByteBeat.ar()) }).add; ) -b = ByteBeatController(Synth.new(\bytebeat)); +b = ByteBeatController(Synth.new(\bytebeat), 0); b.setExpression("((t<<1)^((t<<1)+(t>>7)&t>>12))|t>>(4-(1^7&(t>>19)))|t>>7"); ``` diff --git a/include/expression.hpp b/include/expression.hpp index 9494750..6ded613 100644 --- a/include/expression.hpp +++ b/include/expression.hpp @@ -128,7 +128,13 @@ namespace bb int evaluate(int t) const { - return this->left->evaluate(t) / this->right->evaluate(t); + int divisor = this->right->evaluate(t); + // Dividing by zero will crash the SuperCollider server + if (divisor == 0) + { + return 0; + } + return this->left->evaluate(t) / divisor; }; protected: diff --git a/plugins/ByteBeat/ByteBeat.cpp b/plugins/ByteBeat/ByteBeat.cpp index 9342483..8821187 100644 --- a/plugins/ByteBeat/ByteBeat.cpp +++ b/plugins/ByteBeat/ByteBeat.cpp @@ -5,16 +5,22 @@ #include "ByteBeat.hpp" #include "parse.hpp" +#define BYTEBEAT_SAMPLERATE 8000 + static InterfaceTable *ft; namespace ByteBeat { - ByteBeat::ByteBeat() + ByteBeat::ByteBeat() : mSampleStep(BYTEBEAT_SAMPLERATE / sampleRate()) { mCalcFunc = make_calc_function(); - // Initialize with no audio - mExpression = new bb::Constant(0); + // Initialize with no audio to avoid popping/unitialized buffer. + // Expressions are evaluated as 32-bit signed integers, then cast to + // 8-bit unsigned integers, then transformed to floats with a range of + // +/-1.0. An expression that emits a constant value of 128 will + // correspond roughly to 0.0 at the output. + mExpression = new bb::Constant(128); } ByteBeat::~ByteBeat() @@ -30,10 +36,12 @@ namespace ByteBeat { bb::Expression *prevExpr = mExpression; mExpression = bb::parse(s); + // mNextSample = sampleByteBeat(); delete prevExpr; } catch (invalid_argument &ex) { + Print("%s", ex.what()); // TODO: Send back to client somehow. May not be possible without a // more general sendResponse interface. // See: https://scsynth.org/t/scsynth-plugincmd-and-sending-responses/2638 @@ -51,13 +59,26 @@ namespace ByteBeat for (int i = 0; i < nSamples; ++i) { - // TODO: Run expression at 8khz and resample to server samplerate - uint8_t data = mExpression->evaluate(mTime); - ++mTime; - outbuf[i] = 2 * (float)data / 255 - 1; + float data = lininterp(mAccumulator, mPrevSample, mNextSample); + outbuf[i] = data; + + mAccumulator += mSampleStep; + if (mAccumulator >= 1) + { + mAccumulator -= 1; + ++mTime; + mPrevSample = mNextSample; + mNextSample = sampleByteBeat(); + } } } + inline float ByteBeat::sampleByteBeat() const + { + uint8_t sample = mExpression->evaluate(mTime); + return 2 * (float)sample / 255 - 1; + } + /** * Unit command callback for the /setexpr command. Expects args to contain * a single string argument representing the new bytebeat expression. @@ -65,6 +86,10 @@ namespace ByteBeat void setExprCmd(ByteBeat *unit, sc_msg_iter *args) { unit->setExpression(args->gets()); + if (args->geti(1)) + { + unit->restart(); + } } /** diff --git a/plugins/ByteBeat/ByteBeat.hpp b/plugins/ByteBeat/ByteBeat.hpp index 903e04b..7190579 100644 --- a/plugins/ByteBeat/ByteBeat.hpp +++ b/plugins/ByteBeat/ByteBeat.hpp @@ -42,6 +42,12 @@ namespace ByteBeat * samples. */ void next(int nSamples); + inline float sampleByteBeat() const; + + float mSampleStep; + float mAccumulator = 0; + float mPrevSample = 0; + float mNextSample = 0; /** Time counter passed to the bytebeat expression */ int mTime = 0; diff --git a/plugins/ByteBeat/ByteBeatController.sc b/plugins/ByteBeat/ByteBeatController.sc index 6d9a3ba..aec05f0 100644 --- a/plugins/ByteBeat/ByteBeatController.sc +++ b/plugins/ByteBeat/ByteBeatController.sc @@ -5,8 +5,8 @@ ByteBeatController { ^super.newCopyArgs(synth, synthIndex); } - setExpression { arg input; - this.sendMsg('/setexpr', input) + setExpression { arg input, restart = 0; + this.sendMsg('/setexpr', input, restart) } restart { diff --git a/plugins/ByteBeat/ByteBeatController.schelp b/plugins/ByteBeat/ByteBeatController.schelp index fdd8034..4aed5c6 100644 --- a/plugins/ByteBeat/ByteBeatController.schelp +++ b/plugins/ByteBeat/ByteBeatController.schelp @@ -10,12 +10,12 @@ ByteBeat UGen instance. CODE:: ( -SynthDef.new(\bytebeat, { arg out; - Out.ar(out, ByteBeat.ar()) +SynthDef.new(\bytebeat, { + Out.ar(0, ByteBeat.ar()) }).add; ) -b = ByteBeatController(Synth.new(\bytebeat)); +b = ByteBeatController(Synth.new(\bytebeat), 0); b.setExpression("((t<<1)^((t<<1)+(t>>7)&t>>12))|t>>(4-(1^7&(t>>19)))|t>>7"); b.restart(); @@ -40,5 +40,10 @@ Set the bytebeat expression used to generate audio samples. ARGUMENT:: input The bytebeat expression string +ARGUMENT:: restart +Optionally reset the bytebeat expression's time counter 0 when setting the new +expression. 1 will reset the counter, 0 will preserve the time counter. Defaults +to 0. + METHOD:: restart Reset the bytebeat expression's time counter to 0