mirror of
https://codeberg.org/armin/horizont.git
synced 2026-09-01 04:10:46 +02:00
update
This commit is contained in:
parent
ed28db30a3
commit
6c0d072465
19 changed files with 2212 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# CMake build output
|
||||||
|
build
|
||||||
|
build-*/
|
||||||
|
|
||||||
|
# Auto-fetched JUCE source tree (downloaded by `make`)
|
||||||
|
JUCE/
|
||||||
|
|
||||||
|
# IDE / editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.xcodeproj
|
||||||
|
*.xcworkspace
|
||||||
|
*.swp
|
||||||
76
CMakeLists.txt
Normal file
76
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
|
||||||
|
project(Horizont
|
||||||
|
VERSION 1.0.0
|
||||||
|
LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(DEFINED JUCE_ROOT AND EXISTS "${JUCE_ROOT}/CMakeLists.txt")
|
||||||
|
message(STATUS "Using JUCE from ${JUCE_ROOT}")
|
||||||
|
add_subdirectory("${JUCE_ROOT}" "${CMAKE_BINARY_DIR}/JUCE")
|
||||||
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/JUCE/CMakeLists.txt")
|
||||||
|
message(STATUS "Using JUCE from ${CMAKE_CURRENT_SOURCE_DIR}/JUCE")
|
||||||
|
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/JUCE")
|
||||||
|
else()
|
||||||
|
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/JUCE")
|
||||||
|
find_package(JUCE CONFIG REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
juce_add_plugin(Horizont
|
||||||
|
VERSION "1.0.0"
|
||||||
|
PRODUCT_NAME "Horizont"
|
||||||
|
COMPANY_NAME "Horizont"
|
||||||
|
BUNDLE_ID "com.horizont.Horizont"
|
||||||
|
PLUGIN_MANUFACTURER_CODE "ARMN"
|
||||||
|
PLUGIN_CODE "RVRB"
|
||||||
|
DESCRIPTION "Algorithmic stereo reverb with feedback pitch shifting"
|
||||||
|
IS_SYNTH FALSE
|
||||||
|
IS_MIDI_EFFECT FALSE
|
||||||
|
NEEDS_MIDI_INPUT FALSE
|
||||||
|
NEEDS_MIDI_OUTPUT FALSE
|
||||||
|
FORMATS "VST3" "AU"
|
||||||
|
COPY_PLUGIN_AFTER_BUILD FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
juce_generate_juce_header(Horizont)
|
||||||
|
|
||||||
|
# We only ship a VST3 (never released a VST2), so tell JUCE it doesn't need to
|
||||||
|
# keep parameter IDs compatible with a VST2 version of this plugin.
|
||||||
|
target_compile_definitions(Horizont PUBLIC JUCE_VST3_CAN_REPLACE_VST2=0)
|
||||||
|
|
||||||
|
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d__%H-%M-%S")
|
||||||
|
|
||||||
|
set(HORIZONT_GIT_REVISION "unknown")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(Git_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE HORIZONT_GIT_REVISION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_definitions(Horizont PRIVATE
|
||||||
|
HORIZONT_VERSION="${PROJECT_VERSION}"
|
||||||
|
HORIZONT_GIT_REVISION="${HORIZONT_GIT_REVISION}"
|
||||||
|
HORIZONT_BUILD_TIMESTAMP="${BUILD_TIMESTAMP}")
|
||||||
|
|
||||||
|
target_sources(Horizont PRIVATE
|
||||||
|
Source/PluginProcessor.cpp
|
||||||
|
Source/PluginEditor.cpp
|
||||||
|
Source/HorizontEngine.cpp
|
||||||
|
Source/PitchShifter.cpp
|
||||||
|
Source/HorizontLookAndFeel.cpp
|
||||||
|
Source/Knob.cpp
|
||||||
|
Source/SpectrumDisplay.cpp
|
||||||
|
Source/Presets.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(Horizont PRIVATE juce::juce_dsp)
|
||||||
1
JUCE
Submodule
1
JUCE
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 91ad83ae34a81e0833b1a2b0866f54846370ae53
|
||||||
74
Makefile
Normal file
74
Makefile
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Horizont plugin - build and install (macOS)
|
||||||
|
#
|
||||||
|
# make fetch JUCE, configure and build (Release)
|
||||||
|
# make install build and install into ~/Library/Audio/Plug-Ins
|
||||||
|
# make uninstall remove the installed copies
|
||||||
|
# make clean delete the build directory
|
||||||
|
# make help list available targets
|
||||||
|
|
||||||
|
JUCE_DIR := JUCE
|
||||||
|
JUCE_VERSION := 8.0.15
|
||||||
|
JUCE_REPO := https://github.com/juce-framework/JUCE.git
|
||||||
|
|
||||||
|
BUILD_DIR := build
|
||||||
|
ARTIFACTS := $(BUILD_DIR)/Horizont_artefacts
|
||||||
|
CONFIG := Release
|
||||||
|
|
||||||
|
VST3 := $(ARTIFACTS)/$(CONFIG)/VST3/Horizont.vst3
|
||||||
|
AU := $(ARTIFACTS)/$(CONFIG)/AU/Horizont.component
|
||||||
|
|
||||||
|
PLUGINS_HOME := $(HOME)/Library/Audio/Plug-Ins
|
||||||
|
VST3_INSTALL := $(PLUGINS_HOME)/VST3/Horizont.vst3
|
||||||
|
AU_INSTALL := $(PLUGINS_HOME)/Components/Horizont.component
|
||||||
|
|
||||||
|
CMAKE := cmake
|
||||||
|
JUCE_ROOT := $(abspath $(CURDIR)/$(JUCE_DIR))
|
||||||
|
|
||||||
|
.PHONY: all help deps configure build install uninstall clean
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Horizont plugin targets:"
|
||||||
|
@echo " make build (fetch JUCE into ./$(JUCE_DIR) if missing)"
|
||||||
|
@echo " make install build + install into $(PLUGINS_HOME)"
|
||||||
|
@echo " make uninstall remove installed plugin"
|
||||||
|
@echo " make clean delete $(BUILD_DIR)"
|
||||||
|
|
||||||
|
deps:
|
||||||
|
@if [ -f "$(JUCE_DIR)/CMakeLists.txt" ]; then \
|
||||||
|
echo "==> JUCE already present in ./$(JUCE_DIR)"; \
|
||||||
|
else \
|
||||||
|
echo "==> Fetching JUCE $(JUCE_VERSION) into ./$(JUCE_DIR) ..."; \
|
||||||
|
git clone --depth 1 --branch $(JUCE_VERSION) $(JUCE_REPO) $(JUCE_DIR) || { \
|
||||||
|
echo "!! Could not fetch JUCE. Check network access."; exit 1; }; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure: deps
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(CMAKE) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(CONFIG) -DJUCE_ROOT="$(JUCE_ROOT)"
|
||||||
|
|
||||||
|
build: configure
|
||||||
|
$(CMAKE) --build $(BUILD_DIR) --target Horizont_All --config $(CONFIG) -j
|
||||||
|
|
||||||
|
$(VST3) $(AU): build
|
||||||
|
|
||||||
|
install: build
|
||||||
|
@echo "==> Installing Horizont into $(PLUGINS_HOME)"
|
||||||
|
@mkdir -p "$(PLUGINS_HOME)/VST3" "$(PLUGINS_HOME)/Components"
|
||||||
|
@rm -rf "$(VST3_INSTALL)" "$(AU_INSTALL)"
|
||||||
|
@cp -R "$(VST3)" "$(VST3_INSTALL)"
|
||||||
|
@cp -R "$(AU)" "$(AU_INSTALL)"
|
||||||
|
@codesign --force --deep --sign - "$(VST3_INSTALL)" >/dev/null 2>&1 || true
|
||||||
|
@codesign --force --deep --sign - "$(AU_INSTALL)" >/dev/null 2>&1 || true
|
||||||
|
@echo "==> Installed:"
|
||||||
|
@echo " $(VST3_INSTALL)"
|
||||||
|
@echo " $(AU_INSTALL)"
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
@rm -rf "$(VST3_INSTALL)" "$(AU_INSTALL)"
|
||||||
|
@echo "==> Removed installed plugin."
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@echo "==> Cleaned $(BUILD_DIR)."
|
||||||
218
Source/HorizontEngine.cpp
Normal file
218
Source/HorizontEngine.cpp
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
#include "HorizontEngine.h"
|
||||||
|
|
||||||
|
HorizontEngine::HorizontEngine()
|
||||||
|
{
|
||||||
|
prepare (44100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::prepare (double sr)
|
||||||
|
{
|
||||||
|
sampleRate = sr;
|
||||||
|
const float srScale = (float) (sr / 44100.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
{
|
||||||
|
baseLen[(size_t) i] = baseLen441[(size_t) i] * srScale;
|
||||||
|
baseLenR[(size_t) i] = (baseLen441[(size_t) i] + rightOffset[(size_t) i]) * srScale;
|
||||||
|
|
||||||
|
const int maxL = (int) std::ceil (baseLen441[(size_t) i] * 2.0f * srScale) + 8;
|
||||||
|
combL[(size_t) i].prepare (maxL);
|
||||||
|
combR[(size_t) i].prepare (maxL);
|
||||||
|
}
|
||||||
|
|
||||||
|
diffL1.prepare ((int) (221.0f * srScale));
|
||||||
|
diffL2.prepare ((int) (113.0f * srScale));
|
||||||
|
diffR1.prepare ((int) (229.0f * srScale));
|
||||||
|
diffR2.prepare ((int) (107.0f * srScale));
|
||||||
|
|
||||||
|
shiftL.prepare (sr);
|
||||||
|
shiftR.prepare (sr);
|
||||||
|
|
||||||
|
lowCutG.reset (sr, 0.04);
|
||||||
|
setLowCut (lowCutHz);
|
||||||
|
lowCutG.setCurrentAndTargetValue (lowCutG.getTargetValue());
|
||||||
|
|
||||||
|
highCutG.reset (sr, 0.04);
|
||||||
|
setHighCut (highCutHz);
|
||||||
|
highCutG.setCurrentAndTargetValue (highCutG.getTargetValue());
|
||||||
|
|
||||||
|
decayS.reset (sr, 0.06);
|
||||||
|
setDecay (decaySec);
|
||||||
|
decayS.setCurrentAndTargetValue (decayS.getTargetValue());
|
||||||
|
|
||||||
|
sizeS.reset (sr, 0.06);
|
||||||
|
setSize (sizeVal);
|
||||||
|
sizeS.setCurrentAndTargetValue (sizeS.getTargetValue());
|
||||||
|
|
||||||
|
diffS.reset (sr, 0.05);
|
||||||
|
setDiffusion (diffusionVal);
|
||||||
|
diffS.setCurrentAndTargetValue (diffS.getTargetValue());
|
||||||
|
|
||||||
|
dampS.reset (sr, 0.05);
|
||||||
|
setBrightness (brightnessVal);
|
||||||
|
dampS.setCurrentAndTargetValue (dampS.getTargetValue());
|
||||||
|
|
||||||
|
feedbackS.reset (sr, 0.06);
|
||||||
|
setFeedback (feedbackVal);
|
||||||
|
feedbackS.setCurrentAndTargetValue (feedbackS.getTargetValue());
|
||||||
|
|
||||||
|
ratioS.reset (sr, 0.08);
|
||||||
|
setPitch (pitchVal);
|
||||||
|
ratioS.setCurrentAndTargetValue (ratioS.getTargetValue());
|
||||||
|
|
||||||
|
wetS.reset (sr, 0.03);
|
||||||
|
setDryWet (dryWetVal);
|
||||||
|
wetS.setCurrentAndTargetValue (wetS.getTargetValue());
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::reset()
|
||||||
|
{
|
||||||
|
for (auto& c : combL)
|
||||||
|
c.reset();
|
||||||
|
for (auto& c : combR)
|
||||||
|
c.reset();
|
||||||
|
|
||||||
|
diffL1.reset();
|
||||||
|
diffL2.reset();
|
||||||
|
diffR1.reset();
|
||||||
|
diffR2.reset();
|
||||||
|
|
||||||
|
hpL.reset();
|
||||||
|
lpL.reset();
|
||||||
|
hpR.reset();
|
||||||
|
lpR.reset();
|
||||||
|
|
||||||
|
dcL.reset();
|
||||||
|
dcR.reset();
|
||||||
|
|
||||||
|
shiftL.reset();
|
||||||
|
shiftR.reset();
|
||||||
|
|
||||||
|
lastWetL = 0.0f;
|
||||||
|
lastWetR = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setLowCut (float hz)
|
||||||
|
{
|
||||||
|
lowCutHz = hz;
|
||||||
|
hz = juce::jmin (hz, (float) (sampleRate * 0.48));
|
||||||
|
lowCutG.setTargetValue (std::tan (juce::MathConstants<float>::pi * hz / (float) sampleRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setHighCut (float hz)
|
||||||
|
{
|
||||||
|
highCutHz = hz;
|
||||||
|
hz = juce::jmin (hz, (float) (sampleRate * 0.48));
|
||||||
|
highCutG.setTargetValue (std::tan (juce::MathConstants<float>::pi * hz / (float) sampleRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setDecay (float seconds)
|
||||||
|
{
|
||||||
|
decaySec = seconds;
|
||||||
|
decayS.setTargetValue (juce::jmax (0.1f, seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setSize (float size)
|
||||||
|
{
|
||||||
|
sizeVal = size;
|
||||||
|
sizeS.setTargetValue (juce::jlimit (0.2f, 2.5f, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setDiffusion (float diffusion)
|
||||||
|
{
|
||||||
|
diffusionVal = diffusion;
|
||||||
|
diffS.setTargetValue (0.72f * juce::jlimit (0.0f, 1.0f, diffusion));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setBrightness (float brightness)
|
||||||
|
{
|
||||||
|
brightnessVal = brightness;
|
||||||
|
const float b = juce::jlimit (0.0f, 1.0f, brightness);
|
||||||
|
float fc = 500.0f + 19500.0f * std::pow (b, 1.5f);
|
||||||
|
fc = juce::jmin (fc, (float) (sampleRate * 0.48f));
|
||||||
|
dampS.setTargetValue (1.0f - std::exp (-2.0f * juce::MathConstants<float>::pi * fc / (float) sampleRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setFeedback (float feedback)
|
||||||
|
{
|
||||||
|
feedbackVal = feedback;
|
||||||
|
feedbackS.setTargetValue (juce::jlimit (0.0f, 0.99f, feedback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setPitch (float semitones)
|
||||||
|
{
|
||||||
|
pitchVal = semitones;
|
||||||
|
ratioS.setTargetValue (std::pow (2.0f, semitones / 12.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::setDryWet (float dryWet)
|
||||||
|
{
|
||||||
|
dryWetVal = dryWet;
|
||||||
|
wetS.setTargetValue (juce::jlimit (0.0f, 1.0f, dryWet));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontEngine::processSample (float inL, float inR, float& outL, float& outR)
|
||||||
|
{
|
||||||
|
const float sizeCur = sizeS.getNextValue();
|
||||||
|
const float rtCur = decayS.getNextValue();
|
||||||
|
const float dampC = dampS.getNextValue();
|
||||||
|
const float diffG = diffS.getNextValue();
|
||||||
|
const float fbGain = feedbackS.getNextValue();
|
||||||
|
const float ratio = ratioS.getNextValue();
|
||||||
|
const float wetVal = wetS.getNextValue();
|
||||||
|
const float gLow = lowCutG.getNextValue();
|
||||||
|
const float gHigh = highCutG.getNextValue();
|
||||||
|
|
||||||
|
hpL.setG (gLow);
|
||||||
|
hpL.process (inL);
|
||||||
|
lpL.setG (gHigh);
|
||||||
|
lpL.process (hpL.high);
|
||||||
|
|
||||||
|
hpR.setG (gLow);
|
||||||
|
hpR.process (inR);
|
||||||
|
lpR.setG (gHigh);
|
||||||
|
lpR.process (hpR.high);
|
||||||
|
|
||||||
|
const float fl = lpL.low;
|
||||||
|
const float fr = lpR.low;
|
||||||
|
|
||||||
|
const float invRt = 1.0f / (rtCur * sizeCur * (float) sampleRate);
|
||||||
|
|
||||||
|
const float loopInvRt = invRt * (1.0f - 0.7f * fbGain);
|
||||||
|
|
||||||
|
const float gMax = std::exp (-6.907755f * baseLen[0] * sizeCur * loopInvRt);
|
||||||
|
|
||||||
|
const float fbL = fl + fbGain * 0.5f * (1.0f - gMax) * shiftL.process (lastWetL, ratio);
|
||||||
|
const float fbR = fr + fbGain * 0.5f * (1.0f - gMax) * shiftR.process (lastWetR, ratio);
|
||||||
|
|
||||||
|
const float dL = diffL2.process (diffL1.process (fbL, diffG), diffG);
|
||||||
|
const float dR = diffR2.process (diffR1.process (fbR, diffG), diffG);
|
||||||
|
|
||||||
|
float wetL = 0.0f;
|
||||||
|
float wetR = 0.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
{
|
||||||
|
const float lenL = baseLen[(size_t) i] * sizeCur;
|
||||||
|
const float gainL = std::exp (-6.907755f * lenL * loopInvRt);
|
||||||
|
wetL += 0.25f * combL[(size_t) i].process (dL, lenL, dampC, gainL);
|
||||||
|
|
||||||
|
const float lenR = baseLenR[(size_t) i] * sizeCur;
|
||||||
|
const float gainR = std::exp (-6.907755f * lenR * loopInvRt);
|
||||||
|
wetR += 0.25f * combR[(size_t) i].process (dR, lenR, dampC, gainR);
|
||||||
|
}
|
||||||
|
|
||||||
|
wetL = dcL.process (wetL);
|
||||||
|
wetR = dcR.process (wetR);
|
||||||
|
|
||||||
|
wetL = std::tanh (wetL);
|
||||||
|
wetR = std::tanh (wetR);
|
||||||
|
|
||||||
|
lastWetL = wetL;
|
||||||
|
lastWetR = wetR;
|
||||||
|
|
||||||
|
outL = inL * (1.0f - wetVal) + wetL * wetVal;
|
||||||
|
outR = inR * (1.0f - wetVal) + wetR * wetVal;
|
||||||
|
}
|
||||||
218
Source/HorizontEngine.h
Normal file
218
Source/HorizontEngine.h
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "PitchShifter.h"
|
||||||
|
|
||||||
|
class HorizontEngine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HorizontEngine();
|
||||||
|
|
||||||
|
void prepare (double sampleRate);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void setLowCut (float hz);
|
||||||
|
void setHighCut (float hz);
|
||||||
|
void setDecay (float seconds);
|
||||||
|
void setSize (float size);
|
||||||
|
void setDiffusion (float diffusion);
|
||||||
|
void setBrightness (float brightness);
|
||||||
|
void setFeedback (float feedback);
|
||||||
|
void setPitch (float semitones);
|
||||||
|
void setDryWet (float dryWet);
|
||||||
|
|
||||||
|
void processSample (float inL, float inR, float& outL, float& outR);
|
||||||
|
|
||||||
|
float getLastWetL() const noexcept { return lastWetL; }
|
||||||
|
float getLastWetR() const noexcept { return lastWetR; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct SVF
|
||||||
|
{
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
ic1 = 0.0f;
|
||||||
|
ic2 = 0.0f;
|
||||||
|
g = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setG (float gIn)
|
||||||
|
{
|
||||||
|
g = gIn;
|
||||||
|
a1 = 1.0f / (1.0f + g * (g + k));
|
||||||
|
a2 = g * a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void process (float x)
|
||||||
|
{
|
||||||
|
const float v3 = x - ic2;
|
||||||
|
const float v1 = a1 * ic1 + a2 * v3;
|
||||||
|
const float v2 = ic2 + g * v1;
|
||||||
|
ic1 = 2.0f * v1 - ic1;
|
||||||
|
ic2 = 2.0f * v2 - ic2;
|
||||||
|
low = v2;
|
||||||
|
high = x - k * v1 - v2;
|
||||||
|
}
|
||||||
|
|
||||||
|
float g = 0.0f;
|
||||||
|
float k = 1.41421356f;
|
||||||
|
float a1 = 0.0f;
|
||||||
|
float a2 = 0.0f;
|
||||||
|
float ic1 = 0.0f;
|
||||||
|
float ic2 = 0.0f;
|
||||||
|
float low = 0.0f;
|
||||||
|
float high = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Allpass
|
||||||
|
{
|
||||||
|
void prepare (int length)
|
||||||
|
{
|
||||||
|
len = length;
|
||||||
|
xb.assign ((size_t) len + 1, 0.0f);
|
||||||
|
yb.assign ((size_t) len + 1, 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
std::fill (xb.begin(), xb.end(), 0.0f);
|
||||||
|
std::fill (yb.begin(), yb.end(), 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float process (float x, float g)
|
||||||
|
{
|
||||||
|
int rp = writePos - len;
|
||||||
|
if (rp < 0)
|
||||||
|
rp += len + 1;
|
||||||
|
|
||||||
|
const float xd = xb[(size_t) rp];
|
||||||
|
const float yd = yb[(size_t) rp];
|
||||||
|
xb[(size_t) writePos] = x;
|
||||||
|
|
||||||
|
const float y = -g * x + xd + g * yd;
|
||||||
|
yb[(size_t) writePos] = y;
|
||||||
|
|
||||||
|
++writePos;
|
||||||
|
if (writePos > len)
|
||||||
|
writePos = 0;
|
||||||
|
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> xb;
|
||||||
|
std::vector<float> yb;
|
||||||
|
int len = 0;
|
||||||
|
int writePos = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Comb
|
||||||
|
{
|
||||||
|
void prepare (int maxLength)
|
||||||
|
{
|
||||||
|
buffer.assign ((size_t) maxLength + 4, 0.0f);
|
||||||
|
maxLen = maxLength;
|
||||||
|
writePos = 0;
|
||||||
|
dampState = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
std::fill (buffer.begin(), buffer.end(), 0.0f);
|
||||||
|
dampState = 0.0f;
|
||||||
|
writePos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float process (float input, float delayLen, float dampCoef, float gain)
|
||||||
|
{
|
||||||
|
const float rp = (float) writePos - delayLen;
|
||||||
|
int i0 = (int) std::floor (rp);
|
||||||
|
const float frac = rp - (float) i0;
|
||||||
|
|
||||||
|
i0 %= maxLen;
|
||||||
|
if (i0 < 0)
|
||||||
|
i0 += maxLen;
|
||||||
|
|
||||||
|
int i1 = i0 + 1;
|
||||||
|
if (i1 >= maxLen)
|
||||||
|
i1 -= maxLen;
|
||||||
|
|
||||||
|
const float y = buffer[(size_t) i0] + frac * (buffer[(size_t) i1] - buffer[(size_t) i0]);
|
||||||
|
|
||||||
|
buffer[(size_t) writePos] = input + dampState * gain;
|
||||||
|
dampState += dampCoef * (y - dampState);
|
||||||
|
|
||||||
|
++writePos;
|
||||||
|
if (writePos >= maxLen)
|
||||||
|
writePos = 0;
|
||||||
|
|
||||||
|
return dampState;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> buffer;
|
||||||
|
int maxLen = 0;
|
||||||
|
int writePos = 0;
|
||||||
|
float dampState = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DCBlock
|
||||||
|
{
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
xm = 0.0f;
|
||||||
|
ym = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float process (float x)
|
||||||
|
{
|
||||||
|
const float y = x - xm + 0.999f * ym;
|
||||||
|
xm = x;
|
||||||
|
ym = y;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
float xm = 0.0f;
|
||||||
|
float ym = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::array<float, 8> baseLen441 = { 1553.f, 1787.f, 2017.f, 2237.f, 2437.f, 2657.f, 2879.f, 3073.f };
|
||||||
|
static constexpr std::array<float, 8> rightOffset = { 79.f, 67.f, 53.f, 41.f, 29.f, 23.f, 13.f, 7.f };
|
||||||
|
|
||||||
|
double sampleRate = 44100.0;
|
||||||
|
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> lowCutG;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> highCutG;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> decayS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> sizeS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> diffS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> dampS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> feedbackS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> ratioS;
|
||||||
|
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> wetS;
|
||||||
|
|
||||||
|
SVF hpL, lpL, hpR, lpR;
|
||||||
|
DCBlock dcL, dcR;
|
||||||
|
Allpass diffL1, diffL2, diffR1, diffR2;
|
||||||
|
PitchShifter shiftL, shiftR;
|
||||||
|
std::array<Comb, 8> combL;
|
||||||
|
std::array<Comb, 8> combR;
|
||||||
|
std::array<float, 8> baseLen;
|
||||||
|
std::array<float, 8> baseLenR;
|
||||||
|
|
||||||
|
float lowCutHz = 40.0f;
|
||||||
|
float highCutHz = 14000.0f;
|
||||||
|
float decaySec = 3.5f;
|
||||||
|
float sizeVal = 1.0f;
|
||||||
|
float diffusionVal = 0.6f;
|
||||||
|
float brightnessVal = 0.75f;
|
||||||
|
float feedbackVal = 0.6f;
|
||||||
|
float pitchVal = 0.0f;
|
||||||
|
float dryWetVal = 0.35f;
|
||||||
|
|
||||||
|
float lastWetL = 0.0f;
|
||||||
|
float lastWetR = 0.0f;
|
||||||
|
};
|
||||||
201
Source/HorizontLookAndFeel.cpp
Normal file
201
Source/HorizontLookAndFeel.cpp
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
#include "HorizontLookAndFeel.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const juce::Colour tealAccent (0xFF3CE0C8);
|
||||||
|
const juce::Colour textMain (0xFFE8F6F4);
|
||||||
|
const juce::Colour textDim (0xFFB8CCCC);
|
||||||
|
const juce::Colour charcoal (0xFF0B0E10);
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontLookAndFeel::HorizontLookAndFeel()
|
||||||
|
{
|
||||||
|
setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A3A3D));
|
||||||
|
setColour (juce::ComboBox::textColourId, textMain);
|
||||||
|
setColour (juce::ComboBox::arrowColourId, tealAccent);
|
||||||
|
setColour (juce::PopupMenu::backgroundColourId, juce::Colour (0xFF141B1E));
|
||||||
|
setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour (0xFF1E3A3C));
|
||||||
|
setColour (juce::PopupMenu::textColourId, textMain);
|
||||||
|
setColour (juce::TooltipWindow::textColourId, textMain);
|
||||||
|
setColour (juce::TooltipWindow::backgroundColourId, juce::Colour (0xFF122A2B));
|
||||||
|
setColour (juce::TooltipWindow::outlineColourId, tealAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||||
|
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
|
||||||
|
juce::Slider&)
|
||||||
|
{
|
||||||
|
const auto bounds = juce::Rectangle<float> ((float) x, (float) y, (float) width, (float) height);
|
||||||
|
const auto centre = bounds.getCentre();
|
||||||
|
const float radius = jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
|
||||||
|
const float angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto shadow = bounds.translated (0.0f, radius * 0.10f);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0x80000000),
|
||||||
|
centre.withY (centre.getY() + radius * 0.45f),
|
||||||
|
juce::Colour (0x00000000),
|
||||||
|
centre.withY (centre.getY() + radius * 0.45f).translated (radius * 1.1f, 0.0f),
|
||||||
|
true);
|
||||||
|
grad.addColour (0.55f, juce::Colour (0x30000000));
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto disc = bounds.reduced (radius * 0.05f);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0xFFA9BBC2),
|
||||||
|
centre.translated (-radius * 0.35f, -radius * 0.45f),
|
||||||
|
juce::Colour (0xFF171B1E),
|
||||||
|
centre.translated (-radius * 0.35f, -radius * 0.45f).translated (radius * 1.25f, 0.0f),
|
||||||
|
true);
|
||||||
|
grad.addColour (0.35f, juce::Colour (0xFF6E7C84));
|
||||||
|
grad.addColour (0.75f, juce::Colour (0xFF3A4247));
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (disc);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto rim = bounds.reduced (radius * 0.03f);
|
||||||
|
g.setColour (charcoal);
|
||||||
|
g.drawEllipse (rim, jmax (1.0f, radius * 0.05f));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto face = bounds.reduced (radius * 0.24f);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0xFF333C43),
|
||||||
|
centre.translated (0.0f, -radius * 0.10f),
|
||||||
|
juce::Colour (0xFF101316),
|
||||||
|
centre.translated (0.0f, -radius * 0.10f).translated (radius * 0.85f, 0.0f),
|
||||||
|
true);
|
||||||
|
grad.addColour (0.55f, juce::Colour (0xFF20262B));
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (face);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x383CE0C8));
|
||||||
|
g.drawEllipse (face, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const float tipDist = radius * 0.72f;
|
||||||
|
const float baseDist = radius * 0.34f;
|
||||||
|
const float halfWidth = radius * 0.085f;
|
||||||
|
|
||||||
|
const auto tip = centre.getPointOnCircumference (tipDist, angle);
|
||||||
|
const auto base = centre.getPointOnCircumference (baseDist, angle);
|
||||||
|
const float perp = angle + juce::MathConstants<float>::halfPi;
|
||||||
|
|
||||||
|
const auto b1 = base.translated (std::cos (perp) * halfWidth, std::sin (perp) * halfWidth);
|
||||||
|
const auto b2 = base.translated (-std::cos (perp) * halfWidth, -std::sin (perp) * halfWidth);
|
||||||
|
|
||||||
|
juce::Path notch;
|
||||||
|
notch.addTriangle (b1, b2, tip);
|
||||||
|
notch.closeSubPath();
|
||||||
|
|
||||||
|
g.setColour (tealAccent);
|
||||||
|
g.fillPath (notch);
|
||||||
|
g.setColour (tealAccent);
|
||||||
|
g.strokePath (notch, juce::PathStrokeType (1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto hl = juce::Rectangle<float> (radius * 0.9f, radius * 0.5f)
|
||||||
|
.withCentre (centre.translated (-radius * 0.28f, -radius * 0.32f));
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0x30FFFFFF), hl.getCentre(),
|
||||||
|
juce::Colour (0x00FFFFFF), hl.getCentre().translated (radius * 0.6f, 0.0f),
|
||||||
|
true);
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (hl);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto dot = juce::Rectangle<float> (radius * 0.16f, radius * 0.16f).withCentre (centre);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0xFF5A666D),
|
||||||
|
centre.translated (-radius * 0.03f, -radius * 0.03f),
|
||||||
|
juce::Colour (0xFF171B1E),
|
||||||
|
centre.translated (-radius * 0.03f, -radius * 0.03f).translated (radius * 0.14f, 0.0f),
|
||||||
|
true);
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool,
|
||||||
|
int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&)
|
||||||
|
{
|
||||||
|
auto bounds = juce::Rectangle<float> (0, 0, (float) width, (float) height);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0xFF22292E), { 0.0f, 0.0f },
|
||||||
|
juce::Colour (0xFF0E1215), { 0.0f, (float) height },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillRoundedRectangle (bounds, 4.0f);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0xFF3A4A4D));
|
||||||
|
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
|
||||||
|
|
||||||
|
auto arrowArea = juce::Rectangle<float> ((float) buttonX, (float) buttonY, (float) buttonW, (float) buttonH);
|
||||||
|
juce::Path arrow;
|
||||||
|
arrow.startNewSubPath (arrowArea.getX() + arrowArea.getWidth() * 0.35f, arrowArea.getCentreY() - 2.0f);
|
||||||
|
arrow.lineTo (arrowArea.getCentreX(), arrowArea.getCentreY() + 3.0f);
|
||||||
|
arrow.lineTo (arrowArea.getX() + arrowArea.getWidth() * 0.65f, arrowArea.getCentreY() - 2.0f);
|
||||||
|
|
||||||
|
g.setColour (tealAccent);
|
||||||
|
g.strokePath (arrow, juce::PathStrokeType (1.6f,
|
||||||
|
juce::PathStrokeType::JointStyle::beveled,
|
||||||
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
|
||||||
|
{
|
||||||
|
g.setColour (juce::Colour (0xF0141B1E));
|
||||||
|
g.fillRoundedRectangle (0.0f, 0.0f, (float) width, (float) height, 6.0f);
|
||||||
|
g.setColour (juce::Colour (0xFF3A4A4D));
|
||||||
|
g.drawRoundedRectangle (0.5f, 0.5f, (float) width - 1.0f, (float) height - 1.0f, 6.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle<int>& area,
|
||||||
|
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked,
|
||||||
|
bool, const juce::String& text, const juce::String&,
|
||||||
|
const juce::Drawable*, const juce::Colour* textColour)
|
||||||
|
{
|
||||||
|
if (isSeparator)
|
||||||
|
{
|
||||||
|
auto r = area.reduced (5, 0);
|
||||||
|
g.setColour (juce::Colour (0x334A5A5D));
|
||||||
|
g.fillRect (r.removeFromBottom (1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHighlighted && isActive)
|
||||||
|
{
|
||||||
|
g.setColour (juce::Colour (0xFF1E3A3C));
|
||||||
|
g.fillRoundedRectangle (area.toFloat().reduced (2.0f), 4.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto textArea = area.reduced (12, 0);
|
||||||
|
if (isTicked)
|
||||||
|
textArea.removeFromLeft (12);
|
||||||
|
|
||||||
|
g.setColour (isActive ? (textColour != nullptr ? *textColour : textMain) : textDim);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (13.0f)));
|
||||||
|
g.drawText (text, textArea, juce::Justification::centredLeft, false);
|
||||||
|
|
||||||
|
if (isTicked)
|
||||||
|
{
|
||||||
|
auto dot = juce::Rectangle<float> (4.0f, 4.0f)
|
||||||
|
.withCentre (juce::Point<float> ((float) (area.getX() + 10), (float) area.getCentreY()));
|
||||||
|
g.setColour (tealAccent);
|
||||||
|
g.fillEllipse (dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontLookAndFeel::drawTooltip (juce::Graphics& g, const juce::String& text, int width, int height)
|
||||||
|
{
|
||||||
|
g.setColour (juce::Colour (0xF0122A2B));
|
||||||
|
g.fillRoundedRectangle (0.0f, 0.0f, (float) width, (float) height, 4.0f);
|
||||||
|
g.setColour (tealAccent);
|
||||||
|
g.drawRoundedRectangle (0.5f, 0.5f, (float) width - 1.0f, (float) height - 1.0f, 4.0f, 1.0f);
|
||||||
|
g.setColour (textMain);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (12.0f)));
|
||||||
|
g.drawText (text, juce::Rectangle<int> (6, 0, width - 12, height), juce::Justification::centred, true);
|
||||||
|
}
|
||||||
25
Source/HorizontLookAndFeel.h
Normal file
25
Source/HorizontLookAndFeel.h
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
class HorizontLookAndFeel : public juce::LookAndFeel_V4
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HorizontLookAndFeel();
|
||||||
|
|
||||||
|
void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height,
|
||||||
|
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
|
||||||
|
juce::Slider&) override;
|
||||||
|
|
||||||
|
void drawComboBox (juce::Graphics&, int width, int height, bool isButtonDown,
|
||||||
|
int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&) override;
|
||||||
|
|
||||||
|
void drawPopupMenuBackground (juce::Graphics&, int width, int height) override;
|
||||||
|
|
||||||
|
void drawPopupMenuItem (juce::Graphics&, const juce::Rectangle<int>& area,
|
||||||
|
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked,
|
||||||
|
bool hasSubMenu, const juce::String& text, const juce::String& shortcutKeyText,
|
||||||
|
const juce::Drawable* icon, const juce::Colour* textColour) override;
|
||||||
|
|
||||||
|
void drawTooltip (juce::Graphics&, const juce::String& text, int width, int height) override;
|
||||||
|
};
|
||||||
81
Source/Knob.cpp
Normal file
81
Source/Knob.cpp
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#include "Knob.h"
|
||||||
|
|
||||||
|
Knob::Knob (const juce::String& title,
|
||||||
|
juce::AudioProcessorValueTreeState& apvts,
|
||||||
|
const juce::String& paramID,
|
||||||
|
float defaultValue,
|
||||||
|
std::function<juce::String (float)> formatFn,
|
||||||
|
const juce::String& tooltip)
|
||||||
|
: format (std::move (formatFn))
|
||||||
|
{
|
||||||
|
nameLabel.setText (title, juce::dontSendNotification);
|
||||||
|
nameLabel.setJustificationType (juce::Justification::centred);
|
||||||
|
nameLabel.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::bold)));
|
||||||
|
nameLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFB8CCCC));
|
||||||
|
nameLabel.setInterceptsMouseClicks (false, false);
|
||||||
|
addAndMakeVisible (nameLabel);
|
||||||
|
|
||||||
|
valueLabel.setJustificationType (juce::Justification::centred);
|
||||||
|
valueLabel.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::bold)));
|
||||||
|
valueLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF3CE0C8));
|
||||||
|
valueLabel.setInterceptsMouseClicks (false, false);
|
||||||
|
addAndMakeVisible (valueLabel);
|
||||||
|
|
||||||
|
slider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||||
|
slider.setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
|
||||||
|
slider.setDoubleClickReturnValue (true, defaultValue);
|
||||||
|
slider.setWantsKeyboardFocus (true);
|
||||||
|
slider.setScrollWheelEnabled (true);
|
||||||
|
slider.setTooltip (tooltip);
|
||||||
|
slider.onValueChange = [this] { updateValueLabel(); };
|
||||||
|
addAndMakeVisible (slider);
|
||||||
|
|
||||||
|
attachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (apvts, paramID, slider);
|
||||||
|
|
||||||
|
updateValueLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Knob::setScaleFactor (float scale)
|
||||||
|
{
|
||||||
|
scaleFactor = scale;
|
||||||
|
resized();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Knob::updateValueLabel()
|
||||||
|
{
|
||||||
|
valueLabel.setText (format (slider.getValue()), juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Knob::paint (juce::Graphics& g)
|
||||||
|
{
|
||||||
|
auto b = slider.getBounds().toFloat();
|
||||||
|
auto shadow = b.translated (0.0f, b.getHeight() * 0.05f).expanded (b.getWidth() * 0.05f);
|
||||||
|
auto shadowCentre = shadow.getCentre().withY (shadow.getCentre().getY() + b.getHeight() * 0.4f);
|
||||||
|
auto grad = juce::ColourGradient (juce::Colour (0x60000000), shadowCentre,
|
||||||
|
juce::Colour (0x00000000), shadowCentre.translated (shadow.getWidth() * 0.7f, 0.0f),
|
||||||
|
true);
|
||||||
|
grad.addColour (0.55f, juce::Colour (0x20000000));
|
||||||
|
g.setGradientFill (grad);
|
||||||
|
g.fillEllipse (shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Knob::resized()
|
||||||
|
{
|
||||||
|
auto bounds = getLocalBounds();
|
||||||
|
const float s = scaleFactor;
|
||||||
|
|
||||||
|
const int nameH = juce::roundToInt (18.0f * s);
|
||||||
|
const int valueH = juce::roundToInt (16.0f * s);
|
||||||
|
const int maxKnobSize = juce::roundToInt (84.0f * s);
|
||||||
|
|
||||||
|
auto nameArea = bounds.removeFromTop (nameH);
|
||||||
|
auto valueArea = bounds.removeFromBottom (valueH);
|
||||||
|
nameLabel.setBounds (nameArea);
|
||||||
|
valueLabel.setBounds (valueArea);
|
||||||
|
|
||||||
|
nameLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s), juce::Font::bold)));
|
||||||
|
valueLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s), juce::Font::bold)));
|
||||||
|
|
||||||
|
const int knobSize = jmin (bounds.getWidth(), bounds.getHeight(), maxKnobSize);
|
||||||
|
slider.setBounds (bounds.getCentreX() - knobSize / 2, bounds.getCentreY() - knobSize / 2, knobSize, knobSize);
|
||||||
|
}
|
||||||
31
Source/Knob.h
Normal file
31
Source/Knob.h
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
class Knob : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Knob (const juce::String& title,
|
||||||
|
juce::AudioProcessorValueTreeState& apvts,
|
||||||
|
const juce::String& paramID,
|
||||||
|
float defaultValue,
|
||||||
|
std::function<juce::String (float)> format,
|
||||||
|
const juce::String& tooltip);
|
||||||
|
|
||||||
|
void paint (juce::Graphics&) override;
|
||||||
|
void resized() override;
|
||||||
|
void setScaleFactor (float scale);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateValueLabel();
|
||||||
|
|
||||||
|
std::function<juce::String (float)> format;
|
||||||
|
juce::Label nameLabel;
|
||||||
|
juce::Label valueLabel;
|
||||||
|
juce::Slider slider;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
|
||||||
|
|
||||||
|
float scaleFactor = 1.0f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Knob)
|
||||||
|
};
|
||||||
80
Source/PitchShifter.cpp
Normal file
80
Source/PitchShifter.cpp
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#include "PitchShifter.h"
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
void PitchShifter::prepare (double sampleRate)
|
||||||
|
{
|
||||||
|
window = (float) std::max (256.0, sampleRate * 0.035);
|
||||||
|
baseDelay = window;
|
||||||
|
bufferSize = (int) (baseDelay + window + 16.0f);
|
||||||
|
buffer.assign ((size_t) bufferSize, 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
o = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PitchShifter::reset()
|
||||||
|
{
|
||||||
|
std::fill (buffer.begin(), buffer.end(), 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
o = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float PitchShifter::at (int index) const
|
||||||
|
{
|
||||||
|
index %= bufferSize;
|
||||||
|
if (index < 0)
|
||||||
|
index += bufferSize;
|
||||||
|
|
||||||
|
return buffer[(size_t) index];
|
||||||
|
}
|
||||||
|
|
||||||
|
float PitchShifter::readCubic (float pos) const
|
||||||
|
{
|
||||||
|
const int i = (int) std::floor (pos);
|
||||||
|
const float frac = pos - (float) i;
|
||||||
|
|
||||||
|
const float x0 = at (i - 1);
|
||||||
|
const float x1 = at (i);
|
||||||
|
const float x2 = at (i + 1);
|
||||||
|
const float x3 = at (i + 2);
|
||||||
|
|
||||||
|
const float a0 = -0.5f * x0 + 1.5f * x1 - 1.5f * x2 + 0.5f * x3;
|
||||||
|
const float a1 = x0 - 2.5f * x1 + 2.0f * x2 - 0.5f * x3;
|
||||||
|
const float a2 = -0.5f * x0 + 0.5f * x2;
|
||||||
|
const float a3 = x1;
|
||||||
|
|
||||||
|
return ((a0 * frac + a1) * frac + a2) * frac + a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
float PitchShifter::process (float input, float ratio)
|
||||||
|
{
|
||||||
|
buffer[(size_t) writePos] = input;
|
||||||
|
|
||||||
|
if (std::fabs (ratio - 1.0f) < 1e-4f)
|
||||||
|
return input;
|
||||||
|
|
||||||
|
const float theta = juce::MathConstants<float>::pi * o / window;
|
||||||
|
const float g1 = std::sin (theta) * std::sin (theta);
|
||||||
|
const float g2 = std::cos (theta) * std::cos (theta);
|
||||||
|
|
||||||
|
float o2 = o + window * 0.5f;
|
||||||
|
if (o2 >= window)
|
||||||
|
o2 -= window;
|
||||||
|
|
||||||
|
const float pos1 = (float) writePos - (baseDelay - o);
|
||||||
|
const float pos2 = (float) writePos - (baseDelay - o2);
|
||||||
|
|
||||||
|
const float result = g1 * readCubic (pos1) + g2 * readCubic (pos2);
|
||||||
|
|
||||||
|
o += (ratio - 1.0f);
|
||||||
|
if (o >= window)
|
||||||
|
o -= window;
|
||||||
|
else if (o < 0.0f)
|
||||||
|
o += window;
|
||||||
|
|
||||||
|
++writePos;
|
||||||
|
if (writePos >= bufferSize)
|
||||||
|
writePos = 0;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
23
Source/PitchShifter.h
Normal file
23
Source/PitchShifter.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class PitchShifter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (double sampleRate);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
float process (float input, float ratio);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float at (int index) const;
|
||||||
|
float readCubic (float pos) const;
|
||||||
|
|
||||||
|
std::vector<float> buffer;
|
||||||
|
int bufferSize = 0;
|
||||||
|
int writePos = 0;
|
||||||
|
float o = 0.0f;
|
||||||
|
float baseDelay = 0.0f;
|
||||||
|
float window = 0.0f;
|
||||||
|
};
|
||||||
349
Source/PluginEditor.cpp
Normal file
349
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
#include "PluginEditor.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
juce::String formatHz (float v)
|
||||||
|
{
|
||||||
|
return juce::String (juce::roundToInt (v)) + " Hz";
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String formatSec (float v)
|
||||||
|
{
|
||||||
|
return juce::String (v, 1) + " s";
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String formatSize (float v)
|
||||||
|
{
|
||||||
|
return juce::String (v, 2) + "x";
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String formatPct (float v)
|
||||||
|
{
|
||||||
|
return juce::String (juce::roundToInt (v * 100.0f)) + " %";
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String formatSemis (float v)
|
||||||
|
{
|
||||||
|
const int semis = juce::roundToInt (v);
|
||||||
|
if (semis > 0)
|
||||||
|
return "+" + juce::String (semis) + " st";
|
||||||
|
if (semis < 0)
|
||||||
|
return juce::String (semis) + " st";
|
||||||
|
return "0 st";
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::StringArray presetNames()
|
||||||
|
{
|
||||||
|
juce::StringArray names;
|
||||||
|
for (const auto& p : getPresets())
|
||||||
|
names.add (p.name);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float scalePresets[] = { 0.75f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
|
||||||
|
constexpr int numScalePresets = (int) (sizeof (scalePresets) / sizeof (scalePresets[0]));
|
||||||
|
|
||||||
|
juce::StringArray scaleNames()
|
||||||
|
{
|
||||||
|
juce::StringArray names;
|
||||||
|
for (float v : scalePresets)
|
||||||
|
names.add (juce::String (juce::roundToInt (v * 100.0f)) + " %");
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String buildFooterText()
|
||||||
|
{
|
||||||
|
#ifndef HORIZONT_VERSION
|
||||||
|
#define HORIZONT_VERSION "0.0.0"
|
||||||
|
#endif
|
||||||
|
#ifndef HORIZONT_GIT_REVISION
|
||||||
|
#define HORIZONT_GIT_REVISION "unknown"
|
||||||
|
#endif
|
||||||
|
#ifndef HORIZONT_BUILD_TIMESTAMP
|
||||||
|
#define HORIZONT_BUILD_TIMESTAMP "unknown"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
juce::String text ("HORIZONT v" + juce::String (HORIZONT_VERSION));
|
||||||
|
text << " · git " << HORIZONT_GIT_REVISION;
|
||||||
|
text << " · build " << HORIZONT_BUILD_TIMESTAMP;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontAudioProcessorEditor::HorizontAudioProcessorEditor (HorizontAudioProcessor& p,
|
||||||
|
juce::AudioProcessorValueTreeState& s)
|
||||||
|
: AudioProcessorEditor (&p),
|
||||||
|
processor (p),
|
||||||
|
apvts (s),
|
||||||
|
display (p)
|
||||||
|
{
|
||||||
|
lookAndFeel = std::make_unique<HorizontLookAndFeel>();
|
||||||
|
setLookAndFeel (lookAndFeel.get());
|
||||||
|
|
||||||
|
titleLabel.setText ("HORIZONT", juce::dontSendNotification);
|
||||||
|
titleLabel.setFont (juce::Font (juce::FontOptions (22.0f, juce::Font::bold)));
|
||||||
|
titleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFE8F6F4));
|
||||||
|
addAndMakeVisible (titleLabel);
|
||||||
|
|
||||||
|
subtitleLabel.setText ("Algorithmic Spatial Processor", juce::dontSendNotification);
|
||||||
|
subtitleLabel.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||||||
|
subtitleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
|
||||||
|
addAndMakeVisible (subtitleLabel);
|
||||||
|
|
||||||
|
presetBox.addItemList (presetNames(), 1);
|
||||||
|
presetBox.setSelectedItemIndex (0);
|
||||||
|
presetBox.addListener (this);
|
||||||
|
presetBox.setTooltip ("Recall a factory preset");
|
||||||
|
addAndMakeVisible (presetBox);
|
||||||
|
|
||||||
|
scaleBox.addItemList (scaleNames(), 1);
|
||||||
|
scaleBox.setSelectedItemIndex (2);
|
||||||
|
scaleBox.addListener (this);
|
||||||
|
scaleBox.setTooltip ("Set the UI scale");
|
||||||
|
addAndMakeVisible (scaleBox);
|
||||||
|
|
||||||
|
footerLabel.setText (buildFooterText(), juce::dontSendNotification);
|
||||||
|
footerLabel.setJustificationType (juce::Justification::centred);
|
||||||
|
footerLabel.setInterceptsMouseClicks (false, false);
|
||||||
|
addAndMakeVisible (footerLabel);
|
||||||
|
|
||||||
|
addAndMakeVisible (display);
|
||||||
|
|
||||||
|
auto makeKnob = [this] (const juce::String& title, const juce::String& id,
|
||||||
|
const juce::String& tip, std::function<juce::String (float)> fmt,
|
||||||
|
float defVal) -> std::unique_ptr<Knob>
|
||||||
|
{
|
||||||
|
return std::make_unique<Knob> (title, apvts, id, defVal, fmt, tip);
|
||||||
|
};
|
||||||
|
|
||||||
|
knobs.push_back (makeKnob ("LOW CUT", "lowCut", "Input high-pass filter (20 Hz - 500 Hz)", formatHz, 40.0f));
|
||||||
|
knobs.push_back (makeKnob ("HIGH CUT", "highCut", "Input low-pass filter (800 Hz - 20 kHz)", formatHz, 14000.0f));
|
||||||
|
knobs.push_back (makeKnob ("DECAY", "decay", "Length of the reverb tail (RT60)", formatSec, 3.5f));
|
||||||
|
knobs.push_back (makeKnob ("SIZE", "size", "Room size scaling", formatSize, 1.0f));
|
||||||
|
knobs.push_back (makeKnob ("DIFFUSION", "diffusion", "Echo diffusion and smearing", formatPct, 0.6f));
|
||||||
|
knobs.push_back (makeKnob ("BRIGHTNESS", "brightness", "High-frequency damping of the tail", formatPct, 0.75f));
|
||||||
|
knobs.push_back (makeKnob ("FEEDBACK", "feedback", "Recirculation through the pitch shifter", formatPct, 0.6f));
|
||||||
|
knobs.push_back (makeKnob ("PITCH", "pitch", "Pitch shift applied per feedback pass", formatSemis, 0.0f));
|
||||||
|
knobs.push_back (makeKnob ("DRY / WET", "dryWet", "Dry / wet mix", formatPct, 0.35f));
|
||||||
|
|
||||||
|
for (auto& k : knobs)
|
||||||
|
addAndMakeVisible (k.get());
|
||||||
|
|
||||||
|
setSize (juce::roundToInt (baseWidth), juce::roundToInt (baseHeight));
|
||||||
|
setResizeLimits (juce::roundToInt (baseWidth * 0.75f), juce::roundToInt (baseHeight * 0.75f),
|
||||||
|
juce::roundToInt (baseWidth * 2.0f), juce::roundToInt (baseHeight * 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontAudioProcessorEditor::~HorizontAudioProcessorEditor()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::setScaleFactor (float newScale)
|
||||||
|
{
|
||||||
|
newScale = juce::jlimit (0.6f, 2.0f, newScale);
|
||||||
|
|
||||||
|
setSize (juce::roundToInt (baseWidth * newScale),
|
||||||
|
juce::roundToInt (baseHeight * newScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::updateScaleFactor()
|
||||||
|
{
|
||||||
|
const float newScale = juce::jmin ((float) getWidth() / baseWidth,
|
||||||
|
(float) getHeight() / baseHeight);
|
||||||
|
|
||||||
|
if (juce::approximatelyEqual (scaleFactor, newScale))
|
||||||
|
return;
|
||||||
|
|
||||||
|
scaleFactor = newScale;
|
||||||
|
|
||||||
|
for (auto& k : knobs)
|
||||||
|
k->setScaleFactor (scaleFactor);
|
||||||
|
|
||||||
|
display.setScaleFactor (scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::comboBoxChanged (juce::ComboBox* box)
|
||||||
|
{
|
||||||
|
if (box == &presetBox)
|
||||||
|
{
|
||||||
|
const int idx = presetBox.getSelectedItemIndex();
|
||||||
|
const auto& presets = processor.getPresets();
|
||||||
|
|
||||||
|
if (idx >= 0 && idx < (int) presets.size())
|
||||||
|
processor.applyPreset (presets[(size_t) idx]);
|
||||||
|
}
|
||||||
|
else if (box == &scaleBox)
|
||||||
|
{
|
||||||
|
const int idx = scaleBox.getSelectedItemIndex();
|
||||||
|
|
||||||
|
if (idx >= 0 && idx < numScalePresets)
|
||||||
|
setScaleFactor (scalePresets[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::updateScaleSelection()
|
||||||
|
{
|
||||||
|
int best = 0;
|
||||||
|
float bestDiff = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
|
for (int i = 0; i < numScalePresets; ++i)
|
||||||
|
{
|
||||||
|
const float diff = std::abs (scaleFactor - scalePresets[i]);
|
||||||
|
if (diff < bestDiff)
|
||||||
|
{
|
||||||
|
bestDiff = diff;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleBox.setSelectedItemIndex (best, juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::layoutKnobRow (juce::Rectangle<int> area, int startIndex, int count)
|
||||||
|
{
|
||||||
|
juce::FlexBox fb;
|
||||||
|
fb.flexDirection = juce::FlexBox::Direction::row;
|
||||||
|
fb.justifyContent = juce::FlexBox::JustifyContent::center;
|
||||||
|
fb.alignItems = juce::FlexBox::AlignItems::stretch;
|
||||||
|
|
||||||
|
const float minW = 80.0f * scaleFactor;
|
||||||
|
const float minH = 96.0f * scaleFactor;
|
||||||
|
|
||||||
|
for (int i = startIndex; i < startIndex + count; ++i)
|
||||||
|
fb.items.add (juce::FlexItem (*knobs[(size_t) i]).withMinWidth (minW).withMinHeight (minH).withFlex (1.0f));
|
||||||
|
|
||||||
|
fb.performLayout (area);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails& wheel)
|
||||||
|
{
|
||||||
|
if (wheel.deltaY != 0.0f)
|
||||||
|
{
|
||||||
|
const float zoomStep = 0.1f;
|
||||||
|
setScaleFactor (scaleFactor - wheel.deltaY * zoomStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::resized()
|
||||||
|
{
|
||||||
|
updateScaleFactor();
|
||||||
|
updateScaleSelection();
|
||||||
|
|
||||||
|
auto area = getLocalBounds();
|
||||||
|
|
||||||
|
const float s = scaleFactor;
|
||||||
|
const int headerH = juce::roundToInt (64.0f * s);
|
||||||
|
const int footerH = juce::roundToInt (26.0f * s);
|
||||||
|
const int presetBoxW = juce::roundToInt (180.0f * s);
|
||||||
|
const int scaleBoxW = juce::roundToInt (72.0f * s);
|
||||||
|
const int boxGap = juce::roundToInt (10.0f * s);
|
||||||
|
const int presetBoxMargin = juce::roundToInt (20.0f * s);
|
||||||
|
const int titleH = juce::roundToInt (32.0f * s);
|
||||||
|
const int titleMarginH = juce::roundToInt (18.0f * s);
|
||||||
|
const int titleMarginV = juce::roundToInt (4.0f * s);
|
||||||
|
const int subtitleMarginH = juce::roundToInt (18.0f * s);
|
||||||
|
const int subtitleMarginV = juce::roundToInt (2.0f * s);
|
||||||
|
const int rowsMarginH = juce::roundToInt (16.0f * s);
|
||||||
|
const int rowsMarginV = juce::roundToInt (4.0f * s);
|
||||||
|
const int gap = juce::roundToInt (10.0f * s);
|
||||||
|
|
||||||
|
footerArea = area.removeFromBottom (footerH);
|
||||||
|
footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0));
|
||||||
|
footerLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (9.0f * s))));
|
||||||
|
footerLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
|
||||||
|
|
||||||
|
auto header = area.removeFromTop (headerH);
|
||||||
|
|
||||||
|
const int comboW = presetBoxW + boxGap + scaleBoxW;
|
||||||
|
auto comboArea = header.removeFromRight (comboW).reduced (0, presetBoxMargin);
|
||||||
|
auto scaleArea = comboArea.removeFromLeft (scaleBoxW);
|
||||||
|
scaleBox.setBounds (scaleArea);
|
||||||
|
presetBox.setBounds (comboArea);
|
||||||
|
|
||||||
|
auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV);
|
||||||
|
titleLabel.setBounds (titleArea);
|
||||||
|
subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV));
|
||||||
|
|
||||||
|
titleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (22.0f * s), juce::Font::bold)));
|
||||||
|
subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s))));
|
||||||
|
|
||||||
|
auto modules = area.reduced (rowsMarginH, rowsMarginV);
|
||||||
|
const int panelH = (modules.getHeight() - 2 * gap) / 3;
|
||||||
|
|
||||||
|
auto displayPanel = modules.removeFromTop (panelH);
|
||||||
|
display.setBounds (displayPanel);
|
||||||
|
|
||||||
|
knobRow1Area = modules.removeFromTop (panelH + gap);
|
||||||
|
knobRow2Area = modules.removeFromTop (panelH + gap);
|
||||||
|
|
||||||
|
layoutKnobRow (knobRow1Area, 0, 5);
|
||||||
|
layoutKnobRow (knobRow2Area, 5, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessorEditor::paint (juce::Graphics& g)
|
||||||
|
{
|
||||||
|
const auto bounds = getLocalBounds();
|
||||||
|
|
||||||
|
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF0E2A2E), { 0.0f, 0.0f },
|
||||||
|
juce::Colour (0xFF060B0D), { 0.0f, (float) bounds.getHeight() },
|
||||||
|
false));
|
||||||
|
g.fillRect (bounds);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x1AFFFFFF));
|
||||||
|
g.fillRect (0, 0, bounds.getWidth(), 1);
|
||||||
|
g.setColour (juce::Colour (0x14000000));
|
||||||
|
g.fillRect (0, bounds.getHeight() - 1, bounds.getWidth(), 1);
|
||||||
|
|
||||||
|
if (footerArea.getHeight() > 0)
|
||||||
|
{
|
||||||
|
g.setColour (juce::Colour (0x1AFFFFFF));
|
||||||
|
g.fillRect (0, footerArea.getY(), bounds.getWidth(), 1);
|
||||||
|
g.setColour (juce::Colour (0x14000000));
|
||||||
|
g.fillRect (0, footerArea.getY() + 1, bounds.getWidth(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto drawModule = [&g] (juce::Rectangle<int> moduleArea)
|
||||||
|
{
|
||||||
|
const auto r = moduleArea.toFloat();
|
||||||
|
|
||||||
|
auto shRect = r.expanded (4.0f, 3.0f).translated (0.0f, 3.0f);
|
||||||
|
auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() },
|
||||||
|
juce::Colour (0x00000000), { 0.0f, shRect.getBottom() },
|
||||||
|
false);
|
||||||
|
shadowGrad.addColour (0.45f, juce::Colour (0x70000000));
|
||||||
|
g.setGradientFill (shadowGrad);
|
||||||
|
g.fillRoundedRectangle (shRect, 13.0f);
|
||||||
|
|
||||||
|
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, r.getY() },
|
||||||
|
juce::Colour (0xFF0B1216), { 0.0f, r.getBottom() },
|
||||||
|
false));
|
||||||
|
g.fillRoundedRectangle (r, 12.0f);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x2A3CE0C8));
|
||||||
|
g.drawRoundedRectangle (r.expanded (1.0f), 13.0f, 1.0f);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x38FFFFFF));
|
||||||
|
g.drawHorizontalLine ((int) r.getY() + 1, (int) r.getX() + 3, (int) r.getRight() - 3);
|
||||||
|
g.drawVerticalLine ((int) r.getX() + 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
|
||||||
|
g.setColour (juce::Colour (0x50000000));
|
||||||
|
g.drawHorizontalLine ((int) r.getBottom() - 1, (int) r.getX() + 3, (int) r.getRight() - 3);
|
||||||
|
g.drawVerticalLine ((int) r.getRight() - 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
|
||||||
|
|
||||||
|
g.saveState();
|
||||||
|
g.reduceClipRegion (r.toNearestInt().reduced (1));
|
||||||
|
auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, r.getY() },
|
||||||
|
juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (refl);
|
||||||
|
g.fillRect (r);
|
||||||
|
g.setColour (juce::Colour (0x45FFFFFF));
|
||||||
|
g.fillRect (r.getX() + 2.0f, r.getY() + 1.0f, r.getWidth() - 4.0f, 1.0f);
|
||||||
|
g.restoreState();
|
||||||
|
};
|
||||||
|
|
||||||
|
drawModule (knobRow1Area);
|
||||||
|
drawModule (knobRow2Area);
|
||||||
|
}
|
||||||
54
Source/PluginEditor.h
Normal file
54
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Knob.h"
|
||||||
|
#include "PluginProcessor.h"
|
||||||
|
#include "HorizontLookAndFeel.h"
|
||||||
|
#include "SpectrumDisplay.h"
|
||||||
|
|
||||||
|
class HorizontAudioProcessorEditor : public juce::AudioProcessorEditor,
|
||||||
|
private juce::ComboBox::Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HorizontAudioProcessorEditor (HorizontAudioProcessor&, juce::AudioProcessorValueTreeState&);
|
||||||
|
~HorizontAudioProcessorEditor() override;
|
||||||
|
|
||||||
|
void paint (juce::Graphics&) override;
|
||||||
|
void resized() override;
|
||||||
|
void mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails&) override;
|
||||||
|
|
||||||
|
float getScaleFactor() const noexcept { return scaleFactor; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void comboBoxChanged (juce::ComboBox* boxThatHasChanged) override;
|
||||||
|
void layoutKnobRow (juce::Rectangle<int> area, int startIndex, int count);
|
||||||
|
void setScaleFactor (float newScale);
|
||||||
|
void updateScaleFactor();
|
||||||
|
void updateScaleSelection();
|
||||||
|
|
||||||
|
HorizontAudioProcessor& processor;
|
||||||
|
juce::AudioProcessorValueTreeState& apvts;
|
||||||
|
|
||||||
|
std::unique_ptr<HorizontLookAndFeel> lookAndFeel;
|
||||||
|
juce::Label titleLabel;
|
||||||
|
juce::Label subtitleLabel;
|
||||||
|
juce::ComboBox presetBox;
|
||||||
|
juce::ComboBox scaleBox;
|
||||||
|
juce::Label footerLabel;
|
||||||
|
SpectrumDisplay display;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<Knob>> knobs;
|
||||||
|
|
||||||
|
juce::Rectangle<int> knobRow1Area;
|
||||||
|
juce::Rectangle<int> knobRow2Area;
|
||||||
|
juce::Rectangle<int> footerArea;
|
||||||
|
|
||||||
|
float scaleFactor = 1.0f;
|
||||||
|
static constexpr float baseWidth = 860.0f;
|
||||||
|
static constexpr float baseHeight = 606.0f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HorizontAudioProcessorEditor)
|
||||||
|
};
|
||||||
314
Source/PluginProcessor.cpp
Normal file
314
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
#include "PluginProcessor.h"
|
||||||
|
|
||||||
|
#include "PluginEditor.h"
|
||||||
|
|
||||||
|
HorizontAudioProcessor::HorizontAudioProcessor()
|
||||||
|
: AudioProcessor (BusesProperties()
|
||||||
|
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
|
||||||
|
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
|
||||||
|
apvts (*this, nullptr, "Parameters", createParameterLayout()),
|
||||||
|
presets (getPresets())
|
||||||
|
{
|
||||||
|
lowCutParam = apvts.getRawParameterValue ("lowCut");
|
||||||
|
highCutParam = apvts.getRawParameterValue ("highCut");
|
||||||
|
decayParam = apvts.getRawParameterValue ("decay");
|
||||||
|
sizeParam = apvts.getRawParameterValue ("size");
|
||||||
|
diffusionParam = apvts.getRawParameterValue ("diffusion");
|
||||||
|
brightnessParam = apvts.getRawParameterValue ("brightness");
|
||||||
|
feedbackParam = apvts.getRawParameterValue ("feedback");
|
||||||
|
pitchParam = apvts.getRawParameterValue ("pitch");
|
||||||
|
dryWetParam = apvts.getRawParameterValue ("dryWet");
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontAudioProcessor::~HorizontAudioProcessor()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessorValueTreeState::ParameterLayout HorizontAudioProcessor::createParameterLayout()
|
||||||
|
{
|
||||||
|
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("lowCut", "Low Cut",
|
||||||
|
juce::NormalisableRange<float> (20.0f, 500.0f, 0.0f, 0.35f), 40.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("highCut", "High Cut",
|
||||||
|
juce::NormalisableRange<float> (800.0f, 20000.0f, 0.0f, 0.25f), 14000.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("decay", "Decay",
|
||||||
|
juce::NormalisableRange<float> (0.2f, 10.0f, 0.0f, 0.5f), 3.5f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("size", "Size",
|
||||||
|
juce::NormalisableRange<float> (0.4f, 2.0f, 0.0f, 0.5f), 1.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("diffusion", "Diffusion",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.6f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("brightness", "Brightness",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.75f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("feedback", "Feedback",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.6f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("pitch", "Pitch",
|
||||||
|
juce::NormalisableRange<float> (-12.0f, 12.0f, 0.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> ("dryWet", "Dry/Wet",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.35f));
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||||
|
{
|
||||||
|
wetScratch.resize ((size_t) juce::jmax (samplesPerBlock, 1));
|
||||||
|
dryScratch.resize ((size_t) juce::jmax (samplesPerBlock, 1));
|
||||||
|
wetFifo.reset();
|
||||||
|
engine.prepare (sampleRate);
|
||||||
|
updateParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::releaseResources()
|
||||||
|
{
|
||||||
|
engine.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HorizontAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
|
||||||
|
{
|
||||||
|
if (layouts.getMainOutputChannelSet().isDisabled())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const int outCh = layouts.getMainOutputChannelSet().size();
|
||||||
|
const int inCh = layouts.getMainInputChannelSet().size();
|
||||||
|
|
||||||
|
if (inCh == 0)
|
||||||
|
return outCh <= 2;
|
||||||
|
|
||||||
|
return inCh <= 2 && outCh <= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::updateParameters()
|
||||||
|
{
|
||||||
|
engine.setLowCut (lowCutParam->load());
|
||||||
|
engine.setHighCut (highCutParam->load());
|
||||||
|
engine.setDecay (decayParam->load());
|
||||||
|
engine.setSize (sizeParam->load());
|
||||||
|
engine.setDiffusion (diffusionParam->load());
|
||||||
|
engine.setBrightness (brightnessParam->load());
|
||||||
|
engine.setFeedback (feedbackParam->load());
|
||||||
|
engine.setPitch (pitchParam->load());
|
||||||
|
engine.setDryWet (dryWetParam->load());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||||
|
{
|
||||||
|
juce::ScopedNoDenormals noDenormals;
|
||||||
|
|
||||||
|
const int numSamples = buffer.getNumSamples();
|
||||||
|
if (numSamples == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int numOut = juce::jmin (buffer.getNumChannels(), 2);
|
||||||
|
if (numOut == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int ch = numOut; ch < buffer.getNumChannels(); ++ch)
|
||||||
|
buffer.clear (ch, 0, numSamples);
|
||||||
|
|
||||||
|
updateParameters();
|
||||||
|
|
||||||
|
const int inputChannels = getChannelCountOfBus (true, 0);
|
||||||
|
const bool haveLeft = buffer.getNumChannels() > 0 && inputChannels > 0;
|
||||||
|
const bool haveRight = buffer.getNumChannels() > 1 && inputChannels > 1;
|
||||||
|
const bool canCaptureWet = wetScratch.size() >= (size_t) numSamples;
|
||||||
|
|
||||||
|
for (int s = 0; s < numSamples; ++s)
|
||||||
|
{
|
||||||
|
const float inL = haveLeft ? buffer.getSample (0, s) : 0.0f;
|
||||||
|
const float inR = haveRight ? buffer.getSample (1, s) : inL;
|
||||||
|
|
||||||
|
float outL = 0.0f;
|
||||||
|
float outR = 0.0f;
|
||||||
|
engine.processSample (inL, inR, outL, outR);
|
||||||
|
|
||||||
|
buffer.setSample (0, s, outL);
|
||||||
|
if (numOut > 1)
|
||||||
|
buffer.setSample (1, s, outR);
|
||||||
|
|
||||||
|
if (canCaptureWet)
|
||||||
|
{
|
||||||
|
wetScratch[(size_t) s] = engine.getLastWetL();
|
||||||
|
dryScratch[(size_t) s] = inL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canCaptureWet)
|
||||||
|
{
|
||||||
|
pushWetSamples (wetScratch.data(), numSamples);
|
||||||
|
appendDrySamples (dryScratch.data(), numSamples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::pushWetSamples (const float* src, int numToWrite)
|
||||||
|
{
|
||||||
|
if (numToWrite <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
numToWrite = juce::jmin (numToWrite, wetFifo.getFreeSpace());
|
||||||
|
if (numToWrite <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
auto handle = wetFifo.write (numToWrite);
|
||||||
|
handle.forEach ([&] (int idx) { wetRing.setSample (0, idx, src[i++]); });
|
||||||
|
}
|
||||||
|
|
||||||
|
int HorizontAudioProcessor::readWetSamples (float* dest, int numToRead)
|
||||||
|
{
|
||||||
|
const int avail = wetFifo.getNumReady();
|
||||||
|
if (avail <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
numToRead = juce::jmin (numToRead, avail);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
auto handle = wetFifo.read (numToRead);
|
||||||
|
handle.forEach ([&] (int idx) { dest[i++] = wetRing.getSample (0, idx); });
|
||||||
|
|
||||||
|
return numToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::appendDrySamples (const float* src, int numToWrite)
|
||||||
|
{
|
||||||
|
if (numToWrite <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t w = dryWritePos.load (std::memory_order_relaxed);
|
||||||
|
for (int i = 0; i < numToWrite; ++i)
|
||||||
|
{
|
||||||
|
dryRingLive[w] = src[i];
|
||||||
|
w = (w + 1) % (size_t) dryRingSize;
|
||||||
|
}
|
||||||
|
dryWritePos.store (w, std::memory_order_release);
|
||||||
|
dryTotalWritten.fetch_add ((size_t) numToWrite, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
int HorizontAudioProcessor::copyRecentDrySamples (float* dest, int numToRead)
|
||||||
|
{
|
||||||
|
numToRead = juce::jmin (numToRead, dryRingSize);
|
||||||
|
|
||||||
|
const size_t written = dryTotalWritten.load (std::memory_order_acquire);
|
||||||
|
const size_t wp = dryWritePos.load (std::memory_order_acquire);
|
||||||
|
|
||||||
|
if (written < (size_t) numToRead)
|
||||||
|
{
|
||||||
|
const int avail = (int) written;
|
||||||
|
const int lead = numToRead - avail;
|
||||||
|
std::fill (dest, dest + lead, 0.0f);
|
||||||
|
for (int i = 0; i < avail; ++i)
|
||||||
|
{
|
||||||
|
const size_t idx = (wp + (size_t) dryRingSize - (size_t) avail + (size_t) i) % (size_t) dryRingSize;
|
||||||
|
dest[lead + i] = dryRingLive[idx];
|
||||||
|
}
|
||||||
|
return numToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < numToRead; ++i)
|
||||||
|
{
|
||||||
|
const size_t idx = (wp + (size_t) dryRingSize - (size_t) numToRead + (size_t) i) % (size_t) dryRingSize;
|
||||||
|
dest[i] = dryRingLive[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
return numToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessorEditor* HorizontAudioProcessor::createEditor()
|
||||||
|
{
|
||||||
|
return new HorizontAudioProcessorEditor (*this, apvts);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HorizontAudioProcessor::hasEditor() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::String HorizontAudioProcessor::getName() const
|
||||||
|
{
|
||||||
|
return "Horizont";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HorizontAudioProcessor::acceptsMidi() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HorizontAudioProcessor::producesMidi() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double HorizontAudioProcessor::getTailLengthSeconds() const
|
||||||
|
{
|
||||||
|
return 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int HorizontAudioProcessor::getNumPrograms()
|
||||||
|
{
|
||||||
|
return (int) presets.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int HorizontAudioProcessor::getCurrentProgram()
|
||||||
|
{
|
||||||
|
return currentProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::setCurrentProgram (int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < (int) presets.size())
|
||||||
|
{
|
||||||
|
currentProgram = index;
|
||||||
|
applyPreset (presets[(size_t) index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::String HorizontAudioProcessor::getProgramName (int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < (int) presets.size())
|
||||||
|
return presets[(size_t) index].name;
|
||||||
|
|
||||||
|
return "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::changeProgramName (int, const juce::String&)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::applyPreset (const Preset& preset)
|
||||||
|
{
|
||||||
|
for (const auto& v : preset.values)
|
||||||
|
{
|
||||||
|
if (auto* param = apvts.getParameter (v.id))
|
||||||
|
param->setValueNotifyingHost (param->convertTo0to1 (v.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||||
|
{
|
||||||
|
auto state = apvts.copyState();
|
||||||
|
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
||||||
|
copyXmlToBinary (*xml, destData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||||
|
{
|
||||||
|
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||||
|
|
||||||
|
if (xml != nullptr)
|
||||||
|
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
||||||
|
|
||||||
|
updateParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||||
|
{
|
||||||
|
return new HorizontAudioProcessor();
|
||||||
|
}
|
||||||
81
Source/PluginProcessor.h
Normal file
81
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "Presets.h"
|
||||||
|
#include "HorizontEngine.h"
|
||||||
|
|
||||||
|
class HorizontAudioProcessor : public juce::AudioProcessor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HorizontAudioProcessor();
|
||||||
|
~HorizontAudioProcessor() override;
|
||||||
|
|
||||||
|
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||||
|
void releaseResources() override;
|
||||||
|
|
||||||
|
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
|
||||||
|
|
||||||
|
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||||
|
|
||||||
|
juce::AudioProcessorEditor* createEditor() override;
|
||||||
|
bool hasEditor() const override;
|
||||||
|
|
||||||
|
const juce::String getName() const override;
|
||||||
|
bool acceptsMidi() const override;
|
||||||
|
bool producesMidi() const override;
|
||||||
|
double getTailLengthSeconds() const override;
|
||||||
|
|
||||||
|
int getNumPrograms() override;
|
||||||
|
int getCurrentProgram() override;
|
||||||
|
void setCurrentProgram (int index) override;
|
||||||
|
const juce::String getProgramName (int index) override;
|
||||||
|
void changeProgramName (int index, const juce::String& newName) override;
|
||||||
|
|
||||||
|
void getStateInformation (juce::MemoryBlock& destData) override;
|
||||||
|
void setStateInformation (const void* data, int sizeInBytes) override;
|
||||||
|
|
||||||
|
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||||
|
|
||||||
|
void applyPreset (const Preset& preset);
|
||||||
|
const std::vector<Preset>& getPresets() const noexcept { return presets; }
|
||||||
|
|
||||||
|
void pushWetSamples (const float* data, int numToWrite);
|
||||||
|
int readWetSamples (float* dest, int numToRead);
|
||||||
|
|
||||||
|
void appendDrySamples (const float* data, int numToWrite);
|
||||||
|
int copyRecentDrySamples (float* dest, int numToRead);
|
||||||
|
|
||||||
|
juce::AudioProcessorValueTreeState apvts;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateParameters();
|
||||||
|
|
||||||
|
HorizontEngine engine;
|
||||||
|
std::vector<Preset> presets;
|
||||||
|
int currentProgram = 0;
|
||||||
|
|
||||||
|
juce::AbstractFifo wetFifo { 2048 };
|
||||||
|
juce::AudioBuffer<float> wetRing { 1, 2048 };
|
||||||
|
std::vector<float> wetScratch;
|
||||||
|
|
||||||
|
std::vector<float> dryScratch;
|
||||||
|
static constexpr int dryRingSize = 4096;
|
||||||
|
std::vector<float> dryRingLive = std::vector<float> (dryRingSize, 0.0f);
|
||||||
|
std::atomic<size_t> dryWritePos { 0 };
|
||||||
|
std::atomic<size_t> dryTotalWritten { 0 };
|
||||||
|
|
||||||
|
std::atomic<float>* lowCutParam = nullptr;
|
||||||
|
std::atomic<float>* highCutParam = nullptr;
|
||||||
|
std::atomic<float>* decayParam = nullptr;
|
||||||
|
std::atomic<float>* sizeParam = nullptr;
|
||||||
|
std::atomic<float>* diffusionParam = nullptr;
|
||||||
|
std::atomic<float>* brightnessParam = nullptr;
|
||||||
|
std::atomic<float>* feedbackParam = nullptr;
|
||||||
|
std::atomic<float>* pitchParam = nullptr;
|
||||||
|
std::atomic<float>* dryWetParam = nullptr;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HorizontAudioProcessor)
|
||||||
|
};
|
||||||
54
Source/Presets.h
Normal file
54
Source/Presets.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Preset
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
|
||||||
|
struct ParamValue
|
||||||
|
{
|
||||||
|
const char* id;
|
||||||
|
float value;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ParamValue> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const std::vector<Preset>& getPresets()
|
||||||
|
{
|
||||||
|
static const std::vector<Preset> presets =
|
||||||
|
{
|
||||||
|
{ "Studio Plate",
|
||||||
|
{ { "lowCut", 30.0f }, { "highCut", 16000.0f }, { "decay", 2.5f },
|
||||||
|
{ "size", 0.75f }, { "diffusion", 0.70f }, { "brightness", 0.80f },
|
||||||
|
{ "feedback", 0.25f }, { "pitch", 0.0f }, { "dryWet", 0.40f } } },
|
||||||
|
|
||||||
|
{ "Warm Hall",
|
||||||
|
{ { "lowCut", 60.0f }, { "highCut", 8000.0f }, { "decay", 4.5f },
|
||||||
|
{ "size", 1.25f }, { "diffusion", 0.60f }, { "brightness", 0.45f },
|
||||||
|
{ "feedback", 0.50f }, { "pitch", 0.0f }, { "dryWet", 0.45f } } },
|
||||||
|
|
||||||
|
{ "Big Cathedral",
|
||||||
|
{ { "lowCut", 40.0f }, { "highCut", 12000.0f }, { "decay", 8.5f },
|
||||||
|
{ "size", 1.9f }, { "diffusion", 0.75f }, { "brightness", 0.60f },
|
||||||
|
{ "feedback", 0.70f }, { "pitch", 0.0f }, { "dryWet", 0.55f } } },
|
||||||
|
|
||||||
|
{ "Shimmer Space",
|
||||||
|
{ { "lowCut", 45.0f }, { "highCut", 15000.0f }, { "decay", 6.0f },
|
||||||
|
{ "size", 1.5f }, { "diffusion", 0.65f }, { "brightness", 0.70f },
|
||||||
|
{ "feedback", 0.90f }, { "pitch", 7.0f }, { "dryWet", 0.60f } } },
|
||||||
|
|
||||||
|
{ "Dark Pulse",
|
||||||
|
{ { "lowCut", 80.0f }, { "highCut", 7000.0f }, { "decay", 3.0f },
|
||||||
|
{ "size", 0.9f }, { "diffusion", 0.50f }, { "brightness", 0.35f },
|
||||||
|
{ "feedback", 0.85f }, { "pitch", -5.0f }, { "dryWet", 0.50f } } },
|
||||||
|
|
||||||
|
{ "Tight Room",
|
||||||
|
{ { "lowCut", 90.0f }, { "highCut", 16000.0f }, { "decay", 0.6f },
|
||||||
|
{ "size", 0.5f }, { "diffusion", 0.55f }, { "brightness", 0.85f },
|
||||||
|
{ "feedback", 0.20f }, { "pitch", 0.0f }, { "dryWet", 0.30f } } }
|
||||||
|
};
|
||||||
|
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
273
Source/SpectrumDisplay.cpp
Normal file
273
Source/SpectrumDisplay.cpp
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
#include "SpectrumDisplay.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr float minDb = -66.0f;
|
||||||
|
constexpr float topDb = -6.0f;
|
||||||
|
constexpr float dbRange = topDb - minDb;
|
||||||
|
|
||||||
|
const juce::Colour wetLineCol (0xDD9DF5E9);
|
||||||
|
const juce::Colour wetFillCol (0xE63CE0C8);
|
||||||
|
const juce::Colour wetPeakCol (0x883CE0C8);
|
||||||
|
const juce::Colour dryLineCol (0xCCF0A03C);
|
||||||
|
const juce::Colour dryFillCol (0x22F0A03C);
|
||||||
|
const juce::Colour dryPeakCol (0x55E89B3A);
|
||||||
|
|
||||||
|
juce::String formatFreq (float hz)
|
||||||
|
{
|
||||||
|
if (hz >= 1000.0f)
|
||||||
|
return juce::String (juce::roundToInt (hz / 1000.0f)) + "k";
|
||||||
|
return juce::String (juce::roundToInt (hz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpectrumDisplay::SpectrumDisplay (HorizontAudioProcessor& p)
|
||||||
|
: processor (p)
|
||||||
|
{
|
||||||
|
window.resize ((size_t) fftSize);
|
||||||
|
|
||||||
|
for (int i = 0; i < fftSize; ++i)
|
||||||
|
window[(size_t) i] = 0.5f * (1.0f - std::cos (2.0f * juce::MathConstants<float>::pi * (float) i / (float) (fftSize - 1)));
|
||||||
|
|
||||||
|
startTimerHz (30);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpectrumDisplay::~SpectrumDisplay()
|
||||||
|
{
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpectrumDisplay::setScaleFactor (float scale)
|
||||||
|
{
|
||||||
|
scaleFactor = juce::jlimit (0.6f, 2.0f, scale);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpectrumDisplay::computeLevels (const float* samples, int got, std::array<float, numBins>& out)
|
||||||
|
{
|
||||||
|
for (auto& l : out)
|
||||||
|
l = -200.0f;
|
||||||
|
|
||||||
|
if (got <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < got; ++i)
|
||||||
|
fftData[(size_t) i] = samples[i] * window[(size_t) i];
|
||||||
|
for (int i = got; i < fftSize; ++i)
|
||||||
|
fftData[(size_t) i] = 0.0f;
|
||||||
|
|
||||||
|
std::fill (fftData.begin() + fftSize, fftData.end(), 0.0f);
|
||||||
|
fft.performRealOnlyForwardTransform (fftData.data(), true);
|
||||||
|
|
||||||
|
const float logNyquist = std::log ((float) (fftSize / 2));
|
||||||
|
const float binScale = 2.0f / (float) fftSize;
|
||||||
|
|
||||||
|
for (int i = 0; i < numBins; ++i)
|
||||||
|
{
|
||||||
|
const int kLo = jmax (1, (int) std::floor (std::exp (logNyquist * (float) i / (float) numBins)));
|
||||||
|
const int kHi = jmin (fftSize / 2 - 1, (int) std::ceil (std::exp (logNyquist * (float) (i + 1) / (float) numBins)));
|
||||||
|
|
||||||
|
for (int k = kLo; k <= kHi; ++k)
|
||||||
|
{
|
||||||
|
const float re = fftData[(size_t) (2 * k)];
|
||||||
|
const float im = fftData[(size_t) (2 * k + 1)];
|
||||||
|
const float mag = std::sqrt (re * re + im * im) * binScale;
|
||||||
|
const float db = 20.0f * std::log10 (mag + 1e-6f);
|
||||||
|
|
||||||
|
out[(size_t) i] = jmax (out[(size_t) i], db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpectrumDisplay::timerCallback()
|
||||||
|
{
|
||||||
|
juce::AudioBuffer<float> wetBuf (1, fftSize);
|
||||||
|
juce::AudioBuffer<float> dryBuf (1, fftSize);
|
||||||
|
const int got = processor.readWetSamples (wetBuf.getWritePointer (0), fftSize);
|
||||||
|
processor.copyRecentDrySamples (dryBuf.getWritePointer (0), fftSize);
|
||||||
|
|
||||||
|
computeLevels (wetBuf.getReadPointer (0), got, level);
|
||||||
|
computeLevels (dryBuf.getReadPointer (0), fftSize, dryLevel);
|
||||||
|
|
||||||
|
for (int i = 0; i < numBins; ++i)
|
||||||
|
{
|
||||||
|
const int lo = jmax (0, i - 1);
|
||||||
|
const int hi = jmin (numBins - 1, i + 1);
|
||||||
|
|
||||||
|
const float wetSmooth = 0.25f * level[(size_t) lo] + 0.5f * level[(size_t) i] + 0.25f * level[(size_t) hi];
|
||||||
|
const float wetNorm = (juce::jlimit (minDb, topDb, wetSmooth) - minDb) / dbRange;
|
||||||
|
float& wd = display[(size_t) i];
|
||||||
|
wd += (wetNorm - wd) * (wetNorm > wd ? 0.45f : 0.15f);
|
||||||
|
peak[(size_t) i] = jmax (peak[(size_t) i] * 0.93f, display[(size_t) i]);
|
||||||
|
|
||||||
|
const float drySmooth = 0.25f * dryLevel[(size_t) lo] + 0.5f * dryLevel[(size_t) i] + 0.25f * dryLevel[(size_t) hi];
|
||||||
|
const float dryNorm = (juce::jlimit (minDb, topDb, drySmooth) - minDb) / dbRange;
|
||||||
|
float& dd = dryDisplay[(size_t) i];
|
||||||
|
dd += (dryNorm - dd) * (dryNorm > dd ? 0.45f : 0.15f);
|
||||||
|
dryPeak[(size_t) i] = jmax (dryPeak[(size_t) i] * 0.93f, dryDisplay[(size_t) i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::Path SpectrumDisplay::makeCurve (const juce::Rectangle<float>& graph, const std::array<float, numBins>& data) const
|
||||||
|
{
|
||||||
|
juce::Path p;
|
||||||
|
for (int i = 0; i < numBins; ++i)
|
||||||
|
{
|
||||||
|
const float x = graph.getX() + graph.getWidth() * (float) (i + 0.5f) / (float) numBins;
|
||||||
|
const float y = graph.getBottom() - data[(size_t) i] * graph.getHeight();
|
||||||
|
if (i == 0)
|
||||||
|
p.startNewSubPath (x, y);
|
||||||
|
else
|
||||||
|
p.lineTo (x, y);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
void SpectrumDisplay::paint (juce::Graphics& g)
|
||||||
|
{
|
||||||
|
const auto bounds = getLocalBounds().toFloat();
|
||||||
|
const float s = scaleFactor;
|
||||||
|
|
||||||
|
auto shRect = bounds.expanded (6.0f * s, 4.0f * s).translated (0.0f, 5.0f * s);
|
||||||
|
auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() },
|
||||||
|
juce::Colour (0x00000000), { 0.0f, shRect.getBottom() },
|
||||||
|
false);
|
||||||
|
shadowGrad.addColour (0.45f, juce::Colour (0x70000000));
|
||||||
|
g.setGradientFill (shadowGrad);
|
||||||
|
g.fillRoundedRectangle (shRect, 14.0f * s);
|
||||||
|
|
||||||
|
auto bodyGrad = juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, bounds.getY() },
|
||||||
|
juce::Colour (0xFF0B1216), { 0.0f, bounds.getBottom() },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (bodyGrad);
|
||||||
|
g.fillRoundedRectangle (bounds, 12.0f * s);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x3A3CE0C8));
|
||||||
|
g.drawRoundedRectangle (bounds.expanded (1.0f * s), 13.0f * s, 1.0f * s);
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x38FFFFFF));
|
||||||
|
g.drawHorizontalLine (juce::roundToInt (bounds.getY() + 1.0f * s), juce::roundToInt (bounds.getX() + 3.0f * s), juce::roundToInt (bounds.getRight() - 3.0f * s));
|
||||||
|
g.drawVerticalLine (juce::roundToInt (bounds.getX() + 1.0f * s), juce::roundToInt (bounds.getY() + 3.0f * s), juce::roundToInt (bounds.getBottom() - 3.0f * s));
|
||||||
|
g.setColour (juce::Colour (0x50000000));
|
||||||
|
g.drawHorizontalLine (juce::roundToInt (bounds.getBottom() - 1.0f * s), juce::roundToInt (bounds.getX() + 3.0f * s), juce::roundToInt (bounds.getRight() - 3.0f * s));
|
||||||
|
g.drawVerticalLine (juce::roundToInt (bounds.getRight() - 1.0f * s), juce::roundToInt (bounds.getY() + 3.0f * s), juce::roundToInt (bounds.getBottom() - 3.0f * s));
|
||||||
|
|
||||||
|
g.saveState();
|
||||||
|
g.reduceClipRegion (bounds.toNearestInt().reduced (juce::roundToInt (1.0f * s)));
|
||||||
|
auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, bounds.getY() },
|
||||||
|
juce::Colour (0x00FFFFFF), { 0.0f, bounds.getY() + bounds.getHeight() * 0.45f },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (refl);
|
||||||
|
g.fillRect (bounds);
|
||||||
|
g.setColour (juce::Colour (0x45FFFFFF));
|
||||||
|
g.fillRect (bounds.getX() + 2.0f * s, bounds.getY() + 1.0f * s, bounds.getWidth() - 4.0f * s, 1.0f * s);
|
||||||
|
g.restoreState();
|
||||||
|
|
||||||
|
const auto area = bounds.reduced (14.0f * s, 8.0f * s);
|
||||||
|
|
||||||
|
const float dbAxisW = 30.0f * s;
|
||||||
|
const float graphAreaBottom = area.getBottom() - 16.0f * s;
|
||||||
|
const float graphH = graphAreaBottom - area.getY();
|
||||||
|
const auto graph = area.withHeight (graphH).withLeft (area.getX() + dbAxisW);
|
||||||
|
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f * s)));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
const float db = -6.0f - (float) i * 15.0f;
|
||||||
|
const float y = graph.getBottom() - ((db - minDb) / dbRange) * graph.getHeight();
|
||||||
|
g.setColour (juce::Colour (0x223A4A4D));
|
||||||
|
g.drawHorizontalLine (juce::roundToInt (y), juce::roundToInt (graph.getX()), juce::roundToInt (graph.getRight()));
|
||||||
|
g.setColour (juce::Colour (0x55FFFFFF));
|
||||||
|
g.drawText (juce::String (juce::roundToInt (db)), juce::Rectangle<float> (graph.getX() - dbAxisW, y - 6.0f * s, dbAxisW - 6.0f * s, 12.0f * s),
|
||||||
|
juce::Justification::right, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float sr = juce::jmax (1000.0f, (float) processor.getSampleRate());
|
||||||
|
const float logNyquist = std::log ((float) (fftSize / 2));
|
||||||
|
|
||||||
|
const float freqs[] = { 50.0f, 100.0f, 200.0f, 500.0f, 1000.0f, 2000.0f, 5000.0f, 10000.0f, 20000.0f };
|
||||||
|
for (float f : freqs)
|
||||||
|
{
|
||||||
|
const float k = f * (float) fftSize / sr;
|
||||||
|
if (k < 1.0f || k > (float) (fftSize / 2))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const float x = graph.getX() + (std::log (k) / logNyquist) * graph.getWidth();
|
||||||
|
g.setColour (juce::Colour (0x223A4A4D));
|
||||||
|
g.drawVerticalLine (juce::roundToInt (x), juce::roundToInt (graph.getY()), juce::roundToInt (graph.getBottom()));
|
||||||
|
g.setColour (juce::Colour (0x55FFFFFF));
|
||||||
|
g.drawText (formatFreq (f), juce::Rectangle<float> (x - 14.0f * s, graph.getBottom() + 3.0f * s, 28.0f * s, 12.0f * s),
|
||||||
|
juce::Justification::centred, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x33FFFFFF));
|
||||||
|
g.drawRect (graph, 0.6f * s);
|
||||||
|
|
||||||
|
if (graph.getWidth() > 2.0f * s && graph.getHeight() > 2.0f * s)
|
||||||
|
{
|
||||||
|
auto curve = makeCurve (graph, display);
|
||||||
|
auto dry = makeCurve (graph, dryDisplay);
|
||||||
|
|
||||||
|
auto wetFill = juce::Path (curve);
|
||||||
|
wetFill.lineTo (graph.getRight(), graph.getBottom());
|
||||||
|
wetFill.lineTo (graph.getX(), graph.getBottom());
|
||||||
|
wetFill.closeSubPath();
|
||||||
|
|
||||||
|
auto dryFill = juce::Path (dry);
|
||||||
|
dryFill.lineTo (graph.getRight(), graph.getBottom());
|
||||||
|
dryFill.lineTo (graph.getX(), graph.getBottom());
|
||||||
|
dryFill.closeSubPath();
|
||||||
|
|
||||||
|
auto wetGrad = juce::ColourGradient (wetFillCol, { 0.0f, graph.getY() },
|
||||||
|
juce::Colour (0x063CE0C8), { 0.0f, graph.getBottom() },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (wetGrad);
|
||||||
|
g.fillPath (wetFill);
|
||||||
|
|
||||||
|
auto dryGrad = juce::ColourGradient (dryFillCol, { 0.0f, graph.getY() },
|
||||||
|
juce::Colour (0x00FFFFFF), { 0.0f, graph.getBottom() },
|
||||||
|
false);
|
||||||
|
g.setGradientFill (dryGrad);
|
||||||
|
g.fillPath (dryFill);
|
||||||
|
|
||||||
|
g.setColour (wetPeakCol);
|
||||||
|
g.strokePath (makeCurve (graph, peak), juce::PathStrokeType (1.0f * s));
|
||||||
|
|
||||||
|
g.setColour (dryLineCol);
|
||||||
|
g.strokePath (dry, juce::PathStrokeType (1.2f * s,
|
||||||
|
juce::PathStrokeType::JointStyle::curved,
|
||||||
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||||||
|
|
||||||
|
g.setColour (dryPeakCol);
|
||||||
|
g.strokePath (makeCurve (graph, dryPeak), juce::PathStrokeType (0.8f * s));
|
||||||
|
|
||||||
|
g.setColour (wetLineCol);
|
||||||
|
g.strokePath (curve, juce::PathStrokeType (1.4f * s,
|
||||||
|
juce::PathStrokeType::JointStyle::curved,
|
||||||
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour (juce::Colour (0x66FFFFFF));
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (10.0f * s, juce::Font::bold)));
|
||||||
|
g.drawText ("REVERB SPECTRUM", graph.withTop (graph.getY() - 2.0f * s).withBottom (graph.getY() + 14.0f * s),
|
||||||
|
juce::Justification::bottomLeft, false);
|
||||||
|
|
||||||
|
const float legendY = graph.getY() + 4.0f * s;
|
||||||
|
const float legendX = graph.getRight() - 2.0f * s;
|
||||||
|
const auto legendRow = juce::Rectangle<float> (legendX - 84.0f * s, legendY - 9.0f * s, 84.0f * s, 12.0f * s);
|
||||||
|
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f * s)));
|
||||||
|
g.setColour (wetPeakCol);
|
||||||
|
g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX()), juce::roundToInt (legendRow.getX() + 11.0f * s));
|
||||||
|
g.setColour (juce::Colour (0x88FFFFFF));
|
||||||
|
g.drawText ("WET", legendRow.withX (legendRow.getX() + 14.0f * s).withWidth (28.0f * s),
|
||||||
|
juce::Justification::left, false);
|
||||||
|
|
||||||
|
g.setColour (dryPeakCol);
|
||||||
|
g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX() + 42.0f * s), juce::roundToInt (legendRow.getX() + 53.0f * s));
|
||||||
|
g.setColour (juce::Colour (0x88FFFFFF));
|
||||||
|
g.drawText ("DRY", legendRow.withX (legendRow.getX() + 56.0f * s).withWidth (28.0f * s),
|
||||||
|
juce::Justification::left, false);
|
||||||
|
}
|
||||||
43
Source/SpectrumDisplay.h
Normal file
43
Source/SpectrumDisplay.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "PluginProcessor.h"
|
||||||
|
|
||||||
|
class SpectrumDisplay : public juce::Component, private juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SpectrumDisplay (HorizontAudioProcessor&);
|
||||||
|
~SpectrumDisplay() override;
|
||||||
|
|
||||||
|
void paint (juce::Graphics&) override;
|
||||||
|
void setScaleFactor (float scale);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void timerCallback() override;
|
||||||
|
|
||||||
|
HorizontAudioProcessor& processor;
|
||||||
|
|
||||||
|
static constexpr int fftOrder = 10;
|
||||||
|
static constexpr int fftSize = 1 << fftOrder;
|
||||||
|
static constexpr int numBins = 48;
|
||||||
|
|
||||||
|
void computeLevels (const float* samples, int got, std::array<float, numBins>& out);
|
||||||
|
juce::Path makeCurve (const juce::Rectangle<float>& graph, const std::array<float, numBins>& data) const;
|
||||||
|
|
||||||
|
juce::dsp::FFT fft { fftOrder };
|
||||||
|
std::vector<float> fftData = std::vector<float> (2 * fftSize, 0.0f);
|
||||||
|
std::vector<float> window;
|
||||||
|
std::array<float, numBins> level {};
|
||||||
|
std::array<float, numBins> display {};
|
||||||
|
std::array<float, numBins> peak {};
|
||||||
|
std::array<float, numBins> dryLevel {};
|
||||||
|
std::array<float, numBins> dryDisplay {};
|
||||||
|
std::array<float, numBins> dryPeak {};
|
||||||
|
|
||||||
|
float scaleFactor = 1.0f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SpectrumDisplay)
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue