362 lines
13 KiB
C++
362 lines
13 KiB
C++
/*
|
|
* DISTRHO Cardinal Plugin
|
|
* Copyright (C) 2021-2022 Bram Giesen
|
|
* Copyright (C) 2022 Filipe Coelho <falktx@falktx.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License as
|
|
* published by the Free Software Foundation; either version 3 of
|
|
* the License, or any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* For a full copy of the GNU General Public License see the LICENSE file.
|
|
*/
|
|
|
|
#include "plugincontext.hpp"
|
|
#include "ModuleWidgets.hpp"
|
|
#include "Widgets.hpp"
|
|
|
|
extern "C" {
|
|
#include "aubio.h"
|
|
}
|
|
|
|
USE_NAMESPACE_DISTRHO;
|
|
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
// aubio setup values (tested under 48 kHz sample rate)
|
|
static constexpr const uint32_t kAubioHopSize = 1;
|
|
static constexpr const uint32_t kAubioBufferSize = (1024 + 256 + 128) / kAubioHopSize;
|
|
|
|
// default values
|
|
static constexpr const float kDefaultSensitivity = 50.f;
|
|
static constexpr const float kDefaultTolerance = 6.25f;
|
|
static constexpr const float kDefaultThreshold = 12.5f;
|
|
|
|
// static checks
|
|
static_assert(sizeof(smpl_t) == sizeof(float), "smpl_t is float");
|
|
static_assert(kAubioBufferSize % kAubioHopSize == 0, "kAubioBufferSize / kAubioHopSize has no remainder");
|
|
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
struct AudioToCVPitch : Module {
|
|
enum ParamIds {
|
|
PARAM_SENSITIVITY,
|
|
PARAM_CONFIDENCETHRESHOLD,
|
|
PARAM_TOLERANCE,
|
|
PARAM_OCTAVE,
|
|
NUM_PARAMS
|
|
};
|
|
enum InputIds {
|
|
AUDIO_INPUT,
|
|
NUM_INPUTS
|
|
};
|
|
enum OutputIds {
|
|
CV_PITCH,
|
|
CV_GATE,
|
|
NUM_OUTPUTS
|
|
};
|
|
enum LightIds {
|
|
NUM_LIGHTS
|
|
};
|
|
|
|
bool holdOutputPitch = true;
|
|
bool smooth = true;
|
|
int octave = 0;
|
|
|
|
float lastKnownPitchInHz = 0.f;
|
|
float lastKnownPitchConfidence = 0.f;
|
|
|
|
float lastUsedTolerance = kDefaultTolerance;
|
|
float lastUsedOutputPitch = 0.f;
|
|
float lastUsedOutputSignal = 0.f;
|
|
|
|
fvec_t* const detectedPitch = new_fvec(1);
|
|
fvec_t* const inputBuffer = new_fvec(kAubioBufferSize);
|
|
uint32_t inputBufferPos = 0;
|
|
|
|
aubio_pitch_t* pitchDetector = nullptr;
|
|
|
|
dsp::SlewLimiter smoothOutputSignal;
|
|
|
|
AudioToCVPitch()
|
|
{
|
|
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
|
|
|
|
configInput(AUDIO_INPUT, "Audio");
|
|
configOutput(CV_PITCH, "Pitch");
|
|
configOutput(CV_GATE, "Gate");
|
|
configParam(PARAM_SENSITIVITY, 0.1f, 99.f, kDefaultSensitivity, "Sensitivity", " %");
|
|
configParam(PARAM_CONFIDENCETHRESHOLD, 0.f, 99.f, kDefaultThreshold, "Confidence Threshold", " %");
|
|
configParam(PARAM_TOLERANCE, 0.f, 99.f, kDefaultTolerance, "Tolerance", " %");
|
|
}
|
|
|
|
void process(const ProcessArgs& args) override
|
|
{
|
|
float cvPitch = lastUsedOutputPitch;
|
|
float cvSignal = lastUsedOutputSignal;
|
|
|
|
inputBuffer->data[inputBufferPos] = inputs[AUDIO_INPUT].getVoltage() * 0.1f
|
|
* params[PARAM_SENSITIVITY].getValue();
|
|
|
|
if (++inputBufferPos == kAubioBufferSize)
|
|
{
|
|
inputBufferPos = 0;
|
|
|
|
const float tolerance = params[PARAM_TOLERANCE].getValue();
|
|
if (d_isNotEqual(lastUsedTolerance, tolerance))
|
|
{
|
|
lastUsedTolerance = tolerance;
|
|
aubio_pitch_set_tolerance(pitchDetector, tolerance * 0.01f);
|
|
}
|
|
|
|
aubio_pitch_do(pitchDetector, inputBuffer, detectedPitch);
|
|
const float detectedPitchInHz = fvec_get_sample(detectedPitch, 0);
|
|
const float pitchConfidence = aubio_pitch_get_confidence(pitchDetector);
|
|
|
|
if (detectedPitchInHz > 0.f && pitchConfidence >= params[PARAM_CONFIDENCETHRESHOLD].getValue() * 0.01f)
|
|
{
|
|
const float linearPitch = 12.f * (log2f(detectedPitchInHz / 440.f) + octave - 5) + 69.f;
|
|
cvPitch = std::max(-10.f, std::min(10.f, linearPitch * (1.f/12.f)));
|
|
lastKnownPitchInHz = detectedPitchInHz;
|
|
cvSignal = 10.f;
|
|
}
|
|
else
|
|
{
|
|
if (! holdOutputPitch)
|
|
lastKnownPitchInHz = cvPitch = 0.0f;
|
|
|
|
cvSignal = 0.f;
|
|
}
|
|
|
|
lastKnownPitchConfidence = pitchConfidence;
|
|
lastUsedOutputPitch = cvPitch;
|
|
lastUsedOutputSignal = cvSignal;
|
|
}
|
|
|
|
outputs[CV_PITCH].setVoltage(smooth ? smoothOutputSignal.process(args.sampleTime, cvPitch) : cvPitch);
|
|
outputs[CV_GATE].setVoltage(cvSignal);
|
|
}
|
|
|
|
void onReset() override
|
|
{
|
|
inputBufferPos = 0;
|
|
smooth = true;
|
|
holdOutputPitch = true;
|
|
octave = 0;
|
|
}
|
|
|
|
void onSampleRateChange(const SampleRateChangeEvent& e) override
|
|
{
|
|
float tolerance;
|
|
|
|
if (pitchDetector != nullptr)
|
|
{
|
|
tolerance = aubio_pitch_get_tolerance(pitchDetector);
|
|
del_aubio_pitch(pitchDetector);
|
|
}
|
|
else
|
|
{
|
|
tolerance = kDefaultTolerance * 0.01f;
|
|
}
|
|
|
|
pitchDetector = new_aubio_pitch("yinfast", kAubioBufferSize, kAubioHopSize, e.sampleRate);
|
|
DISTRHO_SAFE_ASSERT_RETURN(pitchDetector != nullptr,);
|
|
|
|
aubio_pitch_set_silence(pitchDetector, -30.0f);
|
|
aubio_pitch_set_tolerance(pitchDetector, tolerance);
|
|
aubio_pitch_set_unit(pitchDetector, "Hz");
|
|
|
|
const double fall = 1.0 / (double(kAubioBufferSize) / e.sampleRate);
|
|
smoothOutputSignal.reset();
|
|
smoothOutputSignal.setRiseFall(fall, fall);
|
|
}
|
|
|
|
json_t* dataToJson() override
|
|
{
|
|
json_t* const rootJ = json_object();
|
|
DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
|
|
|
|
json_object_set_new(rootJ, "holdOutputPitch", json_boolean(holdOutputPitch));
|
|
json_object_set_new(rootJ, "smooth", json_boolean(smooth));
|
|
json_object_set_new(rootJ, "octave", json_integer(octave));
|
|
|
|
return rootJ;
|
|
}
|
|
|
|
void dataFromJson(json_t* const rootJ) override
|
|
{
|
|
if (json_t* const holdOutputPitchJ = json_object_get(rootJ, "holdOutputPitch"))
|
|
holdOutputPitch = json_boolean_value(holdOutputPitchJ);
|
|
|
|
if (json_t* const smoothJ = json_object_get(rootJ, "smooth"))
|
|
smooth = json_boolean_value(smoothJ);
|
|
|
|
if (json_t* const octaveJ = json_object_get(rootJ, "octave"))
|
|
octave = json_integer_value(octaveJ);
|
|
}
|
|
};
|
|
|
|
struct SmallPercentageNanoKnob : NanoKnob<2, 0> {
|
|
SmallPercentageNanoKnob() {
|
|
box.size = Vec(32, 32);
|
|
displayLabel = "";
|
|
}
|
|
|
|
void onChange(const ChangeEvent&) override
|
|
{
|
|
engine::ParamQuantity* const pq = getParamQuantity();
|
|
DISTRHO_SAFE_ASSERT_RETURN(pq != nullptr,);
|
|
|
|
displayString = string::f("%.1f %%", pq->getDisplayValue());
|
|
}
|
|
};
|
|
|
|
struct AudioToCVPitchWidget : ModuleWidgetWith9HP {
|
|
static constexpr const float startX = 10.0f;
|
|
static constexpr const float startY_top = 71.0f;
|
|
static constexpr const float startY_cv1 = 115.0f;
|
|
static constexpr const float startY_cv2 = 145.0f;
|
|
static constexpr const float padding = 32.0f;
|
|
|
|
AudioToCVPitch* const module;
|
|
std::string monoFontPath;
|
|
|
|
AudioToCVPitchWidget(AudioToCVPitch* const m)
|
|
: module(m)
|
|
{
|
|
setModule(m);
|
|
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/AudioToCVPitch.svg")));
|
|
monoFontPath = asset::system("res/fonts/ShareTechMono-Regular.ttf");
|
|
|
|
createAndAddScrews();
|
|
|
|
addInput(createInput<PJ301MPort>(Vec(startX, startY_cv1 + 0 * padding), m, AudioToCVPitch::AUDIO_INPUT));
|
|
addOutput(createOutput<PJ301MPort>(Vec(startX, startY_cv2 + 0 * padding), m, AudioToCVPitch::CV_PITCH));
|
|
addOutput(createOutput<PJ301MPort>(Vec(startX, startY_cv2 + 1 * padding), m, AudioToCVPitch::CV_GATE));
|
|
|
|
SmallPercentageNanoKnob* knobSens = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 85.f),
|
|
module, AudioToCVPitch::PARAM_SENSITIVITY);
|
|
knobSens->displayString = "50 %";
|
|
addChild(knobSens);
|
|
|
|
SmallPercentageNanoKnob* knobTolerance = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 135.f),
|
|
module, AudioToCVPitch::PARAM_TOLERANCE);
|
|
knobTolerance->displayString = "6.25 %";
|
|
addChild(knobTolerance);
|
|
|
|
SmallPercentageNanoKnob* knobThres = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 185.f),
|
|
module, AudioToCVPitch::PARAM_CONFIDENCETHRESHOLD);
|
|
knobThres->displayString = "12.5 %";
|
|
addChild(knobThres);
|
|
}
|
|
|
|
void drawInputLine(NVGcontext* const vg, const uint offset, const char* const text)
|
|
{
|
|
const float y = startY_cv1 + offset * padding;
|
|
nvgBeginPath(vg);
|
|
nvgFillColor(vg, nvgRGB(0xd0, 0xd0, 0xd0));
|
|
nvgText(vg, startX + 28, y + 16, text, nullptr);
|
|
}
|
|
|
|
void drawOutputLine(NVGcontext* const vg, const uint offset, const char* const text)
|
|
{
|
|
const float y = startY_cv2 + offset * padding;
|
|
nvgBeginPath(vg);
|
|
nvgRoundedRect(vg, startX - 1.f, y - 2.f, box.size.x - startX * 2 + 2.f, 28.f, 4);
|
|
nvgFillColor(vg, nvgRGB(0xd0, 0xd0, 0xd0));
|
|
nvgFill(vg);
|
|
nvgBeginPath(vg);
|
|
nvgFillColor(vg, color::BLACK);
|
|
nvgText(vg, startX + 28, y + 16, text, nullptr);
|
|
}
|
|
|
|
void draw(const DrawArgs& args) override
|
|
{
|
|
drawBackground(args.vg);
|
|
|
|
nvgFontFaceId(args.vg, 0);
|
|
nvgFontSize(args.vg, 14);
|
|
|
|
drawInputLine(args.vg, 0, "Input");
|
|
drawOutputLine(args.vg, 0, "Pitch");
|
|
drawOutputLine(args.vg, 1, "Gate");
|
|
|
|
nvgFontSize(args.vg, 11);
|
|
nvgBeginPath(args.vg);
|
|
nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0));
|
|
nvgTextLineHeight(args.vg, 0.8f);
|
|
nvgTextAlign(args.vg, NVG_ALIGN_CENTER);
|
|
nvgTextBox(args.vg, startX + 6.f, startY_cv2 + 75.f, 11.f, "S\ne\nn\ns", nullptr);
|
|
nvgTextBox(args.vg, box.size.x - startX - 16.f, startY_cv2 + 130.f, 11.f, "T\no\nl", nullptr);
|
|
nvgTextBox(args.vg, startX + 6.f, startY_cv2 + 175.f, 11.f, "T\nh\nr\ne\ns", nullptr);
|
|
|
|
nvgBeginPath(args.vg);
|
|
nvgRoundedRect(args.vg, 10.0f, startY_top, box.size.x - 20.f, 38.0f, 4);
|
|
nvgFillColor(args.vg, color::BLACK);
|
|
nvgFill(args.vg);
|
|
|
|
ModuleWidgetWith9HP::draw(args);
|
|
}
|
|
|
|
void drawLayer(const DrawArgs& args, int layer) override
|
|
{
|
|
if (layer == 1)
|
|
{
|
|
nvgFontSize(args.vg, 17);
|
|
nvgFillColor(args.vg, nvgRGBf(0.76f, 0.11f, 0.22f));
|
|
|
|
char pitchConfString[24];
|
|
char pitchFreqString[24];
|
|
|
|
std::shared_ptr<Font> monoFont = APP->window->loadFont(monoFontPath);
|
|
|
|
if (module != nullptr && monoFont != nullptr)
|
|
{
|
|
nvgFontFaceId(args.vg, monoFont->handle);
|
|
|
|
std::snprintf(pitchConfString, sizeof(pitchConfString), "%5.1f %%", module->lastKnownPitchConfidence * 100.f);
|
|
std::snprintf(pitchFreqString, sizeof(pitchFreqString), "%5.0f Hz", module->lastKnownPitchInHz);
|
|
}
|
|
else
|
|
{
|
|
std::strcpy(pitchConfString, "0.0 %");
|
|
std::strcpy(pitchFreqString, "0 Hz");
|
|
}
|
|
|
|
nvgTextAlign(args.vg, NVG_ALIGN_CENTER);
|
|
nvgText(args.vg, box.size.x * 0.5f, startY_top + 15.0f, pitchConfString, nullptr);
|
|
nvgText(args.vg, box.size.x * 0.5f, startY_top + 33.0f, pitchFreqString, nullptr);
|
|
}
|
|
|
|
ModuleWidgetWith9HP::drawLayer(args, layer);
|
|
}
|
|
|
|
void appendContextMenu(Menu* const menu) override
|
|
{
|
|
menu->addChild(new MenuSeparator);
|
|
|
|
menu->addChild(createBoolPtrMenuItem("Hold Output Pitch", "", &module->holdOutputPitch));
|
|
menu->addChild(createBoolPtrMenuItem("Smooth Output Pitch", "", &module->smooth));
|
|
|
|
static const std::vector<int> octaves = {-4, -3, -2, -1, 0, 1, 2, 3, 4};
|
|
menu->addChild(createSubmenuItem("Octave", string::f("%d", module->octave), [=](Menu* menu) {
|
|
for (size_t i = 0; i < octaves.size(); i++) {
|
|
menu->addChild(createCheckMenuItem(string::f("%d", octaves[i]), "",
|
|
[=]() {return module->octave == octaves[i];},
|
|
[=]() {module->octave = octaves[i];}
|
|
));
|
|
}
|
|
}));
|
|
}
|
|
};
|
|
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
Model* modelAudioToCVPitch = createModel<AudioToCVPitch, AudioToCVPitchWidget>("AudioToCVPitch");
|
|
|
|
// --------------------------------------------------------------------------------------------------------------------
|