@@ -70,6 +70,7 @@ option('plugins', | |||
'tal-vocoder-2', | |||
'temper', | |||
'vex', | |||
'vitalium', | |||
'wolpertinger', | |||
], | |||
) |
@@ -6,6 +6,7 @@ if linux_embed | |||
else | |||
plugins = [ | |||
'chow', | |||
'vitalium', | |||
] | |||
endif | |||
@@ -0,0 +1 @@ | |||
vital-git |
@@ -0,0 +1,108 @@ | |||
/* ========================================================================================= | |||
This is an auto-generated file: Any edits you make may be overwritten! | |||
*/ | |||
#pragma once | |||
namespace BinaryData | |||
{ | |||
extern const char* _5_Limit_scl; | |||
const int _5_Limit_sclSize = 87; | |||
extern const char* _7_Limit_scl; | |||
const int _7_Limit_sclSize = 85; | |||
extern const char* Pythagorean_scl; | |||
const int Pythagorean_sclSize = 108; | |||
extern const char* default_vitalskin; | |||
const int default_vitalskinSize = 10485; | |||
extern const char* DroidSansMono_ttf; | |||
const int DroidSansMono_ttfSize = 119380; | |||
extern const char* LatoLight_ttf; | |||
const int LatoLight_ttfSize = 77192; | |||
extern const char* LatoRegular_ttf; | |||
const int LatoRegular_ttfSize = 75136; | |||
extern const char* MontserratLight_otf; | |||
const int MontserratLight_otfSize = 228068; | |||
extern const char* MontserratRegular_ttf; | |||
const int MontserratRegular_ttfSize = 245708; | |||
extern const char* chorus_svg; | |||
const int chorus_svgSize = 12842; | |||
extern const char* cog_svg; | |||
const int cog_svgSize = 472; | |||
extern const char* compressor_svg; | |||
const int compressor_svgSize = 3175; | |||
extern const char* delay_svg; | |||
const int delay_svgSize = 3334; | |||
extern const char* distortion_svg; | |||
const int distortion_svgSize = 2015; | |||
extern const char* drag_drop_arrows_svg; | |||
const int drag_drop_arrows_svgSize = 3810; | |||
extern const char* effects_filter_svg; | |||
const int effects_filter_svgSize = 1495; | |||
extern const char* equalizer_svg; | |||
const int equalizer_svgSize = 2816; | |||
extern const char* flanger_svg; | |||
const int flanger_svgSize = 3334; | |||
extern const char* folder_svg; | |||
const int folder_svgSize = 154; | |||
extern const char* link_svg; | |||
const int link_svgSize = 317; | |||
extern const char* phaser_svg; | |||
const int phaser_svgSize = 1588; | |||
extern const char* reverb_svg; | |||
const int reverb_svgSize = 14394; | |||
extern const char* shuffle_svg; | |||
const int shuffle_svgSize = 382; | |||
extern const char* vital_ring_svg; | |||
const int vital_ring_svgSize = 2658; | |||
extern const char* vital_v_svg; | |||
const int vital_v_svgSize = 2658; | |||
extern const char* vital_word_svg; | |||
const int vital_word_svgSize = 3979; | |||
extern const char* vital_word_ring_svg; | |||
const int vital_word_ring_svgSize = 1888; | |||
// Number of elements in the namedResourceList and originalFileNames arrays. | |||
const int namedResourceListSize = 27; | |||
// Points to the start of a list of resource names. | |||
extern const char* namedResourceList[]; | |||
// Points to the start of a list of resource filenames. | |||
extern const char* originalFilenames[]; | |||
// If you provide the name of one of the binary resource variables above, this function will | |||
// return the corresponding data and its size (or a null pointer if the name isn't found). | |||
const char* getNamedResource (const char* resourceNameUTF8, int& dataSizeInBytes); | |||
// If you provide the name of one of the binary resource variables above, this function will | |||
// return the corresponding original, non-mangled filename (or a null pointer if the name isn't found). | |||
const char* getNamedResourceOriginalFilename (const char* resourceNameUTF8); | |||
} |
@@ -0,0 +1,19 @@ | |||
#ifndef __JUCE_HEADER_H__ | |||
#define __JUCE_HEADER_H__ | |||
#include "JucePluginMain.h" | |||
#include "BinaryData.h" | |||
using namespace juce; | |||
namespace ProjectInfo | |||
{ | |||
const char* const projectName = "Vitalium"; | |||
const char* const companyName = "DISTRHO"; | |||
const char* const versionString = "1.0.6"; | |||
const int versionNumber = 0x10006; | |||
} | |||
#endif |
@@ -0,0 +1,42 @@ | |||
#ifndef __JUCE_PLUGIN_CHARACTERISTICS_H__ | |||
#define __JUCE_PLUGIN_CHARACTERISTICS_H__ | |||
#define JucePlugin_Name "Vitalium" | |||
#define JucePlugin_Desc "Vitalium" | |||
#define JucePlugin_Manufacturer "DISTRHO" | |||
#define JucePlugin_ManufacturerCode 'DSTR' | |||
#define JucePlugin_ManufacturerEmail "info@kx.studio" | |||
#define JucePlugin_ManufacturerWebsite "vitalium.distrho.kx.studio" | |||
#define JucePlugin_PluginCode 'Vita' | |||
#define JucePlugin_VersionCode 0x10006 | |||
#define JucePlugin_VersionString "1.0.6" | |||
#define JucePlugin_MaxNumInputChannels 0 | |||
#define JucePlugin_MaxNumOutputChannels 2 | |||
#define JucePlugin_PreferredChannelConfigurations { 0, 2 } | |||
#define JucePlugin_IsSynth 1 | |||
#define JucePlugin_WantsMidiInput 1 | |||
#define JucePlugin_ProducesMidiOutput 0 | |||
#define JucePlugin_SilenceInProducesSilenceOut 0 | |||
#define JucePlugin_EditorRequiresKeyboardFocus 1 // pluginEditorRequiresKeys | |||
#define JucePlugin_IsMidiEffect 0 | |||
// aaxIdentifier="audio.vial.synth" | |||
// pluginAUExportPrefix="vial" | |||
// pluginRTASCategory="" | |||
// pluginAAXCategory="2048" | |||
#define JucePlugin_VSTUniqueID JucePlugin_PluginCode | |||
#define JucePlugin_VSTCategory kPlugCategSynth | |||
#define JucePlugin_LV2URI "urn:distrho:vitalium" | |||
#define JucePlugin_LV2Category "InstrumentPlugin" | |||
#define JucePlugin_WantsLV2Latency 0 | |||
#define JucePlugin_WantsLV2State 1 | |||
#define JucePlugin_WantsLV2TimePos 1 | |||
#define JucePlugin_WantsLV2FixedBlockSize 1 | |||
#define JucePlugin_WantsLV2Presets 0 | |||
#endif |
@@ -0,0 +1,27 @@ | |||
#!/bin/bash | |||
set -e | |||
cd $(dirname ${0}) | |||
# fetch git repo if not done yet | |||
if [ ! -d vital-git ]; then | |||
git clone --depth=1 --recursive git@github.com:mtytel/vital.git vital-git | |||
fi | |||
# clean and update git repo | |||
# pushd vital-git | |||
# git checkout . | |||
# git submodule update | |||
# git pull | |||
# git submodule update | |||
# popd | |||
# TODO generate binarydata ourselves | |||
cp vital-git/plugin/JuceLibraryCode/BinaryData.{cpp,h} . | |||
rm -rf source third_party | |||
mkdir source | |||
mkdir third_party | |||
cp -r vital-git/src/{common,interface,plugin,synthesis,unity_build} source/ | |||
cp -r vital-git/third_party/{concurrentqueue,json} third_party/ |
@@ -0,0 +1,53 @@ | |||
############################################################################### | |||
plugin_extra_build_flags = [ | |||
'-Wno-char-subscripts', | |||
'-Wno-deprecated-declarations', | |||
'-Wno-sign-compare', | |||
'-DNO_AUTH=1', | |||
] | |||
# -Ofast -flto | |||
# -ftree-vectorize -ftree-slp-vectorize -funroll-loops | |||
# -DREQUIRE_AUTH | |||
# "-DJUCE_DSP_USE_SHARED_FFTW=1" | |||
plugin_extra_include_dirs = include_directories([ | |||
'.', | |||
'source/common', | |||
'source/common/wavetable', | |||
'source/interface/editor_components', | |||
'source/interface/editor_sections', | |||
'source/interface/look_and_feel', | |||
'source/interface/wavetable', | |||
'source/interface/wavetable/editors', | |||
'source/interface/wavetable/overlays', | |||
'source/plugin', | |||
'source/synthesis/synth_engine', | |||
'source/synthesis/effects', | |||
'source/synthesis/filters', | |||
'source/synthesis/framework', | |||
'source/synthesis/lookups', | |||
'source/synthesis/modulators', | |||
'source/synthesis/modules', | |||
'source/synthesis/producers', | |||
'source/synthesis/utilities', | |||
'third_party', | |||
]) | |||
plugin_srcs = files([ | |||
'BinaryData.cpp', | |||
'source/unity_build/common.cpp', | |||
'source/unity_build/interface_editor_components.cpp', | |||
'source/unity_build/interface_editor_sections.cpp', | |||
'source/unity_build/interface_editor_sections2.cpp', | |||
'source/unity_build/interface_look_and_feel.cpp', | |||
'source/unity_build/interface_wavetable.cpp', | |||
'source/unity_build/plugin.cpp', | |||
'source/unity_build/synthesis.cpp', | |||
]) | |||
plugin_name = 'vitalium' | |||
plugin_uses_opengl = true | |||
############################################################################### |
@@ -0,0 +1,103 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#if NDEBUG && !NO_AUTH | |||
#include "JuceHeader.h" | |||
#include "load_save.h" | |||
#if defined(__APPLE__) | |||
#include <firebase/app.h> | |||
#include <firebase/auth.h> | |||
#else | |||
#include "firebase/app.h" | |||
#include "firebase/auth.h" | |||
#endif | |||
class Authentication { | |||
public: | |||
static void onTokenRefreshResult(const firebase::Future<std::string>& completed_future, void* ref_data) { | |||
const MessageManagerLock lock(Thread::getCurrentThread()); | |||
if (!lock.lockWasGained()) | |||
return; | |||
if (completed_future.status() != firebase::kFutureStatusComplete) { | |||
LoadSave::writeErrorLog("Firebase getting token error: not complete"); | |||
return; | |||
} | |||
if (completed_future.error()) { | |||
std::string error = "Firebase getting token error: error code "; | |||
LoadSave::writeErrorLog(error + std::to_string(completed_future.error())); | |||
return; | |||
} | |||
Authentication* reference = (Authentication*)ref_data; | |||
reference->setToken(*completed_future.result()); | |||
} | |||
static void create() { | |||
if (firebase::App::GetInstance() != nullptr) | |||
return; | |||
firebase::AppOptions auth_app_options = firebase::AppOptions(); | |||
auth_app_options.set_app_id(""); | |||
auth_app_options.set_api_key(""); | |||
auth_app_options.set_project_id(""); | |||
firebase::App::Create(auth_app_options); | |||
} | |||
Authentication() : auth_(nullptr) { } | |||
void init() { | |||
if (auth_ == nullptr) | |||
auth_ = firebase::auth::Auth::GetAuth(firebase::App::GetInstance()); | |||
} | |||
bool hasAuth() const { return auth_ != nullptr; } | |||
firebase::auth::Auth* auth() const { return auth_; } | |||
void setToken(const std::string& token) { token_ = token; } | |||
std::string token() const { return token_; } | |||
bool loggedIn() { return auth_ && auth_->current_user() != nullptr; } | |||
void refreshToken() { | |||
if (auth_ == nullptr || auth_->current_user() == nullptr) | |||
return; | |||
firebase::Future<std::string> future = auth_->current_user()->GetToken(false); | |||
future.OnCompletion(onTokenRefreshResult, this); | |||
} | |||
private: | |||
firebase::auth::Auth* auth_; | |||
std::string token_; | |||
}; | |||
#else | |||
class Authentication { | |||
public: | |||
static void create() { } | |||
std::string token() { return ""; } | |||
bool loggedIn() { return false; } | |||
void refreshToken() { } | |||
}; | |||
#endif |
@@ -0,0 +1,66 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "border_bounds_constrainer.h" | |||
#include "full_interface.h" | |||
#include "load_save.h" | |||
#include "synth_gui_interface.h" | |||
void BorderBoundsConstrainer::checkBounds(Rectangle<int>& bounds, const Rectangle<int>& previous, | |||
const Rectangle<int>& limits, | |||
bool stretching_top, bool stretching_left, | |||
bool stretching_bottom, bool stretching_right) { | |||
border_.subtractFrom(bounds); | |||
double aspect_ratio = getFixedAspectRatio(); | |||
ComponentBoundsConstrainer::checkBounds(bounds, previous, limits, | |||
stretching_top, stretching_left, | |||
stretching_bottom, stretching_right); | |||
Rectangle<int> display_area = Desktop::getInstance().getDisplays().getTotalBounds(true); | |||
if (gui_) { | |||
ComponentPeer* peer = gui_->getPeer(); | |||
if (peer) | |||
peer->getFrameSize().subtractFrom(display_area); | |||
} | |||
if (display_area.getWidth() < bounds.getWidth()) { | |||
int new_width = display_area.getWidth(); | |||
int new_height = std::round(new_width / aspect_ratio); | |||
bounds.setWidth(new_width); | |||
bounds.setHeight(new_height); | |||
} | |||
if (display_area.getHeight() < bounds.getHeight()) { | |||
int new_height = display_area.getHeight(); | |||
int new_width = std::round(new_height * aspect_ratio); | |||
bounds.setWidth(new_width); | |||
bounds.setHeight(new_height); | |||
} | |||
border_.addTo(bounds); | |||
} | |||
void BorderBoundsConstrainer::resizeStart() { | |||
if (gui_) | |||
gui_->enableRedoBackground(false); | |||
} | |||
void BorderBoundsConstrainer::resizeEnd() { | |||
if (gui_) { | |||
LoadSave::saveWindowSize(gui_->getWidth() / (1.0f * vital::kDefaultWindowWidth)); | |||
gui_->enableRedoBackground(true); | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
class FullInterface; | |||
class BorderBoundsConstrainer : public ComponentBoundsConstrainer { | |||
public: | |||
BorderBoundsConstrainer() : ComponentBoundsConstrainer(), gui_(nullptr) { } | |||
virtual void checkBounds(Rectangle<int>& bounds, const Rectangle<int>& previous, | |||
const Rectangle<int>& limits, | |||
bool stretching_top, bool stretching_left, | |||
bool stretching_bottom, bool stretching_right) override; | |||
virtual void resizeStart() override; | |||
virtual void resizeEnd() override; | |||
void setBorder(const BorderSize<int>& border) { border_ = border; } | |||
void setGui(FullInterface* gui) { gui_ = gui; } | |||
protected: | |||
FullInterface* gui_; | |||
BorderSize<int> border_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BorderBoundsConstrainer) | |||
}; | |||
@@ -0,0 +1,187 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
namespace vital { | |||
#if INTEL_IPP | |||
#include "ipps.h" | |||
class FourierTransform { | |||
public: | |||
FourierTransform(int bits) : size_(1 << bits) { | |||
int spec_size = 0; | |||
int spec_buffer_size = 0; | |||
int buffer_size = 0; | |||
ippsFFTGetSize_R_32f(bits, IPP_FFT_DIV_INV_BY_N, ippAlgHintNone, &spec_size, &spec_buffer_size, &buffer_size); | |||
spec_ = std::make_unique<Ipp8u[]>(spec_size); | |||
spec_buffer_ = std::make_unique<Ipp8u[]>(spec_buffer_size); | |||
buffer_ = std::make_unique<Ipp8u[]>(buffer_size); | |||
ippsFFTInit_R_32f(&ipp_specs_, bits, IPP_FFT_DIV_INV_BY_N, ippAlgHintNone, spec_.get(), spec_buffer_.get()); | |||
} | |||
void transformRealForward(float* data) { | |||
data[size_] = 0.0f; | |||
ippsFFTFwd_RToPerm_32f_I((Ipp32f*)data, ipp_specs_, buffer_.get()); | |||
data[size_] = data[1]; | |||
data[size_ + 1] = 0.0f; | |||
data[1] = 0.0f; | |||
} | |||
void transformRealInverse(float* data) { | |||
data[1] = data[size_]; | |||
ippsFFTInv_PermToR_32f_I((Ipp32f*)data, ipp_specs_, buffer_.get()); | |||
memset(data + size_, 0, size_ * sizeof(float)); | |||
} | |||
private: | |||
int size_; | |||
IppsFFTSpec_R_32f *ipp_specs_; | |||
std::unique_ptr<Ipp8u[]> spec_; | |||
std::unique_ptr<Ipp8u[]> spec_buffer_; | |||
std::unique_ptr<Ipp8u[]> buffer_; | |||
JUCE_LEAK_DETECTOR(FourierTransform) | |||
}; | |||
#elif JUCE_MODULE_AVAILABLE_juce_dsp | |||
class FourierTransform { | |||
public: | |||
FourierTransform(int bits) : fft_(bits) { } | |||
void transformRealForward(float* data) { fft_.performRealOnlyForwardTransform(data, true); } | |||
void transformRealInverse(float* data) { fft_.performRealOnlyInverseTransform(data); } | |||
private: | |||
dsp::FFT fft_; | |||
JUCE_LEAK_DETECTOR(FourierTransform) | |||
}; | |||
#elif __APPLE__ | |||
#define VIMAGE_H | |||
#include <Accelerate/Accelerate.h> | |||
class FourierTransform { | |||
public: | |||
FourierTransform(vDSP_Length bits) : setup_(vDSP_create_fftsetup(bits, 2)), bits_(bits), size_(1 << bits) { } | |||
~FourierTransform() { | |||
vDSP_destroy_fftsetup(setup_); | |||
} | |||
void transformRealForward(float* data) { | |||
static const float kMult = 0.5f; | |||
data[size_] = 0.0f; | |||
DSPSplitComplex split = { data, data + 1 }; | |||
vDSP_fft_zrip(setup_, &split, 2, bits_, kFFTDirection_Forward); | |||
vDSP_vsmul(data, 1, &kMult, data, 1, size_); | |||
data[size_] = data[1]; | |||
data[size_ + 1] = 0.0f; | |||
data[1] = 0.0f; | |||
} | |||
void transformRealInverse(float* data) { | |||
float multiplier = 1.0f / size_; | |||
DSPSplitComplex split = { data, data + 1 }; | |||
data[1] = data[size_]; | |||
vDSP_fft_zrip(setup_, &split, 2, bits_, kFFTDirection_Inverse); | |||
vDSP_vsmul(data, 1, &multiplier, data, 1, size_ * 2); | |||
memset(data + size_, 0, size_ * sizeof(float)); | |||
} | |||
private: | |||
FFTSetup setup_; | |||
vDSP_Length bits_; | |||
vDSP_Length size_; | |||
JUCE_LEAK_DETECTOR(FourierTransform) | |||
}; | |||
#else | |||
#include "kissfft/kissfft.h" | |||
class FourierTransform { | |||
public: | |||
FourierTransform(size_t bits) : bits_(bits), size_(1 << bits), forward_(size_, false), inverse_(size_, true) { | |||
buffer_ = std::make_unique<std::complex<float>[]>(size_); | |||
} | |||
~FourierTransform() { } | |||
void transformRealForward(float* data) { | |||
for (int i = size_ - 1; i >= 0; --i) { | |||
data[2 * i] = data[i]; | |||
data[2 * i + 1] = 0.0f; | |||
} | |||
forward_.transform((std::complex<float>*)data, buffer_.get()); | |||
int num_floats = size_ * 2; | |||
memcpy(data, buffer_.get(), num_floats * sizeof(float)); | |||
data[size_] = data[1]; | |||
data[size_ + 1] = 0.0f; | |||
data[1] = 0.0f; | |||
} | |||
void transformRealInverse(float* data) { | |||
data[0] *= 0.5f; | |||
data[1] = data[size_]; | |||
inverse_.transform((std::complex<float>*)data, buffer_.get()); | |||
int num_floats = size_ * 2; | |||
float multiplier = 2.0f / size_; | |||
for (int i = 0; i < size_; ++i) | |||
data[i] = buffer_[i].real() * multiplier; | |||
memset(data + size_, 0, size_ * sizeof(float)); | |||
} | |||
private: | |||
size_t bits_; | |||
size_t size_; | |||
std::unique_ptr<std::complex<float>[]> buffer_; | |||
kissfft<float> forward_; | |||
kissfft<float> inverse_; | |||
JUCE_LEAK_DETECTOR(FourierTransform) | |||
}; | |||
#endif | |||
template <size_t bits> | |||
class FFT { | |||
public: | |||
static FourierTransform* transform() { | |||
static FFT<bits> instance; | |||
return &instance.fourier_transform_; | |||
} | |||
private: | |||
FFT() : fourier_transform_(bits) { } | |||
FourierTransform fourier_transform_; | |||
}; | |||
} // namespace vital |
@@ -0,0 +1,308 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "line_generator.h" | |||
#include "poly_utils.h" | |||
#include "futils.h" | |||
LineGenerator::LineGenerator(int resolution) : points_(), powers_(), num_points_(2), resolution_(resolution), | |||
loop_(false), smooth_(false), linear_(true), render_count_(0) { | |||
buffer_ = std::make_unique<vital::mono_float[]>(resolution + kExtraValues); | |||
initLinear(); | |||
} | |||
void LineGenerator::initLinear() { | |||
points_[0] = { 0.0f, 1.0f }; | |||
points_[1] = { 1.0f, 0.0f }; | |||
powers_[0] = 0.0f; | |||
powers_[1] = 0.0f; | |||
num_points_ = 2; | |||
linear_ = true; | |||
name_ = "Linear"; | |||
smooth_ = false; | |||
render(); | |||
} | |||
void LineGenerator::initTriangle() { | |||
points_[0] = { 0.0f, 1.0f }; | |||
points_[1] = { 0.5f, 0.0f }; | |||
points_[2] = { 1.0f, 1.0f }; | |||
powers_[0] = 0.0f; | |||
powers_[1] = 0.0f; | |||
powers_[2] = 0.0f; | |||
num_points_ = 3; | |||
linear_ = false; | |||
name_ = "Triangle"; | |||
smooth_ = false; | |||
render(); | |||
} | |||
void LineGenerator::initSquare() { | |||
num_points_ = 5; | |||
points_[0] = { 0.0f, 1.0f }; | |||
points_[1] = { 0.0f, 0.0f }; | |||
points_[2] = { 0.5f, 0.0f }; | |||
points_[3] = { 0.5f, 1.0f }; | |||
points_[4] = { 1.0f, 1.0f }; | |||
for (int i = 0; i < num_points_; ++i) | |||
powers_[i] = 0.0f; | |||
linear_ = false; | |||
name_ = "Square"; | |||
smooth_ = false; | |||
render(); | |||
} | |||
void LineGenerator::initSin() { | |||
points_[0] = { 0.0f, 1.0f }; | |||
points_[1] = { 0.5f, 0.0f }; | |||
points_[2] = { 1.0f, 1.0f }; | |||
powers_[0] = 0.0f; | |||
powers_[1] = 0.0f; | |||
powers_[2] = 0.0f; | |||
num_points_ = 3; | |||
linear_ = false; | |||
name_ = "Sin"; | |||
smooth_ = true; | |||
render(); | |||
} | |||
void LineGenerator::initSawUp() { | |||
num_points_ = 3; | |||
points_[0] = { 0.0f, 1.0f }; | |||
points_[1] = { 1.0f, 0.0f }; | |||
points_[2] = { 1.0f, 1.0f }; | |||
powers_[0] = 0.0f; | |||
powers_[1] = 0.0f; | |||
powers_[2] = 0.0f; | |||
linear_ = false; | |||
name_ = "Saw Up"; | |||
smooth_ = false; | |||
render(); | |||
} | |||
void LineGenerator::initSawDown() { | |||
num_points_ = 3; | |||
points_[0] = { 0.0f, 0.0f }; | |||
points_[1] = { 1.0f, 1.0f }; | |||
points_[2] = { 1.0f, 0.0f }; | |||
powers_[0] = 0.0f; | |||
powers_[1] = 0.0f; | |||
powers_[2] = 0.0f; | |||
linear_ = false; | |||
name_ = "Saw Down"; | |||
smooth_ = false; | |||
render(); | |||
} | |||
json LineGenerator::stateToJson() { | |||
json point_data; | |||
json power_data; | |||
for (int i = 0; i < num_points_; ++i) { | |||
std::pair<float, float> point = points_[i]; | |||
point_data.push_back(point.first); | |||
point_data.push_back(point.second); | |||
power_data.push_back(powers_[i]); | |||
} | |||
json data; | |||
data["num_points"] = num_points_; | |||
data["points"] = point_data; | |||
data["powers"] = power_data; | |||
data["name"] = name_; | |||
data["smooth"] = smooth_; | |||
return data; | |||
} | |||
bool LineGenerator::isValidJson(json data) { | |||
if (!data.count("num_points") || !data.count("points") || !data.count("powers")) | |||
return false; | |||
json point_data = data["points"]; | |||
json power_data = data["powers"]; | |||
return point_data.is_array() && power_data.is_array(); | |||
} | |||
void LineGenerator::jsonToState(json data) { | |||
num_points_ = data["num_points"]; | |||
json point_data = data["points"]; | |||
json power_data = data["powers"]; | |||
name_ = ""; | |||
if (data.count("name")) | |||
name_ = data["name"].get<std::string>(); | |||
smooth_ = false; | |||
if (data.count("smooth")) | |||
smooth_ = data["smooth"]; | |||
int num_read = kMaxPoints; | |||
num_read = std::min(num_read, static_cast<int>(power_data.size())); | |||
for (int i = 0; i < num_read; ++i) { | |||
points_[i] = std::pair<float, float>(point_data[2 * i], point_data[2 * i + 1]); | |||
powers_[i] = power_data[i]; | |||
} | |||
checkLineIsLinear(); | |||
render(); | |||
} | |||
void LineGenerator::render() { | |||
render_count_++; | |||
int point_index = 0; | |||
std::pair<float, float> last_point = points_[point_index]; | |||
float current_power = 0.0f; | |||
std::pair<float, float> current_point = points_[point_index]; | |||
if (loop_) { | |||
last_point = points_[num_points_ - 1]; | |||
last_point.first -= 1.0f; | |||
current_power = powers_[num_points_ - 1]; | |||
} | |||
for (int i = 0; i < resolution_; ++i) { | |||
float x = i / (resolution_ - 1.0f); | |||
float t = 1.0f; | |||
if (current_point.first > last_point.first) | |||
t = (x - last_point.first) / (current_point.first - last_point.first); | |||
if (smooth_) | |||
t = smoothTransition(t); | |||
t = vital::utils::clamp(vital::futils::powerScale(t, current_power), 0.0f, 1.0f); | |||
float y = last_point.second + t * (current_point.second - last_point.second); | |||
buffer_[i + 1] = 1.0f - y; | |||
while (x > current_point.first && point_index < num_points_) { | |||
current_power = powers_[point_index % num_points_]; | |||
point_index++; | |||
last_point = current_point; | |||
current_point = points_[point_index % num_points_]; | |||
if (point_index >= num_points_) { | |||
current_point.first += 1.0f; | |||
break; | |||
} | |||
} | |||
} | |||
if (loop_) { | |||
buffer_[0] = buffer_[resolution_]; | |||
buffer_[resolution_ + 1] = buffer_[1]; | |||
buffer_[resolution_ + 2] = buffer_[2]; | |||
} | |||
else { | |||
buffer_[0] = buffer_[1]; | |||
buffer_[resolution_ + 1] = buffer_[resolution_]; | |||
buffer_[resolution_ + 2] = buffer_[resolution_]; | |||
} | |||
} | |||
float LineGenerator::valueAtPhase(float phase) { | |||
float scaled_phase = vital::utils::clamp(phase, 0.0f, 1.0f) * resolution_; | |||
int index = scaled_phase; | |||
return vital::utils::interpolate(buffer_[index + 1], buffer_[index + 2], scaled_phase - index); | |||
} | |||
void LineGenerator::checkLineIsLinear() { | |||
linear_ = !smooth_ && num_points_ == 2 && powers_[0] == 0.0f && | |||
points_[0] == std::pair<float, float>(0.0f, 1.0f) && | |||
points_[1] == std::pair<float, float>(1.0f, 0.0f); | |||
} | |||
float LineGenerator::getValueBetweenPoints(float x, int index_from, int index_to) { | |||
VITAL_ASSERT(index_from >= 0 && index_to < num_points_); | |||
std::pair<float, float> first = points_[index_from]; | |||
std::pair<float, float> second = points_[index_to]; | |||
float power = powers_[index_from]; | |||
float width = second.first - first.first; | |||
if (width <= 0.0f) | |||
return second.second; | |||
float t = (x - first.first) / width; | |||
if (smooth_) | |||
t = smoothTransition(t); | |||
t = vital::utils::clamp(vital::futils::powerScale(t, power), 0.0f, 1.0f); | |||
return t * (second.second - first.second) + first.second; | |||
} | |||
float LineGenerator::getValueAtPhase(float phase) { | |||
for (int i = 0; i < num_points_ - 1; ++i) { | |||
if (points_[i].first <= phase && points_[i + 1].first >= phase) | |||
return getValueBetweenPoints(phase, i, i + 1); | |||
} | |||
return lastPoint().second; | |||
} | |||
void LineGenerator::addPoint(int index, std::pair<float, float> position) { | |||
for (int i = num_points_; i > index; --i) { | |||
points_[i] = points_[i - 1]; | |||
powers_[i] = powers_[i - 1]; | |||
} | |||
num_points_++; | |||
points_[index] = position; | |||
powers_[index] = 0.0f; | |||
checkLineIsLinear(); | |||
} | |||
void LineGenerator::addMiddlePoint(int index) { | |||
VITAL_ASSERT(index > 0); | |||
float x = (points_[index - 1].first + points_[index].first) * 0.5f; | |||
float y = getValueBetweenPoints(x, index - 1, index); | |||
addPoint(index, { x, y }); | |||
} | |||
void LineGenerator::removePoint(int index) { | |||
num_points_--; | |||
for (int i = index; i < num_points_; ++i) { | |||
points_[i] = points_[i + 1]; | |||
powers_[i] = powers_[i + 1]; | |||
} | |||
checkLineIsLinear(); | |||
} | |||
void LineGenerator::flipHorizontal() { | |||
for (int i = 0; i < (num_points_ + 1) / 2; ++i) { | |||
float tmp_x = 1.0f - points_[i].first; | |||
float tmp_y = points_[i].second; | |||
points_[i].first = 1.0f - points_[num_points_ - i - 1].first; | |||
points_[i].second = points_[num_points_ - i - 1].second; | |||
points_[num_points_ - i - 1].first = tmp_x; | |||
points_[num_points_ - i - 1].second = tmp_y; | |||
} | |||
for (int i = 0; i < num_points_ / 2; ++i) { | |||
float tmp_power = powers_[i]; | |||
powers_[i] = -powers_[num_points_ - i - 2]; | |||
powers_[num_points_ - i - 2] = -tmp_power; | |||
} | |||
render(); | |||
checkLineIsLinear(); | |||
} | |||
void LineGenerator::flipVertical() { | |||
for (int i = 0; i < num_points_; ++i) | |||
points_[i].second = 1.0f - points_[i].second; | |||
render(); | |||
checkLineIsLinear(); | |||
} |
@@ -0,0 +1,126 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "common.h" | |||
#include "json/json.h" | |||
using json = nlohmann::json; | |||
class LineGenerator { | |||
public: | |||
static constexpr int kMaxPoints = 100; | |||
static constexpr int kDefaultResolution = 2048; | |||
static constexpr int kExtraValues = 3; | |||
static force_inline float smoothTransition(float t) { | |||
return 0.5f * sinf((t - 0.5f) * vital::kPi) + 0.5f; | |||
} | |||
LineGenerator(int resolution = kDefaultResolution); | |||
virtual ~LineGenerator() { } | |||
void setLoop(bool loop) { loop_ = loop; render(); } | |||
void setName(const std::string& name) { name_ = name; } | |||
void setLastBrowsedFile(const std::string& path) { last_browsed_file_ = path; } | |||
void setSmooth(bool smooth) { smooth_ = smooth; checkLineIsLinear(); render(); } | |||
void initLinear(); | |||
void initTriangle(); | |||
void initSquare(); | |||
void initSin(); | |||
void initSawUp(); | |||
void initSawDown(); | |||
void render(); | |||
json stateToJson(); | |||
static bool isValidJson(json data); | |||
void jsonToState(json data); | |||
float valueAtPhase(float phase); | |||
void checkLineIsLinear(); | |||
float getValueBetweenPoints(float x, int index_from, int index_to); | |||
float getValueAtPhase(float phase); | |||
std::string getName() const { return name_; } | |||
std::string getLastBrowsedFile() const { return last_browsed_file_; } | |||
void addPoint(int index, std::pair<float, float> position); | |||
void addMiddlePoint(int index); | |||
void removePoint(int index); | |||
void flipHorizontal(); | |||
void flipVertical(); | |||
std::pair<float, float> lastPoint() const { return points_[num_points_ - 1]; } | |||
float lastPower() const { return powers_[num_points_ - 1]; } | |||
force_inline int resolution() const { return resolution_; } | |||
force_inline bool linear() const { return linear_; } | |||
force_inline bool smooth() const { return smooth_; } | |||
force_inline vital::mono_float* getBuffer() const { return buffer_.get() + 1; } | |||
force_inline vital::mono_float* getCubicInterpolationBuffer() const { return buffer_.get(); } | |||
force_inline std::pair<float, float> getPoint(int index) const { | |||
VITAL_ASSERT(index < kMaxPoints && index >= 0); | |||
return points_[index]; | |||
} | |||
force_inline float getPower(int index) const { | |||
VITAL_ASSERT(index < kMaxPoints && index >= 0); | |||
return powers_[index]; | |||
} | |||
force_inline int getNumPoints() const { | |||
return num_points_; | |||
} | |||
force_inline void setPoint(int index, std::pair<float, float> point) { | |||
VITAL_ASSERT(index < kMaxPoints && index >= 0); | |||
points_[index] = point; | |||
checkLineIsLinear(); | |||
} | |||
force_inline void setPower(int index, float power) { | |||
VITAL_ASSERT(index < kMaxPoints && index >= 0); | |||
powers_[index] = power; | |||
checkLineIsLinear(); | |||
} | |||
force_inline void setNumPoints(int num_points) { | |||
VITAL_ASSERT(num_points <= kMaxPoints && num_points >= 0); | |||
num_points_ = num_points; | |||
checkLineIsLinear(); | |||
} | |||
int getRenderCount() const { return render_count_; } | |||
protected: | |||
std::string name_; | |||
std::string last_browsed_file_; | |||
std::pair<float, float> points_[kMaxPoints]; | |||
float powers_[kMaxPoints]; | |||
int num_points_; | |||
int resolution_; | |||
std::unique_ptr<vital::mono_float[]> buffer_; | |||
bool loop_; | |||
bool smooth_; | |||
bool linear_; | |||
int render_count_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LineGenerator) | |||
}; | |||
@@ -0,0 +1,183 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "json/json.h" | |||
#include <map> | |||
#include <set> | |||
#include <string> | |||
using json = nlohmann::json; | |||
namespace vital { | |||
class StringLayout; | |||
} | |||
class MidiManager; | |||
class SynthBase; | |||
class LoadSave { | |||
public: | |||
class FileSorterAscending { | |||
public: | |||
FileSorterAscending() { } | |||
static int compareElements(File a, File b) { | |||
return a.getFullPathName().toLowerCase().compareNatural(b.getFullPathName().toLowerCase()); | |||
} | |||
private: | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileSorterAscending) | |||
}; | |||
enum PresetStyle { | |||
kBass, | |||
kLead, | |||
kKeys, | |||
kPad, | |||
kPercussion, | |||
kSequence, | |||
kExperimental, | |||
kSfx, | |||
kTemplate, | |||
kNumPresetStyles | |||
}; | |||
static const int kMaxCommentLength = 500; | |||
static const std::string kUserDirectoryName; | |||
static const std::string kPresetFolderName; | |||
static const std::string kWavetableFolderName; | |||
static const std::string kSkinFolderName; | |||
static const std::string kSampleFolderName; | |||
static const std::string kLfoFolderName; | |||
static const std::string kAdditionalWavetableFoldersName; | |||
static const std::string kAdditionalSampleFoldersName; | |||
static void convertBufferToPcm(json& data, const std::string& field); | |||
static void convertPcmToFloatBuffer(json& data, const std::string& field); | |||
static json stateToJson(SynthBase* synth, const CriticalSection& critical_section); | |||
static void loadControls(SynthBase* synth, const json& data); | |||
static void loadModulations(SynthBase* synth, const json& modulations); | |||
static void loadSample(SynthBase* synth, const json& sample); | |||
static void loadWavetables(SynthBase* synth, const json& wavetables); | |||
static void loadLfos(SynthBase* synth, const json& lfos); | |||
static void loadSaveState(std::map<std::string, String>& save_info, json data); | |||
static void initSaveInfo(std::map<std::string, String>& save_info); | |||
static json updateFromOldVersion(json state); | |||
static bool jsonToState(SynthBase* synth, std::map<std::string, String>& save_info, json state); | |||
static String getAuthorFromFile(const File& file); | |||
static String getStyleFromFile(const File& file); | |||
static std::string getAuthor(json file); | |||
static std::string getLicense(json state); | |||
static File getConfigFile(); | |||
static void writeCrashLog(String crash_log); | |||
static void writeErrorLog(String error_log); | |||
static json getConfigJson(); | |||
static File getFavoritesFile(); | |||
static File getDefaultSkin(); | |||
static json getFavoritesJson(); | |||
static void addFavorite(const File& new_favorite); | |||
static void removeFavorite(const File& old_favorite); | |||
static std::set<std::string> getFavorites(); | |||
static bool hasDataDirectory(); | |||
static File getAvailablePacksFile(); | |||
static json getAvailablePacks(); | |||
static File getInstalledPacksFile(); | |||
static json getInstalledPacks(); | |||
static void saveInstalledPacks(const json& packs); | |||
static void markPackInstalled(int id); | |||
static void markPackInstalled(const std::string& name); | |||
static void saveDataDirectory(const File& data_directory); | |||
static bool isInstalled(); | |||
static bool wasUpgraded(); | |||
static bool isExpired(); | |||
static bool doesExpire(); | |||
static int getDaysToExpire(); | |||
static bool shouldCheckForUpdates(); | |||
static bool shouldWorkOffline(); | |||
static std::string getLoadedSkin(); | |||
static bool shouldAnimateWidgets(); | |||
static bool displayHzFrequency(); | |||
static bool authenticated(); | |||
static int getOversamplingAmount(); | |||
static float loadWindowSize(); | |||
static String loadVersion(); | |||
static String loadContentVersion(); | |||
static void saveJsonToConfig(json config_state); | |||
static void saveJsonToFavorites(json favorites_json); | |||
static void saveAuthor(std::string author); | |||
static void savePreferredTTWTLanguage(std::string language); | |||
static void saveLayoutConfig(vital::StringLayout* layout); | |||
static void saveVersionConfig(); | |||
static void saveContentVersion(std::string version); | |||
static void saveUpdateCheckConfig(bool check_for_updates); | |||
static void saveWorkOffline(bool work_offline); | |||
static void saveLoadedSkin(const std::string& name); | |||
static void saveAnimateWidgets(bool animate_widgets); | |||
static void saveDisplayHzFrequency(bool display_hz); | |||
static void saveAuthenticated(bool authenticated); | |||
static void saveWindowSize(float window_size); | |||
static void saveMidiMapConfig(MidiManager* midi_manager); | |||
static void loadConfig(MidiManager* midi_manager, vital::StringLayout* layout = nullptr); | |||
static std::wstring getComputerKeyboardLayout(); | |||
static std::string getPreferredTTWTLanguage(); | |||
static std::string getAuthor(); | |||
static std::pair<wchar_t, wchar_t> getComputerKeyboardOctaveControls(); | |||
static void saveAdditionalFolders(const std::string& name, std::vector<std::string> folders); | |||
static std::vector<std::string> getAdditionalFolders(const std::string& name); | |||
static File getDataDirectory(); | |||
static std::vector<File> getDirectories(const String& folder_name); | |||
static std::vector<File> getPresetDirectories(); | |||
static std::vector<File> getWavetableDirectories(); | |||
static std::vector<File> getSkinDirectories(); | |||
static std::vector<File> getSampleDirectories(); | |||
static std::vector<File> getLfoDirectories(); | |||
static File getUserDirectory(); | |||
static File getUserPresetDirectory(); | |||
static File getUserWavetableDirectory(); | |||
static File getUserSkinDirectory(); | |||
static File getUserSampleDirectory(); | |||
static File getUserLfoDirectory(); | |||
static void getAllFilesOfTypeInDirectories(Array<File>& files, const String& extensions, | |||
const std::vector<File>& directories); | |||
static void getAllPresets(Array<File>& presets); | |||
static void getAllWavetables(Array<File>& wavetables); | |||
static void getAllSkins(Array<File>& skins); | |||
static void getAllLfos(Array<File>& lfos); | |||
static void getAllSamples(Array<File>& samples); | |||
static void getAllUserPresets(Array<File>& presets); | |||
static void getAllUserWavetables(Array<File>& wavetables); | |||
static void getAllUserLfos(Array<File>& lfos); | |||
static void getAllUserSamples(Array<File>& samples); | |||
static int compareFeatureVersionStrings(String a, String b); | |||
static int compareVersionStrings(String a, String b); | |||
static File getShiftedFile(const String directory_name, const String& extensions, | |||
const std::string& additional_folders_name, const File& current_file, int shift); | |||
private: | |||
LoadSave() { } | |||
}; | |||
@@ -0,0 +1,324 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "midi_manager.h" | |||
#include "sound_engine.h" | |||
#include "synth_types.h" | |||
#include "load_save.h" | |||
#include "synth_base.h" | |||
namespace { | |||
constexpr int kMidiControlBits = 7; | |||
constexpr float kHighResolutionMax = (1 << (2 * kMidiControlBits)) - 1.0f; | |||
constexpr float kControlMax = (1 << kMidiControlBits) - 1.0f; | |||
force_inline vital::mono_float toHighResolutionValue(int msb, int lsb) { | |||
if (lsb < 0) | |||
return msb / kControlMax; | |||
return ((msb << kMidiControlBits) + lsb) / kHighResolutionMax; | |||
} | |||
} // namespace | |||
MidiManager::MidiManager(SynthBase* synth, MidiKeyboardState* keyboard_state, | |||
std::map<std::string, String>* gui_state, Listener* listener) : | |||
synth_(synth), keyboard_state_(keyboard_state), gui_state_(gui_state), | |||
listener_(listener), armed_value_(nullptr), | |||
msb_pressure_values_(), msb_slide_values_() { | |||
engine_ = synth_->getEngine(); | |||
current_bank_ = -1; | |||
current_folder_ = -1; | |||
current_preset_ = -1; | |||
for (int i = 0; i < vital::kNumMidiChannels; ++i) { | |||
lsb_slide_values_[i] = -1; | |||
lsb_pressure_values_[i] = -1; | |||
} | |||
mpe_enabled_ = false; | |||
mpe_zone_layout_.setLowerZone(vital::kNumMidiChannels - 1); | |||
} | |||
MidiManager::~MidiManager() { | |||
} | |||
void MidiManager::armMidiLearn(std::string name) { | |||
current_bank_ = -1; | |||
current_folder_ = -1; | |||
current_preset_ = -1; | |||
armed_value_ = &vital::Parameters::getDetails(name); | |||
} | |||
void MidiManager::cancelMidiLearn() { | |||
armed_value_ = nullptr; | |||
} | |||
void MidiManager::clearMidiLearn(const std::string& name) { | |||
for (auto& controls : midi_learn_map_) { | |||
if (controls.second.count(name)) { | |||
midi_learn_map_[controls.first].erase(name); | |||
LoadSave::saveMidiMapConfig(this); | |||
} | |||
} | |||
} | |||
void MidiManager::midiInput(int midi_id, vital::mono_float value) { | |||
if (armed_value_) { | |||
midi_learn_map_[midi_id][armed_value_->name] = armed_value_; | |||
armed_value_ = nullptr; | |||
// TODO: Probably shouldn't write this config on the audio thread. | |||
LoadSave::saveMidiMapConfig(this); | |||
} | |||
if (midi_learn_map_.count(midi_id)) { | |||
for (auto& control : midi_learn_map_[midi_id]) { | |||
const vital::ValueDetails* details = control.second; | |||
vital::mono_float percent = value / kControlMax; | |||
vital::mono_float range = details->max - details->min; | |||
vital::mono_float translated = percent * range + details->min; | |||
if (details->value_scale == vital::ValueDetails::kIndexed) | |||
translated = std::round(translated); | |||
listener_->valueChangedThroughMidi(control.first, translated); | |||
} | |||
} | |||
} | |||
bool MidiManager::isMidiMapped(const std::string& name) const { | |||
for (auto& controls : midi_learn_map_) { | |||
if (controls.second.count(name)) | |||
return true; | |||
} | |||
return false; | |||
} | |||
void MidiManager::setSampleRate(double sample_rate) { | |||
midi_collector_.reset(sample_rate); | |||
} | |||
void MidiManager::removeNextBlockOfMessages(MidiBuffer& buffer, int num_samples) { | |||
midi_collector_.removeNextBlockOfMessages(buffer, num_samples); | |||
} | |||
void MidiManager::readMpeMessage(const MidiMessage& message) { | |||
mpe_zone_layout_.processNextMidiEvent(message); | |||
} | |||
void MidiManager::processAllNotesOff(const MidiMessage& midi_message, int sample_position, int channel) { | |||
if (isMpeChannelMasterLowerZone(channel)) | |||
engine_->allNotesOffRange(sample_position, lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
else if (isMpeChannelMasterUpperZone(channel)) | |||
engine_->allNotesOffRange(sample_position, upperZoneStartChannel(), upperZoneEndChannel()); | |||
else | |||
engine_->allNotesOff(sample_position, channel); | |||
} | |||
void MidiManager::processAllSoundsOff() { | |||
engine_->allSoundsOff(); | |||
} | |||
void MidiManager::processSustain(const MidiMessage& midi_message, int sample_position, int channel) { | |||
bool on = midi_message.isSustainPedalOn(); | |||
if (isMpeChannelMasterLowerZone(channel)) { | |||
if (on) | |||
engine_->sustainOnRange(lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
else | |||
engine_->sustainOffRange(sample_position, lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
} | |||
else if (isMpeChannelMasterUpperZone(channel)) { | |||
if (on) | |||
engine_->sustainOnRange(upperZoneStartChannel(), upperZoneEndChannel()); | |||
else | |||
engine_->sustainOffRange(sample_position, upperZoneStartChannel(), upperZoneEndChannel()); | |||
} | |||
else { | |||
if (on) | |||
engine_->sustainOn(channel); | |||
else | |||
engine_->sustainOff(sample_position, channel); | |||
} | |||
} | |||
void MidiManager::processSostenuto(const MidiMessage& midi_message, int sample_position, int channel) { | |||
bool on = midi_message.isSostenutoPedalOn(); | |||
if (isMpeChannelMasterLowerZone(channel)) { | |||
if (on) | |||
engine_->sostenutoOnRange(lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
else | |||
engine_->sostenutoOffRange(sample_position, lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
} | |||
else if (isMpeChannelMasterUpperZone(channel)) { | |||
if (on) | |||
engine_->sostenutoOnRange(upperZoneStartChannel(), upperZoneEndChannel()); | |||
else | |||
engine_->sostenutoOffRange(sample_position, upperZoneStartChannel(), upperZoneEndChannel()); | |||
} | |||
else { | |||
if (on) | |||
engine_->sostenutoOn(channel); | |||
else | |||
engine_->sostenutoOff(sample_position, channel); | |||
} | |||
} | |||
void MidiManager::processPitchBend(const MidiMessage& midi_message, int sample_position, int channel) { | |||
vital::mono_float percent = midi_message.getPitchWheelValue() / kHighResolutionMax; | |||
vital::mono_float value = 2 * percent - 1.0f; | |||
if (isMpeChannelMasterLowerZone(channel)) { | |||
engine_->setZonedPitchWheel(value, lowerMasterChannel(), lowerMasterChannel() + 1); | |||
engine_->setZonedPitchWheel(value, lowerZoneStartChannel(), lowerZoneEndChannel()); | |||
listener_->pitchWheelMidiChanged(value); | |||
} | |||
else if (isMpeChannelMasterUpperZone(channel)) { | |||
engine_->setZonedPitchWheel(value, upperMasterChannel(), upperMasterChannel() + 1); | |||
engine_->setZonedPitchWheel(value, upperZoneStartChannel(), upperZoneEndChannel()); | |||
listener_->pitchWheelMidiChanged(value); | |||
} | |||
else if (mpe_enabled_) | |||
engine_->setPitchWheel(value, channel); | |||
else { | |||
engine_->setZonedPitchWheel(value, channel, channel); | |||
listener_->pitchWheelMidiChanged(value); | |||
} | |||
} | |||
void MidiManager::processPressure(const MidiMessage& midi_message, int sample_position, int channel) { | |||
vital::mono_float value = toHighResolutionValue(msb_pressure_values_[channel], lsb_pressure_values_[channel]); | |||
if (isMpeChannelMasterLowerZone(channel)) | |||
engine_->setChannelRangeAftertouch(lowerZoneStartChannel(), lowerZoneEndChannel(), value, 0); | |||
else if (isMpeChannelMasterUpperZone(channel)) | |||
engine_->setChannelRangeAftertouch(upperZoneStartChannel(), upperZoneEndChannel(), value, 0); | |||
else | |||
engine_->setChannelAftertouch(channel, value, sample_position); | |||
} | |||
void MidiManager::processSlide(const MidiMessage& midi_message, int sample_position, int channel) { | |||
vital::mono_float value = toHighResolutionValue(msb_slide_values_[channel], lsb_slide_values_[channel]); | |||
if (isMpeChannelMasterLowerZone(channel)) | |||
engine_->setChannelRangeSlide(value, lowerZoneStartChannel(), lowerZoneEndChannel(), 0); | |||
else if (isMpeChannelMasterUpperZone(channel)) | |||
engine_->setChannelRangeSlide(value, upperZoneStartChannel(), upperZoneEndChannel(), 0); | |||
else | |||
engine_->setChannelSlide(channel, value, sample_position); | |||
} | |||
force_inline bool MidiManager::isMpeChannelMasterLowerZone(int channel) { | |||
return mpe_enabled_ && mpe_zone_layout_.getLowerZone().isActive() && lowerMasterChannel() == channel; | |||
} | |||
force_inline bool MidiManager::isMpeChannelMasterUpperZone(int channel) { | |||
return mpe_enabled_ && mpe_zone_layout_.getUpperZone().isActive() && upperMasterChannel() == channel; | |||
} | |||
void MidiManager::processMidiMessage(const MidiMessage& midi_message, int sample_position) { | |||
if (midi_message.isController()) | |||
readMpeMessage(midi_message); | |||
int channel = midi_message.getChannel() - 1; | |||
MidiMainType type = static_cast<MidiMainType>(midi_message.getRawData()[0] & 0xf0); | |||
switch (type) { | |||
case kProgramChange: | |||
return; | |||
case kNoteOn: { | |||
uint8 velocity = midi_message.getVelocity(); | |||
if (velocity) | |||
engine_->noteOn(midi_message.getNoteNumber(), velocity / kControlMax, sample_position, channel); | |||
else | |||
engine_->noteOff(midi_message.getNoteNumber(), velocity / kControlMax, sample_position, channel); | |||
return; | |||
} | |||
case kNoteOff: { | |||
vital::mono_float velocity = midi_message.getVelocity() / kControlMax; | |||
engine_->noteOff(midi_message.getNoteNumber(), velocity, sample_position, channel); | |||
return; | |||
} | |||
case kAftertouch: { | |||
int note = midi_message.getNoteNumber(); | |||
vital::mono_float value = midi_message.getAfterTouchValue() / kControlMax; | |||
engine_->setAftertouch(note, value, sample_position, channel); | |||
return; | |||
} | |||
case kChannelPressure: { | |||
msb_pressure_values_[channel] = midi_message.getChannelPressureValue(); | |||
processPressure(midi_message, sample_position, channel); | |||
return; | |||
} | |||
case kPitchWheel: { | |||
processPitchBend(midi_message, sample_position, channel); | |||
return; | |||
} | |||
case kController: { | |||
MidiSecondaryType secondary_type = static_cast<MidiSecondaryType>(midi_message.getControllerNumber()); | |||
switch (secondary_type) { | |||
case kSlide: { | |||
msb_slide_values_[channel] = midi_message.getControllerValue(); | |||
processSlide(midi_message, sample_position, channel); | |||
break; | |||
} | |||
case kLsbPressure: { | |||
lsb_pressure_values_[channel] = midi_message.getControllerValue(); | |||
processPressure(midi_message, sample_position, channel); | |||
break; | |||
} | |||
case kLsbSlide: { | |||
lsb_slide_values_[channel] = midi_message.getControllerValue(); | |||
processSlide(midi_message, sample_position, channel); | |||
break; | |||
} | |||
case kSustainPedal: { | |||
processSustain(midi_message, sample_position, channel); | |||
break; | |||
} | |||
case kSostenutoPedal: { | |||
processSostenuto(midi_message, sample_position, channel); | |||
break; | |||
} | |||
case kSoftPedalOn: // TODO | |||
break; | |||
case kModWheel: { | |||
vital::mono_float percent = (1.0f * midi_message.getControllerValue()) / kControlMax; | |||
engine_->setModWheel(percent, channel); | |||
listener_->modWheelMidiChanged(percent); | |||
break; | |||
} | |||
case kAllNotesOff: | |||
case kAllControllersOff: | |||
processAllNotesOff(midi_message, sample_position, channel); | |||
return; | |||
case kAllSoundsOff: | |||
processAllSoundsOff(); | |||
break; | |||
case kBankSelect: | |||
current_bank_ = midi_message.getControllerValue(); | |||
return; | |||
case kFolderSelect: | |||
current_folder_ = midi_message.getControllerValue(); | |||
return; | |||
} | |||
midiInput(midi_message.getControllerNumber(), midi_message.getControllerValue()); | |||
} | |||
} | |||
} | |||
void MidiManager::handleIncomingMidiMessage(MidiInput* source, const MidiMessage &midi_message) { | |||
midi_collector_.addMessageToQueue(midi_message); | |||
} | |||
void MidiManager::replaceKeyboardMessages(MidiBuffer& buffer, int num_samples) { | |||
keyboard_state_->processNextMidiBuffer(buffer, 0, num_samples, true); | |||
} |
@@ -0,0 +1,169 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "common.h" | |||
#include <string> | |||
#include <map> | |||
#if !defined(JUCE_AUDIO_DEVICES_H_INCLUDED) | |||
class MidiInput { }; | |||
class MidiInputCallback { | |||
public: | |||
virtual ~MidiInputCallback() { } | |||
virtual void handleIncomingMidiMessage(MidiInput *source, const MidiMessage &midi_message) { } | |||
}; | |||
class MidiMessageCollector { | |||
public: | |||
void reset(int sample_rate) { } | |||
void removeNextBlockOfMessages(MidiBuffer& buffer, int num_samples) { } | |||
void addMessageToQueue(const MidiMessage &midi_message) { } | |||
}; | |||
#endif | |||
class SynthBase; | |||
namespace vital { | |||
class SoundEngine; | |||
struct ValueDetails; | |||
} // namespace vital | |||
class MidiManager : public MidiInputCallback { | |||
public: | |||
typedef std::map<int, std::map<std::string, const vital::ValueDetails*>> midi_map; | |||
enum MidiMainType { | |||
kNoteOff = 0x80, | |||
kNoteOn = 0x90, | |||
kAftertouch = 0xa0, | |||
kController = 0xb0, | |||
kProgramChange = 0xc0, | |||
kChannelPressure = 0xd0, | |||
kPitchWheel = 0xe0, | |||
}; | |||
enum MidiSecondaryType { | |||
kBankSelect = 0x00, | |||
kModWheel = 0x01, | |||
kFolderSelect = 0x20, | |||
kSustainPedal = 0x40, | |||
kSostenutoPedal = 0x42, | |||
kSoftPedalOn = 0x43, | |||
kSlide = 0x4a, | |||
kLsbPressure = 0x66, | |||
kLsbSlide = 0x6a, | |||
kAllSoundsOff = 0x78, | |||
kAllControllersOff = 0x79, | |||
kAllNotesOff = 0x7b, | |||
}; | |||
class Listener { | |||
public: | |||
virtual ~Listener() { } | |||
virtual void valueChangedThroughMidi(const std::string& name, vital::mono_float value) = 0; | |||
virtual void pitchWheelMidiChanged(vital::mono_float value) = 0; | |||
virtual void modWheelMidiChanged(vital::mono_float value) = 0; | |||
virtual void presetChangedThroughMidi(File preset) = 0; | |||
}; | |||
MidiManager(SynthBase* synth, MidiKeyboardState* keyboard_state, | |||
std::map<std::string, String>* gui_state, Listener* listener = nullptr); | |||
virtual ~MidiManager(); | |||
void armMidiLearn(std::string name); | |||
void cancelMidiLearn(); | |||
void clearMidiLearn(const std::string& name); | |||
void midiInput(int control, vital::mono_float value); | |||
void processMidiMessage(const MidiMessage &midi_message, int sample_position = 0); | |||
bool isMidiMapped(const std::string& name) const; | |||
void setSampleRate(double sample_rate); | |||
void removeNextBlockOfMessages(MidiBuffer& buffer, int num_samples); | |||
void replaceKeyboardMessages(MidiBuffer& buffer, int num_samples); | |||
void processAllNotesOff(const MidiMessage& midi_message, int sample_position, int channel); | |||
void processAllSoundsOff(); | |||
void processSustain(const MidiMessage& midi_message, int sample_position, int channel); | |||
void processSostenuto(const MidiMessage& midi_message, int sample_position, int channel); | |||
void processPitchBend(const MidiMessage& midi_message, int sample_position, int channel); | |||
void processPressure(const MidiMessage& midi_message, int sample_position, int channel); | |||
void processSlide(const MidiMessage& midi_message, int sample_position, int channel); | |||
bool isMpeChannelMasterLowerZone(int channel); | |||
bool isMpeChannelMasterUpperZone(int channel); | |||
force_inline int lowerZoneStartChannel() { return mpe_zone_layout_.getLowerZone().getFirstMemberChannel() - 1; } | |||
force_inline int upperZoneStartChannel() { return mpe_zone_layout_.getUpperZone().getLastMemberChannel() - 1; } | |||
force_inline int lowerZoneEndChannel() { return mpe_zone_layout_.getLowerZone().getLastMemberChannel() - 1; } | |||
force_inline int upperZoneEndChannel() { return mpe_zone_layout_.getUpperZone().getFirstMemberChannel() - 1; } | |||
force_inline int lowerMasterChannel() { return mpe_zone_layout_.getLowerZone().getMasterChannel() - 1; } | |||
force_inline int upperMasterChannel() { return mpe_zone_layout_.getUpperZone().getMasterChannel() - 1; } | |||
void setMpeEnabled(bool enabled) { mpe_enabled_ = enabled; } | |||
midi_map getMidiLearnMap() { return midi_learn_map_; } | |||
void setMidiLearnMap(const midi_map& midi_learn_map) { midi_learn_map_ = midi_learn_map; } | |||
// MidiInputCallback | |||
void handleIncomingMidiMessage(MidiInput *source, const MidiMessage &midi_message) override; | |||
struct PresetLoadedCallback : public CallbackMessage { | |||
PresetLoadedCallback(Listener* lis, File pre) : listener(lis), preset(std::move(pre)) { } | |||
void messageCallback() override { | |||
if (listener) | |||
listener->presetChangedThroughMidi(preset); | |||
} | |||
Listener* listener; | |||
File preset; | |||
}; | |||
protected: | |||
void readMpeMessage(const MidiMessage& message); | |||
SynthBase* synth_; | |||
vital::SoundEngine* engine_; | |||
MidiKeyboardState* keyboard_state_; | |||
MidiMessageCollector midi_collector_; | |||
std::map<std::string, String>* gui_state_; | |||
Listener* listener_; | |||
int current_bank_; | |||
int current_folder_; | |||
int current_preset_; | |||
const vital::ValueDetails* armed_value_; | |||
midi_map midi_learn_map_; | |||
int msb_pressure_values_[vital::kNumMidiChannels]; | |||
int lsb_pressure_values_[vital::kNumMidiChannels]; | |||
int msb_slide_values_[vital::kNumMidiChannels]; | |||
int lsb_slide_values_[vital::kNumMidiChannels]; | |||
bool mpe_enabled_; | |||
MPEZoneLayout mpe_zone_layout_; | |||
MidiRPNDetector rpn_detector_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiManager) | |||
}; | |||
@@ -0,0 +1,38 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "startup.h" | |||
#include "load_save.h" | |||
#include "JuceHeader.h" | |||
#include "synth_base.h" | |||
void Startup::doStartupChecks(MidiManager* midi_manager, vital::StringLayout* layout) { | |||
if (!LoadSave::isInstalled()) | |||
return; | |||
if (LoadSave::wasUpgraded()) | |||
LoadSave::saveVersionConfig(); | |||
LoadSave::loadConfig(midi_manager, layout); | |||
} | |||
bool Startup::isComputerCompatible() { | |||
#if defined(__ARM_NEON__) | |||
return true; | |||
#else | |||
return SystemStats::hasSSE2() || SystemStats::hasAVX2(); | |||
#endif | |||
} |
@@ -0,0 +1,39 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include <map> | |||
class SynthBase; | |||
namespace vital { | |||
class StringLayout; | |||
} | |||
class MidiManager; | |||
class Startup { | |||
public: | |||
static void doStartupChecks(MidiManager* midi_manager, vital::StringLayout* layout = nullptr); | |||
static bool isComputerCompatible(); | |||
private: | |||
Startup() = delete; | |||
}; | |||
@@ -0,0 +1,770 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "synth_base.h" | |||
#include "sample_source.h" | |||
#include "sound_engine.h" | |||
#include "load_save.h" | |||
#include "memory.h" | |||
#include "modulation_connection_processor.h" | |||
#include "startup.h" | |||
#include "synth_gui_interface.h" | |||
#include "synth_parameters.h" | |||
#include "utils.h" | |||
SynthBase::SynthBase() : expired_(false) { | |||
expired_ = LoadSave::isExpired(); | |||
self_reference_ = std::make_shared<SynthBase*>(); | |||
*self_reference_ = this; | |||
engine_ = std::make_unique<vital::SoundEngine>(); | |||
engine_->setTuning(&tuning_); | |||
mod_connections_.reserve(vital::kMaxModulationConnections); | |||
for (int i = 0; i < vital::kNumOscillators; ++i) { | |||
vital::Wavetable* wavetable = engine_->getWavetable(i); | |||
if (wavetable) { | |||
wavetable_creators_[i] = std::make_unique<WavetableCreator>(wavetable); | |||
wavetable_creators_[i]->init(); | |||
} | |||
} | |||
keyboard_state_ = std::make_unique<MidiKeyboardState>(); | |||
midi_manager_ = std::make_unique<MidiManager>(this, keyboard_state_.get(), &save_info_, this); | |||
last_played_note_ = 0.0f; | |||
last_num_pressed_ = 0; | |||
audio_memory_ = std::make_unique<vital::StereoMemory>(vital::kAudioMemorySamples); | |||
memset(oscilloscope_memory_, 0, 2 * vital::kOscilloscopeMemoryResolution * sizeof(vital::poly_float)); | |||
memset(oscilloscope_memory_write_, 0, 2 * vital::kOscilloscopeMemoryResolution * sizeof(vital::poly_float)); | |||
memory_reset_period_ = vital::kOscilloscopeMemoryResolution; | |||
memory_input_offset_ = 0; | |||
memory_index_ = 0; | |||
controls_ = engine_->getControls(); | |||
Startup::doStartupChecks(midi_manager_.get()); | |||
} | |||
SynthBase::~SynthBase() { } | |||
void SynthBase::valueChanged(const std::string& name, vital::mono_float value) { | |||
controls_[name]->set(value); | |||
} | |||
void SynthBase::valueChangedInternal(const std::string& name, vital::mono_float value) { | |||
valueChanged(name, value); | |||
setValueNotifyHost(name, value); | |||
} | |||
void SynthBase::valueChangedThroughMidi(const std::string& name, vital::mono_float value) { | |||
controls_[name]->set(value); | |||
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, name, value); | |||
setValueNotifyHost(name, value); | |||
callback->post(); | |||
} | |||
void SynthBase::pitchWheelMidiChanged(vital::mono_float value) { | |||
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, "pitch_wheel", value); | |||
callback->post(); | |||
} | |||
void SynthBase::modWheelMidiChanged(vital::mono_float value) { | |||
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, "mod_wheel", value); | |||
callback->post(); | |||
} | |||
void SynthBase::pitchWheelGuiChanged(vital::mono_float value) { | |||
engine_->setZonedPitchWheel(value, 0, vital::kNumMidiChannels - 1); | |||
} | |||
void SynthBase::modWheelGuiChanged(vital::mono_float value) { | |||
engine_->setModWheelAllChannels(value); | |||
} | |||
void SynthBase::presetChangedThroughMidi(File preset) { | |||
SynthGuiInterface* gui_interface = getGuiInterface(); | |||
if (gui_interface) { | |||
gui_interface->updateFullGui(); | |||
gui_interface->notifyFresh(); | |||
} | |||
} | |||
void SynthBase::valueChangedExternal(const std::string& name, vital::mono_float value) { | |||
valueChanged(name, value); | |||
if (name == "mod_wheel") | |||
engine_->setModWheelAllChannels(value); | |||
else if (name == "pitch_wheel") | |||
engine_->setZonedPitchWheel(value, 0, vital::kNumMidiChannels - 1); | |||
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, name, value); | |||
callback->post(); | |||
} | |||
vital::ModulationConnection* SynthBase::getConnection(const std::string& source, const std::string& destination) { | |||
for (vital::ModulationConnection* connection : mod_connections_) { | |||
if (connection->source_name == source && connection->destination_name == destination) | |||
return connection; | |||
} | |||
return nullptr; | |||
} | |||
int SynthBase::getConnectionIndex(const std::string& source, const std::string& destination) { | |||
vital::ModulationConnectionBank& modulation_bank = getModulationBank(); | |||
for (int i = 0; i < vital::kMaxModulationConnections; ++i) { | |||
vital::ModulationConnection* connection = modulation_bank.atIndex(i); | |||
if (connection->source_name == source && connection->destination_name == destination) | |||
return i; | |||
} | |||
return -1; | |||
} | |||
vital::modulation_change SynthBase::createModulationChange(vital::ModulationConnection* connection) { | |||
vital::modulation_change change; | |||
change.source = engine_->getModulationSource(connection->source_name); | |||
change.mono_destination = engine_->getMonoModulationDestination(connection->destination_name); | |||
change.mono_modulation_switch = engine_->getMonoModulationSwitch(connection->destination_name); | |||
VITAL_ASSERT(change.source != nullptr); | |||
VITAL_ASSERT(change.mono_destination != nullptr); | |||
VITAL_ASSERT(change.mono_modulation_switch != nullptr); | |||
change.destination_scale = vital::Parameters::getParameterRange(connection->destination_name); | |||
change.poly_modulation_switch = engine_->getPolyModulationSwitch(connection->destination_name); | |||
change.poly_destination = engine_->getPolyModulationDestination(connection->destination_name); | |||
change.modulation_processor = connection->modulation_processor.get(); | |||
int num_audio_rate = 0; | |||
vital::ModulationConnectionBank& modulation_bank = getModulationBank(); | |||
for (int i = 0; i < vital::kMaxModulationConnections; ++i) { | |||
if (modulation_bank.atIndex(i)->source_name == connection->source_name && | |||
modulation_bank.atIndex(i)->destination_name != connection->destination_name && | |||
!modulation_bank.atIndex(i)->modulation_processor->isControlRate()) { | |||
num_audio_rate++; | |||
} | |||
} | |||
change.num_audio_rate = num_audio_rate; | |||
return change; | |||
} | |||
bool SynthBase::isInvalidConnection(const vital::modulation_change& change) { | |||
return change.poly_destination && change.poly_destination->router() == change.modulation_processor; | |||
} | |||
void SynthBase::connectModulation(vital::ModulationConnection* connection) { | |||
vital::modulation_change change = createModulationChange(connection); | |||
if (isInvalidConnection(change)) { | |||
connection->destination_name = ""; | |||
connection->source_name = ""; | |||
} | |||
else if (mod_connections_.count(connection) == 0) { | |||
change.disconnecting = false; | |||
mod_connections_.push_back(connection); | |||
modulation_change_queue_.enqueue(change); | |||
} | |||
} | |||
bool SynthBase::connectModulation(const std::string& source, const std::string& destination) { | |||
vital::ModulationConnection* connection = getConnection(source, destination); | |||
bool create = connection == nullptr; | |||
if (create) | |||
connection = getModulationBank().createConnection(source, destination); | |||
if (connection) | |||
connectModulation(connection); | |||
return create; | |||
} | |||
void SynthBase::disconnectModulation(vital::ModulationConnection* connection) { | |||
if (mod_connections_.count(connection) == 0) | |||
return; | |||
vital::modulation_change change = createModulationChange(connection); | |||
connection->source_name = ""; | |||
connection->destination_name = ""; | |||
mod_connections_.remove(connection); | |||
change.disconnecting = true; | |||
modulation_change_queue_.enqueue(change); | |||
} | |||
void SynthBase::disconnectModulation(const std::string& source, const std::string& destination) { | |||
vital::ModulationConnection* connection = getConnection(source, destination); | |||
if (connection) | |||
disconnectModulation(connection); | |||
} | |||
void SynthBase::clearModulations() { | |||
clearModulationQueue(); | |||
while (mod_connections_.size()) { | |||
vital::ModulationConnection* connection = *mod_connections_.begin(); | |||
mod_connections_.remove(connection); | |||
vital::modulation_change change = createModulationChange(connection); | |||
change.disconnecting = true; | |||
engine_->disconnectModulation(change); | |||
connection->source_name = ""; | |||
connection->destination_name = ""; | |||
} | |||
int num_connections = static_cast<int>(getModulationBank().numConnections()); | |||
for (int i = 0; i < num_connections; ++i) | |||
getModulationBank().atIndex(i)->modulation_processor->lineMapGenerator()->initLinear(); | |||
engine_->disableUnnecessaryModSources(); | |||
} | |||
void SynthBase::forceShowModulation(const std::string& source, bool force) { | |||
if (force) | |||
engine_->enableModSource(source); | |||
else if (!isSourceConnected(source)) | |||
engine_->disableModSource(source); | |||
} | |||
bool SynthBase::isModSourceEnabled(const std::string& source) { | |||
return engine_->isModSourceEnabled(source); | |||
} | |||
int SynthBase::getNumModulations(const std::string& destination) { | |||
int connections = 0; | |||
for (vital::ModulationConnection* connection : mod_connections_) { | |||
if (connection->destination_name == destination) | |||
connections++; | |||
} | |||
return connections; | |||
} | |||
std::vector<vital::ModulationConnection*> SynthBase::getSourceConnections(const std::string& source) { | |||
std::vector<vital::ModulationConnection*> connections; | |||
for (vital::ModulationConnection* connection : mod_connections_) { | |||
if (connection->source_name == source) | |||
connections.push_back(connection); | |||
} | |||
return connections; | |||
} | |||
bool SynthBase::isSourceConnected(const std::string& source) { | |||
for (vital::ModulationConnection* connection : mod_connections_) { | |||
if (connection->source_name == source) | |||
return true; | |||
} | |||
return false; | |||
} | |||
std::vector<vital::ModulationConnection*> SynthBase::getDestinationConnections(const std::string& destination) { | |||
std::vector<vital::ModulationConnection*> connections; | |||
for (vital::ModulationConnection* connection : mod_connections_) { | |||
if (connection->destination_name == destination) | |||
connections.push_back(connection); | |||
} | |||
return connections; | |||
} | |||
const vital::StatusOutput* SynthBase::getStatusOutput(const std::string& name) { | |||
return engine_->getStatusOutput(name); | |||
} | |||
vital::Wavetable* SynthBase::getWavetable(int index) { | |||
return engine_->getWavetable(index); | |||
} | |||
WavetableCreator* SynthBase::getWavetableCreator(int index) { | |||
return wavetable_creators_[index].get(); | |||
} | |||
vital::Sample* SynthBase::getSample() { | |||
return engine_->getSample(); | |||
} | |||
LineGenerator* SynthBase::getLfoSource(int index) { | |||
return engine_->getLfoSource(index); | |||
} | |||
json SynthBase::saveToJson() { | |||
return LoadSave::stateToJson(this, getCriticalSection()); | |||
} | |||
int SynthBase::getSampleRate() { | |||
return engine_->getSampleRate(); | |||
} | |||
void SynthBase::initEngine() { | |||
clearModulations(); | |||
if (getWavetableCreator(0)) { | |||
for (int i = 0; i < vital::kNumOscillators; ++i) | |||
getWavetableCreator(i)->init(); | |||
engine_->getSample()->init(); | |||
} | |||
for (int i = 0; i < vital::kNumLfos; ++i) | |||
getLfoSource(i)->initTriangle(); | |||
vital::control_map controls = engine_->getControls(); | |||
for (auto& control : controls) { | |||
vital::ValueDetails details = vital::Parameters::getDetails(control.first); | |||
control.second->set(details.default_value); | |||
} | |||
checkOversampling(); | |||
clearActiveFile(); | |||
} | |||
void SynthBase::loadTuningFile(const File& file) { | |||
tuning_.loadFile(file); | |||
} | |||
void SynthBase::loadInitPreset() { | |||
pauseProcessing(true); | |||
engine_->allSoundsOff(); | |||
initEngine(); | |||
LoadSave::initSaveInfo(save_info_); | |||
pauseProcessing(false); | |||
} | |||
bool SynthBase::loadFromJson(const json& data) { | |||
pauseProcessing(true); | |||
engine_->allSoundsOff(); | |||
try { | |||
bool result = LoadSave::jsonToState(this, save_info_, data); | |||
pauseProcessing(false); | |||
return result; | |||
} | |||
catch (const json::exception& e) { | |||
pauseProcessing(false); | |||
throw e; | |||
} | |||
} | |||
bool SynthBase::loadFromFile(File preset, std::string& error) { | |||
if (!preset.exists()) | |||
return false; | |||
try { | |||
json parsed_json_state = json::parse(preset.loadFileAsString().toStdString(), nullptr); | |||
if (!loadFromJson(parsed_json_state)) { | |||
error = "Preset was created with a newer version."; | |||
return false; | |||
} | |||
active_file_ = preset; | |||
} | |||
catch (const json::exception& e) { | |||
error = "Preset file is corrupted."; | |||
return false; | |||
} | |||
setPresetName(preset.getFileNameWithoutExtension()); | |||
SynthGuiInterface* gui_interface = getGuiInterface(); | |||
if (gui_interface) { | |||
gui_interface->updateFullGui(); | |||
gui_interface->notifyFresh(); | |||
} | |||
return true; | |||
} | |||
void SynthBase::renderAudioToFile(File file, float seconds, float bpm, std::vector<int> notes, bool render_images) { | |||
static constexpr int kSampleRate = 44100; | |||
static constexpr int kPreProcessSamples = 44100; | |||
static constexpr int kFadeSamples = 200; | |||
static constexpr int kBufferSize = 64; | |||
static constexpr int kVideoRate = 30; | |||
static constexpr int kImageNumberPlaces = 3; | |||
static constexpr int kImageWidth = 500; | |||
static constexpr int kImageHeight = 250; | |||
static constexpr int kOscilloscopeResolution = 512; | |||
static constexpr float kFadeRatio = 0.3f; | |||
ScopedLock lock(getCriticalSection()); | |||
processModulationChanges(); | |||
engine_->setSampleRate(kSampleRate); | |||
engine_->setBpm(bpm); | |||
engine_->updateAllModulationSwitches(); | |||
double sample_time = 1.0 / getSampleRate(); | |||
double current_time = -kPreProcessSamples * sample_time; | |||
for (int samples = 0; samples < kPreProcessSamples; samples += kBufferSize) { | |||
engine_->correctToTime(current_time); | |||
current_time += kBufferSize * sample_time; | |||
engine_->process(kBufferSize); | |||
} | |||
for (int note : notes) | |||
engine_->noteOn(note, 0.7f, 0, 0); | |||
file.deleteFile(); | |||
std::unique_ptr<FileOutputStream> file_stream = file.createOutputStream(); | |||
WavAudioFormat wav_format; | |||
std::unique_ptr<AudioFormatWriter> writer(wav_format.createWriterFor(file_stream.get(), kSampleRate, 2, 16, {}, 0)); | |||
int on_samples = seconds * kSampleRate; | |||
int total_samples = on_samples + seconds * kSampleRate * kFadeRatio; | |||
std::unique_ptr<float[]> left_buffer = std::make_unique<float[]>(kBufferSize); | |||
std::unique_ptr<float[]> right_buffer = std::make_unique<float[]>(kBufferSize); | |||
float* buffers[2] = { left_buffer.get(), right_buffer.get() }; | |||
const vital::mono_float* engine_output = (const vital::mono_float*)engine_->output(0)->buffer; | |||
#if JUCE_MODULE_AVAILABLE_juce_graphics | |||
int current_image_index = -1; | |||
PNGImageFormat png; | |||
File images_folder = File::getCurrentWorkingDirectory().getChildFile("images"); | |||
if (!images_folder.exists() && render_images) | |||
images_folder.createDirectory(); | |||
const vital::poly_float* memory = getOscilloscopeMemory(); | |||
#endif | |||
for (int samples = 0; samples < total_samples; samples += kBufferSize) { | |||
engine_->correctToTime(current_time); | |||
current_time += kBufferSize * sample_time; | |||
engine_->process(kBufferSize); | |||
updateMemoryOutput(kBufferSize, engine_->output(0)->buffer); | |||
if (on_samples > samples && on_samples <= samples + kBufferSize) { | |||
for (int note : notes) | |||
engine_->noteOff(note, 0.5f, 0, 0); | |||
} | |||
for (int i = 0; i < kBufferSize; ++i) { | |||
vital::mono_float t = (total_samples - samples) / (1.0f * kFadeSamples); | |||
t = vital::utils::min(t, 1.0f); | |||
left_buffer[i] = t * engine_output[vital::poly_float::kSize * i]; | |||
right_buffer[i] = t * engine_output[vital::poly_float::kSize * i + 1]; | |||
} | |||
writer->writeFromFloatArrays(buffers, 2, kBufferSize); | |||
#if JUCE_MODULE_AVAILABLE_juce_graphics | |||
int image_index = (samples * kVideoRate) / kSampleRate; | |||
if (image_index > current_image_index && render_images) { | |||
current_image_index = image_index; | |||
String number(image_index); | |||
while (number.length() < kImageNumberPlaces) | |||
number = "0" + number; | |||
File image_file = images_folder.getChildFile("rendered_image" + number + ".png"); | |||
FileOutputStream image_file_stream(image_file); | |||
Image image(Image::RGB, kImageWidth, kImageHeight, true); | |||
Graphics g(image); | |||
g.fillAll(Colour(0xff1d2125)); | |||
Path left_path; | |||
Path right_path; | |||
left_path.startNewSubPath(-2.0f, kImageHeight / 2); | |||
right_path.startNewSubPath(-2.0f, kImageHeight / 2); | |||
for (int i = 0; i < kOscilloscopeResolution; ++i) { | |||
float t = i / (kOscilloscopeResolution - 1.0f); | |||
float memory_spot = (1.0f * i * vital::kOscilloscopeMemoryResolution) / kOscilloscopeResolution; | |||
int memory_index = memory_spot; | |||
float remainder = memory_spot - memory_index; | |||
vital::poly_float from = memory[memory_index]; | |||
vital::poly_float to = memory[memory_index + 1]; | |||
vital::poly_float y = -vital::utils::interpolate(from, to, remainder) * kImageHeight / 2.0f + kImageHeight / 2; | |||
left_path.lineTo(t * kImageWidth, y[0]); | |||
right_path.lineTo(t * kImageWidth, y[1]); | |||
} | |||
left_path.lineTo(kImageWidth + 2.0f, kImageHeight / 2.0f); | |||
right_path.lineTo(kImageWidth + 2.0f, kImageHeight / 2.0f); | |||
g.setColour(Colour(0x64aa88ff)); | |||
g.fillPath(left_path); | |||
g.fillPath(right_path); | |||
g.setColour(Colour(0xffaa88ff)); | |||
g.strokePath(left_path, PathStrokeType(2.0f, PathStrokeType::curved, PathStrokeType::rounded)); | |||
g.strokePath(right_path, PathStrokeType(2.0f, PathStrokeType::curved, PathStrokeType::rounded)); | |||
png.writeImageToStream(image, image_file_stream); | |||
} | |||
#endif | |||
} | |||
writer->flush(); | |||
file_stream->flush(); | |||
writer = nullptr; | |||
file_stream.release(); | |||
} | |||
void SynthBase::renderAudioForResynthesis(float* data, int samples, int note) { | |||
static constexpr int kPreProcessSamples = 44100; | |||
static constexpr int kBufferSize = 64; | |||
ScopedLock lock(getCriticalSection()); | |||
double sample_time = 1.0 / getSampleRate(); | |||
double current_time = -kPreProcessSamples * sample_time; | |||
engine_->allSoundsOff(); | |||
for (int s = 0; s < kPreProcessSamples; s += kBufferSize) { | |||
engine_->correctToTime(current_time); | |||
current_time += kBufferSize * sample_time; | |||
engine_->process(kBufferSize); | |||
} | |||
engine_->noteOn(note, 0.7f, 0, 0); | |||
const vital::poly_float* engine_output = engine_->output(0)->buffer; | |||
float max_value = 0.01f; | |||
for (int s = 0; s < samples; s += kBufferSize) { | |||
int num_samples = std::min(samples - s, kBufferSize); | |||
engine_->correctToTime(current_time); | |||
current_time += num_samples * sample_time; | |||
engine_->process(num_samples); | |||
for (int i = 0; i < num_samples; ++i) { | |||
float sample = engine_output[i][0]; | |||
data[s + i] = sample; | |||
max_value = std::max(max_value, fabsf(sample)); | |||
} | |||
} | |||
float scale = 1.0f / max_value; | |||
for (int s = 0; s < samples; ++s) | |||
data[s] *= scale; | |||
engine_->allSoundsOff(); | |||
} | |||
bool SynthBase::saveToFile(File preset) { | |||
preset = preset.withFileExtension(String(vital::kPresetExtension)); | |||
File parent = preset.getParentDirectory(); | |||
if (!parent.exists()) { | |||
if (!parent.createDirectory().wasOk() || !parent.hasWriteAccess()) | |||
return false; | |||
} | |||
setPresetName(preset.getFileNameWithoutExtension()); | |||
SynthGuiInterface* gui_interface = getGuiInterface(); | |||
if (gui_interface) | |||
gui_interface->notifyFresh(); | |||
if (preset.replaceWithText(saveToJson().dump())) { | |||
active_file_ = preset; | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool SynthBase::saveToActiveFile() { | |||
if (!active_file_.exists() || !active_file_.hasWriteAccess()) | |||
return false; | |||
return saveToFile(active_file_); | |||
} | |||
void SynthBase::setMpeEnabled(bool enabled) { | |||
midi_manager_->setMpeEnabled(enabled); | |||
} | |||
void SynthBase::processAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset) { | |||
if (expired_) | |||
return; | |||
engine_->process(samples); | |||
writeAudio(buffer, channels, samples, offset); | |||
} | |||
void SynthBase::processAudioWithInput(AudioSampleBuffer* buffer, const vital::poly_float* input_buffer, | |||
int channels, int samples, int offset) { | |||
if (expired_) | |||
return; | |||
engine_->processWithInput(input_buffer, samples); | |||
writeAudio(buffer, channels, samples, offset); | |||
} | |||
void SynthBase::writeAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset) { | |||
const vital::mono_float* engine_output = (const vital::mono_float*)engine_->output(0)->buffer; | |||
for (int channel = 0; channel < channels; ++channel) { | |||
float* channel_data = buffer->getWritePointer(channel, offset); | |||
for (int i = 0; i < samples; ++i) { | |||
channel_data[i] = engine_output[vital::poly_float::kSize * i + channel]; | |||
VITAL_ASSERT(std::isfinite(channel_data[i])); | |||
} | |||
} | |||
updateMemoryOutput(samples, engine_->output(0)->buffer); | |||
} | |||
void SynthBase::processMidi(MidiBuffer& midi_messages, int start_sample, int end_sample) { | |||
bool process_all = end_sample == 0; | |||
for (const MidiMessageMetadata message : midi_messages) { | |||
int midi_sample = message.samplePosition; | |||
if (process_all || (midi_sample >= start_sample && midi_sample < end_sample)) | |||
midi_manager_->processMidiMessage(message.getMessage(), midi_sample - start_sample); | |||
} | |||
} | |||
void SynthBase::processKeyboardEvents(MidiBuffer& buffer, int num_samples) { | |||
midi_manager_->replaceKeyboardMessages(buffer, num_samples); | |||
} | |||
void SynthBase::processModulationChanges() { | |||
vital::modulation_change change; | |||
while (getNextModulationChange(change)) { | |||
if (change.disconnecting) | |||
engine_->disconnectModulation(change); | |||
else | |||
engine_->connectModulation(change); | |||
} | |||
} | |||
void SynthBase::updateMemoryOutput(int samples, const vital::poly_float* audio) { | |||
for (int i = 0; i < samples; ++i) | |||
audio_memory_->push(audio[i]); | |||
vital::mono_float last_played = engine_->getLastActiveNote(); | |||
last_played = vital::utils::clamp(last_played, kOutputWindowMinNote, kOutputWindowMaxNote); | |||
int num_pressed = engine_->getNumPressedNotes(); | |||
int output_inc = std::max<int>(1, engine_->getSampleRate() / vital::kOscilloscopeMemorySampleRate); | |||
int oscilloscope_samples = 2 * vital::kOscilloscopeMemoryResolution; | |||
if (last_played && (last_played_note_ != last_played || num_pressed > last_num_pressed_)) { | |||
last_played_note_ = last_played; | |||
vital::mono_float frequency = vital::utils::midiNoteToFrequency(last_played_note_); | |||
vital::mono_float period = engine_->getSampleRate() / frequency; | |||
int window_length = output_inc * vital::kOscilloscopeMemoryResolution; | |||
memory_reset_period_ = period; | |||
while (memory_reset_period_ < window_length) | |||
memory_reset_period_ += memory_reset_period_; | |||
memory_reset_period_ = std::min(memory_reset_period_, 2.0f * window_length); | |||
memory_index_ = 0; | |||
vital::utils::copyBuffer(oscilloscope_memory_, oscilloscope_memory_write_, oscilloscope_samples); | |||
} | |||
last_num_pressed_ = num_pressed; | |||
for (; memory_input_offset_ < samples; memory_input_offset_ += output_inc) { | |||
int input_index = vital::utils::iclamp(memory_input_offset_, 0, samples); | |||
memory_index_ = vital::utils::iclamp(memory_index_, 0, oscilloscope_samples - 1); | |||
VITAL_ASSERT(input_index >= 0); | |||
VITAL_ASSERT(input_index < samples); | |||
VITAL_ASSERT(memory_index_ >= 0); | |||
VITAL_ASSERT(memory_index_ < oscilloscope_samples); | |||
oscilloscope_memory_write_[memory_index_++] = audio[input_index]; | |||
if (memory_index_ * output_inc >= memory_reset_period_) { | |||
memory_input_offset_ += memory_reset_period_ - memory_index_ * output_inc; | |||
memory_index_ = 0; | |||
vital::utils::copyBuffer(oscilloscope_memory_, oscilloscope_memory_write_, oscilloscope_samples); | |||
} | |||
} | |||
memory_input_offset_ -= samples; | |||
} | |||
void SynthBase::armMidiLearn(const std::string& name) { | |||
midi_manager_->armMidiLearn(name); | |||
} | |||
void SynthBase::cancelMidiLearn() { | |||
midi_manager_->cancelMidiLearn(); | |||
} | |||
void SynthBase::clearMidiLearn(const std::string& name) { | |||
midi_manager_->clearMidiLearn(name); | |||
} | |||
bool SynthBase::isMidiMapped(const std::string& name) { | |||
return midi_manager_->isMidiMapped(name); | |||
} | |||
void SynthBase::setAuthor(const String& author) { | |||
save_info_["author"] = author; | |||
} | |||
void SynthBase::setComments(const String& comments) { | |||
save_info_["comments"] = comments; | |||
} | |||
void SynthBase::setStyle(const String& style) { | |||
save_info_["style"] = style; | |||
} | |||
void SynthBase::setPresetName(const String& preset_name) { | |||
save_info_["preset_name"] = preset_name; | |||
} | |||
void SynthBase::setMacroName(int index, const String& macro_name) { | |||
save_info_["macro" + std::to_string(index + 1)] = macro_name; | |||
} | |||
String SynthBase::getAuthor() { | |||
return save_info_["author"]; | |||
} | |||
String SynthBase::getComments() { | |||
return save_info_["comments"]; | |||
} | |||
String SynthBase::getStyle() { | |||
return save_info_["style"]; | |||
} | |||
String SynthBase::getPresetName() { | |||
return save_info_["preset_name"]; | |||
} | |||
String SynthBase::getMacroName(int index) { | |||
String name = save_info_["macro" + std::to_string(index + 1)]; | |||
if (name.trim().isEmpty()) | |||
return "MACRO " + String(index + 1); | |||
return name; | |||
} | |||
const vital::StereoMemory* SynthBase::getEqualizerMemory() { | |||
if (engine_) | |||
return engine_->getEqualizerMemory(); | |||
return nullptr; | |||
} | |||
vital::ModulationConnectionBank& SynthBase::getModulationBank() { | |||
return engine_->getModulationBank(); | |||
} | |||
void SynthBase::notifyOversamplingChanged() { | |||
pauseProcessing(true); | |||
engine_->allSoundsOff(); | |||
checkOversampling(); | |||
pauseProcessing(false); | |||
} | |||
void SynthBase::checkOversampling() { | |||
return engine_->checkOversampling(); | |||
} | |||
void SynthBase::ValueChangedCallback::messageCallback() { | |||
if (auto synth_base = listener.lock()) { | |||
SynthGuiInterface* gui_interface = (*synth_base)->getGuiInterface(); | |||
if (gui_interface) { | |||
gui_interface->updateGuiControl(control_name, value); | |||
if (control_name != "pitch_wheel") | |||
gui_interface->notifyChange(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,211 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "concurrentqueue/concurrentqueue.h" | |||
#include "line_generator.h" | |||
#include "synth_constants.h" | |||
#include "synth_types.h" | |||
#include "midi_manager.h" | |||
#include "tuning.h" | |||
#include "wavetable_creator.h" | |||
#include <set> | |||
#include <string> | |||
namespace vital { | |||
class SoundEngine; | |||
struct Output; | |||
class StatusOutput; | |||
class StereoMemory; | |||
class Sample; | |||
class WaveFrame; | |||
class Wavetable; | |||
} | |||
class SynthGuiInterface; | |||
class SynthBase : public MidiManager::Listener { | |||
public: | |||
static constexpr float kOutputWindowMinNote = 16.0f; | |||
static constexpr float kOutputWindowMaxNote = 128.0f; | |||
SynthBase(); | |||
virtual ~SynthBase(); | |||
void valueChanged(const std::string& name, vital::mono_float value); | |||
void valueChangedThroughMidi(const std::string& name, vital::mono_float value) override; | |||
void pitchWheelMidiChanged(vital::mono_float value) override; | |||
void modWheelMidiChanged(vital::mono_float value) override; | |||
void pitchWheelGuiChanged(vital::mono_float value); | |||
void modWheelGuiChanged(vital::mono_float value); | |||
void presetChangedThroughMidi(File preset) override; | |||
void valueChangedExternal(const std::string& name, vital::mono_float value); | |||
void valueChangedInternal(const std::string& name, vital::mono_float value); | |||
bool connectModulation(const std::string& source, const std::string& destination); | |||
void connectModulation(vital::ModulationConnection* connection); | |||
void disconnectModulation(const std::string& source, const std::string& destination); | |||
void disconnectModulation(vital::ModulationConnection* connection); | |||
void clearModulations(); | |||
void forceShowModulation(const std::string& source, bool force); | |||
bool isModSourceEnabled(const std::string& source); | |||
int getNumModulations(const std::string& destination); | |||
int getConnectionIndex(const std::string& source, const std::string& destination); | |||
vital::CircularQueue<vital::ModulationConnection*> getModulationConnections() { return mod_connections_; } | |||
std::vector<vital::ModulationConnection*> getSourceConnections(const std::string& source); | |||
bool isSourceConnected(const std::string& source); | |||
std::vector<vital::ModulationConnection*> getDestinationConnections(const std::string& destination); | |||
const vital::StatusOutput* getStatusOutput(const std::string& name); | |||
vital::Wavetable* getWavetable(int index); | |||
WavetableCreator* getWavetableCreator(int index); | |||
vital::Sample* getSample(); | |||
LineGenerator* getLfoSource(int index); | |||
int getSampleRate(); | |||
void initEngine(); | |||
void loadTuningFile(const File& file); | |||
void loadInitPreset(); | |||
bool loadFromFile(File preset, std::string& error); | |||
void renderAudioToFile(File file, float seconds, float bpm, std::vector<int> notes, bool render_images); | |||
void renderAudioForResynthesis(float* data, int samples, int note); | |||
bool saveToFile(File preset); | |||
bool saveToActiveFile(); | |||
void clearActiveFile() { active_file_ = File(); } | |||
File getActiveFile() { return active_file_; } | |||
void setMpeEnabled(bool enabled); | |||
virtual void beginChangeGesture(const std::string& name) { } | |||
virtual void endChangeGesture(const std::string& name) { } | |||
virtual void setValueNotifyHost(const std::string& name, vital::mono_float value) { } | |||
void armMidiLearn(const std::string& name); | |||
void cancelMidiLearn(); | |||
void clearMidiLearn(const std::string& name); | |||
bool isMidiMapped(const std::string& name); | |||
void setAuthor(const String& author); | |||
void setComments(const String& comments); | |||
void setStyle(const String& comments); | |||
void setPresetName(const String& preset_name); | |||
void setMacroName(int index, const String& macro_name); | |||
String getAuthor(); | |||
String getComments(); | |||
String getStyle(); | |||
String getPresetName(); | |||
String getMacroName(int index); | |||
vital::control_map& getControls() { return controls_; } | |||
vital::SoundEngine* getEngine() { return engine_.get(); } | |||
MidiKeyboardState* getKeyboardState() { return keyboard_state_.get(); } | |||
const vital::poly_float* getOscilloscopeMemory() { return oscilloscope_memory_; } | |||
const vital::StereoMemory* getAudioMemory() { return audio_memory_.get(); } | |||
const vital::StereoMemory* getEqualizerMemory(); | |||
vital::ModulationConnectionBank& getModulationBank(); | |||
void notifyOversamplingChanged(); | |||
void checkOversampling(); | |||
virtual const CriticalSection& getCriticalSection() = 0; | |||
virtual void pauseProcessing(bool pause) = 0; | |||
Tuning* getTuning() { return &tuning_; } | |||
struct ValueChangedCallback : public CallbackMessage { | |||
ValueChangedCallback(std::shared_ptr<SynthBase*> listener, std::string name, vital::mono_float val) : | |||
listener(listener), control_name(std::move(name)), value(val) { } | |||
void messageCallback() override; | |||
std::weak_ptr<SynthBase*> listener; | |||
std::string control_name; | |||
vital::mono_float value; | |||
}; | |||
protected: | |||
vital::modulation_change createModulationChange(vital::ModulationConnection* connection); | |||
bool isInvalidConnection(const vital::modulation_change& change); | |||
virtual SynthGuiInterface* getGuiInterface() = 0; | |||
json saveToJson(); | |||
bool loadFromJson(const json& state); | |||
vital::ModulationConnection* getConnection(const std::string& source, const std::string& destination); | |||
inline bool getNextModulationChange(vital::modulation_change& change) { | |||
return modulation_change_queue_.try_dequeue_non_interleaved(change); | |||
} | |||
inline void clearModulationQueue() { | |||
vital::modulation_change change; | |||
while (modulation_change_queue_.try_dequeue_non_interleaved(change)) | |||
; | |||
} | |||
void processAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset); | |||
void processAudioWithInput(AudioSampleBuffer* buffer, const vital::poly_float* input_buffer, | |||
int channels, int samples, int offset); | |||
void writeAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset); | |||
void processMidi(MidiBuffer& buffer, int start_sample = 0, int end_sample = 0); | |||
void processKeyboardEvents(MidiBuffer& buffer, int num_samples); | |||
void processModulationChanges(); | |||
void updateMemoryOutput(int samples, const vital::poly_float* audio); | |||
std::unique_ptr<vital::SoundEngine> engine_; | |||
std::unique_ptr<MidiManager> midi_manager_; | |||
std::unique_ptr<MidiKeyboardState> keyboard_state_; | |||
std::unique_ptr<WavetableCreator> wavetable_creators_[vital::kNumOscillators]; | |||
std::shared_ptr<SynthBase*> self_reference_; | |||
File active_file_; | |||
vital::poly_float oscilloscope_memory_[2 * vital::kOscilloscopeMemoryResolution]; | |||
vital::poly_float oscilloscope_memory_write_[2 * vital::kOscilloscopeMemoryResolution]; | |||
std::unique_ptr<vital::StereoMemory> audio_memory_; | |||
vital::mono_float last_played_note_; | |||
int last_num_pressed_; | |||
vital::mono_float memory_reset_period_; | |||
vital::mono_float memory_input_offset_; | |||
int memory_index_; | |||
bool expired_; | |||
std::map<std::string, String> save_info_; | |||
vital::control_map controls_; | |||
vital::CircularQueue<vital::ModulationConnection*> mod_connections_; | |||
moodycamel::ConcurrentQueue<vital::control_change> value_change_queue_; | |||
moodycamel::ConcurrentQueue<vital::modulation_change> modulation_change_queue_; | |||
Tuning tuning_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SynthBase) | |||
}; | |||
class HeadlessSynth : public SynthBase { | |||
public: | |||
virtual const CriticalSection& getCriticalSection() override { | |||
return critical_section_; | |||
} | |||
virtual void pauseProcessing(bool pause) override { | |||
if (pause) | |||
critical_section_.enter(); | |||
else | |||
critical_section_.exit(); | |||
} | |||
protected: | |||
virtual SynthGuiInterface* getGuiInterface() override { return nullptr; } | |||
private: | |||
CriticalSection critical_section_; | |||
}; |
@@ -0,0 +1,172 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "value.h" | |||
#include <string> | |||
namespace vital { | |||
constexpr int kNumLfos = 8; | |||
constexpr int kNumOscillators = 3; | |||
constexpr int kNumOscillatorWaveFrames = 257; | |||
constexpr int kNumEnvelopes = 6; | |||
constexpr int kNumRandomLfos = 4; | |||
constexpr int kNumMacros = 4; | |||
constexpr int kNumFilters = 2; | |||
constexpr int kNumFormants = 4; | |||
constexpr int kNumChannels = 2; | |||
constexpr int kMaxPolyphony = 33; | |||
constexpr int kMaxActivePolyphony = 32; | |||
constexpr int kLfoDataResolution = 2048; | |||
constexpr int kMaxModulationConnections = 64; | |||
constexpr int kOscilloscopeMemorySampleRate = 22000; | |||
constexpr int kOscilloscopeMemoryResolution = 512; | |||
constexpr int kAudioMemorySamples = 1 << 15; | |||
constexpr int kDefaultWindowWidth = 1400; | |||
constexpr int kDefaultWindowHeight = 820; | |||
constexpr int kMinWindowWidth = 350; | |||
constexpr int kMinWindowHeight = 205; | |||
constexpr int kDefaultKeyboardOffset = 48; | |||
constexpr wchar_t kDefaultKeyboardOctaveUp = 'x'; | |||
constexpr wchar_t kDefaultKeyboardOctaveDown = 'z'; | |||
const std::wstring kDefaultKeyboard = L"awsedftgyhujkolp;'"; | |||
const std::string kPresetExtension = "vital"; | |||
const std::string kWavetableExtension = "vitaltable"; | |||
const std::string kWavetableExtensionsList = "*." + vital::kWavetableExtension + ";*.wav;*.flac"; | |||
const std::string kSampleExtensionsList = "*.wav;*.flac"; | |||
const std::string kSkinExtension = "vitalskin"; | |||
const std::string kLfoExtension = "vitallfo"; | |||
const std::string kBankExtension = "vitalbank"; | |||
namespace constants { | |||
enum SourceDestination { | |||
kFilter1, | |||
kFilter2, | |||
kDualFilters, | |||
kEffects, | |||
kDirectOut, | |||
kNumSourceDestinations | |||
}; | |||
static SourceDestination toggleFilter1(SourceDestination current_destination, bool on) { | |||
if (on) { | |||
if (current_destination == vital::constants::kFilter2) | |||
return vital::constants::kDualFilters; | |||
else | |||
return vital::constants::kFilter1; | |||
} | |||
else if (current_destination == vital::constants::kDualFilters) | |||
return vital::constants::kFilter2; | |||
else if (current_destination == vital::constants::kFilter1) | |||
return vital::constants::kEffects; | |||
return current_destination; | |||
} | |||
static SourceDestination toggleFilter2(SourceDestination current_destination, bool on) { | |||
if (on) { | |||
if (current_destination == vital::constants::kFilter1) | |||
return vital::constants::kDualFilters; | |||
else | |||
return vital::constants::kFilter2; | |||
} | |||
else if (current_destination == vital::constants::kDualFilters) | |||
return vital::constants::kFilter1; | |||
else if (current_destination == vital::constants::kFilter2) | |||
return vital::constants::kEffects; | |||
return current_destination; | |||
} | |||
enum Effect { | |||
kChorus, | |||
kCompressor, | |||
kDelay, | |||
kDistortion, | |||
kEq, | |||
kFilterFx, | |||
kFlanger, | |||
kPhaser, | |||
kReverb, | |||
kNumEffects | |||
}; | |||
enum FilterModel { | |||
kAnalog, | |||
kDirty, | |||
kLadder, | |||
kDigital, | |||
kDiode, | |||
kFormant, | |||
kComb, | |||
kPhase, | |||
kNumFilterModels | |||
}; | |||
enum RetriggerStyle { | |||
kFree, | |||
kRetrigger, | |||
kSyncToPlayHead, | |||
kNumRetriggerStyles, | |||
}; | |||
constexpr int kNumSyncedFrequencyRatios = 13; | |||
constexpr vital::mono_float kSyncedFrequencyRatios[kNumSyncedFrequencyRatios] = { | |||
0.0f, | |||
1.0f / 128.0f, | |||
1.0f / 64.0f, | |||
1.0f / 32.0f, | |||
1.0f / 16.0f, | |||
1.0f / 8.0f, | |||
1.0f / 4.0f, | |||
1.0f / 2.0f, | |||
1.0f, | |||
2.0f, | |||
4.0f, | |||
8.0f, | |||
16.0f | |||
}; | |||
const poly_float kLeftOne(1.0f, 0.0f); | |||
const poly_float kRightOne(0.0f, 1.0f); | |||
const poly_float kFirstVoiceOne(1.0f, 1.0f, 0.0f, 0.0f); | |||
const poly_float kSecondVoiceOne(0.0f, 0.0f, 1.0f, 1.0f); | |||
const poly_float kStereoSplit = kLeftOne - kRightOne; | |||
const poly_float kPolySqrt2 = kSqrt2; | |||
const poly_mask kFullMask = poly_float::equal(0.0f, 0.0f); | |||
const poly_mask kLeftMask = poly_float::equal(kLeftOne, 1.0f); | |||
const poly_mask kRightMask = poly_float::equal(kRightOne, 1.0f); | |||
const poly_mask kFirstMask = poly_float::equal(kFirstVoiceOne, 1.0f); | |||
const poly_mask kSecondMask = poly_float::equal(kSecondVoiceOne, 1.0f); | |||
const cr::Value kValueZero(0.0f); | |||
const cr::Value kValueOne(1.0f); | |||
const cr::Value kValueTwo(2.0f); | |||
const cr::Value kValueHalf(0.5f); | |||
const cr::Value kValueFifth(0.2f); | |||
const cr::Value kValueTenth(0.1f); | |||
const cr::Value kValuePi(kPi); | |||
const cr::Value kValue2Pi(2.0f * kPi); | |||
const cr::Value kValueSqrt2(kSqrt2); | |||
const cr::Value kValueNegOne(-1.0f); | |||
} // namespace constants | |||
} // namespace vital |
@@ -0,0 +1,220 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "synth_gui_interface.h" | |||
#include "authentication.h" | |||
#include "modulation_connection_processor.h" | |||
#include "sound_engine.h" | |||
#include "load_save.h" | |||
#include "synth_base.h" | |||
SynthGuiData::SynthGuiData(SynthBase* synth_base) : synth(synth_base) { | |||
controls = synth->getControls(); | |||
mono_modulations = synth->getEngine()->getMonoModulations(); | |||
poly_modulations = synth->getEngine()->getPolyModulations(); | |||
modulation_sources = synth->getEngine()->getModulationSources(); | |||
for (int i = 0; i < vital::kNumOscillators; ++i) | |||
wavetable_creators[i] = synth->getWavetableCreator(i); | |||
} | |||
#if HEADLESS | |||
SynthGuiInterface::SynthGuiInterface(SynthBase* synth, bool use_gui) : synth_(synth) { } | |||
SynthGuiInterface::~SynthGuiInterface() { } | |||
void SynthGuiInterface::updateFullGui() { } | |||
void SynthGuiInterface::updateGuiControl(const std::string& name, vital::mono_float value) { } | |||
vital::mono_float SynthGuiInterface::getControlValue(const std::string& name) { return 0.0f; } | |||
void SynthGuiInterface::connectModulation(std::string source, std::string destination) { } | |||
void SynthGuiInterface::connectModulation(vital::ModulationConnection* connection) { } | |||
void SynthGuiInterface::setModulationValues(const std::string& source, const std::string& destination, | |||
vital::mono_float amount, bool bipolar, bool stereo, bool bypass) { } | |||
void SynthGuiInterface::disconnectModulation(std::string source, std::string destination) { } | |||
void SynthGuiInterface::disconnectModulation(vital::ModulationConnection* connection) { } | |||
void SynthGuiInterface::setFocus() { } | |||
void SynthGuiInterface::notifyChange() { } | |||
void SynthGuiInterface::notifyFresh() { } | |||
void SynthGuiInterface::openSaveDialog() { } | |||
void SynthGuiInterface::externalPresetLoaded(File preset) { } | |||
void SynthGuiInterface::setGuiSize(float scale) { } | |||
#else | |||
#include "default_look_and_feel.h" | |||
#include "full_interface.h" | |||
SynthGuiInterface::SynthGuiInterface(SynthBase* synth, bool use_gui) : synth_(synth) { | |||
if (use_gui) { | |||
LineGenerator* lfo_sources[vital::kNumLfos]; | |||
for (int i = 0; i < vital::kNumLfos; ++i) | |||
lfo_sources[i] = synth->getLfoSource(i); | |||
SynthGuiData synth_data(synth_); | |||
gui_ = std::make_unique<FullInterface>(&synth_data); | |||
} | |||
} | |||
SynthGuiInterface::~SynthGuiInterface() { } | |||
void SynthGuiInterface::updateFullGui() { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->setAllValues(synth_->getControls()); | |||
gui_->reset(); | |||
} | |||
void SynthGuiInterface::updateGuiControl(const std::string& name, vital::mono_float value) { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->setValue(name, value, NotificationType::dontSendNotification); | |||
} | |||
vital::mono_float SynthGuiInterface::getControlValue(const std::string& name) { | |||
return synth_->getControls()[name]->value(); | |||
} | |||
void SynthGuiInterface::notifyModulationsChanged() { | |||
gui_->modulationChanged(); | |||
} | |||
void SynthGuiInterface::notifyModulationValueChanged(int index) { | |||
gui_->modulationValueChanged(index); | |||
} | |||
void SynthGuiInterface::connectModulation(std::string source, std::string destination) { | |||
bool created = synth_->connectModulation(source, destination); | |||
if (created) | |||
initModulationValues(source, destination); | |||
notifyModulationsChanged(); | |||
} | |||
void SynthGuiInterface::connectModulation(vital::ModulationConnection* connection) { | |||
synth_->connectModulation(connection); | |||
notifyModulationsChanged(); | |||
} | |||
void SynthGuiInterface::initModulationValues(const std::string& source, const std::string& destination) { | |||
int connection_index = synth_->getConnectionIndex(source, destination); | |||
if (connection_index < 0) | |||
return; | |||
vital::ModulationConnection* connection = synth_->getModulationBank().atIndex(connection_index); | |||
LineGenerator* map_generator = connection->modulation_processor->lineMapGenerator(); | |||
map_generator->initLinear(); | |||
std::string power_name = "modulation_" + std::to_string(connection_index + 1) + "_power"; | |||
synth_->valueChanged(power_name, 0.0f); | |||
gui_->setValue(power_name, 0.0f, NotificationType::dontSendNotification); | |||
} | |||
void SynthGuiInterface::setModulationValues(const std::string& source, const std::string& destination, | |||
vital::mono_float amount, bool bipolar, bool stereo, bool bypass) { | |||
int connection_index = synth_->getConnectionIndex(source, destination); | |||
if (connection_index < 0) | |||
return; | |||
std::string number = std::to_string(connection_index + 1); | |||
std::string amount_name = "modulation_" + number + "_amount"; | |||
std::string bipolar_name = "modulation_" + number + "_bipolar"; | |||
std::string stereo_name = "modulation_" + number + "_stereo"; | |||
std::string bypass_name = "modulation_" + number + "_bypass"; | |||
float bipolar_amount = bipolar ? 1.0f : 0.0f; | |||
float stereo_amount = stereo ? 1.0f : 0.0f; | |||
float bypass_amount = bypass ? 1.0f : 0.0f; | |||
synth_->valueChanged(amount_name, amount); | |||
synth_->valueChanged(bipolar_name, bipolar_amount); | |||
synth_->valueChanged(stereo_name, stereo_amount); | |||
synth_->valueChanged(bypass_name, bypass_amount); | |||
gui_->setValue(amount_name, amount, NotificationType::dontSendNotification); | |||
gui_->setValue(bipolar_name, bipolar_amount, NotificationType::dontSendNotification); | |||
gui_->setValue(stereo_name, stereo_amount, NotificationType::dontSendNotification); | |||
gui_->setValue(bypass_name, bypass_amount, NotificationType::dontSendNotification); | |||
} | |||
void SynthGuiInterface::disconnectModulation(std::string source, std::string destination) { | |||
synth_->disconnectModulation(source, destination); | |||
notifyModulationsChanged(); | |||
} | |||
void SynthGuiInterface::disconnectModulation(vital::ModulationConnection* connection) { | |||
synth_->disconnectModulation(connection); | |||
notifyModulationsChanged(); | |||
} | |||
void SynthGuiInterface::setFocus() { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->setFocus(); | |||
} | |||
void SynthGuiInterface::notifyChange() { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->notifyChange(); | |||
} | |||
void SynthGuiInterface::notifyFresh() { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->notifyFresh(); | |||
} | |||
void SynthGuiInterface::openSaveDialog() { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->openSaveDialog(); | |||
} | |||
void SynthGuiInterface::externalPresetLoaded(File preset) { | |||
if (gui_ == nullptr) | |||
return; | |||
gui_->externalPresetLoaded(preset); | |||
} | |||
void SynthGuiInterface::setGuiSize(float scale) { | |||
if (gui_ == nullptr) | |||
return; | |||
Point<int> position = gui_->getScreenBounds().getCentre(); | |||
const Displays::Display& display = Desktop::getInstance().getDisplays().findDisplayForPoint(position); | |||
Rectangle<int> display_area = Desktop::getInstance().getDisplays().getTotalBounds(true); | |||
ComponentPeer* peer = gui_->getPeer(); | |||
if (peer) | |||
peer->getFrameSize().subtractFrom(display_area); | |||
float window_size = scale / display.scale; | |||
window_size = std::min(window_size, display_area.getWidth() * 1.0f / vital::kDefaultWindowWidth); | |||
window_size = std::min(window_size, display_area.getHeight() * 1.0f / vital::kDefaultWindowHeight); | |||
LoadSave::saveWindowSize(window_size); | |||
int width = std::round(window_size * vital::kDefaultWindowWidth); | |||
int height = std::round(window_size * vital::kDefaultWindowHeight); | |||
Rectangle<int> bounds = gui_->getBounds(); | |||
bounds.setWidth(width); | |||
bounds.setHeight(height); | |||
gui_->getParentComponent()->setBounds(bounds); | |||
gui_->redoBackground(); | |||
} | |||
#endif |
@@ -0,0 +1,79 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "synth_base.h" | |||
#if HEADLESS | |||
class FullInterface { }; | |||
class AudioDeviceManager { }; | |||
#endif | |||
class FullInterface; | |||
class Authentication; | |||
struct SynthGuiData { | |||
SynthGuiData(SynthBase* synth_base); | |||
vital::control_map controls; | |||
vital::output_map mono_modulations; | |||
vital::output_map poly_modulations; | |||
vital::output_map modulation_sources; | |||
WavetableCreator* wavetable_creators[vital::kNumOscillators]; | |||
SynthBase* synth; | |||
}; | |||
class SynthGuiInterface { | |||
public: | |||
SynthGuiInterface(SynthBase* synth, bool use_gui = true); | |||
virtual ~SynthGuiInterface(); | |||
virtual AudioDeviceManager* getAudioDeviceManager() { return nullptr; } | |||
SynthBase* getSynth() { return synth_; } | |||
virtual void updateFullGui(); | |||
virtual void updateGuiControl(const std::string& name, vital::mono_float value); | |||
vital::mono_float getControlValue(const std::string& name); | |||
void notifyModulationsChanged(); | |||
void notifyModulationValueChanged(int index); | |||
void connectModulation(std::string source, std::string destination); | |||
void connectModulation(vital::ModulationConnection* connection); | |||
void setModulationValues(const std::string& source, const std::string& destination, | |||
vital::mono_float amount, bool bipolar, bool stereo, bool bypass); | |||
void initModulationValues(const std::string& source, const std::string& destination); | |||
void disconnectModulation(std::string source, std::string destination); | |||
void disconnectModulation(vital::ModulationConnection* connection); | |||
void setFocus(); | |||
void notifyChange(); | |||
void notifyFresh(); | |||
void openSaveDialog(); | |||
void externalPresetLoaded(File preset); | |||
void setGuiSize(float scale); | |||
FullInterface* getGui() { return gui_.get(); } | |||
protected: | |||
SynthBase* synth_; | |||
std::unique_ptr<FullInterface> gui_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SynthGuiInterface) | |||
}; | |||
@@ -0,0 +1,621 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "synth_parameters.h" | |||
#include "compressor.h" | |||
#include "distortion.h" | |||
#include "digital_svf.h" | |||
#include "synth_constants.h" | |||
#include "random_lfo.h" | |||
#include "synth_lfo.h" | |||
#include "synth_oscillator.h" | |||
#include "synth_strings.h" | |||
#include "voice_handler.h" | |||
#include "wavetable.h" | |||
#include "utils.h" | |||
#include <cfloat> | |||
namespace vital { | |||
bool compareValueDetails(const ValueDetails* a, const ValueDetails* b) { | |||
if (a->version_added != b->version_added) | |||
return a->version_added < b->version_added; | |||
return a->name.compare(b->name) < 0; | |||
} | |||
using namespace constants; | |||
static const std::string kIdDelimiter = "_"; | |||
static const std::string kEnvIdPrefix = "env"; | |||
static const std::string kLfoIdPrefix = "lfo"; | |||
static const std::string kRandomIdPrefix = "random"; | |||
static const std::string kOscIdPrefix = "osc"; | |||
static const std::string kFilterIdPrefix = "filter"; | |||
static const std::string kModulationIdPrefix = "modulation"; | |||
static const std::string kNameDelimiter = " "; | |||
static const std::string kEnvNamePrefix = "Envelope"; | |||
static const std::string kLfoNamePrefix = "LFO"; | |||
static const std::string kRandomNamePrefix = "Random LFO"; | |||
static const std::string kOscNamePrefix = "Oscillator"; | |||
static const std::string kFilterNamePrefix = "Filter"; | |||
static const std::string kModulationNamePrefix = "Modulation"; | |||
const ValueDetails ValueDetailsLookup::parameter_list[] = { | |||
{ "bypass", 0x000702, 0.0, 1.0, 0.0, 0.0, 60.0, | |||
ValueDetails::kIndexed, false, "", "Bypass", nullptr }, | |||
{ "beats_per_minute", 0x000000, 0.333333333, 5.0, 2.0, 0.0, 60.0, | |||
ValueDetails::kLinear, false, "", "Beats Per Minute", nullptr }, | |||
{ "delay_dry_wet", 0x000000, 0.0, 1.0, 0.3334, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Delay Mix", nullptr }, | |||
{ "delay_feedback", 0x000000, -1.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Delay Feedback", nullptr }, | |||
{ "delay_frequency", 0x000000, -2.0, 9.0, 2.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Delay Frequency", nullptr }, | |||
{ "delay_aux_frequency", 0x000507, -2.0, 9.0, 2.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Delay Frequency 2", nullptr }, | |||
{ "delay_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Switch", strings::kOffOnNames }, | |||
{ "delay_style", 0x000000, 0.0, 3.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Style", strings::kDelayStyleNames }, | |||
{ "delay_filter_cutoff", 0x000000, 8.0, 136.0, 60.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Delay Filter Cutoff", nullptr }, | |||
{ "delay_filter_spread", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Delay Filter Spread", nullptr }, | |||
{ "delay_sync", 0x000000, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Sync", strings::kFrequencySyncNames }, | |||
{ "delay_tempo", 0x000000, 4.0, 12.0, 9.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Tempo", strings::kSyncedFrequencyNames }, | |||
{ "delay_aux_sync", 0x000507, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Sync 2", strings::kFrequencySyncNames }, | |||
{ "delay_aux_tempo", 0x000507, 4.0, 12.0, 9.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Delay Tempo 2", strings::kSyncedFrequencyNames }, | |||
{ "distortion_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Distortion Switch", strings::kOffOnNames }, | |||
{ "distortion_type", 0x000000, 0.0, 5.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Distortion Type", strings::kDistortionTypeNames }, | |||
{ "distortion_drive", 0x000000, Distortion::kMinDrive, Distortion::kMaxDrive, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Distortion Drive", nullptr }, | |||
{ "distortion_mix", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Distortion Mix", nullptr }, | |||
{ "distortion_filter_order", 0x000000, 0.0, 2.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Distortion Filter Order", strings::kDistortionFilterOrderNames }, | |||
{ "distortion_filter_cutoff", 0x000000, 8.0, 136.0, 80.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Distortion Filter Cutoff", nullptr }, | |||
{ "distortion_filter_resonance", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Distortion Filter Resonance", nullptr }, | |||
{ "distortion_filter_blend", 0x000000, 0.0, 2.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Distortion Filter Blend", nullptr }, | |||
{ "legato", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Legato", strings::kOffOnNames }, | |||
{ "macro_control_1", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Macro 1", nullptr }, | |||
{ "macro_control_2", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Macro 2", nullptr }, | |||
{ "macro_control_3", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Macro 3", nullptr }, | |||
{ "macro_control_4", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Macro 4", nullptr }, | |||
{ "pitch_bend_range", 0x000000, 0.0, 48.0, 2.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, " semitones", "Pitch Bend Range", nullptr }, | |||
{ "polyphony", 0x000000, 1.0, kMaxPolyphony - 1, 8.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, " voices", "Polyphony", nullptr }, | |||
{ "voice_tune", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, " cents", "Voice Tune", nullptr }, | |||
{ "voice_transpose", 0x000604, -48.0, 48.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Voice Transpose", nullptr }, | |||
{ "voice_amplitude", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Voice Amplitude", nullptr }, | |||
{ "stereo_routing", 0x000000, 0.0, 1.0, 1.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Stereo Routing", nullptr }, | |||
{ "stereo_mode", 0x000605, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Stereo Mode", strings::kStereoModeNames }, | |||
{ "portamento_time", 0x000000, -10.0, 4.0, -10.0, 0.0, 1.0, | |||
ValueDetails::kExponential, false, " secs", "Portamento Time", nullptr }, | |||
{ "portamento_slope", 0x000000, -8.0, 8.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Portamento Slope", nullptr }, | |||
{ "portamento_force", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Portamento Force", strings::kOffOnNames }, | |||
{ "portamento_scale", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Portamento Scale", strings::kOffOnNames }, | |||
{ "reverb_pre_low_cutoff", 0x000000, 0.0, 128.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Reverb Pre Low Cutoff", nullptr }, | |||
{ "reverb_pre_high_cutoff", 0x000000, 0.0, 128.0, 110.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Reverb Pre High Cutoff", nullptr }, | |||
{ "reverb_low_shelf_cutoff", 0x000000, 0.0, 128.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Reverb Low Cutoff", nullptr }, | |||
{ "reverb_low_shelf_gain", 0x000000, -6.0, 0.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Reverb Low Gain", nullptr }, | |||
{ "reverb_high_shelf_cutoff", 0x000000, 0.0, 128.0, 90.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Reverb High Cutoff", nullptr }, | |||
{ "reverb_high_shelf_gain", 0x000000, -6.0, 0.0, -1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Reverb High Gain", nullptr }, | |||
{ "reverb_dry_wet", 0x000000, 0.0, 1.0, 0.25, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Reverb Mix", nullptr }, | |||
{ "reverb_delay", 0x000609, 0.0, 0.3, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " secs", "Reverb Delay", nullptr }, | |||
{ "reverb_decay_time", 0x000000, -6.0, 6.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kExponential, false, " secs", "Reverb Decay Time", nullptr }, | |||
{ "reverb_size", 0x000506, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Reverb Size", nullptr }, | |||
{ "reverb_chorus_amount", 0x000000, 0.0, 1.0, 0.223607, 0.0, 100.0, | |||
ValueDetails::kQuadratic, false, "%", "Reverb Chorus Amount", nullptr }, | |||
{ "reverb_chorus_frequency", 0x000000, -8.0, 3.0, -2.0, 0.0, 1.0, | |||
ValueDetails::kExponential, false, " Hz", "Reverb Chorus Frequency", nullptr }, | |||
{ "reverb_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Reverb Switch", strings::kOffOnNames }, | |||
{ "sub_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sub Switch", strings::kOffOnNames }, | |||
{ "sub_direct_out", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sub Direct Out", nullptr }, | |||
{ "sub_transpose", 0x000000, -48.0, 48.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sub Transpose", nullptr }, | |||
{ "sub_transpose_quantize", 0x000000, 0.0, 8191.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sub Transpose Quantize", nullptr }, | |||
{ "sub_tune", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "", "Sub Tune", nullptr }, | |||
{ "sub_level", 0x000000, 0.0, 1.0, 0.70710678119, 0.0, 1.0, | |||
ValueDetails::kQuadratic, false, "", "Sub Level", nullptr }, | |||
{ "sub_pan", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Sub Pan", nullptr }, | |||
{ "sub_waveform", 0x000000, 0.0, PredefinedWaveFrames::kNumShapes - 1, 2.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sub Osc Waveform", nullptr }, | |||
{ "sample_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Switch", strings::kOffOnNames }, | |||
{ "sample_random_phase", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Random Phase", strings::kOffOnNames }, | |||
{ "sample_keytrack", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Keytrack", strings::kOffOnNames }, | |||
{ "sample_loop", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Loop", strings::kOffOnNames }, | |||
{ "sample_bounce", 0x000603, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Bounce", strings::kOffOnNames }, | |||
{ "sample_transpose", 0x000000, -48.0, 48.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Transpose", nullptr }, | |||
{ "sample_transpose_quantize", 0x000000, 0.0, 8191.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Transpose Quantize", nullptr }, | |||
{ "sample_tune", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "", "Sample Tune", nullptr }, | |||
{ "sample_level", 0x000000, 0.0, 1.0, 0.70710678119, 0.0, 1.0, | |||
ValueDetails::kQuadratic, false, "", "Sample Level", nullptr }, | |||
{ "sample_destination", 0x000500, 0.0, constants::kNumSourceDestinations + constants::kNumEffects, 3.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sample Destination", strings::kDestinationNames }, | |||
{ "sample_pan", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Sample Pan", nullptr }, | |||
{ "velocity_track", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Velocity Track", nullptr }, | |||
{ "volume", 0x000000, 0.0, 7399.4404, 5473.0404, -80, 1.0, | |||
ValueDetails::kSquareRoot, false, "dB", "Volume", nullptr }, | |||
{ "phaser_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Phaser Switch", strings::kOffOnNames }, | |||
{ "phaser_dry_wet", 0x000000, 0.0, 1.0, 1.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Phaser Mix", nullptr }, | |||
{ "phaser_feedback", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Phaser Feedback", nullptr }, | |||
{ "phaser_frequency", 0x000000, -5.0, 2.0, -3.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Phaser Frequency", nullptr }, | |||
{ "phaser_sync", 0x000000, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Phaser Sync",strings::kFrequencySyncNames }, | |||
{ "phaser_tempo", 0x000000, 0.0, 10.0, 3.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Phaser Tempo", strings::kSyncedFrequencyNames }, | |||
{ "phaser_center", 0x000000, 8.0, 136.0, 80.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Phaser Center", nullptr }, | |||
{ "phaser_blend", 0x000509, 0.0, 2.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Phaser Blend", nullptr }, | |||
{ "phaser_mod_depth", 0x000000, 0.0, 48.0, 24.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Phaser Mod Depth", nullptr }, | |||
{ "phaser_phase_offset", 0x000000, 0, 1.0, 0.33333333, 0.0, kDegreesPerCycle, | |||
ValueDetails::kLinear, false, "", "Phaser Phase Offset", nullptr }, | |||
{ "flanger_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Flanger Switch", strings::kOffOnNames }, | |||
{ "flanger_dry_wet", 0x000000, 0.0, 0.5, 0.5, 0.0, 200.0, | |||
ValueDetails::kLinear, false, "%", "Flanger Mix", nullptr }, | |||
{ "flanger_feedback", 0x000000, -1.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Flanger Feedback", nullptr }, | |||
{ "flanger_frequency", 0x000000, -5.0, 2.0, 2.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Flanger Frequency", nullptr }, | |||
{ "flanger_sync", 0x000000, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Flanger Sync", strings::kFrequencySyncNames }, | |||
{ "flanger_tempo", 0x000000, 0.0, 10.0, 4.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Flanger Tempo", strings::kSyncedFrequencyNames }, | |||
{ "flanger_center", 0x000505, 8.0, 136.0, 64.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Flanger Center", nullptr }, | |||
{ "flanger_mod_depth", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Flanger Mod Depth", nullptr }, | |||
{ "flanger_phase_offset", 0x000000, 0, 1.0, 0.33333333, 0.0, kDegreesPerCycle, | |||
ValueDetails::kLinear, false, "", "Flanger Phase Offset", nullptr }, | |||
{ "chorus_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Chorus Switch", strings::kOffOnNames }, | |||
{ "chorus_dry_wet", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Chorus Mix", nullptr }, | |||
{ "chorus_feedback", 0x000000, -0.95, 0.95, 0.4, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Chorus Feedback", nullptr }, | |||
{ "chorus_cutoff", 0x000000, 8.0, 136.0, 60.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Chorus Filter Cutoff", nullptr }, | |||
{ "chorus_spread", 0x000607, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Chorus Filter Spread", nullptr }, | |||
{ "chorus_voices", 0x000508, 1.0, 4.0, 4.0, 0.0, 4.0, | |||
ValueDetails::kIndexed, false, "", "Chorus Voices", nullptr }, | |||
{ "chorus_frequency", 0x000000, -6.0, 3.0, -3.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Chorus Frequency", nullptr }, | |||
{ "chorus_sync", 0x000000, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Chorus Sync", strings::kFrequencySyncNames }, | |||
{ "chorus_tempo", 0x000000, 0.0, 10.0, 4.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Chorus Tempo", strings::kSyncedFrequencyNames }, | |||
{ "chorus_mod_depth", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Chorus Mod Depth", nullptr }, | |||
{ "chorus_delay_1", 0x000000, -10.0, -5.64386, -9.0, 0.0, 1000.0, | |||
ValueDetails::kExponential, false, "ms", "Chorus Delay 1", nullptr }, | |||
{ "chorus_delay_2", 0x000000, -10.0, -5.64386, -7.0, 0.0, 1000.0, | |||
ValueDetails::kExponential, false, " ms", "Chorus Delay 2", nullptr }, | |||
{ "compressor_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Compressor Switch", strings::kOffOnNames }, | |||
{ "compressor_low_upper_threshold", 0x000000, -80.0, 0.0, -28.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Low Upper Threshold", nullptr }, | |||
{ "compressor_band_upper_threshold", 0x000000, -80.0, 0.0, -25.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Band Upper Threshold", nullptr }, | |||
{ "compressor_high_upper_threshold", 0x000000, -80.0, 0.0, -30.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "High Upper Threshold", nullptr }, | |||
{ "compressor_low_lower_threshold", 0x000000, -80.0, 0.0, -35.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Low Lower Threshold", nullptr }, | |||
{ "compressor_band_lower_threshold", 0x000000, -80.0, 0.0, -36.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Band Lower Threshold", nullptr }, | |||
{ "compressor_high_lower_threshold", 0x000000, -80.0, 0.0, -35.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "High Lower Threshold", nullptr }, | |||
{ "compressor_low_upper_ratio", 0x000000, 0.0, 1.0, 0.9, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Low Upper Ratio", nullptr }, | |||
{ "compressor_band_upper_ratio", 0x000000, 0.0, 1.0, 0.857, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Band Upper Ratio", nullptr }, | |||
{ "compressor_high_upper_ratio", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "High Upper Ratio", nullptr }, | |||
{ "compressor_low_lower_ratio", 0x000000, -1.0, 1.0, 0.8, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Low Lower Ratio", nullptr }, | |||
{ "compressor_band_lower_ratio", 0x000000, -1.0, 1.0, 0.8, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Band Lower Ratio", nullptr }, | |||
{ "compressor_high_lower_ratio", 0x000000, -1.0, 1.0, 0.8, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "High Lower Ratio", nullptr }, | |||
{ "compressor_low_gain", 0x000000, -30.0, 30.0, 16.3, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Compressor Low Gain", nullptr }, | |||
{ "compressor_band_gain", 0x000000, -30.0, 30.0, 11.7, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Compressor Band Gain", nullptr }, | |||
{ "compressor_high_gain", 0x000000, -30.0, 30.0, 16.3, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "Compressor High Gain", nullptr }, | |||
{ "compressor_attack", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Compressor Attack", nullptr }, | |||
{ "compressor_release", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Compressor Release", nullptr }, | |||
{ "compressor_enabled_bands", 0x000000, 0.0, vital::MultibandCompressor::kNumBandOptions - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Compressor Enabled Bands", strings::kCompressorBandNames }, | |||
{ "compressor_mix", 0x000602, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Compressor Mix", nullptr }, | |||
{ "compressor_low_band_unused", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Compressor Unused", nullptr }, | |||
{ "eq_on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "EQ Switch", strings::kOffOnNames }, | |||
{ "eq_low_mode", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "EQ Low Mode", strings::kEqLowModeNames }, | |||
{ "eq_low_cutoff", 0x000000, 8.0, 136.0, 40.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "EQ Low Cutoff", nullptr }, | |||
{ "eq_low_gain", 0x000000, DigitalSvf::kMinGain, DigitalSvf::kMaxGain, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "EQ Low Gain", nullptr }, | |||
{ "eq_low_resonance", 0x000000, 0.0, 1.0, 0.3163, 0.0, 100.0, | |||
ValueDetails::kQuadratic, false, "%", "EQ Low Resonance", nullptr }, | |||
{ "eq_band_mode", 0x000506, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "EQ Band Mode", strings::kEqBandModeNames }, | |||
{ "eq_band_cutoff", 0x000000, 8.0, 136.0, 80.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "EQ Band Cutoff", nullptr }, | |||
{ "eq_band_gain", 0x000000, DigitalSvf::kMinGain, DigitalSvf::kMaxGain, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "EQ Band Gain", nullptr }, | |||
{ "eq_band_resonance", 0x000000, 0.0, 1.0, 0.4473, 0.0, 100.0, | |||
ValueDetails::kQuadratic, false, "", "EQ Band Resonance", nullptr }, | |||
{ "eq_high_mode", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "EQ High Mode", strings::kEqHighModeNames }, | |||
{ "eq_high_cutoff", 0x000000, 8.0, 136.0, 100.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "EQ High Cutoff", nullptr }, | |||
{ "eq_high_gain", 0x000000, DigitalSvf::kMinGain, DigitalSvf::kMaxGain, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " dB", "EQ High Gain", nullptr }, | |||
{ "eq_high_resonance", 0x000000, 0.0, 1.0, 0.3163, 0.0, 100.0, | |||
ValueDetails::kQuadratic, false, "", "EQ High Resonance", nullptr }, | |||
{ "effect_chain_order", 0x000000, 0.0, vital::utils::factorial(vital::kNumEffects) - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Effect Chain Order", nullptr }, | |||
{ "voice_priority", 0x000000, 0.0, VoiceHandler::kNumVoicePriorities - 1, VoiceHandler::kRoundRobin, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Voice Priority", strings::kVoicePriorityNames }, | |||
{ "voice_override", 0x000700, 0.0, VoiceHandler::kNumVoiceOverrides - 1, VoiceHandler::kKill, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Voice Override", strings::kVoiceOverrideNames }, | |||
{ "oversampling", 0x000000, 0.0, 3.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Oversampling", strings::kOversamplingNames }, | |||
{ "pitch_wheel", 0x000400, -1.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Pitch Wheel", nullptr }, | |||
{ "mod_wheel", 0x000400, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Mod Wheel", nullptr }, | |||
{ "mpe_enabled", 0x000501, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "MPE Enabled", strings::kOffOnNames }, | |||
{ "view_spectrogram", 0x000803, 0.0, 2.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "View Spectrogram", strings::kOffOnNames }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::env_parameter_list[] = { | |||
{ "delay", 0x000503, 0.0, 1.4142135624, 0.0, 0.0, 1.0, | |||
ValueDetails::kQuartic, false, " secs", "Delay", nullptr }, | |||
{ "attack", 0x000000, 0.0, 2.37842, 0.1495, 0.0, 1.0, | |||
ValueDetails::kQuartic, false, " secs", "Attack", nullptr }, | |||
{ "hold", 0x000504, 0.0, 1.4142135624, 0.0, 0.0, 1.0, | |||
ValueDetails::kQuartic, false, " secs", "Hold", nullptr }, | |||
{ "decay", 0x000000, 0.0, 2.37842, 1.0, 0.0, 1.0, | |||
ValueDetails::kQuartic, false, " secs", "Decay", nullptr }, | |||
{ "release", 0x000000, 0.0, 2.37842, 0.5476, 0.0, 1.0, | |||
ValueDetails::kQuartic, false, " secs", "Release", nullptr }, | |||
{ "attack_power", 0x000000, -20.0, 20.0, 0.0f, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Attack Power", nullptr }, | |||
{ "decay_power", 0x000000, -20.0, 20.0, -2.0f, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Decay Power", nullptr }, | |||
{ "release_power", 0x000000, -20.0, 20.0, -2.0f, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Release Power", nullptr }, | |||
{ "sustain", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Sustain", nullptr }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::lfo_parameter_list[] = { | |||
{ "phase", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Phase", nullptr }, | |||
{ "sync_type", 0x000000, 0.0, SynthLfo::kNumSyncTypes - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sync Type", strings::kSyncNames }, | |||
{ "frequency", 0x000000, -7.0, 9.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Frequency", nullptr }, | |||
{ "sync", 0x000000, 0.0, SynthLfo::kNumSyncOptions - 1, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sync", strings::kFrequencySyncNames }, | |||
{ "tempo", 0x000000, 0.0, 12.0, 7.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Tempo", strings::kSyncedFrequencyNames }, | |||
{ "fade_time", 0x000000, 0.0, 8.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " secs", "Fade In", nullptr }, | |||
{ "smooth_mode", 0x000801, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Smooth Mode", strings::kOffOnNames }, | |||
{ "smooth_time", 0x000801, -10.0, 4.0, -7.5, 0.0, 1.0, | |||
ValueDetails::kExponential, false, " secs", "Smooth Time", nullptr }, | |||
{ "delay_time", 0x000000, 0.0, 4.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " secs", "Delay", nullptr }, | |||
{ "stereo", 0x000406, -0.5, 0.5, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Stereo", nullptr }, | |||
{ "keytrack_transpose", 0x000704, -60.0, 36.0, -12.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Transpose", nullptr }, | |||
{ "keytrack_tune", 0x000704, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "", "Tune", nullptr }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::random_lfo_parameter_list[] = { | |||
{ "style", 0x000401, 0.0, RandomLfo::kNumStyles - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Style", strings::kRandomNames }, | |||
{ "frequency", 0x000401, -7.0, 9.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kExponential, true, " secs", "Frequency", nullptr }, | |||
{ "sync", 0x000401, 0.0, SynthLfo::kNumSyncOptions - 1, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sync", strings::kFrequencySyncNames }, | |||
{ "tempo", 0x000401, 0.0, 12.0, 8.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Tempo", strings::kSyncedFrequencyNames }, | |||
{ "stereo", 0x000401, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Stereo", strings::kOffOnNames }, | |||
{ "sync_type", 0x000600, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Sync Type", strings::kOffOnNames }, | |||
{ "keytrack_transpose", 0x000704, -60.0, 36.0, -12.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Transpose", nullptr }, | |||
{ "keytrack_tune", 0x000704, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "", "Tune", nullptr }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::filter_parameter_list[] = { | |||
{ "mix", 0x000000, 0.0f, 1.0f, 1.0f, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Mix", nullptr }, | |||
{ "cutoff", 0x000000, 8.0, 136.0, 60.0, -60.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Cutoff", nullptr }, | |||
{ "resonance", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Resonance", nullptr }, | |||
{ "drive", 0x000000, 0.0, 20.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "dB", "Drive", nullptr }, | |||
{ "blend", 0x000000, 0.0, 2.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Blend", nullptr }, | |||
{ "style", 0x000000, 0.0, 9.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Style", strings::kFilterStyleNames }, | |||
{ "model", 0x000000, 0.0, kNumFilterModels - 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Model", strings::kFilterModelNames }, | |||
{ "on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Switch", strings::kOffOnNames }, | |||
{ "blend_transpose", 0x000000, 0.0, 84.0, 42.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, " semitones", "Comb Blend Offset", nullptr }, | |||
{ "keytrack", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Key Track", nullptr }, | |||
{ "formant_x", 0x000000, 0.0, 1.0, 0.5, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Formant X", nullptr }, | |||
{ "formant_y", 0x000000, 0.0, 1.0, 0.5, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Formant Y", nullptr }, | |||
{ "formant_transpose", 0x000000, -12.0, 12.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Formant Transpose", nullptr }, | |||
{ "formant_resonance", 0x000000, 0.3, 1.0, 0.85, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Formant Resonance", nullptr }, | |||
{ "formant_spread", 0x000707, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Formant Spread", nullptr }, | |||
{ "osc1_input", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "OSC 1 Input", strings::kOffOnNames }, | |||
{ "osc2_input", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "OSC 2 Input", strings::kOffOnNames }, | |||
{ "osc3_input", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "OSC 3 Input", strings::kOffOnNames }, | |||
{ "sample_input", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "SAMPLE Input", strings::kOffOnNames }, | |||
{ "filter_input", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "FILTER Input", strings::kOffOnNames }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::osc_parameter_list[] = { | |||
{ "on", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Switch", strings::kOffOnNames }, | |||
{ "transpose", 0x000000, -48.0, 48.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Transpose", nullptr }, | |||
{ "transpose_quantize", 0x000000, 0.0, 8191.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Transpose Quantize", nullptr }, | |||
{ "tune", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "", "Tune", nullptr }, | |||
{ "pan", 0x000000, -1.0, 1.0, 0.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Pan", nullptr }, | |||
{ "stack_style", 0x000000, 0.0, SynthOscillator::kNumUnisonStackTypes - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Stack Style", strings::kUnisonStackNames }, | |||
{ "unison_detune", 0x000000, 0.0, 10.0, 4.472135955, 0.0, 1.0, | |||
ValueDetails::kQuadratic, false, "%", "Unison Detune", nullptr }, | |||
{ "unison_voices", 0x000000, 1.0, 16.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "v", "Unison Voices", nullptr }, | |||
{ "unison_blend", 0x000000, 0.0, 1.0, 0.8, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Blend", nullptr }, | |||
{ "detune_power", 0x000000, -5.0, 5.0, 1.5, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Detune Power", nullptr }, | |||
{ "detune_range", 0x000000, 0.0, 48.0, 2.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Detune Range", nullptr }, | |||
{ "level", 0x000000, 0.0, 1.0, 0.70710678119, 0.0, 1.0, | |||
ValueDetails::kQuadratic, false, "", "Level", nullptr }, | |||
{ "midi_track", 0x000000, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Midi Track", strings::kOffOnNames }, | |||
{ "smooth_interpolation", 0x000000, 0.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Smooth Interpolation", strings::kOffOnNames }, | |||
{ "spectral_unison", 0x000500, 0.0, 1.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Spectral Unison", strings::kOffOnNames }, | |||
{ "wave_frame", 0x000000, 0.0, kNumOscillatorWaveFrames - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Wave Frame", nullptr }, | |||
{ "frame_spread", 0x000000, -kNumOscillatorWaveFrames / 2, kNumOscillatorWaveFrames / 2, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Unison Frame Spread", nullptr }, | |||
{ "stereo_spread", 0x000000, 0.0, 1.0, 1.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Stereo Spread", nullptr }, | |||
{ "phase", 0x000000, 0.0, 1.0, 0.5, 0.0, 360.0, | |||
ValueDetails::kLinear, false, "", "Phase", nullptr }, | |||
{ "distortion_phase", 0x000000, 0.0, 1.0, 0.5, 0.0, 360.0, | |||
ValueDetails::kLinear, false, "", "Distortion Phase", nullptr }, | |||
{ "random_phase", 0x000000, 0.0, 1.0, 1.0, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Phase Randomization", nullptr }, | |||
{ "distortion_type", 0x000000, 0.0, SynthOscillator::kNumDistortionTypes - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Distortion Type", strings::kPhaseDistortionNames }, | |||
{ "distortion_amount", 0x000000, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Distortion Amount", nullptr }, | |||
{ "distortion_spread", 0x000000, -0.5, 0.5, 0.0, 0.0, 200.0, | |||
ValueDetails::kLinear, false, "%", "Distortion Spread", nullptr }, | |||
{ "spectral_morph_type", 0x000407, 0.0, SynthOscillator::kNumSpectralMorphTypes - 1, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Frequency Morph Type", strings::kSpectralMorphNames }, | |||
{ "spectral_morph_amount", 0x000407, 0.0, 1.0, 0.5, 0.0, 100.0, | |||
ValueDetails::kLinear, false, "%", "Frequency Morph Amount", nullptr }, | |||
{ "spectral_morph_spread", 0x000407, -0.5, 0.5, 0.0, 0.0, 200.0, | |||
ValueDetails::kLinear, false, "%", "Frequency Morph Spread", nullptr }, | |||
{ "destination", 0x000500, 0.0, constants::kNumSourceDestinations + constants::kNumEffects, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Destination", strings::kDestinationNames }, | |||
{ "view_2d", 0x000402, 0.0, 2.0, 1.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "View 2D", strings::kOffOnNames }, | |||
}; | |||
const ValueDetails ValueDetailsLookup::mod_parameter_list[] = { | |||
{ "amount", 0x000000, -1.0, 1.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Amount", nullptr }, | |||
{ "power", 0x000000, -10.0, 10.0, 0.0, 0.0, 1.0, | |||
ValueDetails::kLinear, false, "", "Power", nullptr }, | |||
{ "bipolar", 0x000000, 0.0, 1.0f, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Bipolar", strings::kOffOnNames }, | |||
{ "stereo", 0x000000, 0.0, 1.0f, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Stereo", strings::kOffOnNames }, | |||
{ "bypass", 0x000000, 0.0, 1.0f, 0.0, 0.0, 1.0, | |||
ValueDetails::kIndexed, false, "", "Bypass", strings::kOffOnNames } | |||
}; | |||
ValueDetailsLookup::ValueDetailsLookup() { | |||
static constexpr int kNumOscillatorsOld = 2; | |||
static constexpr int kNewOscillatorVersion = 0x000500; | |||
static constexpr int kOldMaxModulations = 32; | |||
static constexpr int kNewModulationVersion = 0x000601; | |||
int num_parameters = sizeof(parameter_list) / sizeof(ValueDetails); | |||
for (int i = 0; i < num_parameters; ++i) { | |||
details_lookup_[parameter_list[i].name] = parameter_list[i]; | |||
details_list_.push_back(¶meter_list[i]); | |||
VITAL_ASSERT(parameter_list[i].default_value <= parameter_list[i].max); | |||
VITAL_ASSERT(parameter_list[i].default_value >= parameter_list[i].min); | |||
} | |||
int num_env_parameters = sizeof(env_parameter_list) / sizeof(ValueDetails); | |||
for (int env = 0; env < kNumEnvelopes; ++env) | |||
addParameterGroup(env_parameter_list, num_env_parameters, env, kEnvIdPrefix, kEnvNamePrefix); | |||
int num_lfo_parameters = sizeof(lfo_parameter_list) / sizeof(ValueDetails); | |||
for (int lfo = 0; lfo < kNumLfos; ++lfo) | |||
addParameterGroup(lfo_parameter_list, num_lfo_parameters, lfo, kLfoIdPrefix, kLfoNamePrefix); | |||
int num_random_lfo_parameters = sizeof(random_lfo_parameter_list) / sizeof(ValueDetails); | |||
for (int lfo = 0; lfo < kNumRandomLfos; ++lfo) | |||
addParameterGroup(random_lfo_parameter_list, num_random_lfo_parameters, lfo, kRandomIdPrefix, kRandomNamePrefix); | |||
int num_osc_parameters = sizeof(osc_parameter_list) / sizeof(ValueDetails); | |||
for (int osc = 0; osc < kNumOscillatorsOld; ++osc) | |||
addParameterGroup(osc_parameter_list, num_osc_parameters, osc, kOscIdPrefix, kOscNamePrefix); | |||
for (int osc = kNumOscillatorsOld; osc < kNumOscillators; ++osc) { | |||
addParameterGroup(osc_parameter_list, num_osc_parameters, osc, kOscIdPrefix, | |||
kOscNamePrefix, kNewOscillatorVersion); | |||
} | |||
int num_filter_parameters = sizeof(filter_parameter_list) / sizeof(ValueDetails); | |||
for (int filter = 0; filter < kNumFilters; ++filter) | |||
addParameterGroup(filter_parameter_list, num_filter_parameters, filter, kFilterIdPrefix, kFilterNamePrefix); | |||
addParameterGroup(filter_parameter_list, num_filter_parameters, "fx", kFilterIdPrefix, kFilterNamePrefix); | |||
int num_mod_parameters = sizeof(mod_parameter_list) / sizeof(ValueDetails); | |||
for (int modulation = 0; modulation < kOldMaxModulations; ++modulation) { | |||
addParameterGroup(mod_parameter_list, num_mod_parameters, modulation, | |||
kModulationIdPrefix, kModulationNamePrefix); | |||
} | |||
for (int modulation = kOldMaxModulations; modulation < kMaxModulationConnections; ++modulation) { | |||
addParameterGroup(mod_parameter_list, num_mod_parameters, modulation, | |||
kModulationIdPrefix, kModulationNamePrefix, kNewModulationVersion); | |||
} | |||
details_lookup_["osc_1_on"].default_value = 1.0f; | |||
details_lookup_["osc_2_destination"].default_value = 1.0f; | |||
details_lookup_["osc_3_destination"].default_value = 3.0f; | |||
details_lookup_["filter_1_osc1_input"].default_value = 1.0f; | |||
details_lookup_["filter_2_osc2_input"].default_value = 1.0f; | |||
std::sort(details_list_.begin(), details_list_.end(), compareValueDetails); | |||
} | |||
void ValueDetailsLookup::addParameterGroup(const ValueDetails* list, int num_parameters, int index, | |||
std::string id_prefix, std::string name_prefix, int version) { | |||
std::string string_num = std::to_string(index + 1); | |||
addParameterGroup(list, num_parameters, string_num, id_prefix, name_prefix, version); | |||
} | |||
void ValueDetailsLookup::addParameterGroup(const ValueDetails* list, int num_parameters, std::string id, | |||
std::string id_prefix, std::string name_prefix, int version) { | |||
std::string id_start = id_prefix + kIdDelimiter + id + kIdDelimiter; | |||
std::string name_start = name_prefix + kNameDelimiter + id + kNameDelimiter; | |||
for (int i = 0; i < num_parameters; ++i) { | |||
ValueDetails details = list[i]; | |||
if (version > details.version_added) | |||
details.version_added = version; | |||
details.name = id_start + details.name; | |||
details.local_description = details.display_name; | |||
details.display_name = name_start + details.display_name; | |||
details_lookup_[details.name] = details; | |||
details_list_.push_back(&details_lookup_[details.name]); | |||
} | |||
} | |||
ValueDetailsLookup Parameters::lookup_; | |||
} // namespace vital |
@@ -0,0 +1,147 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "common.h" | |||
#include <map> | |||
#include <string> | |||
namespace vital { | |||
struct ValueDetails { | |||
enum ValueScale { | |||
kIndexed, | |||
kLinear, | |||
kQuadratic, | |||
kCubic, | |||
kQuartic, | |||
kSquareRoot, | |||
kExponential | |||
}; | |||
std::string name; | |||
int version_added = 0; | |||
mono_float min = 0.0f; | |||
mono_float max = 1.0f; | |||
mono_float default_value = 0.0f; | |||
// post_offset used to offset quadratic and exponential scaling. | |||
mono_float post_offset = 0.0f; | |||
mono_float display_multiply = 1.0f; | |||
ValueScale value_scale = kLinear; | |||
bool display_invert = false; | |||
std::string display_units; | |||
std::string display_name; | |||
const std::string* string_lookup = nullptr; | |||
std::string local_description; | |||
} typedef ValueDetails; | |||
class ValueDetailsLookup { | |||
public: | |||
ValueDetailsLookup(); | |||
const bool isParameter(const std::string& name) const { | |||
return details_lookup_.count(name); | |||
} | |||
const ValueDetails& getDetails(const std::string& name) const { | |||
auto details = details_lookup_.find(name); | |||
VITAL_ASSERT(details != details_lookup_.end()); | |||
return details->second; | |||
} | |||
const ValueDetails* getDetails(int index) const { | |||
return details_list_[index]; | |||
} | |||
std::string getDisplayName(const std::string& name) const { | |||
return getDetails(name).display_name; | |||
} | |||
int getNumParameters() const { | |||
return static_cast<int>(details_list_.size()); | |||
} | |||
mono_float getParameterRange(const std::string& name) const { | |||
auto details = details_lookup_.find(name); | |||
VITAL_ASSERT(details != details_lookup_.end()); | |||
return details->second.max - details->second.min; | |||
} | |||
std::map<std::string, ValueDetails> getAllDetails() const { | |||
return details_lookup_; | |||
} | |||
void addParameterGroup(const ValueDetails* list, int num_parameters, int index, | |||
std::string id_prefix, std::string name_prefix, int version = -1); | |||
void addParameterGroup(const ValueDetails* list, int num_parameters, std::string id, | |||
std::string id_prefix, std::string name_prefix, int version = -1); | |||
static const ValueDetails parameter_list[]; | |||
static const ValueDetails env_parameter_list[]; | |||
static const ValueDetails lfo_parameter_list[]; | |||
static const ValueDetails random_lfo_parameter_list[]; | |||
static const ValueDetails filter_parameter_list[]; | |||
static const ValueDetails osc_parameter_list[]; | |||
static const ValueDetails mod_parameter_list[]; | |||
private: | |||
std::map<std::string, ValueDetails> details_lookup_; | |||
std::vector<const ValueDetails*> details_list_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ValueDetailsLookup) | |||
}; | |||
class Parameters { | |||
public: | |||
static const ValueDetails& getDetails(const std::string& name) { | |||
return lookup_.getDetails(name); | |||
} | |||
static int getNumParameters() { | |||
return lookup_.getNumParameters(); | |||
} | |||
static const ValueDetails* getDetails(int index) { | |||
return lookup_.getDetails(index); | |||
} | |||
static std::string getDisplayName(const std::string& name) { | |||
return lookup_.getDisplayName(name); | |||
} | |||
static const mono_float getParameterRange(const std::string& name) { | |||
return lookup_.getParameterRange(name); | |||
} | |||
static const bool isParameter(const std::string& name) { | |||
return lookup_.isParameter(name); | |||
} | |||
static std::map<std::string, ValueDetails> getAllDetails() { | |||
return lookup_.getAllDetails(); | |||
} | |||
static ValueDetailsLookup lookup_; | |||
private: | |||
Parameters() { } | |||
}; | |||
} // namespace vital | |||
@@ -0,0 +1,72 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "synth_types.h" | |||
#include "synth_constants.h" | |||
#include "modulation_connection_processor.h" | |||
namespace vital { | |||
namespace { | |||
const std::string kModulationSourceDelimiter = "_"; | |||
const std::set<std::string> kBipolarModulationSourcePrefixes = { | |||
"lfo", | |||
"stereo", | |||
"random", | |||
"pitch" | |||
}; | |||
force_inline bool isConnectionAvailable(ModulationConnection* connection) { | |||
return connection->source_name.empty() && connection->destination_name.empty(); | |||
} | |||
} | |||
ModulationConnection::ModulationConnection(int index, std::string from, std::string to) : | |||
source_name(std::move(from)), destination_name(std::move(to)) { | |||
modulation_processor = std::make_unique<ModulationConnectionProcessor>(index); | |||
} | |||
ModulationConnection::~ModulationConnection() { } | |||
bool ModulationConnection::isModulationSourceDefaultBipolar(const std::string& source) { | |||
std::size_t pos = source.find(kModulationSourceDelimiter); | |||
std::string prefix = source.substr(0, pos); | |||
return kBipolarModulationSourcePrefixes.count(prefix) > 0; | |||
} | |||
ModulationConnectionBank::ModulationConnectionBank() { | |||
for (int i = 0; i < kMaxModulationConnections; ++i) { | |||
std::unique_ptr<ModulationConnection> connection = std::make_unique<ModulationConnection>(i); | |||
all_connections_.push_back(std::move(connection)); | |||
} | |||
} | |||
ModulationConnectionBank::~ModulationConnectionBank() { } | |||
ModulationConnection* ModulationConnectionBank::createConnection(const std::string& from, const std::string& to) { | |||
int index = 1; | |||
for (auto& connection : all_connections_) { | |||
std::string invalid_connection = "modulation_" + std::to_string(index++) + "_amount"; | |||
if (to != invalid_connection && isConnectionAvailable(connection.get())) { | |||
connection->resetConnection(from, to); | |||
connection->modulation_processor->setBipolar(ModulationConnection::isModulationSourceDefaultBipolar(from)); | |||
return connection.get(); | |||
} | |||
} | |||
return nullptr; | |||
} | |||
} // namespace vital |
@@ -0,0 +1,105 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "circular_queue.h" | |||
#include "synth_parameters.h" | |||
#include "operators.h" | |||
#include "value.h" | |||
#include <map> | |||
#include <string> | |||
namespace vital { | |||
class ValueSwitch; | |||
class ModulationConnectionProcessor; | |||
struct ModulationConnection { | |||
ModulationConnection(int index) : ModulationConnection(index, "", "") { } | |||
ModulationConnection(int index, std::string from, std::string to); | |||
~ModulationConnection(); | |||
static bool isModulationSourceDefaultBipolar(const std::string& source); | |||
void resetConnection(const std::string& from, const std::string& to) { | |||
source_name = from; | |||
destination_name = to; | |||
} | |||
std::string source_name; | |||
std::string destination_name; | |||
std::unique_ptr<ModulationConnectionProcessor> modulation_processor; | |||
private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationConnection) | |||
}; | |||
class ModulationConnectionBank { | |||
public: | |||
ModulationConnectionBank(); | |||
~ModulationConnectionBank(); | |||
ModulationConnection* createConnection(const std::string& from, const std::string& to); | |||
ModulationConnection* atIndex(int index) { return all_connections_[index].get(); } | |||
size_t numConnections() { return all_connections_.size(); } | |||
private: | |||
std::vector<std::unique_ptr<ModulationConnection>> all_connections_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationConnectionBank) | |||
}; | |||
class StringLayout { | |||
public: | |||
StringLayout() : up_key_(0), down_key_(0) { } | |||
std::wstring getLayout() { return layout_; } | |||
void setLayout(const std::wstring& layout) { layout_ = layout; } | |||
wchar_t getUpKey() { return up_key_; } | |||
void setUpKey(wchar_t up_key) { up_key_ = up_key; } | |||
wchar_t getDownKey() { return down_key_; } | |||
void setDownKey(wchar_t down_key) { down_key_ = down_key; } | |||
protected: | |||
std::wstring layout_; | |||
int up_key_; | |||
int down_key_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StringLayout) | |||
}; | |||
typedef struct { | |||
Output* source; | |||
Processor* mono_destination; | |||
Processor* poly_destination; | |||
mono_float destination_scale; | |||
ValueSwitch* mono_modulation_switch; | |||
ValueSwitch* poly_modulation_switch; | |||
ModulationConnectionProcessor* modulation_processor; | |||
bool disconnecting; | |||
int num_audio_rate; | |||
} modulation_change; | |||
typedef std::map<std::string, Value*> control_map; | |||
typedef std::pair<Value*, mono_float> control_change; | |||
typedef std::map<std::string, Processor*> input_map; | |||
typedef std::map<std::string, Output*> output_map; | |||
} // namespace vital | |||
@@ -0,0 +1,433 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "tuning.h" | |||
#include "utils.h" | |||
namespace { | |||
constexpr char kScalaFileExtension[] = ".scl"; | |||
constexpr char kKeyboardMapExtension[] = ".kbm"; | |||
constexpr char kTunFileExtension[] = ".tun"; | |||
constexpr int kDefaultMidiReference = 60; | |||
constexpr char kScalaKbmComment = '!'; | |||
constexpr char kTunComment = ';'; | |||
enum ScalaReadingState { | |||
kDescription, | |||
kScaleLength, | |||
kScaleRatios | |||
}; | |||
enum KbmPositions { | |||
kMapSizePosition, | |||
kStartMidiMapPosition, | |||
kEndMidiMapPosition, | |||
kMidiMapMiddlePosition, | |||
kReferenceNotePosition, | |||
kReferenceFrequencyPosition, | |||
kScaleDegreePosition, | |||
}; | |||
enum TunReadingState { | |||
kScanningForSection, | |||
kTuning, | |||
kExactTuning | |||
}; | |||
String extractFirstToken(const String& source) { | |||
StringArray tokens; | |||
tokens.addTokens(source, false); | |||
return tokens[0]; | |||
} | |||
float readCentsToTranspose(const String& cents) { | |||
return cents.getFloatValue() / vital::kCentsPerNote; | |||
} | |||
float readRatioToTranspose(const String& ratio) { | |||
StringArray tokens; | |||
tokens.addTokens(ratio, "/", ""); | |||
float value = tokens[0].getIntValue(); | |||
if (tokens.size() == 2) | |||
value /= tokens[1].getIntValue(); | |||
return vital::utils::ratioToMidiTranspose(value); | |||
} | |||
String readTunSection(const String& line) { | |||
return line.substring(1, line.length() - 1).toLowerCase(); | |||
} | |||
bool isBaseFrequencyAssignment(const String& line) { | |||
return line.upToFirstOccurrenceOf("=", false, true).toLowerCase().trim() == "basefreq"; | |||
} | |||
int getNoteAssignmentIndex(const String& line) { | |||
String variable = line.upToFirstOccurrenceOf("=", false, true); | |||
StringArray tokens; | |||
tokens.addTokens(variable, false); | |||
if (tokens.size() <= 1 || tokens[0].toLowerCase() != "note") | |||
return -1; | |||
int index = tokens[1].getIntValue(); | |||
if (index < 0 || index >= vital::kMidiSize) | |||
return -1; | |||
return index; | |||
} | |||
float getAssignmentValue(const String& line) { | |||
String value = line.fromLastOccurrenceOf("=", false, true).trim(); | |||
return value.getFloatValue(); | |||
} | |||
} | |||
String Tuning::allFileExtensions() { | |||
return String("*") + kScalaFileExtension + String(";") + | |||
String("*") + kKeyboardMapExtension + String(";") + | |||
String("*") + kTunFileExtension; | |||
} | |||
int Tuning::noteToMidiKey(const String& note_text) { | |||
constexpr int kNotesInScale = 7; | |||
constexpr int kOctaveStart = -1; | |||
constexpr int kScale[kNotesInScale] = { -3, -1, 0, 2, 4, 5, 7 }; | |||
String text = note_text.toLowerCase().removeCharacters(" "); | |||
if (note_text.length() < 2) | |||
return -1; | |||
char note_in_scale = text[0] - 'a'; | |||
if (note_in_scale < 0 || note_in_scale >= kNotesInScale) | |||
return -1; | |||
int offset = kScale[note_in_scale]; | |||
text = text.substring(1); | |||
if (text[0] == '#') { | |||
text = text.substring(1); | |||
offset++; | |||
} | |||
else if (text[0] == 'b') { | |||
text = text.substring(1); | |||
offset--; | |||
} | |||
if (text.length() == 0) | |||
return -1; | |||
bool negative = false; | |||
if (text[0] == '-') { | |||
text = text.substring(1); | |||
negative = true; | |||
if (text.length() == 0) | |||
return -1; | |||
} | |||
int octave = text[0] - '0'; | |||
if (negative) | |||
octave = -octave; | |||
octave = octave - kOctaveStart; | |||
return vital::kNotesPerOctave * octave + offset; | |||
} | |||
Tuning Tuning::getTuningForFile(File file) { | |||
return Tuning(file); | |||
} | |||
void Tuning::loadFile(File file) { | |||
String extension = file.getFileExtension().toLowerCase(); | |||
if (extension == String(kScalaFileExtension)) | |||
loadScalaFile(file); | |||
else if (extension == String(kTunFileExtension)) | |||
loadTunFile(file); | |||
else if (extension == String(kKeyboardMapExtension)) | |||
loadKeyboardMapFile(file); | |||
default_ = false; | |||
} | |||
void Tuning::loadScalaFile(const StringArray& scala_lines) { | |||
ScalaReadingState state = kDescription; | |||
int scale_length = 1; | |||
std::vector<float> scale; | |||
scale.push_back(0.0f); | |||
for (const String& line : scala_lines) { | |||
String trimmed_line = line.trim(); | |||
if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment) | |||
continue; | |||
if (scale.size() >= scale_length + 1) | |||
break; | |||
switch (state) { | |||
case kDescription: | |||
state = kScaleLength; | |||
break; | |||
case kScaleLength: | |||
scale_length = extractFirstToken(trimmed_line).getIntValue(); | |||
state = kScaleRatios; | |||
break; | |||
case kScaleRatios: { | |||
String tuning = extractFirstToken(trimmed_line); | |||
if (tuning.contains(".")) | |||
scale.push_back(readCentsToTranspose(tuning)); | |||
else | |||
scale.push_back(readRatioToTranspose(tuning)); | |||
break; | |||
} | |||
} | |||
} | |||
keyboard_mapping_.clear(); | |||
for (int i = 0; i < scale.size() - 1; ++i) | |||
keyboard_mapping_.push_back(i); | |||
scale_start_midi_note_ = kDefaultMidiReference; | |||
reference_midi_note_ = 0; | |||
loadScale(scale); | |||
default_ = false; | |||
} | |||
void Tuning::loadScalaFile(File scala_file) { | |||
StringArray lines; | |||
scala_file.readLines(lines); | |||
loadScalaFile(lines); | |||
tuning_name_ = scala_file.getFileNameWithoutExtension().toStdString(); | |||
} | |||
void Tuning::loadKeyboardMapFile(File kbm_file) { | |||
static constexpr int kHeaderSize = 7; | |||
StringArray lines; | |||
kbm_file.readLines(lines); | |||
float header_data[kHeaderSize]; | |||
memset(header_data, 0, kHeaderSize * sizeof(float)); | |||
int header_position = 0; | |||
int map_size = 0; | |||
int last_scale_value = 0; | |||
keyboard_mapping_.clear(); | |||
for (const String& line : lines) { | |||
String trimmed_line = line.trim(); | |||
if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment) | |||
continue; | |||
if (header_position >= kHeaderSize) { | |||
String token = extractFirstToken(trimmed_line); | |||
if (token.toLowerCase()[0] != 'x') | |||
last_scale_value = token.getIntValue(); | |||
keyboard_mapping_.push_back(last_scale_value); | |||
if (keyboard_mapping_.size() >= map_size) | |||
break; | |||
} | |||
else { | |||
header_data[header_position] = extractFirstToken(trimmed_line).getFloatValue(); | |||
if (header_position == kMapSizePosition) | |||
map_size = header_data[header_position]; | |||
header_position++; | |||
} | |||
} | |||
setStartMidiNote(header_data[kMidiMapMiddlePosition]); | |||
setReferenceNoteFrequency(header_data[kReferenceNotePosition], header_data[kReferenceFrequencyPosition]); | |||
loadScale(scale_); | |||
mapping_name_ = kbm_file.getFileNameWithoutExtension().toStdString(); | |||
} | |||
void Tuning::loadTunFile(File tun_file) { | |||
keyboard_mapping_.clear(); | |||
TunReadingState state = kScanningForSection; | |||
StringArray lines; | |||
tun_file.readLines(lines); | |||
int last_read_note = 0; | |||
float base_frequency = vital::kMidi0Frequency; | |||
std::vector<float> scale; | |||
for (int i = 0; i < vital::kMidiSize; ++i) | |||
scale.push_back(i); | |||
for (const String& line : lines) { | |||
String trimmed_line = line.trim(); | |||
if (trimmed_line.length() == 0 || trimmed_line[0] == kTunComment) | |||
continue; | |||
if (trimmed_line[0] == '[') { | |||
String section = readTunSection(trimmed_line); | |||
if (section == "tuning") | |||
state = kTuning; | |||
else if (section == "exact tuning") | |||
state = kExactTuning; | |||
else | |||
state = kScanningForSection; | |||
} | |||
else if (state == kTuning || state == kExactTuning) { | |||
if (isBaseFrequencyAssignment(trimmed_line)) | |||
base_frequency = getAssignmentValue(trimmed_line); | |||
else { | |||
int index = getNoteAssignmentIndex(trimmed_line); | |||
last_read_note = std::max(last_read_note, index); | |||
if (index >= 0) | |||
scale[index] = getAssignmentValue(trimmed_line) / vital::kCentsPerNote; | |||
} | |||
} | |||
} | |||
scale.resize(last_read_note + 1); | |||
loadScale(scale); | |||
setStartMidiNote(0); | |||
setReferenceFrequency(base_frequency); | |||
tuning_name_ = tun_file.getFileNameWithoutExtension().toStdString(); | |||
} | |||
Tuning::Tuning() : default_(true) { | |||
scale_start_midi_note_ = kDefaultMidiReference; | |||
reference_midi_note_ = 0; | |||
setDefaultTuning(); | |||
} | |||
Tuning::Tuning(File file) : Tuning() { | |||
loadFile(file); | |||
} | |||
void Tuning::loadScale(std::vector<float> scale) { | |||
scale_ = scale; | |||
if (scale.size() <= 1) { | |||
setConstantTuning(kDefaultMidiReference); | |||
return; | |||
} | |||
int scale_size = static_cast<int>(scale.size() - 1); | |||
int mapping_size = scale_size; | |||
if (keyboard_mapping_.size()) | |||
mapping_size = static_cast<int>(keyboard_mapping_.size()); | |||
float octave_offset = scale[scale_size]; | |||
int start_octave = -kTuningCenter / mapping_size - 1; | |||
int mapping_position = -kTuningCenter - start_octave * mapping_size; | |||
float current_offset = start_octave * octave_offset; | |||
for (int i = 0; i < kTuningSize; ++i) { | |||
if (mapping_position >= mapping_size) { | |||
current_offset += octave_offset; | |||
mapping_position = 0; | |||
} | |||
int note_in_scale = mapping_position; | |||
if (keyboard_mapping_.size()) | |||
note_in_scale = keyboard_mapping_[mapping_position]; | |||
tuning_[i] = current_offset + scale[note_in_scale]; | |||
mapping_position++; | |||
} | |||
} | |||
void Tuning::setConstantTuning(float note) { | |||
for (int i = 0; i < kTuningSize; ++i) | |||
tuning_[i] = note; | |||
} | |||
void Tuning::setDefaultTuning() { | |||
for (int i = 0; i < kTuningSize; ++i) | |||
tuning_[i] = i - kTuningCenter; | |||
scale_.clear(); | |||
for (int i = 0; i <= vital::kNotesPerOctave; ++i) | |||
scale_.push_back(i); | |||
keyboard_mapping_.clear(); | |||
default_ = true; | |||
tuning_name_ = ""; | |||
mapping_name_ = ""; | |||
} | |||
vital::mono_float Tuning::convertMidiNote(int note) const { | |||
int scale_offset = note - scale_start_midi_note_; | |||
return tuning_[kTuningCenter + scale_offset] + scale_start_midi_note_ + reference_midi_note_; | |||
} | |||
void Tuning::setReferenceFrequency(float frequency) { | |||
setReferenceNoteFrequency(0, frequency); | |||
} | |||
void Tuning::setReferenceNoteFrequency(int midi_note, float frequency) { | |||
reference_midi_note_ = vital::utils::frequencyToMidiNote(frequency) - midi_note; | |||
} | |||
void Tuning::setReferenceRatio(float ratio) { | |||
reference_midi_note_ = vital::utils::ratioToMidiTranspose(ratio); | |||
} | |||
json Tuning::stateToJson() const { | |||
json data; | |||
data["scale_start_midi_note"] = scale_start_midi_note_; | |||
data["reference_midi_note"] = reference_midi_note_; | |||
data["tuning_name"] = tuning_name_; | |||
data["mapping_name"] = mapping_name_; | |||
data["default"] = default_; | |||
json scale_data; | |||
for (float scale_value : scale_) | |||
scale_data.push_back(scale_value); | |||
data["scale"] = scale_data; | |||
if (keyboard_mapping_.size()) { | |||
json mapping_data; | |||
for (int mapping_value : keyboard_mapping_) | |||
mapping_data.push_back(mapping_value); | |||
data["mapping"] = mapping_data; | |||
} | |||
return data; | |||
} | |||
void Tuning::jsonToState(const json& data) { | |||
scale_start_midi_note_ = data["scale_start_midi_note"]; | |||
reference_midi_note_ = data["reference_midi_note"]; | |||
std::string tuning_name = data["tuning_name"]; | |||
tuning_name_ = tuning_name; | |||
std::string mapping_name = data["mapping_name"]; | |||
mapping_name_ = mapping_name; | |||
if (data.count("default")) | |||
default_ = data["default"]; | |||
json scale_data = data["scale"]; | |||
scale_.clear(); | |||
for (json& value : scale_data) { | |||
float scale_value = value; | |||
scale_.push_back(scale_value); | |||
} | |||
keyboard_mapping_.clear(); | |||
if (data.count("mapping")) { | |||
json mapping_data = data["mapping"]; | |||
for (json& value : mapping_data) { | |||
int keyboard_value = value; | |||
keyboard_mapping_.push_back(keyboard_value); | |||
} | |||
} | |||
loadScale(scale_); | |||
} |
@@ -0,0 +1,82 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "common.h" | |||
#include "json/json.h" | |||
using json = nlohmann::json; | |||
class Tuning { | |||
public: | |||
static constexpr int kTuningSize = 2 * vital::kMidiSize; | |||
static constexpr int kTuningCenter = vital::kMidiSize; | |||
static Tuning getTuningForFile(File file); | |||
static String allFileExtensions(); | |||
static int noteToMidiKey(const String& note); | |||
Tuning(); | |||
Tuning(File file); | |||
void loadScale(std::vector<float> scale); | |||
void loadFile(File file); | |||
void setConstantTuning(float note); | |||
void setDefaultTuning(); | |||
vital::mono_float convertMidiNote(int note) const; | |||
void setStartMidiNote(int start_midi_note) { scale_start_midi_note_ = start_midi_note; } | |||
void setReferenceNote(int reference_note) { reference_midi_note_ = reference_note; } | |||
void setReferenceFrequency(float frequency); | |||
void setReferenceNoteFrequency(int midi_note, float frequency); | |||
void setReferenceRatio(float ratio); | |||
std::string getName() const { | |||
if (mapping_name_.size() == 0) | |||
return tuning_name_; | |||
if (tuning_name_.size() == 0) | |||
return mapping_name_; | |||
return tuning_name_ + " / " + mapping_name_; | |||
} | |||
void setName(const std::string& name) { | |||
mapping_name_ = ""; | |||
tuning_name_ = name; | |||
} | |||
bool isDefault() const { return default_; } | |||
json stateToJson() const; | |||
void jsonToState(const json& data); | |||
void loadScalaFile(const StringArray& scala_lines); | |||
private: | |||
void loadScalaFile(File scala_file); | |||
void loadKeyboardMapFile(File kbm_file); | |||
void loadTunFile(File tun_file); | |||
int scale_start_midi_note_; | |||
float reference_midi_note_; | |||
std::vector<float> scale_; | |||
std::vector<int> keyboard_mapping_; | |||
vital::mono_float tuning_[kTuningSize]; | |||
std::string tuning_name_; | |||
std::string mapping_name_; | |||
bool default_; | |||
JUCE_LEAK_DETECTOR(Tuning) | |||
}; | |||
@@ -0,0 +1,400 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "file_source.h" | |||
FileSource::FileSourceKeyframe::FileSourceKeyframe(SampleBuffer* sample_buffer) { | |||
sample_buffer_ = sample_buffer; | |||
start_position_ = 0.0f; | |||
window_fade_ = 1.0f; | |||
window_size_ = vital::WaveFrame::kWaveformSize; | |||
fade_style_ = kWaveBlend; | |||
phase_style_ = kNone; | |||
overridden_phase_ = nullptr; | |||
interpolate_from_frame_ = nullptr; | |||
interpolate_to_frame_ = nullptr; | |||
} | |||
void FileSource::FileSourceKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const FileSourceKeyframe* source = dynamic_cast<const FileSourceKeyframe*>(keyframe); | |||
VITAL_ASSERT(source); | |||
start_position_ = source->start_position_; | |||
window_fade_ = source->window_fade_; | |||
} | |||
void FileSource::FileSourceKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const FileSourceKeyframe* from = dynamic_cast<const FileSourceKeyframe*>(from_keyframe); | |||
const FileSourceKeyframe* to = dynamic_cast<const FileSourceKeyframe*>(to_keyframe); | |||
VITAL_ASSERT(from); | |||
VITAL_ASSERT(to); | |||
start_position_ = linearTween(from->start_position_, to->start_position_, t); | |||
window_fade_ = linearTween(from->window_fade_, to->window_fade_, t); | |||
} | |||
force_inline float FileSource::FileSourceKeyframe::getScaledInterpolatedSample(float position) { | |||
const float* buffer = getCubicInterpolationBuffer(); | |||
float clamped_position = vital::utils::clamp(position, 0.0f, sample_buffer_->size - 1); | |||
int start_index = clamped_position; | |||
float t = clamped_position - start_index; | |||
vital::matrix interpolation_matrix = vital::utils::getCatmullInterpolationMatrix(t); | |||
vital::matrix value_matrix = vital::utils::getValueMatrix(buffer, start_index); | |||
value_matrix.transpose(); | |||
return interpolation_matrix.multiplyAndSumRows(value_matrix)[0]; | |||
} | |||
float FileSource::FileSourceKeyframe::getNormalizationScale() { | |||
const float* buffer = getDataBuffer(); | |||
if (buffer == nullptr) | |||
return 1.0f; | |||
double cycles_in = start_position_ / window_size_; | |||
int cycle = cycles_in; | |||
double start_index = cycle * window_size_; | |||
float max = 0.0f; | |||
float min = 0.0f; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
double t = i / (vital::WaveFrame::kWaveformSize * 1.0); | |||
double position = std::min<double>(start_index + t * window_size_, sample_buffer_->size - 1); | |||
int from_index = position; | |||
int to_index = std::min(sample_buffer_->size - 1, from_index + 1); | |||
VITAL_ASSERT(from_index >= 0 && from_index < sample_buffer_->size); | |||
VITAL_ASSERT(to_index >= 0 && to_index < sample_buffer_->size); | |||
float from_sample = buffer[from_index]; | |||
float to_sample = buffer[to_index]; | |||
max = std::max(from_sample, max); | |||
max = std::max(to_sample, max); | |||
min = std::min(from_sample, min); | |||
min = std::min(to_sample, min); | |||
} | |||
return 2.0f / std::max(max - min, 0.001f); | |||
} | |||
void FileSource::FileSourceKeyframe::render(vital::WaveFrame* wave_frame) { | |||
if (sample_buffer_->size <= 0) { | |||
wave_frame->clear(); | |||
return; | |||
} | |||
if (fade_style_ == kWaveBlend) | |||
renderWaveBlend(wave_frame); | |||
else if (fade_style_ == kNoInterpolate) | |||
renderNoInterpolate(wave_frame); | |||
else if (fade_style_ == kTimeInterpolate) | |||
renderTimeInterpolate(wave_frame); | |||
else if (fade_style_ == kFreqInterpolate) | |||
renderFreqInterpolate(wave_frame); | |||
if (phase_style_ == kClear || phase_style_ == kVocode) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
float amplitude = std::abs(wave_frame->frequency_domain[i]); | |||
wave_frame->frequency_domain[i] = std::polar(amplitude, overridden_phase_[i]); | |||
} | |||
} | |||
wave_frame->toTimeDomain(); | |||
} | |||
void FileSource::FileSourceKeyframe::renderWaveBlend(vital::WaveFrame* wave_frame) { | |||
double window_ratio = window_size_ / vital::WaveFrame::kWaveformSize; | |||
int waveform_middle = vital::WaveFrame::kWaveformSize / 2; | |||
int start_index = start_position_ / window_ratio + window_size_ / 2.0f + waveform_middle; | |||
start_index = start_index % vital::WaveFrame::kWaveformSize; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
double t = i / (vital::WaveFrame::kWaveformSize * 1.0); | |||
double position = start_position_ + t * window_size_; | |||
int write_index = (start_index + i) % vital::WaveFrame::kWaveformSize; | |||
wave_frame->time_domain[write_index] = getScaledInterpolatedSample(position); | |||
} | |||
int fade_samples = window_fade_ * vital::WaveFrame::kWaveformSize; | |||
double fade_size = fade_samples * window_ratio; | |||
for (int i = 0; i < fade_samples; ++i) { | |||
double t = i / (fade_samples - 1.0f); | |||
double fade = 0.5 + 0.5 * cos(vital::kPi * t); | |||
int write_index = (start_index + i) % vital::WaveFrame::kWaveformSize; | |||
double position = start_position_ + window_size_ + t * fade_size; | |||
double existing_value = wave_frame->time_domain[write_index]; | |||
double fade_value = getScaledInterpolatedSample(position); | |||
wave_frame->time_domain[write_index] = linearTween(existing_value, fade_value, fade); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
void FileSource::FileSourceKeyframe::renderNoInterpolate(vital::WaveFrame* wave_frame) { | |||
double cycles_in = start_position_ / window_size_; | |||
int cycle = cycles_in; | |||
double start_index = cycle * window_size_; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
double t = i / (vital::WaveFrame::kWaveformSize * 1.0); | |||
double position = start_index + t * window_size_; | |||
wave_frame->time_domain[i] = getScaledInterpolatedSample(position); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
void FileSource::FileSourceKeyframe::renderTimeInterpolate(vital::WaveFrame* wave_frame) { | |||
double cycles_in = start_position_ / window_size_; | |||
int from_cycle = cycles_in; | |||
int to_cycle = from_cycle + 1; | |||
float transition = cycles_in - from_cycle; | |||
double start_index_from = from_cycle * window_size_; | |||
double start_index_to = to_cycle * window_size_; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
double t = i / (vital::WaveFrame::kWaveformSize * 1.0); | |||
double from_position = start_index_from + t * window_size_; | |||
double to_position = start_index_to + t * window_size_; | |||
float from_sample = getScaledInterpolatedSample(from_position); | |||
float to_sample = getScaledInterpolatedSample(to_position); | |||
wave_frame->time_domain[i] = vital::utils::interpolate(from_sample, to_sample, transition); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
void FileSource::FileSourceKeyframe::renderFreqInterpolate(vital::WaveFrame* wave_frame) { | |||
double cycles_in = start_position_ / window_size_; | |||
int from_cycle = cycles_in; | |||
int to_cycle = from_cycle + 1; | |||
float transition = cycles_in - from_cycle; | |||
double start_index_from = from_cycle * window_size_; | |||
double start_index_to = to_cycle * window_size_; | |||
vital::WaveFrame* from_wave_frame = interpolate_from_frame_->wave_frame(); | |||
vital::WaveFrame* to_wave_frame = interpolate_to_frame_->wave_frame(); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
double t = i / (vital::WaveFrame::kWaveformSize * 1.0); | |||
double from_position = start_index_from + t * window_size_; | |||
double to_position = start_index_to + t * window_size_; | |||
from_wave_frame->time_domain[i] = getScaledInterpolatedSample(from_position); | |||
to_wave_frame->time_domain[i] = getScaledInterpolatedSample(to_position); | |||
} | |||
from_wave_frame->toFrequencyDomain(); | |||
to_wave_frame->toFrequencyDomain(); | |||
interpolate_from_frame_->linearFrequencyInterpolate(from_wave_frame, to_wave_frame, transition); | |||
wave_frame->copy(interpolate_from_frame_->wave_frame()); | |||
} | |||
json FileSource::FileSourceKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["start_position"] = start_position_; | |||
data["window_fade"] = window_fade_; | |||
data["window_size"] = window_size_; | |||
return data; | |||
} | |||
void FileSource::FileSourceKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
start_position_ = data["start_position"]; | |||
window_fade_ = data["window_fade"]; | |||
window_size_ = data["window_size"]; | |||
} | |||
FileSource::FileSource() : compute_frame_(&sample_buffer_), overridden_phase_(), | |||
fade_style_(kWaveBlend), phase_style_(kNone), | |||
normalize_gain_(false), normalize_mult_(false), | |||
random_generator_(-vital::kPi, vital::kPi) { | |||
window_size_ = vital::WaveFrame::kWaveformSize; | |||
random_seed_ = random_generator_.next() * (INT_MAX / vital::kPi); | |||
} | |||
WavetableKeyframe* FileSource::createKeyframe(int position) { | |||
FileSourceKeyframe* keyframe = new FileSourceKeyframe(&sample_buffer_); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void FileSource::render(vital::WaveFrame* wave_frame, float position) { | |||
if (sample_buffer_.data == nullptr) | |||
wave_frame->clear(); | |||
else { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.setWindowSize(window_size_); | |||
compute_frame_.setFadeStyle(fade_style_); | |||
compute_frame_.setPhaseStyle(phase_style_); | |||
compute_frame_.setInterpolateFromFrame(&interpolate_from_frame_); | |||
compute_frame_.setInterpolateToFrame(&interpolate_to_frame_); | |||
compute_frame_.setOverriddenPhaseBuffer(overridden_phase_); | |||
compute_frame_.render(wave_frame); | |||
wave_frame->setFrequencyRatio(window_size_ / vital::WaveFrame::kWaveformSize); | |||
wave_frame->setSampleRate(sample_buffer_.sample_rate); | |||
if (normalize_mult_) | |||
wave_frame->normalize(normalize_gain_); | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
} | |||
WavetableComponentFactory::ComponentType FileSource::getType() { | |||
return WavetableComponentFactory::kFileSource; | |||
} | |||
json FileSource::stateToJson() { | |||
double max_position = 0; | |||
for (int i = 0; i < numFrames(); ++i) | |||
max_position = std::max(max_position, getKeyframe(i)->getStartPosition()); | |||
json data = WavetableComponent::stateToJson(); | |||
data["normalize_gain"] = normalize_gain_; | |||
data["normalize_mult"] = normalize_mult_; | |||
data["window_size"] = window_size_; | |||
data["fade_style"] = fade_style_; | |||
data["phase_style"] = phase_style_; | |||
data["random_seed"] = random_seed_; | |||
data["audio_sample_rate"] = sample_buffer_.sample_rate; | |||
int save_samples = max_position + 2 * window_size_ + kExtraSaveSamples; | |||
int num_samples = std::min(sample_buffer_.size, save_samples); | |||
String encoded = ""; | |||
if (getDataBuffer()) { | |||
std::unique_ptr<int16_t[]> pcm_data = std::make_unique<int16_t[]>(num_samples); | |||
vital::utils::floatToPcmData(pcm_data.get(), getDataBuffer(), num_samples); | |||
encoded = Base64::toBase64(pcm_data.get(), num_samples * sizeof(int16_t)); | |||
} | |||
data["audio_file"] = encoded.toStdString(); | |||
return data; | |||
} | |||
void FileSource::jsonToState(json data) { | |||
normalize_gain_ = data["normalize_gain"]; | |||
if (data.count("normalize_mult")) | |||
normalize_mult_ = data["normalize_mult"]; | |||
else | |||
normalize_mult_ = true; | |||
window_size_ = data["window_size"]; | |||
fade_style_ = kWaveBlend; | |||
if (data.count("fade_style")) | |||
fade_style_ = data["fade_style"]; | |||
phase_style_ = kNone; | |||
if (data.count("phase_style")) | |||
phase_style_ = data["phase_style"]; | |||
if (data.count("random_seed")) | |||
random_seed_ = data["random_seed"]; | |||
writePhaseOverrideBuffer(); | |||
WavetableComponent::jsonToState(data); | |||
int sample_rate = vital::kDefaultSampleRate; | |||
if (data.count("audio_sample_rate")) | |||
sample_rate = data["audio_sample_rate"]; | |||
MemoryOutputStream decoded; | |||
std::string audio_data = data["audio_file"]; | |||
Base64::convertFromBase64(decoded, audio_data); | |||
int size = static_cast<int>(decoded.getDataSize()) / sizeof(int16_t); | |||
std::unique_ptr<float[]> float_data = std::make_unique<float[]>(size); | |||
vital::utils::pcmToFloatData(float_data.get(), (int16_t*)decoded.getData(), size); | |||
loadBuffer(float_data.get(), size, sample_rate); | |||
} | |||
FileSource::FileSourceKeyframe* FileSource::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<FileSource::FileSourceKeyframe*>(wavetable_keyframe); | |||
} | |||
void FileSource::setPhaseStyle(PhaseStyle phase_style) { | |||
if (phase_style_ == phase_style) | |||
return; | |||
phase_style_ = phase_style; | |||
if (phase_style_ == kVocode) | |||
random_seed_++; | |||
writePhaseOverrideBuffer(); | |||
} | |||
void FileSource::writePhaseOverrideBuffer() { | |||
if (phase_style_ == kClear) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize / 2; ++i) { | |||
overridden_phase_[2 * i] = -0.5f * vital::kPi; | |||
overridden_phase_[2 * i + 1] = 0.5f * vital::kPi; | |||
} | |||
} | |||
else if (phase_style_ == kVocode) { | |||
random_generator_.seed(random_seed_); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
overridden_phase_[i] = random_generator_.next(); | |||
} | |||
} | |||
void FileSource::loadBuffer(const float* buffer, int size, int sample_rate) { | |||
sample_buffer_.sample_rate = sample_rate; | |||
sample_buffer_.size = size; | |||
sample_buffer_.data = std::make_unique<float[]>(size + kExtraBufferSamples); | |||
memcpy(sample_buffer_.data.get() + 1, buffer, size * sizeof(float)); | |||
sample_buffer_.data[0] = sample_buffer_.data[1]; | |||
for (int i = 1; i < kExtraBufferSamples; ++i) | |||
sample_buffer_.data[sample_buffer_.size + i] = sample_buffer_.data[size]; | |||
} | |||
void FileSource::detectPitch(int max_period) { | |||
int start = (sample_buffer_.size - kPitchDetectMaxPeriod) / 3; | |||
pitch_detector_.loadSignal(getDataBuffer() + start, kPitchDetectMaxPeriod); | |||
float period = pitch_detector_.matchPeriod(max_period); | |||
setWindowSize(period); | |||
} | |||
void FileSource::detectWaveEditTable() { | |||
static constexpr int kWaveEditFrameLength = 256; | |||
static constexpr int kFrequencyDomainTotals = 8; | |||
static constexpr int kWaveEditNumFrames = 64; | |||
if (sample_buffer_.size != kWaveEditFrameLength * kWaveEditNumFrames) | |||
return; | |||
vital::WaveFrame wave_frame; | |||
const float* buffer = getDataBuffer(); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame.time_domain[i] = buffer[i]; | |||
wave_frame.toFrequencyDomain(); | |||
int size_mult = vital::WaveFrame::kWaveformSize / kWaveEditFrameLength; | |||
std::unique_ptr<float[]> totals = std::make_unique<float[]>(size_mult); | |||
for (int i = 0; i < size_mult; ++i) { | |||
for (int j = 0; j < kFrequencyDomainTotals; ++j) | |||
totals[i] += std::abs(wave_frame.frequency_domain[i + 1 + j * size_mult]); | |||
} | |||
for (int i = 0; i < size_mult - 1; ++i) { | |||
if (totals[i] > totals[size_mult - 1]) | |||
return; | |||
} | |||
setWindowSize(kWaveEditFrameLength); | |||
} |
@@ -0,0 +1,174 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "pitch_detector.h" | |||
#include "wavetable_component.h" | |||
#include "wave_frame.h" | |||
#include "wave_source.h" | |||
#include "utils.h" | |||
class FileSource : public WavetableComponent { | |||
public: | |||
static constexpr float kMaxFileSourceSamples = 176400; | |||
static constexpr int kExtraSaveSamples = 4; | |||
static constexpr int kExtraBufferSamples = 4; | |||
static constexpr int kPitchDetectMaxPeriod = 8096; | |||
enum FadeStyle { | |||
kWaveBlend, | |||
kNoInterpolate, | |||
kTimeInterpolate, | |||
kFreqInterpolate, | |||
kNumFadeStyles | |||
}; | |||
enum PhaseStyle { | |||
kNone, | |||
kClear, | |||
kVocode, | |||
kNumPhaseStyles | |||
}; | |||
struct SampleBuffer { | |||
SampleBuffer() : size(0), sample_rate(0) { } | |||
std::unique_ptr<float[]> data; | |||
int size; | |||
int sample_rate; | |||
}; | |||
class FileSourceKeyframe : public WavetableKeyframe { | |||
public: | |||
FileSourceKeyframe(SampleBuffer* sample_buffer); | |||
virtual ~FileSourceKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
float getNormalizationScale(); | |||
void render(vital::WaveFrame* wave_frame) override; | |||
void renderWaveBlend(vital::WaveFrame* wave_frame); | |||
void renderNoInterpolate(vital::WaveFrame* wave_frame); | |||
void renderTimeInterpolate(vital::WaveFrame* wave_frame); | |||
void renderFreqInterpolate(vital::WaveFrame* wave_frame); | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
double getStartPosition() { return start_position_; } | |||
double getWindowSize() { return window_size_; } | |||
double getWindowFade() { return window_fade_; } | |||
double getWindowFadeSamples() { return window_fade_ * window_size_; } | |||
int getSamplesNeeded() { return getWindowSize() + getWindowFadeSamples(); } | |||
force_inline void setStartPosition(double start_position) { start_position_ = start_position; } | |||
force_inline void setWindowFade(double window_fade) { window_fade_ = window_fade; } | |||
force_inline void setWindowSize(double window_size) { window_size_ = window_size; } | |||
force_inline void setFadeStyle(FadeStyle fade_style) { fade_style_ = fade_style; } | |||
force_inline void setPhaseStyle(PhaseStyle phase_style) { phase_style_ = phase_style; } | |||
force_inline void setOverriddenPhaseBuffer(const float* buffer) { overridden_phase_ = buffer; } | |||
force_inline const float* getDataBuffer() { | |||
if (sample_buffer_ == nullptr || sample_buffer_->data == nullptr) | |||
return nullptr; | |||
return sample_buffer_->data.get() + 1; | |||
} | |||
force_inline const float* getCubicInterpolationBuffer() { | |||
if (sample_buffer_ == nullptr) | |||
return nullptr; | |||
return sample_buffer_->data.get(); | |||
} | |||
float getScaledInterpolatedSample(float time); | |||
void setInterpolateFromFrame(WaveSourceKeyframe* frame) { | |||
interpolate_from_frame_ = frame; | |||
} | |||
void setInterpolateToFrame(WaveSourceKeyframe* frame) { | |||
interpolate_to_frame_ = frame; | |||
} | |||
protected: | |||
SampleBuffer* sample_buffer_; | |||
const float* overridden_phase_; | |||
WaveSourceKeyframe* interpolate_from_frame_; | |||
WaveSourceKeyframe* interpolate_to_frame_; | |||
double start_position_; | |||
double window_fade_; | |||
double window_size_; | |||
FadeStyle fade_style_; | |||
PhaseStyle phase_style_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileSourceKeyframe) | |||
}; | |||
FileSource(); | |||
virtual ~FileSource() { } | |||
WavetableKeyframe* createKeyframe(int position) override; | |||
void render(vital::WaveFrame* wave_frame, float position) override; | |||
WavetableComponentFactory::ComponentType getType() override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
FileSourceKeyframe* getKeyframe(int index); | |||
const SampleBuffer* buffer() const { return &sample_buffer_; } | |||
FadeStyle getFadeStyle() { return fade_style_; } | |||
PhaseStyle getPhaseStyle() { return phase_style_; } | |||
bool getNormalizeGain() { return normalize_gain_; } | |||
void setNormalizeGain(bool normalize_gain) { normalize_gain_ = normalize_gain; } | |||
void setWindowSize(double window_size) { window_size_ = window_size; } | |||
void setFadeStyle(FadeStyle fade_style) { fade_style_ = fade_style; } | |||
void setPhaseStyle(PhaseStyle phase_style); | |||
void writePhaseOverrideBuffer(); | |||
double getWindowSize() { return window_size_; } | |||
void loadBuffer(const float* buffer, int size, int sample_rate); | |||
void detectPitch(int max_period = vital::WaveFrame::kWaveformSize); | |||
void detectWaveEditTable(); | |||
force_inline const float* getDataBuffer() { | |||
if (sample_buffer_.data == nullptr) | |||
return nullptr; | |||
return sample_buffer_.data.get() + 1; | |||
} | |||
force_inline const float* getCubicInterpolationBuffer() { return sample_buffer_.data.get(); } | |||
protected: | |||
FileSourceKeyframe compute_frame_; | |||
WaveSourceKeyframe interpolate_from_frame_; | |||
WaveSourceKeyframe interpolate_to_frame_; | |||
SampleBuffer sample_buffer_; | |||
float overridden_phase_[vital::WaveFrame::kWaveformSize]; | |||
FadeStyle fade_style_; | |||
PhaseStyle phase_style_; | |||
bool normalize_gain_; | |||
bool normalize_mult_; | |||
double window_size_; | |||
int random_seed_; | |||
vital::utils::RandomGenerator random_generator_; | |||
PitchDetector pitch_detector_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FileSource) | |||
}; | |||
@@ -0,0 +1,148 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "frequency_filter_modifier.h" | |||
#include "futils.h" | |||
#include "utils.h" | |||
#include "wave_frame.h" | |||
namespace { | |||
constexpr float kMinPower = -9.0f; | |||
constexpr float kMaxPower = 9.0f; | |||
constexpr int kMaxSlopeReach = 128; | |||
force_inline double powerScale(double value, double power) { | |||
static constexpr float kMinPower = 0.01f; | |||
if (fabs(power) < kMinPower) | |||
return value; | |||
double abs_value = fabs(value); | |||
double numerator = exp(power * abs_value) - 1.0f; | |||
double denominator = exp(power) - 1.0f; | |||
if (value >= 0.0f) | |||
return numerator / denominator; | |||
return -numerator / denominator; | |||
} | |||
force_inline float combWave(float t, float power) { | |||
float range = t - floorf(t); | |||
return 2.0f * powerScale(1.0f - fabsf(2.0f * range - 1.0f), power); | |||
} | |||
} // namespace | |||
FrequencyFilterModifier::FrequencyFilterModifierKeyframe::FrequencyFilterModifierKeyframe() { | |||
cutoff_ = 4.0f; | |||
shape_ = 0.5f; | |||
style_ = kLowPass; | |||
normalize_ = true; | |||
} | |||
void FrequencyFilterModifier::FrequencyFilterModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const FrequencyFilterModifierKeyframe* source = dynamic_cast<const FrequencyFilterModifierKeyframe*>(keyframe); | |||
shape_ = source->shape_; | |||
cutoff_ = source->cutoff_; | |||
} | |||
void FrequencyFilterModifier::FrequencyFilterModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const FrequencyFilterModifierKeyframe* from = dynamic_cast<const FrequencyFilterModifierKeyframe*>(from_keyframe); | |||
const FrequencyFilterModifierKeyframe* to = dynamic_cast<const FrequencyFilterModifierKeyframe*>(to_keyframe); | |||
shape_ = linearTween(from->shape_, to->shape_, t); | |||
cutoff_ = linearTween(from->cutoff_, to->cutoff_, t); | |||
} | |||
void FrequencyFilterModifier::FrequencyFilterModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
for (int i = 0; i < vital::WaveFrame::kNumRealComplex; ++i) | |||
wave_frame->frequency_domain[i] *= getMultiplier(i); | |||
wave_frame->toTimeDomain(); | |||
if (normalize_) { | |||
wave_frame->normalize(true); | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
} | |||
json FrequencyFilterModifier::FrequencyFilterModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["cutoff"] = cutoff_; | |||
data["shape"] = shape_; | |||
return data; | |||
} | |||
void FrequencyFilterModifier::FrequencyFilterModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
cutoff_ = data["cutoff"]; | |||
shape_ = data["shape"]; | |||
} | |||
float FrequencyFilterModifier::FrequencyFilterModifierKeyframe::getMultiplier(float index) { | |||
float cutoff_index = std::pow(2.0f, cutoff_); | |||
float cutoff_delta = index - cutoff_index; | |||
float slope = 1.0f / vital::utils::interpolate(1.0f, kMaxSlopeReach, shape_ * shape_); | |||
float power = vital::utils::interpolate(kMinPower, kMaxPower, shape_); | |||
if (style_ == kLowPass) | |||
return vital::utils::clamp(1.0f - slope * cutoff_delta, 0.0f, 1.0f); | |||
if (style_ == kBandPass) | |||
return vital::utils::clamp(1.0f - fabsf(slope * cutoff_delta), 0.0f, 1.0f); | |||
if (style_ == kHighPass) | |||
return vital::utils::clamp(1.0f + slope * cutoff_delta, 0.0f, 1.0f); | |||
if (style_ == kComb) | |||
return combWave(index / (cutoff_index * 2.0f), power); | |||
return 0.0f; | |||
} | |||
WavetableKeyframe* FrequencyFilterModifier::createKeyframe(int position) { | |||
FrequencyFilterModifierKeyframe* keyframe = new FrequencyFilterModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void FrequencyFilterModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.setStyle(style_); | |||
compute_frame_.setNormalize(normalize_); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType FrequencyFilterModifier::getType() { | |||
return WavetableComponentFactory::kFrequencyFilter; | |||
} | |||
json FrequencyFilterModifier::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["style"] = style_; | |||
data["normalize"] = normalize_; | |||
return data; | |||
} | |||
void FrequencyFilterModifier::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
style_ = data["style"]; | |||
normalize_ = data["normalize"]; | |||
} | |||
FrequencyFilterModifier::FrequencyFilterModifierKeyframe* FrequencyFilterModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<FrequencyFilterModifier::FrequencyFilterModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,87 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class FrequencyFilterModifier : public WavetableComponent { | |||
public: | |||
enum FilterStyle { | |||
kLowPass, | |||
kBandPass, | |||
kHighPass, | |||
kComb, | |||
kNumFilterStyles | |||
}; | |||
class FrequencyFilterModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
FrequencyFilterModifierKeyframe(); | |||
virtual ~FrequencyFilterModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
float getMultiplier(float index); | |||
float getCutoff() { return cutoff_; } | |||
float getShape() { return shape_; } | |||
void setStyle(FilterStyle style) { style_ = style; } | |||
void setCutoff(float cutoff) { cutoff_ = cutoff; } | |||
void setShape(float shape) { shape_ = shape; } | |||
void setNormalize(bool normalize) { normalize_ = normalize; } | |||
protected: | |||
FilterStyle style_; | |||
bool normalize_; | |||
float cutoff_; | |||
float shape_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FrequencyFilterModifierKeyframe) | |||
}; | |||
FrequencyFilterModifier() : style_(kLowPass), normalize_(true) { } | |||
virtual ~FrequencyFilterModifier() { } | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
FrequencyFilterModifierKeyframe* getKeyframe(int index); | |||
FilterStyle getStyle() { return style_; } | |||
bool getNormalize() { return normalize_; } | |||
void setStyle(FilterStyle style) { style_ = style; } | |||
void setNormalize(bool normalize) { normalize_ = normalize; } | |||
protected: | |||
FilterStyle style_; | |||
bool normalize_; | |||
FrequencyFilterModifierKeyframe compute_frame_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FrequencyFilterModifier) | |||
}; | |||
@@ -0,0 +1,127 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "phase_modifier.h" | |||
#include "wave_frame.h" | |||
#include "wavetable_component_factory.h" | |||
namespace { | |||
std::complex<float> multiplyAndMix(std::complex<float> value, std::complex<float> mult, float mix) { | |||
std::complex<float> result = value * mult; | |||
return mix * result + (1.0f - mix) * value; | |||
} | |||
} // namespace | |||
PhaseModifier::PhaseModifierKeyframe::PhaseModifierKeyframe() : phase_(0.0f), mix_(1.0f), phase_style_(kNormal) { } | |||
void PhaseModifier::PhaseModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const PhaseModifierKeyframe* source = dynamic_cast<const PhaseModifierKeyframe*>(keyframe); | |||
phase_ = source->phase_; | |||
mix_ = source->mix_; | |||
} | |||
void PhaseModifier::PhaseModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const PhaseModifierKeyframe* from = dynamic_cast<const PhaseModifierKeyframe*>(from_keyframe); | |||
const PhaseModifierKeyframe* to = dynamic_cast<const PhaseModifierKeyframe*>(to_keyframe); | |||
phase_ = linearTween(from->phase_, to->phase_, t); | |||
mix_ = linearTween(from->mix_, to->mix_, t); | |||
} | |||
void PhaseModifier::PhaseModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
std::complex<float> phase_shift = std::polar(1.0f, -phase_); | |||
if (phase_style_ == kHarmonic) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame->frequency_domain[i] = multiplyAndMix(wave_frame->frequency_domain[i], phase_shift, mix_); | |||
} | |||
else if (phase_style_ == kHarmonicEvenOdd) { | |||
std::complex<float> odd_shift = 1.0f / phase_shift; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; i += 2) { | |||
wave_frame->frequency_domain[i] = multiplyAndMix(wave_frame->frequency_domain[i], phase_shift, mix_); | |||
wave_frame->frequency_domain[i + 1] = multiplyAndMix(wave_frame->frequency_domain[i + 1], odd_shift, mix_); | |||
} | |||
} | |||
else if (phase_style_ == kNormal) { | |||
std::complex<float> current_phase_shift = 1.0f; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
wave_frame->frequency_domain[i] = multiplyAndMix(wave_frame->frequency_domain[i], current_phase_shift, mix_); | |||
current_phase_shift *= phase_shift; | |||
} | |||
} | |||
else if (phase_style_ == kEvenOdd) { | |||
std::complex<float> current_phase_shift = 1.0f; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; i += 2) { | |||
wave_frame->frequency_domain[i] = multiplyAndMix(wave_frame->frequency_domain[i], current_phase_shift, mix_); | |||
std::complex<float> odd_shift = 1.0f / (current_phase_shift * phase_shift); | |||
wave_frame->frequency_domain[i + 1] = multiplyAndMix(wave_frame->frequency_domain[i + 1], odd_shift, mix_); | |||
current_phase_shift *= phase_shift * phase_shift; | |||
} | |||
} | |||
else if (phase_style_ == kClear) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame->frequency_domain[i] = std::abs(wave_frame->frequency_domain[i]); | |||
} | |||
wave_frame->toTimeDomain(); | |||
} | |||
json PhaseModifier::PhaseModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["phase"] = phase_; | |||
data["mix"] = mix_; | |||
return data; | |||
} | |||
void PhaseModifier::PhaseModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
phase_ = data["phase"]; | |||
mix_ = data["mix"]; | |||
} | |||
WavetableKeyframe* PhaseModifier::createKeyframe(int position) { | |||
PhaseModifierKeyframe* keyframe = new PhaseModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void PhaseModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
compute_frame_.setPhaseStyle(phase_style_); | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType PhaseModifier::getType() { | |||
return WavetableComponentFactory::kPhaseModifier; | |||
} | |||
json PhaseModifier::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["style"] = phase_style_; | |||
return data; | |||
} | |||
void PhaseModifier::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
phase_style_ = data["style"]; | |||
} | |||
PhaseModifier::PhaseModifierKeyframe* PhaseModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<PhaseModifier::PhaseModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,79 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class PhaseModifier : public WavetableComponent { | |||
public: | |||
enum PhaseStyle { | |||
kNormal, | |||
kEvenOdd, | |||
kHarmonic, | |||
kHarmonicEvenOdd, | |||
kClear, | |||
kNumPhaseStyles | |||
}; | |||
class PhaseModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
PhaseModifierKeyframe(); | |||
virtual ~PhaseModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
float getPhase() { return phase_; } | |||
float getMix() { return mix_; } | |||
void setPhase(float phase) { phase_ = phase; } | |||
void setMix(float mix) { mix_ = mix; } | |||
void setPhaseStyle(PhaseStyle style) { phase_style_ = style; } | |||
protected: | |||
float phase_; | |||
float mix_; | |||
PhaseStyle phase_style_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PhaseModifierKeyframe) | |||
}; | |||
PhaseModifier() : phase_style_(kNormal) { } | |||
virtual ~PhaseModifier() = default; | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
PhaseModifierKeyframe* getKeyframe(int index); | |||
void setPhaseStyle(PhaseStyle style) { phase_style_ = style; } | |||
PhaseStyle getPhaseStyle() const { return phase_style_; } | |||
protected: | |||
PhaseModifierKeyframe compute_frame_; | |||
PhaseStyle phase_style_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PhaseModifier) | |||
}; | |||
@@ -0,0 +1,103 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "pitch_detector.h" | |||
#include "synth_constants.h" | |||
#include "wave_frame.h" | |||
#include <climits> | |||
PitchDetector::PitchDetector() { | |||
size_ = 0; | |||
signal_data_ = nullptr; | |||
} | |||
void PitchDetector::loadSignal(const float* signal, int size) { | |||
size_ = size; | |||
signal_data_ = std::make_unique<float[]>(size); | |||
memcpy(signal_data_.get(), signal, sizeof(float) * size); | |||
} | |||
float PitchDetector::getPeriodError(float period) { | |||
static constexpr float kDcDeltaErrorMultiplier = 0.015f; | |||
float error = 0.0f; | |||
int waves = size_ / period - 1; | |||
VITAL_ASSERT(waves > 0); | |||
int points = kNumPoints / waves; | |||
for (int w = 0; w < waves; ++w) { | |||
float total_from = 0.0f; | |||
float total_to = 0.0f; | |||
for (int i = 0; i < points; ++i) { | |||
float first_position = w * period + i * period / points; | |||
float second_position = (w + 1) * period + i * period / points; | |||
int first_index = first_position; | |||
float first_t = first_position - first_index; | |||
float first_from = signal_data_[first_index]; | |||
float first_to = signal_data_[first_index + 1]; | |||
float first_value = vital::utils::interpolate(first_from, first_to, first_t); | |||
total_from += first_value; | |||
int second_index = second_position; | |||
float second_t = second_position - second_index; | |||
float second_from = signal_data_[second_index]; | |||
float second_to = signal_data_[second_index + 1]; | |||
float second_value = vital::utils::interpolate(second_from, second_to, second_t); | |||
total_to += second_value; | |||
float delta = first_value - second_value; | |||
error += delta * delta; | |||
} | |||
float total_diff = total_from - total_to; | |||
error += total_diff * total_diff * kDcDeltaErrorMultiplier; | |||
} | |||
return error; | |||
} | |||
float PitchDetector::findYinPeriod(int max_period) { | |||
constexpr float kMinLength = 300.0f; | |||
float max_length = std::min<float>(size_ / 2.0f, max_period); | |||
float best_error = INT_MAX; | |||
float match = kMinLength; | |||
for (float length = kMinLength; length < max_length; length += 1.0f) { | |||
float error = getPeriodError(length); | |||
if (error < best_error) { | |||
best_error = error; | |||
match = length; | |||
} | |||
} | |||
float best_match = match; | |||
for (float length = match - 1.0f; length <= match + 1.0f; length += 0.1f) { | |||
float error = getPeriodError(length); | |||
if (error < best_error) { | |||
best_error = error; | |||
best_match = length; | |||
} | |||
} | |||
return best_match; | |||
} | |||
float PitchDetector::matchPeriod(int max_period) { | |||
return findYinPeriod(max_period); | |||
} |
@@ -0,0 +1,42 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
class PitchDetector { | |||
public: | |||
static constexpr int kNumPoints = 2520; | |||
PitchDetector(); | |||
void setSize(int size) { size_ = size; } | |||
void loadSignal(const float* signal, int size); | |||
float getPeriodError(float period); | |||
float findYinPeriod(int max_period); | |||
float matchPeriod(int max_period); | |||
const float* data() const { return signal_data_.get(); } | |||
protected: | |||
int size_; | |||
std::unique_ptr<float[]> signal_data_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchDetector) | |||
}; | |||
@@ -0,0 +1,48 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "shepard_tone_source.h" | |||
#include "wavetable_component_factory.h" | |||
ShepardToneSource::ShepardToneSource() { | |||
loop_frame_ = std::make_unique<WaveSourceKeyframe>(); | |||
} | |||
ShepardToneSource::~ShepardToneSource() { } | |||
void ShepardToneSource::render(vital::WaveFrame* wave_frame, float position) { | |||
if (numFrames() == 0) | |||
return; | |||
WaveSourceKeyframe* keyframe = getKeyframe(0); | |||
vital::WaveFrame* key_wave_frame = keyframe->wave_frame(); | |||
vital::WaveFrame* loop_wave_frame = loop_frame_->wave_frame(); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize / 2; ++i) { | |||
loop_wave_frame->frequency_domain[i * 2] = key_wave_frame->frequency_domain[i]; | |||
loop_wave_frame->frequency_domain[i * 2 + 1] = 0.0f; | |||
} | |||
loop_wave_frame->toTimeDomain(); | |||
compute_frame_->setInterpolationMode(interpolation_mode_); | |||
compute_frame_->interpolate(keyframe, loop_frame_.get(), position / (vital::kNumOscillatorWaveFrames - 1.0f)); | |||
wave_frame->copy(compute_frame_->wave_frame()); | |||
} | |||
WavetableComponentFactory::ComponentType ShepardToneSource::getType() { | |||
return WavetableComponentFactory::kShepardToneSource; | |||
} |
@@ -0,0 +1,35 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wave_source.h" | |||
class ShepardToneSource : public WaveSource { | |||
public: | |||
ShepardToneSource(); | |||
virtual ~ShepardToneSource(); | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual bool hasKeyframes() override { return false; } | |||
protected: | |||
std::unique_ptr<WaveSourceKeyframe> loop_frame_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ShepardToneSource) | |||
}; |
@@ -0,0 +1,94 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "slew_limit_modifier.h" | |||
#include "wave_frame.h" | |||
SlewLimitModifier::SlewLimitModifierKeyframe::SlewLimitModifierKeyframe() { | |||
slew_down_run_rise_ = 0.0f; | |||
slew_up_run_rise_ = 0.0f; | |||
} | |||
void SlewLimitModifier::SlewLimitModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const SlewLimitModifierKeyframe* source = dynamic_cast<const SlewLimitModifierKeyframe*>(keyframe); | |||
slew_down_run_rise_ = source->slew_down_run_rise_; | |||
slew_up_run_rise_ = source->slew_up_run_rise_; | |||
} | |||
void SlewLimitModifier::SlewLimitModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const SlewLimitModifierKeyframe* from = dynamic_cast<const SlewLimitModifierKeyframe*>(from_keyframe); | |||
const SlewLimitModifierKeyframe* to = dynamic_cast<const SlewLimitModifierKeyframe*>(to_keyframe); | |||
slew_down_run_rise_ = linearTween(from->slew_down_run_rise_, to->slew_down_run_rise_, t); | |||
slew_up_run_rise_ = linearTween(from->slew_up_run_rise_, to->slew_up_run_rise_, t); | |||
} | |||
void SlewLimitModifier::SlewLimitModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
float min_slew_limit = 1.0f / vital::WaveFrame::kWaveformSize; | |||
float max_up_delta = (2.0f / vital::WaveFrame::kWaveformSize) / std::max(slew_up_run_rise_, min_slew_limit); | |||
float max_down_delta = (2.0f / vital::WaveFrame::kWaveformSize) / std::max(slew_down_run_rise_, min_slew_limit); | |||
float current_value = wave_frame->time_domain[0]; | |||
for (int i = 1; i < 2 * vital::WaveFrame::kWaveformSize; ++i) { | |||
int index = i % vital::WaveFrame::kWaveformSize; | |||
float target_value = wave_frame->time_domain[index]; | |||
float delta = target_value - current_value; | |||
if (delta > 0.0f) | |||
current_value += std::min(delta, max_up_delta); | |||
else | |||
current_value -= std::min(-delta, max_down_delta); | |||
wave_frame->time_domain[index] = current_value; | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
json SlewLimitModifier::SlewLimitModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["up_run_rise"] = slew_up_run_rise_; | |||
data["down_run_rise"] = slew_down_run_rise_; | |||
return data; | |||
} | |||
void SlewLimitModifier::SlewLimitModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
slew_up_run_rise_ = data["up_run_rise"]; | |||
slew_down_run_rise_ = data["down_run_rise"]; | |||
} | |||
WavetableKeyframe* SlewLimitModifier::createKeyframe(int position) { | |||
SlewLimitModifierKeyframe* keyframe = new SlewLimitModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void SlewLimitModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType SlewLimitModifier::getType() { | |||
return WavetableComponentFactory::kSlewLimiter; | |||
} | |||
SlewLimitModifier::SlewLimitModifierKeyframe* SlewLimitModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<SlewLimitModifier::SlewLimitModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,63 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class SlewLimitModifier : public WavetableComponent { | |||
public: | |||
class SlewLimitModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
SlewLimitModifierKeyframe(); | |||
virtual ~SlewLimitModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
float getSlewUpLimit() { return slew_up_run_rise_; } | |||
float getSlewDownLimit() { return slew_down_run_rise_; } | |||
void setSlewUpLimit(float slew_up_limit) { slew_up_run_rise_ = slew_up_limit; } | |||
void setSlewDownLimit(float slew_down_limit) { slew_down_run_rise_ = slew_down_limit; } | |||
protected: | |||
float slew_up_run_rise_; | |||
float slew_down_run_rise_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SlewLimitModifierKeyframe) | |||
}; | |||
SlewLimitModifier() { } | |||
virtual ~SlewLimitModifier() { } | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
SlewLimitModifierKeyframe* getKeyframe(int index); | |||
protected: | |||
SlewLimitModifierKeyframe compute_frame_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SlewLimitModifier) | |||
}; | |||
@@ -0,0 +1,81 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wave_fold_modifier.h" | |||
#include "utils.h" | |||
#include "wave_frame.h" | |||
WaveFoldModifier::WaveFoldModifierKeyframe::WaveFoldModifierKeyframe() { | |||
wave_fold_boost_ = 1.0f; | |||
} | |||
void WaveFoldModifier::WaveFoldModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const WaveFoldModifierKeyframe* source = dynamic_cast<const WaveFoldModifierKeyframe*>(keyframe); | |||
wave_fold_boost_ = source->wave_fold_boost_; | |||
} | |||
void WaveFoldModifier::WaveFoldModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const WaveFoldModifierKeyframe* from = dynamic_cast<const WaveFoldModifierKeyframe*>(from_keyframe); | |||
const WaveFoldModifierKeyframe* to = dynamic_cast<const WaveFoldModifierKeyframe*>(to_keyframe); | |||
wave_fold_boost_ = linearTween(from->wave_fold_boost_, to->wave_fold_boost_, t); | |||
} | |||
void WaveFoldModifier::WaveFoldModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
float max_value = std::max(1.0f, wave_frame->getMaxZeroOffset()); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
float value = vital::utils::clamp(wave_frame->time_domain[i] / max_value, -1.0f, 1.0f); | |||
float adjusted_value = max_value * wave_fold_boost_ * asinf(value); | |||
wave_frame->time_domain[i] = sinf(adjusted_value); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
json WaveFoldModifier::WaveFoldModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["fold_boost"] = wave_fold_boost_; | |||
return data; | |||
} | |||
void WaveFoldModifier::WaveFoldModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
wave_fold_boost_ = data["fold_boost"]; | |||
} | |||
WavetableKeyframe* WaveFoldModifier::createKeyframe(int position) { | |||
WaveFoldModifierKeyframe* keyframe = new WaveFoldModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void WaveFoldModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType WaveFoldModifier::getType() { | |||
return WavetableComponentFactory::kWaveFolder; | |||
} | |||
WaveFoldModifier::WaveFoldModifierKeyframe* WaveFoldModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<WaveFoldModifier::WaveFoldModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,59 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class WaveFoldModifier : public WavetableComponent { | |||
public: | |||
class WaveFoldModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
WaveFoldModifierKeyframe(); | |||
virtual ~WaveFoldModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
float getWaveFoldBoost() { return wave_fold_boost_; } | |||
void setWaveFoldBoost(float boost) { wave_fold_boost_ = boost; } | |||
protected: | |||
float wave_fold_boost_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveFoldModifierKeyframe) | |||
}; | |||
WaveFoldModifier() { } | |||
virtual ~WaveFoldModifier() { } | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
WaveFoldModifierKeyframe* getKeyframe(int index); | |||
protected: | |||
WaveFoldModifierKeyframe compute_frame_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveFoldModifier) | |||
}; | |||
@@ -0,0 +1,126 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wave_line_source.h" | |||
#include "futils.h" | |||
#include "utils.h" | |||
#include "poly_utils.h" | |||
#include "wave_frame.h" | |||
#include "wavetable_component_factory.h" | |||
WaveLineSource::WaveLineSourceKeyframe::WaveLineSourceKeyframe() : | |||
line_generator_(vital::WaveFrame::kWaveformSize) { | |||
pull_power_ = 0.0f; | |||
} | |||
void WaveLineSource::WaveLineSourceKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const WaveLineSourceKeyframe* source = dynamic_cast<const WaveLineSourceKeyframe*>(keyframe); | |||
const LineGenerator* source_generator = source->getLineGenerator(); | |||
line_generator_.setNumPoints(source_generator->getNumPoints()); | |||
line_generator_.setSmooth(source_generator->smooth()); | |||
for (int i = 0; i < line_generator_.getNumPoints(); ++i) { | |||
line_generator_.setPoint(i, source_generator->getPoint(i)); | |||
line_generator_.setPower(i, source_generator->getPower(i)); | |||
} | |||
} | |||
void WaveLineSource::WaveLineSourceKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const WaveLineSourceKeyframe* from = dynamic_cast<const WaveLineSourceKeyframe*>(from_keyframe); | |||
const WaveLineSourceKeyframe* to = dynamic_cast<const WaveLineSourceKeyframe*>(to_keyframe); | |||
VITAL_ASSERT(from->getNumPoints() == to->getNumPoints()); | |||
float relative_power = from->getPullPower() - to->getPullPower(); | |||
float adjusted_t = vital::futils::powerScale(t, relative_power); | |||
const LineGenerator* from_generator = from->getLineGenerator(); | |||
const LineGenerator* to_generator = to->getLineGenerator(); | |||
int num_points = from_generator->getNumPoints(); | |||
line_generator_.setNumPoints(num_points); | |||
line_generator_.setSmooth(from_generator->smooth()); | |||
for (int i = 0; i < num_points; ++i) { | |||
std::pair<float, float> from_point = from_generator->getPoint(i); | |||
std::pair<float, float> to_point = to_generator->getPoint(i); | |||
line_generator_.setPoint(i, { | |||
linearTween(from_point.first, to_point.first, adjusted_t), | |||
linearTween(from_point.second, to_point.second, adjusted_t), | |||
}); | |||
line_generator_.setPower(i, linearTween(from_generator->getPower(i), to_generator->getPower(i), adjusted_t)); | |||
} | |||
} | |||
void WaveLineSource::WaveLineSourceKeyframe::render(vital::WaveFrame* wave_frame) { | |||
line_generator_.render(); | |||
memcpy(wave_frame->time_domain, line_generator_.getBuffer(), vital::WaveFrame::kWaveformSize * sizeof(float)); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame->time_domain[i] = wave_frame->time_domain[i] * 2.0f - 1.0f; | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
json WaveLineSource::WaveLineSourceKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["pull_power"] = pull_power_; | |||
data["line"] = line_generator_.stateToJson(); | |||
return data; | |||
} | |||
void WaveLineSource::WaveLineSourceKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
pull_power_ = 0.0f; | |||
if (data.count("pull_power")) | |||
pull_power_ = data["pull_power"]; | |||
if (data.count("line")) | |||
line_generator_.jsonToState(data["line"]); | |||
} | |||
WavetableKeyframe* WaveLineSource::createKeyframe(int position) { | |||
WaveLineSourceKeyframe* keyframe = new WaveLineSourceKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void WaveLineSource::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType WaveLineSource::getType() { | |||
return WavetableComponentFactory::kLineSource; | |||
} | |||
json WaveLineSource::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["num_points"] = num_points_; | |||
return data; | |||
} | |||
void WaveLineSource::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
setNumPoints(data["num_points"]); | |||
} | |||
void WaveLineSource::setNumPoints(int num_points) { | |||
num_points_ = num_points; | |||
} | |||
WaveLineSource::WaveLineSourceKeyframe* WaveLineSource::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<WaveLineSource::WaveLineSourceKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,78 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class WaveLineSource : public WavetableComponent { | |||
public: | |||
static constexpr int kDefaultLinePoints = 4; | |||
class WaveLineSourceKeyframe : public WavetableKeyframe { | |||
public: | |||
WaveLineSourceKeyframe(); | |||
virtual ~WaveLineSourceKeyframe() = default; | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
inline std::pair<float, float> getPoint(int index) const { return line_generator_.getPoint(index); } | |||
inline float getPower(int index) const { return line_generator_.getPower(index); } | |||
inline void setPoint(std::pair<float, float> point, int index) { line_generator_.setPoint(index, point); } | |||
inline void setPower(float power, int index) { line_generator_.setPower(index, power); } | |||
inline void removePoint(int index) { line_generator_.removePoint(index); } | |||
inline void addMiddlePoint(int index) { line_generator_.addMiddlePoint(index); } | |||
inline int getNumPoints() const { return line_generator_.getNumPoints(); } | |||
inline void setSmooth(bool smooth) { line_generator_.setSmooth(smooth); } | |||
void setPullPower(float power) { pull_power_ = power; } | |||
float getPullPower() const { return pull_power_; } | |||
const LineGenerator* getLineGenerator() const { return &line_generator_; } | |||
LineGenerator* getLineGenerator() { return &line_generator_; } | |||
protected: | |||
LineGenerator line_generator_; | |||
float pull_power_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveLineSourceKeyframe) | |||
}; | |||
WaveLineSource() : num_points_(kDefaultLinePoints), compute_frame_() { } | |||
virtual ~WaveLineSource() = default; | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
void setNumPoints(int num_points); | |||
int numPoints() { return num_points_; } | |||
WaveLineSourceKeyframe* getKeyframe(int index); | |||
protected: | |||
int num_points_; | |||
WaveLineSourceKeyframe compute_frame_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveLineSource) | |||
}; | |||
@@ -0,0 +1,208 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wave_source.h" | |||
#include "wave_frame.h" | |||
#include "wavetable_component_factory.h" | |||
WaveSource::WaveSource() { | |||
compute_frame_ = std::make_unique<WaveSourceKeyframe>(); | |||
interpolation_mode_ = kFrequency; | |||
} | |||
WaveSource::~WaveSource() { } | |||
WavetableKeyframe* WaveSource::createKeyframe(int position) { | |||
WaveSourceKeyframe* keyframe = new WaveSourceKeyframe(); | |||
render(keyframe->wave_frame(), position); | |||
return keyframe; | |||
} | |||
void WaveSource::render(vital::WaveFrame* wave_frame, float position) { | |||
compute_frame_->setInterpolationMode(interpolation_mode_); | |||
interpolate(compute_frame_.get(), position); | |||
wave_frame->copy(compute_frame_->wave_frame()); | |||
} | |||
WavetableComponentFactory::ComponentType WaveSource::getType() { | |||
return WavetableComponentFactory::kWaveSource; | |||
} | |||
json WaveSource::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["interpolation"] = interpolation_mode_; | |||
return data; | |||
} | |||
void WaveSource::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
interpolation_mode_ = data["interpolation"]; | |||
compute_frame_->setInterpolationMode(interpolation_mode_); | |||
} | |||
vital::WaveFrame* WaveSource::getWaveFrame(int index) { | |||
return getKeyframe(index)->wave_frame(); | |||
} | |||
WaveSourceKeyframe* WaveSource::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<WaveSourceKeyframe*>(wavetable_keyframe); | |||
} | |||
void WaveSourceKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const WaveSourceKeyframe* source = dynamic_cast<const WaveSourceKeyframe*>(keyframe); | |||
wave_frame_->copy(source->wave_frame_.get()); | |||
} | |||
void WaveSourceKeyframe::linearTimeInterpolate(const vital::WaveFrame* from, const vital::WaveFrame* to, float t) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame_->time_domain[i] = linearTween(from->time_domain[i], to->time_domain[i], t); | |||
wave_frame_->toFrequencyDomain(); | |||
} | |||
void WaveSourceKeyframe::cubicTimeInterpolate(const vital::WaveFrame* prev, const vital::WaveFrame* from, | |||
const vital::WaveFrame* to, const vital::WaveFrame* next, | |||
float range_prev, float range, float range_next, float t) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
wave_frame_->time_domain[i] = cubicTween(prev->time_domain[i], from->time_domain[i], | |||
to->time_domain[i], next->time_domain[i], | |||
range_prev, range, range_next, t); | |||
} | |||
wave_frame_->toFrequencyDomain(); | |||
} | |||
void WaveSourceKeyframe::linearFrequencyInterpolate(const vital::WaveFrame* from, | |||
const vital::WaveFrame* to, float t) { | |||
for (int i = 0; i < vital::WaveFrame::kNumRealComplex; ++i) { | |||
float amplitude_from = sqrtf(std::abs(from->frequency_domain[i])); | |||
float amplitude_to = sqrtf(std::abs(to->frequency_domain[i])); | |||
float amplitude = linearTween(amplitude_from, amplitude_to, t); | |||
amplitude *= amplitude; | |||
float phase_from = std::arg(from->frequency_domain[i]); | |||
float phase_delta = std::arg(std::conj(from->frequency_domain[i]) * to->frequency_domain[i]); | |||
float phase = phase_from + t * phase_delta; | |||
if (amplitude_from == 0) | |||
phase = std::arg(to->frequency_domain[i]); | |||
wave_frame_->frequency_domain[i] = std::polar(amplitude, phase); | |||
} | |||
float dc_from = from->frequency_domain[0].real(); | |||
float dc_to = to->frequency_domain[0].real(); | |||
wave_frame_->frequency_domain[0] = linearTween(dc_from, dc_to, t); | |||
int last = vital::WaveFrame::kNumRealComplex - 1; | |||
float last_harmonic_from = from->frequency_domain[last].real(); | |||
float last_harmonic_to = to->frequency_domain[last].real(); | |||
wave_frame_->frequency_domain[last] = linearTween(last_harmonic_from, last_harmonic_to, t); | |||
wave_frame_->toTimeDomain(); | |||
} | |||
void WaveSourceKeyframe::cubicFrequencyInterpolate(const vital::WaveFrame* prev, const vital::WaveFrame* from, | |||
const vital::WaveFrame* to, const vital::WaveFrame* next, | |||
float range_prev, float range, float range_next, float t) { | |||
for (int i = 0; i < vital::WaveFrame::kNumRealComplex; ++i) { | |||
float amplitude_prev = sqrtf(std::abs(prev->frequency_domain[i])); | |||
float amplitude_from = sqrtf(std::abs(from->frequency_domain[i])); | |||
float amplitude_to = sqrtf(std::abs(to->frequency_domain[i])); | |||
float amplitude_next = sqrtf(std::abs(next->frequency_domain[i])); | |||
float amplitude = cubicTween(amplitude_prev, amplitude_from, amplitude_to, amplitude_next, | |||
range_prev, range, range_next, t); | |||
amplitude *= amplitude; | |||
float phase_delta_from = std::arg(std::conj(prev->frequency_domain[i]) * from->frequency_domain[i]); | |||
float phase_delta_to = std::arg(std::conj(from->frequency_domain[i]) * to->frequency_domain[i]); | |||
float phase_delta_next = std::arg(std::conj(to->frequency_domain[i]) * next->frequency_domain[i]); | |||
float phase_prev = std::arg(prev->frequency_domain[i]); | |||
float phase_from = phase_prev; | |||
if (amplitude_from) | |||
phase_from += phase_delta_from; | |||
float phase_to = phase_from; | |||
if (amplitude_to) | |||
phase_to += phase_delta_to; | |||
float phase_next = phase_to; | |||
if (amplitude_next) | |||
phase_next += phase_delta_next; | |||
float phase = cubicTween(phase_prev, phase_from, phase_to, phase_next, range_prev, range, range_next, t); | |||
wave_frame_->frequency_domain[i] = std::polar(amplitude, phase); | |||
} | |||
float dc_prev = prev->frequency_domain[0].real(); | |||
float dc_from = from->frequency_domain[0].real(); | |||
float dc_to = to->frequency_domain[0].real(); | |||
float dc_next = next->frequency_domain[0].real(); | |||
wave_frame_->frequency_domain[0] = cubicTween(dc_prev, dc_from, dc_to, dc_next, range_prev, range, range_next, t); | |||
int last = vital::WaveFrame::kNumRealComplex - 1; | |||
float last_harmonic_prev = prev->frequency_domain[last].real(); | |||
float last_harmonic_from = from->frequency_domain[last].real(); | |||
float last_harmonic_to = to->frequency_domain[last].real(); | |||
float last_harmonic_next = next->frequency_domain[last].real(); | |||
wave_frame_->frequency_domain[last] = cubicTween(last_harmonic_prev, last_harmonic_from, | |||
last_harmonic_to, last_harmonic_next, | |||
range_prev, range, range_next, t); | |||
wave_frame_->toTimeDomain(); | |||
} | |||
void WaveSourceKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) { | |||
const WaveSourceKeyframe* from = dynamic_cast<const WaveSourceKeyframe*>(from_keyframe); | |||
const WaveSourceKeyframe* to = dynamic_cast<const WaveSourceKeyframe*>(to_keyframe); | |||
if (interpolation_mode_ == WaveSource::kFrequency) | |||
linearFrequencyInterpolate(from->wave_frame_.get(), to->wave_frame_.get(), t); | |||
else | |||
linearTimeInterpolate(from->wave_frame_.get(), to->wave_frame_.get(), t); | |||
} | |||
void WaveSourceKeyframe::smoothInterpolate(const WavetableKeyframe* prev_keyframe, | |||
const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
const WavetableKeyframe* next_keyframe, float t) { | |||
const vital::WaveFrame* prev = dynamic_cast<const WaveSourceKeyframe*>(prev_keyframe)->wave_frame_.get(); | |||
const vital::WaveFrame* from = dynamic_cast<const WaveSourceKeyframe*>(from_keyframe)->wave_frame_.get(); | |||
const vital::WaveFrame* to = dynamic_cast<const WaveSourceKeyframe*>(to_keyframe)->wave_frame_.get(); | |||
const vital::WaveFrame* next = dynamic_cast<const WaveSourceKeyframe*>(next_keyframe)->wave_frame_.get(); | |||
float range_prev = from_keyframe->position() - prev_keyframe->position(); | |||
float range = to_keyframe->position() - from_keyframe->position(); | |||
float range_next = next_keyframe->position() - to_keyframe->position(); | |||
if (interpolation_mode_ == WaveSource::kFrequency) | |||
cubicFrequencyInterpolate(prev, from, to, next, range_prev, range, range_next, t); | |||
else | |||
cubicTimeInterpolate(prev, from, to, next, range_prev, range, range_next, t); | |||
} | |||
json WaveSourceKeyframe::stateToJson() { | |||
String encoded = Base64::toBase64(wave_frame_->time_domain, sizeof(float) * vital::WaveFrame::kWaveformSize); | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["wave_data"] = encoded.toStdString(); | |||
return data; | |||
} | |||
void WaveSourceKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
MemoryOutputStream decoded(sizeof(float) * vital::WaveFrame::kWaveformSize); | |||
std::string wave_data = data["wave_data"]; | |||
Base64::convertFromBase64(decoded, wave_data); | |||
memcpy(wave_frame_->time_domain, decoded.getData(), sizeof(float) * vital::WaveFrame::kWaveformSize); | |||
wave_frame_->toFrequencyDomain(); | |||
} |
@@ -0,0 +1,97 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
#include "wave_frame.h" | |||
class WaveSourceKeyframe; | |||
class WaveSource : public WavetableComponent { | |||
public: | |||
enum InterpolationMode { | |||
kTime, | |||
kFrequency | |||
}; | |||
WaveSource(); | |||
virtual ~WaveSource(); | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
vital::WaveFrame* getWaveFrame(int index); | |||
WaveSourceKeyframe* getKeyframe(int index); | |||
void setInterpolationMode(InterpolationMode mode) { interpolation_mode_ = mode; } | |||
InterpolationMode getInterpolationMode() const { return interpolation_mode_; } | |||
protected: | |||
std::unique_ptr<WaveSourceKeyframe> compute_frame_; | |||
InterpolationMode interpolation_mode_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveSource) | |||
}; | |||
class WaveSourceKeyframe : public WavetableKeyframe { | |||
public: | |||
WaveSourceKeyframe() : interpolation_mode_(WaveSource::kFrequency) { | |||
wave_frame_ = std::make_unique<vital::WaveFrame>(); | |||
} | |||
virtual ~WaveSourceKeyframe() { } | |||
vital::WaveFrame* wave_frame() { return wave_frame_.get(); } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void linearTimeInterpolate(const vital::WaveFrame* from, const vital::WaveFrame* to, float t); | |||
void cubicTimeInterpolate(const vital::WaveFrame* prev, const vital::WaveFrame* from, | |||
const vital::WaveFrame* to, const vital::WaveFrame* next, | |||
float range_prev, float range, float range_next, float t); | |||
void linearFrequencyInterpolate(const vital::WaveFrame* from, const vital::WaveFrame* to, float t); | |||
void cubicFrequencyInterpolate(const vital::WaveFrame* prev, const vital::WaveFrame* from, | |||
const vital::WaveFrame* to, const vital::WaveFrame* next, | |||
float range_prev, float range, float range_next, float t); | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void smoothInterpolate(const WavetableKeyframe* prev_keyframe, | |||
const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
const WavetableKeyframe* next_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override { | |||
wave_frame->copy(wave_frame_.get()); | |||
} | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
void setInterpolationMode(WaveSource::InterpolationMode mode) { interpolation_mode_ = mode; } | |||
WaveSource::InterpolationMode getInterpolationMode() const { return interpolation_mode_; } | |||
protected: | |||
std::unique_ptr<vital::WaveFrame> wave_frame_; | |||
WaveSource::InterpolationMode interpolation_mode_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveSourceKeyframe) | |||
}; |
@@ -0,0 +1,136 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wave_warp_modifier.h" | |||
#include "futils.h" | |||
#include "wave_frame.h" | |||
#include "utils.h" | |||
namespace { | |||
inline double highResPowerScale(float value, float power) { | |||
static constexpr float kMinPower = 0.01f; | |||
if (fabsf(power) < kMinPower) | |||
return value; | |||
double abs_value = fabsf(value); | |||
double numerator = exp(power * abs_value) - 1.0f; | |||
double denominator = exp(power) - 1.0f; | |||
if (value >= 0.0f) | |||
return numerator / denominator; | |||
return -numerator / denominator; | |||
} | |||
} | |||
WaveWarpModifier::WaveWarpModifierKeyframe::WaveWarpModifierKeyframe() { | |||
horizontal_power_ = 0.0f; | |||
vertical_power_ = 0.0f; | |||
horizontal_asymmetric_ = false; | |||
vertical_asymmetric_ = false; | |||
} | |||
void WaveWarpModifier::WaveWarpModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const WaveWarpModifierKeyframe* source = dynamic_cast<const WaveWarpModifierKeyframe*>(keyframe); | |||
horizontal_power_ = source->horizontal_power_; | |||
vertical_power_ = source->vertical_power_; | |||
} | |||
void WaveWarpModifier::WaveWarpModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const WaveWarpModifierKeyframe* from = dynamic_cast<const WaveWarpModifierKeyframe*>(from_keyframe); | |||
const WaveWarpModifierKeyframe* to = dynamic_cast<const WaveWarpModifierKeyframe*>(to_keyframe); | |||
horizontal_power_ = linearTween(from->horizontal_power_, to->horizontal_power_, t); | |||
vertical_power_ = linearTween(from->vertical_power_, to->vertical_power_, t); | |||
} | |||
void WaveWarpModifier::WaveWarpModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) | |||
wave_frame->frequency_domain[i] = wave_frame->time_domain[i]; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
float horizontal = i / (vital::WaveFrame::kWaveformSize - 1.0f); | |||
float warped_horizontal = 0.0f; | |||
if (horizontal_asymmetric_) | |||
warped_horizontal = highResPowerScale(horizontal, horizontal_power_); | |||
else | |||
warped_horizontal = 0.5f * highResPowerScale(2.0f * horizontal - 1.0f, horizontal_power_) + 0.5f; | |||
float float_index = (vital::WaveFrame::kWaveformSize - 1) * warped_horizontal; | |||
int index = float_index; | |||
index = vital::utils::iclamp(index, 0, vital::WaveFrame::kWaveformSize - 2); | |||
float vertical_from = wave_frame->frequency_domain[index].real(); | |||
float vertical_to = wave_frame->frequency_domain[index + 1].real(); | |||
float vertical = linearTween(vertical_from, vertical_to, float_index - index); | |||
vertical = vital::utils::clamp(vertical, -1.0f, 1.0f); | |||
if (vertical_asymmetric_) | |||
wave_frame->time_domain[i] = 2.0f * highResPowerScale(0.5f * vertical + 0.5f, vertical_power_) - 1.0f; | |||
else | |||
wave_frame->time_domain[i] = highResPowerScale(vertical, vertical_power_); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
json WaveWarpModifier::WaveWarpModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["horizontal_power"] = horizontal_power_; | |||
data["vertical_power"] = vertical_power_; | |||
return data; | |||
} | |||
void WaveWarpModifier::WaveWarpModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
horizontal_power_ = data["horizontal_power"]; | |||
vertical_power_ = data["vertical_power"]; | |||
} | |||
WavetableKeyframe* WaveWarpModifier::createKeyframe(int position) { | |||
WaveWarpModifierKeyframe* keyframe = new WaveWarpModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void WaveWarpModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.setHorizontalAsymmetric(horizontal_asymmetric_); | |||
compute_frame_.setVerticalAsymmetric(vertical_asymmetric_); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType WaveWarpModifier::getType() { | |||
return WavetableComponentFactory::kWaveWarp; | |||
} | |||
json WaveWarpModifier::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["horizontal_asymmetric"] = horizontal_asymmetric_; | |||
data["vertical_asymmetric"] = vertical_asymmetric_; | |||
return data; | |||
} | |||
void WaveWarpModifier::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
horizontal_asymmetric_ = data["horizontal_asymmetric"]; | |||
vertical_asymmetric_ = data["vertical_asymmetric"]; | |||
} | |||
WaveWarpModifier::WaveWarpModifierKeyframe* WaveWarpModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<WaveWarpModifier::WaveWarpModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,79 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class WaveWarpModifier : public WavetableComponent { | |||
public: | |||
class WaveWarpModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
WaveWarpModifierKeyframe(); | |||
virtual ~WaveWarpModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
float getHorizontalPower() { return horizontal_power_; } | |||
float getVerticalPower() { return vertical_power_; } | |||
void setHorizontalPower(float horizontal_power) { horizontal_power_ = horizontal_power; } | |||
void setVerticalPower(float vertical_power) { vertical_power_ = vertical_power; } | |||
void setHorizontalAsymmetric(bool horizontal_asymmetric) { horizontal_asymmetric_ = horizontal_asymmetric; } | |||
void setVerticalAsymmetric(bool vertical_asymmetric) { vertical_asymmetric_ = vertical_asymmetric; } | |||
protected: | |||
float horizontal_power_; | |||
float vertical_power_; | |||
bool horizontal_asymmetric_; | |||
bool vertical_asymmetric_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveWarpModifierKeyframe) | |||
}; | |||
WaveWarpModifier() : horizontal_asymmetric_(false), vertical_asymmetric_(false) { } | |||
virtual ~WaveWarpModifier() = default; | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
void setHorizontalAsymmetric(bool horizontal_asymmetric) { horizontal_asymmetric_ = horizontal_asymmetric; } | |||
void setVerticalAsymmetric(bool vertical_asymmetric) { vertical_asymmetric_ = vertical_asymmetric; } | |||
bool getHorizontalAsymmetric() const { return horizontal_asymmetric_; } | |||
bool getVerticalAsymmetric() const { return vertical_asymmetric_; } | |||
WaveWarpModifierKeyframe* getKeyframe(int index); | |||
protected: | |||
WaveWarpModifierKeyframe compute_frame_; | |||
bool horizontal_asymmetric_; | |||
bool vertical_asymmetric_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveWarpModifier) | |||
}; | |||
@@ -0,0 +1,120 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wave_frame.h" | |||
#include "wave_window_modifier.h" | |||
float WaveWindowModifier::applyWindow(WindowShape window_shape, float t) { | |||
if (window_shape == kCos) | |||
return 0.5f - 0.5f * cosf(vital::kPi * t); | |||
if (window_shape == kHalfSin) | |||
return sinf(vital::kPi * t / 2.0f); | |||
if (window_shape == kSquare) | |||
return t < 1.0f ? 0.0f : 1.0f; | |||
if (window_shape == kWiggle) | |||
return t * cosf(vital::kPi * (t * 1.5f + 0.5f)); | |||
return t; | |||
} | |||
WaveWindowModifier::WaveWindowModifierKeyframe::WaveWindowModifierKeyframe() { | |||
static constexpr float kDefaultOffset = 0.25f; | |||
left_position_ = kDefaultOffset; | |||
right_position_ = 1.0f - kDefaultOffset; | |||
window_shape_ = kCos; | |||
} | |||
void WaveWindowModifier::WaveWindowModifierKeyframe::copy(const WavetableKeyframe* keyframe) { | |||
const WaveWindowModifierKeyframe* source = dynamic_cast<const WaveWindowModifierKeyframe*>(keyframe); | |||
left_position_ = source->left_position_; | |||
right_position_ = source->right_position_; | |||
} | |||
void WaveWindowModifier::WaveWindowModifierKeyframe::interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
float t) { | |||
const WaveWindowModifierKeyframe* from = dynamic_cast<const WaveWindowModifierKeyframe*>(from_keyframe); | |||
const WaveWindowModifierKeyframe* to = dynamic_cast<const WaveWindowModifierKeyframe*>(to_keyframe); | |||
left_position_ = linearTween(from->left_position_, to->left_position_, t); | |||
right_position_ = linearTween(from->right_position_, to->right_position_, t); | |||
} | |||
void WaveWindowModifier::WaveWindowModifierKeyframe::render(vital::WaveFrame* wave_frame) { | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
float t = i / (vital::WaveFrame::kWaveformSize - 1.0f); | |||
if (t >= left_position_) | |||
break; | |||
wave_frame->time_domain[i] *= applyWindow(t / left_position_); | |||
} | |||
for (int i = vital::WaveFrame::kWaveformSize; i >= 0; --i) { | |||
float t = i / (vital::WaveFrame::kWaveformSize - 1.0f); | |||
if (t <= right_position_) | |||
break; | |||
wave_frame->time_domain[i] *= applyWindow((1.0f - t) / (1.0f - right_position_)); | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
} | |||
json WaveWindowModifier::WaveWindowModifierKeyframe::stateToJson() { | |||
json data = WavetableKeyframe::stateToJson(); | |||
data["left_position"] = left_position_; | |||
data["right_position"] = right_position_; | |||
return data; | |||
} | |||
void WaveWindowModifier::WaveWindowModifierKeyframe::jsonToState(json data) { | |||
WavetableKeyframe::jsonToState(data); | |||
left_position_ = data["left_position"]; | |||
right_position_ = data["right_position"]; | |||
} | |||
WavetableKeyframe* WaveWindowModifier::createKeyframe(int position) { | |||
WaveWindowModifierKeyframe* keyframe = new WaveWindowModifierKeyframe(); | |||
interpolate(keyframe, position); | |||
return keyframe; | |||
} | |||
void WaveWindowModifier::render(vital::WaveFrame* wave_frame, float position) { | |||
interpolate(&compute_frame_, position); | |||
compute_frame_.setWindowShape(window_shape_); | |||
compute_frame_.render(wave_frame); | |||
} | |||
WavetableComponentFactory::ComponentType WaveWindowModifier::getType() { | |||
return WavetableComponentFactory::kWaveWindow; | |||
} | |||
json WaveWindowModifier::stateToJson() { | |||
json data = WavetableComponent::stateToJson(); | |||
data["window_shape"] = window_shape_; | |||
return data; | |||
} | |||
void WaveWindowModifier::jsonToState(json data) { | |||
WavetableComponent::jsonToState(data); | |||
window_shape_ = data["window_shape"]; | |||
} | |||
WaveWindowModifier::WaveWindowModifierKeyframe* WaveWindowModifier::getKeyframe(int index) { | |||
WavetableKeyframe* wavetable_keyframe = keyframes_[index].get(); | |||
return dynamic_cast<WaveWindowModifier::WaveWindowModifierKeyframe*>(wavetable_keyframe); | |||
} |
@@ -0,0 +1,84 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_component.h" | |||
class WaveWindowModifier : public WavetableComponent { | |||
public: | |||
enum WindowShape { | |||
kCos, | |||
kHalfSin, | |||
kLinear, | |||
kSquare, | |||
kWiggle, | |||
kNumWindowShapes | |||
}; | |||
static float applyWindow(WindowShape window_shape, float t); | |||
class WaveWindowModifierKeyframe : public WavetableKeyframe { | |||
public: | |||
WaveWindowModifierKeyframe(); | |||
virtual ~WaveWindowModifierKeyframe() { } | |||
void copy(const WavetableKeyframe* keyframe) override; | |||
void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) override; | |||
void render(vital::WaveFrame* wave_frame) override; | |||
json stateToJson() override; | |||
void jsonToState(json data) override; | |||
void setLeft(float left) { left_position_ = left; } | |||
void setRight(float right) { right_position_ = right; } | |||
float getLeft() { return left_position_; } | |||
float getRight() { return right_position_; } | |||
void setWindowShape(WindowShape window_shape) { window_shape_ = window_shape; } | |||
protected: | |||
inline float applyWindow(float t) { return WaveWindowModifier::applyWindow(window_shape_, t); } | |||
float left_position_; | |||
float right_position_; | |||
WindowShape window_shape_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveWindowModifierKeyframe) | |||
}; | |||
WaveWindowModifier() : window_shape_(kCos) { } | |||
virtual ~WaveWindowModifier() { } | |||
virtual WavetableKeyframe* createKeyframe(int position) override; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) override; | |||
virtual WavetableComponentFactory::ComponentType getType() override; | |||
virtual json stateToJson() override; | |||
virtual void jsonToState(json data) override; | |||
WaveWindowModifierKeyframe* getKeyframe(int index); | |||
void setWindowShape(WindowShape window_shape) { window_shape_ = window_shape; } | |||
WindowShape getWindowShape() { return window_shape_; } | |||
protected: | |||
WaveWindowModifierKeyframe compute_frame_; | |||
WindowShape window_shape_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveWindowModifier) | |||
}; | |||
@@ -0,0 +1,134 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wavetable_component.h" | |||
WavetableKeyframe* WavetableComponent::insertNewKeyframe(int position) { | |||
VITAL_ASSERT(position >= 0 && position < vital::kNumOscillatorWaveFrames); | |||
WavetableKeyframe* keyframe = createKeyframe(position); | |||
keyframe->setOwner(this); | |||
keyframe->setPosition(position); | |||
int index = getIndexFromPosition(position); | |||
keyframes_.insert(keyframes_.begin() + index, std::unique_ptr<WavetableKeyframe>(keyframe)); | |||
return keyframe; | |||
} | |||
void WavetableComponent::reposition(WavetableKeyframe* keyframe) { | |||
int start_index = indexOf(keyframe); | |||
keyframes_[start_index].release(); | |||
keyframes_.erase(keyframes_.begin() + start_index); | |||
int new_index = getIndexFromPosition(keyframe->position()); | |||
keyframes_.insert(keyframes_.begin() + new_index, std::unique_ptr<WavetableKeyframe>(keyframe)); | |||
} | |||
void WavetableComponent::remove(WavetableKeyframe* keyframe) { | |||
int start_index = indexOf(keyframe); | |||
keyframes_.erase(keyframes_.begin() + start_index); | |||
} | |||
void WavetableComponent::jsonToState(json data) { | |||
keyframes_.clear(); | |||
for (json json_keyframe : data["keyframes"]) { | |||
WavetableKeyframe* keyframe = insertNewKeyframe(json_keyframe["position"]); | |||
keyframe->jsonToState(json_keyframe); | |||
} | |||
if (data.count("interpolation_style")) | |||
interpolation_style_ = data["interpolation_style"]; | |||
} | |||
json WavetableComponent::stateToJson() { | |||
json keyframes_data; | |||
for (int i = 0; i < keyframes_.size(); ++i) | |||
keyframes_data.emplace_back(keyframes_[i]->stateToJson()); | |||
return { | |||
{ "keyframes", keyframes_data }, | |||
{ "type", WavetableComponentFactory::getComponentName(getType()) }, | |||
{ "interpolation_style", interpolation_style_ } | |||
}; | |||
} | |||
void WavetableComponent::reset() { | |||
keyframes_.clear(); | |||
insertNewKeyframe(0); | |||
} | |||
void WavetableComponent::interpolate(WavetableKeyframe* dest, float position) { | |||
if (numFrames() == 0) | |||
return; | |||
int index = getIndexFromPosition(position) - 1; | |||
int clamped_index = std::min(std::max(index, 0), numFrames() - 1); | |||
WavetableKeyframe* from_frame = keyframes_[clamped_index].get(); | |||
if (index < 0 || index >= numFrames() - 1 || interpolation_style_ == kNone) | |||
dest->copy(from_frame); | |||
else if (interpolation_style_ == kLinear) { | |||
WavetableKeyframe* to_frame = keyframes_[index + 1].get(); | |||
int from_position = keyframes_[index]->position(); | |||
int to_position = keyframes_[index + 1]->position(); | |||
float t = (1.0f * position - from_position) / (to_position - from_position); | |||
dest->interpolate(from_frame, to_frame, t); | |||
} | |||
else if (interpolation_style_ == kCubic) { | |||
int next_index = index + 2; | |||
int prev_index = index - 1; | |||
if (next_index >= numFrames()) | |||
next_index = index; | |||
if (prev_index < 0) | |||
prev_index = index + 1; | |||
WavetableKeyframe* to_frame = keyframes_[index + 1].get(); | |||
WavetableKeyframe* next_frame = keyframes_[next_index].get(); | |||
WavetableKeyframe* prev_frame = keyframes_[prev_index].get(); | |||
int from_position = keyframes_[index]->position(); | |||
int to_position = keyframes_[index + 1]->position(); | |||
float t = (1.0f * position - from_position) / (to_position - from_position); | |||
dest->smoothInterpolate(prev_frame, from_frame, to_frame, next_frame, t); | |||
} | |||
} | |||
int WavetableComponent::getIndexFromPosition(int position) const { | |||
int index = 0; | |||
for (auto& keyframe : keyframes_) { | |||
if (position < keyframe->position()) | |||
break; | |||
index++; | |||
} | |||
return index; | |||
} | |||
WavetableKeyframe* WavetableComponent::getFrameAtPosition(int position) { | |||
int index = getIndexFromPosition(position); | |||
if (index < 0 || index >= keyframes_.size()) | |||
return nullptr; | |||
return keyframes_[index].get(); | |||
} | |||
int WavetableComponent::getLastKeyframePosition() { | |||
if (keyframes_.size() == 0) | |||
return 0; | |||
if (!hasKeyframes()) | |||
return vital::kNumOscillatorWaveFrames - 1; | |||
return keyframes_[keyframes_.size() - 1]->position(); | |||
} |
@@ -0,0 +1,78 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wavetable_keyframe.h" | |||
#include "wavetable_component_factory.h" | |||
#include "json/json.h" | |||
using json = nlohmann::json; | |||
namespace vital { | |||
class WaveFrame; | |||
} // namespace vital | |||
class WavetableComponent { | |||
public: | |||
enum InterpolationStyle { | |||
kNone, | |||
kLinear, | |||
kCubic, | |||
kNumInterpolationStyles | |||
}; | |||
WavetableComponent() : interpolation_style_(kLinear) { } | |||
virtual ~WavetableComponent() { } | |||
virtual WavetableKeyframe* createKeyframe(int position) = 0; | |||
virtual void render(vital::WaveFrame* wave_frame, float position) = 0; | |||
virtual WavetableComponentFactory::ComponentType getType() = 0; | |||
virtual json stateToJson(); | |||
virtual void jsonToState(json data); | |||
virtual void prerender() { } | |||
virtual bool hasKeyframes() { return true; } | |||
void reset(); | |||
void interpolate(WavetableKeyframe* dest, float position); | |||
WavetableKeyframe* insertNewKeyframe(int position); | |||
void reposition(WavetableKeyframe* keyframe); | |||
void remove(WavetableKeyframe* keyframe); | |||
inline int numFrames() const { return static_cast<int>(keyframes_.size()); } | |||
inline int indexOf(WavetableKeyframe* keyframe) const { | |||
for (int i = 0; i < keyframes_.size(); ++i) { | |||
if (keyframes_[i].get() == keyframe) | |||
return i; | |||
} | |||
return -1; | |||
} | |||
inline WavetableKeyframe* getFrameAt(int index) const { return keyframes_[index].get(); } | |||
int getIndexFromPosition(int position) const; | |||
WavetableKeyframe* getFrameAtPosition(int position); | |||
int getLastKeyframePosition(); | |||
void setInterpolationStyle(InterpolationStyle type) { interpolation_style_ = type; } | |||
InterpolationStyle getInterpolationStyle() const { return interpolation_style_; } | |||
protected: | |||
std::vector<std::unique_ptr<WavetableKeyframe>> keyframes_; | |||
InterpolationStyle interpolation_style_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WavetableComponent) | |||
}; | |||
@@ -0,0 +1,109 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wavetable_component_factory.h" | |||
#include "file_source.h" | |||
#include "frequency_filter_modifier.h" | |||
#include "phase_modifier.h" | |||
#include "shepard_tone_source.h" | |||
#include "slew_limit_modifier.h" | |||
#include "wave_frame.h" | |||
#include "wave_fold_modifier.h" | |||
#include "wave_line_source.h" | |||
#include "wave_source.h" | |||
#include "wave_warp_modifier.h" | |||
#include "wave_window_modifier.h" | |||
WavetableComponent* WavetableComponentFactory::createComponent(ComponentType type) { | |||
switch (type) { | |||
case kWaveSource: | |||
return new WaveSource(); | |||
case kLineSource: | |||
return new WaveLineSource(); | |||
case kFileSource: | |||
return new FileSource(); | |||
case kShepardToneSource: | |||
return new ShepardToneSource(); | |||
case kPhaseModifier: | |||
return new PhaseModifier(); | |||
case kWaveWindow: | |||
return new WaveWindowModifier(); | |||
case kFrequencyFilter: | |||
return new FrequencyFilterModifier(); | |||
case kSlewLimiter: | |||
return new SlewLimitModifier(); | |||
case kWaveFolder: | |||
return new WaveFoldModifier(); | |||
case kWaveWarp: | |||
return new WaveWarpModifier(); | |||
default: | |||
VITAL_ASSERT(false); | |||
return nullptr; | |||
} | |||
} | |||
WavetableComponent* WavetableComponentFactory::createComponent(const std::string& type) { | |||
if (type == "Wave Source") | |||
return new WaveSource(); | |||
if (type == "Line Source") | |||
return new WaveLineSource(); | |||
if (type == "Audio File Source") | |||
return new FileSource(); | |||
if (type == "Shepard Tone Source") | |||
return new ShepardToneSource(); | |||
if (type == "Phase Shift") | |||
return new PhaseModifier(); | |||
if (type == "Wave Window") | |||
return new WaveWindowModifier(); | |||
if (type == "Frequency Filter") | |||
return new FrequencyFilterModifier(); | |||
if (type == "Slew Limiter") | |||
return new SlewLimitModifier(); | |||
if (type == "Wave Folder") | |||
return new WaveFoldModifier(); | |||
if (type == "Wave Warp") | |||
return new WaveWarpModifier(); | |||
VITAL_ASSERT(false); | |||
return nullptr; | |||
} | |||
std::string WavetableComponentFactory::getComponentName(ComponentType type) { | |||
switch (type) { | |||
case kWaveSource: | |||
return "Wave Source"; | |||
case kLineSource: | |||
return "Line Source"; | |||
case kFileSource: | |||
return "Audio File Source"; | |||
case kShepardToneSource: | |||
return "Shepard Tone Source"; | |||
case kPhaseModifier: | |||
return "Phase Shift"; | |||
case kWaveWindow: | |||
return "Wave Window"; | |||
case kFrequencyFilter: | |||
return "Frequency Filter"; | |||
case kSlewLimiter: | |||
return "Slew Limiter"; | |||
case kWaveFolder: | |||
return "Wave Folder"; | |||
case kWaveWarp: | |||
return "Wave Warp"; | |||
default: | |||
return ""; | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
class WavetableComponent; | |||
class WavetableComponentFactory { | |||
public: | |||
enum ComponentType { | |||
kWaveSource, | |||
kLineSource, | |||
kFileSource, | |||
kNumSourceTypes, | |||
kShepardToneSource = kNumSourceTypes, // Deprecated | |||
kBeginModifierTypes = kNumSourceTypes + 1, | |||
kPhaseModifier = kBeginModifierTypes, | |||
kWaveWindow, | |||
kFrequencyFilter, | |||
kSlewLimiter, | |||
kWaveFolder, | |||
kWaveWarp, | |||
kNumComponentTypes | |||
}; | |||
static int numComponentTypes() { return kNumComponentTypes; } | |||
static int numSourceTypes() { return kNumSourceTypes; } | |||
static int numModifierTypes() { return kNumComponentTypes - kBeginModifierTypes; } | |||
static WavetableComponent* createComponent(ComponentType type); | |||
static WavetableComponent* createComponent(const std::string& type); | |||
static std::string getComponentName(ComponentType type); | |||
static ComponentType getSourceType(int type) { return static_cast<ComponentType>(type); } | |||
static ComponentType getModifierType(int type) { | |||
return (ComponentType)(type + kBeginModifierTypes); | |||
} | |||
private: | |||
WavetableComponentFactory() { } | |||
}; | |||
@@ -0,0 +1,516 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wavetable_creator.h" | |||
#include "line_generator.h" | |||
#include "load_save.h" | |||
#include "synth_constants.h" | |||
#include "wave_frame.h" | |||
#include "wave_line_source.h" | |||
#include "wave_source.h" | |||
#include "wavetable.h" | |||
namespace { | |||
int getFirstNonZeroSample(const float* audio_buffer, int num_samples) { | |||
for (int i = 0; i < num_samples; ++i) { | |||
if (audio_buffer[i]) | |||
return i; | |||
} | |||
return 0; | |||
} | |||
} | |||
int WavetableCreator::getGroupIndex(WavetableGroup* group) { | |||
for (int i = 0; i < groups_.size(); ++i) { | |||
if (groups_[i].get() == group) | |||
return i; | |||
} | |||
return -1; | |||
} | |||
void WavetableCreator::moveUp(int index) { | |||
if (index <= 0) | |||
return; | |||
groups_[index].swap(groups_[index - 1]); | |||
} | |||
void WavetableCreator::moveDown(int index) { | |||
if (index < 0 || index >= groups_.size() - 1) | |||
return; | |||
groups_[index].swap(groups_[index + 1]); | |||
} | |||
void WavetableCreator::removeGroup(int index) { | |||
if (index < 0 || index >= groups_.size()) | |||
return; | |||
std::unique_ptr<WavetableGroup> group = std::move(groups_[index]); | |||
groups_.erase(groups_.begin() + index); | |||
} | |||
float WavetableCreator::render(int position) { | |||
compute_frame_combine_.clear(); | |||
compute_frame_combine_.index = position; | |||
compute_frame_.index = position; | |||
for (auto& group : groups_) { | |||
group->render(&compute_frame_, position); | |||
compute_frame_combine_.addFrom(&compute_frame_); | |||
} | |||
if (groups_.size() > 1) | |||
compute_frame_combine_.multiply(1.0f / groups_.size()); | |||
if (remove_all_dc_) | |||
compute_frame_combine_.removedDc(); | |||
float max_value = 0.0f; | |||
float min_value = 0.0f; | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
max_value = std::max(compute_frame_combine_.time_domain[i], max_value); | |||
min_value = std::min(compute_frame_combine_.time_domain[i], min_value); | |||
} | |||
wavetable_->loadWaveFrame(&compute_frame_combine_); | |||
return max_value - min_value; | |||
} | |||
void WavetableCreator::render() { | |||
int last_waveframe = 0; | |||
bool shepard = groups_.size() > 0; | |||
for (auto& group : groups_) { | |||
group->prerender(); | |||
last_waveframe = std::max(last_waveframe, group->getLastKeyframePosition()); | |||
shepard = shepard && group->isShepardTone(); | |||
} | |||
wavetable_->setNumFrames(last_waveframe + 1); | |||
wavetable_->setShepardTable(shepard); | |||
float max_span = 0.0f; | |||
for (int i = 0; i < last_waveframe + 1; ++i) | |||
max_span = std::max(render(i), max_span); | |||
wavetable_->setFrequencyRatio(compute_frame_.frequency_ratio); | |||
wavetable_->setSampleRate(compute_frame_.sample_rate); | |||
postRender(max_span); | |||
} | |||
void WavetableCreator::postRender(float max_span) { | |||
if (full_normalize_) | |||
wavetable_->postProcess(max_span); | |||
else | |||
wavetable_->postProcess(0.0f); | |||
} | |||
void WavetableCreator::renderToBuffer(float* buffer, int num_frames, int frame_size) { | |||
int total_samples = num_frames * frame_size; | |||
for (int i = 0; i < num_frames; ++i) { | |||
float position = (1.0f * i * vital::kNumOscillatorWaveFrames) / num_frames; | |||
compute_frame_combine_.clear(); | |||
compute_frame_combine_.index = position; | |||
compute_frame_.index = position; | |||
for (auto& group : groups_) { | |||
group->render(&compute_frame_, position); | |||
compute_frame_combine_.addFrom(&compute_frame_); | |||
} | |||
float* output_buffer = buffer + (i * frame_size); | |||
if (frame_size != vital::WaveFrame::kWaveformSize) | |||
VITAL_ASSERT(false); // TODO: support different waveframe size. | |||
else { | |||
for (int s = 0; s < vital::WaveFrame::kWaveformSize; ++s) | |||
output_buffer[s] = compute_frame_combine_.time_domain[s]; | |||
} | |||
} | |||
float max_value = 1.0f; | |||
for (int i = 0; i < total_samples; ++i) | |||
max_value = std::max(max_value, fabsf(buffer[i])); | |||
float scale = 1.0f / max_value; | |||
for (int i = 0; i < total_samples; ++i) | |||
buffer[i] *= scale; | |||
} | |||
void WavetableCreator::init() { | |||
clear(); | |||
loadDefaultCreator(); | |||
render(); | |||
} | |||
void WavetableCreator::clear() { | |||
groups_.clear(); | |||
remove_all_dc_ = true; | |||
full_normalize_ = true; | |||
} | |||
void WavetableCreator::loadDefaultCreator() { | |||
wavetable_->setName("Init"); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
new_group->loadDefaultGroup(); | |||
addGroup(new_group); | |||
} | |||
void WavetableCreator::initPredefinedWaves() { | |||
clear(); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
WaveSource* wave_source = new WaveSource(); | |||
int num_shapes = vital::PredefinedWaveFrames::kNumShapes; | |||
for (int i = 0; i < num_shapes; ++i) { | |||
int position = (vital::kNumOscillatorWaveFrames * i) / num_shapes; | |||
wave_source->insertNewKeyframe(position); | |||
WaveSourceKeyframe* keyframe = wave_source->getKeyframe(i); | |||
vital::PredefinedWaveFrames::Shape shape = static_cast<vital::PredefinedWaveFrames::Shape>(i); | |||
keyframe->wave_frame()->copy(vital::PredefinedWaveFrames::getWaveFrame(shape)); | |||
} | |||
wave_source->setInterpolationStyle(WaveSource::kNone); | |||
full_normalize_ = false; | |||
remove_all_dc_ = false; | |||
new_group->addComponent(wave_source); | |||
addGroup(new_group); | |||
render(); | |||
} | |||
void WavetableCreator::initFromAudioFile(const float* audio_buffer, int num_samples, int sample_rate, | |||
AudioFileLoadStyle load_style, FileSource::FadeStyle fade_style) { | |||
int beginning_sample = getFirstNonZeroSample(audio_buffer, num_samples); | |||
int shortened_num_samples = num_samples - beginning_sample; | |||
if (load_style == kVocoded) | |||
initFromVocodedAudioFile(audio_buffer + beginning_sample, shortened_num_samples, sample_rate, false); | |||
else if (load_style == kTtwt) | |||
initFromVocodedAudioFile(audio_buffer + beginning_sample, shortened_num_samples, sample_rate, true); | |||
else if (load_style == kPitched) | |||
initFromPitchedAudioFile(audio_buffer + beginning_sample, shortened_num_samples, sample_rate); | |||
else | |||
initFromSplicedAudioFile(audio_buffer, num_samples, sample_rate, fade_style); | |||
} | |||
void WavetableCreator::initFromSplicedAudioFile(const float* audio_buffer, int num_samples, int sample_rate, | |||
FileSource::FadeStyle fade_style) { | |||
clear(); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
FileSource* file_source = new FileSource(); | |||
file_source->loadBuffer(audio_buffer, num_samples, sample_rate); | |||
file_source->setFadeStyle(fade_style); | |||
file_source->setPhaseStyle(FileSource::PhaseStyle::kNone); | |||
file_source->insertNewKeyframe(0); | |||
file_source->detectWaveEditTable(); | |||
double window_size = file_source->getWindowSize(); | |||
if (fade_style == FileSource::kNoInterpolate) { | |||
int num_cycles = std::max<int>(1, num_samples / window_size); | |||
int buffer_frames = vital::kNumOscillatorWaveFrames / num_cycles; | |||
file_source->insertNewKeyframe(std::max(0, vital::kNumOscillatorWaveFrames - 1 - buffer_frames)); | |||
} | |||
else | |||
file_source->insertNewKeyframe(vital::kNumOscillatorWaveFrames - 1); | |||
file_source->getKeyframe(0)->setStartPosition(0); | |||
int last_sample_position = num_samples - window_size; | |||
last_sample_position = std::min<int>(file_source->getKeyframe(1)->position() * window_size, last_sample_position); | |||
file_source->getKeyframe(1)->setStartPosition(std::max(0, last_sample_position)); | |||
new_group->addComponent(file_source); | |||
addGroup(new_group); | |||
render(); | |||
} | |||
void WavetableCreator::initFromVocodedAudioFile(const float* audio_buffer, int num_samples, | |||
int sample_rate, bool ttwt) { | |||
static constexpr float kMaxTTWTPeriod = .02f; | |||
clear(); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
FileSource* file_source = new FileSource(); | |||
file_source->loadBuffer(audio_buffer, num_samples, sample_rate); | |||
if (ttwt) | |||
file_source->detectPitch(kMaxTTWTPeriod * sample_rate); | |||
else | |||
file_source->detectPitch(); | |||
file_source->setFadeStyle(FileSource::FadeStyle::kWaveBlend); | |||
file_source->setPhaseStyle(FileSource::PhaseStyle::kVocode); | |||
file_source->insertNewKeyframe(0); | |||
file_source->insertNewKeyframe(vital::kNumOscillatorWaveFrames - 1); | |||
file_source->getKeyframe(0)->setStartPosition(0); | |||
int samples_needed = file_source->getKeyframe(1)->getSamplesNeeded(); | |||
file_source->getKeyframe(1)->setStartPosition(num_samples - samples_needed); | |||
new_group->addComponent(file_source); | |||
addGroup(new_group); | |||
render(); | |||
} | |||
void WavetableCreator::initFromPitchedAudioFile(const float* audio_buffer, int num_samples, int sample_rate) { | |||
clear(); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
FileSource* file_source = new FileSource(); | |||
file_source->loadBuffer(audio_buffer, num_samples, sample_rate); | |||
file_source->detectPitch(); | |||
file_source->setFadeStyle(FileSource::FadeStyle::kWaveBlend); | |||
file_source->insertNewKeyframe(0); | |||
file_source->insertNewKeyframe(vital::kNumOscillatorWaveFrames - 1); | |||
file_source->getKeyframe(0)->setStartPosition(0); | |||
int samples_needed = file_source->getKeyframe(1)->getSamplesNeeded(); | |||
file_source->getKeyframe(1)->setStartPosition(num_samples - samples_needed); | |||
new_group->addComponent(file_source); | |||
addGroup(new_group); | |||
render(); | |||
} | |||
void WavetableCreator::initFromLineGenerator(LineGenerator* line_generator) { | |||
clear(); | |||
wavetable_->setName(line_generator->getName()); | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
WaveLineSource* line_source = new WaveLineSource(); | |||
line_source->insertNewKeyframe(0); | |||
WaveLineSource::WaveLineSourceKeyframe* keyframe = line_source->getKeyframe(0); | |||
keyframe->getLineGenerator()->jsonToState(line_generator->stateToJson()); | |||
new_group->addComponent(line_source); | |||
addGroup(new_group); | |||
render(); | |||
} | |||
bool WavetableCreator::isValidJson(json data) { | |||
if (LineGenerator::isValidJson(data)) | |||
return true; | |||
if (!data.count("version") || !data.count("groups") || !data.count("name")) | |||
return false; | |||
json groups_data = data["groups"]; | |||
return groups_data.is_array(); | |||
} | |||
json WavetableCreator::updateJson(json data) { | |||
std::string version = "0.0.0"; | |||
if (data.count("version")) { | |||
std::string ver = data["version"]; | |||
version = ver; | |||
} | |||
if (LoadSave::compareVersionStrings(version, "0.3.3") < 0) { | |||
const std::string kOldOrder[] = { | |||
"Wave Source", "Line Source", "Audio File Source", "Phase Shift", "Wave Window", | |||
"Frequency Filter", "Slew Limiter", "Wave Folder", "Wave Warp" | |||
}; | |||
json json_groups = data["groups"]; | |||
json new_groups; | |||
for (json json_group : json_groups) { | |||
json json_components = json_group["components"]; | |||
json new_components; | |||
for (json json_component : json_components) { | |||
int int_type = json_component["type"]; | |||
json_component["type"] = kOldOrder[int_type]; | |||
new_components.push_back(json_component); | |||
} | |||
json_group["components"] = new_components; | |||
new_groups.push_back(json_group); | |||
} | |||
data["groups"] = new_groups; | |||
} | |||
if (LoadSave::compareVersionStrings(version, "0.3.7") < 0) { | |||
json json_groups = data["groups"]; | |||
json new_groups; | |||
for (json json_group : json_groups) { | |||
json json_components = json_group["components"]; | |||
json new_components; | |||
for (json json_component : json_components) { | |||
std::string type = json_component["type"]; | |||
if (type == "Audio File Source") | |||
LoadSave::convertBufferToPcm(json_component, "audio_file"); | |||
new_components.push_back(json_component); | |||
} | |||
json_group["components"] = new_components; | |||
new_groups.push_back(json_group); | |||
} | |||
data["groups"] = new_groups; | |||
} | |||
if (LoadSave::compareVersionStrings(version, "0.3.8") < 0) | |||
data["remove_all_dc"] = false; | |||
if (LoadSave::compareVersionStrings(version, "0.3.9") < 0 && LoadSave::compareVersionStrings(version, "0.3.7") >= 0) { | |||
json json_groups = data["groups"]; | |||
json new_groups; | |||
for (json json_group : json_groups) { | |||
json json_components = json_group["components"]; | |||
json new_components; | |||
for (json json_component : json_components) { | |||
std::string type = json_component["type"]; | |||
if (type == "Wave Source" || type == "Shepard Tone Source") { | |||
json old_keyframes = json_component["keyframes"]; | |||
json new_keyframes; | |||
for (json json_keyframe : old_keyframes) { | |||
LoadSave::convertPcmToFloatBuffer(json_keyframe, "wave_data"); | |||
new_keyframes.push_back(json_keyframe); | |||
} | |||
json_component["keyframes"] = new_keyframes; | |||
} | |||
new_components.push_back(json_component); | |||
} | |||
json_group["components"] = new_components; | |||
new_groups.push_back(json_group); | |||
} | |||
data["groups"] = new_groups; | |||
} | |||
if (LoadSave::compareVersionStrings(version, "0.4.7") < 0) | |||
data["full_normalize"] = false; | |||
if (LoadSave::compareVersionStrings(version, "0.7.7") < 0) { | |||
LineGenerator line_converter; | |||
json json_groups = data["groups"]; | |||
json new_groups; | |||
for (json json_group : json_groups) { | |||
json json_components = json_group["components"]; | |||
json new_components; | |||
for (json json_component : json_components) { | |||
std::string type = json_component["type"]; | |||
if (type == "Line Source") { | |||
json old_keyframes = json_component["keyframes"]; | |||
int num_points = json_component["num_points"]; | |||
json_component["num_points"] = num_points + 2; | |||
line_converter.setNumPoints(num_points + 2); | |||
json new_keyframes; | |||
for (json json_keyframe : old_keyframes) { | |||
json point_data = json_keyframe["points"]; | |||
json power_data = json_keyframe["powers"]; | |||
for (int i = 0; i < num_points; ++i) { | |||
float x = point_data[2 * i]; | |||
float y = point_data[2 * i + 1]; | |||
line_converter.setPoint(i + 1, { x, y * 0.5f + 0.5f }); | |||
line_converter.setPower(i + 1, power_data[i]); | |||
} | |||
float start_x = point_data[0]; | |||
float start_y = point_data[1]; | |||
float end_x = point_data[2 * (num_points - 1)]; | |||
float end_y = point_data[2 * (num_points - 1) + 1]; | |||
float range_x = start_x - end_x + 1.0f; | |||
float y = end_y; | |||
if (range_x < 0.001f) | |||
y = 0.5f * (start_y + end_y); | |||
else { | |||
float t = (1.0f - end_x) / range_x; | |||
y = vital::utils::interpolate(end_y, start_y, t); | |||
} | |||
line_converter.setPoint(0, { 0.0f, y * 0.5f + 0.5f }); | |||
line_converter.setPoint(num_points + 1, { 1.0f, y * 0.5f + 0.5f }); | |||
line_converter.setPower(0, 0.0f); | |||
line_converter.setPower(num_points + 1, 0.0f); | |||
json_keyframe["line"] = line_converter.stateToJson(); | |||
new_keyframes.push_back(json_keyframe); | |||
} | |||
json_component["keyframes"] = new_keyframes; | |||
} | |||
new_components.push_back(json_component); | |||
} | |||
json_group["components"] = new_components; | |||
new_groups.push_back(json_group); | |||
} | |||
data["groups"] = new_groups; | |||
} | |||
return data; | |||
} | |||
json WavetableCreator::stateToJson() { | |||
json json_groups; | |||
for (auto& group : groups_) | |||
json_groups.push_back(group->stateToJson()); | |||
return { | |||
{ "groups", json_groups }, | |||
{ "name", wavetable_->getName() }, | |||
{ "author", wavetable_->getAuthor() }, | |||
{ "version", ProjectInfo::versionString }, | |||
{ "remove_all_dc", remove_all_dc_ }, | |||
{ "full_normalize", full_normalize_ }, | |||
}; | |||
} | |||
void WavetableCreator::jsonToState(json data) { | |||
if (LineGenerator::isValidJson(data)) { | |||
LineGenerator generator(vital::WaveFrame::kWaveformSize); | |||
generator.jsonToState(data); | |||
initFromLineGenerator(&generator); | |||
return; | |||
} | |||
clear(); | |||
data = updateJson(data); | |||
std::string name = ""; | |||
if (data.count("name")) | |||
name = data["name"].get<std::string>(); | |||
wavetable_->setName(name); | |||
std::string author = ""; | |||
if (data.count("author")) | |||
author = data["author"].get<std::string>(); | |||
wavetable_->setAuthor(author); | |||
if (data.count("remove_all_dc")) | |||
remove_all_dc_ = data["remove_all_dc"]; | |||
if (data.count("full_normalize")) | |||
full_normalize_ = data["full_normalize"]; | |||
else | |||
full_normalize_ = false; | |||
json json_groups = data["groups"]; | |||
for (const json& json_group : json_groups) { | |||
WavetableGroup* new_group = new WavetableGroup(); | |||
new_group->jsonToState(json_group); | |||
addGroup(new_group); | |||
} | |||
render(); | |||
} |
@@ -0,0 +1,96 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wave_frame.h" | |||
#include "wavetable_group.h" | |||
#include "file_source.h" | |||
#include "json/json.h" | |||
#include "wavetable.h" | |||
using json = nlohmann::json; | |||
class LineGenerator; | |||
class WavetableCreator { | |||
public: | |||
enum AudioFileLoadStyle { | |||
kNone, | |||
kWavetableSplice, | |||
kVocoded, | |||
kTtwt, | |||
kPitched, | |||
kNumDragLoadStyles | |||
}; | |||
WavetableCreator(vital::Wavetable* wavetable) : wavetable_(wavetable), | |||
full_normalize_(true), remove_all_dc_(true) { } | |||
int getGroupIndex(WavetableGroup* group); | |||
void addGroup(WavetableGroup* group) { groups_.push_back(std::unique_ptr<WavetableGroup>(group)); } | |||
void removeGroup(int index); | |||
void moveUp(int index); | |||
void moveDown(int index); | |||
int numGroups() const { return static_cast<int>(groups_.size()); } | |||
WavetableGroup* getGroup(int index) const { return groups_[index].get(); } | |||
float render(int position); | |||
void render(); | |||
void postRender(float max_span); | |||
void renderToBuffer(float* buffer, int num_frames, int frame_size); | |||
void init(); | |||
void clear(); | |||
void loadDefaultCreator(); | |||
void initPredefinedWaves(); | |||
void initFromAudioFile(const float* audio_buffer, int num_samples, int sample_rate, | |||
AudioFileLoadStyle load_style, FileSource::FadeStyle fade_style); | |||
void setName(const std::string& name) { wavetable_->setName(name); } | |||
void setAuthor(const std::string& author) { wavetable_->setAuthor(author); } | |||
void setFileLoaded(const std::string& path) { last_file_loaded_ = path; } | |||
std::string getName() const { return wavetable_->getName(); } | |||
std::string getAuthor() const { return wavetable_->getAuthor(); } | |||
std::string getLastFileLoaded() { return last_file_loaded_; } | |||
static bool isValidJson(json data); | |||
json updateJson(json data); | |||
json stateToJson(); | |||
void jsonToState(json data); | |||
vital::Wavetable* getWavetable() { return wavetable_; } | |||
protected: | |||
void initFromSplicedAudioFile(const float* audio_buffer, int num_samples, int sample_rate, | |||
FileSource::FadeStyle fade_style); | |||
void initFromVocodedAudioFile(const float* audio_buffer, int num_samples, int sample_rate, bool ttwt); | |||
void initFromPitchedAudioFile(const float* audio_buffer, int num_samples, int sample_rate); | |||
void initFromLineGenerator(LineGenerator* line_generator); | |||
vital::WaveFrame compute_frame_combine_; | |||
vital::WaveFrame compute_frame_; | |||
std::vector<std::unique_ptr<WavetableGroup>> groups_; | |||
std::string last_file_loaded_; | |||
vital::Wavetable* wavetable_; | |||
bool full_normalize_; | |||
bool remove_all_dc_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WavetableCreator) | |||
}; | |||
@@ -0,0 +1,132 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wavetable_group.h" | |||
#include "synth_constants.h" | |||
#include "wave_frame.h" | |||
#include "wave_source.h" | |||
#include "wavetable.h" | |||
int WavetableGroup::getComponentIndex(WavetableComponent* component) { | |||
for (int i = 0; i < components_.size(); ++i) { | |||
if (components_[i].get() == component) | |||
return i; | |||
} | |||
return -1; | |||
} | |||
void WavetableGroup::moveUp(int index) { | |||
if (index <= 0) | |||
return; | |||
components_[index].swap(components_[index - 1]); | |||
} | |||
void WavetableGroup::moveDown(int index) { | |||
if (index < 0 || index >= components_.size() - 1) | |||
return; | |||
components_[index].swap(components_[index + 1]); | |||
} | |||
void WavetableGroup::removeComponent(int index) { | |||
if (index < 0 || index >= components_.size()) | |||
return; | |||
std::unique_ptr<WavetableComponent> component = std::move(components_[index]); | |||
components_.erase(components_.begin() + index); | |||
} | |||
void WavetableGroup::reset() { | |||
components_.clear(); | |||
loadDefaultGroup(); | |||
} | |||
void WavetableGroup::prerender() { | |||
for (auto& component : components_) | |||
component->prerender(); | |||
} | |||
bool WavetableGroup::isShepardTone() { | |||
for (auto& component : components_) { | |||
if (component->getType() != WavetableComponentFactory::kShepardToneSource) | |||
return false; | |||
} | |||
return true; | |||
} | |||
void WavetableGroup::render(vital::WaveFrame* wave_frame, float position) const { | |||
wave_frame->index = position; | |||
for (auto& component : components_) | |||
component->render(wave_frame, position); | |||
} | |||
void WavetableGroup::renderTo(vital::Wavetable* wavetable) { | |||
for (int i = 0; i < vital::kNumOscillatorWaveFrames; ++i) { | |||
compute_frame_.index = i; | |||
for (auto& component : components_) | |||
component->render(&compute_frame_, i); | |||
wavetable->loadWaveFrame(&compute_frame_); | |||
} | |||
} | |||
void WavetableGroup::loadDefaultGroup() { | |||
WaveSource* wave_source = new WaveSource(); | |||
wave_source->insertNewKeyframe(0); | |||
vital::WaveFrame* wave_frame = wave_source->getWaveFrame(0); | |||
for (int i = 0; i < vital::WaveFrame::kWaveformSize; ++i) { | |||
float t = i / (vital::WaveFrame::kWaveformSize - 1.0f); | |||
int half_shift = (i + vital::WaveFrame::kWaveformSize / 2) % vital::WaveFrame::kWaveformSize; | |||
wave_frame->time_domain[half_shift] = 1.0f - 2.0f * t; | |||
} | |||
wave_frame->toFrequencyDomain(); | |||
addComponent(wave_source); | |||
} | |||
int WavetableGroup::getLastKeyframePosition() { | |||
int last_position = 0; | |||
for (auto& component : components_) | |||
last_position = std::max(last_position, component->getLastKeyframePosition()); | |||
return last_position; | |||
} | |||
json WavetableGroup::stateToJson() { | |||
json json_components; | |||
for (auto& component : components_) { | |||
json json_component = component->stateToJson(); | |||
json_components.push_back(json_component); | |||
} | |||
return { { "components", json_components } }; | |||
} | |||
void WavetableGroup::jsonToState(json data) { | |||
components_.clear(); | |||
json json_components = data["components"]; | |||
for (json json_component : json_components) { | |||
std::string type = json_component["type"]; | |||
WavetableComponent* component = WavetableComponentFactory::createComponent(type); | |||
component->jsonToState(json_component); | |||
addComponent(component); | |||
} | |||
} |
@@ -0,0 +1,58 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "wave_frame.h" | |||
#include "wavetable_component.h" | |||
namespace vital { | |||
class Wavetable; | |||
} // namespace vital | |||
class WavetableGroup { | |||
public: | |||
WavetableGroup() { } | |||
int getComponentIndex(WavetableComponent* component); | |||
void addComponent(WavetableComponent* component) { | |||
components_.push_back(std::unique_ptr< WavetableComponent>(component)); | |||
} | |||
void removeComponent(int index); | |||
void moveUp(int index); | |||
void moveDown(int index); | |||
void reset(); | |||
void prerender(); | |||
int numComponents() const { return static_cast<int>(components_.size()); } | |||
WavetableComponent* getComponent(int index) const { return components_[index].get(); } | |||
bool isShepardTone(); | |||
void render(vital::WaveFrame* wave_frame, float position) const; | |||
void renderTo(vital::Wavetable* wavetable); | |||
void loadDefaultGroup(); | |||
int getLastKeyframePosition(); | |||
json stateToJson(); | |||
void jsonToState(json data); | |||
protected: | |||
vital::WaveFrame compute_frame_; | |||
std::vector<std::unique_ptr<WavetableComponent>> components_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WavetableGroup) | |||
}; | |||
@@ -0,0 +1,51 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "wavetable_keyframe.h" | |||
#include "utils.h" | |||
#include "wavetable_component.h" | |||
float WavetableKeyframe::linearTween(float point_from, float point_to, float t) { | |||
return vital::utils::interpolate(point_from, point_to, t); | |||
} | |||
float WavetableKeyframe::cubicTween(float point_prev, float point_from, float point_to, float point_next, | |||
float range_prev, float range, float range_next, float t) { | |||
float slope_from = 0.0f; | |||
float slope_to = 0.0f; | |||
if (range_prev > 0.0f) | |||
slope_from = (point_to - point_prev) / (1.0f + range_prev / range); | |||
if (range_next > 0.0f) | |||
slope_to = (point_next - point_from) / (1.0f + range_next / range); | |||
float delta = point_to - point_from; | |||
float movement = linearTween(point_from, point_to, t); | |||
float smooth = t * (1.0f - t) * ((1.0f - t) * (slope_from - delta) + t * (delta - slope_to)); | |||
return movement + smooth; | |||
} | |||
int WavetableKeyframe::index() { | |||
return owner()->indexOf(this); | |||
} | |||
json WavetableKeyframe::stateToJson() { | |||
return { { "position", position_ } }; | |||
} | |||
void WavetableKeyframe::jsonToState(json data) { | |||
position_ = data["position"]; | |||
} |
@@ -0,0 +1,68 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "json/json.h" | |||
#include "synth_constants.h" | |||
using json = nlohmann::json; | |||
class WavetableComponent; | |||
namespace vital { | |||
class WaveFrame; | |||
} // namespace vital | |||
class WavetableKeyframe { | |||
public: | |||
static float linearTween(float point_from, float point_to, float t); | |||
static float cubicTween(float point_prev, float point_from, float point_to, float point_next, | |||
float range_prev, float range, float range_next, float t); | |||
WavetableKeyframe() : position_(0), owner_(nullptr) { } | |||
virtual ~WavetableKeyframe() { } | |||
int index(); | |||
int position() const { return position_; } | |||
void setPosition(int position) { | |||
VITAL_ASSERT(position >= 0 && position < vital::kNumOscillatorWaveFrames); | |||
position_ = position; | |||
} | |||
virtual void copy(const WavetableKeyframe* keyframe) = 0; | |||
virtual void interpolate(const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, float t) = 0; | |||
virtual void smoothInterpolate(const WavetableKeyframe* prev_keyframe, | |||
const WavetableKeyframe* from_keyframe, | |||
const WavetableKeyframe* to_keyframe, | |||
const WavetableKeyframe* next_keyframe, float t) { } | |||
virtual void render(vital::WaveFrame* wave_frame) = 0; | |||
virtual json stateToJson(); | |||
virtual void jsonToState(json data); | |||
WavetableComponent* owner() { return owner_; } | |||
void setOwner(WavetableComponent* owner) { owner_ = owner; } | |||
protected: | |||
int position_; | |||
WavetableComponent* owner_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WavetableKeyframe) | |||
}; | |||
@@ -0,0 +1,74 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include <climits> | |||
class AudioFileDropSource : public FileDragAndDropTarget { | |||
public: | |||
class Listener { | |||
public: | |||
virtual ~Listener() { } | |||
virtual void audioFileLoaded(const File& file) = 0; | |||
}; | |||
AudioFileDropSource() { | |||
format_manager_.registerBasicFormats(); | |||
} | |||
bool isInterestedInFileDrag(const StringArray& files) override { | |||
if (files.size() != 1) | |||
return false; | |||
String file = files[0]; | |||
StringArray wildcards; | |||
wildcards.addTokens(getExtensions(), ";", "\""); | |||
for (const String& wildcard : wildcards) { | |||
if (file.matchesWildcard(wildcard, true)) | |||
return true; | |||
} | |||
return false; | |||
} | |||
void filesDropped(const StringArray& files, int x, int y) override { | |||
if (files.size() == 0) | |||
return; | |||
File file(files[0]); | |||
audioFileLoaded(file); | |||
for (Listener* listener : listeners_) | |||
listener->audioFileLoaded(file); | |||
} | |||
virtual void audioFileLoaded(const File& file) = 0; | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
String getExtensions() { return format_manager_.getWildcardForAllFormats(); } | |||
AudioFormatManager& formatManager() { return format_manager_; } | |||
protected: | |||
AudioFormatManager format_manager_; | |||
private: | |||
std::vector<Listener*> listeners_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioFileDropSource) | |||
}; | |||
@@ -0,0 +1,236 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "bar_renderer.h" | |||
#include "skin.h" | |||
#include "shaders.h" | |||
#include "utils.h" | |||
BarRenderer::BarRenderer(int num_points, bool vertical) : | |||
shader_(nullptr), vertical_(vertical), additive_blending_(true), display_scale_(1.0f), power_scale_(false), | |||
square_scale_(false), dirty_(false), num_points_(num_points), total_points_(num_points) { | |||
addRoundedCorners(); | |||
scale_ = 1.0f; | |||
offset_ = 0.0f; | |||
bar_data_ = std::make_unique<float[]>(kFloatsPerBar * total_points_); | |||
bar_indices_ = std::make_unique<int[]>(kTriangleIndicesPerBar * total_points_); | |||
bar_corner_data_ = std::make_unique<float[]>(kCornerFloatsPerBar * total_points_); | |||
bar_buffer_ = 0; | |||
bar_corner_buffer_ = 0; | |||
bar_indices_buffer_ = 0; | |||
bar_width_ = 1.0f; | |||
for (int i = 0; i < total_points_; ++i) { | |||
float t = i / (total_points_ * 1.0f); | |||
int index = i * kFloatsPerBar; | |||
for (int v = 0; v < kVerticesPerBar; ++v) { | |||
int vertex_index = v * kFloatsPerVertex + index; | |||
bar_data_[vertex_index] = 2.0f * t - 1.0f; | |||
bar_data_[vertex_index + 1] = -1.0f; | |||
} | |||
int bar_index = i * kTriangleIndicesPerBar; | |||
int bar_vertex_index = i * kVerticesPerBar; | |||
bar_indices_[bar_index] = bar_vertex_index; | |||
bar_indices_[bar_index + 1] = bar_vertex_index + 1; | |||
bar_indices_[bar_index + 2] = bar_vertex_index + 2; | |||
bar_indices_[bar_index + 3] = bar_vertex_index + 1; | |||
bar_indices_[bar_index + 4] = bar_vertex_index + 2; | |||
bar_indices_[bar_index + 5] = bar_vertex_index + 3; | |||
int corner_index = i * kCornerFloatsPerBar; | |||
bar_corner_data_[corner_index] = 0.0f; | |||
bar_corner_data_[corner_index + 1] = 1.0f; | |||
bar_corner_data_[corner_index + 2] = 1.0f; | |||
bar_corner_data_[corner_index + 3] = 1.0f; | |||
bar_corner_data_[corner_index + 4] = 0.0f; | |||
bar_corner_data_[corner_index + 5] = 0.0f; | |||
bar_corner_data_[corner_index + 6] = 1.0f; | |||
bar_corner_data_[corner_index + 7] = 0.0f; | |||
} | |||
} | |||
BarRenderer::~BarRenderer() { } | |||
void BarRenderer::init(OpenGlWrapper& open_gl) { | |||
OpenGlComponent::init(open_gl); | |||
open_gl.context.extensions.glGenBuffers(1, &bar_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, bar_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(kFloatsPerBar * total_points_ * sizeof(float)); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, bar_data_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &bar_corner_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, bar_corner_buffer_); | |||
GLsizeiptr corner_size = static_cast<GLsizeiptr>(kCornerFloatsPerBar * total_points_ * sizeof(float)); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, corner_size, bar_corner_data_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &bar_indices_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bar_indices_buffer_); | |||
GLsizeiptr bar_size = static_cast<GLsizeiptr>(kTriangleIndicesPerBar * total_points_ * sizeof(int)); | |||
open_gl.context.extensions.glBufferData(GL_ELEMENT_ARRAY_BUFFER, bar_size, bar_indices_.get(), GL_STATIC_DRAW); | |||
if (vertical_) | |||
shader_ = open_gl.shaders->getShaderProgram(Shaders::kBarVerticalVertex, Shaders::kBarFragment); | |||
else | |||
shader_ = open_gl.shaders->getShaderProgram(Shaders::kBarHorizontalVertex, Shaders::kBarFragment); | |||
shader_->use(); | |||
color_uniform_ = getUniform(open_gl, *shader_, "color"); | |||
dimensions_uniform_ = getUniform(open_gl, *shader_, "dimensions"); | |||
offset_uniform_ = getUniform(open_gl, *shader_, "offset"); | |||
scale_uniform_ = getUniform(open_gl, *shader_, "scale"); | |||
width_percent_uniform_ = getUniform(open_gl, *shader_, "width_percent"); | |||
position_ = getAttribute(open_gl, *shader_, "position"); | |||
corner_ = getAttribute(open_gl, *shader_, "corner"); | |||
} | |||
void BarRenderer::drawBars(OpenGlWrapper& open_gl) { | |||
if (!setViewPort(open_gl)) | |||
return; | |||
if (shader_ == nullptr) | |||
init(open_gl); | |||
glEnable(GL_BLEND); | |||
glEnable(GL_SCISSOR_TEST); | |||
if (additive_blending_) | |||
glBlendFunc(GL_SRC_ALPHA, GL_ONE); | |||
else | |||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | |||
if (dirty_) { | |||
dirty_ = false; | |||
setBarSizes(); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, bar_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(kFloatsPerBar * total_points_ * sizeof(float)); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, bar_data_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
} | |||
shader_->use(); | |||
color_uniform_->set(color_.getFloatRed(), color_.getFloatGreen(), | |||
color_.getFloatBlue(), color_.getFloatAlpha()); | |||
dimensions_uniform_->set(getWidth(), getHeight()); | |||
offset_uniform_->set(offset_); | |||
scale_uniform_->set(scale_); | |||
float min_width = 4.0f / getWidth(); | |||
width_percent_uniform_->set(std::max(min_width, bar_width_ * scale_ * 2.0f / num_points_)); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, bar_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bar_indices_buffer_); | |||
open_gl.context.extensions.glVertexAttribPointer(position_->attributeID, kFloatsPerVertex, GL_FLOAT, | |||
GL_FALSE, kFloatsPerVertex * sizeof(float), nullptr); | |||
open_gl.context.extensions.glEnableVertexAttribArray(position_->attributeID); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, bar_corner_buffer_); | |||
open_gl.context.extensions.glVertexAttribPointer(corner_->attributeID, kCornerFloatsPerVertex, GL_FLOAT, | |||
GL_FALSE, kCornerFloatsPerVertex * sizeof(float), nullptr); | |||
open_gl.context.extensions.glEnableVertexAttribArray(corner_->attributeID); | |||
glDrawElements(GL_TRIANGLES, kTriangleIndicesPerBar * total_points_, GL_UNSIGNED_INT, nullptr); | |||
open_gl.context.extensions.glDisableVertexAttribArray(position_->attributeID); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); | |||
glDisable(GL_BLEND); | |||
glDisable(GL_SCISSOR_TEST); | |||
} | |||
void BarRenderer::render(OpenGlWrapper& open_gl, bool animate) { | |||
drawBars(open_gl); | |||
} | |||
void BarRenderer::destroy(OpenGlWrapper& open_gl) { | |||
OpenGlComponent::destroy(open_gl); | |||
shader_ = nullptr; | |||
position_ = nullptr; | |||
corner_ = nullptr; | |||
color_uniform_ = nullptr; | |||
dimensions_uniform_ = nullptr; | |||
offset_uniform_ = nullptr; | |||
scale_uniform_ = nullptr; | |||
width_percent_uniform_ = nullptr; | |||
open_gl.context.extensions.glDeleteBuffers(1, &bar_buffer_); | |||
open_gl.context.extensions.glDeleteBuffers(1, &bar_indices_buffer_); | |||
bar_buffer_ = 0; | |||
bar_indices_buffer_ = 0; | |||
} | |||
void BarRenderer::setBarSizes() { | |||
if (vertical_) { | |||
for (int i = 0; i < total_points_; ++i) { | |||
int index = i * kFloatsPerBar; | |||
float height = fabsf(yAt(i) - bottomAt(i)) * 0.5f * display_scale_; | |||
bar_data_[index + 2] = height; | |||
bar_data_[index + kFloatsPerVertex + 2] = height; | |||
bar_data_[index + 2 * kFloatsPerVertex + 2] = height; | |||
bar_data_[index + 3 * kFloatsPerVertex + 2] = height; | |||
} | |||
} | |||
else { | |||
for (int i = 0; i < total_points_; ++i) { | |||
int index = i * kFloatsPerBar; | |||
float width = fabsf(xAt(i) - rightAt(i)) * 0.5f * display_scale_; | |||
bar_data_[index + 2] = width; | |||
bar_data_[index + kFloatsPerVertex + 2] = width; | |||
bar_data_[index + 2 * kFloatsPerVertex + 2] = width; | |||
bar_data_[index + 3 * kFloatsPerVertex + 2] = width; | |||
} | |||
} | |||
} | |||
void BarRenderer::setPowerScale(bool power_scale) { | |||
if (power_scale == power_scale_) | |||
return; | |||
bool old_power_scale = power_scale_; | |||
for (int i = 1; i < num_points_; ++i) { | |||
power_scale_ = old_power_scale; | |||
float old_y = scaledYAt(i); | |||
power_scale_ = power_scale; | |||
setScaledY(i, old_y); | |||
} | |||
dirty_ = true; | |||
} | |||
void BarRenderer::setSquareScale(bool square_scale) { | |||
if (square_scale == square_scale_) | |||
return; | |||
bool old_square_scale = square_scale_; | |||
for (int i = 0; i < num_points_; ++i) { | |||
square_scale_ = old_square_scale; | |||
float old_y = scaledYAt(i); | |||
square_scale_ = square_scale; | |||
setScaledY(i, old_y); | |||
} | |||
dirty_ = true; | |||
} |
@@ -0,0 +1,150 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "common.h" | |||
#include "open_gl_component.h" | |||
#include "open_gl_multi_quad.h" | |||
class BarRenderer : public OpenGlComponent { | |||
public: | |||
static constexpr float kScaleConstant = 5.0f; | |||
static constexpr int kFloatsPerVertex = 3; | |||
static constexpr int kVerticesPerBar = 4; | |||
static constexpr int kFloatsPerBar = kVerticesPerBar * kFloatsPerVertex; | |||
static constexpr int kTriangleIndicesPerBar = 6; | |||
static constexpr int kCornerFloatsPerVertex = 2; | |||
static constexpr int kCornerFloatsPerBar = kVerticesPerBar * kCornerFloatsPerVertex; | |||
BarRenderer(int num_points, bool vertical = true); | |||
virtual ~BarRenderer(); | |||
virtual void init(OpenGlWrapper& open_gl) override; | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) override; | |||
virtual void destroy(OpenGlWrapper& open_gl) override; | |||
void setColor(const Colour& color) { color_ = color; } | |||
void setScale(float scale) { scale_ = scale; } | |||
void setOffset(float offset) { offset_ = offset; } | |||
void setBarWidth(float bar_width) { bar_width_ = bar_width; } | |||
void setNumPoints(int num_points) { num_points_ = num_points; } | |||
float getBarWidth() { return bar_width_; } | |||
inline float xAt(int index) { return bar_data_[kFloatsPerBar * index]; } | |||
inline float rightAt(int index) { return bar_data_[kFloatsPerBar * index + kFloatsPerVertex]; } | |||
inline float yAt(int index) { return bar_data_[kFloatsPerBar * index + 1]; } | |||
inline float bottomAt(int index) { return bar_data_[kFloatsPerBar * index + 2 * kFloatsPerVertex + 1]; } | |||
force_inline void setX(int index, float val) { | |||
bar_data_[kFloatsPerBar * index] = val; | |||
bar_data_[kFloatsPerBar * index + 2 * kFloatsPerVertex] = val; | |||
bar_data_[kFloatsPerBar * index + kFloatsPerVertex] = val; | |||
bar_data_[kFloatsPerBar * index + 3 * kFloatsPerVertex] = val; | |||
dirty_ = true; | |||
} | |||
force_inline void setY(int index, float val) { | |||
bar_data_[kFloatsPerBar * index + 1] = val; | |||
bar_data_[kFloatsPerBar * index + kFloatsPerVertex + 1] = val; | |||
dirty_ = true; | |||
} | |||
force_inline void setBottom(int index, float val) { | |||
bar_data_[kFloatsPerBar * index + 2 * kFloatsPerVertex + 1] = val; | |||
bar_data_[kFloatsPerBar * index + 3 * kFloatsPerVertex + 1] = val; | |||
dirty_ = true; | |||
} | |||
inline void positionBar(int index, float x, float y, float width, float height) { | |||
bar_data_[kFloatsPerBar * index] = x; | |||
bar_data_[kFloatsPerBar * index + 1] = y; | |||
bar_data_[kFloatsPerBar * index + kFloatsPerVertex] = x + width; | |||
bar_data_[kFloatsPerBar * index + kFloatsPerVertex + 1] = y; | |||
bar_data_[kFloatsPerBar * index + 2 * kFloatsPerVertex] = x; | |||
bar_data_[kFloatsPerBar * index + 2 * kFloatsPerVertex + 1] = y + height; | |||
bar_data_[kFloatsPerBar * index + 3 * kFloatsPerVertex] = x + width; | |||
bar_data_[kFloatsPerBar * index + 3 * kFloatsPerVertex + 1] = y + height; | |||
dirty_ = true; | |||
} | |||
void setBarSizes(); | |||
void setPowerScale(bool scale); | |||
void setSquareScale(bool scale); | |||
force_inline float scaledYAt(int index) { | |||
float value = yAt(index) * 0.5f + 0.5f; | |||
if (square_scale_) | |||
value *= value; | |||
if (power_scale_) | |||
value /= std::max(index, 1) / kScaleConstant; | |||
return value; | |||
} | |||
force_inline void setScaledY(int index, float val) { | |||
float value = val; | |||
if (power_scale_) | |||
value *= std::max(index, 1) / kScaleConstant; | |||
if (square_scale_) | |||
value = sqrtf(value); | |||
setY(index, 2.0f * value - 1.0f); | |||
} | |||
force_inline void setAdditiveBlending(bool additive_blending) { additive_blending_ = additive_blending; } | |||
protected: | |||
void drawBars(OpenGlWrapper& open_gl); | |||
OpenGLShaderProgram* shader_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> color_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> dimensions_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> offset_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> scale_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> width_percent_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> position_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> corner_; | |||
Colour color_; | |||
bool vertical_; | |||
float scale_; | |||
float offset_; | |||
float bar_width_; | |||
bool additive_blending_; | |||
float display_scale_; | |||
bool power_scale_; | |||
bool square_scale_; | |||
bool dirty_; | |||
int num_points_; | |||
int total_points_; | |||
std::unique_ptr<float[]> bar_data_; | |||
std::unique_ptr<float[]> bar_corner_data_; | |||
std::unique_ptr<int[]> bar_indices_; | |||
GLuint bar_buffer_; | |||
GLuint bar_corner_buffer_; | |||
GLuint bar_indices_buffer_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BarRenderer) | |||
}; | |||
@@ -0,0 +1,724 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "compressor_editor.h" | |||
#include "skin.h" | |||
#include "shaders.h" | |||
#include "synth_gui_interface.h" | |||
#include "utils.h" | |||
namespace { | |||
float getOpenGlYForDb(float db) { | |||
float t = (db - CompressorEditor::kMinDb) / (CompressorEditor::kMaxDb - CompressorEditor::kMinDb); | |||
return 2.0f * t - 1.0f; | |||
} | |||
vital::poly_float getOpenGlYForDb(vital::poly_float db) { | |||
vital::poly_float t = (db - CompressorEditor::kMinDb) / (CompressorEditor::kMaxDb - CompressorEditor::kMinDb); | |||
return vital::utils::clamp(t * 2.0f - 1.0f, -1.0f, 1.0f); | |||
} | |||
vital::poly_float getOpenGlYForMagnitude(vital::poly_float magnitude) { | |||
vital::poly_float db = vital::utils::magnitudeToDb(vital::utils::max(0.0001f, magnitude)); | |||
return getOpenGlYForDb(db); | |||
} | |||
void setQuadIfRatioMatch(OpenGlMultiQuad& quads, float ratio, float ratio_match, | |||
int index, float x, float y, float w, float h) { | |||
if (ratio == ratio_match || (ratio > 0.0f && ratio_match > 0.0f) || (ratio < 0.0f && ratio_match < 0.0f)) | |||
quads.setQuad(index, x, y, w, h); | |||
else | |||
quads.setQuad(index, -2.0f, -2.0f, 0.0f, 0.0f); | |||
} | |||
std::string formatString(float value, std::string suffix) { | |||
static constexpr int kMaxDecimalPlaces = 4; | |||
String format = String(value, kMaxDecimalPlaces); | |||
int display_characters = kMaxDecimalPlaces; | |||
if (format[0] == '-') | |||
display_characters += 1; | |||
format = format.substring(0, display_characters); | |||
if (format.getLastCharacter() == '.') | |||
format = format.removeCharacters("."); | |||
return format.toStdString() + suffix; | |||
} | |||
} // namespace | |||
CompressorEditor::CompressorEditor() : hover_quad_(Shaders::kColorFragment), | |||
input_dbs_(kNumChannels, Shaders::kColorFragment), | |||
output_dbs_(kNumChannels, Shaders::kRoundedRectangleFragment), | |||
thresholds_(kNumChannels, Shaders::kColorFragment), | |||
ratio_lines_(kTotalRatioLines, Shaders::kFadeSquareFragment) { | |||
addRoundedCorners(); | |||
addAndMakeVisible(hover_quad_); | |||
addAndMakeVisible(input_dbs_); | |||
addAndMakeVisible(output_dbs_); | |||
addAndMakeVisible(thresholds_); | |||
addAndMakeVisible(ratio_lines_); | |||
parent_ = nullptr; | |||
section_parent_ = nullptr; | |||
hover_ = kNone; | |||
animate_ = false; | |||
high_band_active_ = true; | |||
low_band_active_ = true; | |||
size_ratio_ = 1.0f; | |||
active_ = true; | |||
low_upper_threshold_ = kMaxDb; | |||
band_upper_threshold_ = kMaxDb; | |||
high_upper_threshold_ = kMaxDb; | |||
low_lower_threshold_ = kMinDb; | |||
band_lower_threshold_ = kMinDb; | |||
high_lower_threshold_ = kMinDb; | |||
low_upper_ratio_ = 0.0f; | |||
band_upper_ratio_ = 0.0f; | |||
high_upper_ratio_ = 0.0f; | |||
low_lower_ratio_ = 0.0f; | |||
band_lower_ratio_ = 0.0f; | |||
high_lower_ratio_ = 0.0f; | |||
low_input_ms_ = nullptr; | |||
band_input_ms_ = nullptr; | |||
high_input_ms_ = nullptr; | |||
low_output_ms_ = nullptr; | |||
band_output_ms_ = nullptr; | |||
high_output_ms_ = nullptr; | |||
} | |||
CompressorEditor::~CompressorEditor() { } | |||
CompressorEditor::DragPoint CompressorEditor::getHoverPoint(const MouseEvent& e) { | |||
float low_band_high_position = 3.0f * e.position.x / ((1.0f - kCompressorAreaBuffer) * getWidth()); | |||
int index = low_band_high_position; | |||
float local_position = low_band_high_position - index; | |||
if (index >= 3 || index < 0 || local_position < 3.0f * kCompressorAreaBuffer) | |||
return kNone; | |||
if (index == 0 && !low_band_active_) | |||
index = 1; | |||
if (index == 2 && !high_band_active_) | |||
index = 1; | |||
float upper_threshold_values[] = { low_upper_threshold_, band_upper_threshold_, high_upper_threshold_ }; | |||
float lower_threshold_values[] = { low_lower_threshold_, band_lower_threshold_, high_lower_threshold_ }; | |||
DragPoint upper_threshold_points[] = { kLowUpperThreshold, kBandUpperThreshold, kHighUpperThreshold }; | |||
DragPoint lower_threshold_points[] = { kLowLowerThreshold, kBandLowerThreshold, kHighLowerThreshold }; | |||
DragPoint upper_ratio_points[] = { kLowUpperRatio, kBandUpperRatio, kHighUpperRatio }; | |||
DragPoint lower_ratio_points[] = { kLowLowerRatio, kBandLowerRatio, kHighLowerRatio }; | |||
float grab_radius = kGrabRadius * size_ratio_; | |||
int upper_handle_y = std::max(grab_radius, getYForDb(upper_threshold_values[index])); | |||
int lower_handle_y = std::min(getHeight() - grab_radius, getYForDb(lower_threshold_values[index])); | |||
float delta_upper = e.position.y - upper_handle_y; | |||
float delta_lower = e.position.y - lower_handle_y; | |||
if (fabsf(delta_upper) <= grab_radius && fabsf(delta_upper) < fabsf(delta_lower)) | |||
return upper_threshold_points[index]; | |||
if (fabsf(delta_lower) <= grab_radius) | |||
return lower_threshold_points[index]; | |||
if (delta_upper < 0.0f) | |||
return upper_ratio_points[index]; | |||
if (delta_lower > 0.0f) | |||
return lower_ratio_points[index]; | |||
return kNone; | |||
} | |||
void CompressorEditor::mouseDown(const MouseEvent& e) { | |||
last_mouse_position_ = e.getPosition(); | |||
mouseDrag(e); | |||
} | |||
void CompressorEditor::mouseDoubleClick(const MouseEvent& e) { | |||
if (isRatio(hover_)) { | |||
switch (hover_) { | |||
case kLowUpperRatio: | |||
setLowUpperRatio(0.0f); | |||
break; | |||
case kBandUpperRatio: | |||
setBandUpperRatio(0.0f); | |||
break; | |||
case kHighUpperRatio: | |||
setHighUpperRatio(0.0f); | |||
break; | |||
case kLowLowerRatio: | |||
setLowLowerRatio(0.0f); | |||
break; | |||
case kBandLowerRatio: | |||
setBandLowerRatio(0.0f); | |||
break; | |||
case kHighLowerRatio: | |||
setHighLowerRatio(0.0f); | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
} | |||
void CompressorEditor::mouseMove(const MouseEvent& e) { | |||
hover_ = getHoverPoint(e); | |||
if (hover_ != kNone) | |||
setMouseCursor(MouseCursor::BottomEdgeResizeCursor); | |||
else | |||
setMouseCursor(MouseCursor::NormalCursor); | |||
} | |||
void CompressorEditor::mouseDrag(const MouseEvent& e) { | |||
if (hover_ == kNone || parent_ == nullptr) | |||
return; | |||
float delta = (e.getPosition().y - last_mouse_position_.y) * kMouseMultiplier / getHeight(); | |||
float delta_db_value = (kMinDb - kMaxDb) * delta; | |||
last_mouse_position_ = e.getPosition(); | |||
float delta_ratio = delta * kRatioEditMultiplier; | |||
if (e.mods.isShiftDown()) { | |||
setLowUpperThreshold(low_upper_threshold_ + delta_db_value, false); | |||
setBandUpperThreshold(band_upper_threshold_ + delta_db_value, false); | |||
setHighUpperThreshold(high_upper_threshold_ + delta_db_value, false); | |||
setLowLowerThreshold(low_lower_threshold_ + delta_db_value, false); | |||
setBandLowerThreshold(band_lower_threshold_ + delta_db_value, false); | |||
setHighLowerThreshold(high_lower_threshold_ + delta_db_value, false); | |||
} | |||
else { | |||
switch (hover_) { | |||
case kLowUpperThreshold: | |||
setLowUpperThreshold(low_upper_threshold_ + delta_db_value, true); | |||
break; | |||
case kLowUpperRatio: | |||
setLowUpperRatio(low_upper_ratio_ + delta_ratio); | |||
break; | |||
case kBandUpperThreshold: | |||
setBandUpperThreshold(band_upper_threshold_ + delta_db_value, true); | |||
break; | |||
case kBandUpperRatio: | |||
setBandUpperRatio(band_upper_ratio_ + delta_ratio); | |||
break; | |||
case kHighUpperThreshold: | |||
setHighUpperThreshold(high_upper_threshold_ + delta_db_value, true); | |||
break; | |||
case kHighUpperRatio: | |||
setHighUpperRatio(high_upper_ratio_ + delta_ratio); | |||
break; | |||
case kLowLowerThreshold: | |||
setLowLowerThreshold(low_lower_threshold_ + delta_db_value, true); | |||
break; | |||
case kLowLowerRatio: | |||
setLowLowerRatio(low_lower_ratio_ - delta_ratio); | |||
break; | |||
case kBandLowerThreshold: | |||
setBandLowerThreshold(band_lower_threshold_ + delta_db_value, true); | |||
break; | |||
case kBandLowerRatio: | |||
setBandLowerRatio(band_lower_ratio_ - delta_ratio); | |||
break; | |||
case kHighLowerThreshold: | |||
setHighLowerThreshold(high_lower_threshold_ + delta_db_value, true); | |||
break; | |||
case kHighLowerRatio: | |||
setHighLowerRatio(high_lower_ratio_ - delta_ratio); | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
} | |||
void CompressorEditor::mouseUp(const MouseEvent& e) { | |||
if (isRatio(hover_)) | |||
setMouseCursor(MouseCursor::BottomEdgeResizeCursor); | |||
section_parent_->hidePopupDisplay(true); | |||
} | |||
void CompressorEditor::mouseExit(const MouseEvent& e) { | |||
setMouseCursor(MouseCursor::NormalCursor); | |||
hover_ = kNone; | |||
} | |||
void CompressorEditor::paintBackground(Graphics& g) { | |||
OpenGlComponent::paintBackground(g); | |||
g.setColour(findColour(Skin::kLightenScreen, true)); | |||
for (int i = 1; i < kDbLineSections; ++i) { | |||
float t = (i * 1.0f) / kDbLineSections; | |||
int y = getHeight() * t; | |||
g.fillRect(0, y, getWidth(), 1); | |||
} | |||
} | |||
void CompressorEditor::resized() { | |||
OpenGlComponent::resized(); | |||
hover_quad_.setBounds(getLocalBounds()); | |||
input_dbs_.setBounds(getLocalBounds()); | |||
output_dbs_.setBounds(getLocalBounds()); | |||
thresholds_.setBounds(getLocalBounds()); | |||
ratio_lines_.setBounds(getLocalBounds()); | |||
} | |||
void CompressorEditor::init(OpenGlWrapper& open_gl) { | |||
OpenGlComponent::init(open_gl); | |||
hover_quad_.init(open_gl); | |||
input_dbs_.init(open_gl); | |||
output_dbs_.init(open_gl); | |||
thresholds_.init(open_gl); | |||
ratio_lines_.init(open_gl); | |||
} | |||
void CompressorEditor::render(OpenGlWrapper& open_gl, bool animate) { | |||
renderCompressor(open_gl, animate); | |||
renderCorners(open_gl, animate); | |||
} | |||
void CompressorEditor::setThresholdPositions(int low_start, int low_end, int band_start, int band_end, | |||
int high_start, int high_end, float ratio_match) { | |||
thresholds_.setColor(getColorForRatio(ratio_match)); | |||
float width = getWidth(); | |||
float low_start_x = low_start * 2.0f / width - 1.0f; | |||
float low_width = (low_end - low_start) * 2.0f / width; | |||
float band_start_x = band_start * 2.0f / width - 1.0f; | |||
float band_width = (band_end - band_start) * 2.0f / width; | |||
float high_start_x = high_start * 2.0f / width - 1.0f; | |||
float high_width = (high_end - high_start) * 2.0f / width; | |||
setQuadIfRatioMatch(thresholds_, -low_lower_ratio_, ratio_match, 0, | |||
low_start_x, -1.0f, low_width, getOpenGlYForDb(low_lower_threshold_) + 1.0f); | |||
setQuadIfRatioMatch(thresholds_, low_upper_ratio_, ratio_match, 1, | |||
low_start_x, 1.0f, low_width, getOpenGlYForDb(low_upper_threshold_) - 1.0f); | |||
setQuadIfRatioMatch(thresholds_, -band_lower_ratio_, ratio_match, 2, | |||
band_start_x, -1.0f, band_width, getOpenGlYForDb(band_lower_threshold_) + 1.0f); | |||
setQuadIfRatioMatch(thresholds_, band_upper_ratio_, ratio_match, 3, | |||
band_start_x, 1.0f, band_width, getOpenGlYForDb(band_upper_threshold_) - 1.0f); | |||
setQuadIfRatioMatch(thresholds_, -high_lower_ratio_, ratio_match, 4, | |||
high_start_x, -1.0f, high_width, getOpenGlYForDb(high_lower_threshold_) + 1.0f); | |||
setQuadIfRatioMatch(thresholds_, high_upper_ratio_, ratio_match, 5, | |||
high_start_x, 1.0f, high_width, getOpenGlYForDb(high_upper_threshold_) - 1.0f); | |||
} | |||
void CompressorEditor::setRatioLines(int start_index, int start_x, int end_x, | |||
float threshold, float ratio, bool upper, bool hover) { | |||
float db_position = kDbLineSections * (threshold - kMinDb) / (kMaxDb - kMinDb); | |||
int db_index = db_position; | |||
float db_change = -(kMaxDb - kMinDb) / kDbLineSections; | |||
if (upper) { | |||
db_change = (kMaxDb - kMinDb) / kDbLineSections; | |||
db_index = ceil(db_position); | |||
} | |||
float width = getWidth(); | |||
float x = start_x * 2.0f / width - 1.0f; | |||
float ratio_width = (end_x - start_x) * 2.0f / width; | |||
float ratio_height = 4.0f / getHeight(); | |||
float mult = hover ? 5.0f : 2.5f; | |||
float db = db_index * (kMaxDb - kMinDb) / kDbLineSections + kMinDb; | |||
for (int i = 0; i < kRatioDbLines; ++i) { | |||
float adjusted_db = getCompressedDb(db, threshold, ratio, threshold, ratio); | |||
ratio_lines_.setQuad(start_index + i, x, getOpenGlYForDb(adjusted_db) - ratio_height * 0.5f, | |||
ratio_width, ratio_height); | |||
ratio_lines_.setShaderValue(start_index + i, (kRatioDbLines - i) * mult / kRatioDbLines); | |||
db += db_change; | |||
} | |||
} | |||
void CompressorEditor::setRatioLinePositions(int low_start, int low_end, int band_start, int band_end, | |||
int high_start, int high_end) { | |||
setRatioLines(0, low_start, low_end, low_upper_threshold_, low_upper_ratio_, | |||
true, hover_ == kLowUpperRatio); | |||
setRatioLines(kRatioDbLines, low_start, low_end, low_lower_threshold_, low_lower_ratio_, | |||
false, hover_ == kLowLowerRatio); | |||
setRatioLines(2 * kRatioDbLines, band_start, band_end, band_upper_threshold_, band_upper_ratio_, | |||
true, hover_ == kBandUpperRatio); | |||
setRatioLines(3 * kRatioDbLines, band_start, band_end, band_lower_threshold_, band_lower_ratio_, | |||
false, hover_ == kBandLowerRatio); | |||
setRatioLines(4 * kRatioDbLines, high_start, high_end, high_upper_threshold_, high_upper_ratio_, | |||
true, hover_ == kHighUpperRatio); | |||
setRatioLines(5 * kRatioDbLines, high_start, high_end, high_lower_threshold_, high_lower_ratio_, | |||
false, hover_ == kHighLowerRatio); | |||
} | |||
void CompressorEditor::renderHover(OpenGlWrapper& open_gl, int low_start, int low_end, | |||
int band_start, int band_end, int high_start, int high_end) { | |||
if (hover_ == kNone) | |||
return; | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
float threshold_height = 2.0f / height; | |||
float low_start_x = low_start * 2.0f / width - 1.0f; | |||
float low_width = (low_end - low_start) * 2.0f / width; | |||
float band_start_x = band_start * 2.0f / width - 1.0f; | |||
float band_width = (band_end - band_start) * 2.0f / width; | |||
float high_start_x = high_start * 2.0f / width - 1.0f; | |||
float high_width = (high_end - high_start) * 2.0f / width; | |||
switch (hover_) { | |||
case kLowUpperThreshold: | |||
hover_quad_.setQuad(0, low_start_x, getOpenGlYForDb(low_upper_threshold_) - 0.5f * threshold_height, | |||
low_width, threshold_height); | |||
break; | |||
case kBandUpperThreshold: | |||
hover_quad_.setQuad(0, band_start_x, getOpenGlYForDb(band_upper_threshold_) - 0.5f * threshold_height, | |||
band_width, threshold_height); | |||
break; | |||
case kHighUpperThreshold: | |||
hover_quad_.setQuad(0, high_start_x, getOpenGlYForDb(high_upper_threshold_) - 0.5f * threshold_height, | |||
high_width, threshold_height); | |||
break; | |||
case kLowLowerThreshold: | |||
hover_quad_.setQuad(0, low_start_x, getOpenGlYForDb(low_lower_threshold_) - 0.5f * threshold_height, | |||
low_width, threshold_height); | |||
break; | |||
case kBandLowerThreshold: | |||
hover_quad_.setQuad(0, band_start_x, getOpenGlYForDb(band_lower_threshold_) - 0.5f * threshold_height, | |||
band_width, threshold_height); | |||
break; | |||
case kHighLowerThreshold: | |||
hover_quad_.setQuad(0, high_start_x, getOpenGlYForDb(high_lower_threshold_) - 0.5f * threshold_height, | |||
high_width, threshold_height); | |||
break; | |||
default: | |||
return; | |||
} | |||
if (isRatio(hover_)) | |||
hover_quad_.setColor(findColour(Skin::kLightenScreen, true)); | |||
else | |||
hover_quad_.setColor(findColour(Skin::kWidgetCenterLine, true)); | |||
hover_quad_.render(open_gl, true); | |||
} | |||
void CompressorEditor::renderCompressor(OpenGlWrapper& open_gl, bool animate) { | |||
static constexpr float kOutputBarHeight = 2.2f; | |||
if (low_input_ms_ == nullptr || band_input_ms_ == nullptr || high_input_ms_ == nullptr || | |||
low_output_ms_ == nullptr || band_output_ms_ == nullptr || high_output_ms_ == nullptr) { | |||
return; | |||
} | |||
vital::poly_float low_rms = vital::utils::sqrt(low_input_ms_->value()); | |||
vital::poly_float low_input_y = getOpenGlYForMagnitude(low_rms); | |||
vital::poly_float scaled_low_rms = vital::utils::sqrt(low_output_ms_->value()); | |||
vital::poly_float low_output_y = getOpenGlYForMagnitude(scaled_low_rms); | |||
vital::poly_float band_rms = vital::utils::sqrt(band_input_ms_->value()); | |||
vital::poly_float band_input_y = getOpenGlYForMagnitude(band_rms); | |||
vital::poly_float scaled_band_rms = vital::utils::sqrt(band_output_ms_->value()); | |||
vital::poly_float band_output_y = getOpenGlYForMagnitude(scaled_band_rms); | |||
vital::poly_float high_rms = vital::utils::sqrt(high_input_ms_->value()); | |||
vital::poly_float high_input_y = getOpenGlYForMagnitude(high_rms); | |||
vital::poly_float scaled_high_rms = vital::utils::sqrt(high_output_ms_->value()); | |||
vital::poly_float high_output_y = getOpenGlYForMagnitude(scaled_high_rms); | |||
int width = getWidth(); | |||
float active_area = 1.0f - 4.0f * kCompressorAreaBuffer; | |||
float active_section_width = active_area / kMaxBands; | |||
int low_start = std::round(kCompressorAreaBuffer * width); | |||
int low_end = std::round((kCompressorAreaBuffer + active_section_width) * width); | |||
int band_start = std::round((2.0f * kCompressorAreaBuffer + active_section_width) * width); | |||
int band_end = width - band_start; | |||
int high_start = width - low_end; | |||
int high_end = width - low_start; | |||
if (!low_band_active_) { | |||
band_start = low_start; | |||
low_start = low_end = -width; | |||
} | |||
if (!high_band_active_) { | |||
band_end = high_end; | |||
high_start = high_end = -width; | |||
} | |||
setThresholdPositions(low_start, low_end, band_start, band_end, high_start, high_end, 1.0f); | |||
thresholds_.render(open_gl, true); | |||
setThresholdPositions(low_start, low_end, band_start, band_end, high_start, high_end, 0.0f); | |||
thresholds_.render(open_gl, true); | |||
setThresholdPositions(low_start, low_end, band_start, band_end, high_start, high_end, -1.0f); | |||
thresholds_.render(open_gl, true); | |||
setRatioLinePositions(low_start, low_end, band_start, band_end, high_start, high_end); | |||
ratio_lines_.setColor(findColour(Skin::kLightenScreen, true)); | |||
ratio_lines_.render(open_gl, true); | |||
renderHover(open_gl, low_start, low_end, band_start, band_end, high_start, high_end); | |||
int bar_width = kBarWidth * active_section_width * width; | |||
int low_middle = (low_start + low_end) * 0.5f; | |||
int band_middle = (band_start + band_end) * 0.5f; | |||
int high_middle = (high_start + high_end) * 0.5f; | |||
float gl_bar_width = bar_width * 2.0f / width; | |||
float low_left = (low_middle - bar_width) * 2.0f / width - 1.0f; | |||
float low_right = (low_middle + 1) * 2.0f / width - 1.0f; | |||
float band_left = (band_middle - bar_width) * 2.0f / width - 1.0f; | |||
float band_right = (band_middle + 1) * 2.0f / width - 1.0f; | |||
float high_left = (high_middle - bar_width) * 2.0f / width - 1.0f; | |||
float high_right = (high_middle + 1) * 2.0f / width - 1.0f; | |||
output_dbs_.setQuad(0, low_left, low_output_y[0] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
output_dbs_.setQuad(1, low_right, low_output_y[1] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
output_dbs_.setQuad(2, band_left, band_output_y[0] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
output_dbs_.setQuad(3, band_right, band_output_y[1] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
output_dbs_.setQuad(4, high_left, high_output_y[0] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
output_dbs_.setQuad(5, high_right, high_output_y[1] - kOutputBarHeight, gl_bar_width, kOutputBarHeight); | |||
float input_height = 2.0f / getHeight(); | |||
input_dbs_.setQuad(0, low_left, low_input_y[0] - 0.5f * input_height, gl_bar_width, input_height); | |||
input_dbs_.setQuad(1, low_right, low_input_y[1] - 0.5f * input_height, gl_bar_width, input_height); | |||
input_dbs_.setQuad(2, band_left, band_input_y[0] - 0.5f * input_height, gl_bar_width, input_height); | |||
input_dbs_.setQuad(3, band_right, band_input_y[1] - 0.5f * input_height, gl_bar_width, input_height); | |||
input_dbs_.setQuad(4, high_left, high_input_y[0] - 0.5f * input_height, gl_bar_width, input_height); | |||
input_dbs_.setQuad(5, high_right, high_input_y[1] - 0.5f * input_height, gl_bar_width, input_height); | |||
output_dbs_.setColor(findColour(Skin::kWidgetPrimary1, true)); | |||
output_dbs_.render(open_gl, animate); | |||
input_dbs_.setColor(findColour(Skin::kWidgetPrimary2, true)); | |||
input_dbs_.render(open_gl, animate); | |||
} | |||
void CompressorEditor::destroy(OpenGlWrapper& open_gl) { | |||
OpenGlComponent::destroy(open_gl); | |||
hover_quad_.destroy(open_gl); | |||
input_dbs_.destroy(open_gl); | |||
output_dbs_.destroy(open_gl); | |||
thresholds_.destroy(open_gl); | |||
ratio_lines_.destroy(open_gl); | |||
} | |||
void CompressorEditor::setAllValues(vital::control_map& controls) { | |||
low_upper_threshold_ = controls["compressor_low_upper_threshold"]->value(); | |||
band_upper_threshold_ = controls["compressor_band_upper_threshold"]->value(); | |||
high_upper_threshold_ = controls["compressor_high_upper_threshold"]->value(); | |||
low_lower_threshold_ = controls["compressor_low_lower_threshold"]->value(); | |||
band_lower_threshold_ = controls["compressor_band_lower_threshold"]->value(); | |||
high_lower_threshold_ = controls["compressor_high_lower_threshold"]->value(); | |||
low_upper_ratio_ = controls["compressor_low_upper_ratio"]->value(); | |||
band_upper_ratio_ = controls["compressor_band_upper_ratio"]->value(); | |||
high_upper_ratio_ = controls["compressor_high_upper_ratio"]->value(); | |||
low_lower_ratio_ = controls["compressor_low_lower_ratio"]->value(); | |||
band_lower_ratio_ = controls["compressor_band_lower_ratio"]->value(); | |||
high_lower_ratio_ = controls["compressor_high_lower_ratio"]->value(); | |||
} | |||
void CompressorEditor::setLowUpperThreshold(float db, bool clamp) { | |||
low_upper_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
low_upper_threshold_ = db; | |||
synth->valueChangedInternal("compressor_low_upper_threshold", db); | |||
if (low_upper_threshold_ < low_lower_threshold_ && clamp) | |||
setLowLowerThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(low_upper_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setBandUpperThreshold(float db, bool clamp) { | |||
band_upper_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
band_upper_threshold_ = db; | |||
synth->valueChangedInternal("compressor_band_upper_threshold", db); | |||
if (band_upper_threshold_ < band_lower_threshold_ && clamp) | |||
setBandLowerThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(band_upper_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setHighUpperThreshold(float db, bool clamp) { | |||
high_upper_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
high_upper_threshold_ = db; | |||
synth->valueChangedInternal("compressor_high_upper_threshold", db); | |||
if (high_upper_threshold_ < high_lower_threshold_ && clamp) | |||
setHighLowerThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(high_upper_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setLowLowerThreshold(float db, bool clamp) { | |||
low_lower_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
low_lower_threshold_ = db; | |||
synth->valueChangedInternal("compressor_low_lower_threshold", db); | |||
if (low_lower_threshold_ > low_upper_threshold_ && clamp) | |||
setLowUpperThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(low_lower_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setBandLowerThreshold(float db, bool clamp) { | |||
band_lower_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
band_lower_threshold_ = db; | |||
synth->valueChangedInternal("compressor_band_lower_threshold", db); | |||
if (band_lower_threshold_ > band_upper_threshold_ && clamp) | |||
setBandUpperThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(band_lower_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setHighLowerThreshold(float db, bool clamp ) { | |||
high_lower_threshold_ = db; | |||
db = vital::utils::clamp(db, kMinEditDb, kMaxEditDb); | |||
SynthBase* synth = parent_->getSynth(); | |||
if (clamp) | |||
high_lower_threshold_ = db; | |||
synth->valueChangedInternal("compressor_high_lower_threshold", db); | |||
if (high_lower_threshold_ > high_upper_threshold_ && clamp) | |||
setHighUpperThreshold(db, clamp); | |||
section_parent_->showPopupDisplay(this, formatString(high_lower_threshold_, " dB"), BubbleComponent::below, true); | |||
} | |||
void CompressorEditor::setLowUpperRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
low_upper_ratio_ = vital::utils::clamp(ratio, kMinUpperRatio, kMaxUpperRatio); | |||
synth->valueChangedInternal("compressor_low_upper_ratio", ratio); | |||
} | |||
void CompressorEditor::setBandUpperRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
band_upper_ratio_ = vital::utils::clamp(ratio, kMinUpperRatio, kMaxUpperRatio); | |||
synth->valueChangedInternal("compressor_band_upper_ratio", ratio); | |||
} | |||
void CompressorEditor::setHighUpperRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
high_upper_ratio_ = vital::utils::clamp(ratio, kMinUpperRatio, kMaxUpperRatio); | |||
synth->valueChangedInternal("compressor_high_upper_ratio", ratio); | |||
} | |||
void CompressorEditor::setLowLowerRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
low_lower_ratio_ = vital::utils::clamp(ratio, kMinLowerRatio, kMaxLowerRatio); | |||
synth->valueChangedInternal("compressor_low_lower_ratio", ratio); | |||
} | |||
void CompressorEditor::setBandLowerRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
band_lower_ratio_ = vital::utils::clamp(ratio, kMinLowerRatio, kMaxLowerRatio); | |||
synth->valueChangedInternal("compressor_band_lower_ratio", ratio); | |||
} | |||
void CompressorEditor::setHighLowerRatio(float ratio) { | |||
SynthBase* synth = parent_->getSynth(); | |||
high_lower_ratio_ = vital::utils::clamp(ratio, kMinLowerRatio, kMaxLowerRatio); | |||
synth->valueChangedInternal("compressor_high_lower_ratio", ratio); | |||
} | |||
String CompressorEditor::formatValue(float value) { | |||
static constexpr int number_length = 5; | |||
static constexpr int max_decimals = 3; | |||
String format = String(value, max_decimals); | |||
format = format.substring(0, number_length); | |||
int spaces = number_length - format.length(); | |||
for (int i = 0; i < spaces; ++i) | |||
format = " " + format; | |||
return format; | |||
} | |||
void CompressorEditor::parentHierarchyChanged() { | |||
if (parent_ == nullptr) | |||
parent_ = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (section_parent_ == nullptr) | |||
section_parent_ = findParentComponentOfClass<SynthSection>(); | |||
if (parent_ == nullptr) | |||
return; | |||
if (low_input_ms_ == nullptr) | |||
low_input_ms_ = parent_->getSynth()->getStatusOutput("compressor_low_input"); | |||
if (band_input_ms_ == nullptr) | |||
band_input_ms_ = parent_->getSynth()->getStatusOutput("compressor_band_input"); | |||
if (high_input_ms_ == nullptr) | |||
high_input_ms_ = parent_->getSynth()->getStatusOutput("compressor_high_input"); | |||
if (low_output_ms_ == nullptr) | |||
low_output_ms_ = parent_->getSynth()->getStatusOutput("compressor_low_output"); | |||
if (band_output_ms_ == nullptr) | |||
band_output_ms_ = parent_->getSynth()->getStatusOutput("compressor_band_output"); | |||
if (high_output_ms_ == nullptr) | |||
high_output_ms_ = parent_->getSynth()->getStatusOutput("compressor_high_output"); | |||
OpenGlComponent::parentHierarchyChanged(); | |||
} | |||
float CompressorEditor::getYForDb(float db) { | |||
return getHeight() * (1.0f - getOpenGlYForDb(vital::utils::clamp(db, kMinDb, kMaxDb))) * 0.5f; | |||
} | |||
float CompressorEditor::getCompressedDb(float input_db, float upper_threshold, float upper_ratio, | |||
float lower_threshold, float lower_ratio) { | |||
if (input_db < lower_threshold) | |||
return vital::utils::interpolate(input_db, lower_threshold, lower_ratio); | |||
if (input_db > upper_threshold) | |||
return vital::utils::interpolate(input_db, upper_threshold, upper_ratio); | |||
return input_db; | |||
} | |||
Colour CompressorEditor::getColorForRatio(float ratio) { | |||
if (!active_) | |||
return findColour(Skin::kWidgetSecondaryDisabled, true); | |||
if (ratio > 0.0f) | |||
return findColour(Skin::kWidgetSecondary1, true); | |||
if (ratio < 0.0f) | |||
return findColour(Skin::kWidgetSecondary2, true); | |||
return findColour(Skin::kWidgetSecondaryDisabled, true); | |||
} |
@@ -0,0 +1,172 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "line_generator.h" | |||
#include "open_gl_component.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "synth_slider.h" | |||
#include "synth_module.h" | |||
class SynthGuiInterface; | |||
class CompressorEditor : public OpenGlComponent, public SynthSlider::SliderListener { | |||
public: | |||
static constexpr float kGrabRadius = 8.0f; | |||
static constexpr float kMinDb = -80.0f; | |||
static constexpr float kMaxDb = 0.0f; | |||
static constexpr float kDbEditBuffer = 1.0f; | |||
static constexpr float kMinEditDb = kMinDb + kDbEditBuffer; | |||
static constexpr float kMaxEditDb = kMaxDb - kDbEditBuffer; | |||
static constexpr float kMinLowerRatio = -1.0f; | |||
static constexpr float kMaxLowerRatio = 1.0f; | |||
static constexpr float kMinUpperRatio = 0.0f; | |||
static constexpr float kMaxUpperRatio = 1.0f; | |||
static constexpr float kRatioEditMultiplier = 0.6f; | |||
static constexpr float kCompressorAreaBuffer = 0.05f; | |||
static constexpr float kBarWidth = 1.0f / 5.0f; | |||
static constexpr float kInputLineRadius = 0.02f; | |||
static constexpr float kMouseMultiplier = 1.0f; | |||
static constexpr int kMaxBands = 3; | |||
static constexpr int kNumChannels = kMaxBands * 2; | |||
static constexpr int kDbLineSections = 8; | |||
static constexpr int kExtraDbLines = 6; | |||
static constexpr int kRatioDbLines = kDbLineSections + kExtraDbLines; | |||
static constexpr int kTotalRatioLines = kRatioDbLines * kNumChannels; | |||
CompressorEditor(); | |||
virtual ~CompressorEditor(); | |||
void paintBackground(Graphics& g) override; | |||
void resized() override; | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDoubleClick(const MouseEvent& e) override; | |||
void mouseMove(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseUp(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void parentHierarchyChanged() override; | |||
void init(OpenGlWrapper& open_gl) override; | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void renderCompressor(OpenGlWrapper& open_gl, bool animate); | |||
void destroy(OpenGlWrapper& open_gl) override; | |||
void setSizeRatio(float ratio) { size_ratio_ = ratio; } | |||
void setAllValues(vital::control_map& controls); | |||
void setHighBandActive(bool active) { high_band_active_ = active; } | |||
void setLowBandActive(bool active) { low_band_active_ = active; } | |||
void setActive(bool active) { active_ = active; } | |||
private: | |||
enum DragPoint { | |||
kNone, | |||
kLowUpperThreshold, | |||
kBandUpperThreshold, | |||
kHighUpperThreshold, | |||
kLowLowerThreshold, | |||
kBandLowerThreshold, | |||
kHighLowerThreshold, | |||
kLowUpperRatio, | |||
kBandUpperRatio, | |||
kHighUpperRatio, | |||
kLowLowerRatio, | |||
kBandLowerRatio, | |||
kHighLowerRatio, | |||
kNumDragPoints | |||
}; | |||
bool isRatio(DragPoint drag_point) { | |||
return drag_point == kLowLowerRatio || drag_point == kBandLowerRatio || drag_point == kHighLowerRatio || | |||
drag_point == kLowUpperRatio || drag_point == kBandUpperRatio || drag_point == kHighUpperRatio; | |||
} | |||
void setThresholdPositions(int low_start, int low_end, int band_start, int band_end, | |||
int high_start, int high_end, float ratio_match); | |||
void setRatioLines(int start_index, int start_x, int end_x, float threshold, float ratio, bool upper, bool hover); | |||
void setRatioLinePositions(int low_start, int low_end, int band_start, int band_end, | |||
int high_start, int high_end); | |||
void renderHover(OpenGlWrapper& open_gl, int low_start, int low_end, | |||
int band_start, int band_end, int high_start, int high_end); | |||
void setLowUpperThreshold(float db, bool clamp); | |||
void setBandUpperThreshold(float db, bool clamp); | |||
void setHighUpperThreshold(float db, bool clamp); | |||
void setLowLowerThreshold(float db, bool clamp); | |||
void setBandLowerThreshold(float db, bool clamp); | |||
void setHighLowerThreshold(float db, bool clamp); | |||
void setLowUpperRatio(float ratio); | |||
void setBandUpperRatio(float ratio); | |||
void setHighUpperRatio(float ratio); | |||
void setLowLowerRatio(float ratio); | |||
void setBandLowerRatio(float ratio); | |||
void setHighLowerRatio(float ratio); | |||
String formatValue(float value); | |||
DragPoint getHoverPoint(const MouseEvent& e); | |||
float getYForDb(float db); | |||
float getCompressedDb(float input_db, float upper_threshold, float upper_ratio, | |||
float lower_threshold, float lower_ratio); | |||
Colour getColorForRatio(float ratio); | |||
SynthGuiInterface* parent_; | |||
SynthSection* section_parent_; | |||
DragPoint hover_; | |||
Point<int> last_mouse_position_; | |||
OpenGlQuad hover_quad_; | |||
OpenGlMultiQuad input_dbs_; | |||
OpenGlMultiQuad output_dbs_; | |||
OpenGlMultiQuad thresholds_; | |||
OpenGlMultiQuad ratio_lines_; | |||
float low_upper_threshold_; | |||
float band_upper_threshold_; | |||
float high_upper_threshold_; | |||
float low_lower_threshold_; | |||
float band_lower_threshold_; | |||
float high_lower_threshold_; | |||
float low_upper_ratio_; | |||
float band_upper_ratio_; | |||
float high_upper_ratio_; | |||
float low_lower_ratio_; | |||
float band_lower_ratio_; | |||
float high_lower_ratio_; | |||
const vital::StatusOutput* low_input_ms_; | |||
const vital::StatusOutput* band_input_ms_; | |||
const vital::StatusOutput* high_input_ms_; | |||
const vital::StatusOutput* low_output_ms_; | |||
const vital::StatusOutput* band_output_ms_; | |||
const vital::StatusOutput* high_output_ms_; | |||
float size_ratio_; | |||
bool animate_; | |||
bool active_; | |||
bool high_band_active_; | |||
bool low_band_active_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CompressorEditor) | |||
}; | |||
@@ -0,0 +1,311 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "drag_drop_effect_order.h" | |||
#include "skin.h" | |||
#include "fonts.h" | |||
#include "paths.h" | |||
#include "synth_button.h" | |||
#include "synth_gui_interface.h" | |||
#include "synth_strings.h" | |||
#include "text_look_and_feel.h" | |||
#include "utils.h" | |||
namespace { | |||
Path getPathForEffect(String effect) { | |||
if (effect == "compressor") | |||
return Paths::compressor(); | |||
if (effect == "chorus") | |||
return Paths::chorus(); | |||
if (effect == "delay") | |||
return Paths::delay(); | |||
if (effect == "distortion") | |||
return Paths::distortion(); | |||
if (effect == "eq") | |||
return Paths::equalizer(); | |||
if (effect == "filter") | |||
return Paths::effectsFilter(); | |||
if (effect == "flanger") | |||
return Paths::flanger(); | |||
if (effect == "phaser") | |||
return Paths::phaser(); | |||
if (effect == "reverb") | |||
return Paths::reverb(); | |||
return Path(); | |||
} | |||
} | |||
DraggableEffect::DraggableEffect(const String& name, int order) : SynthSection(name), order_(order), hover_(false) { | |||
setInterceptsMouseClicks(false, true); | |||
StringArray tokens; | |||
tokens.addTokens(getName(), "_", ""); | |||
icon_ = getPathForEffect(tokens[0].toLowerCase()); | |||
background_ = std::make_unique<OpenGlImageComponent>("background"); | |||
addOpenGlComponent(background_.get()); | |||
background_->setComponent(this); | |||
background_->paintEntireComponent(false); | |||
enable_ = std::make_unique<SynthButton>(name + "_on"); | |||
enable_->setPowerButton(); | |||
addButton(enable_.get()); | |||
enable_->setButtonText(""); | |||
enable_->getGlComponent()->setAlwaysOnTop(true); | |||
} | |||
void DraggableEffect::paint(Graphics& g) { | |||
static constexpr float kLeftPadding = 0.07f; | |||
static constexpr float kIconSize = 0.6f; | |||
static constexpr int kTextureRows = 2; | |||
static constexpr int kTextureColumns = 3; | |||
static constexpr float kTextureYStart = 0.13f; | |||
static constexpr float kTexturePadding = 0.45f; | |||
static constexpr float kTextureCircleRadiusPercent = 0.25f; | |||
g.setColour(getParentComponent()->findColour(Skin::kBody, true)); | |||
int round_amount = findValue(Skin::kBodyRounding); | |||
g.fillRoundedRectangle(0, 0, getWidth(), getHeight(), round_amount); | |||
if (enable_->getToggleState()) | |||
g.setColour(findColour(Skin::kPowerButtonOn, true)); | |||
else | |||
g.setColour(findColour(Skin::kPowerButtonOff, true)); | |||
g.drawRoundedRectangle(0.5f, 0.5f, getWidth() - 1.0f, getHeight() - 1.0f, round_amount, 1.0f); | |||
g.setFont(Fonts::instance()->proportional_regular().withPointHeight(size_ratio_ * 12.0f)); | |||
float text_x = getWidth() * kLeftPadding; | |||
StringArray tokens; | |||
tokens.addTokens(getName(), "_", ""); | |||
String text = tokens[0]; | |||
text = text.substring(0, 1).toUpperCase() + text.substring(1); | |||
g.drawText(text, text_x, 0, getWidth() - text_x, getHeight(), Justification::centredLeft, true); | |||
float icon_width = kIconSize * getHeight(); | |||
float icon_x = getWidth() / 2.0f + (getWidth() / 2.0f - icon_width) / 2.0f; | |||
float icon_y = (getHeight() - icon_width) / 2.0f; | |||
Rectangle<float> icon_bounds(icon_x, icon_y, icon_width, icon_width); | |||
g.fillPath(icon_, icon_.getTransformToScaleToFit(icon_bounds, true)); | |||
if (hover_) { | |||
g.setColour(findColour(Skin::kLightenScreen, true).withMultipliedAlpha(1.5f)); | |||
int width = getWidth(); | |||
int height = getHeight(); | |||
float spacing = width * (1.0f - 2.0f * kTexturePadding) / (kTextureColumns - 1.0f); | |||
float radius = spacing * kTextureCircleRadiusPercent; | |||
float x = width * kTexturePadding; | |||
float y = height * kTextureYStart; | |||
for (int c = 0; c < kTextureColumns; ++c) { | |||
for (int r = 0; r < kTextureRows; ++r) { | |||
g.fillEllipse(x + spacing * c - radius, y + spacing * r - radius, 2.0f * radius, 2.0f * radius); | |||
g.fillEllipse(x + spacing * c - radius, height - y - spacing * r - radius, 2.0f * radius, 2.0f * radius); | |||
} | |||
} | |||
} | |||
} | |||
void DraggableEffect::resized() { | |||
int width = getTitleWidth(); | |||
enable_->setBounds(0, 0, width, width); | |||
background_->redrawImage(true); | |||
} | |||
void DraggableEffect::buttonClicked(Button* clicked_button) { | |||
for (Listener* listener : listeners_) | |||
listener->effectEnabledChanged(this, clicked_button->getToggleState()); | |||
background_->redrawImage(true); | |||
SynthSection::buttonClicked(clicked_button); | |||
} | |||
void DraggableEffect::hover(bool hover) { | |||
if (hover_ != hover) { | |||
hover_ = hover; | |||
background_->redrawImage(true); | |||
} | |||
} | |||
DragDropEffectOrder::DragDropEffectOrder(String name) : SynthSection(name) { | |||
currently_dragged_ = nullptr; | |||
currently_hovered_ = nullptr; | |||
last_dragged_index_ = 0; | |||
mouse_down_y_ = 0; | |||
dragged_starting_y_ = 0; | |||
for (int i = 0; i < vital::constants::kNumEffects; ++i) { | |||
effect_order_[i] = i; | |||
effect_list_.push_back(std::make_unique<DraggableEffect>(strings::kEffectOrder[i], i)); | |||
addSubSection(effect_list_[i].get()); | |||
effect_list_[i]->addListener(this); | |||
effect_list_[i]->setSkinOverride(static_cast<Skin::SectionOverride>(Skin::kAllEffects + 1 + i)); | |||
} | |||
} | |||
DragDropEffectOrder::~DragDropEffectOrder() { } | |||
void DragDropEffectOrder::resized() { | |||
float padding = size_ratio_ * kEffectPadding; | |||
for (int i = 0; i < vital::constants::kNumEffects; ++i) { | |||
Component* effect = getEffect(i); | |||
int from_y = getEffectY(i); | |||
int to_y = getEffectY(i + 1); | |||
effect->setBounds(0, from_y, getWidth(), to_y - from_y - padding); | |||
} | |||
} | |||
void DragDropEffectOrder::paintBackground(Graphics& g) { | |||
static constexpr float kConnectionWidth = 0.1f; | |||
g.setColour(findColour(Skin::kLightenScreen, true)); | |||
int width = getWidth() * kConnectionWidth; | |||
int center = getWidth() / 2.0f + findValue(Skin::kWidgetRoundedCorner); | |||
g.fillRect(center - width / 2, 0, width, getHeight()); | |||
} | |||
void DragDropEffectOrder::renderOpenGlComponents(OpenGlWrapper& open_gl, bool animate) { | |||
SynthSection::renderOpenGlComponents(open_gl, animate); | |||
DraggableEffect* current = currently_dragged_; | |||
if (current) | |||
current->renderOpenGlComponents(open_gl, animate); | |||
} | |||
void DragDropEffectOrder::mouseMove(const MouseEvent& e) { | |||
int current_index = getEffectIndexFromY(e.y); | |||
DraggableEffect* hover = effect_list_[effect_order_[current_index]].get(); | |||
if (hover != currently_hovered_) { | |||
if (currently_hovered_) | |||
currently_hovered_->hover(false); | |||
if (hover) | |||
hover->hover(true); | |||
currently_hovered_ = hover; | |||
} | |||
} | |||
void DragDropEffectOrder::mouseDown(const MouseEvent& e) { | |||
mouse_down_y_ = e.y; | |||
last_dragged_index_ = getEffectIndexFromY(e.y); | |||
currently_dragged_ = effect_list_[effect_order_[last_dragged_index_]].get(); | |||
dragged_starting_y_ = currently_dragged_->getY(); | |||
currently_dragged_->setAlwaysOnTop(true); | |||
} | |||
void DragDropEffectOrder::mouseDrag(const MouseEvent& e) { | |||
if (currently_dragged_ == nullptr) | |||
return; | |||
int delta_y = e.y - mouse_down_y_; | |||
int clamped_y = vital::utils::iclamp(dragged_starting_y_ + delta_y, 0, | |||
getHeight() - currently_dragged_->getHeight()); | |||
currently_dragged_->setTopLeftPosition(currently_dragged_->getX(), clamped_y); | |||
int next_index = getEffectIndexFromY(e.y); | |||
if (next_index != last_dragged_index_) { | |||
moveEffect(last_dragged_index_, next_index); | |||
last_dragged_index_ = next_index; | |||
} | |||
} | |||
void DragDropEffectOrder::mouseUp(const MouseEvent& e) { | |||
if (currently_dragged_) | |||
currently_dragged_->setAlwaysOnTop(false); | |||
currently_dragged_ = nullptr; | |||
setStationaryEffectPosition(last_dragged_index_); | |||
} | |||
void DragDropEffectOrder::mouseExit(const MouseEvent& e) { | |||
if (currently_hovered_) { | |||
currently_hovered_->hover(false); | |||
currently_hovered_ = nullptr; | |||
} | |||
} | |||
void DragDropEffectOrder::effectEnabledChanged(DraggableEffect* effect, bool enabled) { | |||
for (Listener* listener : listeners_) | |||
listener->effectEnabledChanged(effect->order(), enabled); | |||
} | |||
void DragDropEffectOrder::setAllValues(vital::control_map& controls) { | |||
SynthSection::setAllValues(controls); | |||
float order = controls[getName().toStdString()]->value(); | |||
vital::utils::decodeFloatToOrder(effect_order_, order, vital::constants::kNumEffects); | |||
for (int i = 0; i < vital::constants::kNumEffects; ++i) | |||
setStationaryEffectPosition(i); | |||
for (Listener* listener : listeners_) | |||
listener->orderChanged(this); | |||
} | |||
void DragDropEffectOrder::moveEffect(int start_index, int end_index) { | |||
int delta_index = end_index - start_index; | |||
if (delta_index == 0) | |||
return; | |||
int moving = effect_order_[start_index]; | |||
int direction = delta_index / abs(delta_index); | |||
for (int i = start_index; i != end_index; i += direction) { | |||
effect_order_[i] = effect_order_[i + direction]; | |||
setStationaryEffectPosition(i); | |||
} | |||
effect_order_[end_index] = moving; | |||
float order = vital::utils::encodeOrderToFloat(effect_order_, vital::constants::kNumEffects); | |||
SynthGuiInterface* parent = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (parent) | |||
parent->getSynth()->valueChangedInternal(getName().toStdString(), order); | |||
for (Listener* listener : listeners_) | |||
listener->orderChanged(this); | |||
} | |||
void DragDropEffectOrder::setStationaryEffectPosition(int index) { | |||
float padding = size_ratio_ * kEffectPadding; | |||
Component* effect = getEffect(index); | |||
int from_y = getEffectY(index); | |||
int to_y = getEffectY(index + 1); | |||
effect->setBounds(0, from_y, getWidth(), to_y - from_y - padding); | |||
} | |||
int DragDropEffectOrder::getEffectIndex(int index) const { | |||
int i = vital::utils::iclamp(index, 0, vital::constants::kNumEffects - 1); | |||
return effect_order_[i]; | |||
} | |||
Component* DragDropEffectOrder::getEffect(int index) const { | |||
return effect_list_[getEffectIndex(index)].get(); | |||
} | |||
bool DragDropEffectOrder::effectEnabled(int index) const { | |||
return effect_list_[getEffectIndex(index)]->enabled(); | |||
} | |||
int DragDropEffectOrder::getEffectIndexFromY(float y) const { | |||
float padding = size_ratio_ * kEffectPadding; | |||
int index = vital::constants::kNumEffects * (y + padding / 2.0f) / (getHeight() + padding); | |||
return vital::utils::iclamp(index, 0, vital::constants::kNumEffects - 1); | |||
} | |||
int DragDropEffectOrder::getEffectY(int index) const { | |||
int padding = size_ratio_ * kEffectPadding; | |||
return std::round((1.0f * index * (getHeight() + padding)) / vital::constants::kNumEffects); | |||
} |
@@ -0,0 +1,111 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "synth_section.h" | |||
#include "synth_constants.h" | |||
#include "synth_button.h" | |||
#include "synth_slider.h" | |||
#include "open_gl_image_component.h" | |||
class DraggableEffect : public SynthSection { | |||
public: | |||
class Listener { | |||
public: | |||
virtual ~Listener() { } | |||
virtual void effectEnabledChanged(DraggableEffect* effect, bool enabled) = 0; | |||
}; | |||
DraggableEffect(const String& name, int order); | |||
void paint(Graphics& g) override; | |||
void paintBackground(Graphics& g) override { } | |||
void renderOpenGlComponents(OpenGlWrapper& open_gl, bool animate) override { | |||
SynthSection::renderOpenGlComponents(open_gl, animate); | |||
} | |||
void resized() override; | |||
void buttonClicked(Button* clicked_button) override; | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
bool enabled() const { return enable_->getToggleState(); } | |||
int order() const { return order_; } | |||
void hover(bool hover); | |||
private: | |||
Path icon_; | |||
int order_; | |||
bool hover_; | |||
std::unique_ptr<SynthButton> enable_; | |||
std::unique_ptr<OpenGlImageComponent> background_; | |||
std::vector<Listener*> listeners_; | |||
}; | |||
class DragDropEffectOrder : public SynthSection, public DraggableEffect::Listener { | |||
public: | |||
static constexpr int kEffectPadding = 6; | |||
class Listener { | |||
public: | |||
virtual ~Listener() { } | |||
virtual void orderChanged(DragDropEffectOrder* order) = 0; | |||
virtual void effectEnabledChanged(int order_index, bool enabled) = 0; | |||
}; | |||
DragDropEffectOrder(String name); | |||
virtual ~DragDropEffectOrder(); | |||
void resized() override; | |||
void paintBackground(Graphics& g) override; | |||
void renderOpenGlComponents(OpenGlWrapper& open_gl, bool animate) override; | |||
void mouseMove(const MouseEvent& e) override; | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseUp(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void effectEnabledChanged(DraggableEffect* effect, bool enabled) override; | |||
void setAllValues(vital::control_map& controls) override; | |||
void moveEffect(int start_index, int end_index); | |||
void setStationaryEffectPosition(int index); | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
int getEffectIndex(int index) const; | |||
Component* getEffect(int index) const; | |||
bool effectEnabled(int index) const; | |||
int getEffectIndexFromY(float y) const; | |||
private: | |||
int getEffectY(int index) const; | |||
std::vector<Listener*> listeners_; | |||
DraggableEffect* currently_dragged_; | |||
DraggableEffect* currently_hovered_; | |||
int last_dragged_index_; | |||
int mouse_down_y_; | |||
int dragged_starting_y_; | |||
std::vector<std::unique_ptr<DraggableEffect>> effect_list_; | |||
int effect_order_[vital::constants::kNumEffects]; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DragDropEffectOrder) | |||
}; | |||
@@ -0,0 +1,230 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "skin.h" | |||
#include "open_gl_image.h" | |||
#include "open_gl_line_renderer.h" | |||
#include "synth_module.h" | |||
#include "synth_slider.h" | |||
class EnvelopeEditor : public OpenGlLineRenderer, public SynthSlider::SliderListener { | |||
public: | |||
static constexpr float kMarkerWidth = 9.0f; | |||
static constexpr float kRingThickness = 0.45f; | |||
static constexpr float kPowerMarkerWidth = 7.0f; | |||
static constexpr float kMarkerHoverRadius = 12.0f; | |||
static constexpr float kMarkerGrabRadius = 20.0f; | |||
static constexpr float kTailDecay = 0.965f; | |||
static constexpr float kPaddingX = 0.018f; | |||
static constexpr float kPaddingY = 0.06f; | |||
static constexpr float kMinPointDistanceForPower = 3.0f; | |||
static constexpr float kPowerMouseMultiplier = 0.06f; | |||
static constexpr float kTimeDisplaySize = 0.05f; | |||
static constexpr int kRulerDivisionSize = 4; | |||
static constexpr int kMaxGridLines = 36; | |||
static constexpr int kMaxTimesShown = 24; | |||
static constexpr int kNumPointsPerSection = 98; | |||
static constexpr int kNumSections = 4; | |||
static constexpr int kTotalPoints = kNumSections * kNumPointsPerSection + 1; | |||
EnvelopeEditor(const String& prefix, | |||
const vital::output_map& mono_modulations, const vital::output_map& poly_modulations); | |||
~EnvelopeEditor(); | |||
void paintBackground(Graphics& g) override; | |||
void resized() override { | |||
OpenGlLineRenderer::resized(); | |||
drag_circle_.setBounds(getLocalBounds()); | |||
hover_circle_.setBounds(getLocalBounds()); | |||
grid_lines_.setBounds(getLocalBounds()); | |||
sub_grid_lines_.setBounds(getLocalBounds()); | |||
position_circle_.setBounds(getLocalBounds()); | |||
point_circles_.setBounds(getLocalBounds()); | |||
power_circles_.setBounds(getLocalBounds()); | |||
float font_height = kTimeDisplaySize * getHeight(); | |||
for (int i = 0; i < kMaxTimesShown; ++i) | |||
times_[i]->setTextSize(font_height); | |||
setTimePositions(); | |||
reset_positions_ = true; | |||
} | |||
void resetEnvelopeLine(int index); | |||
void guiChanged(SynthSlider* slider) override; | |||
void setDelaySlider(SynthSlider* delay_slider); | |||
void setAttackSlider(SynthSlider* attack_slider); | |||
void setAttackPowerSlider(SynthSlider* attack_slider); | |||
void setHoldSlider(SynthSlider* hold_slider); | |||
void setDecaySlider(SynthSlider* decay_slider); | |||
void setDecayPowerSlider(SynthSlider* decay_slider); | |||
void setSustainSlider(SynthSlider* sustain_slider); | |||
void setReleaseSlider(SynthSlider* release_slider); | |||
void setReleasePowerSlider(SynthSlider* release_slider); | |||
void setSizeRatio(float ratio) { size_ratio_ = ratio; } | |||
void parentHierarchyChanged() override; | |||
void pickHoverPosition(Point<float> position); | |||
void mouseMove(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseDoubleClick(const MouseEvent& e) override; | |||
void mouseUp(const MouseEvent& e) override; | |||
void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override; | |||
void magnifyZoom(Point<float> delta); | |||
void magnifyReset(); | |||
void init(OpenGlWrapper& open_gl) override; | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void destroy(OpenGlWrapper& open_gl) override; | |||
void resetPositions() { reset_positions_ = true; } | |||
private: | |||
void setEditingCircleBounds(); | |||
void setGridPositions(); | |||
void setTimePositions(); | |||
void setPointPositions(); | |||
void setGlPositions(); | |||
void setColors(); | |||
void zoom(float amount); | |||
static std::pair<vital::Output*, vital::Output*> getOutputs(const vital::output_map& mono_modulations, | |||
const vital::output_map& poly_modulations, | |||
const String& name) { | |||
return { | |||
mono_modulations.at(name.toStdString()), | |||
poly_modulations.at(name.toStdString()) | |||
}; | |||
} | |||
vital::poly_float getOutputsTotal(std::pair<vital::Output*, vital::Output*> outputs, | |||
vital::poly_float default_value); | |||
void drawPosition(OpenGlWrapper& open_gl, int index); | |||
std::pair<float, float> getPosition(int index); | |||
float padX(float x); | |||
float padY(float y); | |||
float unpadX(float x); | |||
float unpadY(float y); | |||
float padOpenGlX(float x); | |||
float padOpenGlY(float y); | |||
float getSliderDelayX(); | |||
float getSliderAttackX(); | |||
float getSliderHoldX(); | |||
float getSliderDecayX(); | |||
float getSliderSustainY(); | |||
float getSliderReleaseX(); | |||
float getDelayTime(int index); | |||
float getAttackTime(int index); | |||
float getHoldTime(int index); | |||
float getDecayTime(int index); | |||
float getReleaseTime(int index); | |||
float getDelayX(int index); | |||
float getAttackX(int index); | |||
float getHoldX(int index); | |||
float getDecayX(int index); | |||
float getSustainY(int index); | |||
float getReleaseX(int index); | |||
float getBackupPhase(float phase, int index); | |||
vital::poly_float getBackupPhase(vital::poly_float phase); | |||
float getEnvelopeValue(float t, float power, float start, float end); | |||
float getSliderAttackValue(float t); | |||
float getSliderDecayValue(float t); | |||
float getSliderReleaseValue(float t); | |||
float getAttackValue(float t, int index); | |||
float getDecayValue(float t, int index); | |||
float getReleaseValue(float t, int index); | |||
void setDelayX(float x); | |||
void setAttackX(float x); | |||
void setHoldX(float x); | |||
void setDecayX(float x); | |||
void setSustainY(float y); | |||
void setReleaseX(float x); | |||
void setPower(SynthSlider* slider, float power); | |||
void setAttackPower(float power); | |||
void setDecayPower(float power); | |||
void setReleasePower(float power); | |||
SynthGuiInterface* parent_; | |||
bool delay_hover_; | |||
bool attack_hover_; | |||
bool hold_hover_; | |||
bool sustain_hover_; | |||
bool release_hover_; | |||
bool attack_power_hover_; | |||
bool decay_power_hover_; | |||
bool release_power_hover_; | |||
bool mouse_down_; | |||
Point<float> last_edit_position_; | |||
bool animate_; | |||
float size_ratio_; | |||
float window_time_; | |||
vital::poly_float current_position_alpha_; | |||
vital::poly_float last_phase_; | |||
Colour line_left_color_; | |||
Colour line_right_color_; | |||
Colour line_center_color_; | |||
Colour fill_left_color_; | |||
Colour fill_right_color_; | |||
Colour background_color_; | |||
Colour time_color_; | |||
bool reset_positions_; | |||
OpenGlQuad drag_circle_; | |||
OpenGlQuad hover_circle_; | |||
OpenGlMultiQuad grid_lines_; | |||
OpenGlMultiQuad sub_grid_lines_; | |||
OpenGlQuad position_circle_; | |||
OpenGlMultiQuad point_circles_; | |||
OpenGlMultiQuad power_circles_; | |||
std::unique_ptr<PlainTextComponent> times_[kMaxTimesShown]; | |||
const vital::StatusOutput* envelope_phase_; | |||
SynthSlider* delay_slider_; | |||
SynthSlider* attack_slider_; | |||
SynthSlider* hold_slider_; | |||
SynthSlider* attack_power_slider_; | |||
SynthSlider* decay_slider_; | |||
SynthSlider* decay_power_slider_; | |||
SynthSlider* sustain_slider_; | |||
SynthSlider* release_slider_; | |||
SynthSlider* release_power_slider_; | |||
std::pair<vital::Output*, vital::Output*> delay_outputs_; | |||
std::pair<vital::Output*, vital::Output*> attack_outputs_; | |||
std::pair<vital::Output*, vital::Output*> hold_outputs_; | |||
std::pair<vital::Output*, vital::Output*> decay_outputs_; | |||
std::pair<vital::Output*, vital::Output*> sustain_outputs_; | |||
std::pair<vital::Output*, vital::Output*> release_outputs_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EnvelopeEditor) | |||
}; |
@@ -0,0 +1,581 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "equalizer_response.h" | |||
#include "shaders.h" | |||
#include "skin.h" | |||
#include "utils.h" | |||
EqualizerResponse::EqualizerResponse() : OpenGlLineRenderer(kResolution), | |||
unselected_points_(2, Shaders::kRingFragment), | |||
selected_point_(Shaders::kCircleFragment), | |||
dragging_point_(Shaders::kCircleFragment), shader_(nullptr) { | |||
unselected_points_.setThickness(1.0f); | |||
setFill(true); | |||
addAndMakeVisible(unselected_points_); | |||
addAndMakeVisible(selected_point_); | |||
addAndMakeVisible(dragging_point_); | |||
low_cutoff_ = nullptr; | |||
low_resonance_ = nullptr; | |||
low_gain_ = nullptr; | |||
band_cutoff_ = nullptr; | |||
band_resonance_ = nullptr; | |||
band_gain_ = nullptr; | |||
high_cutoff_ = nullptr; | |||
high_resonance_ = nullptr; | |||
high_gain_ = nullptr; | |||
low_cutoff_output_ = nullptr; | |||
low_resonance_output_ = nullptr; | |||
low_gain_output_ = nullptr; | |||
band_cutoff_output_ = nullptr; | |||
band_resonance_output_ = nullptr; | |||
band_gain_output_ = nullptr; | |||
high_cutoff_output_ = nullptr; | |||
high_resonance_output_ = nullptr; | |||
high_gain_output_ = nullptr; | |||
current_cutoff_ = nullptr; | |||
current_gain_ = nullptr; | |||
low_filter_.setSampleRate(kViewSampleRate); | |||
band_filter_.setSampleRate(kViewSampleRate); | |||
high_filter_.setSampleRate(kViewSampleRate); | |||
low_filter_.setDriveCompensation(false); | |||
high_filter_.setDriveCompensation(false); | |||
active_ = false; | |||
high_pass_ = false; | |||
notch_ = false; | |||
low_pass_ = false; | |||
animate_ = true; | |||
draw_frequency_lines_ = true; | |||
selected_band_ = 0; | |||
line_data_ = std::make_unique<float[]>(kResolution); | |||
line_buffer_ = 0; | |||
response_buffer_ = 0; | |||
vertex_array_object_ = 0; | |||
for (int i = 0; i < kResolution; ++i) { | |||
float t = i / (kResolution - 1.0f); | |||
line_data_[i] = 2.0f * t - 1.0f; | |||
} | |||
db_buffer_ratio_ = kDefaultDbBufferRatio; | |||
min_db_ = 0.0f; | |||
max_db_ = 1.0f; | |||
} | |||
EqualizerResponse::~EqualizerResponse() = default; | |||
void EqualizerResponse::initEq(const vital::output_map& mono_modulations) { | |||
low_cutoff_output_ = mono_modulations.at("eq_low_cutoff"); | |||
low_resonance_output_ = mono_modulations.at("eq_low_resonance"); | |||
low_gain_output_ = mono_modulations.at("eq_low_gain"); | |||
band_cutoff_output_ = mono_modulations.at("eq_band_cutoff"); | |||
band_resonance_output_ = mono_modulations.at("eq_band_resonance"); | |||
band_gain_output_ = mono_modulations.at("eq_band_gain"); | |||
high_cutoff_output_ = mono_modulations.at("eq_high_cutoff"); | |||
high_resonance_output_ = mono_modulations.at("eq_high_resonance"); | |||
high_gain_output_ = mono_modulations.at("eq_high_gain"); | |||
} | |||
void EqualizerResponse::initReverb(const vital::output_map& mono_modulations) { | |||
low_cutoff_output_ = mono_modulations.at("reverb_low_shelf_cutoff"); | |||
low_gain_output_ = mono_modulations.at("reverb_low_shelf_gain"); | |||
high_cutoff_output_ = mono_modulations.at("reverb_high_shelf_cutoff"); | |||
high_gain_output_ = mono_modulations.at("reverb_high_shelf_gain"); | |||
} | |||
void EqualizerResponse::init(OpenGlWrapper& open_gl) { | |||
OpenGlLineRenderer::init(open_gl); | |||
unselected_points_.init(open_gl); | |||
selected_point_.init(open_gl); | |||
dragging_point_.init(open_gl); | |||
open_gl.context.extensions.glGenVertexArrays(1, &vertex_array_object_); | |||
open_gl.context.extensions.glBindVertexArray(vertex_array_object_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(kResolution * sizeof(float)); | |||
open_gl.context.extensions.glGenBuffers(1, &line_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, line_buffer_); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, line_data_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &response_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, response_buffer_); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, nullptr, GL_STATIC_READ); | |||
const GLchar* varyings[] = { "response_out" }; | |||
shader_ = open_gl.shaders->getShaderProgram(Shaders::kEqFilterResponseVertex, Shaders::kColorFragment, varyings); | |||
shader_->use(); | |||
position_attribute_ = getAttribute(open_gl, *shader_, "position"); | |||
midi_cutoff_uniform_ = getUniform(open_gl, *shader_, "midi_cutoff"); | |||
resonance_uniform_ = getUniform(open_gl, *shader_, "resonance"); | |||
low_amount_uniform_ = getUniform(open_gl, *shader_, "low_amount"); | |||
band_amount_uniform_ = getUniform(open_gl, *shader_, "band_amount"); | |||
high_amount_uniform_ = getUniform(open_gl, *shader_, "high_amount"); | |||
} | |||
void EqualizerResponse::drawResponse(OpenGlWrapper& open_gl, int index) { | |||
glEnable(GL_BLEND); | |||
setLineWidth(findValue(Skin::kWidgetLineWidth)); | |||
setFillCenter(1.0f - 2.0f * max_db_ / (max_db_ - min_db_)); | |||
Colour color_line = findColour(Skin::kWidgetPrimary1, true); | |||
Colour color_fill_to = findColour(Skin::kWidgetSecondary1, true); | |||
if (!active_) { | |||
color_line = findColour(Skin::kWidgetPrimaryDisabled, true); | |||
color_fill_to = findColour(Skin::kWidgetSecondaryDisabled, true); | |||
} | |||
else if (index) { | |||
color_line = findColour(Skin::kWidgetPrimary2, true); | |||
color_fill_to = findColour(Skin::kWidgetSecondary2, true); | |||
} | |||
setColor(color_line); | |||
float fill_fade = findValue(Skin::kWidgetFillFade); | |||
setFillColors(color_fill_to.withMultipliedAlpha(1.0f - fill_fade), color_fill_to); | |||
shader_->use(); | |||
open_gl.context.extensions.glBindVertexArray(vertex_array_object_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, line_buffer_); | |||
open_gl.context.extensions.glVertexAttribPointer(position_attribute_->attributeID, 1, GL_FLOAT, GL_FALSE, | |||
sizeof(float), nullptr); | |||
open_gl.context.extensions.glEnableVertexAttribArray(position_attribute_->attributeID); | |||
open_gl.context.extensions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, response_buffer_); | |||
midi_cutoff_uniform_->set(low_filter_.getMidiCutoff()[index], | |||
band_filter_.getMidiCutoff()[index], | |||
high_filter_.getMidiCutoff()[index]); | |||
resonance_uniform_->set(low_filter_.getResonance()[index], | |||
band_filter_.getResonance()[index], | |||
high_filter_.getResonance()[index]); | |||
low_amount_uniform_->set(low_filter_.getLowAmount()[index], | |||
band_filter_.getLowAmount()[index], | |||
high_filter_.getLowAmount()[index]); | |||
band_amount_uniform_->set(low_filter_.getBandAmount()[index], | |||
band_filter_.getBandAmount()[index], | |||
high_filter_.getBandAmount()[index]); | |||
high_amount_uniform_->set(low_filter_.getHighAmount()[index], | |||
band_filter_.getHighAmount()[index], | |||
high_filter_.getHighAmount()[index]); | |||
open_gl.context.extensions.glBeginTransformFeedback(GL_POINTS); | |||
glDrawArrays(GL_POINTS, 0, kResolution); | |||
open_gl.context.extensions.glEndTransformFeedback(); | |||
void* buffer = open_gl.context.extensions.glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, | |||
kResolution * sizeof(float), GL_MAP_READ_BIT); | |||
float* response_data = (float*)buffer; | |||
float width = getWidth(); | |||
float range = max_db_ - min_db_; | |||
float y_mult = getHeight() / range; | |||
for (int i = 0; i < kResolution; ++i) { | |||
setXAt(i, i * width / (kResolution - 1.0f)); | |||
setYAt(i, (max_db_ - response_data[i]) * y_mult); | |||
} | |||
open_gl.context.extensions.glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER); | |||
OpenGlLineRenderer::render(open_gl, animate_); | |||
} | |||
void EqualizerResponse::render(OpenGlWrapper& open_gl, bool animate) { | |||
animate_ = animate; | |||
computeFilterCoefficients(); | |||
if (active_ && animate_) | |||
drawResponse(open_gl, 1); | |||
drawResponse(open_gl, 0); | |||
open_gl.context.extensions.glDisableVertexAttribArray(position_attribute_->attributeID); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
open_gl.context.extensions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0); | |||
checkGlError(); | |||
drawControlPoints(open_gl); | |||
renderCorners(open_gl, animate); | |||
} | |||
void EqualizerResponse::destroy(OpenGlWrapper& open_gl) { | |||
OpenGlLineRenderer::destroy(open_gl); | |||
unselected_points_.destroy(open_gl); | |||
selected_point_.destroy(open_gl); | |||
dragging_point_.destroy(open_gl); | |||
open_gl.context.extensions.glDeleteBuffers(1, &line_buffer_); | |||
open_gl.context.extensions.glDeleteBuffers(1, &response_buffer_); | |||
line_buffer_ = 0; | |||
response_buffer_ = 0; | |||
shader_ = nullptr; | |||
position_attribute_ = nullptr; | |||
midi_cutoff_uniform_ = nullptr; | |||
resonance_uniform_ = nullptr; | |||
low_amount_uniform_ = nullptr; | |||
band_amount_uniform_ = nullptr; | |||
high_amount_uniform_ = nullptr; | |||
} | |||
void EqualizerResponse::setControlPointBounds(float selected_x, float selected_y, | |||
float unselected_x1, float unselected_y1, | |||
float unselected_x2, float unselected_y2) { | |||
static constexpr float kHandleRadius = 0.06f; | |||
static constexpr float kDraggingRadius = 0.18f; | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
float handle_radius = kHandleRadius * height; | |||
float handle_width = handle_radius * 4.0f / width; | |||
float handle_height = handle_radius * 4.0f / height; | |||
float dragging_radius = kDraggingRadius * height; | |||
float dragging_width = dragging_radius * 4.0f / width; | |||
float dragging_height = dragging_radius * 4.0f / height; | |||
selected_point_.setQuad(0, selected_x - handle_width * 0.5f, selected_y - handle_height * 0.5f, | |||
handle_width, handle_height); | |||
dragging_point_.setQuad(0, selected_x - dragging_width * 0.5f, selected_y - dragging_height * 0.5f, | |||
dragging_width, dragging_height); | |||
unselected_points_.setQuad(0, unselected_x1 - handle_width * 0.5f, unselected_y1 - handle_height * 0.5f, | |||
handle_width, handle_height); | |||
unselected_points_.setQuad(1, unselected_x2 - handle_width * 0.5f, unselected_y2 - handle_height * 0.5f, | |||
handle_width, handle_height); | |||
} | |||
void EqualizerResponse::drawControlPoints(OpenGlWrapper& open_gl) { | |||
Point<float> low_position = getLowPosition(); | |||
Point<float> band_position = getBandPosition(); | |||
Point<float> high_position = getHighPosition(); | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
float low_x = 2.0f * low_position.x / width - 1.0f; | |||
float band_x = 2.0f * band_position.x / width - 1.0f; | |||
float high_x = 2.0f * high_position.x / width - 1.0f; | |||
float low_y = 1.0f - 2.0f * low_position.y / height; | |||
float band_y = 1.0f - 2.0f * band_position.y / height; | |||
float high_y = 1.0f - 2.0f * high_position.y / height; | |||
if (band_cutoff_output_ == nullptr) | |||
band_x = -2.0f; | |||
if (selected_band_ == 0) | |||
setControlPointBounds(low_x, low_y, band_x, band_y, high_x, high_y); | |||
else if (band_cutoff_output_ && selected_band_ == 1) | |||
setControlPointBounds(band_x, band_y, low_x, low_y, high_x, high_y); | |||
else if (selected_band_ == 2) | |||
setControlPointBounds(high_x, high_y, low_x, low_y, band_x, band_y); | |||
dragging_point_.setColor(findColour(Skin::kLightenScreen, true)); | |||
if (current_cutoff_ && current_gain_) | |||
dragging_point_.render(open_gl, true); | |||
selected_point_.setColor(findColour(Skin::kWidgetPrimary1, true)); | |||
selected_point_.render(open_gl, true); | |||
unselected_points_.setColor(findColour(Skin::kWidgetPrimary1, true)); | |||
unselected_points_.render(open_gl, true); | |||
} | |||
void EqualizerResponse::paintBackground(Graphics& g) { | |||
static constexpr int kLineSpacing = 10; | |||
OpenGlLineRenderer::paintBackground(g); | |||
if (!draw_frequency_lines_) | |||
return; | |||
float min_frequency = vital::utils::midiNoteToFrequency(low_cutoff_->getMinimum()); | |||
float max_frequency = vital::utils::midiNoteToFrequency(low_cutoff_->getMaximum()); | |||
int height = getHeight(); | |||
float max_octave = log2f(max_frequency / min_frequency); | |||
g.setColour(findColour(Skin::kLightenScreen, true).withMultipliedAlpha(0.5f)); | |||
float frequency = 0.0f; | |||
float increment = 1.0f; | |||
int x = 0; | |||
while (frequency < max_frequency) { | |||
for (int i = 0; i < kLineSpacing; ++i) { | |||
frequency += increment; | |||
float t = log2f(frequency / min_frequency) / max_octave; | |||
x = std::round(t * getWidth()); | |||
g.fillRect(x, 0, 1, height); | |||
} | |||
g.fillRect(x, 0, 1, height); | |||
increment *= kLineSpacing; | |||
} | |||
} | |||
void EqualizerResponse::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) { | |||
int band = getHoveredBand(e); | |||
if (band == 0 && low_resonance_) | |||
low_resonance_->mouseWheelMove(e, wheel); | |||
else if (band == 1 && band_resonance_) | |||
band_resonance_->mouseWheelMove(e, wheel); | |||
else if (band == 2 && high_resonance_) | |||
high_resonance_->mouseWheelMove(e, wheel); | |||
else | |||
OpenGlComponent::mouseWheelMove(e, wheel); | |||
} | |||
void EqualizerResponse::mouseDown(const MouseEvent& e) { | |||
selected_band_ = getHoveredBand(e); | |||
if (selected_band_ == 0) { | |||
current_cutoff_ = low_cutoff_; | |||
current_gain_ = low_gain_; | |||
for (Listener* listener : listeners_) | |||
listener->lowBandSelected(); | |||
} | |||
else if (selected_band_ == 1) { | |||
current_cutoff_ = band_cutoff_; | |||
current_gain_ = band_gain_; | |||
for (Listener* listener : listeners_) | |||
listener->midBandSelected(); | |||
} | |||
else if (selected_band_ == 2) { | |||
current_cutoff_ = high_cutoff_; | |||
current_gain_ = high_gain_; | |||
for (Listener* listener : listeners_) | |||
listener->highBandSelected(); | |||
} | |||
OpenGlLineRenderer::mouseDown(e); | |||
} | |||
void EqualizerResponse::mouseDrag(const MouseEvent& e) { | |||
moveFilterSettings(e.position); | |||
OpenGlLineRenderer::mouseDrag(e); | |||
} | |||
void EqualizerResponse::mouseUp(const MouseEvent& e) { | |||
if (current_cutoff_ == nullptr && current_gain_ == nullptr) { | |||
OpenGlLineRenderer::mouseUp(e); | |||
return; | |||
} | |||
current_cutoff_ = nullptr; | |||
current_gain_ = nullptr; | |||
OpenGlLineRenderer::mouseUp(e); | |||
} | |||
void EqualizerResponse::mouseExit(const MouseEvent& e) { | |||
if (low_cutoff_) { | |||
low_cutoff_->hidePopup(true); | |||
low_cutoff_->hidePopup(false); | |||
} | |||
OpenGlLineRenderer::mouseExit(e); | |||
} | |||
int EqualizerResponse::getHoveredBand(const MouseEvent& e) { | |||
const float grab_distance = 0.06f * getWidth(); | |||
float delta_low = e.position.getDistanceSquaredFrom(getLowPosition()); | |||
float delta_band = e.position.getDistanceSquaredFrom(getBandPosition()); | |||
float delta_high = e.position.getDistanceSquaredFrom(getHighPosition()); | |||
float min_sqr_dist = grab_distance * grab_distance; | |||
float min = std::min(std::min(min_sqr_dist, delta_low), delta_high); | |||
if (band_cutoff_output_) | |||
min = std::min(min, delta_band); | |||
if (delta_low <= min) | |||
return 0; | |||
if (band_cutoff_output_ && delta_band <= min) | |||
return 1; | |||
if (delta_high <= min) | |||
return 2; | |||
return -1; | |||
} | |||
Point<float> EqualizerResponse::getLowPosition() { | |||
float cutoff_range = low_cutoff_->getMaximum() - low_cutoff_->getMinimum(); | |||
float min_cutoff = low_cutoff_->getMinimum(); | |||
float gain_range = max_db_ - min_db_; | |||
float low_x = getWidth() * (low_cutoff_->getValue() - min_cutoff) / cutoff_range; | |||
float low_y = getHeight() * (max_db_ - low_gain_->getValue()) / gain_range; | |||
return Point<float>(low_x, low_y); | |||
} | |||
Point<float> EqualizerResponse::getBandPosition() { | |||
if (band_cutoff_ == nullptr) | |||
return Point<float>(0.0f, 0.0f); | |||
float cutoff_range = band_cutoff_->getMaximum() - band_cutoff_->getMinimum(); | |||
float min_cutoff = band_cutoff_->getMinimum(); | |||
float gain_range = max_db_ - min_db_; | |||
float band_x = getWidth() * (band_cutoff_->getValue() - min_cutoff) / cutoff_range; | |||
float band_y = getHeight() * (max_db_ - band_gain_->getValue()) / gain_range; | |||
return Point<float>(band_x, band_y); | |||
} | |||
Point<float> EqualizerResponse::getHighPosition() { | |||
float cutoff_range = high_cutoff_->getMaximum() - high_cutoff_->getMinimum(); | |||
float min_cutoff = high_cutoff_->getMinimum(); | |||
float gain_range = max_db_ - min_db_; | |||
float high_x = getWidth() * (high_cutoff_->getValue() - min_cutoff) / cutoff_range; | |||
float high_y = getHeight() * (max_db_ - high_gain_->getValue()) / gain_range; | |||
return Point<float>(high_x, high_y); | |||
} | |||
void EqualizerResponse::computeFilterCoefficients() { | |||
low_filter_state_.midi_cutoff = getOutputTotal(low_cutoff_output_, low_cutoff_); | |||
low_filter_state_.resonance_percent = getOutputTotal(low_resonance_output_, low_resonance_); | |||
low_filter_state_.gain = getOutputTotal(low_gain_output_, low_gain_); | |||
if (high_pass_) { | |||
low_filter_state_.style = vital::DigitalSvf::k12Db; | |||
low_filter_state_.pass_blend = 2.0f; | |||
} | |||
else { | |||
low_filter_state_.style = vital::DigitalSvf::kShelving; | |||
low_filter_state_.pass_blend = 0.0f; | |||
} | |||
low_filter_.setupFilter(low_filter_state_); | |||
band_filter_state_.midi_cutoff = getOutputTotal(band_cutoff_output_, band_cutoff_); | |||
band_filter_state_.resonance_percent = getOutputTotal(band_resonance_output_, band_resonance_); | |||
band_filter_state_.gain = getOutputTotal(band_gain_output_, band_gain_); | |||
band_filter_state_.pass_blend = 1.0f; | |||
if (notch_) | |||
band_filter_state_.style = vital::DigitalSvf::kNotchPassSwap; | |||
else | |||
band_filter_state_.style = vital::DigitalSvf::kShelving; | |||
band_filter_.setupFilter(band_filter_state_); | |||
high_filter_state_.midi_cutoff = getOutputTotal(high_cutoff_output_, high_cutoff_); | |||
high_filter_state_.resonance_percent = getOutputTotal(high_resonance_output_, high_resonance_); | |||
high_filter_state_.gain = getOutputTotal(high_gain_output_, high_gain_); | |||
if (low_pass_) { | |||
high_filter_state_.style = vital::DigitalSvf::k12Db; | |||
high_filter_state_.pass_blend = 0.0f; | |||
} | |||
else { | |||
high_filter_state_.style = vital::DigitalSvf::kShelving; | |||
high_filter_state_.pass_blend = 2.0f; | |||
} | |||
high_filter_.setupFilter(high_filter_state_); | |||
} | |||
void EqualizerResponse::moveFilterSettings(Point<float> position) { | |||
if (current_cutoff_) { | |||
float ratio = vital::utils::clamp(position.x / getWidth(), 0.0f, 1.0f); | |||
float min = current_cutoff_->getMinimum(); | |||
float max = current_cutoff_->getMaximum(); | |||
float new_cutoff = ratio * (max - min) + min; | |||
current_cutoff_->showPopup(true); | |||
current_cutoff_->setValue(new_cutoff); | |||
} | |||
if (current_gain_) { | |||
float local_position = position.y - 0.5f * db_buffer_ratio_ * getHeight(); | |||
float ratio = vital::utils::clamp(local_position / ((1.0f - db_buffer_ratio_) * getHeight()), 0.0f, 1.0f); | |||
float min = current_gain_->getMinimum(); | |||
float max = current_gain_->getMaximum(); | |||
float new_db = ratio * (min - max) + max; | |||
current_gain_->setValue(new_db); | |||
current_gain_->showPopup(false); | |||
} | |||
else | |||
low_gain_->hidePopup(false); | |||
} | |||
void EqualizerResponse::setLowSliders(SynthSlider *cutoff, | |||
SynthSlider *resonance, SynthSlider *gain) { | |||
float buffer = gain->getRange().getLength() * db_buffer_ratio_; | |||
min_db_ = gain->getMinimum() - buffer; | |||
max_db_ = gain->getMaximum() + buffer; | |||
low_cutoff_ = cutoff; | |||
low_resonance_ = resonance; | |||
low_gain_ = gain; | |||
low_cutoff_->addSliderListener(this); | |||
if (low_resonance_) | |||
low_resonance_->addSliderListener(this); | |||
low_gain_->addSliderListener(this); | |||
repaint(); | |||
} | |||
void EqualizerResponse::setBandSliders(SynthSlider *cutoff, | |||
SynthSlider *resonance, SynthSlider *gain) { | |||
band_cutoff_ = cutoff; | |||
band_resonance_ = resonance; | |||
band_gain_ = gain; | |||
band_cutoff_->addSliderListener(this); | |||
if (band_resonance_) | |||
band_resonance_->addSliderListener(this); | |||
band_gain_->addSliderListener(this); | |||
repaint(); | |||
} | |||
void EqualizerResponse::setHighSliders(SynthSlider *cutoff, | |||
SynthSlider *resonance, SynthSlider *gain) { | |||
high_cutoff_ = cutoff; | |||
high_resonance_ = resonance; | |||
high_gain_ = gain; | |||
high_cutoff_->addSliderListener(this); | |||
if (high_resonance_) | |||
high_resonance_->addSliderListener(this); | |||
high_gain_->addSliderListener(this); | |||
repaint(); | |||
} | |||
void EqualizerResponse::setSelectedBand(int selected_band) { | |||
selected_band_ = selected_band; | |||
repaint(); | |||
} | |||
void EqualizerResponse::setActive(bool active) { | |||
active_ = active; | |||
repaint(); | |||
} | |||
void EqualizerResponse::setHighPass(bool high_pass) { | |||
high_pass_ = high_pass; | |||
repaint(); | |||
} | |||
void EqualizerResponse::setNotch(bool notch) { | |||
notch_ = notch; | |||
repaint(); | |||
} | |||
void EqualizerResponse::setLowPass(bool low_pass) { | |||
low_pass_ = low_pass; | |||
repaint(); | |||
} | |||
vital::poly_float EqualizerResponse::getOutputTotal(vital::Output* output, Slider* slider) { | |||
if (output == nullptr || slider == nullptr) | |||
return 0.0f; | |||
if (!active_ || !animate_ || !output->owner->enabled()) | |||
return slider->getValue(); | |||
return output->trigger_value; | |||
} |
@@ -0,0 +1,162 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "digital_svf.h" | |||
#include "open_gl_line_renderer.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "synth_slider.h" | |||
class EqualizerResponse : public OpenGlLineRenderer, SynthSlider::SliderListener { | |||
public: | |||
static constexpr int kResolution = 128; | |||
static constexpr int kViewSampleRate = 100000; | |||
static constexpr float kDefaultDbBufferRatio = 0.2f; | |||
static constexpr float kMouseMultiplier = 0.3f; | |||
class Listener { | |||
public: | |||
virtual ~Listener() = default; | |||
virtual void lowBandSelected() = 0; | |||
virtual void midBandSelected() = 0; | |||
virtual void highBandSelected() = 0; | |||
}; | |||
EqualizerResponse(); | |||
~EqualizerResponse(); | |||
void initEq(const vital::output_map& mono_modulations); | |||
void initReverb(const vital::output_map& mono_modulations); | |||
virtual void init(OpenGlWrapper& open_gl) override; | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) override; | |||
virtual void destroy(OpenGlWrapper& open_gl) override; | |||
void setControlPointBounds(float selected_x, float selected_y, | |||
float unselected_x1, float unselected_y1, | |||
float unselected_x2, float unselected_y2); | |||
void drawControlPoints(OpenGlWrapper& open_gl); | |||
void drawResponse(OpenGlWrapper& open_gl, int index); | |||
void computeFilterCoefficients(); | |||
void moveFilterSettings(Point<float> position); | |||
void setLowSliders(SynthSlider* cutoff, SynthSlider* resonance, SynthSlider* gain); | |||
void setBandSliders(SynthSlider* cutoff, SynthSlider* resonance, SynthSlider* gain); | |||
void setHighSliders(SynthSlider* cutoff, SynthSlider* resonance, SynthSlider* gain); | |||
void setSelectedBand(int selected_band); | |||
Point<float> getLowPosition(); | |||
Point<float> getBandPosition(); | |||
Point<float> getHighPosition(); | |||
void resized() override { | |||
OpenGlLineRenderer::resized(); | |||
unselected_points_.setBounds(getLocalBounds()); | |||
selected_point_.setBounds(getLocalBounds()); | |||
dragging_point_.setBounds(getLocalBounds()); | |||
} | |||
void paintBackground(Graphics& g) override; | |||
void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override; | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseUp(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
int getHoveredBand(const MouseEvent& e); | |||
void setActive(bool active); | |||
void setHighPass(bool high_pass); | |||
void setNotch(bool notch); | |||
void setLowPass(bool high_pass); | |||
void setDbBufferRatio(float ratio) { db_buffer_ratio_ = ratio; } | |||
void setDrawFrequencyLines(bool draw_lines) { draw_frequency_lines_ = draw_lines; } | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
private: | |||
vital::poly_float getOutputTotal(vital::Output* output, Slider* slider); | |||
int resolution_; | |||
bool active_; | |||
bool high_pass_; | |||
bool notch_; | |||
bool low_pass_; | |||
bool animate_; | |||
bool draw_frequency_lines_; | |||
int selected_band_; | |||
float db_buffer_ratio_; | |||
float min_db_; | |||
float max_db_; | |||
OpenGlMultiQuad unselected_points_; | |||
OpenGlQuad selected_point_; | |||
OpenGlQuad dragging_point_; | |||
vital::DigitalSvf low_filter_; | |||
vital::DigitalSvf band_filter_; | |||
vital::DigitalSvf high_filter_; | |||
vital::SynthFilter::FilterState low_filter_state_; | |||
vital::SynthFilter::FilterState band_filter_state_; | |||
vital::SynthFilter::FilterState high_filter_state_; | |||
SynthSlider* low_cutoff_; | |||
SynthSlider* low_resonance_; | |||
SynthSlider* low_gain_; | |||
SynthSlider* band_cutoff_; | |||
SynthSlider* band_resonance_; | |||
SynthSlider* band_gain_; | |||
SynthSlider* high_cutoff_; | |||
SynthSlider* high_resonance_; | |||
SynthSlider* high_gain_; | |||
vital::Output* low_cutoff_output_; | |||
vital::Output* low_resonance_output_; | |||
vital::Output* low_gain_output_; | |||
vital::Output* band_cutoff_output_; | |||
vital::Output* band_resonance_output_; | |||
vital::Output* band_gain_output_; | |||
vital::Output* high_cutoff_output_; | |||
vital::Output* high_resonance_output_; | |||
vital::Output* high_gain_output_; | |||
SynthSlider* current_cutoff_; | |||
SynthSlider* current_gain_; | |||
std::unique_ptr<float[]> line_data_; | |||
OpenGLShaderProgram* shader_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> position_attribute_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> midi_cutoff_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> resonance_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> low_amount_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> band_amount_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> high_amount_uniform_; | |||
GLuint vertex_array_object_; | |||
GLuint line_buffer_; | |||
GLuint response_buffer_; | |||
std::vector<Listener*> listeners_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EqualizerResponse) | |||
}; | |||
@@ -0,0 +1,635 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "filter_response.h" | |||
#include "comb_filter.h" | |||
#include "skin.h" | |||
#include "synth_constants.h" | |||
#include "shaders.h" | |||
#include "synth_slider.h" | |||
#include "utils.h" | |||
namespace { | |||
FilterResponse::FilterShader getShaderForModel(vital::constants::FilterModel model, int style) { | |||
switch (model) { | |||
case vital::constants::kAnalog: | |||
return FilterResponse::kAnalog; | |||
case vital::constants::kComb: { | |||
vital::CombFilter::FeedbackStyle feedback_style = vital::CombFilter::getFeedbackStyle(style); | |||
if (feedback_style == vital::CombFilter::kComb) | |||
return FilterResponse::kComb; | |||
if (feedback_style == vital::CombFilter::kPositiveFlange) | |||
return FilterResponse::kPositiveFlange; | |||
return FilterResponse::kNegativeFlange; | |||
} | |||
case vital::constants::kDiode: | |||
return FilterResponse::kDiode; | |||
case vital::constants::kDirty: | |||
return FilterResponse::kDirty; | |||
case vital::constants::kLadder: | |||
return FilterResponse::kLadder; | |||
case vital::constants::kPhase: | |||
return FilterResponse::kPhase; | |||
case vital::constants::kFormant: | |||
return FilterResponse::kFormant; | |||
case vital::constants::kDigital: | |||
return FilterResponse::kDigital; | |||
default: | |||
VITAL_ASSERT(false); | |||
return FilterResponse::kNumFilterShaders; | |||
} | |||
} | |||
Shaders::VertexShader getVertexShader(FilterResponse::FilterShader shader) { | |||
switch (shader) { | |||
case FilterResponse::kAnalog: | |||
return Shaders::kAnalogFilterResponseVertex; | |||
case FilterResponse::kComb: | |||
return Shaders::kCombFilterResponseVertex; | |||
case FilterResponse::kPositiveFlange: | |||
return Shaders::kPositiveFlangeFilterResponseVertex; | |||
case FilterResponse::kNegativeFlange: | |||
return Shaders::kNegativeFlangeFilterResponseVertex; | |||
case FilterResponse::kDiode: | |||
return Shaders::kDiodeFilterResponseVertex; | |||
case FilterResponse::kDirty: | |||
return Shaders::kDirtyFilterResponseVertex; | |||
case FilterResponse::kLadder: | |||
return Shaders::kLadderFilterResponseVertex; | |||
case FilterResponse::kPhase: | |||
return Shaders::kPhaserFilterResponseVertex; | |||
case FilterResponse::kFormant: | |||
return Shaders::kFormantFilterResponseVertex; | |||
case FilterResponse::kDigital: | |||
return Shaders::kDigitalFilterResponseVertex; | |||
default: | |||
VITAL_ASSERT(false); | |||
return Shaders::kNumVertexShaders; | |||
} | |||
} | |||
std::pair<vital::Output*, vital::Output*> getOutputs(const vital::output_map& mono_modulations, | |||
const std::string& name) { | |||
return { | |||
mono_modulations.at(name), | |||
nullptr | |||
}; | |||
} | |||
std::pair<vital::Output*, vital::Output*> getOutputs(const vital::output_map& mono_modulations, | |||
const vital::output_map& poly_modulations, | |||
const std::string& name) { | |||
return { | |||
mono_modulations.at(name), | |||
poly_modulations.at(name) | |||
}; | |||
} | |||
} // namespace | |||
FilterResponse::FilterResponse() : OpenGlLineRenderer(kResolution), phaser_filter_(false) { | |||
setFill(true); | |||
setFillCenter(-1.0f); | |||
active_ = false; | |||
animate_ = false; | |||
mix_ = 1.0f; | |||
current_resonance_value_ = 0.0; | |||
current_cutoff_value_ = 0.0; | |||
current_formant_x_value_ = 0.0; | |||
current_formant_y_value_ = 0.0; | |||
cutoff_slider_ = nullptr; | |||
resonance_slider_ = nullptr; | |||
formant_x_slider_ = nullptr; | |||
formant_y_slider_ = nullptr; | |||
filter_mix_slider_ = nullptr; | |||
blend_slider_ = nullptr; | |||
transpose_slider_ = nullptr; | |||
formant_transpose_slider_ = nullptr; | |||
formant_resonance_slider_ = nullptr; | |||
formant_spread_slider_ = nullptr; | |||
last_filter_style_ = 0; | |||
last_filter_model_ = static_cast<vital::constants::FilterModel>(0); | |||
filter_model_ = static_cast<vital::constants::FilterModel>(0); | |||
line_data_ = std::make_unique<float[]>(2 * kResolution); | |||
line_buffer_ = 0; | |||
response_buffer_ = 0; | |||
vertex_array_object_ = 0; | |||
for (int i = 0; i < kResolution; ++i) { | |||
float t = i / (kResolution - 1.0f); | |||
line_data_[2 * i] = 2.0f * t - 1.0f; | |||
line_data_[2 * i + 1] = (i / kCombAlternatePeriod) % 2; | |||
} | |||
analog_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
comb_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
digital_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
diode_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
dirty_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
formant_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
ladder_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
phaser_filter_.setSampleRate(kDefaultVisualSampleRate); | |||
} | |||
FilterResponse::FilterResponse(String suffix, const vital::output_map& mono_modulations) : FilterResponse() { | |||
std::string prefix = std::string("filter_") + suffix.toStdString() +"_"; | |||
filter_mix_outputs_ = getOutputs(mono_modulations, prefix + "mix"); | |||
midi_cutoff_outputs_ = getOutputs(mono_modulations, prefix + "cutoff"); | |||
resonance_outputs_ = getOutputs(mono_modulations, prefix + "resonance"); | |||
blend_outputs_ = getOutputs(mono_modulations, prefix + "blend"); | |||
transpose_outputs_ = getOutputs(mono_modulations, prefix + "blend_transpose"); | |||
interpolate_x_outputs_ = getOutputs(mono_modulations, prefix + "formant_x"); | |||
interpolate_y_outputs_ = getOutputs(mono_modulations, prefix + "formant_y"); | |||
formant_transpose_outputs_ = getOutputs(mono_modulations, prefix + "formant_transpose"); | |||
formant_resonance_outputs_ = getOutputs(mono_modulations, prefix + "formant_resonance"); | |||
formant_spread_outputs_ = getOutputs(mono_modulations, prefix + "formant_spread"); | |||
} | |||
FilterResponse::FilterResponse(int index, const vital::output_map& mono_modulations, | |||
const vital::output_map& poly_modulations) : FilterResponse() { | |||
std::string prefix = std::string("filter_") + std::to_string(index) + "_"; | |||
filter_mix_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "mix"); | |||
midi_cutoff_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "cutoff"); | |||
resonance_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "resonance"); | |||
blend_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "blend"); | |||
transpose_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "blend_transpose"); | |||
interpolate_x_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "formant_x"); | |||
interpolate_y_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "formant_y"); | |||
formant_transpose_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "formant_transpose"); | |||
formant_resonance_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "formant_resonance"); | |||
formant_spread_outputs_ = getOutputs(mono_modulations, poly_modulations, prefix + "formant_spread"); | |||
} | |||
FilterResponse::~FilterResponse() { } | |||
void FilterResponse::init(OpenGlWrapper& open_gl) { | |||
OpenGlLineRenderer::init(open_gl); | |||
const GLchar* varyings[] = { "response_out" }; | |||
open_gl.context.extensions.glGenVertexArrays(1, &vertex_array_object_); | |||
open_gl.context.extensions.glBindVertexArray(vertex_array_object_); | |||
GLsizeiptr data_size = static_cast<GLsizeiptr>(kResolution * sizeof(float)); | |||
open_gl.context.extensions.glGenBuffers(1, &line_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, line_buffer_); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, 2 * data_size, line_data_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &response_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, response_buffer_); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, data_size, nullptr, GL_STATIC_READ); | |||
for (int i = 0; i < kNumFilterShaders; ++i) { | |||
Shaders::VertexShader vertex_shader = getVertexShader(static_cast<FilterShader>(i)); | |||
OpenGLShaderProgram* shader = open_gl.shaders->getShaderProgram(vertex_shader, Shaders::kColorFragment, varyings); | |||
shaders_[i].shader = shader; | |||
shader->use(); | |||
shaders_[i].position = getAttribute(open_gl, *shader, "position"); | |||
shaders_[i].mix = getUniform(open_gl, *shader, "mix"); | |||
shaders_[i].midi_cutoff = getUniform(open_gl, *shader, "midi_cutoff"); | |||
shaders_[i].resonance = getUniform(open_gl, *shader, "resonance"); | |||
shaders_[i].drive = getUniform(open_gl, *shader, "drive"); | |||
shaders_[i].db24 = getUniform(open_gl, *shader, "db24"); | |||
shaders_[i].formant_cutoff = getUniform(open_gl, *shader, "formant_cutoff"); | |||
shaders_[i].formant_resonance = getUniform(open_gl, *shader, "formant_resonance"); | |||
shaders_[i].formant_spread = getUniform(open_gl, *shader, "formant_spread"); | |||
shaders_[i].formant_low = getUniform(open_gl, *shader, "low"); | |||
shaders_[i].formant_band = getUniform(open_gl, *shader, "band"); | |||
shaders_[i].formant_high = getUniform(open_gl, *shader, "high"); | |||
for (int s = 0; s < FilterResponseShader::kMaxStages; ++s) { | |||
String stage = String("stage") + String(s); | |||
shaders_[i].stages[s] = getUniform(open_gl, *shader, stage.toRawUTF8()); | |||
} | |||
} | |||
} | |||
void FilterResponse::render(OpenGlWrapper& open_gl, bool animate) { | |||
animate_ = animate; | |||
drawFilterResponse(open_gl); | |||
renderCorners(open_gl, animate); | |||
} | |||
void FilterResponse::destroy(OpenGlWrapper& open_gl) { | |||
OpenGlLineRenderer::destroy(open_gl); | |||
open_gl.context.extensions.glDeleteBuffers(1, &line_buffer_); | |||
open_gl.context.extensions.glDeleteBuffers(1, &response_buffer_); | |||
vertex_array_object_ = 0; | |||
line_buffer_ = 0; | |||
response_buffer_ = 0; | |||
for (int i = 0; i < kNumFilterShaders; ++i) { | |||
shaders_[i].shader = nullptr; | |||
shaders_[i].position = nullptr; | |||
shaders_[i].mix = nullptr; | |||
shaders_[i].midi_cutoff = nullptr; | |||
shaders_[i].resonance = nullptr; | |||
shaders_[i].drive = nullptr; | |||
shaders_[i].db24 = nullptr; | |||
shaders_[i].formant_cutoff = nullptr; | |||
shaders_[i].formant_resonance = nullptr; | |||
shaders_[i].formant_spread = nullptr; | |||
shaders_[i].formant_low = nullptr; | |||
shaders_[i].formant_band = nullptr; | |||
shaders_[i].formant_high = nullptr; | |||
for (int s = 0; s < FilterResponseShader::kMaxStages; ++s) | |||
shaders_[i].stages[s] = nullptr; | |||
} | |||
} | |||
void FilterResponse::paintBackground(Graphics& g) { | |||
g.fillAll(findColour(Skin::kWidgetBackground, true)); | |||
line_left_color_ = findColour(Skin::kWidgetPrimary1, true); | |||
line_right_color_ = findColour(Skin::kWidgetPrimary2, true); | |||
line_disabled_color_ = findColour(Skin::kWidgetPrimaryDisabled, true); | |||
fill_left_color_ = findColour(Skin::kWidgetSecondary1, true); | |||
fill_right_color_ = findColour(Skin::kWidgetSecondary2, true); | |||
fill_disabled_color_ = findColour(Skin::kWidgetSecondaryDisabled, true); | |||
} | |||
void FilterResponse::setFilterSettingsFromPosition(Point<int> position) { | |||
Point<int> delta = position - last_mouse_position_; | |||
last_mouse_position_ = position; | |||
double width = getWidth(); | |||
double height = getHeight(); | |||
current_cutoff_value_ += delta.x * cutoff_slider_->getRange().getLength() / width; | |||
current_formant_x_value_ += delta.x * formant_x_slider_->getRange().getLength() / width; | |||
current_resonance_value_ -= delta.y * resonance_slider_->getRange().getLength() / height; | |||
current_formant_y_value_ -= delta.y * formant_y_slider_->getRange().getLength() / height; | |||
current_cutoff_value_ = cutoff_slider_->getRange().clipValue(current_cutoff_value_); | |||
current_formant_x_value_ = formant_x_slider_->getRange().clipValue(current_formant_x_value_); | |||
current_resonance_value_ = resonance_slider_->getRange().clipValue(current_resonance_value_); | |||
current_formant_y_value_ = formant_y_slider_->getRange().clipValue(current_formant_y_value_); | |||
if (filter_model_ == vital::constants::kFormant) { | |||
formant_x_slider_->setValue(current_formant_x_value_); | |||
formant_x_slider_->showPopup(true); | |||
formant_y_slider_->setValue(current_formant_y_value_); | |||
formant_y_slider_->showPopup(false); | |||
} | |||
else { | |||
cutoff_slider_->setValue(current_cutoff_value_); | |||
cutoff_slider_->showPopup(true); | |||
resonance_slider_->setValue(current_resonance_value_); | |||
resonance_slider_->showPopup(false); | |||
} | |||
} | |||
void FilterResponse::mouseDown(const MouseEvent& e) { | |||
last_mouse_position_ = e.getPosition(); | |||
current_resonance_value_ = resonance_slider_->getValue(); | |||
current_cutoff_value_ = cutoff_slider_->getValue(); | |||
current_formant_x_value_ = formant_x_slider_->getValue(); | |||
current_formant_y_value_ = formant_y_slider_->getValue(); | |||
if (filter_model_ == vital::constants::kFormant) { | |||
formant_x_slider_->showPopup(true); | |||
formant_y_slider_->showPopup(false); | |||
} | |||
else { | |||
cutoff_slider_->showPopup(true); | |||
resonance_slider_->showPopup(false); | |||
} | |||
} | |||
void FilterResponse::mouseDrag(const MouseEvent& e) { | |||
setFilterSettingsFromPosition(e.getPosition()); | |||
} | |||
void FilterResponse::mouseExit(const MouseEvent& e) { | |||
cutoff_slider_->hidePopup(true); | |||
resonance_slider_->hidePopup(false); | |||
OpenGlLineRenderer::mouseExit(e); | |||
} | |||
void FilterResponse::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) { | |||
MouseWheelDetails horizontal_details = wheel; | |||
horizontal_details.deltaY = 0.0f; | |||
MouseWheelDetails vertical_details = wheel; | |||
vertical_details.deltaX = 0.0f; | |||
if (filter_model_ == vital::constants::kFormant) { | |||
formant_x_slider_->mouseWheelMove(e, horizontal_details); | |||
formant_y_slider_->mouseWheelMove(e, vertical_details); | |||
} | |||
else { | |||
cutoff_slider_->mouseWheelMove(e, horizontal_details); | |||
resonance_slider_->mouseWheelMove(e, vertical_details); | |||
} | |||
} | |||
inline vital::poly_float FilterResponse::getOutputsTotal( | |||
std::pair<vital::Output*, vital::Output*> outputs, vital::poly_float default_value) { | |||
if (!active_ || !animate_ || !outputs.first->owner->enabled()) | |||
return default_value; | |||
if (outputs.second == nullptr || num_voices_readout_ == nullptr || num_voices_readout_->value()[0] <= 0.0f) | |||
return outputs.first->trigger_value; | |||
return outputs.first->trigger_value + outputs.second->trigger_value; | |||
} | |||
bool FilterResponse::setupFilterState(vital::constants::FilterModel model) { | |||
vital::poly_float midi_cutoff = getOutputsTotal(midi_cutoff_outputs_, cutoff_slider_->getValue()); | |||
midi_cutoff = vital::utils::max(midi_cutoff, 0.0f); | |||
vital::poly_float mix = getOutputsTotal(filter_mix_outputs_, filter_mix_slider_->getValue()); | |||
mix = vital::utils::clamp(mix, 0.0f, 1.0f); | |||
vital::poly_float resonance_percent = getOutputsTotal(resonance_outputs_, resonance_slider_->getValue()); | |||
vital::poly_float pass_blend = getOutputsTotal(blend_outputs_, blend_slider_->getValue()); | |||
pass_blend = vital::utils::clamp(pass_blend, 0.0f, 2.0f); | |||
vital::poly_float transpose = getOutputsTotal(transpose_outputs_, transpose_slider_->getValue()); | |||
vital::poly_float interpolate_x = getOutputsTotal(interpolate_x_outputs_, formant_x_slider_->getValue()); | |||
vital::poly_float interpolate_y = getOutputsTotal(interpolate_y_outputs_, formant_y_slider_->getValue()); | |||
if (model == vital::constants::kFormant) { | |||
transpose = getOutputsTotal(formant_transpose_outputs_, formant_transpose_slider_->getValue()); | |||
resonance_percent = getOutputsTotal(formant_resonance_outputs_, formant_resonance_slider_->getValue()); | |||
pass_blend = getOutputsTotal(formant_spread_outputs_, formant_spread_slider_->getValue()); | |||
} | |||
vital::poly_mask equal = vital::constants::kFullMask; | |||
equal = equal & vital::poly_float::equal(filter_state_.midi_cutoff, midi_cutoff); | |||
equal = equal & vital::poly_float::equal(mix_, mix); | |||
equal = equal & vital::poly_float::equal(filter_state_.resonance_percent, resonance_percent); | |||
equal = equal & vital::poly_float::equal(filter_state_.pass_blend, pass_blend); | |||
equal = equal & vital::poly_float::equal(filter_state_.transpose, transpose); | |||
equal = equal & vital::poly_float::equal(filter_state_.interpolate_x, interpolate_x); | |||
equal = equal & vital::poly_float::equal(filter_state_.interpolate_y, interpolate_y); | |||
filter_state_.midi_cutoff = midi_cutoff; | |||
mix_ = mix; | |||
filter_state_.resonance_percent = resonance_percent; | |||
filter_state_.pass_blend = pass_blend; | |||
filter_state_.transpose = transpose; | |||
filter_state_.interpolate_x = interpolate_x; | |||
filter_state_.interpolate_y = interpolate_y; | |||
bool new_type = last_filter_model_ != model || last_filter_style_ != filter_state_.style; | |||
last_filter_style_ = filter_state_.style; | |||
last_filter_model_ = model; | |||
return (~equal).anyMask() || new_type; | |||
} | |||
bool FilterResponse::isStereoState() { | |||
vital::poly_mask equal = vital::constants::kFullMask; | |||
equal = equal & vital::poly_float::equal(filter_state_.midi_cutoff, | |||
vital::utils::swapStereo(filter_state_.midi_cutoff)); | |||
equal = equal & vital::poly_float::equal(mix_, vital::utils::swapStereo(mix_)); | |||
equal = equal & vital::poly_float::equal(filter_state_.resonance_percent, | |||
vital::utils::swapStereo(filter_state_.resonance_percent)); | |||
equal = equal & vital::poly_float::equal(filter_state_.pass_blend, | |||
vital::utils::swapStereo(filter_state_.pass_blend)); | |||
equal = equal & vital::poly_float::equal(filter_state_.transpose, | |||
vital::utils::swapStereo(filter_state_.transpose)); | |||
equal = equal & vital::poly_float::equal(filter_state_.interpolate_x, | |||
vital::utils::swapStereo(filter_state_.interpolate_x)); | |||
equal = equal & vital::poly_float::equal(filter_state_.interpolate_y, | |||
vital::utils::swapStereo(filter_state_.interpolate_y)); | |||
return (~equal).anyMask(); | |||
} | |||
void FilterResponse::loadShader(FilterShader shader, vital::constants::FilterModel model, int index) { | |||
if (model == vital::constants::kAnalog) { | |||
analog_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
float resonance = vital::utils::clamp(analog_filter_.getResonance()[index], 0.0f, 2.0f); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(resonance); | |||
shaders_[shader].drive->set(analog_filter_.getDrive()[index]); | |||
shaders_[shader].db24->set(filter_state_.style != vital::SynthFilter::k12Db ? 1.0f : 0.0f); | |||
shaders_[shader].stages[0]->set(analog_filter_.getLowAmount()[index]); | |||
shaders_[shader].stages[1]->set(analog_filter_.getBandAmount()[index]); | |||
shaders_[shader].stages[2]->set(analog_filter_.getHighAmount()[index]); | |||
shaders_[shader].stages[3]->set(analog_filter_.getLowAmount24(filter_state_.style)[index]); | |||
shaders_[shader].stages[4]->set(analog_filter_.getHighAmount24(filter_state_.style)[index]); | |||
} | |||
else if (model == vital::constants::kComb) { | |||
comb_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
float resonance = vital::utils::clamp(comb_filter_.getResonance()[index], -0.99f, 0.99f); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(resonance); | |||
shaders_[shader].drive->set(comb_filter_.getDrive()[index]); | |||
shaders_[shader].stages[0]->set(comb_filter_.getLowAmount()[index]); | |||
shaders_[shader].stages[1]->set(comb_filter_.getHighAmount()[index]); | |||
shaders_[shader].stages[2]->set(comb_filter_.getFilterMidiCutoff()[index]); | |||
shaders_[shader].stages[3]->set(comb_filter_.getFilter2MidiCutoff()[index]); | |||
} | |||
else if (model == vital::constants::kDigital) { | |||
digital_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
float resonance = vital::utils::clamp(digital_filter_.getResonance()[index], 0.0f, 2.0f); | |||
shaders_[shader].midi_cutoff->set(digital_filter_.getMidiCutoff()[index]); | |||
shaders_[shader].resonance->set(resonance); | |||
shaders_[shader].drive->set(digital_filter_.getDrive()[index]); | |||
shaders_[shader].db24->set(filter_state_.style != vital::SynthFilter::k12Db ? 1.0f : 0.0f); | |||
shaders_[shader].stages[0]->set(digital_filter_.getLowAmount()[index]); | |||
shaders_[shader].stages[1]->set(digital_filter_.getBandAmount()[index]); | |||
shaders_[shader].stages[2]->set(digital_filter_.getHighAmount()[index]); | |||
shaders_[shader].stages[3]->set(digital_filter_.getLowAmount24(filter_state_.style)[index]); | |||
shaders_[shader].stages[4]->set(digital_filter_.getHighAmount24(filter_state_.style)[index]); | |||
} | |||
else if (model == vital::constants::kDiode) { | |||
diode_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(diode_filter_.getResonance()[index]); | |||
shaders_[shader].drive->set(diode_filter_.getDrive()[index]); | |||
shaders_[shader].db24->set(diode_filter_.getHighPassAmount()[index]); | |||
shaders_[shader].stages[0]->set(diode_filter_.getHighPassRatio()[index]); | |||
} | |||
else if (model == vital::constants::kDirty) { | |||
dirty_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
float resonance = vital::utils::clamp(dirty_filter_.getResonance()[index], 0.0f, 2.0f); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(resonance); | |||
shaders_[shader].drive->set(dirty_filter_.getDrive()[index]); | |||
shaders_[shader].db24->set(filter_state_.style != vital::SynthFilter::k12Db ? 1.0f : 0.0f); | |||
shaders_[shader].stages[0]->set(dirty_filter_.getLowAmount()[index]); | |||
shaders_[shader].stages[1]->set(dirty_filter_.getBandAmount()[index]); | |||
shaders_[shader].stages[2]->set(dirty_filter_.getHighAmount()[index]); | |||
shaders_[shader].stages[3]->set(dirty_filter_.getLowAmount24(filter_state_.style)[index]); | |||
shaders_[shader].stages[4]->set(dirty_filter_.getHighAmount24(filter_state_.style)[index]); | |||
} | |||
else if (model == vital::constants::kFormant) { | |||
shaders_[shader].shader->use(); | |||
vital::DigitalSvf* formant0 = formant_filter_.getFormant(0); | |||
vital::DigitalSvf* formant1 = formant_filter_.getFormant(1); | |||
vital::DigitalSvf* formant2 = formant_filter_.getFormant(2); | |||
vital::DigitalSvf* formant3 = formant_filter_.getFormant(3); | |||
formant_filter_.setupFilter(filter_state_); | |||
shaders_[shader].formant_cutoff->set(formant0->getMidiCutoff()[index], formant1->getMidiCutoff()[index], | |||
formant2->getMidiCutoff()[index], formant3->getMidiCutoff()[index]); | |||
shaders_[shader].formant_resonance->set(formant0->getResonance()[index], formant1->getResonance()[index], | |||
formant2->getResonance()[index], formant3->getResonance()[index]); | |||
vital::poly_float drive0 = formant0->getDrive(); | |||
vital::poly_float drive1 = formant1->getDrive(); | |||
vital::poly_float drive2 = formant2->getDrive(); | |||
vital::poly_float drive3 = formant3->getDrive(); | |||
vital::poly_float low0 = formant0->getLowAmount() * drive0; | |||
vital::poly_float low1 = formant1->getLowAmount() * drive1; | |||
vital::poly_float low2 = formant2->getLowAmount() * drive2; | |||
vital::poly_float low3 = formant3->getLowAmount() * drive3; | |||
vital::poly_float band0 = formant0->getBandAmount() * drive0; | |||
vital::poly_float band1 = formant1->getBandAmount() * drive1; | |||
vital::poly_float band2 = formant2->getBandAmount() * drive2; | |||
vital::poly_float band3 = formant3->getBandAmount() * drive3; | |||
vital::poly_float high0 = formant0->getHighAmount() * drive0; | |||
vital::poly_float high1 = formant1->getHighAmount() * drive1; | |||
vital::poly_float high2 = formant2->getHighAmount() * drive2; | |||
vital::poly_float high3 = formant3->getHighAmount() * drive3; | |||
shaders_[shader].formant_low->set(low0[index], low1[index], low2[index], low3[index]); | |||
shaders_[shader].formant_band->set(band0[index], band1[index], band2[index], band3[index]); | |||
shaders_[shader].formant_high->set(high0[index], high1[index], high2[index], high3[index]); | |||
} | |||
else if (model == vital::constants::kLadder) { | |||
ladder_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(ladder_filter_.getResonance()[index]); | |||
shaders_[shader].drive->set(ladder_filter_.getDrive()[index]); | |||
for (int s = 0; s < FilterResponseShader::kMaxStages; ++s) | |||
shaders_[shader].stages[s]->set(ladder_filter_.getStageScale(s)[index]); | |||
} | |||
else if (model == vital::constants::kPhase) { | |||
phaser_filter_.setupFilter(filter_state_); | |||
shaders_[shader].shader->use(); | |||
shaders_[shader].midi_cutoff->set(filter_state_.midi_cutoff[index]); | |||
shaders_[shader].resonance->set(phaser_filter_.getResonance()[index]); | |||
shaders_[shader].db24->set(filter_state_.style != vital::SynthFilter::k12Db ? 1.0f : 0.0f); | |||
shaders_[shader].stages[0]->set(phaser_filter_.getPeak1Amount()[index]); | |||
shaders_[shader].stages[1]->set(phaser_filter_.getPeak3Amount()[index]); | |||
shaders_[shader].stages[2]->set(phaser_filter_.getPeak5Amount()[index]); | |||
} | |||
shaders_[shader].mix->set(mix_[index]); | |||
} | |||
void FilterResponse::bind(FilterShader shader, OpenGLContext& open_gl_context) { | |||
open_gl_context.extensions.glBindVertexArray(vertex_array_object_); | |||
open_gl_context.extensions.glBindBuffer(GL_ARRAY_BUFFER, line_buffer_); | |||
OpenGLShaderProgram::Attribute* position = shaders_[shader].position.get(); | |||
open_gl_context.extensions.glVertexAttribPointer(position->attributeID, 2, GL_FLOAT, | |||
GL_FALSE, 2 * sizeof(float), nullptr); | |||
open_gl_context.extensions.glEnableVertexAttribArray(position->attributeID); | |||
open_gl_context.extensions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, response_buffer_); | |||
} | |||
void FilterResponse::unbind(FilterShader shader, OpenGLContext& open_gl_context) { | |||
OpenGLShaderProgram::Attribute* position = shaders_[shader].position.get(); | |||
open_gl_context.extensions.glDisableVertexAttribArray(position->attributeID); | |||
open_gl_context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
open_gl_context.extensions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0); | |||
} | |||
void FilterResponse::drawFilterResponse(OpenGlWrapper& open_gl) { | |||
vital::constants::FilterModel model = filter_model_; | |||
bool new_response = setupFilterState(model); | |||
new_response = new_response || isStereoState(); | |||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | |||
glEnable(GL_BLEND); | |||
setViewPort(open_gl); | |||
Colour color_line = line_right_color_; | |||
Colour color_fill_to = fill_right_color_; | |||
float fill_fade = findValue(Skin::kWidgetFillFade); | |||
Colour color_fill_from = color_fill_to.withMultipliedAlpha(1.0f - fill_fade); | |||
setLineWidth(findValue(Skin::kWidgetLineWidth)); | |||
setFillCenter(findValue(Skin::kWidgetFillCenter)); | |||
FilterShader shader = getShaderForModel(model, filter_state_.style); | |||
if (active_) { | |||
if (new_response) { | |||
bind(shader, open_gl.context); | |||
loadShader(shader, model, 1); | |||
renderLineResponse(open_gl); | |||
} | |||
setFillColors(color_fill_from, color_fill_to); | |||
setColor(color_line); | |||
OpenGlLineRenderer::render(open_gl, animate_); | |||
} | |||
color_line = line_left_color_; | |||
color_fill_to = fill_left_color_; | |||
if (!active_) { | |||
color_line = line_disabled_color_; | |||
color_fill_to = fill_disabled_color_; | |||
} | |||
color_fill_from = color_fill_to.withMultipliedAlpha(1.0f - fill_fade); | |||
if (new_response) { | |||
bind(shader, open_gl.context); | |||
loadShader(shader, model, 0); | |||
renderLineResponse(open_gl); | |||
} | |||
setFillColors(color_fill_from, color_fill_to); | |||
setColor(color_line); | |||
OpenGlLineRenderer::render(open_gl, animate_); | |||
unbind(shader, open_gl.context); | |||
glDisable(GL_BLEND); | |||
checkGlError(); | |||
} | |||
void FilterResponse::renderLineResponse(OpenGlWrapper& open_gl) { | |||
glEnable(GL_BLEND); | |||
open_gl.context.extensions.glBeginTransformFeedback(GL_POINTS); | |||
glDrawArrays(GL_POINTS, 0, kResolution); | |||
open_gl.context.extensions.glEndTransformFeedback(); | |||
void* buffer = open_gl.context.extensions.glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, | |||
kResolution * sizeof(float), GL_MAP_READ_BIT); | |||
float* response_data = (float*)buffer; | |||
float width = getWidth(); | |||
float y_adjust = getHeight() / 2.0f; | |||
for (int i = 0; i < kResolution; ++i) { | |||
setXAt(i, width * i / (kResolution - 1.0f)); | |||
setYAt(i, y_adjust * (1.0f - response_data[i])); | |||
} | |||
open_gl.context.extensions.glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER); | |||
glDisable(GL_BLEND); | |||
} |
@@ -0,0 +1,182 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "open_gl_line_renderer.h" | |||
#include "skin.h" | |||
#include "comb_filter.h" | |||
#include "digital_svf.h" | |||
#include "diode_filter.h" | |||
#include "dirty_filter.h" | |||
#include "formant_filter.h" | |||
#include "ladder_filter.h" | |||
#include "phaser_filter.h" | |||
#include "sallen_key_filter.h" | |||
#include "synth_types.h" | |||
class SynthSlider; | |||
class FilterResponse : public OpenGlLineRenderer { | |||
public: | |||
static constexpr int kResolution = 512; | |||
static constexpr int kDefaultVisualSampleRate = 200000; | |||
static constexpr int kCombAlternatePeriod = 3; | |||
static constexpr double kMouseSensitivityX = 0.3; | |||
static constexpr double kMouseSensitivityY = 0.3; | |||
enum FilterShader { | |||
kAnalog, | |||
kDirty, | |||
kLadder, | |||
kDigital, | |||
kDiode, | |||
kFormant, | |||
kComb, | |||
kPositiveFlange, | |||
kNegativeFlange, | |||
kPhase, | |||
kNumFilterShaders | |||
}; | |||
FilterResponse(String suffix, const vital::output_map& mono_modulations); | |||
FilterResponse(int index, const vital::output_map& mono_modulations, const vital::output_map& poly_modulations); | |||
virtual ~FilterResponse(); | |||
void init(OpenGlWrapper& open_gl) override; | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void destroy(OpenGlWrapper& open_gl) override; | |||
void paintBackground(Graphics& g) override; | |||
void setCutoffSlider(SynthSlider* slider) { cutoff_slider_ = slider; } | |||
void setResonanceSlider(SynthSlider* slider) { resonance_slider_ = slider; } | |||
void setFormantXSlider(SynthSlider* slider) { formant_x_slider_ = slider; } | |||
void setFormantYSlider(SynthSlider* slider) { formant_y_slider_ = slider; } | |||
void setFilterMixSlider(SynthSlider* slider) { filter_mix_slider_ = slider; } | |||
void setBlendSlider(SynthSlider* slider) { blend_slider_ = slider; } | |||
void setTransposeSlider(SynthSlider* slider) { transpose_slider_ = slider; } | |||
void setFormantTransposeSlider(SynthSlider* slider) { formant_transpose_slider_ = slider; } | |||
void setFormantResonanceSlider(SynthSlider* slider) { formant_resonance_slider_ = slider; } | |||
void setFormantSpreadSlider(SynthSlider* slider) { formant_spread_slider_ = slider; } | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override; | |||
void setActive(bool active) { active_ = active; } | |||
void setModel(vital::constants::FilterModel model) { filter_model_ = model; } | |||
void setStyle(int style) { filter_state_.style = style; } | |||
private: | |||
struct FilterResponseShader { | |||
static constexpr int kMaxStages = 5; | |||
OpenGLShaderProgram* shader; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> position; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> mix; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> midi_cutoff; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> resonance; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> drive; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> db24; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> stages[kMaxStages]; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_cutoff; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_resonance; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_spread; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_low; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_band; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> formant_high; | |||
}; | |||
FilterResponse(); | |||
void setFilterSettingsFromPosition(Point<int> position); | |||
void drawFilterResponse(OpenGlWrapper& open_gl); | |||
vital::poly_float getOutputsTotal(std::pair<vital::Output*, vital::Output*> outputs, | |||
vital::poly_float default_value); | |||
bool setupFilterState(vital::constants::FilterModel model); | |||
bool isStereoState(); | |||
void loadShader(FilterShader shader, vital::constants::FilterModel model, int index); | |||
void bind(FilterShader shader, OpenGLContext& open_gl_context); | |||
void unbind(FilterShader shader, OpenGLContext& open_gl_context); | |||
void renderLineResponse(OpenGlWrapper& open_gl); | |||
bool active_; | |||
bool animate_; | |||
Point<int> last_mouse_position_; | |||
double current_resonance_value_; | |||
double current_cutoff_value_; | |||
double current_formant_x_value_; | |||
double current_formant_y_value_; | |||
Colour line_left_color_; | |||
Colour line_right_color_; | |||
Colour line_disabled_color_; | |||
Colour fill_left_color_; | |||
Colour fill_right_color_; | |||
Colour fill_disabled_color_; | |||
vital::SallenKeyFilter analog_filter_; | |||
vital::CombFilter comb_filter_; | |||
vital::DigitalSvf digital_filter_; | |||
vital::DiodeFilter diode_filter_; | |||
vital::DirtyFilter dirty_filter_; | |||
vital::FormantFilter formant_filter_; | |||
vital::LadderFilter ladder_filter_; | |||
vital::PhaserFilter phaser_filter_; | |||
int last_filter_style_; | |||
vital::constants::FilterModel last_filter_model_; | |||
vital::constants::FilterModel filter_model_; | |||
vital::SynthFilter::FilterState filter_state_; | |||
vital::poly_float mix_; | |||
SynthSlider* cutoff_slider_; | |||
SynthSlider* resonance_slider_; | |||
SynthSlider* formant_x_slider_; | |||
SynthSlider* formant_y_slider_; | |||
SynthSlider* filter_mix_slider_; | |||
SynthSlider* blend_slider_; | |||
SynthSlider* transpose_slider_; | |||
SynthSlider* formant_transpose_slider_; | |||
SynthSlider* formant_resonance_slider_; | |||
SynthSlider* formant_spread_slider_; | |||
std::pair<vital::Output*, vital::Output*> filter_mix_outputs_; | |||
std::pair<vital::Output*, vital::Output*> midi_cutoff_outputs_; | |||
std::pair<vital::Output*, vital::Output*> resonance_outputs_; | |||
std::pair<vital::Output*, vital::Output*> blend_outputs_; | |||
std::pair<vital::Output*, vital::Output*> transpose_outputs_; | |||
std::pair<vital::Output*, vital::Output*> interpolate_x_outputs_; | |||
std::pair<vital::Output*, vital::Output*> interpolate_y_outputs_; | |||
std::pair<vital::Output*, vital::Output*> formant_resonance_outputs_; | |||
std::pair<vital::Output*, vital::Output*> formant_spread_outputs_; | |||
std::pair<vital::Output*, vital::Output*> formant_transpose_outputs_; | |||
FilterResponseShader shaders_[kNumFilterShaders]; | |||
std::unique_ptr<float[]> line_data_; | |||
GLuint vertex_array_object_; | |||
GLuint line_buffer_; | |||
GLuint response_buffer_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterResponse) | |||
}; | |||
@@ -0,0 +1,103 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "skin.h" | |||
class IncrementerButtons : public Component, public Button::Listener { | |||
public: | |||
IncrementerButtons(Slider* slider) : slider_(slider), active_(true) { | |||
increment_ = std::make_unique<ShapeButton>("Increment", Colours::black, Colours::black, Colours::black); | |||
addAndMakeVisible(increment_.get()); | |||
increment_->addListener(this); | |||
Path increment_shape; | |||
increment_shape.startNewSubPath(Point<float>(0.5f, 0.1f)); | |||
increment_shape.lineTo(Point<float>(0.2f, 0.45f)); | |||
increment_shape.lineTo(Point<float>(0.8f, 0.45f)); | |||
increment_shape.closeSubPath(); | |||
increment_shape.startNewSubPath(Point<float>(0.0f, 0.0f)); | |||
increment_shape.closeSubPath(); | |||
increment_shape.startNewSubPath(Point<float>(1.0f, 0.5f)); | |||
increment_shape.closeSubPath(); | |||
increment_shape.addLineSegment(Line<float>(0.0f, 0.0f, 0.0f, 0.0f), 0.2f); | |||
increment_shape.addLineSegment(Line<float>(0.5f, 0.5f, 0.5f, 0.5f), 0.2f); | |||
increment_->setShape(increment_shape, true, true, false); | |||
decrement_ = std::make_unique<ShapeButton>("Increment", Colours::black, Colours::black, Colours::black); | |||
addAndMakeVisible(decrement_.get()); | |||
decrement_->addListener(this); | |||
Path decrement_shape; | |||
decrement_shape.startNewSubPath(Point<float>(0.5f, 0.4f)); | |||
decrement_shape.lineTo(Point<float>(0.2f, 0.05f)); | |||
decrement_shape.lineTo(Point<float>(0.8f, 0.05f)); | |||
decrement_shape.closeSubPath(); | |||
decrement_shape.startNewSubPath(Point<float>(0.0f, 0.0f)); | |||
decrement_shape.closeSubPath(); | |||
decrement_shape.startNewSubPath(Point<float>(1.0f, 0.5f)); | |||
decrement_shape.closeSubPath(); | |||
decrement_shape.addLineSegment(Line<float>(0.0f, 0.0f, 0.0f, 0.0f), 0.2f); | |||
decrement_shape.addLineSegment(Line<float>(0.5f, 0.5f, 0.5f, 0.5f), 0.2f); | |||
decrement_->setShape(decrement_shape, true, true, false); | |||
} | |||
void setActive(bool active) { | |||
active_ = active; | |||
repaint(); | |||
} | |||
void resized() override { | |||
Rectangle<int> increment_bounds = getLocalBounds(); | |||
Rectangle<int> decrement_bounds = increment_bounds.removeFromBottom(getHeight() / 2); | |||
increment_->setBounds(increment_bounds); | |||
decrement_->setBounds(decrement_bounds); | |||
} | |||
void paint(Graphics& g) override { | |||
setColors(); | |||
} | |||
void buttonClicked(Button* clicked_button) override { | |||
double value = slider_->getValue(); | |||
if (clicked_button == increment_.get()) | |||
slider_->setValue(value + 1.0); | |||
else if (clicked_button == decrement_.get()) | |||
slider_->setValue(value - 1.0); | |||
} | |||
private: | |||
void setColors() { | |||
Colour normal = findColour(Skin::kIconButtonOff, true); | |||
Colour hover = findColour(Skin::kIconButtonOffHover, true); | |||
Colour down = findColour(Skin::kIconButtonOffPressed, true); | |||
increment_->setColours(normal, hover, down); | |||
decrement_->setColours(normal, hover, down); | |||
} | |||
Slider* slider_; | |||
bool active_; | |||
std::unique_ptr<ShapeButton> increment_; | |||
std::unique_ptr<ShapeButton> decrement_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IncrementerButtons) | |||
}; | |||
@@ -0,0 +1,248 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "lfo_editor.h" | |||
#include "default_look_and_feel.h" | |||
#include "skin.h" | |||
#include "shaders.h" | |||
#include "synth_gui_interface.h" | |||
#include "synth_section.h" | |||
#include "utils.h" | |||
LfoEditor::LfoEditor(LineGenerator* lfo_source, String prefix, | |||
const vital::output_map& mono_modulations, | |||
const vital::output_map& poly_modulations) : LineEditor(lfo_source) { | |||
parent_ = nullptr; | |||
wave_phase_ = nullptr; | |||
frequency_ = nullptr; | |||
last_phase_ = 0.0f; | |||
setFill(true); | |||
setFillCenter(-1.0f); | |||
setName(prefix); | |||
last_voice_ = -1.0f; | |||
} | |||
LfoEditor::~LfoEditor() { } | |||
void LfoEditor::parentHierarchyChanged() { | |||
parent_ = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (wave_phase_ == nullptr && parent_) | |||
wave_phase_ = parent_->getSynth()->getStatusOutput(getName().toStdString() + "_phase"); | |||
if (frequency_ == nullptr && parent_) | |||
frequency_ = parent_->getSynth()->getStatusOutput(getName().toStdString() + "_frequency"); | |||
LineEditor::parentHierarchyChanged(); | |||
} | |||
void LfoEditor::mouseDown(const MouseEvent& e) { | |||
if (e.mods.isPopupMenu()) { | |||
PopupItems options; | |||
int active_point = getActivePoint(); | |||
if (active_point >= 0) { | |||
options.addItem(kSetPhaseToPoint, "Set Start Point"); | |||
if (active_point >= 1 && active_point < getModel()->getNumPoints() - 1) { | |||
options.addItem(kRemovePoint, "Remove Point"); | |||
options.addItem(kEnterPhase, "Enter Point Phase"); | |||
} | |||
options.addItem(kEnterValue, "Enter Point Value"); | |||
options.addItem(-1, ""); | |||
} | |||
else if (getActivePower() >= 0) { | |||
options.addItem(kSetPhaseToPower, "Set Start Point"); | |||
options.addItem(kResetPower, "Reset Power"); | |||
options.addItem(-1, ""); | |||
} | |||
else if (getActiveGridSection() >= 0) | |||
options.addItem(kSetPhaseToGrid, "Set Start Point"); | |||
options.addItem(kCopy, "Copy"); | |||
if (hasMatchingSystemClipboard()) | |||
options.addItem(kPaste, "Paste"); | |||
options.addItem(kSave, "Save to LFOs"); | |||
options.addItem(kFlipHorizontal, "Flip Horizontal"); | |||
options.addItem(kFlipVertical, "Flip Vertical"); | |||
options.addItem(kImportLfo, "Import LFO"); | |||
options.addItem(kExportLfo, "Export LFO"); | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
int point = active_point; | |||
int power = getActivePower(); | |||
parent->showPopupSelector(this, e.getPosition(), options, | |||
[=](int selection) { respondToCallback(point, power, selection); }); | |||
} | |||
else | |||
LineEditor::mouseDown(e); | |||
} | |||
void LfoEditor::mouseDoubleClick(const MouseEvent& e) { | |||
if (!e.mods.isPopupMenu()) | |||
LineEditor::mouseDoubleClick(e); | |||
} | |||
void LfoEditor::mouseUp(const MouseEvent& e) { | |||
if (!e.mods.isPopupMenu()) | |||
LineEditor::mouseUp(e); | |||
} | |||
void LfoEditor::respondToCallback(int point, int power, int result) { | |||
if (result == kSetPhaseToPoint) { | |||
if (point >= 0 && point < numPoints()) | |||
setPhase(getModel()->getPoint(point).first); | |||
} | |||
else if (result == kSetPhaseToPower) { | |||
if (power >= 0 && power < numPoints() - 1) { | |||
float from = getModel()->getPoint(power).first; | |||
float to = getModel()->getPoint(power + 1).first; | |||
setPhase((from + to) / 2.0f); | |||
} | |||
} | |||
else if (result == kSetPhaseToGrid) { | |||
int section = getActiveGridSection(); | |||
int grid_size = getGridSizeX(); | |||
if (section >= 0 && grid_size > 0) | |||
setPhase(section * 1.0f / grid_size); | |||
} | |||
else if (result == kImportLfo) { | |||
for (Listener* listener : listeners_) | |||
listener->importLfo(); | |||
} | |||
else if (result == kExportLfo) { | |||
for (Listener* listener : listeners_) | |||
listener->exportLfo(); | |||
} | |||
else | |||
LineEditor::respondToCallback(point, power, result); | |||
clearActiveMouseActions(); | |||
} | |||
void LfoEditor::setPhase(float phase) { | |||
for (Listener* listener : listeners_) | |||
listener->setPhase(phase); | |||
} | |||
void LfoEditor::render(OpenGlWrapper& open_gl, bool animate) { | |||
static constexpr float kBackupTime = 1.0f / 50.0f; | |||
setGlPositions(); | |||
renderGrid(open_gl, animate); | |||
vital::poly_float encoded_phase = wave_phase_->value(); | |||
vital::poly_mask inactive_mask = 0; | |||
if (wave_phase_->isClearValue(encoded_phase)) { | |||
encoded_phase = 0.0f; | |||
inactive_mask = vital::constants::kFullMask; | |||
} | |||
vital::poly_float frequency = frequency_->value(); | |||
if (frequency_->isClearValue(frequency)) | |||
frequency = 0.0f; | |||
std::pair<vital::poly_float, vital::poly_float> decoded = vital::utils::decodePhaseAndVoice(encoded_phase); | |||
vital::poly_float phase = decoded.first; | |||
vital::poly_float voice = decoded.second; | |||
vital::poly_float phase_delta = vital::poly_float::abs(phase - last_phase_); | |||
vital::poly_float decay = vital::poly_float(1.0f) - phase_delta * kSpeedDecayMult; | |||
decay = vital::utils::clamp(decay, kBoostDecay, 1.0f); | |||
decay = vital::utils::maskLoad(decay, kBoostDecay, inactive_mask); | |||
decayBoosts(decay); | |||
vital::poly_mask switch_mask = vital::poly_float::notEqual(voice, last_voice_) | inactive_mask; | |||
vital::poly_float phase_reset = vital::utils::max(0.0f, phase - frequency * kBackupTime); | |||
last_phase_ = vital::utils::maskLoad(last_phase_, phase_reset, switch_mask); | |||
bool animating = animate; | |||
if (parent_) | |||
animating = animating && parent_->getSynth()->isModSourceEnabled(getName().toStdString()); | |||
if (animating) | |||
boostRange(adjustBoostPhase(last_phase_), adjustBoostPhase(phase), kNumWrapPoints, decay); | |||
else | |||
decayBoosts(0.0f); | |||
last_phase_ = phase; | |||
last_voice_ = voice; | |||
setLineWidth(findValue(Skin::kWidgetLineWidth)); | |||
setFillCenter(findValue(Skin::kWidgetFillCenter)); | |||
Colour fill_color = findColour(Skin::kWidgetSecondary1, true); | |||
float fill_fade = findValue(Skin::kWidgetFillFade); | |||
Colour fill_color_fade = fill_color.withMultipliedAlpha(1.0f - fill_fade); | |||
Colour position_color = findColour(Skin::kWidgetPrimary1, true); | |||
Colour fill_color_stereo = findColour(Skin::kWidgetSecondary2, true); | |||
Colour fill_color_stereo_fade = fill_color_stereo.withMultipliedAlpha(1.0f - fill_fade); | |||
Colour position_color_stereo = findColour(Skin::kWidgetPrimary2, true); | |||
if (animating) { | |||
setFill(true); | |||
setBoostAmount(findValue(Skin::kWidgetLineBoost)); | |||
setFillBoostAmount(findValue(Skin::kWidgetFillBoost)); | |||
setIndex(1); | |||
setColor(findColour(Skin::kWidgetPrimary2, true)); | |||
setFillColors(fill_color_stereo_fade, fill_color_stereo); | |||
drawLines(open_gl, false); | |||
setIndex(0); | |||
setColor(findColour(Skin::kWidgetPrimary1, true)); | |||
setFillColors(fill_color_fade, fill_color); | |||
drawLines(open_gl, anyBoostValue()); | |||
setBoostAmount(0.0f); | |||
setFill(false); | |||
setColor(findColour(Skin::kWidgetCenterLine, true)); | |||
drawLines(open_gl, anyBoostValue()); | |||
setViewPort(open_gl); | |||
if (switch_mask.sum() == 0) { | |||
drawPosition(open_gl, position_color_stereo, phase[1]); | |||
drawPosition(open_gl, position_color, phase[0]); | |||
} | |||
} | |||
else { | |||
setBoostAmount(0.0f); | |||
setFillBoostAmount(0.0f); | |||
setFill(true); | |||
setColor(findColour(Skin::kWidgetPrimary2, true)); | |||
setFillColors(fill_color_stereo_fade, fill_color_stereo); | |||
drawLines(open_gl, false); | |||
setColor(findColour(Skin::kWidgetPrimary1, true)); | |||
setFillColors(fill_color_fade, fill_color); | |||
drawLines(open_gl, false); | |||
setFill(false); | |||
setColor(findColour(Skin::kWidgetCenterLine, true)); | |||
drawLines(open_gl, false); | |||
} | |||
renderPoints(open_gl, animate); | |||
renderCorners(open_gl, animate); | |||
} |
@@ -0,0 +1,66 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "line_generator.h" | |||
#include "open_gl_image.h" | |||
#include "line_editor.h" | |||
#include "synth_lfo.h" | |||
#include "synth_module.h" | |||
class SynthGuiInterface; | |||
class LfoEditor : public LineEditor { | |||
public: | |||
static constexpr float kBoostDecay = 0.9f; | |||
static constexpr float kSpeedDecayMult = 5.0f; | |||
enum { | |||
kSetPhaseToPoint = kNumMenuOptions, | |||
kSetPhaseToPower, | |||
kSetPhaseToGrid, | |||
kImportLfo, | |||
kExportLfo, | |||
}; | |||
LfoEditor(LineGenerator* lfo_source, String prefix, | |||
const vital::output_map& mono_modulations, const vital::output_map& poly_modulations); | |||
virtual ~LfoEditor(); | |||
void parentHierarchyChanged() override; | |||
virtual void mouseDown(const MouseEvent& e) override; | |||
virtual void mouseDoubleClick(const MouseEvent& e) override; | |||
virtual void mouseUp(const MouseEvent& e) override; | |||
void respondToCallback(int point, int power, int result) override; | |||
void setPhase(float phase); | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
private: | |||
SynthGuiInterface* parent_; | |||
const vital::StatusOutput* wave_phase_; | |||
const vital::StatusOutput* frequency_; | |||
vital::poly_float last_phase_; | |||
vital::poly_float last_voice_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LfoEditor) | |||
}; | |||
@@ -0,0 +1,236 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "line_generator.h" | |||
#include "open_gl_image_component.h" | |||
#include "open_gl_multi_image.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "open_gl_line_renderer.h" | |||
#include "synth_lfo.h" | |||
#include "synth_module.h" | |||
class LineEditor : public OpenGlLineRenderer, public TextEditor::Listener { | |||
public: | |||
static constexpr float kPositionWidth = 9.0f; | |||
static constexpr float kPowerWidth = 7.0f; | |||
static constexpr float kRingThickness = 0.45f; | |||
static constexpr float kGrabRadius = 12.0f; | |||
static constexpr float kDragRadius = 20.0f; | |||
static constexpr int kResolution = 64; | |||
static constexpr int kNumWrapPoints = 8; | |||
static constexpr int kDrawPoints = kResolution + LineGenerator::kMaxPoints; | |||
static constexpr int kTotalPoints = kDrawPoints + 2 * kNumWrapPoints; | |||
static constexpr int kMaxGridSizeX = 32; | |||
static constexpr int kMaxGridSizeY = 24; | |||
static constexpr float kPaddingY = 6.0f; | |||
static constexpr float kPaddingX = 0.0f; | |||
static constexpr float kPowerMouseMultiplier = 9.0f; | |||
static constexpr float kMinPointDistanceForPower = 3.0f; | |||
enum MenuOptions { | |||
kCancel, | |||
kCopy, | |||
kPaste, | |||
kSave, | |||
kEnterPhase, | |||
kEnterValue, | |||
kResetPower, | |||
kRemovePoint, | |||
kInit, | |||
kFlipHorizontal, | |||
kFlipVertical, | |||
kNumMenuOptions | |||
}; | |||
class Listener { | |||
public: | |||
virtual ~Listener() { } | |||
virtual void setPhase(float phase) = 0; | |||
virtual void lineEditorScrolled(const MouseEvent& e, const MouseWheelDetails& wheel) = 0; | |||
virtual void togglePaintMode(bool enabled, bool temporary_switch) = 0; | |||
virtual void fileLoaded() = 0; | |||
virtual void importLfo() = 0; | |||
virtual void exportLfo() = 0; | |||
virtual void pointChanged(int index, Point<float> position, bool mouse_up) { } | |||
virtual void powersChanged(bool mouse_up) { } | |||
virtual void pointAdded(int index, Point<float> position) { } | |||
virtual void pointRemoved(int index) { } | |||
virtual void pointsAdded(int index, int num_points_added) { } | |||
virtual void pointsRemoved(int index, int num_points_removed) { } | |||
}; | |||
LineEditor(LineGenerator* line_source); | |||
virtual ~LineEditor(); | |||
void resetWavePath(); | |||
void resized() override { | |||
OpenGlLineRenderer::resized(); | |||
drag_circle_.setBounds(getLocalBounds()); | |||
hover_circle_.setBounds(getLocalBounds()); | |||
grid_lines_.setBounds(getLocalBounds()); | |||
position_circle_.setBounds(getLocalBounds()); | |||
point_circles_.setBounds(getLocalBounds()); | |||
power_circles_.setBounds(getLocalBounds()); | |||
resetPositions(); | |||
} | |||
float padY(float y); | |||
float unpadY(float y); | |||
float padX(float x); | |||
float unpadX(float x); | |||
virtual void mouseDown(const MouseEvent& e) override; | |||
virtual void mouseDoubleClick(const MouseEvent& e) override; | |||
virtual void mouseMove(const MouseEvent& e) override; | |||
virtual void mouseDrag(const MouseEvent& e) override; | |||
virtual void mouseUp(const MouseEvent& e) override; | |||
virtual void respondToCallback(int point, int power, int option); | |||
bool hasMatchingSystemClipboard(); | |||
void paintLine(const MouseEvent& e); | |||
void drawDown(const MouseEvent& e); | |||
void drawDrag(const MouseEvent& e); | |||
void drawUp(const MouseEvent& e); | |||
void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void clearActiveMouseActions(); | |||
void renderGrid(OpenGlWrapper& open_gl, bool animate); | |||
void renderPoints(OpenGlWrapper& open_gl, bool animate); | |||
void init(OpenGlWrapper& open_gl) override; | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void destroy(OpenGlWrapper& open_gl) override; | |||
void setSizeRatio(float ratio) { size_ratio_ = ratio; } | |||
float sizeRatio() const { return size_ratio_; } | |||
void setLoop(bool loop) { loop_ = loop; } | |||
void setSmooth(bool smooth) { model_->setSmooth(smooth); resetPositions(); } | |||
bool getSmooth() const { return model_->smooth(); } | |||
void setPaint(bool paint); | |||
void setPaintPattern(std::vector<std::pair<float, float>> pattern) { paint_pattern_ = pattern; } | |||
virtual void setGridSizeX(int size) { grid_size_x_ = size; setGridPositions(); } | |||
virtual void setGridSizeY(int size) { grid_size_y_ = size; setGridPositions(); } | |||
int getGridSizeX() { return grid_size_x_; } | |||
int getGridSizeY() { return grid_size_y_; } | |||
void setModel(LineGenerator* model) { model_ = model; resetPositions(); } | |||
LineGenerator* getModel() { return model_; } | |||
void showTextEntry(); | |||
void hideTextEntry(); | |||
void textEditorReturnKeyPressed(TextEditor& editor) override; | |||
void textEditorFocusLost(TextEditor& editor) override; | |||
void textEditorEscapeKeyPressed(TextEditor& editor) override; | |||
void setSliderPositionFromText(); | |||
void setAllowFileLoading(bool allow) { allow_file_loading_ = allow; } | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
void setActive(bool active) { active_ = active; } | |||
force_inline void resetPositions() { reset_positions_ = true; } | |||
OpenGlComponent* getTextEditorComponent() { | |||
if (value_entry_) | |||
return value_entry_->getImageComponent(); | |||
return nullptr; | |||
} | |||
protected: | |||
void drawPosition(OpenGlWrapper& open_gl, Colour color, float fraction_x); | |||
void setEditingCircleBounds(); | |||
void setGridPositions(); | |||
void setPointPositions(); | |||
void setGlPositions(); | |||
int getActivePoint() { return active_point_; } | |||
int getActivePower() { return active_power_; } | |||
int getActiveGridSection() { return active_grid_section_; } | |||
bool isPainting() { return paint_ != temporary_paint_toggle_; } | |||
bool isPaintEnabled() { return paint_; } | |||
vital::poly_float adjustBoostPhase(vital::poly_float phase); | |||
virtual void enableTemporaryPaintToggle(bool toggle); | |||
bool active_; | |||
std::vector<Listener*> listeners_; | |||
private: | |||
float adjustBoostPhase(float phase); | |||
static std::pair<vital::Output*, vital::Output*> getOutputs(const vital::output_map& mono_modulations, | |||
const vital::output_map& poly_modulations, | |||
String name) { | |||
return { | |||
mono_modulations.at(name.toStdString()), | |||
poly_modulations.at(name.toStdString()) | |||
}; | |||
} | |||
int getHoverPoint(Point<float> position); | |||
int getHoverPower(Point<float> position); | |||
float getSnapRadiusX(); | |||
float getSnapRadiusY(); | |||
float getSnappedX(float x); | |||
float getSnappedY(float y); | |||
void addPointAt(Point<float> position); | |||
void movePoint(int index, Point<float> position, bool snap); | |||
void movePower(int index, Point<float> position, bool all, bool alternate); | |||
void removePoint(int index); | |||
float getMinX(int index); | |||
float getMaxX(int index); | |||
Point<float> valuesToOpenGlPosition(float x, float y); | |||
Point<float> getPowerPosition(int index); | |||
bool powerActive(int index); | |||
LineGenerator* model_; | |||
int active_point_; | |||
int active_power_; | |||
int active_grid_section_; | |||
bool dragging_; | |||
bool reset_positions_; | |||
bool allow_file_loading_; | |||
Point<float> last_mouse_position_; | |||
int last_model_render_; | |||
bool loop_; | |||
int grid_size_x_; | |||
int grid_size_y_; | |||
bool paint_; | |||
bool temporary_paint_toggle_; | |||
std::vector<std::pair<float, float>> paint_pattern_; | |||
vital::poly_float last_phase_; | |||
vital::poly_float last_voice_; | |||
vital::poly_float last_last_voice_; | |||
float size_ratio_; | |||
OpenGlQuad drag_circle_; | |||
OpenGlQuad hover_circle_; | |||
OpenGlMultiQuad grid_lines_; | |||
OpenGlQuad position_circle_; | |||
OpenGlMultiQuad point_circles_; | |||
OpenGlMultiQuad power_circles_; | |||
std::unique_ptr<OpenGlTextEditor> value_entry_; | |||
bool entering_phase_; | |||
int entering_index_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LineEditor) | |||
}; | |||
@@ -0,0 +1,129 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "line_map_editor.h" | |||
#include "skin.h" | |||
#include "shaders.h" | |||
#include "synth_gui_interface.h" | |||
#include "utils.h" | |||
LineMapEditor::LineMapEditor(LineGenerator* line_source, String name) : LineEditor(line_source) { | |||
animate_ = true; | |||
raw_input_ = nullptr; | |||
last_phase_ = 0.0f; | |||
setFill(true); | |||
setFillCenter(-1.0f); | |||
setLoop(false); | |||
setName(name); | |||
setBoostAmount(0.0f); | |||
setFillBoostAmount(0.0f); | |||
} | |||
LineMapEditor::~LineMapEditor() { } | |||
void LineMapEditor::parentHierarchyChanged() { | |||
SynthGuiInterface* parent = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (parent && raw_input_ == nullptr) | |||
raw_input_ = parent->getSynth()->getStatusOutput(getName().toStdString()); | |||
LineEditor::parentHierarchyChanged(); | |||
} | |||
void LineMapEditor::render(OpenGlWrapper& open_gl, bool animate) { | |||
setGlPositions(); | |||
renderGrid(open_gl, animate); | |||
setLineWidth(findValue(Skin::kWidgetLineWidth)); | |||
Colour envelope_graph_fill = findColour(Skin::kWidgetSecondary1, true); | |||
Colour envelope_graph_fill_stereo = findColour(Skin::kWidgetSecondary2, true); | |||
float fill_fade = findValue(Skin::kWidgetFillFade); | |||
if (!active_) { | |||
envelope_graph_fill = findColour(Skin::kWidgetSecondaryDisabled, true); | |||
envelope_graph_fill_stereo = envelope_graph_fill; | |||
} | |||
Colour envelope_graph_fill_fade = envelope_graph_fill.withMultipliedAlpha(1.0f - fill_fade); | |||
Colour envelope_graph_fill_stereo_fade = envelope_graph_fill_stereo.withMultipliedAlpha(1.0f - fill_fade); | |||
Colour position_color = findColour(Skin::kWidgetPrimary1, true); | |||
Colour position_color_stereo = findColour(Skin::kWidgetPrimary2, true); | |||
Colour center = findColour(Skin::kWidgetCenterLine, true); | |||
if (!active_) { | |||
position_color = findColour(Skin::kWidgetPrimaryDisabled, true); | |||
center = position_color; | |||
position_color_stereo = position_color; | |||
} | |||
if (animate && animate_) { | |||
decayBoosts(kTailDecay); | |||
vital::poly_float phase = raw_input_->value(); | |||
if (!raw_input_->isClearValue(phase)) { | |||
vital::poly_float adjusted_phase = adjustBoostPhase(phase); | |||
boostRange(last_phase_, adjusted_phase, kNumWrapPoints, kTailDecay); | |||
last_phase_ = adjusted_phase; | |||
} | |||
setFill(true); | |||
setBoostAmount(findValue(Skin::kWidgetLineBoost)); | |||
setFillBoostAmount(findValue(Skin::kWidgetFillBoost)); | |||
setIndex(1); | |||
setColor(position_color_stereo); | |||
setFillColors(envelope_graph_fill_stereo_fade, envelope_graph_fill_stereo); | |||
drawLines(open_gl, false); | |||
setIndex(0); | |||
setColor(position_color); | |||
setFillColors(envelope_graph_fill_fade, envelope_graph_fill); | |||
drawLines(open_gl, true); | |||
setFill(false); | |||
setBoostAmount(0.0f); | |||
setFillBoostAmount(0.0f); | |||
setColor(center); | |||
LineEditor::render(open_gl, false); | |||
setViewPort(open_gl); | |||
drawPosition(open_gl, position_color_stereo, phase[1]); | |||
drawPosition(open_gl, position_color, phase[0]); | |||
} | |||
else { | |||
setBoostAmount(0.0f); | |||
setFillBoostAmount(0.0f); | |||
decayBoosts(0.0f); | |||
setFill(true); | |||
setColor(position_color_stereo); | |||
setFillColors(envelope_graph_fill_stereo_fade, envelope_graph_fill_stereo); | |||
drawLines(open_gl, false); | |||
setColor(position_color); | |||
setFillColors(envelope_graph_fill_fade, envelope_graph_fill); | |||
drawLines(open_gl, true); | |||
setFill(false); | |||
setColor(center); | |||
drawLines(open_gl, true); | |||
} | |||
renderPoints(open_gl, animate); | |||
renderCorners(open_gl, animate); | |||
} |
@@ -0,0 +1,46 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "line_generator.h" | |||
#include "open_gl_image.h" | |||
#include "line_editor.h" | |||
#include "synth_lfo.h" | |||
#include "synth_module.h" | |||
class LineMapEditor : public LineEditor { | |||
public: | |||
static constexpr float kTailDecay = 0.93f; | |||
LineMapEditor(LineGenerator* line_source, String name); | |||
virtual ~LineMapEditor(); | |||
void parentHierarchyChanged() override; | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void setAnimate(bool animate) { animate_ = animate; } | |||
private: | |||
const vital::StatusOutput* raw_input_; | |||
bool animate_; | |||
vital::poly_float last_phase_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LineMapEditor) | |||
}; | |||
@@ -0,0 +1,256 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "midi_keyboard.h" | |||
const float MidiKeyboard::kBlackKeyOffsets[kNumBlackKeysPerOctave] = { | |||
1.0f - 0.6f * kBlackKeyWidthRatio, | |||
2.0f - 0.4f * kBlackKeyWidthRatio, | |||
4.0f - 0.7f * kBlackKeyWidthRatio, | |||
5.0f - 0.5f * kBlackKeyWidthRatio, | |||
6.0f - 0.3f * kBlackKeyWidthRatio, | |||
}; | |||
const bool MidiKeyboard::kWhiteKeys[vital::kNotesPerOctave] = { | |||
true, false, true, false, true, true, false, true, false, true, false, true | |||
}; | |||
namespace { | |||
int getBlackKeyOctaveOffset(int black_key_index) { | |||
for (int i = 0; i < vital::kNotesPerOctave; ++i) { | |||
if (!MidiKeyboard::kWhiteKeys[i]) { | |||
if (black_key_index == 0) | |||
return i; | |||
black_key_index--; | |||
} | |||
} | |||
return -1; | |||
} | |||
int getWhiteKeyOctaveOffset(int white_key_index) { | |||
for (int i = 0; i < vital::kNotesPerOctave; ++i) { | |||
if (MidiKeyboard::kWhiteKeys[i]) { | |||
if (white_key_index == 0) | |||
return i; | |||
white_key_index--; | |||
} | |||
} | |||
return -1; | |||
} | |||
int getBlackKeyIndexFromOffset(int note_offset) { | |||
int black_key_index = 0; | |||
for (int i = 0; i < note_offset; ++i) { | |||
if (!MidiKeyboard::kWhiteKeys[i]) | |||
black_key_index++; | |||
} | |||
return black_key_index; | |||
} | |||
int getWhiteKeyIndexFromOffset(int note_offset) { | |||
int white_key_index = 0; | |||
for (int i = 0; i < note_offset; ++i) { | |||
if (MidiKeyboard::kWhiteKeys[i]) | |||
white_key_index++; | |||
} | |||
return white_key_index; | |||
} | |||
} | |||
MidiKeyboard::MidiKeyboard(MidiKeyboardState& state) : | |||
OpenGlComponent("keyboard"), state_(state), midi_channel_(1), hover_note_(-1), | |||
black_notes_(kNumBlackKeys, Shaders::kRoundedRectangleFragment), | |||
white_pressed_notes_(kNumWhiteKeys, Shaders::kRoundedRectangleFragment), | |||
black_pressed_notes_(kNumBlackKeys, Shaders::kRoundedRectangleFragment), | |||
hover_note_quad_(Shaders::kRoundedRectangleFragment) { | |||
black_notes_.setTargetComponent(this); | |||
white_pressed_notes_.setTargetComponent(this); | |||
black_pressed_notes_.setTargetComponent(this); | |||
hover_note_quad_.setTargetComponent(this); | |||
hover_note_quad_.setQuad(0, -2.0f, -2.0f, 0.0f, 0.0f); | |||
int num_children = getNumChildComponents(); | |||
for (int i = 0; i < num_children; ++i) { | |||
Component* child = getChildComponent(i); | |||
child->setWantsKeyboardFocus(false); | |||
} | |||
} | |||
void MidiKeyboard::paintBackground(Graphics& g) { | |||
float width = getWidth(); | |||
int height = getHeight(); | |||
g.setColour(black_key_color_); | |||
for (int i = 1; i < kNumWhiteKeys; ++i) { | |||
int x = i * width / kNumWhiteKeys; | |||
g.fillRect(x, 0, 1, height); | |||
} | |||
} | |||
void MidiKeyboard::parentHierarchyChanged() { | |||
setColors(); | |||
} | |||
void MidiKeyboard::setColors() { | |||
if (findParentComponentOfClass<SynthGuiInterface>() == nullptr) | |||
return; | |||
key_press_color_ = findColour(Skin::kWidgetPrimary1, true); | |||
hover_color_ = findColour(Skin::kWidgetAccent2, true); | |||
white_key_color_ = findColour(Skin::kWidgetSecondary1, true); | |||
black_key_color_ = findColour(Skin::kWidgetSecondary2, true); | |||
} | |||
void MidiKeyboard::resized() { | |||
OpenGlComponent::resized(); | |||
setColors(); | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
float black_key_height = 2.0f * ((int)(height * kBlackKeyHeightRatio)) / height; | |||
float black_key_y = 1.0f - black_key_height; | |||
float white_key_width = 2.0f / kNumWhiteKeys; | |||
float black_key_width = (((int)(kBlackKeyWidthRatio * white_key_width * width / 4.0f)) * 4.0f + 2.0f) / width; | |||
float octave_width = kNumWhiteKeysPerOctave * white_key_width; | |||
for (int i = 0; i < kNumBlackKeys; ++i) { | |||
int octave = i / kNumBlackKeysPerOctave; | |||
int index = i % kNumBlackKeysPerOctave; | |||
float x = -1.0f + octave_width * octave + kBlackKeyOffsets[index] * white_key_width; | |||
x = ((int)((x + 1.0f) * width / 2.0f)) * 2.0f / width - 1.0f; | |||
black_notes_.setQuad(i, x, black_key_y, black_key_width, black_key_height + 0.5f); | |||
} | |||
float widget_rounding = findValue(Skin::kWidgetRoundedCorner); | |||
black_notes_.setRounding(widget_rounding); | |||
hover_note_quad_.setRounding(widget_rounding); | |||
black_pressed_notes_.setRounding(widget_rounding); | |||
} | |||
int MidiKeyboard::getNoteAtPosition(Point<float> position) { | |||
float white_key_position = kNumWhiteKeys * position.x / getWidth(); | |||
int octave = white_key_position / kNumWhiteKeysPerOctave; | |||
float white_key_in_octave = white_key_position - octave * kNumWhiteKeysPerOctave; | |||
if (isBlackKeyHeight(position)) { | |||
for (int i = 0; i < kNumBlackKeysPerOctave; ++i) { | |||
float note_offset = white_key_in_octave - kBlackKeyOffsets[i]; | |||
if (note_offset <= kBlackKeyWidthRatio && note_offset >= 0.0f) { | |||
int note = octave * vital::kNotesPerOctave + getBlackKeyOctaveOffset(i); | |||
return std::min(vital::kMidiSize - 1, std::max(note, 0)); | |||
} | |||
} | |||
} | |||
int white_key_index = std::min<int>(kNumWhiteKeysPerOctave - 1, white_key_in_octave); | |||
int note = octave * vital::kNotesPerOctave + getWhiteKeyOctaveOffset(white_key_index); | |||
return std::min(vital::kMidiSize - 1, std::max(note, 0)); | |||
} | |||
float MidiKeyboard::getVelocityForNote(int midi, Point<float> position) { | |||
static constexpr float kMinVelocity = 1.0f / (vital::kMidiSize - 1); | |||
float velocity = position.y / getHeight(); | |||
if (!isWhiteKey(midi)) | |||
velocity = position.y / (kBlackKeyHeightRatio * getHeight()); | |||
return std::max(kMinVelocity, std::min(1.0f, velocity)); | |||
} | |||
void MidiKeyboard::render(OpenGlWrapper& open_gl, bool animate) { | |||
setPressedKeyPositions(); | |||
hover_note_quad_.setColor(hover_color_); | |||
int hover_note = hover_note_; | |||
if (hover_note >= 0) { | |||
int octave = hover_note / vital::kNotesPerOctave; | |||
int note_offset = hover_note - octave * vital::kNotesPerOctave; | |||
if (isWhiteKey(hover_note)) { | |||
int index = octave * kNumWhiteKeysPerOctave + getWhiteKeyIndexFromOffset(note_offset); | |||
setWhiteKeyQuad(&hover_note_quad_, 0, index); | |||
hover_note_quad_.render(open_gl, animate); | |||
} | |||
else { | |||
int index = octave * kNumBlackKeysPerOctave + getBlackKeyIndexFromOffset(note_offset); | |||
setBlackKeyQuad(&hover_note_quad_, 0, index); | |||
} | |||
} | |||
white_pressed_notes_.setColor(key_press_color_); | |||
white_pressed_notes_.render(open_gl, animate); | |||
black_notes_.setColor(black_key_color_); | |||
black_notes_.render(open_gl, animate); | |||
if (hover_note >= 0 && !isWhiteKey(hover_note)) | |||
hover_note_quad_.render(open_gl, animate); | |||
black_pressed_notes_.setColor(key_press_color_); | |||
black_pressed_notes_.render(open_gl, animate); | |||
} | |||
void MidiKeyboard::setPressedKeyPositions() { | |||
int num_pressed_white_keys = 0; | |||
int num_pressed_black_keys = 0; | |||
int white_key_index = 0; | |||
int black_key_index = 0; | |||
for (int i = 0; i < vital::kMidiSize; ++i) { | |||
bool white_key = isWhiteKey(i); | |||
if (state_.isNoteOnForChannels(0xffff, i)) { | |||
if (white_key) { | |||
setWhiteKeyQuad(&white_pressed_notes_, num_pressed_white_keys, white_key_index); | |||
num_pressed_white_keys++; | |||
} | |||
else { | |||
setBlackKeyQuad(&black_pressed_notes_, num_pressed_black_keys, black_key_index); | |||
num_pressed_black_keys++; | |||
} | |||
} | |||
if (white_key) | |||
white_key_index++; | |||
else | |||
black_key_index++; | |||
} | |||
white_pressed_notes_.setNumQuads(num_pressed_white_keys); | |||
black_pressed_notes_.setNumQuads(num_pressed_black_keys); | |||
} | |||
void MidiKeyboard::setWhiteKeyQuad(OpenGlMultiQuad* quads, int quad_index, int white_key_index) { | |||
float full_width = getWidth(); | |||
int start_x = white_key_index * full_width / kNumWhiteKeys + 1; | |||
int end_x = (white_key_index + 1) * full_width / kNumWhiteKeys; | |||
float x = 2.0f * start_x / full_width - 1.0f; | |||
float width = 2.0f * (end_x - start_x) / full_width; | |||
quads->setQuad(quad_index, x, -2.0f, width, 4.0f); | |||
} | |||
void MidiKeyboard::setBlackKeyQuad(OpenGlMultiQuad* quads, int quad_index, int black_key_index) { | |||
float x = black_notes_.getQuadX(black_key_index); | |||
float y = black_notes_.getQuadY(black_key_index); | |||
float width = black_notes_.getQuadWidth(black_key_index); | |||
float height = black_notes_.getQuadHeight(black_key_index); | |||
float border = 2.0f / getWidth(); | |||
float y_adjust = 2.0f / getHeight(); | |||
quads->setQuad(quad_index, x + border, y + y_adjust, width - 2.0f * border, height); | |||
} |
@@ -0,0 +1,120 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "synth_gui_interface.h" | |||
class MidiKeyboard : public OpenGlComponent { | |||
public: | |||
static const float kBlackKeyOffsets[]; | |||
static const bool kWhiteKeys[]; | |||
static constexpr int kNumWhiteKeys = 75; | |||
static constexpr int kNumWhiteKeysPerOctave = 7; | |||
static constexpr int kNumBlackKeys = vital::kMidiSize - kNumWhiteKeys; | |||
static constexpr int kNumBlackKeysPerOctave = vital::kNotesPerOctave - kNumWhiteKeysPerOctave; | |||
static constexpr float kBlackKeyHeightRatio = 0.7f; | |||
static constexpr float kBlackKeyWidthRatio = 0.8f; | |||
force_inline static bool isWhiteKey(int midi) { | |||
return kWhiteKeys[midi % vital::kNotesPerOctave]; | |||
} | |||
MidiKeyboard(MidiKeyboardState& state); | |||
void paintBackground(Graphics& g) override; | |||
void parentHierarchyChanged() override; | |||
void resized() override; | |||
int getNoteAtPosition(Point<float> position); | |||
bool isBlackKeyHeight(Point<float> position) { return position.y / getHeight() < kBlackKeyHeightRatio; } | |||
float getVelocityForNote(int midi, Point<float> position); | |||
void init(OpenGlWrapper& open_gl) override { | |||
black_notes_.init(open_gl); | |||
white_pressed_notes_.init(open_gl); | |||
black_pressed_notes_.init(open_gl); | |||
} | |||
void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void setPressedKeyPositions(); | |||
void destroy(OpenGlWrapper& open_gl) override { | |||
black_notes_.destroy(open_gl); | |||
white_pressed_notes_.destroy(open_gl); | |||
black_pressed_notes_.destroy(open_gl); | |||
} | |||
void mouseDown(const MouseEvent& e) override { | |||
hover_note_ = getNoteAtPosition(e.position); | |||
state_.noteOn(midi_channel_, hover_note_, getVelocityForNote(hover_note_, e.position)); | |||
} | |||
void mouseUp(const MouseEvent& e) override { | |||
state_.noteOff(midi_channel_, hover_note_, 0.0f); | |||
hover_note_ = getNoteAtPosition(e.position); | |||
} | |||
void mouseEnter(const MouseEvent& e) override { | |||
hover_note_ = getNoteAtPosition(e.position); | |||
} | |||
void mouseExit(const MouseEvent& e) override { | |||
hover_note_ = -1; | |||
} | |||
void mouseDrag(const MouseEvent& e) override { | |||
int note = getNoteAtPosition(e.position); | |||
if (note == hover_note_) | |||
return; | |||
state_.noteOff(midi_channel_, hover_note_, 0.0f); | |||
state_.noteOn(midi_channel_, note, getVelocityForNote(note, e.position)); | |||
hover_note_ = note; | |||
} | |||
void mouseMove(const MouseEvent& e) override { | |||
hover_note_ = getNoteAtPosition(e.position); | |||
} | |||
void setMidiChannel(int channel) { midi_channel_ = channel; } | |||
void setColors(); | |||
private: | |||
void setWhiteKeyQuad(OpenGlMultiQuad* quads, int quad_index, int white_key_index); | |||
void setBlackKeyQuad(OpenGlMultiQuad* quads, int quad_index, int black_key_index); | |||
MidiKeyboardState& state_; | |||
int midi_channel_; | |||
int hover_note_; | |||
OpenGlMultiQuad black_notes_; | |||
OpenGlMultiQuad white_pressed_notes_; | |||
OpenGlMultiQuad black_pressed_notes_; | |||
OpenGlQuad hover_note_quad_; | |||
Colour key_press_color_; | |||
Colour hover_color_; | |||
Colour white_key_color_; | |||
Colour black_key_color_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiKeyboard) | |||
}; | |||
@@ -0,0 +1,343 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "modulation_button.h" | |||
#include "default_look_and_feel.h" | |||
#include "modulation_matrix.h" | |||
#include "synth_gui_interface.h" | |||
#include "fonts.h" | |||
#include "skin.h" | |||
ModulationButton::ModulationButton(String name) : PlainShapeComponent(std::move(name)), parent_(nullptr), | |||
mouse_state_(kNone), selected_(false), connect_right_(false), | |||
draw_border_(false), active_modulation_(false), font_size_(12.0f), | |||
show_drag_drop_(false), drag_drop_alpha_(0.0f) { | |||
setWantsKeyboardFocus(true); | |||
Path shape = Paths::dragDropArrows(); | |||
shape.addLineSegment(Line<float>(-50.0f, -50.0f, -50.0f, -50.0f), 0.2f); | |||
setShape(Paths::dragDropArrows()); | |||
setComponent(&drag_drop_area_); | |||
setActive(false); | |||
setUseAlpha(true); | |||
setInterceptsMouseClicks(true, false); | |||
addAndMakeVisible(drag_drop_area_); | |||
drag_drop_area_.setInterceptsMouseClicks(false, false); | |||
setColor(Colours::transparentWhite); | |||
} | |||
ModulationButton::~ModulationButton() { | |||
if (parent_) | |||
parent_->getSynth()->forceShowModulation(getName().toStdString(), false); | |||
} | |||
bool ModulationButton::hasAnyModulation() { | |||
if (parent_) | |||
return parent_->getSynth()->isSourceConnected(getName().toStdString()); | |||
return false; | |||
} | |||
Rectangle<int> ModulationButton::getModulationAmountBounds(int index, int total) { | |||
int columns = kModulationKnobColumns; | |||
int row = index / columns; | |||
int column = index % columns; | |||
Rectangle<int> all_bounds = getModulationAreaBounds(); | |||
int x = all_bounds.getX() + (all_bounds.getWidth() * column) / columns; | |||
int right = all_bounds.getX() + (all_bounds.getWidth() * (column + 1)) / columns; | |||
int width = right - x; | |||
int y = all_bounds.getY() + all_bounds.getHeight() - width * (row + 1); | |||
return Rectangle<int>(x, y, width, width); | |||
} | |||
Rectangle<int> ModulationButton::getMeterBounds() { | |||
static constexpr int kMinMeterWidth = 4; | |||
int width = getWidth(); | |||
int meter_width = std::max<int>(kMinMeterWidth, std::round(width * kMeterAreaRatio / 2.0f) * 2); | |||
int meter_height = getHeight() - 2; | |||
return Rectangle<int>(1, 1, meter_width, meter_height); | |||
} | |||
Rectangle<int> ModulationButton::getModulationAreaBounds() { | |||
static constexpr int kMaxWidthHeightRatio = 3; | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
int widget_margin = 0; | |||
if (parent) | |||
widget_margin = parent->findValue(Skin::kWidgetMargin); | |||
int width = getWidth() - getMeterBounds().getRight(); | |||
int height = getHeight(); | |||
int widget_width = width - 2 * widget_margin; | |||
int knob_width = widget_width / kModulationKnobColumns; | |||
widget_width = knob_width * kModulationKnobColumns; | |||
int widget_x = getMeterBounds().getRight() + (width - widget_width) / 2; | |||
int min_y = kFontAreaHeightRatio * width; | |||
int max_widget_height = ceilf(widget_width * 2.0f / 3.0f); | |||
int widget_y = std::max(min_y, height - widget_margin - max_widget_height); | |||
int widget_height = height - widget_y - widget_margin; | |||
int center_y = widget_y + widget_height / 2; | |||
widget_height = std::max(widget_height, (widget_width + kMaxWidthHeightRatio - 1) / kMaxWidthHeightRatio); | |||
widget_y = center_y - widget_height / 2; | |||
return Rectangle<int>(widget_x, widget_y, widget_width, widget_height); | |||
} | |||
void ModulationButton::paintBackground(Graphics& g) { | |||
static constexpr float kShadowArea = 0.04f; | |||
if (getWidth() == 0 || getHeight() == 0) | |||
return; | |||
if (selected_) | |||
g.setColour(findColour(Skin::kModulationButtonSelected, true)); | |||
else | |||
g.setColour(findColour(Skin::kModulationButtonUnselected, true)); | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
int rounding_amount = 0; | |||
if (parent) | |||
rounding_amount = parent->findValue(Skin::kBodyRounding); | |||
Rectangle<float> meter_bounds = getMeterBounds().toFloat(); | |||
int width = getWidth(); | |||
int adjusted_width = connect_right_ ? width * 2 : width; | |||
Rectangle<float> bounds(0, 0, adjusted_width, getHeight()); | |||
g.fillRoundedRectangle(bounds, rounding_amount); | |||
g.setColour(findColour(Skin::kWidgetBackground, true)); | |||
g.fillRoundedRectangle(meter_bounds, meter_bounds.getWidth() / 2.0f); | |||
float meter_width = meter_bounds.getWidth(); | |||
g.fillRect(meter_bounds.getX() + meter_width / 2.0f, meter_bounds.getY(), meter_width / 2, meter_bounds.getHeight()); | |||
if (draw_border_) { | |||
g.setColour(findColour(Skin::kBorder, true)); | |||
g.drawRoundedRectangle(bounds.reduced(0.5f), rounding_amount, 1.0f); | |||
} | |||
int height = getHeight(); | |||
g.setColour(findColour(Skin::kBodyText, true)); | |||
g.setFont(Fonts::instance()->proportional_regular().withPointHeight(font_size_)); | |||
String text = text_override_; | |||
if (text.isEmpty()) | |||
text = ModulationMatrix::getUiSourceDisplayName(getName()); | |||
int font_area_height = kFontAreaHeightRatio * width; | |||
g.drawText(text, meter_bounds.getRight(), 0, width - meter_bounds.getRight(), | |||
font_area_height, Justification::centred); | |||
if (connect_right_ && !selected_) { | |||
int shadow_width = width * kShadowArea; | |||
Colour shadow_color = findColour(Skin::kShadow, true); | |||
ColourGradient gradient(shadow_color, width, 0, shadow_color.withAlpha(0.0f), width - shadow_width, 0, false); | |||
g.setGradientFill(gradient); | |||
g.fillRect(width - shadow_width, 0, shadow_width, height); | |||
} | |||
} | |||
void ModulationButton::parentHierarchyChanged() { | |||
if (parent_ == nullptr) { | |||
parent_ = findParentComponentOfClass<SynthGuiInterface>(); | |||
setForceEnableModulationSource(); | |||
} | |||
} | |||
void ModulationButton::resized() { | |||
static constexpr float kBorder = 0.2f; | |||
PlainShapeComponent::resized(); | |||
Rectangle<float> meter_bounds = getMeterBounds().toFloat(); | |||
int left = meter_bounds.getRight(); | |||
int width = getWidth() - left; | |||
int font_area_height = kFontAreaHeightRatio * width; | |||
int top = font_area_height - (font_area_height - font_size_) * 0.5f; | |||
int height = getHeight() - top; | |||
float size_mult = 1.0f - 2.0f * kBorder; | |||
drag_drop_area_.setBounds(left + width * kBorder, top + height * kBorder, | |||
width * size_mult, height * size_mult); | |||
} | |||
void ModulationButton::render(OpenGlWrapper& open_gl, bool animate) { | |||
static constexpr float kDeltaAlpha = 0.15f; | |||
float target = 0.0f; | |||
if (show_drag_drop_) { | |||
target = 1.0f; | |||
if (mouse_state_ == kMouseDown || mouse_state_ == kMouseDragging) | |||
target = 2.0f; | |||
} | |||
bool increase = drag_drop_alpha_ < target; | |||
if (increase) | |||
drag_drop_alpha_ = std::min(drag_drop_alpha_ + kDeltaAlpha, target); | |||
else | |||
drag_drop_alpha_ = std::max(drag_drop_alpha_ - kDeltaAlpha, target); | |||
if (drag_drop_alpha_ <= 0.0f) { | |||
drag_drop_alpha_ = 0.0f; | |||
setActive(false); | |||
} | |||
setColor(drag_drop_color_.withMultipliedAlpha(drag_drop_alpha_)); | |||
PlainShapeComponent::render(open_gl, animate); | |||
} | |||
void ModulationButton::mouseDown(const MouseEvent& e) { | |||
if (e.mods.isPopupMenu()) { | |||
if (parent_ == nullptr) | |||
return; | |||
std::vector<vital::ModulationConnection*> connections = | |||
parent_->getSynth()->getSourceConnections(getName().toStdString()); | |||
if (connections.empty()) | |||
return; | |||
mouse_state_ = kNone; | |||
PopupItems options; | |||
std::string disconnect = "Disconnect from "; | |||
for (int i = 0; i < connections.size(); ++i) { | |||
std::string destination = vital::Parameters::getDisplayName(connections[i]->destination_name); | |||
options.addItem(kModulationList + i, disconnect + destination); | |||
} | |||
if (connections.size() > 1) | |||
options.addItem(kDisconnect, "Disconnect all"); | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
parent->showPopupSelector(this, e.getPosition(), options, [=](int selection) { disconnectIndex(selection); }); | |||
} | |||
else { | |||
setActiveModulation(true); | |||
mouse_state_ = kMouseDown; | |||
for (Listener* listener : listeners_) | |||
listener->modulationSelected(this); | |||
} | |||
} | |||
void ModulationButton::mouseDrag(const MouseEvent& e) { | |||
if (e.mods.isRightButtonDown()) | |||
return; | |||
if (!getLocalBounds().contains(e.getPosition()) && mouse_state_ != kDraggingOut) { | |||
for (Listener* listener : listeners_) | |||
listener->startModulationMap(this, e); | |||
mouse_state_ = kDraggingOut; | |||
setMouseCursor(MouseCursor::DraggingHandCursor); | |||
} | |||
if (mouse_state_ == kDraggingOut) { | |||
for (Listener* listener : listeners_) | |||
listener->modulationDragged(e); | |||
} | |||
else if (mouse_state_ != kMouseDragging) | |||
mouse_state_ = kMouseDragging; | |||
} | |||
void ModulationButton::mouseUp(const MouseEvent& e) { | |||
if (!e.mods.isRightButtonDown() && mouse_state_ == kDraggingOut) { | |||
for (Listener* listener : listeners_) | |||
listener->endModulationMap(); | |||
} | |||
else if (!e.mods.isRightButtonDown()) { | |||
for (Listener* listener : listeners_) | |||
listener->modulationClicked(this); | |||
} | |||
setMouseCursor(MouseCursor::ParentCursor); | |||
mouse_state_ = kHover; | |||
} | |||
void ModulationButton::mouseEnter(const MouseEvent& e) { | |||
mouse_state_ = kHover; | |||
drag_drop_color_ = findColour(Skin::kLightenScreen, true); | |||
show_drag_drop_ = parent_->getSynth()->getSourceConnections(getName().toStdString()).empty(); | |||
setActive(show_drag_drop_); | |||
redrawImage(true); | |||
} | |||
void ModulationButton::mouseExit(const MouseEvent& e) { | |||
mouse_state_ = kNone; | |||
show_drag_drop_ = false; | |||
} | |||
void ModulationButton::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) { | |||
for (Listener* listener : listeners_) | |||
listener->modulationWheelMoved(e, wheel); | |||
} | |||
void ModulationButton::focusLost(FocusChangeType cause) { | |||
for (Listener* listener : listeners_) | |||
listener->modulationLostFocus(this); | |||
} | |||
void ModulationButton::addListener(Listener* listener) { | |||
listeners_.push_back(listener); | |||
} | |||
void ModulationButton::disconnectIndex(int index) { | |||
if (parent_ == nullptr) | |||
return; | |||
std::vector<vital::ModulationConnection*> connections = | |||
parent_->getSynth()->getSourceConnections(getName().toStdString()); | |||
if (index == kDisconnect) { | |||
for (vital::ModulationConnection* connection : connections) | |||
disconnectModulation(connection); | |||
} | |||
else if (index >= kModulationList) { | |||
int connection_index = index - kModulationList; | |||
disconnectModulation(connections[connection_index]); | |||
} | |||
} | |||
void ModulationButton::select(bool select) { | |||
selected_ = select; | |||
setForceEnableModulationSource(); | |||
} | |||
void ModulationButton::setActiveModulation(bool active) { | |||
active_modulation_ = active; | |||
setForceEnableModulationSource(); | |||
} | |||
void ModulationButton::setForceEnableModulationSource() { | |||
if (parent_) | |||
parent_->getSynth()->forceShowModulation(getName().toStdString(), active_modulation_); | |||
} | |||
void ModulationButton::disconnectModulation(vital::ModulationConnection* connection) { | |||
int modulations_left = parent_->getSynth()->getNumModulations(connection->destination_name); | |||
for (Listener* listener : listeners_) { | |||
listener->modulationDisconnected(connection, modulations_left <= 1); | |||
listener->modulationConnectionChanged(); | |||
} | |||
parent_->disconnectModulation(connection); | |||
if (modulations_left <= 1) { | |||
for (Listener* listener : listeners_) | |||
listener->modulationCleared(); | |||
} | |||
} | |||
@@ -0,0 +1,121 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "open_gl_image_component.h" | |||
namespace vital { | |||
struct ModulationConnection; | |||
} // namespace vital | |||
class SynthGuiInterface; | |||
class ModulationButton : public PlainShapeComponent { | |||
public: | |||
static constexpr float kFontAreaHeightRatio = 0.3f; | |||
static constexpr int kModulationKnobColumns = 3; | |||
static constexpr int kModulationKnobRows = 2; | |||
static constexpr int kMaxModulationKnobs = kModulationKnobRows * kModulationKnobColumns; | |||
static constexpr float kMeterAreaRatio = 0.05f; | |||
enum MenuId { | |||
kCancel = 0, | |||
kDisconnect, | |||
kModulationList | |||
}; | |||
enum MouseState { | |||
kNone, | |||
kHover, | |||
kMouseDown, | |||
kMouseDragging, | |||
kDraggingOut | |||
}; | |||
class Listener { | |||
public: | |||
virtual ~Listener() = default; | |||
virtual void modulationConnectionChanged() { } | |||
virtual void modulationDisconnected(vital::ModulationConnection* connection, bool last) { } | |||
virtual void modulationSelected(ModulationButton* source) { } | |||
virtual void modulationLostFocus(ModulationButton* source) { } | |||
virtual void startModulationMap(ModulationButton* source, const MouseEvent& e) { } | |||
virtual void modulationDragged(const MouseEvent& e) { } | |||
virtual void modulationWheelMoved(const MouseEvent& e, const MouseWheelDetails& wheel) { } | |||
virtual void endModulationMap() { } | |||
virtual void modulationClicked(ModulationButton* source) { } | |||
virtual void modulationCleared() { } | |||
}; | |||
ModulationButton(String name); | |||
virtual ~ModulationButton(); | |||
void paintBackground(Graphics& g) override; | |||
void parentHierarchyChanged() override; | |||
void resized() override; | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) override; | |||
void mouseDown(const MouseEvent& e) override; | |||
void mouseDrag(const MouseEvent& e) override; | |||
void mouseUp(const MouseEvent& e) override; | |||
void mouseEnter(const MouseEvent& e) override; | |||
void mouseExit(const MouseEvent& e) override; | |||
void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override; | |||
void focusLost(FocusChangeType cause) override; | |||
void addListener(Listener* listener); | |||
void disconnectIndex(int index); | |||
void select(bool select); | |||
bool isSelected() const { return selected_; } | |||
void setActiveModulation(bool active); | |||
bool isActiveModulation() const { return active_modulation_; } | |||
void setForceEnableModulationSource(); | |||
bool hasAnyModulation(); | |||
void setFontSize(float size) { font_size_ = size; } | |||
Rectangle<int> getModulationAmountBounds(int index, int total); | |||
Rectangle<int> getModulationAreaBounds(); | |||
Rectangle<int> getMeterBounds(); | |||
void setConnectRight(bool connect) { connect_right_ = connect; repaint(); } | |||
void setDrawBorder(bool border) { draw_border_ = border; repaint(); } | |||
void overrideText(String text) { text_override_ = std::move(text); repaint(); } | |||
private: | |||
void disconnectModulation(vital::ModulationConnection* connection); | |||
String text_override_; | |||
SynthGuiInterface* parent_; | |||
std::vector<Listener*> listeners_; | |||
MouseState mouse_state_; | |||
bool selected_; | |||
bool connect_right_; | |||
bool draw_border_; | |||
bool active_modulation_; | |||
OpenGlImageComponent drag_drop_; | |||
Component drag_drop_area_; | |||
float font_size_; | |||
Colour drag_drop_color_; | |||
bool show_drag_drop_; | |||
float drag_drop_alpha_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationButton) | |||
}; | |||
@@ -0,0 +1,214 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "modulation_meter.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "synth_gui_interface.h" | |||
#include "shaders.h" | |||
#include "synth_section.h" | |||
#include "synth_slider.h" | |||
#include "text_look_and_feel.h" | |||
ModulationMeter::ModulationMeter(const vital::Output* mono_total, const vital::Output* poly_total, | |||
const SynthSlider* slider, OpenGlMultiQuad* quads, int index) : | |||
mono_total_(mono_total), poly_total_(poly_total), destination_(slider), | |||
quads_(quads), index_(index), current_value_(0.0), mod_percent_(0.0) { | |||
rotary_ = destination_->isRotary() && !destination_->isTextOrCurve(); | |||
if (destination_->getSliderStyle() == Slider::LinearBarVertical || destination_->isTextOrCurve()) | |||
quads->setRotatedCoordinates(index, -1.0f, -1.0f, 2.0f, 2.0f); | |||
setInterceptsMouseClicks(false, false); | |||
updateDrawing(false); | |||
} | |||
ModulationMeter::~ModulationMeter() { } | |||
void ModulationMeter::resized() { | |||
SynthGuiInterface* parent = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (parent) { | |||
std::vector<vital::ModulationConnection*> connections; | |||
connections = parent->getSynth()->getSourceConnections(getName().toStdString()); | |||
setModulated(!connections.empty()); | |||
} | |||
if (isVisible()) | |||
setVertices(); | |||
else | |||
collapseVertices(); | |||
} | |||
void ModulationMeter::setActive(bool active) { | |||
if (active) | |||
setVertices(); | |||
else | |||
collapseVertices(); | |||
} | |||
Rectangle<float> ModulationMeter::getMeterBounds() { | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
if (!destination_->isRotary() && !destination_->isTextOrCurve()) { | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
int widget_margin = parent->getWidgetMargin(); | |||
int total_width = destination_->isHorizontal() ? destination_->getHeight() : destination_->getWidth(); | |||
int extra = total_width % 2; | |||
int slider_width = std::floor(SynthSlider::kLinearWidthPercent * total_width * 0.5f) * 2.0f + extra; | |||
int inner_area = (total_width - slider_width) / 2; | |||
int outer_area = inner_area - widget_margin; | |||
int meter_width = SynthSlider::kLinearModulationPercent * total_width; | |||
int border = std::max<int>(1, (widget_margin - meter_width) * 0.5f); | |||
if (destination_->isHorizontal()) | |||
return Rectangle<float>(0.0f, outer_area + border, width, inner_area - outer_area - 2.0f * border); | |||
return Rectangle<float>(outer_area + border, 0.0f, inner_area - outer_area - 2.0f * border, height); | |||
} | |||
else if (!destination_->isTextOrCurve()) { | |||
float knob_scale = destination_->getKnobSizeScale(); | |||
float meter_width = destination_->findValue(Skin::kKnobModMeterArcSize) * knob_scale; | |||
meter_width += destination_->findValue(Skin::kKnobModMeterArcThickness) * (1.0f - knob_scale); | |||
float offset = destination_->findValue(Skin::kKnobOffset); | |||
float center_x = getWidth() * 0.5f; | |||
float center_y = getHeight() * 0.5f; | |||
return Rectangle<float>(center_x - meter_width * 0.5f, center_y - meter_width * 0.5f + offset, | |||
meter_width, meter_width); | |||
} | |||
return getLocalBounds().toFloat(); | |||
} | |||
void ModulationMeter::setVertices() { | |||
Rectangle<int> parent_bounds = getParentComponent()->getBounds(); | |||
Rectangle<int> bounds = getBounds(); | |||
Rectangle<float> meter_bounds = getMeterBounds(); | |||
float left = bounds.getX() + meter_bounds.getX(); | |||
float right = bounds.getX() + meter_bounds.getRight(); | |||
float top = parent_bounds.getHeight() - (bounds.getY() + meter_bounds.getY()); | |||
float bottom = parent_bounds.getHeight() - (bounds.getY() + meter_bounds.getBottom()); | |||
left_ = 2.0f * left / parent_bounds.getWidth() - 1.0f; | |||
right_ = 2.0f * right / parent_bounds.getWidth() - 1.0f; | |||
top_ = 2.0f * top / parent_bounds.getHeight() - 1.0f; | |||
bottom_ = 2.0f * bottom / parent_bounds.getHeight() - 1.0f; | |||
quads_->setQuad(index_, left_, bottom_, right_ - left_, top_ - bottom_); | |||
} | |||
void ModulationMeter::collapseVertices() { | |||
left_ = right_ = top_ = bottom_= 0.0f; | |||
quads_->setQuad(index_, left_, bottom_, right_ - left_, top_ - bottom_); | |||
mod_percent_ = 0.0f; | |||
} | |||
void ModulationMeter::setAmountQuadVertices(OpenGlQuad& quad) { | |||
Rectangle<float> meter_bounds = getMeterBounds(); | |||
if (rotary_) | |||
meter_bounds.expand(2.0f, 2.0f); | |||
float width = getWidth(); | |||
float height = getHeight(); | |||
float left = 2.0f * meter_bounds.getX() / width - 1.0f; | |||
float bottom = 1.0f - 2.0f * meter_bounds.getBottom() / height; | |||
bool vertical_bar = destination_->getSliderStyle() == Slider::LinearBarVertical || destination_->isTextOrCurve(); | |||
if (vertical_bar) | |||
quad.setRotatedCoordinates(0, -1.0f, -1.0f, 2.0f, 2.0f); | |||
else | |||
quad.setCoordinates(0, -1.0f, -1.0f, 2.0f, 2.0f); | |||
if (rotary_) | |||
quad.setQuad(0, left, bottom, 2.0f * meter_bounds.getWidth() / width, 2.0f * meter_bounds.getHeight() / height); | |||
else if (vertical_bar) { | |||
float thickness = 2.0f / width; | |||
quad.setQuad(0, left, bottom, thickness, 2.0f * meter_bounds.getHeight() / height); | |||
} | |||
else { | |||
float thickness = 2.0f / height; | |||
quad.setQuad(0, left, bottom + 2.0f * meter_bounds.getHeight() / height - thickness, | |||
2.0f * meter_bounds.getWidth() / width, thickness); | |||
} | |||
} | |||
void ModulationMeter::updateDrawing(bool use_poly) { | |||
if (mono_total_) { | |||
current_value_ = mono_total_->trigger_value; | |||
if (poly_total_ && use_poly) | |||
current_value_ += poly_total_->trigger_value; | |||
} | |||
float range = destination_->getMaximum() - destination_->getMinimum(); | |||
vital::poly_float value = (current_value_ - destination_->getMinimum()) * (1.0f / range); | |||
mod_percent_ = vital::utils::clamp(value, 0.0f, 1.0f); | |||
float knob_percent = (destination_->getValue() - destination_->getMinimum()) / range; | |||
vital::poly_float min_percent = vital::utils::min(mod_percent_, knob_percent); | |||
vital::poly_float max_percent = vital::utils::max(mod_percent_, knob_percent); | |||
quads_->setQuad(index_, left_, bottom_, right_ - left_, top_ - bottom_); | |||
if (rotary_) { | |||
if (&destination_->getLookAndFeel() == TextLookAndFeel::instance()) { | |||
min_percent = vital::utils::interpolate(-vital::kPi, 0.0f, min_percent); | |||
max_percent = vital::utils::interpolate(-vital::kPi, 0.0f, max_percent); | |||
} | |||
else { | |||
float angle = SynthSlider::kRotaryAngle; | |||
min_percent = vital::utils::interpolate(-angle, angle, min_percent); | |||
max_percent = vital::utils::interpolate(-angle, angle, max_percent); | |||
} | |||
} | |||
quads_->setShaderValue(index_, min_percent[0], 0); | |||
quads_->setShaderValue(index_, max_percent[0], 1); | |||
quads_->setShaderValue(index_, min_percent[1], 2); | |||
quads_->setShaderValue(index_, max_percent[1], 3); | |||
} | |||
void ModulationMeter::setModulationAmountQuad(OpenGlQuad& quad, float amount, bool bipolar) { | |||
float range = destination_->getMaximum() - destination_->getMinimum(); | |||
float knob_percent = (destination_->getValue() - destination_->getMinimum()) / range; | |||
float min_percent = std::min(knob_percent + amount, knob_percent); | |||
float max_percent = std::max(knob_percent + amount, knob_percent); | |||
if (bipolar) { | |||
min_percent = std::min(knob_percent + amount * 0.5f, knob_percent - amount * 0.5f); | |||
max_percent = std::max(knob_percent + amount * 0.5f, knob_percent - amount * 0.5f); | |||
} | |||
if (rotary_) { | |||
if (&destination_->getLookAndFeel() == TextLookAndFeel::instance()) { | |||
min_percent = vital::utils::interpolate(-vital::kPi, 0.0f, min_percent); | |||
max_percent = vital::utils::interpolate(-vital::kPi, 0.0f, max_percent); | |||
} | |||
else { | |||
float angle = SynthSlider::kRotaryAngle; | |||
min_percent = vital::utils::interpolate(-angle, angle, min_percent); | |||
max_percent = vital::utils::interpolate(-angle, angle, max_percent); | |||
min_percent = std::max(-angle, min_percent); | |||
max_percent = std::min(angle, max_percent); | |||
} | |||
} | |||
quad.setShaderValue(0, min_percent, 0); | |||
quad.setShaderValue(0, max_percent, 1); | |||
quad.setShaderValue(0, min_percent, 2); | |||
quad.setShaderValue(0, max_percent, 3); | |||
} |
@@ -0,0 +1,73 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "common.h" | |||
class OpenGlMultiQuad; | |||
class SynthSlider; | |||
namespace vital { | |||
struct Output; | |||
} | |||
class ModulationMeter : public Component { | |||
public: | |||
ModulationMeter(const vital::Output* mono_total, const vital::Output* poly_total, | |||
const SynthSlider* slider, OpenGlMultiQuad* quads, int index); | |||
virtual ~ModulationMeter(); | |||
void resized() override; | |||
void setActive(bool active); | |||
void updateDrawing(bool use_poly); | |||
void setModulationAmountQuad(OpenGlQuad& quad, float amount, bool bipolar); | |||
void setAmountQuadVertices(OpenGlQuad& quad); | |||
bool isModulated() const { return modulated_; } | |||
bool isRotary() const { return rotary_; } | |||
void setModulated(bool modulated) { modulated_ = modulated; } | |||
vital::poly_float getModPercent() { return mod_percent_; } | |||
const SynthSlider* destination() { return destination_; } | |||
private: | |||
ModulationMeter() = delete; | |||
Rectangle<float> getMeterBounds(); | |||
void setVertices(); | |||
void collapseVertices(); | |||
const vital::Output* mono_total_; | |||
const vital::Output* poly_total_; | |||
const SynthSlider* destination_; | |||
OpenGlMultiQuad* quads_; | |||
int index_; | |||
vital::poly_float current_value_; | |||
vital::poly_float mod_percent_; | |||
bool modulated_; | |||
bool rotary_; | |||
float left_, right_, top_, bottom_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationMeter) | |||
}; | |||
@@ -0,0 +1,185 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "modulation_tab_selector.h" | |||
#include "modulation_button.h" | |||
#include "synth_section.h" | |||
#include "skin.h" | |||
ModulationTabSelector::ModulationTabSelector(std::string prefix, int number) : | |||
SynthSection(prefix), vertical_(true), selections_enabled_(false), min_modulations_shown_(0), num_shown_(0) { | |||
for (int i = 0; i < number; ++i) { | |||
std::string name = prefix + "_" + std::to_string(i + 1); | |||
modulation_buttons_.push_back(std::make_unique<ModulationButton>(name)); | |||
addOpenGlComponent(modulation_buttons_.back().get()); | |||
modulation_buttons_.back()->addListener(this); | |||
} | |||
} | |||
ModulationTabSelector::ModulationTabSelector(String name, int number, const char** names) : | |||
SynthSection(name), vertical_(true), selections_enabled_(false), min_modulations_shown_(0), num_shown_(0) { | |||
for (int i = 0; i < number; ++i) { | |||
modulation_buttons_.push_back(std::make_unique<ModulationButton>(names[i])); | |||
addOpenGlComponent(modulation_buttons_.back().get()); | |||
modulation_buttons_.back()->addListener(this); | |||
} | |||
} | |||
ModulationTabSelector::~ModulationTabSelector() = default; | |||
void ModulationTabSelector::paintBackground(Graphics& g) { | |||
int num_to_show = getNumModulationsToShow(); | |||
if (num_shown_ != num_to_show) { | |||
checkNumShown(false); | |||
num_shown_ = num_to_show; | |||
} | |||
g.fillAll(findColour(Skin::kBackground, true)); | |||
paintTabShadow(g); | |||
for (auto& button : modulation_buttons_) { | |||
if (button->isVisible()) { | |||
g.saveState(); | |||
Rectangle<int> bounds = getLocalArea(button.get(), button->getLocalBounds()); | |||
g.reduceClipRegion(bounds); | |||
g.setOrigin(bounds.getTopLeft()); | |||
button->paintBackground(g); | |||
g.restoreState(); | |||
} | |||
} | |||
} | |||
void ModulationTabSelector::paintTabShadow(Graphics& g) { | |||
SynthSection* parent = findParentComponentOfClass<SynthSection>(); | |||
if (parent == nullptr) | |||
return; | |||
int rounding_amount = parent->findValue(Skin::kBodyRounding); | |||
g.setColour(findColour(Skin::kShadow, true)); | |||
g.fillRoundedRectangle(getLocalBounds().toFloat(), rounding_amount); | |||
} | |||
void ModulationTabSelector::resized() { | |||
checkNumShown(false); | |||
} | |||
void ModulationTabSelector::checkNumShown(bool should_repaint) { | |||
int num_to_show = getNumModulationsToShow(); | |||
if (vertical_) { | |||
float cell_height = float(getHeight() + 1) / num_to_show; | |||
int y = 0; | |||
for (int i = 0; i < num_to_show; ++i) { | |||
int last_y = y; | |||
y = std::round((i + 1) * cell_height); | |||
modulation_buttons_[i]->setBounds(0.0f, last_y, getWidth(), y - last_y - 1); | |||
modulation_buttons_[i]->setVisible(true); | |||
} | |||
} | |||
else { | |||
float cell_width = float(getWidth() + 1) / num_to_show; | |||
int x = 0; | |||
for (int i = 0; i < num_to_show; ++i) { | |||
int last_x = x; | |||
x = std::round((i + 1) * cell_width); | |||
modulation_buttons_[i]->setBounds(last_x, 0, x - last_x - 1, getHeight()); | |||
modulation_buttons_[i]->setVisible(true); | |||
} | |||
} | |||
for (int i = num_to_show; i < modulation_buttons_.size(); ++i) | |||
modulation_buttons_[i]->setVisible(false); | |||
if (num_to_show != num_shown_ && should_repaint) | |||
repaintBackground(); | |||
} | |||
void ModulationTabSelector::reset() { | |||
for (auto& button : modulation_buttons_) { | |||
button->select(false); | |||
button->setActiveModulation(false); | |||
} | |||
modulation_buttons_[0]->select(selections_enabled_); | |||
if (getNumModulationsToShow() != num_shown_) | |||
checkNumShown(true); | |||
modulation_buttons_[0]->select(selections_enabled_); | |||
for (Listener *listener : listeners_) | |||
listener->modulationSelected(this, 0); | |||
} | |||
void ModulationTabSelector::modulationClicked(ModulationButton* source) { | |||
int index = getModulationIndex(source->getName()); | |||
if (selections_enabled_) { | |||
for (int i = 0; i < modulation_buttons_.size(); ++i) | |||
modulation_buttons_[i]->select(index == i); | |||
} | |||
repaintBackground(); | |||
for (Listener *listener : listeners_) | |||
listener->modulationSelected(this, index); | |||
} | |||
void ModulationTabSelector::endModulationMap() { | |||
if (getNumModulationsToShow() != num_shown_) | |||
checkNumShown(true); | |||
} | |||
void ModulationTabSelector::modulationConnectionChanged() { | |||
if (getNumModulationsToShow() != num_shown_) | |||
checkNumShown(true); | |||
} | |||
void ModulationTabSelector::modulationCleared() { | |||
if (getNumModulationsToShow() != num_shown_) | |||
checkNumShown(true); | |||
} | |||
void ModulationTabSelector::registerModulationButtons(SynthSection* synth_section) { | |||
for (auto& button : modulation_buttons_) | |||
synth_section->addModulationButton(button.get(), false); | |||
} | |||
void ModulationTabSelector::setFontSize(float font_size) { | |||
for (auto& button : modulation_buttons_) | |||
button->setFontSize(font_size); | |||
} | |||
int ModulationTabSelector::getNumModulationsToShow() { | |||
int num_to_show = static_cast<int>(modulation_buttons_.size()); | |||
if (min_modulations_shown_ > 0) { | |||
num_to_show = min_modulations_shown_; | |||
for (int i = min_modulations_shown_ - 1; i < modulation_buttons_.size(); ++i) { | |||
if (modulation_buttons_[i]->hasAnyModulation()) | |||
num_to_show = i + 2; | |||
} | |||
} | |||
return std::min(num_to_show, static_cast<int>(modulation_buttons_.size())); | |||
} | |||
int ModulationTabSelector::getModulationIndex(String name) { | |||
for (int i = 0; i < modulation_buttons_.size(); ++i) { | |||
if (name == modulation_buttons_[i]->getName()) | |||
return i; | |||
} | |||
return 0; | |||
} |
@@ -0,0 +1,82 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "modulation_button.h" | |||
#include "modulation_button.h" | |||
class SynthSection; | |||
class ModulationTabSelector : public SynthSection, public ModulationButton::Listener { | |||
public: | |||
class Listener { | |||
public: | |||
virtual ~Listener() = default; | |||
virtual void modulationSelected(ModulationTabSelector* selector, int index) = 0; | |||
}; | |||
ModulationTabSelector(std::string prefix, int number); | |||
ModulationTabSelector(String name, int number, const char** names); | |||
virtual ~ModulationTabSelector(); | |||
void paintBackground(Graphics& g) override; | |||
void paintTabShadow(Graphics& g) override; | |||
void resized() override; | |||
void checkNumShown(bool should_repaint); | |||
void reset() override; | |||
void modulationClicked(ModulationButton* source) override; | |||
void modulationConnectionChanged() override; | |||
void endModulationMap() override; | |||
void modulationCleared() override; | |||
void addListener(Listener* listener) { listeners_.push_back(listener); } | |||
void registerModulationButtons(SynthSection* synth_section); | |||
void setFontSize(float font_size); | |||
void setVertical(bool vertical) { vertical_ = vertical; } | |||
void enableSelections() { selections_enabled_ = true; modulation_buttons_[0]->select(true); } | |||
void setMinModulationsShown(int num) { min_modulations_shown_ = num; } | |||
void connectRight(bool connect) { | |||
for (auto& modulation_button : modulation_buttons_) | |||
modulation_button->setConnectRight(connect); | |||
} | |||
ModulationButton* getButton(int index) { return modulation_buttons_[index].get(); } | |||
void drawBorders(bool draw) { | |||
for (auto& button : modulation_buttons_) | |||
button->setDrawBorder(draw); | |||
} | |||
private: | |||
int getModulationIndex(String name); | |||
int getNumModulationsToShow(); | |||
std::vector<std::unique_ptr<ModulationButton>> modulation_buttons_; | |||
std::vector<Listener*> listeners_; | |||
bool vertical_; | |||
bool selections_enabled_; | |||
int min_modulations_shown_; | |||
int num_shown_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationTabSelector) | |||
}; | |||
@@ -0,0 +1,149 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "open_gl_background.h" | |||
#include "open_gl_component.h" | |||
#include "common.h" | |||
#include "shaders.h" | |||
OpenGlBackground::OpenGlBackground() : image_shader_(nullptr), vertices_() { | |||
new_background_ = false; | |||
vertex_buffer_ = 0; | |||
triangle_buffer_ = 0; | |||
} | |||
OpenGlBackground::~OpenGlBackground() { } | |||
void OpenGlBackground::init(OpenGlWrapper& open_gl) { | |||
static const float vertices[] = { | |||
-1.0f, 1.0f, 0.0f, 1.0f, | |||
-1.0f, -1.0f, 0.0f, 0.0f, | |||
1.0f, -1.0f, 1.0f, 0.0f, | |||
1.0f, 1.0f, 1.0f, 1.0f | |||
}; | |||
memcpy(vertices_, vertices, 16 * sizeof(float)); | |||
static const int triangles[] = { | |||
0, 1, 2, | |||
2, 3, 0 | |||
}; | |||
open_gl.context.extensions.glGenBuffers(1, &vertex_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(static_cast<size_t>(sizeof(vertices))); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, vertices_, GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &triangle_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangle_buffer_); | |||
GLsizeiptr tri_size = static_cast<GLsizeiptr>(static_cast<size_t>(sizeof(triangles))); | |||
open_gl.context.extensions.glBufferData(GL_ELEMENT_ARRAY_BUFFER, tri_size, triangles, GL_STATIC_DRAW); | |||
image_shader_ = open_gl.shaders->getShaderProgram(Shaders::kImageVertex, Shaders::kImageFragment); | |||
image_shader_->use(); | |||
position_ = OpenGlComponent::getAttribute(open_gl, *image_shader_, "position"); | |||
texture_coordinates_ = OpenGlComponent::getAttribute(open_gl, *image_shader_, "tex_coord_in"); | |||
texture_uniform_ = OpenGlComponent::getUniform(open_gl, *image_shader_, "image"); | |||
} | |||
void OpenGlBackground::destroy(OpenGlWrapper& open_gl) { | |||
if (background_.getWidth()) | |||
background_.release(); | |||
image_shader_ = nullptr; | |||
position_ = nullptr; | |||
texture_coordinates_ = nullptr; | |||
texture_uniform_ = nullptr; | |||
open_gl.context.extensions.glDeleteBuffers(1, &vertex_buffer_); | |||
open_gl.context.extensions.glDeleteBuffers(1, &triangle_buffer_); | |||
} | |||
void OpenGlBackground::bind(OpenGLContext& open_gl_context) { | |||
open_gl_context.extensions.glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); | |||
open_gl_context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangle_buffer_); | |||
background_.bind(); | |||
} | |||
void OpenGlBackground::enableAttributes(OpenGLContext& open_gl_context) { | |||
if (position_ != nullptr) { | |||
open_gl_context.extensions.glVertexAttribPointer(position_->attributeID, 2, GL_FLOAT, | |||
GL_FALSE, 4 * sizeof(float), nullptr); | |||
open_gl_context.extensions.glEnableVertexAttribArray(position_->attributeID); | |||
} | |||
if (texture_coordinates_ != nullptr) { | |||
open_gl_context.extensions.glVertexAttribPointer(texture_coordinates_->attributeID, 2, GL_FLOAT, | |||
GL_FALSE, 4 * sizeof(float), | |||
(GLvoid*)(2 * sizeof(float))); | |||
open_gl_context.extensions.glEnableVertexAttribArray(texture_coordinates_->attributeID); | |||
} | |||
} | |||
void OpenGlBackground::disableAttributes(OpenGLContext& open_gl_context) { | |||
if (position_ != nullptr) | |||
open_gl_context.extensions.glDisableVertexAttribArray(position_->attributeID); | |||
if (texture_coordinates_ != nullptr) | |||
open_gl_context.extensions.glDisableVertexAttribArray(texture_coordinates_->attributeID); | |||
} | |||
void OpenGlBackground::render(OpenGlWrapper& open_gl) { | |||
mutex_.lock(); | |||
if ((new_background_ || background_.getWidth() == 0) && background_image_.getWidth() > 0) { | |||
new_background_ = false; | |||
background_.loadImage(background_image_); | |||
float width_ratio = (1.0f * background_.getWidth()) / background_image_.getWidth(); | |||
float height_ratio = (1.0f * background_.getHeight()) / background_image_.getHeight(); | |||
float width_end = 2.0f * width_ratio - 1.0f; | |||
float height_end = 1.0f - 2.0f * height_ratio; | |||
vertices_[8] = vertices_[12] = width_end; | |||
vertices_[5] = vertices_[9] = height_end; | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(static_cast<size_t>(16 * sizeof(float))); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, vertices_, GL_STATIC_DRAW); | |||
} | |||
glDisable(GL_BLEND); | |||
glDisable(GL_SCISSOR_TEST); | |||
image_shader_->use(); | |||
bind(open_gl.context); | |||
open_gl.context.extensions.glActiveTexture(GL_TEXTURE0); | |||
if (texture_uniform_ != nullptr && background_.getWidth()) | |||
texture_uniform_->set(0); | |||
enableAttributes(open_gl.context); | |||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); | |||
disableAttributes(open_gl.context); | |||
background_.unbind(); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); | |||
mutex_.unlock(); | |||
} | |||
void OpenGlBackground::updateBackgroundImage(Image background) { | |||
background_image_ = background; | |||
new_background_ = true; | |||
} |
@@ -0,0 +1,62 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "open_gl_component.h" | |||
#include <mutex> | |||
class OpenGlBackground { | |||
public: | |||
OpenGlBackground(); | |||
virtual ~OpenGlBackground(); | |||
void updateBackgroundImage(Image background); | |||
virtual void init(OpenGlWrapper& open_gl); | |||
virtual void render(OpenGlWrapper& open_gl); | |||
virtual void destroy(OpenGlWrapper& open_gl); | |||
void lock() { mutex_.lock(); } | |||
void unlock() { mutex_.unlock(); } | |||
OpenGLShaderProgram* shader() { return image_shader_; } | |||
OpenGLShaderProgram::Uniform* texture_uniform() { return texture_uniform_.get(); } | |||
void bind(OpenGLContext& open_gl_context); | |||
void enableAttributes(OpenGLContext& open_gl_context); | |||
void disableAttributes(OpenGLContext& open_gl_context); | |||
private: | |||
OpenGLShaderProgram* image_shader_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> texture_uniform_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> position_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> texture_coordinates_; | |||
float vertices_[16]; | |||
std::mutex mutex_; | |||
OpenGLTexture background_; | |||
bool new_background_; | |||
Image background_image_; | |||
GLuint vertex_buffer_; | |||
GLuint triangle_buffer_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OpenGlBackground) | |||
}; | |||
@@ -0,0 +1,193 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "open_gl_component.h" | |||
#include "open_gl_multi_quad.h" | |||
#include "full_interface.h" | |||
#include "skin.h" | |||
namespace { | |||
Rectangle<int> getGlobalBounds(Component* component, Rectangle<int> bounds) { | |||
Component* parent = component->getParentComponent(); | |||
while (parent && dynamic_cast<FullInterface*>(component) == nullptr) { | |||
bounds = bounds + component->getPosition(); | |||
component = parent; | |||
parent = component->getParentComponent(); | |||
} | |||
return bounds; | |||
} | |||
Rectangle<int> getGlobalVisibleBounds(Component* component, Rectangle<int> visible_bounds) { | |||
Component* parent = component->getParentComponent(); | |||
while (parent && dynamic_cast<FullInterface*>(parent) == nullptr) { | |||
visible_bounds = visible_bounds + component->getPosition(); | |||
parent->getLocalBounds().intersectRectangle(visible_bounds); | |||
component = parent; | |||
parent = component->getParentComponent(); | |||
} | |||
return visible_bounds + component->getPosition(); | |||
} | |||
} | |||
OpenGlComponent::OpenGlComponent(String name) : Component(name), only_bottom_corners_(false), | |||
parent_(nullptr), skin_override_(Skin::kNone), | |||
num_voices_readout_(nullptr) { | |||
background_color_ = Colours::transparentBlack; | |||
} | |||
OpenGlComponent::~OpenGlComponent() { } | |||
bool OpenGlComponent::setViewPort(Component* component, Rectangle<int> bounds, OpenGlWrapper& open_gl) { | |||
FullInterface* top_level = component->findParentComponentOfClass<FullInterface>(); | |||
float scale = open_gl.display_scale; | |||
float resize_scale = top_level->getResizingScale(); | |||
float render_scale = 1.0f; | |||
if (scale == 1.0f) | |||
render_scale *= open_gl.context.getRenderingScale(); | |||
float gl_scale = render_scale * resize_scale; | |||
Rectangle<int> top_level_bounds = top_level->getBounds(); | |||
Rectangle<int> global_bounds = getGlobalBounds(component, bounds); | |||
Rectangle<int> visible_bounds = getGlobalVisibleBounds(component, bounds); | |||
glViewport(gl_scale * global_bounds.getX(), | |||
std::ceil(scale * render_scale * top_level_bounds.getHeight()) - gl_scale * global_bounds.getBottom(), | |||
gl_scale * global_bounds.getWidth(), gl_scale * global_bounds.getHeight()); | |||
if (visible_bounds.getWidth() <= 0 || visible_bounds.getHeight() <= 0) | |||
return false; | |||
glScissor(gl_scale * visible_bounds.getX(), | |||
std::ceil(scale * render_scale * top_level_bounds.getHeight()) - gl_scale * visible_bounds.getBottom(), | |||
gl_scale * visible_bounds.getWidth(), gl_scale * visible_bounds.getHeight()); | |||
return true; | |||
} | |||
bool OpenGlComponent::setViewPort(Component* component, OpenGlWrapper& open_gl) { | |||
return setViewPort(component, component->getLocalBounds(), open_gl); | |||
} | |||
bool OpenGlComponent::setViewPort(OpenGlWrapper& open_gl) { | |||
return setViewPort(this, open_gl); | |||
} | |||
void OpenGlComponent::setScissor(Component* component, OpenGlWrapper& open_gl) { | |||
setScissorBounds(component, component->getLocalBounds(), open_gl); | |||
} | |||
void OpenGlComponent::setScissorBounds(Component* component, Rectangle<int> bounds, OpenGlWrapper& open_gl) { | |||
if (component == nullptr) | |||
return; | |||
FullInterface* top_level = component->findParentComponentOfClass<FullInterface>(); | |||
float scale = open_gl.display_scale; | |||
float resize_scale = top_level->getResizingScale(); | |||
float render_scale = 1.0f; | |||
if (scale == 1.0f) | |||
render_scale *= open_gl.context.getRenderingScale(); | |||
float gl_scale = render_scale * resize_scale; | |||
Rectangle<int> top_level_bounds = top_level->getBounds(); | |||
Rectangle<int> visible_bounds = getGlobalVisibleBounds(component, bounds); | |||
if (visible_bounds.getHeight() > 0 && visible_bounds.getWidth() > 0) { | |||
glScissor(gl_scale * visible_bounds.getX(), | |||
std::ceil(scale * render_scale * top_level_bounds.getHeight()) - gl_scale * visible_bounds.getBottom(), | |||
gl_scale * visible_bounds.getWidth(), gl_scale * visible_bounds.getHeight()); | |||
} | |||
} | |||
void OpenGlComponent::paintBackground(Graphics& g) { | |||
if (!isVisible()) | |||
return; | |||
g.fillAll(findColour(Skin::kWidgetBackground, true)); | |||
} | |||
void OpenGlComponent::repaintBackground() { | |||
if (!isShowing()) | |||
return; | |||
FullInterface* parent = findParentComponentOfClass<FullInterface>(); | |||
if (parent) | |||
parent->repaintOpenGlBackground(this); | |||
} | |||
void OpenGlComponent::resized() { | |||
if (corners_) | |||
corners_->setBounds(getLocalBounds()); | |||
body_color_ = findColour(Skin::kBody, true); | |||
} | |||
void OpenGlComponent::parentHierarchyChanged() { | |||
if (num_voices_readout_ == nullptr) { | |||
SynthGuiInterface* parent = findParentComponentOfClass<SynthGuiInterface>(); | |||
if (parent) | |||
num_voices_readout_ = parent->getSynth()->getStatusOutput("num_voices"); | |||
} | |||
Component::parentHierarchyChanged(); | |||
} | |||
void OpenGlComponent::addRoundedCorners() { | |||
corners_ = std::make_unique<OpenGlCorners>(); | |||
addAndMakeVisible(corners_.get()); | |||
} | |||
void OpenGlComponent::addBottomRoundedCorners() { | |||
only_bottom_corners_ = true; | |||
addRoundedCorners(); | |||
} | |||
void OpenGlComponent::init(OpenGlWrapper& open_gl) { | |||
if (corners_) | |||
corners_->init(open_gl); | |||
} | |||
void OpenGlComponent::renderCorners(OpenGlWrapper& open_gl, bool animate, Colour color, float rounding) { | |||
if (corners_) { | |||
if (only_bottom_corners_) | |||
corners_->setBottomCorners(getLocalBounds(), rounding); | |||
else | |||
corners_->setCorners(getLocalBounds(), rounding); | |||
corners_->setColor(color); | |||
corners_->render(open_gl, animate); | |||
} | |||
} | |||
void OpenGlComponent::renderCorners(OpenGlWrapper& open_gl, bool animate) { | |||
renderCorners(open_gl, animate, body_color_, findValue(Skin::kWidgetRoundedCorner)); | |||
} | |||
void OpenGlComponent::destroy(OpenGlWrapper& open_gl) { | |||
if (corners_) | |||
corners_->destroy(open_gl); | |||
} | |||
float OpenGlComponent::findValue(Skin::ValueId value_id) { | |||
if (parent_) | |||
return parent_->findValue(value_id); | |||
VITAL_ASSERT(false); | |||
return 0.0f; | |||
} |
@@ -0,0 +1,114 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "common.h" | |||
#include "shaders.h" | |||
#include "skin.h" | |||
#include "synth_module.h" | |||
class SynthSection; | |||
class OpenGlCorners; | |||
class OpenGlComponent : public Component { | |||
public: | |||
static bool setViewPort(Component* component, Rectangle<int> bounds, OpenGlWrapper& open_gl); | |||
static bool setViewPort(Component* component, OpenGlWrapper& open_gl); | |||
static void setScissor(Component* component, OpenGlWrapper& open_gl); | |||
static void setScissorBounds(Component* component, Rectangle<int> bounds, OpenGlWrapper& open_gl); | |||
static std::unique_ptr<OpenGLShaderProgram::Uniform> getUniform(const OpenGlWrapper& open_gl, | |||
const OpenGLShaderProgram& program, | |||
const char* name) { | |||
if (open_gl.context.extensions.glGetUniformLocation(program.getProgramID(), name) >= 0) | |||
return std::make_unique<OpenGLShaderProgram::Uniform>(program, name); | |||
return nullptr; | |||
} | |||
static std::unique_ptr<OpenGLShaderProgram::Attribute> getAttribute(const OpenGlWrapper& open_gl, | |||
const OpenGLShaderProgram& program, | |||
const char* name) { | |||
if (open_gl.context.extensions.glGetAttribLocation(program.getProgramID(), name) >= 0) | |||
return std::make_unique<OpenGLShaderProgram::Attribute>(program, name); | |||
return nullptr; | |||
} | |||
OpenGlComponent(String name = ""); | |||
virtual ~OpenGlComponent(); | |||
virtual void resized() override; | |||
virtual void parentHierarchyChanged() override; | |||
void addRoundedCorners(); | |||
void addBottomRoundedCorners(); | |||
virtual void init(OpenGlWrapper& open_gl); | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) = 0; | |||
void renderCorners(OpenGlWrapper& open_gl, bool animate, Colour color, float rounding); | |||
void renderCorners(OpenGlWrapper& open_gl, bool animate); | |||
virtual void destroy(OpenGlWrapper& open_gl); | |||
virtual void paintBackground(Graphics& g); | |||
void repaintBackground(); | |||
Colour getBodyColor() const { return body_color_; } | |||
void setParent(const SynthSection* parent) { parent_ = parent; } | |||
float findValue(Skin::ValueId value_id); | |||
void setSkinValues(const Skin& skin) { | |||
skin.setComponentColors(this, skin_override_, false); | |||
} | |||
void setSkinOverride(Skin::SectionOverride skin_override) { skin_override_ = skin_override; } | |||
static inline String translateFragmentShader(const String& code) { | |||
#if OPENGL_ES | |||
return String("#version 300 es\n") + "out mediump vec4 fragColor;\n" + | |||
code.replace("varying", "in").replace("texture2D", "texture").replace("gl_FragColor", "fragColor"); | |||
#else | |||
return OpenGLHelpers::translateFragmentShaderToV3(code); | |||
#endif | |||
} | |||
static inline String translateVertexShader(const String& code) { | |||
#if OPENGL_ES | |||
return String("#version 300 es\n") + code.replace("attribute", "in").replace("varying", "out"); | |||
#else | |||
return OpenGLHelpers::translateVertexShaderToV3(code); | |||
#endif | |||
} | |||
force_inline void checkGlError() { | |||
#if DEBUG | |||
int error = glGetError(); | |||
assert(error == GL_NO_ERROR); | |||
#endif | |||
} | |||
void setBackgroundColor(const Colour& color) { background_color_ = color; } | |||
protected: | |||
bool setViewPort(OpenGlWrapper& open_gl); | |||
std::unique_ptr<OpenGlCorners> corners_; | |||
bool only_bottom_corners_; | |||
Colour background_color_; | |||
Colour body_color_; | |||
const SynthSection* parent_; | |||
Skin::SectionOverride skin_override_; | |||
const vital::StatusOutput* num_voices_readout_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OpenGlComponent) | |||
}; | |||
@@ -0,0 +1,143 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "open_gl_image.h" | |||
#include "open_gl_component.h" | |||
#include "shaders.h" | |||
#include "utils.h" | |||
namespace { | |||
constexpr int kNumPositions = 16; | |||
constexpr int kNumTriangleIndices = 6; | |||
} // namespace | |||
OpenGlImage::OpenGlImage() : dirty_(true), image_(nullptr), image_width_(0), image_height_(0), | |||
additive_(false), use_alpha_(false), scissor_(false) { | |||
position_vertices_ = std::make_unique<float[]>(kNumPositions); | |||
float position_vertices[kNumPositions] = { | |||
0.0f, 1.0f, 0.0f, 1.0f, | |||
0.0f, -1.0f, 0.0f, 0.0f, | |||
0.1f, -1.0f, 1.0f, 0.0f, | |||
0.1f, 1.0f, 1.0f, 1.0f | |||
}; | |||
memcpy(position_vertices_.get(), position_vertices, kNumPositions * sizeof(float)); | |||
position_triangles_ = std::make_unique<int[]>(kNumTriangleIndices); | |||
int position_triangles[kNumTriangleIndices] = { | |||
0, 1, 2, | |||
2, 3, 0 | |||
}; | |||
memcpy(position_triangles_.get(), position_triangles, kNumTriangleIndices * sizeof(int)); | |||
} | |||
OpenGlImage::~OpenGlImage() { } | |||
void OpenGlImage::init(OpenGlWrapper& open_gl) { | |||
open_gl.context.extensions.glGenBuffers(1, &vertex_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(static_cast<size_t>(kNumPositions * sizeof(float))); | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, | |||
position_vertices_.get(), GL_STATIC_DRAW); | |||
open_gl.context.extensions.glGenBuffers(1, &triangle_buffer_); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangle_buffer_); | |||
GLsizeiptr tri_size = static_cast<GLsizeiptr>(static_cast<size_t>(kNumTriangleIndices * sizeof(float))); | |||
open_gl.context.extensions.glBufferData(GL_ELEMENT_ARRAY_BUFFER, tri_size, | |||
position_triangles_.get(), GL_STATIC_DRAW); | |||
image_shader_ = open_gl.shaders->getShaderProgram(Shaders::kImageVertex, Shaders::kTintedImageFragment); | |||
image_shader_->use(); | |||
image_color_ = OpenGlComponent::getUniform(open_gl, *image_shader_, "color"); | |||
image_position_ = OpenGlComponent::getAttribute(open_gl, *image_shader_, "position"); | |||
texture_coordinates_ = OpenGlComponent::getAttribute(open_gl, *image_shader_, "tex_coord_in"); | |||
} | |||
void OpenGlImage::drawImage(OpenGlWrapper& open_gl) { | |||
mutex_.lock(); | |||
if (image_) { | |||
texture_.loadImage(*image_); | |||
image_ = nullptr; | |||
} | |||
mutex_.unlock(); | |||
glEnable(GL_BLEND); | |||
if (scissor_) | |||
glEnable(GL_SCISSOR_TEST); | |||
else | |||
glDisable(GL_SCISSOR_TEST); | |||
if (additive_) | |||
glBlendFunc(GL_ONE, GL_ONE); | |||
else if (use_alpha_) | |||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | |||
else | |||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); | |||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); | |||
GLsizeiptr vert_size = static_cast<GLsizeiptr>(static_cast<size_t>(kNumPositions * sizeof(float))); | |||
mutex_.lock(); | |||
if (dirty_) | |||
open_gl.context.extensions.glBufferData(GL_ARRAY_BUFFER, vert_size, position_vertices_.get(), GL_STATIC_DRAW); | |||
dirty_ = false; | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangle_buffer_); | |||
texture_.bind(); | |||
open_gl.context.extensions.glActiveTexture(GL_TEXTURE0); | |||
mutex_.unlock(); | |||
image_shader_->use(); | |||
image_color_->set(color_.getFloatRed(), color_.getFloatGreen(), color_.getFloatBlue(), color_.getFloatAlpha()); | |||
open_gl.context.extensions.glVertexAttribPointer(image_position_->attributeID, 2, GL_FLOAT, | |||
GL_FALSE, 4 * sizeof(float), nullptr); | |||
open_gl.context.extensions.glEnableVertexAttribArray(image_position_->attributeID); | |||
open_gl.context.extensions.glVertexAttribPointer(texture_coordinates_->attributeID, 2, GL_FLOAT, | |||
GL_FALSE, 4 * sizeof(float), | |||
(GLvoid*)(2 * sizeof(float))); | |||
open_gl.context.extensions.glEnableVertexAttribArray(texture_coordinates_->attributeID); | |||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); | |||
open_gl.context.extensions.glDisableVertexAttribArray(image_position_->attributeID); | |||
open_gl.context.extensions.glDisableVertexAttribArray(texture_coordinates_->attributeID); | |||
texture_.unbind(); | |||
open_gl.context.extensions.glBindBuffer(GL_ARRAY_BUFFER, 0); | |||
open_gl.context.extensions.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); | |||
glDisable(GL_BLEND); | |||
glDisable(GL_SCISSOR_TEST); | |||
} | |||
void OpenGlImage::destroy(OpenGlWrapper& open_gl) { | |||
texture_.release(); | |||
image_shader_ = nullptr; | |||
image_color_ = nullptr; | |||
image_position_ = nullptr; | |||
texture_coordinates_ = nullptr; | |||
open_gl.context.extensions.glDeleteBuffers(1, &vertex_buffer_); | |||
open_gl.context.extensions.glDeleteBuffers(1, &triangle_buffer_); | |||
} |
@@ -0,0 +1,95 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "JuceHeader.h" | |||
#include "open_gl_component.h" | |||
#include <mutex> | |||
class OpenGlImage { | |||
public: | |||
OpenGlImage(); | |||
virtual ~OpenGlImage(); | |||
void init(OpenGlWrapper& open_gl); | |||
void drawImage(OpenGlWrapper& open_gl); | |||
void destroy(OpenGlWrapper& open_gl); | |||
void lock() { mutex_.lock(); } | |||
void unlock() { mutex_.unlock(); } | |||
void setOwnImage(Image& image) { | |||
mutex_.lock(); | |||
owned_image_ = std::make_unique<Image>(image); | |||
setImage(owned_image_.get()); | |||
mutex_.unlock(); | |||
} | |||
void setImage(Image* image) { | |||
image_ = image; | |||
image_width_ = image->getWidth(); | |||
image_height_ = image->getHeight(); | |||
} | |||
void setColor(Colour color) { color_ = color; } | |||
inline void setPosition(float x, float y, int index) { | |||
position_vertices_[index] = x; | |||
position_vertices_[index + 1] = y; | |||
dirty_ = true; | |||
} | |||
inline void setTopLeft(float x, float y) { setPosition(x, y, 0); } | |||
inline void setBottomLeft(float x, float y) { setPosition(x, y, 4); } | |||
inline void setBottomRight(float x, float y) { setPosition(x, y, 8); } | |||
inline void setTopRight(float x, float y) { setPosition(x, y, 12); } | |||
int getImageWidth() { return image_width_; } | |||
int getImageHeight() { return image_height_; } | |||
void setAdditive(bool additive) { additive_ = additive; } | |||
void setUseAlpha(bool use_alpha) { use_alpha_ = use_alpha; } | |||
void setScissor(bool scissor) { scissor_ = scissor; } | |||
private: | |||
std::mutex mutex_; | |||
bool dirty_; | |||
Image* image_; | |||
int image_width_; | |||
int image_height_; | |||
std::unique_ptr<Image> owned_image_; | |||
Colour color_; | |||
OpenGLTexture texture_; | |||
bool additive_; | |||
bool use_alpha_; | |||
bool scissor_; | |||
OpenGLShaderProgram* image_shader_; | |||
std::unique_ptr<OpenGLShaderProgram::Uniform> image_color_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> image_position_; | |||
std::unique_ptr<OpenGLShaderProgram::Attribute> texture_coordinates_; | |||
std::unique_ptr<float[]> position_vertices_; | |||
std::unique_ptr<int[]> position_triangles_; | |||
GLuint vertex_buffer_; | |||
GLuint triangle_buffer_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OpenGlImage) | |||
}; | |||
@@ -0,0 +1,86 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#include "open_gl_image_component.h" | |||
OpenGlImageComponent::OpenGlImageComponent(String name) : OpenGlComponent(name), component_(nullptr), | |||
active_(true), static_image_(false), | |||
paint_entire_component_(true) { | |||
image_.setTopLeft(-1.0f, 1.0f); | |||
image_.setTopRight(1.0f, 1.0f); | |||
image_.setBottomLeft(-1.0f, -1.0f); | |||
image_.setBottomRight(1.0f, -1.0f); | |||
image_.setColor(Colours::white); | |||
if (name == "") | |||
setInterceptsMouseClicks(false, false); | |||
} | |||
void OpenGlImageComponent::redrawImage(bool force) { | |||
if (!active_) | |||
return; | |||
Component* component = component_ ? component_ : this; | |||
int pixel_scale = Desktop::getInstance().getDisplays().findDisplayForPoint(getScreenPosition()).scale; | |||
int width = component->getWidth() * pixel_scale; | |||
int height = component->getHeight() * pixel_scale; | |||
if (width <= 0 || height <= 0) | |||
return; | |||
bool new_image = draw_image_ == nullptr || draw_image_->getWidth() != width || draw_image_->getHeight() != height; | |||
if (!new_image && (static_image_ || !force)) | |||
return; | |||
image_.lock(); | |||
if (new_image) | |||
draw_image_ = std::make_unique<Image>(Image::ARGB, width, height, false); | |||
draw_image_->clear(Rectangle<int>(0, 0, width, height)); | |||
Graphics g(*draw_image_); | |||
g.addTransform(AffineTransform::scale(pixel_scale)); | |||
paintToImage(g); | |||
image_.setImage(draw_image_.get()); | |||
float gl_width = vital::utils::nextPowerOfTwo(width); | |||
float gl_height = vital::utils::nextPowerOfTwo(height); | |||
float width_ratio = gl_width / width; | |||
float height_ratio = gl_height / height; | |||
float right = -1.0f + 2.0f * width_ratio; | |||
float bottom = 1.0f - 2.0f * height_ratio; | |||
image_.setTopRight(right, 1.0f); | |||
image_.setBottomLeft(-1.0f, bottom); | |||
image_.setBottomRight(right, bottom); | |||
image_.unlock(); | |||
} | |||
void OpenGlImageComponent::init(OpenGlWrapper& open_gl) { | |||
image_.init(open_gl); | |||
} | |||
void OpenGlImageComponent::render(OpenGlWrapper& open_gl, bool animate) { | |||
Component* component = component_ ? component_ : this; | |||
if (!active_ || !setViewPort(component, open_gl) || !component->isVisible()) | |||
return; | |||
image_.drawImage(open_gl); | |||
} | |||
void OpenGlImageComponent::destroy(OpenGlWrapper& open_gl) { | |||
image_.destroy(open_gl); | |||
} |
@@ -0,0 +1,286 @@ | |||
/* Copyright 2013-2019 Matt Tytel | |||
* | |||
* vital 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 | |||
* (at your option) any later version. | |||
* | |||
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
#pragma once | |||
#include "open_gl_component.h" | |||
#include "fonts.h" | |||
#include "open_gl_image.h" | |||
class SynthSection; | |||
class OpenGlImageComponent : public OpenGlComponent { | |||
public: | |||
OpenGlImageComponent(String name = ""); | |||
virtual ~OpenGlImageComponent() = default; | |||
virtual void paintBackground(Graphics& g) override { | |||
redrawImage(false); | |||
} | |||
virtual void paintToImage(Graphics& g) { | |||
Component* component = component_ ? component_ : this; | |||
if (paint_entire_component_) | |||
component->paintEntireComponent(g, false); | |||
else | |||
component->paint(g); | |||
} | |||
virtual void init(OpenGlWrapper& open_gl) override; | |||
virtual void render(OpenGlWrapper& open_gl, bool animate) override; | |||
virtual void destroy(OpenGlWrapper& open_gl) override; | |||
virtual void redrawImage(bool force); | |||
void setComponent(Component* component) { component_ = component; } | |||
void setScissor(bool scissor) { image_.setScissor(scissor); } | |||
void setUseAlpha(bool use_alpha) { image_.setUseAlpha(use_alpha); } | |||
void setColor(Colour color) { image_.setColor(color); } | |||
OpenGlImage& image() { return image_; } | |||
void setActive(bool active) { active_ = active; } | |||
void setStatic(bool static_image) { static_image_ = static_image; } | |||
void paintEntireComponent(bool paint_entire_component) { paint_entire_component_ = paint_entire_component; } | |||
bool isActive() const { return active_; } | |||
protected: | |||
Component* component_; | |||
bool active_; | |||
bool static_image_; | |||
bool paint_entire_component_; | |||
std::unique_ptr<Image> draw_image_; | |||
OpenGlImage image_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OpenGlImageComponent) | |||
}; | |||
template <class ComponentType> | |||
class OpenGlAutoImageComponent : public ComponentType { | |||
public: | |||
using ComponentType::ComponentType; | |||
virtual void mouseDown(const MouseEvent& e) override { | |||
ComponentType::mouseDown(e); | |||
redoImage(); | |||
} | |||
virtual void mouseUp(const MouseEvent& e) override { | |||
ComponentType::mouseUp(e); | |||
redoImage(); | |||
} | |||
virtual void mouseDoubleClick(const MouseEvent& e) override { | |||
ComponentType::mouseDoubleClick(e); | |||
redoImage(); | |||
} | |||
virtual void mouseEnter(const MouseEvent& e) override { | |||
ComponentType::mouseEnter(e); | |||
redoImage(); | |||
} | |||
virtual void mouseExit(const MouseEvent& e) override { | |||
ComponentType::mouseExit(e); | |||
redoImage(); | |||
} | |||
virtual void mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& wheel) override { | |||
ComponentType::mouseWheelMove(e, wheel); | |||
redoImage(); | |||
} | |||
OpenGlImageComponent* getImageComponent() { return &image_component_; } | |||
virtual void redoImage() { image_component_.redrawImage(true); } | |||
protected: | |||
OpenGlImageComponent image_component_; | |||
}; | |||
class OpenGlTextEditor : public OpenGlAutoImageComponent<TextEditor>, public TextEditor::Listener { | |||
public: | |||
OpenGlTextEditor(String name) : OpenGlAutoImageComponent(name) { | |||
monospace_ = false; | |||
image_component_.setComponent(this); | |||
addListener(this); | |||
} | |||
OpenGlTextEditor(String name, wchar_t password_char) : OpenGlAutoImageComponent(name, password_char) { | |||
monospace_ = false; | |||
image_component_.setComponent(this); | |||
addListener(this); | |||
} | |||
bool keyPressed(const KeyPress& key) override { | |||
bool result = TextEditor::keyPressed(key); | |||
redoImage(); | |||
return result; | |||
} | |||
void textEditorTextChanged(TextEditor&) override { redoImage(); } | |||
void textEditorFocusLost(TextEditor&) override { redoImage(); } | |||
virtual void mouseDrag(const MouseEvent& e) override { | |||
TextEditor::mouseDrag(e); | |||
redoImage(); | |||
} | |||
void applyFont() { | |||
Font font; | |||
if (monospace_) | |||
font = Fonts::instance()->monospace().withPointHeight(getHeight() / 2.0f); | |||
else | |||
font = Fonts::instance()->proportional_light().withPointHeight(getHeight() / 2.0f); | |||
applyFontToAllText(font); | |||
redoImage(); | |||
} | |||
void visibilityChanged() override { | |||
TextEditor::visibilityChanged(); | |||
if (isVisible() && !isMultiLine()) | |||
applyFont(); | |||
} | |||
void resized() override { | |||
TextEditor::resized(); | |||
if (isMultiLine()) { | |||
float indent = image_component_.findValue(Skin::kLabelBackgroundRounding); | |||
setIndents(indent, indent); | |||
return; | |||
} | |||
if (monospace_) | |||
setIndents(getHeight() * 0.2, getHeight() * 0.17); | |||
else | |||
setIndents(getHeight() * 0.2, getHeight() * 0.15); | |||
if (isVisible()) | |||
applyFont(); | |||
} | |||
void setMonospace() { | |||
monospace_ = true; | |||
} | |||
private: | |||
bool monospace_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OpenGlTextEditor) | |||
}; | |||
class PlainTextComponent : public OpenGlImageComponent { | |||
public: | |||
enum FontType { | |||
kTitle, | |||
kLight, | |||
kRegular, | |||
kMono, | |||
kNumFontTypes | |||
}; | |||
PlainTextComponent(String name, String text) : OpenGlImageComponent(name), text_(std::move(text)), | |||
text_size_(1.0f), font_type_(kRegular), | |||
justification_(Justification::centred), | |||
buffer_(0) { | |||
setInterceptsMouseClicks(false, false); | |||
} | |||
void resized() override { | |||
OpenGlImageComponent::resized(); | |||
redrawImage(true); | |||
} | |||
void setText(String text) { | |||
if (text_ == text) | |||
return; | |||
text_ = text; | |||
redrawImage(true); | |||
} | |||
String getText() const { return text_; } | |||
void paintToImage(Graphics& g) override { | |||
g.setColour(Colours::white); | |||
if (font_type_ == kTitle) | |||
g.setFont(Fonts::instance()->proportional_title().withPointHeight(text_size_)); | |||
else if (font_type_ == kLight) | |||
g.setFont(Fonts::instance()->proportional_light().withPointHeight(text_size_)); | |||
else if (font_type_ == kRegular) | |||
g.setFont(Fonts::instance()->proportional_regular().withPointHeight(text_size_)); | |||
else | |||
g.setFont(Fonts::instance()->monospace().withPointHeight(text_size_)); | |||
Component* component = component_ ? component_ : this; | |||
g.drawFittedText(text_, buffer_, 0, component->getWidth() - 2 * buffer_, | |||
component->getHeight(), justification_, false); | |||
} | |||
void setTextSize(float size) { | |||
text_size_ = size; | |||
redrawImage(true); | |||
} | |||
void setFontType(FontType font_type) { | |||
font_type_ = font_type; | |||
} | |||
void setJustification(Justification justification) { | |||
justification_ = justification; | |||
} | |||
void setBuffer(int buffer) { buffer_ = buffer; } | |||
private: | |||
String text_; | |||
float text_size_; | |||
FontType font_type_; | |||
Justification justification_; | |||
int buffer_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PlainTextComponent) | |||
}; | |||
class PlainShapeComponent : public OpenGlImageComponent { | |||
public: | |||
PlainShapeComponent(String name) : OpenGlImageComponent(name), justification_(Justification::centred) { | |||
setInterceptsMouseClicks(false, false); | |||
} | |||
void paintToImage(Graphics& g) override { | |||
Component* component = component_ ? component_ : this; | |||
Rectangle<float> bounds = component->getLocalBounds().toFloat(); | |||
Path shape = shape_; | |||
shape.applyTransform(shape.getTransformToScaleToFit(bounds, true, justification_)); | |||
g.setColour(Colours::white); | |||
g.fillPath(shape); | |||
} | |||
void setShape(Path shape) { | |||
shape_ = shape; | |||
redrawImage(true); | |||
} | |||
void setJustification(Justification justification) { justification_ = justification; } | |||
private: | |||
Path shape_; | |||
Justification justification_; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PlainShapeComponent) | |||
}; |