From 9f50e9d40e9452ba36965a1544a0e49ae8026e7b Mon Sep 17 00:00:00 2001 From: yaw-man Date: Fri, 19 Aug 2022 15:03:29 -0300 Subject: [PATCH] Added synthesis. --- src/yaw-shepard/DistrhoPluginInfo.h | 13 +--- src/yaw-shepard/dsp.cpp | 37 ++++------ src/yaw-shepard/synth.cpp | 104 ++++++++++++++++++++++++++++ src/yaw-shepard/synth.h | 37 ++++++++++ 4 files changed, 157 insertions(+), 34 deletions(-) create mode 100644 src/yaw-shepard/synth.cpp create mode 100644 src/yaw-shepard/synth.h diff --git a/src/yaw-shepard/DistrhoPluginInfo.h b/src/yaw-shepard/DistrhoPluginInfo.h index ffdcd0e..13d8023 100644 --- a/src/yaw-shepard/DistrhoPluginInfo.h +++ b/src/yaw-shepard/DistrhoPluginInfo.h @@ -2,22 +2,15 @@ #define DISTRHO_PLUGIN_INFO_H_INCLUDED #define DISTRHO_PLUGIN_BRAND "yaw-audio" -#define DISTRHO_PLUGIN_NAME "yaw-tab" -#define DISTRHO_PLUGIN_URI "https://yaw.man/plugins/yaw-tab" +#define DISTRHO_PLUGIN_NAME "yaw-shepard" +#define DISTRHO_PLUGIN_URI "https://yaw.man/plugins/yaw-shepard" #define DISTRHO_PLUGIN_HAS_UI 1 #define DISTRHO_PLUGIN_IS_RT_SAFE 1 #define DISTRHO_PLUGIN_NUM_INPUTS 0 -#define DISTRHO_PLUGIN_NUM_OUTPUTS 2 +#define DISTRHO_PLUGIN_NUM_OUTPUTS 1 #define DISTRHO_UI_USE_NANOVG 1 -// only checking if supported, not actually used -#define DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST 1 - -#ifdef __MOD_DEVICES__ -#define DISTRHO_PLUGIN_USES_MODGUI 1 -#endif - enum Parameters { ktpax = 0, ktpay, diff --git a/src/yaw-shepard/dsp.cpp b/src/yaw-shepard/dsp.cpp index 8658b69..25ffa1f 100644 --- a/src/yaw-shepard/dsp.cpp +++ b/src/yaw-shepard/dsp.cpp @@ -1,4 +1,5 @@ #include "DistrhoPlugin.hpp" +#include "synth.h" START_NAMESPACE_DISTRHO @@ -7,19 +8,18 @@ class TabPlugin : public Plugin public: TabPlugin() : Plugin(kParameterCount, 0, 0), - sampleRate(getSampleRate()) + sampleRate(getSampleRate()), + synth(sampleRate), + fParameters { 0 } { - // clear all parameters - std::memset(fParameters, 0, sizeof(float) * kParameterCount); - } protected: - const char* getLabel() const override { return "yaw-tab-shepard"; } - const char* getDescription() const override { return "Drawing tablet Shepard buzz tone"; } + const char* getLabel() const override { return "yaw-shepard"; } + const char* getDescription() const override { return "Generalized Shepard tone, tablet UI"; } const char* getMaker() const override { return "yaw-audio"; } const char* getHomePage() const override { return "https://yaw.man/plugins/yaw-tab"; } - const char* getLicense() const override { return "ISC"; } + const char* getLicense() const override { return "Fuck You Pay Me"; } uint32_t getVersion() const override { return d_version(1, 0, 0); } int64_t getUniqueId() const override { return d_cconst('y', 's', 'p', 'd'); } @@ -69,6 +69,7 @@ protected: void sampleRateChanged(double newRate) override { + synth.setSampleRate(sampleRate, newRate); sampleRate = newRate; } @@ -82,14 +83,15 @@ protected: fParameters[idx] = val; switch (idx) { case ktpax: - period = 0.02f * val * static_cast(sampleRate); + synth.setFrequencyShift(val); break; case ktpay: + synth.setTargetRatio(val); break; case ktpaz: break; case ktpap: - volume = val; + synth.setVolume(val); break; case kParameterButtonA: break; @@ -100,30 +102,17 @@ protected: void run(const float** inputs, float** outputs, uint32_t frames) override { - - for (uint32_t i = 0; i < frames; ++i) { - counter++; - if (counter > period) { - outputs[0][i] = outputs[1][i] = volume; - counter = 0; - } - else { - outputs[0][i] = outputs[1][i] = 0.0f; - } - } - + synth.process(*outputs, frames); parity = !parity; fParameters[kParameterTime] += parity ? 1 : -1; } private: float fParameters[kParameterCount]; - float period = 0.f; - float counter = 0.f; - float volume = 0.f; double sampleRate; bool parity = false; + Synth synth; DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TabPlugin) }; diff --git a/src/yaw-shepard/synth.cpp b/src/yaw-shepard/synth.cpp new file mode 100644 index 0000000..097ed5a --- /dev/null +++ b/src/yaw-shepard/synth.cpp @@ -0,0 +1,104 @@ +#include "synth.h" + +Synth::Synth(double sampleRate){ + setSampleRate(48000.0, sampleRate); +} + +#define frac(x) ((x) - ((long)x)) +constexpr double MIN_VOLUME = 0.00001; + +//Volume of voice as a function of sample rate independent frequency. +static constexpr double getAmplitude( double hz ) +{ + if( hz < 20.0 ) return 0.0; + if( hz < 440.0 ) return hz / 440.0; + if( hz < 16000.0 ) return (16000.0 - hz) / 15560.0; + return 0.0; +} + +//Sanity checks: voices should become silent outside audible frequencies. +//Voice index code will loop infinitely if these fail. +static_assert( MIN_VOLUME > getAmplitude(10.0) ); +static_assert( MIN_VOLUME > getAmplitude(20000.0)); + +void Synth::process(float* output, const uint32_t frames) +{ + double hz; + + //Render. + for(uint32_t i = 0; i < frames; i++){ + + //Set pitch. + hz = hzFund *= hzShift; + + //Set timbre. + ratio = ratioSlewRate * ratio + (1.0 - ratioSlewRate) * targetRatio; + + for(uint voice = 0; voice < NUM_VOICES; ++voice){ + //Get new phase. + double phase = phases[(voice + idxFund) & VOICE_MASK]; + phase += hz * sampleInterval; + phase = frac(phase); + + //Don't bother rendering anything over the Nyquist rate. + if( hz > hzNyq ) break; + output[i] += getAmplitude(hz) * sin(2.0 * M_PI * phase); + + //Remember phase, move to higher overtone. + phases[(voice + idxFund) & VOICE_MASK] = phase; + hz *= ratio; + } + + output[i] *= volume; + } + + //Make timbre cyclical. + if( hzShift < 1.0 ) + { + //Pitch low and decreasing, shift bottom voices to top. + while( getAmplitude(hz) > MIN_VOLUME + && getAmplitude(hzFund) < MIN_VOLUME) + { + ++idxFund; + hz *= ratio; + hzFund *= ratio; + } + } + + if( (hzShift > 1.0) || (targetRatio > ratio)) + { + //Pitch high and increasing. Shift top voices to bottom. + while( getAmplitude(hz) < MIN_VOLUME + && getAmplitude(hzFund) > MIN_VOLUME) + { + --idxFund; + hz /= ratio; + hzFund /= ratio; + } + } +} + +void Synth::setSampleRate(double oldRate, double newRate){ + sampleInterval = 1.0 / newRate; + hzNyq = newRate / 2; + ratioSlewRate *= newRate / oldRate; + hzShift = pow(hzShift, newRate / oldRate); +} + +//Takes value from 0 to 1 representing frequency shift factor. +//0 : lower one octave per second +//1 : raise one octave per second +void Synth::setFrequencyShift(double in){ + hzShift = exp2(2.0 * (in - 0.5) * sampleInterval); +} + +//Slew to given ratio. +//0 : next voice is one fifth above previous voice. +//1 : next voice is one octave above previous voice. +void Synth::setTargetRatio(double in){ + targetRatio = 1.5 + in * 0.5; +} + +void Synth::setVolume(double in){ + volume = in; +} \ No newline at end of file diff --git a/src/yaw-shepard/synth.h b/src/yaw-shepard/synth.h new file mode 100644 index 0000000..f4a15c5 --- /dev/null +++ b/src/yaw-shepard/synth.h @@ -0,0 +1,37 @@ +#include "DistrhoPlugin.hpp" +#include + +struct Voice +{ + double hz; + double phase; +}; + +constexpr int NUM_VOICES = 256; +constexpr uchar VOICE_MASK = 0xFF; + +class Synth +{ +public: + explicit Synth(double sampleRate); + void process(float *output, uint32_t frames); + void setFrequencyShift(double in); + void setTargetRatio(double in); + void setSampleRate(double oldRate, double newRate); + void setVolume(double in); + +private: + //Phase must persist between run invocation. + //Phase of each voice, considered as a period function on the unit interval. + std::array phases = {0}; + //Index of lowest voice in phase array. + uchar idxFund = 0; + float volume = 0.0f; + double hzFund = 32.7; + double hzShift = 1.0; + double hzNyq = 24000.0; + double ratio = 2.0; + double targetRatio = 2.0; + double ratioSlewRate = 0.99999; + double sampleInterval = 1.0 / 48000.0; +}; \ No newline at end of file