diff --git a/.gitmodules b/.gitmodules index 458bc28..6e25929 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "lib/DPF"] - ignore = all path = lib/DPF url = https://github.com/DISTRHO/DPF.git \ No newline at end of file diff --git a/lib/DPF b/lib/DPF index 86a621b..23692d0 160000 --- a/lib/DPF +++ b/lib/DPF @@ -1 +1 @@ -Subproject commit 86a621bfd86922a49ce593fec2a618a1e0cc6ef3 +Subproject commit 23692d024e57c617bd0692beb1835d48f98eb914 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cc47410..df15ee5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,4 +2,5 @@ cmake_minimum_required(VERSION 3.10) add_subdirectory(yaw-tab) add_subdirectory(yaw-shepard) add_subdirectory(yaw-vowel) -add_subdirectory(yaw-totune) \ No newline at end of file +add_subdirectory(yaw-totune) +add_subdirectory(yaw-shep) \ No newline at end of file diff --git a/src/yaw-shep/CMakeLists.txt b/src/yaw-shep/CMakeLists.txt new file mode 100644 index 0000000..bf07c21 --- /dev/null +++ b/src/yaw-shep/CMakeLists.txt @@ -0,0 +1,11 @@ +dpf_add_plugin(yaw-shep + TARGETS vst2 vst3 + FILES_DSP + dsp.cpp + synth.cpp + FILES_UI + ui.cpp) + +target_include_directories(yaw-shep PUBLIC + "." + "../../lib/scale") \ No newline at end of file diff --git a/src/yaw-shep/DistrhoPluginInfo.h b/src/yaw-shep/DistrhoPluginInfo.h new file mode 100644 index 0000000..1169f41 --- /dev/null +++ b/src/yaw-shep/DistrhoPluginInfo.h @@ -0,0 +1,26 @@ +#ifndef DISTRHO_PLUGIN_INFO_H_INCLUDED +#define DISTRHO_PLUGIN_INFO_H_INCLUDED + +#define DISTRHO_PLUGIN_BRAND "yaw-audio" +#define DISTRHO_PLUGIN_NAME "yaw-shep" +#define DISTRHO_PLUGIN_URI "https://yaw.man/plugins/yaw-shep" + + +#define DISTRHO_PLUGIN_IS_RT_SAFE 1 +#define DISTRHO_PLUGIN_NUM_INPUTS 0 +#define DISTRHO_PLUGIN_NUM_OUTPUTS 1 +#define DISTRHO_PLUGIN_WANT_MIDI_INPUT 1 +#define DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 0 + +#define DISTRHO_PLUGIN_HAS_UI 1 +#define DISTRHO_UI_USE_NANOVG 1 + +enum Parameters { + kMouseX = 0, + kMouseY, + kVolume, + kHz, + kParameterCount, +}; + +#endif // DISTRHO_PLUGIN_INFO_H_INCLUDED diff --git a/src/yaw-shep/dsp.cpp b/src/yaw-shep/dsp.cpp new file mode 100644 index 0000000..e9a384d --- /dev/null +++ b/src/yaw-shep/dsp.cpp @@ -0,0 +1,143 @@ +#include "DistrhoPlugin.hpp" +#include "synth.h" + +START_NAMESPACE_DISTRHO + +class ShepPlug : public Plugin +{ +public: + ShepPlug() + : Plugin(kParameterCount, 0, 0), + sampleRate(getSampleRate()), + synth(sampleRate), + fParameters { 0 } + { + } + +protected: + const char* getLabel() const override { return "yaw-shep"; } + const char* getDescription() const override { return "Generalized Shepard tone, mouse UI"; } + const char* getMaker() const override { return "yaw-audio"; } + const char* getHomePage() const override { return "https://yaw.man/plugins/yaw-shep"; } + 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', 'm'); } + + + void initParameter(uint32_t index, Parameter& parameter) override + { + parameter.hints = kParameterIsAutomable; + parameter.ranges.def = 0.0f; + parameter.ranges.min = 0.0f; + parameter.ranges.max = 1.0f; + + switch (index) + { + case kHz: + parameter.name = "hz"; + parameter.symbol = "hz"; + parameter.hints = kParameterIsOutput; + parameter.ranges.def = 55.0f; + parameter.ranges.min = 55.0f; + parameter.ranges.max = 880.0f; + case kMouseX: + parameter.name = "x"; + parameter.symbol = "x"; + break; + case kMouseY: + parameter.name = "y"; + parameter.symbol = "y"; + break; + case kVolume: + parameter.name = "Volume"; + parameter.symbol = "vol"; + break; + } + } + + void sampleRateChanged(double newRate) override + { + synth.setSampleRate(sampleRate, newRate); + sampleRate = newRate; + } + + float getParameterValue(uint32_t index) const override + { + return fParameters[index]; + } + + void setParameterValue(uint32_t idx, float val) override + { + fParameters[idx] = val; + switch (idx) { + case kMouseX: + synth.setPhaseVelocity(val); + break; + case kMouseY: + synth.setTone(val); + break; + case kVolume: + synth.setVolume(val); + break; + } + } + + void run(const float** inputs, float** outputs, uint32_t frames, + const MidiEvent* midiEvents, uint32_t midiEventCount) override + { + + for (uint32_t i = 0; i < midiEventCount; ++i) + { + if (midiEvents[i].size <= 3) + { + uint8_t status = midiEvents[i].data[0]; + uint8_t data = midiEvents[i].data[1] & 127; + uint8_t velocity = midiEvents[i].data[2] & 127; + + float vol = velocity / 127.0f; + + switch (status & 0xf0) + { + //Controller change. + case 0xb0: + //Channel Volume. + if( data == 7 ) setParameterValue(kVolume, vol); + //Breath Control. + if( data == 2 ) setParameterValue(kVolume, vol); + break; + //Aftertouch. + case 0xd0: + setParameterValue(kVolume, vol); + break; + //Note On. + case 0x90: + synth.setNote(data); + /* fall through */ + //Note Off. + case 0x80: + setParameterValue(kVolume, vol); + break; + + } + } + } + + synth.process(*outputs, frames); + fParameters[kHz] = synth.hzFund; + } + +private: + float fParameters[kParameterCount]; + double sampleRate; + bool parity = false; + + Synth synth; + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ShepPlug) +}; + +Plugin* createPlugin() +{ + return new ShepPlug(); +} + +END_NAMESPACE_DISTRHO diff --git a/src/yaw-shep/synth.cpp b/src/yaw-shep/synth.cpp new file mode 100644 index 0000000..21dfe77 --- /dev/null +++ b/src/yaw-shep/synth.cpp @@ -0,0 +1,119 @@ +#include "synth.h" + +Synth::Synth(double sampleRate){ + setSampleRate(48000.0, sampleRate); +} + +void Synth::resetPhase(){ + spectrumPhase = 0.0; +} + +#define frac(x) ((x) - ((long)x)) +constexpr double MIN_VOLUME = 0.00001; + +static constexpr double amin = 50; +static constexpr double apiq = 500; +static constexpr double amax = 10000; +//Volume of voice as a function of sample rate independent frequency. +static constexpr inline float getAmplitude( double hz ) +{ + if( hz < amin ) return 0.0; + if( hz < apiq ) { + double a = (hz - amin) / (apiq - amin); + return a * a;} + if( hz < amax ) { + double a = 1.0 - (hz - apiq) / (amax - apiq); + return a * a; + } + return 0.0; +} + +//Interpolation function on unit interval with good regularity as a function on S^1. +static constexpr inline float smooth( double x ) +{ + return x * x * x;//(3.0 * x - 2.0); +} + +//Sanity checks: voices should become silent outside audible frequencies. +static_assert( MIN_VOLUME > getAmplitude(10.0) ); +static_assert( MIN_VOLUME > getAmplitude(20000.0)); + +//New fundamental is twice as high as old fundamental. +//Even overtones become plain overtones. +void Synth::shiftUp() +{ + tablePhase *= 2.0; + spectrumPhase += 1.0; +} + +//New fundamental is half as high as old fundamental. +//Overtones become even overtones. +void Synth::shiftDown() +{ + tablePhase /= 2.0; + spectrumPhase -= 1.0; +} + +void Synth::process(float* output, const uint32_t frames) +{ + //Render. + for(uint32_t i = 0; i < frames; i++){ + + //Set pitch. + hzFund = exp2(spectrumPhase) * fMin; + + tablePhase += hzFund * sampleInterval; + tablePhase = frac(tablePhase); + + bool isOddHarmonic = true; + + for(uint voice = 0; voice < NUM_VOICES; ++voice){ + + double voicePhase = 2.0 * M_PI * tablePhase * (voice + 1.0); + //Anti-aliasing: don't bother rendering anything over the Nyquist rate. + if( hzFund * (voice + 1.0) > hzNyq ) break; + + output[i] += (!isOddHarmonic + isOddHarmonic * smooth(spectrumPhase)) //Fade in odd harmonics. + * getAmplitude(hzFund * (voice + 1.0)) //Frequency response. + * sinf(static_cast(voicePhase)); //Additives. + + isOddHarmonic = !isOddHarmonic; + } + + output[i] *= fMin * volume / static_cast(8 * NUM_VOICES); + + //Wrapping. + spectrumPhase += spectrumPhaseVelocity * sampleInterval; + if( spectrumPhase > 1.0) shiftDown(); + if( spectrumPhase < 0.0) shiftUp(); + } + +} + +void Synth::setSampleRate(double oldRate, double newRate){ + sampleInterval = 1.0 / newRate; + hzNyq = newRate / 2; +} + +//Takes value from 0 to 1 representing rate of wrapping pitch increase. +//0 : lower one octave per second +//1 : raise one octave per second +void Synth::setPhaseVelocity(double in){ + spectrumPhaseVelocity = 2.0 * (in - 0.5); +} + +//Vary pitch of fundamental in a non-wrapping kinda way. +//Current range is six octaves, current bottom is 13.75 Hz (A0) +void Synth::setTone(double in){ + falloff = in; +} + + +void Synth::setVolume(double in){ + volume = in; +} + +void Synth::setNote(uint8_t step){ + fMin = 440.0 * exp2((step - 69.0) / 12.0); + Synth::resetPhase(); +} \ No newline at end of file diff --git a/src/yaw-shep/synth.h b/src/yaw-shep/synth.h new file mode 100644 index 0000000..2259639 --- /dev/null +++ b/src/yaw-shep/synth.h @@ -0,0 +1,45 @@ +#include "DistrhoPlugin.hpp" +#include + +constexpr unsigned int NUM_VOICES = 512; +constexpr unsigned int VOICE_MASK = NUM_VOICES - 1; + +class Synth +{ +public: + explicit Synth(double sampleRate); + void process(float *output, uint32_t frames); + void setPhaseVelocity(double in); + void setTone(double in); + void setSampleRate(double oldRate, double newRate); + void setVolume(double in); + void setNote(uint8_t step); + + //For recordable performances. + //For now it requires a button push, but perhaps + //we should require time position and have the phase reset automatically + //on song start? + void resetPhase(); + + + //Current fundamental frequency of blit. + double hzFund = fMin; + +private: + void shiftUp(); + void shiftDown(); + //Phase of wavetable. + double tablePhase = 0.0; + float volume = 0.0f; + //Parameter in unit circle controlling pitch (varies by one octave). + double spectrumPhase = 1.0f; + //Revolutions per second of spectrum phase. + double spectrumPhaseVelocity = 0.0; + //Lowest fundamental frequency of blit. + double fMin = 55.0; + + double falloff = 1.0; + + double hzNyq = 24000.0; + double sampleInterval = 1.0 / 48000.0; +}; \ No newline at end of file diff --git a/src/yaw-shep/ui.cpp b/src/yaw-shep/ui.cpp new file mode 100644 index 0000000..7a7dfc0 --- /dev/null +++ b/src/yaw-shep/ui.cpp @@ -0,0 +1,143 @@ +#include "ui.h" +#include +#include + +START_NAMESPACE_DISTRHO + +static constexpr uint kInitialWidth = 800; +static constexpr uint kInitialHeight = 600; + +MouseUI::MouseUI() + : UI(kInitialWidth, kInitialHeight) +{ + +#ifdef DGL_NO_SHARED_RESOURCES + createFontFromFile("sans", "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf"); +#else + loadSharedResources(); +#endif +} + + +void MouseUI::parameterChanged(uint32_t index, float value) +{ + + if (index < kParameterCount) + { + switch (index) + { + case (kHz): + hz = value; break; + case (kMouseX): + x = value; break; + case (kMouseY): + y = value; break; + case (kVolume): + vol = value; break; + } + return; + } + +} + +void MouseUI::uiIdle() +{ +} + +bool MouseUI::onMouse(const MouseEvent &ev) +{ + record = ev.press; + repaint(); + return false; // Allow event to propagate. +} + +bool MouseUI::onMotion(const MotionEvent &ev) +{ + if(!record) return false; + + x = std::clamp(ev.pos.getX() / (1.0 * getWidth()), 0.0, 1.0); + y = std::clamp(ev.pos.getY() / (1.0 * getHeight()), 0.0, 1.0); + setParameterValue(kMouseX, x); + setParameterValue(kMouseY, y); + repaint(); + + return false; +} + +void MouseUI::onResize(const ResizeEvent &ev) +{ + return UI::onResize(ev); +} + +void MouseUI::onNanoDisplay() +{ + fontSize(15.0f); + textLineHeight(1.f); + + // Numerical feedback. + beginPath(); + fillColor(200, 200, 200); + textBox(0.f, 15.f, 250.f, + std::format("Frequency: {:.3f}\nNearest: {:.3f}\n", + hz, scale.getNearestNoteNumber(hz)) + .c_str(), + nullptr); + closePath(); + + beginPath(); + fillColor(200, 200, 200); + textBox(0.f, 45.f, 250.f, + std::format("x: {:.3f}\ny: {:.3f}\n", + x * getWidth(), y * getHeight()) + .c_str(), + nullptr); + closePath(); + + + // Pen position and pressure. + drawCircle(x, y, 0.f, 0.5f); +} + +void MouseUI::drawCircle(float x, float y, float z = 0.f, float p = 1.f) +{ + + static constexpr float circleRadius = 25.f; + x *= getWidth(); + y *= getHeight(); + z = 1.f - z; + + beginPath(); + strokeColor(1.f, 1.f, 1.f, 0.5f); + moveTo(x - z * circleRadius, y); + lineTo(x + z * circleRadius, y); + stroke(); + closePath(); + + beginPath(); + strokeColor(1.f, 1.f, 1.f, 0.5f); + moveTo(x, y - z * circleRadius); + lineTo(x, y + z * circleRadius); + stroke(); + closePath(); + + beginPath(); + fillColor(1.f, 1.f, 1.f, p); + strokeColor(255, 255, 255, 255); + circle(x, y, circleRadius); + fill(); + stroke(); + closePath(); + + beginPath(); + strokeColor(1.f, 1.f, 1.f, z); + circle(x, y, z * circleRadius); + stroke(); + closePath(); +} + +UI *createUI() +{ + return new MouseUI(); +} + +END_NAMESPACE_DISTRHO diff --git a/src/yaw-shep/ui.h b/src/yaw-shep/ui.h new file mode 100644 index 0000000..a2e24b8 --- /dev/null +++ b/src/yaw-shep/ui.h @@ -0,0 +1,37 @@ +#include "DistrhoUI.hpp" +#include "scale.h" +#ifdef DEBUG +#include +#include +#endif + +START_NAMESPACE_DISTRHO + +class MouseUI : public UI +{ +public: + explicit MouseUI(); + + //void buttonClicked(SubWidget *const widget, int) override; + void parameterChanged(uint32_t index, float value) override; + + void uiIdle() override; + bool onMouse(const MouseEvent &ev) override; + bool onMotion(const MotionEvent &ev) override; + void onResize(const ResizeEvent &ev) override; + void onNanoDisplay() override; + +private: + void drawCircle(float x, float y, float z, float p); + + bool record = false; + float x = 0.f; + float y = 0.f; + float vol = 0.f; + float hz = 55.f; + Scale scale{440.0}; + + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MouseUI) +}; + +END_NAMESPACE_DISTRHO \ No newline at end of file