/*------------------------------------------------------------------------
Copyright (C) 2001-2023 Tom Murphy 7 and Sophia Poirier
This file is part of Transverb.
Transverb 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 2 of the License, or
(at your option) any later version.
Transverb 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.
You should have received a copy of the GNU General Public License
along with Transverb. If not, see .
To contact the author, use the contact form at http://destroyfx.org
------------------------------------------------------------------------*/
#include "transverbeditor.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "dfxmath.h"
#include "dfxmisc.h"
using namespace dfx::TV;
//-----------------------------------------------------------------------------
// positions
enum
{
kWideFaderX = 20,
kWideFaderY = 35,
kWideFaderInc = 24,
kWideFaderMoreInc = 42,
kWideFaderEvenMoreInc = 50,
kTallFaderX = kWideFaderX,
kTallFaderY = 265,
kTallFaderInc = 28,
kDisplayX = 318 + 1,
kDisplayWidth = 180,
kDisplayHeight = dfx::kFontContainerHeight_Snooty10px,
kDisplayY = kWideFaderY - kDisplayHeight - 1,
kQualityButtonX = 425,
kTomsoundButtonX = kQualityButtonX,
kFreezeButtonX = kWideFaderX,
kRandomButtonX = 185,
kButtonY = 236,
kButtonIncY = 18,
kFineDownButtonX = 503,
kFineUpButtonX = 512,
kFineButtonY = kWideFaderY,
kSpeedModeButtonX = 503,
kSpeedModeButtonY = 22,
kMidiLearnButtonX = 237,
kMidiLearnButtonY = kButtonY + kButtonIncY,
kMidiResetButtonX = 288,
kMidiResetButtonY = kMidiLearnButtonY,
kDFXLinkX = 107,
kDFXLinkY = 281,
kDestroyFXLinkX = 351,
kDestroyFXLinkY = 339
};
constexpr auto kDisplayFont = dfx::kFontName_Snooty10px;
constexpr DGColor kDisplayTextColor(103, 161, 215);
constexpr auto kDisplayTextSize = dfx::kFontSize_Snooty10px;
constexpr float kUnusedControlAlpha = 0.39f;
constexpr float kFineTuneInc = 0.0001f;
constexpr float kSemitonesPerOctave = 12.0f;
//-----------------------------------------------------------------------------
// value text display procedures
template
constexpr T modfMagnitude(T inValue)
{
T integral_ignored {};
return std::fabs(std::modf(inValue, &integral_ignored));
}
static bool bsizeDisplayProcedure(float inValue, char* outText, void*)
{
int const thousands = static_cast(inValue) / 1000;
auto const remainder = std::fmod(inValue, 1000.0f);
bool success = false;
if (thousands > 0)
{
success = std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%d,%05.1f", thousands, remainder) > 0;
}
else
{
success = std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%.1f", inValue) > 0;
}
dfx::StrlCat(outText, " ms", DGTextDisplay::kTextMaxLength);
return success;
}
static bool speedDisplayProcedure(float inValue, char* outText, void*)
{
std::array semitonesString {};
auto speed = inValue;
auto const remainder = modfMagnitude(speed);
float semitones = remainder * kSemitonesPerOctave;
// make sure that these float crap doesn't result in wacky stuff
// like displays that say "-1 octave & 12.00 semitones"
std::snprintf(semitonesString.data(), semitonesString.size(), "%.3f", semitones);
std::string const semitonesStdString(semitonesString.data());
if ((semitonesStdString == "12.000") || (semitonesStdString == "-12.000"))
{
semitones = 0.0f;
if (speed < 0.0f)
{
speed -= 0.003f;
}
else
{
speed += 0.003f;
}
}
auto const octaves = static_cast(speed);
if (speed > 0.0f)
{
if (octaves == 0)
{
return std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%s%.2f semitones", (semitones < 0.000003f) ? "" : "+", semitones) > 0;
}
auto const octavesSuffix = (octaves == 1) ? "" : "s";
return std::snprintf(outText, DGTextDisplay::kTextMaxLength, "+%d octave%s & %.2f semitones", octaves, octavesSuffix, semitones) > 0;
}
else if (octaves == 0)
{
return std::snprintf(outText, DGTextDisplay::kTextMaxLength, "-%.2f semitones", semitones) > 0;
}
else
{
auto const octavesSuffix = (octaves == -1) ? "" : "s";
return std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%d octave%s & %.2f semitones", octaves, octavesSuffix, semitones) > 0;
}
}
static std::optional speedTextConvertProcedure(std::string const& inText, DGTextDisplay*)
{
auto filteredText = inText;
// TODO: does not support locale for number format, and ignores minus and periods that are not part of fractional numbers
std::erase_if(filteredText, [](auto character)
{
return !(std::isdigit(character) || std::isspace(character) || (character == '-') || (character == '.'));
});
float octaves = 0.0f, semitones = 0.0f;
auto const scanCount = std::sscanf(filteredText.c_str(), "%f%f", &octaves, &semitones);
if ((scanCount > 0) && (scanCount != EOF))
{
// the user only entered one number, which is for octaves,
// so convert any fractional part of the octaves value into semitones
if (scanCount == 1)
{
// unless we find the one number labeled as semitones, in which case treat as those
std::vector word(inText.size() + 1, '\0');
auto const wordScanCount = std::sscanf(inText.c_str(), "%*f%s", word.data());
if ((wordScanCount > 0) && (wordScanCount != EOF) && dfx::ToLower(word.data()).starts_with("semi"))
{
return octaves / kSemitonesPerOctave;
}
return octaves;
}
// ignore the sign for the semitones unless the octaves value was in the zero range
auto const negative = std::signbit(octaves) || ((std::fabs(octaves) < 1.0f) && std::signbit(semitones));
return (std::floor(std::fabs(octaves)) + (std::fabs(semitones) / kSemitonesPerOctave)) * (negative ? -1.0f : 1.0f);
}
return {};
}
static bool feedbackDisplayProcedure(float inValue, char* outText, void*)
{
return std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%d%%", static_cast(inValue)) > 0;
}
static bool distDisplayProcedure(float inValue, char* outText, void* inEditor)
{
float const distance = inValue * static_cast(inEditor)->getparameter_f(kBsize);
int const thousands = static_cast(distance) / 1000;
auto const remainder = std::fmod(distance, 1000.0f);
bool success = false;
if (thousands > 0)
{
success = std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%d,%06.2f", thousands, remainder) > 0;
}
else
{
success = std::snprintf(outText, DGTextDisplay::kTextMaxLength, "%.2f", distance) > 0;
}
dfx::StrlCat(outText, " ms", DGTextDisplay::kTextMaxLength);
return success;
}
static float distValueFromTextConvertProcedure(float inValue, DGTextDisplay* inTextDisplay)
{
auto const bsize = static_cast(inTextDisplay->getOwnerEditor()->getparameter_f(kBsize));
return !dfx::math::IsZero(bsize) ? (inValue / bsize) : inValue;
}
//-----------------------------------------------------------------------------
static double nearestIntegerBelow(double number)
{
bool const sign = (number >= 0.0);
auto const fraction = modfMagnitude(number);
if (fraction <= 0.0001)
{
return number;
}
if (sign)
{
return static_cast(static_cast(std::fabs(number)));
}
else
{
return -static_cast(static_cast(std::fabs(number)) + 1);
}
}
static double nearestIntegerAbove(double number)
{
bool const sign = (number >= 0.0);
double const fraction = modfMagnitude(number);
if (fraction <= 0.0001)
{
return number;
}
if (sign)
{
return static_cast(static_cast(std::fabs(number)) + 1);
}
else
{
return -static_cast(static_cast(std::fabs(number)));
}
}
//-----------------------------------------------------------------------------
void TransverbSpeedTuneButton::onMouseDownEvent(VSTGUI::MouseDownEvent& ioEvent)
{
if ((mTuneMode == kSpeedMode_Fine) || !ioEvent.buttonState.isLeft())
{
return DGFineTuneButton::onMouseDownEvent(ioEvent);
}
beginEdit();
mEntryValue = getValue();
auto const oldSpeedValue = getOwnerEditor()->getparameter_f(getParameterID());
bool const isInc = (mValueChangeAmount >= 0.0f);
double const snapAmount = isInc ? 1.001 : -1.001;
double const snapScalar = (mTuneMode == kSpeedMode_Semitone) ? 12.0 : 1.0;
double newSpeedValue = (oldSpeedValue * snapScalar) + snapAmount;
newSpeedValue = isInc ? nearestIntegerBelow(newSpeedValue) : nearestIntegerAbove(newSpeedValue);
newSpeedValue /= snapScalar;
if (isParameterAttached())
{
newSpeedValue = getOwnerEditor()->dfxgui_ContractParameterValue(getParameterID(), newSpeedValue);
}
mNewValue = std::clamp(static_cast(newSpeedValue), getMin(), getMax());
mMouseIsDown = true;
if (mNewValue != mEntryValue)
{
setValue(mNewValue);
valueChanged();
invalid();
}
else
{
//redraw(); // at least make sure that redrawing occurs for mMouseIsDown change
// XXX or do I prefer it not to do the mouse-down state when nothing is happening anyway?
}
ioEvent.consumed = true;
}
#pragma mark -
//-----------------------------------------------------------------------------
DFX_EDITOR_ENTRY(TransverbEditor)
//-----------------------------------------------------------------------------
TransverbEditor::TransverbEditor(DGEditorListenerInstance inInstance)
: DfxGuiEditor(inInstance)
{
for (size_t i = 0; i < kNumDelays; i++)
{
RegisterPropertyChange(speedModeIndexToPropertyID(i));
}
}
//-----------------------------------------------------------------------------
void TransverbEditor::OpenEditor()
{
// slider handles
auto const horizontalSliderHandleImage = LoadImage("purple-wide-fader-handle.png");
auto const grayHorizontalSliderHandleImage = LoadImage("grey-wide-fader-handle.png");
auto const horizontalSliderHandleImage_glowing = LoadImage("wide-fader-handle-glowing.png");
auto const verticalSliderHandleImage = LoadImage("tall-fader-handle.png");
auto const verticalSliderHandleImage_glowing = LoadImage("tall-fader-handle-glowing.png");
// slider backgrounds
auto const horizontalSliderBackgroundImage = LoadImage("purple-wide-fader-slide.png");
auto const grayHorizontalSliderBackgroundImage = LoadImage("grey-wide-fader-slide.png");
auto const verticalSliderBackgroundImage = LoadImage("tall-fader-slide.png");
// buttons
auto const qualityButtonImage = LoadImage("quality-button.png");
auto const tomsoundButtonImage = LoadImage("tomsound-button.png");
auto const freezeButtonImage = LoadImage("freeze-button.png");
auto const randomizeButtonImage = LoadImage("randomize-button.png");
auto const fineDownButtonImage = LoadImage("fine-down-button.png");
auto const fineUpButtonImage = LoadImage("fine-up-button.png");
auto const speedModeButtonImage = LoadImage("speed-mode-button.png");
auto const midiLearnButtonImage = LoadImage("midi-learn-button.png");
auto const midiResetButtonImage = LoadImage("midi-reset-button.png");
auto const dfxLinkButtonImage = LoadImage("dfx-link.png");
auto const destroyFXLinkButtonImage = LoadImage("super-destroy-fx-link.png");
DGRect pos, textDisplayPos, tuneDownButtonPos, tuneUpButtonPos;
constexpr int sliderRangeMargin = 1;
// Make horizontal sliders and add them to the pane
pos.set(kWideFaderX, kWideFaderY, horizontalSliderBackgroundImage->getWidth(), horizontalSliderBackgroundImage->getHeight());
textDisplayPos.set(kDisplayX, kDisplayY, kDisplayWidth, kDisplayHeight);
tuneDownButtonPos.set(kFineDownButtonX, kFineButtonY, fineDownButtonImage->getWidth(), fineDownButtonImage->getHeight() / 2);
tuneUpButtonPos.set(kFineUpButtonX, kFineButtonY, fineUpButtonImage->getWidth(), fineUpButtonImage->getHeight() / 2);
for (dfx::ParameterID parameterID = kSpeed1; parameterID <= kDistParameters.back(); parameterID++)
{
VSTGUI::CParamDisplayValueToStringProc displayProc = nullptr;
void* userData = nullptr;
// TODO C++23: std::ranges::contains
if (std::ranges::find(kSpeedParameters, parameterID) != kSpeedParameters.cend())
{
displayProc = speedDisplayProcedure;
}
// TODO C++23: std::ranges::contains
else if (std::ranges::find(kFeedParameters, parameterID) != kFeedParameters.cend())
{
displayProc = feedbackDisplayProcedure;
}
else
{
displayProc = distDisplayProcedure;
userData = this;
}
assert(displayProc);
emplaceControl(this, parameterID, pos, dfx::kAxis_Horizontal, horizontalSliderHandleImage, horizontalSliderBackgroundImage, sliderRangeMargin)->setAlternateHandle(horizontalSliderHandleImage_glowing);
auto const textDisplay = emplaceControl(this, parameterID, textDisplayPos, displayProc, userData, nullptr,
dfx::TextAlignment::Right, kDisplayTextSize, kDisplayTextColor, kDisplayFont);
if (auto const speedParameterID = std::ranges::find(kSpeedParameters, parameterID); speedParameterID != kSpeedParameters.cend())
{
auto const head = static_cast(std::ranges::distance(kSpeedParameters.cbegin(), speedParameterID));
mSpeedDownButtons[head] = emplaceControl(this, parameterID, tuneDownButtonPos, fineDownButtonImage, -kFineTuneInc);
mSpeedUpButtons[head] = emplaceControl(this, parameterID, tuneUpButtonPos, fineUpButtonImage, kFineTuneInc);
textDisplay->setTextToValueProc(speedTextConvertProcedure);
}
else
{
emplaceControl(this, parameterID, tuneDownButtonPos, fineDownButtonImage, -kFineTuneInc);
emplaceControl(this, parameterID, tuneUpButtonPos, fineUpButtonImage, kFineTuneInc);
}
auto yoff = kWideFaderInc;
for (size_t head = 0; head < kNumDelays; head++)
{
if (parameterID == kDistParameters[head])
{
bool const lastHead = (kDistParameters[head] == kDistParameters.back());
yoff = lastHead ? kWideFaderEvenMoreInc : kWideFaderMoreInc;
mDistanceTextDisplays[head] = textDisplay;
textDisplay->setValueFromTextConvertProc(distValueFromTextConvertProcedure);
}
}
pos.offset(0, yoff);
textDisplayPos.offset(0, yoff);
tuneDownButtonPos.offset(0, yoff);
tuneUpButtonPos.offset(0, yoff);
}
emplaceControl(this, kBsize, pos, dfx::kAxis_Horizontal, grayHorizontalSliderHandleImage, grayHorizontalSliderBackgroundImage, sliderRangeMargin)->setAlternateHandle(horizontalSliderHandleImage_glowing);
emplaceControl(this, kBsize, textDisplayPos, bsizeDisplayProcedure, nullptr, nullptr,
dfx::TextAlignment::Right, kDisplayTextSize, kDisplayTextColor, kDisplayFont);
emplaceControl(this, kBsize, tuneDownButtonPos, fineDownButtonImage, -kFineTuneInc);
emplaceControl(this, kBsize, tuneUpButtonPos, fineUpButtonImage, kFineTuneInc);
// make horizontal sliders and add them to the view
pos.set(kTallFaderX, kTallFaderY, verticalSliderBackgroundImage->getWidth(), verticalSliderBackgroundImage->getHeight());
for (dfx::ParameterID parameterID = kDrymix; parameterID <= kMixParameters.back(); parameterID++)
{
emplaceControl(this, parameterID, pos, dfx::kAxis_Vertical, verticalSliderHandleImage, verticalSliderBackgroundImage, sliderRangeMargin)->setAlternateHandle(verticalSliderHandleImage_glowing);
pos.offset(kTallFaderInc, 0);
}
// quality mode button
pos.set(kQualityButtonX, kButtonY, qualityButtonImage->getWidth() / 2, qualityButtonImage->getHeight() / kQualityMode_NumModes);
emplaceControl(this, kQuality, pos, qualityButtonImage, DGButton::Mode::Increment, true);
// TOMSOUND button
emplaceControl(this, kTomsound, kTomsoundButtonX, kButtonY + kButtonIncY, tomsoundButtonImage, true);
// freeze button
emplaceControl(this, kFreeze, kFreezeButtonX, kButtonY, freezeButtonImage, true);
// randomize button
pos.set(kRandomButtonX, kButtonY, randomizeButtonImage->getWidth(), randomizeButtonImage->getHeight() / 2);
auto const button = emplaceControl(this, pos, randomizeButtonImage, 2, DGButton::Mode::Momentary);
button->setUserProcedure(std::bind(&TransverbEditor::randomizeparameters, this, true));
// speed mode buttons
for (size_t speedModeIndex = 0; speedModeIndex < mSpeedModeButtons.size(); speedModeIndex++)
{
pos.set(kSpeedModeButtonX, kSpeedModeButtonY + (((kWideFaderInc * 2) + kWideFaderMoreInc) * speedModeIndex),
speedModeButtonImage->getWidth() / 2, speedModeButtonImage->getHeight() / kSpeedMode_NumModes);
mSpeedModeButtons[speedModeIndex] = emplaceControl(this, pos, speedModeButtonImage, kSpeedMode_NumModes, DGButton::Mode::Increment, true);
mSpeedModeButtons[speedModeIndex]->setUserProcedure(std::bind_front(&TransverbEditor::HandleSpeedModeButton, this, speedModeIndex));
}
// MIDI learn button
CreateMidiLearnButton(kMidiLearnButtonX, kMidiLearnButtonY, midiLearnButtonImage, true);
// MIDI reset button
CreateMidiResetButton(kMidiResetButtonX, kMidiResetButtonY, midiResetButtonImage);
// DFX web link
pos.set(kDFXLinkX, kDFXLinkY, dfxLinkButtonImage->getWidth(), dfxLinkButtonImage->getHeight() / 2);
emplaceControl(this, pos, dfxLinkButtonImage, DESTROYFX_URL);
// Super Destroy FX web link
pos.set(kDestroyFXLinkX, kDestroyFXLinkY, destroyFXLinkButtonImage->getWidth(), destroyFXLinkButtonImage->getHeight() / 2);
emplaceControl(this, pos, destroyFXLinkButtonImage, DESTROYFX_URL);
SetParameterHelpText(kBsize, "the size of the buffer that both delays use");
constexpr char const* const speedHelpText = "how quickly or slowly the delay playback moves through the delay buffer";
for (auto const parameterID : kSpeedParameters)
{
SetParameterHelpText(parameterID, speedHelpText);
}
std::ranges::for_each(mSpeedModeButtons, [](auto* control){ control->setHelpText(speedHelpText); });
for (auto const parameterID : kFeedParameters)
{
SetParameterHelpText(parameterID, "how much of the delay sound gets mixed back into the delay buffer");
}
for (auto const parameterID : kDistParameters)
{
SetParameterHelpText(parameterID, "how far behind the delay is from the input signal (only really makes a difference when the speed is at zero)");
}
SetParameterHelpText(kDrymix, "input audio mix level");
for (size_t i = 0; i < kMixParameters.size(); i++)
{
SetParameterHelpText(kMixParameters[i], ("delay head #" + std::to_string(i + 1) + " mix level").c_str());
}
SetParameterHelpText(kQuality, "level of transposition quality of the delays' speed");
SetParameterHelpText(kTomsound, "megaharsh sound");
SetParameterHelpText(kFreeze, "pause recording new audio into the delay buffer");
#if 1
pos.set(120, getFrame()->getHeight() - 24, 210, 16);
emplaceControl(this, kDistChangeMode, pos, dfx::TextAlignment::Center, kDisplayTextSize, VSTGUI::MakeCColor(92, 151, 209), kDisplayFont);
#endif
}
//-----------------------------------------------------------------------------
void TransverbEditor::PostOpenEditor()
{
for (size_t i = 0; i < kNumDelays; i++)
{
HandleSpeedModeChange(i);
}
HandleFreezeChange();
}
//-----------------------------------------------------------------------------
void TransverbEditor::CloseEditor()
{
mSpeedModeButtons.fill(nullptr);
mSpeedDownButtons.fill(nullptr);
mSpeedUpButtons.fill(nullptr);
mDistanceTextDisplays.fill(nullptr);
}
//-----------------------------------------------------------------------------
void TransverbEditor::parameterChanged(dfx::ParameterID inParameterID)
{
switch (inParameterID)
{
case kBsize:
// trigger re-conversion of numerical value to text
std::ranges::for_each(mDistanceTextDisplays, std::bind_front(&DGTextDisplay::refreshText));
break;
case kFreeze:
HandleFreezeChange();
break;
}
}
//-----------------------------------------------------------------------------
void TransverbEditor::HandlePropertyChange(dfx::PropertyID inPropertyID, dfx::Scope /*inScope*/, unsigned int /*inItemIndex*/)
{
if (isSpeedModePropertyID(inPropertyID))
{
HandleSpeedModeChange(speedModePropertyIDToIndex(inPropertyID));
}
}
//-----------------------------------------------------------------------------
void TransverbEditor::HandleSpeedModeButton(size_t inIndex, long inValue)
{
auto const value_fixedSize = static_cast(inValue);
[[maybe_unused]] bool const ok = dfxgui_SetProperty(speedModeIndexToPropertyID(inIndex), value_fixedSize);
assert(ok);
}
//-----------------------------------------------------------------------------
void TransverbEditor::HandleSpeedModeChange(size_t inIndex)
{
auto const tuneMode = dfxgui_GetProperty(speedModeIndexToPropertyID(inIndex));
assert(tuneMode.has_value());
if (tuneMode)
{
auto&& speedModeButton = mSpeedModeButtons.at(inIndex);
assert(speedModeButton);
speedModeButton->setValue_i(*tuneMode);
if (speedModeButton->isDirty())
{
speedModeButton->invalid();
}
mSpeedDownButtons.at(inIndex)->setTuneMode(*tuneMode);
mSpeedUpButtons.at(inIndex)->setTuneMode(*tuneMode);
}
}
//-----------------------------------------------------------------------------
void TransverbEditor::HandleFreezeChange()
{
float const alpha = getparameter_b(kFreeze) ? kUnusedControlAlpha : 1.f;
std::ranges::for_each(kFeedParameters, [this, alpha](auto parameterID){ SetParameterAlpha(parameterID, alpha); });
}