@@ -70,6 +70,7 @@ option('plugins', | |||||
'tal-vocoder-2', | 'tal-vocoder-2', | ||||
'temper', | 'temper', | ||||
'vex', | 'vex', | ||||
'vitalium', | |||||
'wolpertinger', | 'wolpertinger', | ||||
], | ], | ||||
) | ) |
@@ -6,6 +6,7 @@ if linux_embed | |||||
else | else | ||||
plugins = [ | plugins = [ | ||||
'chow', | 'chow', | ||||
'vitalium', | |||||
] | ] | ||||
endif | 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) | |||||
}; |