DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

ExamplePluginMetronome.cpp (12710B)


      1 /*
      2  * DISTRHO Plugin Framework (DPF)
      3  * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
      4  * Copyright (C) 2020 Takamitsu Endo
      5  *
      6  * Permission to use, copy, modify, and/or distribute this software for any purpose with
      7  * or without fee is hereby granted, provided that the above copyright notice and this
      8  * permission notice appear in all copies.
      9  *
     10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
     11  * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
     12  * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
     13  * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
     14  * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
     15  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     16  */
     17 
     18 #include "DistrhoPlugin.hpp"
     19 
     20 START_NAMESPACE_DISTRHO
     21 
     22 // -----------------------------------------------------------------------------------------------------------
     23 
     24 /**
     25   1-pole lowpass filter to smooth out parameters and envelopes.
     26   This filter is guaranteed not to overshoot.
     27  */
     28 class Smoother {
     29     float kp;
     30 
     31 public:
     32     float value;
     33 
     34     Smoother()
     35         : kp(0.0f),
     36           value(0.0f) {}
     37 
     38     /**
     39       Set kp from cutoff frequency in Hz.
     40       For derivation, see the answer of Matt L. on the url below. Equation 3 is used.
     41 
     42       Computation is done on double for accuracy. When using float, kp will be inaccurate
     43       if the cutoffHz is below around 3.0 to 4.0 Hz.
     44 
     45       Reference:
     46       - Single-pole IIR low-pass filter - which is the correct formula for the decay coefficient?
     47         https://dsp.stackexchange.com/questions/54086/single-pole-iir-low-pass-filter-which-is-the-correct-formula-for-the-decay-coe
     48      */
     49     void setCutoff(const float sampleRate, const float cutoffHz)
     50     {
     51         double omega_c = 2.0 * M_PI * cutoffHz / sampleRate;
     52         double y = 1.0 - std::cos(omega_c);
     53         kp = float(-y + std::sqrt((y + 2.0) * y));
     54     }
     55 
     56     inline float process(const float input)
     57     {
     58         return value += kp * (input - value);
     59     }
     60 };
     61 
     62 // -----------------------------------------------------------------------------------------------------------
     63 
     64 /**
     65   Plugin that demonstrates tempo sync in DPF.
     66   The tempo sync implementation is on the first if branch in run() method.
     67  */
     68 class ExamplePluginMetronome : public Plugin
     69 {
     70 public:
     71     ExamplePluginMetronome()
     72         : Plugin(4, 0, 0), // 4 parameters, 0 programs, 0 states
     73           sampleRate(getSampleRate()),
     74           counter(0),
     75           wasPlaying(false),
     76           phase(0.0f),
     77           envelope(1.0f),
     78           decay(0.0f),
     79           gain(0.5f),
     80           semitone(72),
     81           cent(0),
     82           decayTime(0.2f)
     83     {
     84         sampleRateChanged(sampleRate);
     85     }
     86 
     87 protected:
     88    /* --------------------------------------------------------------------------------------------------------
     89     * Information */
     90 
     91    /**
     92       Get the plugin label.
     93       A plugin label follows the same rules as Parameter::symbol, with the exception that it can start with numbers.
     94     */
     95     const char* getLabel() const override
     96     {
     97         return "Metronome";
     98     }
     99 
    100    /**
    101       Get an extensive comment/description about the plugin.
    102     */
    103     const char* getDescription() const override
    104     {
    105         return "Simple metronome plugin which outputs impulse at the start of every beat.";
    106     }
    107 
    108    /**
    109       Get the plugin author/maker.
    110     */
    111     const char* getMaker() const override
    112     {
    113         return "DISTRHO";
    114     }
    115 
    116    /**
    117       Get the plugin homepage.
    118     */
    119     const char* getHomePage() const override
    120     {
    121         return "https://github.com/DISTRHO/DPF";
    122     }
    123 
    124    /**
    125       Get the plugin license name (a single line of text).
    126       For commercial plugins this should return some short copyright information.
    127     */
    128     const char* getLicense() const override
    129     {
    130         return "ISC";
    131     }
    132 
    133    /**
    134       Get the plugin version, in hexadecimal.
    135     */
    136     uint32_t getVersion() const override
    137     {
    138         return d_version(1, 0, 0);
    139     }
    140 
    141    /**
    142       Get the plugin unique Id.
    143       This value is used by LADSPA, DSSI and VST plugin formats.
    144     */
    145     int64_t getUniqueId() const override
    146     {
    147         return d_cconst('d', 'M', 'e', 't');
    148     }
    149 
    150    /* --------------------------------------------------------------------------------------------------------
    151     * Init */
    152 
    153    /**
    154       Initialize the audio port @a index.@n
    155       This function will be called once, shortly after the plugin is created.
    156     */
    157     void initAudioPort(bool input, uint32_t index, AudioPort& port) override
    158     {
    159         // treat meter audio ports as stereo
    160         port.groupId = kPortGroupMono;
    161 
    162         // everything else is as default
    163         Plugin::initAudioPort(input, index, port);
    164     }
    165 
    166    /**
    167       Initialize the parameter @a index.
    168       This function will be called once, shortly after the plugin is created.
    169     */
    170     void initParameter(uint32_t index, Parameter& parameter) override
    171     {
    172         parameter.hints = kParameterIsAutomatable;
    173 
    174         switch (index)
    175         {
    176         case 0:
    177             parameter.name = "Gain";
    178             parameter.hints |= kParameterIsLogarithmic;
    179             parameter.ranges.min = 0.001f;
    180             parameter.ranges.max = 1.0f;
    181             parameter.ranges.def = 0.5f;
    182             break;
    183         case 1:
    184             parameter.name = "DecayTime";
    185             parameter.hints |= kParameterIsLogarithmic;
    186             parameter.ranges.min = 0.001f;
    187             parameter.ranges.max = 1.0f;
    188             parameter.ranges.def = 0.2f;
    189             break;
    190         case 2:
    191             parameter.name = "Semitone";
    192             parameter.hints |= kParameterIsInteger;
    193             parameter.ranges.min = 0;
    194             parameter.ranges.max = 127;
    195             parameter.ranges.def = 72;
    196             break;
    197         case 3:
    198             parameter.name = "Cent";
    199             parameter.hints |= kParameterIsInteger;
    200             parameter.ranges.min = -100;
    201             parameter.ranges.max = 100;
    202             parameter.ranges.def = 0;
    203             break;
    204         }
    205 
    206         parameter.symbol = parameter.name;
    207     }
    208 
    209    /* --------------------------------------------------------------------------------------------------------
    210     * Internal data */
    211 
    212    /**
    213       Get the current value of a parameter.
    214     */
    215     float getParameterValue(uint32_t index) const override
    216     {
    217         switch (index)
    218         {
    219         case 0:
    220             return gain;
    221         case 1:
    222             return decayTime;
    223         case 2:
    224             return semitone;
    225         case 3:
    226             return cent;
    227         }
    228 
    229         return 0.0f;
    230     }
    231 
    232    /**
    233       Change a parameter value.
    234     */
    235     void setParameterValue(uint32_t index, float value) override
    236     {
    237         switch (index)
    238         {
    239         case 0:
    240             gain = value;
    241             break;
    242         case 1:
    243             decayTime = value;
    244             break;
    245         case 2:
    246             semitone = value;
    247             break;
    248         case 3:
    249             cent = value;
    250             break;
    251         }
    252     }
    253 
    254    /* --------------------------------------------------------------------------------------------------------
    255     * Process */
    256 
    257    /**
    258       Activate this plugin.
    259       We use this to reset our filter states.
    260     */
    261    void activate() override
    262    {
    263         deltaPhaseSmoother.value = 0.0f;
    264         envelopeSmoother.value = 0.0f;
    265         gainSmoother.value = gain;
    266    }
    267 
    268    /**
    269       Run/process function for plugins without MIDI input.
    270       `inputs` is commented out because this plugin has no inputs.
    271     */
    272     void run(const float** /* inputs */, float** outputs, uint32_t frames) override
    273     {
    274         const TimePosition& timePos(getTimePosition());
    275         float* const output = outputs[0];
    276 
    277         if (timePos.playing && timePos.bbt.valid)
    278         {
    279             // Better to use double when manipulating time.
    280             double secondsPerBeat = 60.0 / timePos.bbt.beatsPerMinute;
    281             double framesPerBeat  = sampleRate * secondsPerBeat;
    282             double beatFraction   = timePos.bbt.tick / timePos.bbt.ticksPerBeat;
    283 
    284             // If beatFraction is zero, next beat is exactly at the start of currenct cycle.
    285             // Otherwise, reset counter to the frames to the next beat.
    286             counter = d_isZero(beatFraction)
    287                     ? 0
    288                     : static_cast<uint32_t>(framesPerBeat * (1.0 - beatFraction));
    289 
    290             // Compute deltaPhase in normalized frequency.
    291             // semitone is midi note number, which is A4 (440Hz at standard tuning) at 69.
    292             // Frequency goes up to 1 octave higher at the start of bar.
    293             float frequency = 440.0f * std::pow(2.0f, (100.0f * (semitone - 69.0f) + cent) / 1200.0f);
    294             float deltaPhase = frequency / sampleRate;
    295             float octave = timePos.bbt.beat == 1 ? 2.0f : 1.0f;
    296 
    297             // Envelope reaches 1e-5 at decayTime after triggering.
    298             decay = std::pow(1e-5, 1.0 / (decayTime * sampleRate));
    299 
    300             // Reset phase and frequency at the start of transpose.
    301             if (!wasPlaying)
    302             {
    303                 phase = 0.0f;
    304 
    305                 deltaPhaseSmoother.value = deltaPhase;
    306                 envelopeSmoother.value = 0.0f;
    307                 gainSmoother.value = 0.0f;
    308             }
    309 
    310             for (uint32_t i = 0; i < frames; ++i)
    311             {
    312                 if (counter <= 0)
    313                 {
    314                     envelope = 1.0f;
    315                     counter = static_cast<uint32_t>(framesPerBeat + 0.5);
    316                     octave = (!wasPlaying || timePos.bbt.beat == static_cast<int32_t>(timePos.bbt.beatsPerBar)) ? 2.0f
    317                                                                                                                 : 1.0f;
    318                 }
    319                 --counter;
    320 
    321                 envelope *= decay;
    322 
    323                 phase += octave * deltaPhaseSmoother.process(deltaPhase);
    324                 phase -= std::floor(phase);
    325 
    326                 output[i] = gainSmoother.process(gain)
    327                           * envelopeSmoother.process(envelope)
    328                           * std::sin(float(2.0 * M_PI) * phase);
    329             }
    330         }
    331         else
    332         {
    333             // Stop metronome if not playing or timePos.bbt is invalid.
    334             std::memset(output, 0, sizeof(float)*frames);
    335         }
    336 
    337         wasPlaying = timePos.playing;
    338     }
    339 
    340    /* --------------------------------------------------------------------------------------------------------
    341     * Callbacks (optional) */
    342 
    343    /**
    344       Optional callback to inform the plugin about a sample rate change.
    345       This function will only be called when the plugin is deactivated.
    346     */
    347     void sampleRateChanged(double newSampleRate) override
    348     {
    349         sampleRate = newSampleRate;
    350 
    351         // Cutoff value was tuned manually.
    352         deltaPhaseSmoother.setCutoff(sampleRate, 100.0f);
    353         gainSmoother.setCutoff(sampleRate, 500.0f);
    354         envelopeSmoother.setCutoff(sampleRate, 250.0f);
    355     }
    356 
    357     // -------------------------------------------------------------------------------------------------------
    358 
    359 private:
    360     float sampleRate;
    361     uint32_t counter; // Stores number of frames to the next beat.
    362     bool wasPlaying;  // Used to reset phase and frequency at the start of transpose.
    363     float phase;      // Sine wave phase. Normalized in [0, 1).
    364     float envelope;   // Current value of gain envelope.
    365     float decay;      // Coefficient to decay envelope in a frame.
    366 
    367     Smoother deltaPhaseSmoother;
    368     Smoother envelopeSmoother;
    369     Smoother gainSmoother;
    370 
    371     // Parameters.
    372     float gain;
    373     float semitone;
    374     float cent;
    375     float decayTime;
    376 
    377    /**
    378       Set our plugin class as non-copyable and add a leak detector just in case.
    379     */
    380     DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ExamplePluginMetronome)
    381 };
    382 
    383 /* ------------------------------------------------------------------------------------------------------------
    384  * Plugin entry point, called by DPF to create a new plugin instance. */
    385 
    386 Plugin* createPlugin()
    387 {
    388     return new ExamplePluginMetronome();
    389 }
    390 
    391 // -----------------------------------------------------------------------------------------------------------
    392 
    393 END_NAMESPACE_DISTRHO