mirror of
https://codeberg.org/armin/mindball.git
synced 2026-09-01 03:40:49 +02:00
init
This commit is contained in:
parent
1b541ba587
commit
78e52e612f
9 changed files with 1553 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Build directories
|
||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# CMake generated files
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
cmake_install.cmake
|
||||||
|
CMakeUserPresets.json
|
||||||
|
CMakeLists.txt.user
|
||||||
|
|
||||||
|
# IDE / editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.xcworkspace/
|
||||||
|
*.xcodeproj/
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Plugin build products
|
||||||
|
*.vst3/
|
||||||
|
*.component/
|
||||||
|
*.app/
|
||||||
|
*.aaxplugin/
|
||||||
|
*.lv2/
|
||||||
124
CMakeLists.txt
Normal file
124
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
project(Mindball VERSION 1.0.0 LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0 CACHE STRING "Minimum macOS deployment target")
|
||||||
|
|
||||||
|
if(CMAKE_BUILD_TYPE STREQUAL "" AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MINDBALL_FORMATS "AU VST3 Standalone" CACHE STRING "Plugin formats to build (space separated)")
|
||||||
|
separate_arguments(_formats UNIX_COMMAND "${MINDBALL_FORMATS}")
|
||||||
|
|
||||||
|
set(JUCE_ROOT "" CACHE PATH "Path to a local JUCE source tree (default: fetched from GitHub)")
|
||||||
|
if(JUCE_ROOT)
|
||||||
|
add_subdirectory(${JUCE_ROOT} juce)
|
||||||
|
else()
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(juce
|
||||||
|
GIT_REPOSITORY https://github.com/juce-framework/JUCE.git
|
||||||
|
GIT_TAG 8.0.9
|
||||||
|
GIT_SHALLOW TRUE)
|
||||||
|
FetchContent_MakeAvailable(juce)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# A JUCE GUI plugin needs the core system headers to compile on Linux. Detect
|
||||||
|
# them up front so a missing package produces a clear error instead of a
|
||||||
|
# confusing compile failure. GTK and libcurl are never required.
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
set(_mindball_headers "X11/Xlib.h" "freetype2/freetype/freetype.h"
|
||||||
|
"fontconfig/fontconfig.h" "alsa/asoundlib.h")
|
||||||
|
set(_mindball_packages "libx11-dev" "libfreetype-dev"
|
||||||
|
"libfontconfig-dev" "libasound2-dev")
|
||||||
|
set(_mindball_missing "")
|
||||||
|
|
||||||
|
foreach(_i RANGE 0 3)
|
||||||
|
list(GET _mindball_headers ${_i} _header)
|
||||||
|
list(GET _mindball_packages ${_i} _package)
|
||||||
|
find_path(_mindball_hdr_${_i} NAMES "${_header}" QUIET)
|
||||||
|
if(NOT _mindball_hdr_${_i})
|
||||||
|
string(APPEND _mindball_missing " ${_header} (package: ${_package})\n")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(_mindball_missing)
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"Missing required system headers for the Linux build:\n${_mindball_missing}"
|
||||||
|
"Install them with:\n"
|
||||||
|
" sudo apt-get install pkg-config libx11-dev libfreetype-dev libfontconfig-dev libasound2-dev\n")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
juce_add_plugin(Mindball
|
||||||
|
PRODUCT_NAME "Mindball"
|
||||||
|
VENDOR_NAME "Mindball"
|
||||||
|
VERSION "1.0.0"
|
||||||
|
DESCRIPTION "Mindball - a tiny, minimal delay plugin"
|
||||||
|
FORMATS ${_formats}
|
||||||
|
PLUGIN_MANUFACTURER_CODE Mind
|
||||||
|
PLUGIN_CODE Mbll
|
||||||
|
COPY_PLUGIN_AFTER_BUILD TRUE
|
||||||
|
COMPANY_COPYRIGHT "2026 Mindball")
|
||||||
|
|
||||||
|
target_sources(Mindball PRIVATE
|
||||||
|
Source/PluginProcessor.cpp
|
||||||
|
Source/PluginEditor.cpp)
|
||||||
|
|
||||||
|
target_include_directories(Mindball PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/buildinfo)
|
||||||
|
target_link_libraries(Mindball PRIVATE
|
||||||
|
juce::juce_audio_processors
|
||||||
|
juce::juce_audio_utils
|
||||||
|
juce::juce_audio_devices
|
||||||
|
juce::juce_gui_basics)
|
||||||
|
|
||||||
|
# New VST3-only plugin: no VST2 has ever been released, so the parameter-ID
|
||||||
|
# migration warning is irrelevant.
|
||||||
|
target_compile_definitions(Mindball PRIVATE JUCE_IGNORE_VST3_MISMATCHED_PARAMETER_ID_WARNING)
|
||||||
|
|
||||||
|
# juce_core enables libcurl by default on Linux; without NEEDS_CURL the JUCE
|
||||||
|
# helper targets don't link -lcurl, so that would fail at link time with
|
||||||
|
# undefined references to curl_easy_setopt. We use no network features, so
|
||||||
|
# disable it. (GTK/webkit2gtk is never required because juce_gui_extra is not
|
||||||
|
# linked.)
|
||||||
|
target_compile_definitions(Mindball PRIVATE JUCE_USE_CURL=0)
|
||||||
|
|
||||||
|
# ---- git / build info -----------------------------------------------------
|
||||||
|
find_package(Git QUIET)
|
||||||
|
set(MINDBALL_GIT_HASH "n/a")
|
||||||
|
set(MINDBALL_GIT_BRANCH "n/a")
|
||||||
|
set(MINDBALL_GIT_DIRTY false)
|
||||||
|
if(Git_FOUND)
|
||||||
|
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
RESULT_VARIABLE _git_hash_res
|
||||||
|
OUTPUT_VARIABLE _git_hash
|
||||||
|
ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
if(_git_hash_res EQUAL 0 AND _git_hash)
|
||||||
|
set(MINDBALL_GIT_HASH "${_git_hash}")
|
||||||
|
execute_process(COMMAND ${GIT_EXECUTABLE} branch --show-current
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
RESULT_VARIABLE _git_branch_res
|
||||||
|
OUTPUT_VARIABLE _git_branch
|
||||||
|
ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
if(_git_branch_res EQUAL 0 AND _git_branch)
|
||||||
|
set(MINDBALL_GIT_BRANCH "${_git_branch}")
|
||||||
|
endif()
|
||||||
|
execute_process(COMMAND ${GIT_EXECUTABLE} status --porcelain
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
RESULT_VARIABLE _git_status_res
|
||||||
|
OUTPUT_VARIABLE _git_status
|
||||||
|
ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
string(LENGTH "${_git_status}" _status_len)
|
||||||
|
if(_status_len GREATER 0)
|
||||||
|
set(MINDBALL_GIT_DIRTY true)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
string(TIMESTAMP MINDBALL_BUILD_TIME "%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Source/BuildInfo.h.in
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/buildinfo/BuildInfo.h @ONLY)
|
||||||
|
|
||||||
|
juce_generate_juce_header(Mindball)
|
||||||
12
Source/BuildInfo.h.in
Normal file
12
Source/BuildInfo.h.in
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace mindball
|
||||||
|
{
|
||||||
|
namespace BuildInfo
|
||||||
|
{
|
||||||
|
inline constexpr const char* gitHash = "@MINDBALL_GIT_HASH@";
|
||||||
|
inline constexpr const char* gitBranch = "@MINDBALL_GIT_BRANCH@";
|
||||||
|
inline constexpr bool gitDirty = @MINDBALL_GIT_DIRTY@;
|
||||||
|
inline constexpr const char* buildTime = "@MINDBALL_BUILD_TIME@";
|
||||||
|
}
|
||||||
|
}
|
||||||
282
Source/DelayEngine.h
Normal file
282
Source/DelayEngine.h
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DelayLine.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace mindball
|
||||||
|
{
|
||||||
|
|
||||||
|
class DelayEngine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Mode
|
||||||
|
{
|
||||||
|
normal = 0,
|
||||||
|
invert,
|
||||||
|
pingpong,
|
||||||
|
superpong
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Params
|
||||||
|
{
|
||||||
|
float feedback = 0.35f;
|
||||||
|
float loCutHz = 100.0f;
|
||||||
|
float hiCutHz = 12000.0f;
|
||||||
|
float dryWet = 0.4f;
|
||||||
|
float timeMs = 300.0f;
|
||||||
|
int timeSyncIndex = 0;
|
||||||
|
int mode = normal;
|
||||||
|
bool sync = false;
|
||||||
|
bool doubleTap = false;
|
||||||
|
bool center = false;
|
||||||
|
bool autoPan = false;
|
||||||
|
float panDepth = 1.0f;
|
||||||
|
double bpm = 120.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void prepare (double sampleRate, double maxDelaySeconds)
|
||||||
|
{
|
||||||
|
sr = static_cast<float> (sampleRate);
|
||||||
|
dl.prepare (static_cast<float> (maxDelaySeconds * sampleRate));
|
||||||
|
dr.prepare (static_cast<float> (maxDelaySeconds * sampleRate));
|
||||||
|
clear();
|
||||||
|
|
||||||
|
sFeedback = 0.0f;
|
||||||
|
sLoCut = 20.0f;
|
||||||
|
sHiCut = 18000.0f;
|
||||||
|
sDryWet = 0.0f;
|
||||||
|
head1 = 1.0f;
|
||||||
|
head2 = 1.0f;
|
||||||
|
blend = 0.0f;
|
||||||
|
lfoPhase = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
dl.clear();
|
||||||
|
dr.clear();
|
||||||
|
|
||||||
|
lpL.reset(); hpL.reset();
|
||||||
|
lpR.reset(); hpR.reset();
|
||||||
|
|
||||||
|
lfoPhase = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void process (const float* inL, const float* inR,
|
||||||
|
float* outL, float* outR,
|
||||||
|
int numSamples, const Params& p,
|
||||||
|
float* wetLOut = nullptr, float* wetROut = nullptr)
|
||||||
|
{
|
||||||
|
constexpr float pi = 3.14159265358979f;
|
||||||
|
|
||||||
|
const float kPar = 1.0f - std::exp (-1.0f / (0.02f * sr));
|
||||||
|
|
||||||
|
const float fadeInc = 1.0f / (0.012f * sr);
|
||||||
|
|
||||||
|
float targetTimeSamples;
|
||||||
|
if (p.sync)
|
||||||
|
{
|
||||||
|
const int idx = std::clamp (p.timeSyncIndex, 0, numDivisions - 1);
|
||||||
|
const double secondsPerBeat = 60.0 / std::max (1.0, p.bpm);
|
||||||
|
targetTimeSamples = static_cast<float> (divisionBeats[static_cast<std::size_t> (idx)]
|
||||||
|
* secondsPerBeat * sr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetTimeSamples = p.timeMs * 0.001f * sr;
|
||||||
|
}
|
||||||
|
targetTimeSamples = std::max (2.0f, targetTimeSamples);
|
||||||
|
|
||||||
|
const float targetFeedback = p.feedback;
|
||||||
|
const float targetLoCut = p.loCutHz;
|
||||||
|
const float targetHiCut = p.hiCutHz;
|
||||||
|
const float targetDryWet = p.dryWet;
|
||||||
|
|
||||||
|
const bool dbl = p.doubleTap;
|
||||||
|
const bool ctr = p.center;
|
||||||
|
const bool needB = dbl || (ctr && (p.mode == pingpong || p.mode == superpong));
|
||||||
|
|
||||||
|
const float lfoRate = p.autoPan ? (p.sync ? static_cast<float> (p.bpm / 240.0) : 0.25f) : 0.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
sFeedback += (targetFeedback - sFeedback) * kPar;
|
||||||
|
sLoCut += (targetLoCut - sLoCut) * kPar;
|
||||||
|
sHiCut += (targetHiCut - sHiCut) * kPar;
|
||||||
|
sDryWet += (targetDryWet - sDryWet) * kPar;
|
||||||
|
|
||||||
|
// Delay time changes are done with two stationary read heads and a
|
||||||
|
// short crossfade, never by gliding a read position. This avoids the
|
||||||
|
// pitch-bend "sped up / slowed down" artifacts that come from a
|
||||||
|
// moving read head. When the target moves, snap head2 to it and fade
|
||||||
|
// between the two fixed heads; once blended, head2 takes over.
|
||||||
|
if (blend == 0.0f && std::fabs (targetTimeSamples - head1) > 0.5f)
|
||||||
|
head2 = targetTimeSamples;
|
||||||
|
|
||||||
|
if (blend < 1.0f)
|
||||||
|
{
|
||||||
|
blend += fadeInc;
|
||||||
|
if (blend >= 1.0f)
|
||||||
|
{
|
||||||
|
head1 = head2;
|
||||||
|
blend = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lpL.setFc (sHiCut, sr); hpL.setFc (sLoCut, sr);
|
||||||
|
lpR.setFc (sHiCut, sr); hpR.setFc (sLoCut, sr);
|
||||||
|
|
||||||
|
const float h1L = dl.read (head1);
|
||||||
|
const float h2L = dl.read (head2);
|
||||||
|
const float h1R = dr.read (head1);
|
||||||
|
const float h2R = dr.read (head2);
|
||||||
|
const float aL = h1L + (h2L - h1L) * blend;
|
||||||
|
const float aR = h1R + (h2R - h1R) * blend;
|
||||||
|
|
||||||
|
float bL = 0.0f, bR = 0.0f;
|
||||||
|
if (needB)
|
||||||
|
{
|
||||||
|
const float two1 = 2.0f * head1;
|
||||||
|
const float two2 = 2.0f * head2;
|
||||||
|
bL = dl.read (two1) + (dl.read (two2) - dl.read (two1)) * blend;
|
||||||
|
bR = dr.read (two1) + (dr.read (two2) - dr.read (two1)) * blend;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbL = 0.0f, fbR = 0.0f;
|
||||||
|
float wetL = 0.0f, wetR = 0.0f;
|
||||||
|
|
||||||
|
switch (p.mode)
|
||||||
|
{
|
||||||
|
case normal:
|
||||||
|
fbL = aL;
|
||||||
|
fbR = aR;
|
||||||
|
wetL = aL + bL;
|
||||||
|
wetR = aR + bR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case invert:
|
||||||
|
fbL = aL;
|
||||||
|
fbR = -aR;
|
||||||
|
wetL = aL + bL;
|
||||||
|
wetR = -(aR + bR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case pingpong:
|
||||||
|
fbL = aR;
|
||||||
|
fbR = aL;
|
||||||
|
wetL = aL + bL;
|
||||||
|
wetR = aR + bR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case superpong:
|
||||||
|
{
|
||||||
|
const float sumA = aL + aR;
|
||||||
|
const float sumAB = sumA + (needB ? bL + bR : 0.0f);
|
||||||
|
fbL = 0.5f * sumA;
|
||||||
|
fbR = 0.5f * sumA;
|
||||||
|
wetL = sumAB * 0.5f;
|
||||||
|
wetR = sumAB * 0.5f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctr && (p.mode == pingpong || p.mode == superpong))
|
||||||
|
{
|
||||||
|
const float c = 0.5f * (aL + aR);
|
||||||
|
wetL = c + (needB ? bL : 0.0f);
|
||||||
|
wetR = c + (needB ? bR : 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.autoPan)
|
||||||
|
{
|
||||||
|
const float pan = std::sin (2.0f * pi * lfoPhase);
|
||||||
|
lfoPhase += lfoRate / sr;
|
||||||
|
if (lfoPhase >= 1.0f)
|
||||||
|
lfoPhase -= std::floor (lfoPhase);
|
||||||
|
|
||||||
|
const float s = std::clamp (p.panDepth, 0.0f, 1.0f);
|
||||||
|
const float angle = (1.0f - s) * pi * 0.25f + s * (pan + 1.0f) * pi * 0.25f;
|
||||||
|
const float gL = std::cos (angle);
|
||||||
|
const float gR = std::sin (angle);
|
||||||
|
wetL *= gL;
|
||||||
|
wetR *= gR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float g = sFeedback * (1.0f + 0.002f * std::pow (sFeedback, 100.0f));
|
||||||
|
dl.write (inL[i] + softClip (lpL.processLP (hpL.processHP (fbL * g))));
|
||||||
|
dr.write (inR[i] + softClip (lpR.processLP (hpR.processHP (fbR * g))));
|
||||||
|
|
||||||
|
const float w = sDryWet;
|
||||||
|
float dryGain, wetGain;
|
||||||
|
if (w <= 0.5f)
|
||||||
|
{
|
||||||
|
dryGain = 1.0f;
|
||||||
|
wetGain = 2.0f * w;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dryGain = 2.0f * (1.0f - w);
|
||||||
|
wetGain = 1.0f;
|
||||||
|
}
|
||||||
|
outL[i] = inL[i] * dryGain + wetL * wetGain;
|
||||||
|
outR[i] = inR[i] * dryGain + wetR * wetGain;
|
||||||
|
if (wetLOut != nullptr)
|
||||||
|
wetLOut[i] = wetL * wetGain;
|
||||||
|
if (wetROut != nullptr)
|
||||||
|
wetROut[i] = wetR * wetGain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static float softClip (float x)
|
||||||
|
{
|
||||||
|
const float ax = std::fabs (x);
|
||||||
|
if (ax < 0.8f)
|
||||||
|
return x;
|
||||||
|
return std::copysign (0.8f + 0.2f * std::tanh ((ax - 0.8f) / 0.2f), x);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OnePole
|
||||||
|
{
|
||||||
|
float alpha = 0.0f;
|
||||||
|
float state = 0.0f;
|
||||||
|
float xPrev = 0.0f;
|
||||||
|
|
||||||
|
void reset() { state = 0.0f; xPrev = 0.0f; }
|
||||||
|
|
||||||
|
void setFc (float fc, float sr)
|
||||||
|
{
|
||||||
|
constexpr float pi = 3.14159265358979f;
|
||||||
|
alpha = 1.0f - std::exp (-2.0f * pi * fc / sr);
|
||||||
|
}
|
||||||
|
|
||||||
|
float processLP (float x) { state += alpha * (x - state); return state; }
|
||||||
|
float processHP (float x) { state = x - xPrev + (1.0f - alpha) * state; xPrev = x; return state; }
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int numDivisions = 12;
|
||||||
|
static constexpr std::array<float, numDivisions> divisionBeats = {
|
||||||
|
1.0f / 16.0f, 1.0f / 8.0f, 1.0f / 4.0f, 1.0f / 3.0f, 1.0f / 2.0f, 3.0f / 4.0f,
|
||||||
|
1.0f, 3.0f / 2.0f, 2.0f, 3.0f, 4.0f, 8.0f
|
||||||
|
};
|
||||||
|
|
||||||
|
DelayLine dl, dr;
|
||||||
|
OnePole lpL, hpL, lpR, hpR;
|
||||||
|
|
||||||
|
float sr = 48000.0f;
|
||||||
|
float lfoPhase = 0.0f;
|
||||||
|
|
||||||
|
float sFeedback = 0.0f;
|
||||||
|
float sLoCut = 0.0f;
|
||||||
|
float sHiCut = 0.0f;
|
||||||
|
float sDryWet = 0.0f;
|
||||||
|
float head1 = 1.0f;
|
||||||
|
float head2 = 1.0f;
|
||||||
|
float blend = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
59
Source/DelayLine.h
Normal file
59
Source/DelayLine.h
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mindball
|
||||||
|
{
|
||||||
|
|
||||||
|
// Simple fractional-delay ring buffer with linear interpolation.
|
||||||
|
class DelayLine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (float maxDelaySamples)
|
||||||
|
{
|
||||||
|
maxDelay = std::max (1.0f, maxDelaySamples);
|
||||||
|
buffer.assign (static_cast<std::size_t> (maxDelay) + 2, 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
std::fill (buffer.begin(), buffer.end(), 0.0f);
|
||||||
|
writePos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write (float sample)
|
||||||
|
{
|
||||||
|
buffer[static_cast<std::size_t> (writePos)] = sample;
|
||||||
|
writePos = (writePos + 1) % static_cast<int> (buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
float read (float delaySamples) const
|
||||||
|
{
|
||||||
|
if (buffer.empty())
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
|
const float d = std::max (1.0f, std::min (delaySamples, maxDelay - 1.0f));
|
||||||
|
const int size = static_cast<int> (buffer.size());
|
||||||
|
const float readPos = static_cast<float> (writePos) - d;
|
||||||
|
const float floorPos = std::floor (readPos);
|
||||||
|
|
||||||
|
int i0 = static_cast<int> (floorPos) % size;
|
||||||
|
if (i0 < 0)
|
||||||
|
i0 += size;
|
||||||
|
|
||||||
|
const int i1 = (i0 + 1) % size;
|
||||||
|
const float frac = readPos - floorPos;
|
||||||
|
return buffer[static_cast<std::size_t> (i0)]
|
||||||
|
+ (buffer[static_cast<std::size_t> (i1)] - buffer[static_cast<std::size_t> (i0)]) * frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<float> buffer;
|
||||||
|
float maxDelay = 1.0f;
|
||||||
|
int writePos = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
320
Source/PluginEditor.cpp
Normal file
320
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
#include "PluginEditor.h"
|
||||||
|
#include "BuildInfo.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
class UI final : public juce::Component, public juce::Timer, private juce::AudioProcessorValueTreeState::Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit UI (MindballAudioProcessor& p)
|
||||||
|
: proc (p),
|
||||||
|
feedbackKnob ("FEEDBACK", 0.35),
|
||||||
|
loCutKnob ("LO-CUT", 100.0),
|
||||||
|
hiCutKnob ("HI-CUT", 12000.0),
|
||||||
|
timeKnob ("TIME", 300.0),
|
||||||
|
mixKnob ("DRY/WET", 0.4),
|
||||||
|
panKnob ("PAN", 1.0),
|
||||||
|
syncToggle ("SYNC"),
|
||||||
|
doubleToggle ("DOUBLE"),
|
||||||
|
centerToggle ("CENTER"),
|
||||||
|
autoPanToggle ("AUTO-PAN"),
|
||||||
|
onToggle ("ON"),
|
||||||
|
modeToggle (p.apvts),
|
||||||
|
dryMeter ("DRY"),
|
||||||
|
wetMeter ("WET"),
|
||||||
|
feedbackAtt (p.apvts, ParameterIDs::feedback, feedbackKnob),
|
||||||
|
loCutAtt (p.apvts, ParameterIDs::locut, loCutKnob),
|
||||||
|
hiCutAtt (p.apvts, ParameterIDs::hicut, hiCutKnob),
|
||||||
|
mixAtt (p.apvts, ParameterIDs::drywet, mixKnob),
|
||||||
|
panAtt (p.apvts, ParameterIDs::panDepth, panKnob),
|
||||||
|
syncAtt (p.apvts, ParameterIDs::sync, syncToggle),
|
||||||
|
doubleAtt (p.apvts, ParameterIDs::doubleTap, doubleToggle),
|
||||||
|
centerAtt (p.apvts, ParameterIDs::center, centerToggle),
|
||||||
|
autoPanAtt (p.apvts, ParameterIDs::autopan, autoPanToggle),
|
||||||
|
onAtt (p.apvts, ParameterIDs::bypass, onToggle)
|
||||||
|
{
|
||||||
|
for (auto* c : { static_cast<juce::Component*> (&feedbackKnob), static_cast<juce::Component*> (&loCutKnob),
|
||||||
|
static_cast<juce::Component*> (&hiCutKnob), static_cast<juce::Component*> (&timeKnob),
|
||||||
|
static_cast<juce::Component*> (&mixKnob), static_cast<juce::Component*> (&panKnob),
|
||||||
|
static_cast<juce::Component*> (&syncToggle), static_cast<juce::Component*> (&modeToggle),
|
||||||
|
static_cast<juce::Component*> (&doubleToggle), static_cast<juce::Component*> (¢erToggle),
|
||||||
|
static_cast<juce::Component*> (&autoPanToggle), static_cast<juce::Component*> (&onToggle) })
|
||||||
|
addAndMakeVisible (c);
|
||||||
|
|
||||||
|
addAndMakeVisible (dryMeter);
|
||||||
|
addAndMakeVisible (wetMeter);
|
||||||
|
|
||||||
|
feedbackKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
|
||||||
|
loCutKnob.setValueTextFunction ([] (double v) { return formatHz (v); });
|
||||||
|
hiCutKnob.setValueTextFunction ([] (double v) { return formatHz (v); });
|
||||||
|
timeKnob.setValueTextFunction ([this] (double v)
|
||||||
|
{
|
||||||
|
if (proc.apvts.getRawParameterValue (ParameterIDs::sync)->load() > 0.5f)
|
||||||
|
return syncDivisionNames [juce::jlimit (0, syncDivisionNames.size() - 1, juce::roundToInt (v))];
|
||||||
|
return juce::String (juce::roundToInt (v)) + " ms";
|
||||||
|
});
|
||||||
|
mixKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
|
||||||
|
panKnob.setValueTextFunction ([] (double v) { return juce::String (juce::roundToInt (v * 100.0)) + "%"; });
|
||||||
|
onToggle.setInverted (true);
|
||||||
|
|
||||||
|
buildInfo.setJustificationType (juce::Justification::centredRight);
|
||||||
|
buildInfo.setFont (juce::Font (juce::Font::getDefaultMonospacedFontName(), 9.0f, 0));
|
||||||
|
|
||||||
|
juce::String dirty = mindball::BuildInfo::gitDirty ? " +dirty" : "";
|
||||||
|
juce::String hash = juce::String (mindball::BuildInfo::gitHash);
|
||||||
|
juce::String branch = juce::String (mindball::BuildInfo::gitBranch);
|
||||||
|
if (hash.isEmpty() || hash == "n/a")
|
||||||
|
hash = "no-commit";
|
||||||
|
if (branch.isEmpty() || branch == "n/a")
|
||||||
|
branch = "-";
|
||||||
|
|
||||||
|
buildInfo.setText ("Mindball v1.0.0\n"
|
||||||
|
+ hash + " " + branch + dirty + "\n"
|
||||||
|
+ juce::String (mindball::BuildInfo::buildTime),
|
||||||
|
juce::dontSendNotification);
|
||||||
|
|
||||||
|
addAndMakeVisible (buildInfo);
|
||||||
|
|
||||||
|
proc.apvts.addParameterListener (ParameterIDs::sync, this);
|
||||||
|
updateTimeAttachment();
|
||||||
|
startTimerHz (30);
|
||||||
|
}
|
||||||
|
|
||||||
|
~UI() override
|
||||||
|
{
|
||||||
|
stopTimer();
|
||||||
|
proc.apvts.removeParameterListener (ParameterIDs::sync, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
const float decay = std::exp (-1.0f / (0.9f * 30.0f));
|
||||||
|
dryMeter.update (proc.getDryMeterL(), proc.getDryMeterR(), decay);
|
||||||
|
wetMeter.update (proc.getWetMeterL(), proc.getWetMeterR(), decay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void parameterChanged (const juce::String& paramID, float) override
|
||||||
|
{
|
||||||
|
if (paramID == ParameterIDs::sync)
|
||||||
|
{
|
||||||
|
const juce::Component::SafePointer<UI> safeThis (this);
|
||||||
|
juce::MessageManager::callAsync ([safeThis]
|
||||||
|
{
|
||||||
|
if (safeThis != nullptr)
|
||||||
|
safeThis->updateTimeAttachment();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint (juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
g.fillAll (MindballColors::bg);
|
||||||
|
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.fillRect (0.0f, static_cast<float> (getHeight()) - 46.0f,
|
||||||
|
static_cast<float> (getWidth()), 1.0f);
|
||||||
|
|
||||||
|
g.setColour (MindballColors::textBright);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||||||
|
g.drawText ("MINDBALL", 30, getHeight() - 40, 200, 15, juce::Justification::left);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
const int width = getWidth();
|
||||||
|
const int margin = 30;
|
||||||
|
const int knobW = 96;
|
||||||
|
const int knobH = 118;
|
||||||
|
const int gap = (width - 2 * margin - 6 * knobW) / 5;
|
||||||
|
|
||||||
|
int x = margin;
|
||||||
|
for (auto* knob : { static_cast<juce::Slider*> (&feedbackKnob), static_cast<juce::Slider*> (&loCutKnob),
|
||||||
|
static_cast<juce::Slider*> (&hiCutKnob), static_cast<juce::Slider*> (&timeKnob),
|
||||||
|
static_cast<juce::Slider*> (&panKnob), static_cast<juce::Slider*> (&mixKnob) })
|
||||||
|
{
|
||||||
|
knob->setBounds (x, 14, knobW, knobH);
|
||||||
|
x += knobW + gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = margin;
|
||||||
|
for (auto* toggle : { static_cast<juce::Button*> (&onToggle), static_cast<juce::Button*> (&modeToggle),
|
||||||
|
static_cast<juce::Button*> (&doubleToggle), static_cast<juce::Button*> (&syncToggle),
|
||||||
|
static_cast<juce::Button*> (&autoPanToggle), static_cast<juce::Button*> (¢erToggle) })
|
||||||
|
{
|
||||||
|
toggle->setBounds (x, 146, knobW, 30);
|
||||||
|
x += knobW + gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int meterW = (width - 2 * margin - 20) / 2;
|
||||||
|
dryMeter.setBounds (margin, 206, meterW, 41);
|
||||||
|
wetMeter.setBounds (margin + meterW + 20, 206, meterW, 41);
|
||||||
|
|
||||||
|
buildInfo.setBounds (width - 270, getHeight() - 44, 240, 38);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static juce::String formatHz (double v)
|
||||||
|
{
|
||||||
|
if (v >= 1000.0)
|
||||||
|
return juce::String (v / 1000.0, 1) + " kHz";
|
||||||
|
return juce::String (juce::roundToInt (v)) + " Hz";
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTimeAttachment()
|
||||||
|
{
|
||||||
|
timeAtt.reset();
|
||||||
|
timeSyncAtt.reset();
|
||||||
|
|
||||||
|
if (proc.apvts.getRawParameterValue (ParameterIDs::sync)->load() > 0.5f)
|
||||||
|
timeSyncAtt = std::make_unique<SliderAttachment> (proc.apvts, ParameterIDs::timeSync, timeKnob);
|
||||||
|
else
|
||||||
|
timeAtt = std::make_unique<SliderAttachment> (proc.apvts, ParameterIDs::time, timeKnob);
|
||||||
|
|
||||||
|
timeKnob.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
|
||||||
|
using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment;
|
||||||
|
|
||||||
|
MindballAudioProcessor& proc;
|
||||||
|
|
||||||
|
static const juce::StringArray syncDivisionNames;
|
||||||
|
|
||||||
|
Knob feedbackKnob, loCutKnob, hiCutKnob, timeKnob, mixKnob, panKnob;
|
||||||
|
PillToggle syncToggle, doubleToggle, centerToggle, autoPanToggle, onToggle;
|
||||||
|
ModeToggle modeToggle;
|
||||||
|
LevelMeter dryMeter, wetMeter;
|
||||||
|
|
||||||
|
SliderAttachment feedbackAtt, loCutAtt, hiCutAtt, mixAtt, panAtt;
|
||||||
|
ButtonAttachment syncAtt, doubleAtt, centerAtt, autoPanAtt, onAtt;
|
||||||
|
std::unique_ptr<SliderAttachment> timeAtt, timeSyncAtt;
|
||||||
|
|
||||||
|
juce::Label buildInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const juce::StringArray UI::syncDivisionNames = { "1/16", "1/8", "1/4", "1/3", "1/2", "3/4", "1", "3/2", "2", "3", "4", "8" };
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MindballAudioProcessorEditor::MindballAudioProcessorEditor (MindballAudioProcessor& p)
|
||||||
|
: juce::AudioProcessorEditor (p),
|
||||||
|
proc (p),
|
||||||
|
ui (std::make_unique<UI> (p)),
|
||||||
|
scaleButton ([this] (float s) { setUiScale (s); }),
|
||||||
|
presetButton ([this] (int index) { applyPreset (index); })
|
||||||
|
{
|
||||||
|
setSize (juce::roundToInt (baseW * uiScale), juce::roundToInt (baseH * uiScale));
|
||||||
|
|
||||||
|
addAndMakeVisible (ui.get());
|
||||||
|
addAndMakeVisible (scaleButton);
|
||||||
|
addAndMakeVisible (presetButton);
|
||||||
|
|
||||||
|
presetButton.setCurrentProvider ([this] { return findMatchingPreset(); });
|
||||||
|
presetButton.setCurrent (findMatchingPreset());
|
||||||
|
}
|
||||||
|
|
||||||
|
MindballAudioProcessorEditor::~MindballAudioProcessorEditor() = default;
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::paint (juce::Graphics& g)
|
||||||
|
{
|
||||||
|
g.fillAll (MindballColors::bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::resized()
|
||||||
|
{
|
||||||
|
ui->setBounds (0, 0, baseW, baseH);
|
||||||
|
ui->setTransform (juce::AffineTransform::scale (uiScale));
|
||||||
|
|
||||||
|
const int btnH = 24;
|
||||||
|
const int btnY = 8;
|
||||||
|
scaleButton.setBounds (getWidth() - 52, btnY, 42, btnH);
|
||||||
|
presetButton.setBounds (getWidth() - 218, btnY, 160, btnH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::setParam (const char* paramID, float value)
|
||||||
|
{
|
||||||
|
if (auto* param = proc.apvts.getParameter (paramID))
|
||||||
|
{
|
||||||
|
param->beginChangeGesture();
|
||||||
|
param->setValueNotifyingHost (param->convertTo0to1 (value));
|
||||||
|
param->endChangeGesture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::setChoiceParam (const char* paramID, int index)
|
||||||
|
{
|
||||||
|
if (auto* param = static_cast<juce::AudioParameterChoice*> (proc.apvts.getParameter (paramID)))
|
||||||
|
{
|
||||||
|
param->beginChangeGesture();
|
||||||
|
param->setValueNotifyingHost (param->convertTo0to1 (index));
|
||||||
|
param->endChangeGesture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::applyPreset (int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= Presets::count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& p = Presets::all[index];
|
||||||
|
setParam (ParameterIDs::feedback, p.feedback);
|
||||||
|
setParam (ParameterIDs::locut, p.loCut);
|
||||||
|
setParam (ParameterIDs::hicut, p.hiCut);
|
||||||
|
setParam (ParameterIDs::time, p.timeMs);
|
||||||
|
setChoiceParam (ParameterIDs::timeSync, p.timeSyncIndex);
|
||||||
|
setParam (ParameterIDs::drywet, p.dryWet);
|
||||||
|
setParam (ParameterIDs::sync, p.sync ? 1.0f : 0.0f);
|
||||||
|
setChoiceParam (ParameterIDs::mode, p.mode);
|
||||||
|
setParam (ParameterIDs::doubleTap, p.doubleTap ? 1.0f : 0.0f);
|
||||||
|
setParam (ParameterIDs::center, p.center ? 1.0f : 0.0f);
|
||||||
|
setParam (ParameterIDs::autopan, p.autoPan ? 1.0f : 0.0f);
|
||||||
|
setParam (ParameterIDs::panDepth, p.panDepth);
|
||||||
|
|
||||||
|
presetButton.setCurrent (index);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MindballAudioProcessorEditor::presetMatches (const Presets::Definition& p) const
|
||||||
|
{
|
||||||
|
auto val = [this] (const char* paramID) { return proc.apvts.getRawParameterValue (paramID)->load(); };
|
||||||
|
auto near = [] (float a, float b) { return std::fabs (a - b) < 0.02f; };
|
||||||
|
|
||||||
|
if (! near (val (ParameterIDs::feedback), p.feedback)) return false;
|
||||||
|
if (! near (val (ParameterIDs::locut), p.loCut)) return false;
|
||||||
|
if (! near (val (ParameterIDs::hicut), p.hiCut)) return false;
|
||||||
|
if (! near (val (ParameterIDs::time), p.timeMs)) return false;
|
||||||
|
if (! near (val (ParameterIDs::drywet), p.dryWet)) return false;
|
||||||
|
if (! near (val (ParameterIDs::panDepth), p.panDepth)) return false;
|
||||||
|
|
||||||
|
if (std::round (val (ParameterIDs::timeSync)) != p.timeSyncIndex) return false;
|
||||||
|
if (std::round (val (ParameterIDs::mode)) != p.mode) return false;
|
||||||
|
|
||||||
|
auto flag = [&val] (const char* paramID, bool expected)
|
||||||
|
{
|
||||||
|
return (val (paramID) > 0.5f) == expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! flag (ParameterIDs::sync, p.sync)) return false;
|
||||||
|
if (! flag (ParameterIDs::doubleTap, p.doubleTap)) return false;
|
||||||
|
if (! flag (ParameterIDs::center, p.center)) return false;
|
||||||
|
if (! flag (ParameterIDs::autopan, p.autoPan)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MindballAudioProcessorEditor::findMatchingPreset() const
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Presets::count; ++i)
|
||||||
|
{
|
||||||
|
if (presetMatches (Presets::all[i]))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessorEditor::setUiScale (float s)
|
||||||
|
{
|
||||||
|
uiScale = s;
|
||||||
|
scaleButton.setScale (s);
|
||||||
|
ui->setTransform (juce::AffineTransform::scale (s));
|
||||||
|
setSize (juce::roundToInt (baseW * s), juce::roundToInt (baseH * s));
|
||||||
|
}
|
||||||
455
Source/PluginEditor.h
Normal file
455
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "PluginProcessor.h"
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace MindballColors
|
||||||
|
{
|
||||||
|
inline const juce::Colour bg { 0xff171718 };
|
||||||
|
inline const juce::Colour arcBg { 0xff292a2b };
|
||||||
|
inline const juce::Colour accent { 0xff6fc1ff };
|
||||||
|
inline const juce::Colour textDim { 0xff757575 };
|
||||||
|
inline const juce::Colour textBright { 0xffe6e6e6 };
|
||||||
|
inline const juce::Colour meterGreen { 0xff45a9f9 };
|
||||||
|
inline const juce::Colour meterRed { 0xffff2c6d };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace Presets
|
||||||
|
{
|
||||||
|
struct Definition
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
float feedback;
|
||||||
|
float loCut;
|
||||||
|
float hiCut;
|
||||||
|
float timeMs;
|
||||||
|
int timeSyncIndex;
|
||||||
|
float dryWet;
|
||||||
|
bool sync;
|
||||||
|
int mode;
|
||||||
|
bool doubleTap;
|
||||||
|
bool center;
|
||||||
|
bool autoPan;
|
||||||
|
float panDepth;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr Definition all[] = {
|
||||||
|
{ "Default", 0.35f, 100.0f, 12000.0f, 300.0f, 4, 0.40f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Slapback", 0.20f, 80.0f, 15000.0f, 120.0f, 4, 0.30f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Analog Echo", 0.45f, 120.0f, 5000.0f, 350.0f, 4, 0.35f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Tape Echo", 0.55f, 100.0f, 3000.0f, 450.0f, 4, 0.40f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Dub Delay", 0.70f, 150.0f, 2000.0f, 600.0f, 4, 0.50f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Ping-Pong", 0.45f, 100.0f, 10000.0f, 400.0f, 4, 0.40f, false, 2, false, false, false, 1.00f },
|
||||||
|
{ "Wide Ping-Pong", 0.40f, 100.0f, 10000.0f, 500.0f, 4, 0.45f, false, 2, true, false, false, 1.00f },
|
||||||
|
{ "SuperPong", 0.50f, 100.0f, 8000.0f, 350.0f, 4, 0.45f, false, 3, false, false, false, 1.00f },
|
||||||
|
{ "Sync 1/16", 0.35f, 100.0f, 12000.0f, 250.0f, 0, 0.35f, true, 0, false, false, false, 1.00f },
|
||||||
|
{ "Sync 1/8", 0.40f, 100.0f, 12000.0f, 250.0f, 1, 0.40f, true, 0, false, false, false, 1.00f },
|
||||||
|
{ "Sync 1/4", 0.45f, 100.0f, 12000.0f, 300.0f, 2, 0.45f, true, 0, false, false, false, 1.00f },
|
||||||
|
{ "Sync 1/2", 0.50f, 100.0f, 12000.0f, 300.0f, 4, 0.45f, true, 0, false, false, false, 1.00f },
|
||||||
|
{ "Sync 3/4", 0.55f, 100.0f, 12000.0f, 300.0f, 5, 0.50f, true, 0, false, false, false, 1.00f },
|
||||||
|
{ "Invert", 0.40f, 100.0f, 12000.0f, 300.0f, 4, 0.40f, false, 1, false, false, false, 1.00f },
|
||||||
|
{ "Hall Echo", 0.25f, 200.0f, 8000.0f, 1200.0f, 4, 0.30f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Whale Song", 0.15f, 300.0f, 6000.0f, 3000.0f, 4, 0.50f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Filtered Space", 0.55f, 300.0f, 4000.0f, 900.0f, 4, 0.45f, false, 0, false, false, false, 1.00f },
|
||||||
|
{ "Auto-Pan Delay", 0.35f, 100.0f, 12000.0f, 250.0f, 4, 0.40f, false, 0, false, false, true, 1.00f },
|
||||||
|
{ "Centered Pong", 0.60f, 100.0f, 8000.0f, 500.0f, 4, 0.45f, false, 2, false, true, false, 1.00f },
|
||||||
|
{ "Clean Echo", 0.15f, 60.0f, 16000.0f, 280.0f, 4, 0.25f, false, 0, false, false, false, 1.00f },
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr int count = static_cast<int> (std::size (all));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Knob final : public juce::Slider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Knob (const juce::String& labelText, double defaultValue)
|
||||||
|
: label (labelText)
|
||||||
|
{
|
||||||
|
setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag);
|
||||||
|
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||||
|
setDoubleClickReturnValue (true, defaultValue);
|
||||||
|
setColour (juce::Slider::ColourIds::thumbColourId, MindballColors::accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setValueTextFunction (std::function<juce::String (double)> fn)
|
||||||
|
{
|
||||||
|
valueTextFn = std::move (fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint (juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
const auto area = getLocalBounds().toFloat();
|
||||||
|
const float cx = area.getCentreX();
|
||||||
|
const float cy = area.getCentreY() - 8.0f;
|
||||||
|
const float radius = juce::jmin (area.getWidth(), area.getHeight()) * 0.30f;
|
||||||
|
|
||||||
|
const float range = static_cast<float> (getMaximum()) - static_cast<float> (getMinimum());
|
||||||
|
const float norm = juce::jlimit (0.0f, 1.0f,
|
||||||
|
range > 0.0f
|
||||||
|
? (static_cast<float> (getValue()) - static_cast<float> (getMinimum())) / range
|
||||||
|
: 0.0f);
|
||||||
|
|
||||||
|
const float sweep = juce::MathConstants<float>::twoPi * 0.75f;
|
||||||
|
|
||||||
|
// JUCE's addCentredArc measures clockwise from the top, so the arc
|
||||||
|
// angles are the screen angles shifted by +90 degrees.
|
||||||
|
const float arcStart = juce::degreesToRadians (225.0f); // down-left
|
||||||
|
const float arcEnd = arcStart + sweep; // down-right
|
||||||
|
|
||||||
|
juce::Path arc;
|
||||||
|
arc.addCentredArc (cx, cy, radius, radius, 0.0f, arcStart, arcEnd, true);
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.strokePath (arc, juce::PathStrokeType (3.0f,
|
||||||
|
juce::PathStrokeType::JointStyle::curved,
|
||||||
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||||||
|
|
||||||
|
juce::Path valueArc;
|
||||||
|
valueArc.addCentredArc (cx, cy, radius, radius, 0.0f, arcStart, arcStart + sweep * norm, true);
|
||||||
|
g.setColour (MindballColors::accent);
|
||||||
|
g.strokePath (valueArc, juce::PathStrokeType (3.0f,
|
||||||
|
juce::PathStrokeType::JointStyle::curved,
|
||||||
|
juce::PathStrokeType::EndCapStyle::rounded));
|
||||||
|
|
||||||
|
if (valueTextFn != nullptr)
|
||||||
|
{
|
||||||
|
const juce::String valueText = valueTextFn (getValue());
|
||||||
|
if (valueText.isNotEmpty())
|
||||||
|
{
|
||||||
|
g.setColour (MindballColors::textBright);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||||||
|
g.drawFittedText (valueText, getLocalBounds().removeFromBottom (32).removeFromTop (16),
|
||||||
|
juce::Justification::centred, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour (MindballColors::textDim);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||||||
|
g.drawFittedText (label, getLocalBounds().removeFromBottom (16),
|
||||||
|
juce::Justification::centredBottom, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::String label;
|
||||||
|
std::function<juce::String (double)> valueTextFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class LevelMeter final : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit LevelMeter (const juce::String& meterName)
|
||||||
|
: name (meterName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void update (float linL, float linR, float decay)
|
||||||
|
{
|
||||||
|
levels[0] = linL;
|
||||||
|
levels[1] = linR;
|
||||||
|
peaks[0] = std::max (peaks[0] * decay, linL);
|
||||||
|
peaks[1] = std::max (peaks[1] * decay, linR);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint (juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
constexpr int steps = 15;
|
||||||
|
constexpr float dbMin = -30.0f;
|
||||||
|
constexpr float dbPerStep = 2.0f;
|
||||||
|
|
||||||
|
auto b = getLocalBounds().toFloat();
|
||||||
|
g.setColour (MindballColors::textDim);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||||||
|
g.drawText (name, b.removeFromTop (13.0f), juce::Justification::centred, false);
|
||||||
|
|
||||||
|
const float rowH = (b.getHeight() - 4.0f) * 0.5f;
|
||||||
|
const float segGap = 1.0f;
|
||||||
|
const float segW = (b.getWidth() - (steps - 1) * segGap) / static_cast<float> (steps);
|
||||||
|
|
||||||
|
for (int ch = 0; ch < 2; ++ch)
|
||||||
|
{
|
||||||
|
auto row = b.removeFromTop (rowH);
|
||||||
|
const float lin = std::max (0.0f, levels[ch]);
|
||||||
|
const float db = 20.0f * std::log10 (lin > 0.0f ? lin : 1e-9f);
|
||||||
|
const int lit = juce::jlimit (0, steps, juce::roundToInt ((db - dbMin) / dbPerStep));
|
||||||
|
const int pk = juce::jlimit (0, steps, juce::roundToInt ((20.0f * std::log10 (std::max (1e-9f, peaks[ch])) - dbMin) / dbPerStep));
|
||||||
|
|
||||||
|
for (int s = 0; s < steps; ++s)
|
||||||
|
{
|
||||||
|
const auto seg = juce::Rectangle<float> (row.getX() + s * (segW + segGap),
|
||||||
|
row.getY() + 2.0f, segW, rowH - 4.0f);
|
||||||
|
g.setColour (s < lit ? MindballColors::meterGreen : MindballColors::arcBg);
|
||||||
|
g.fillRoundedRectangle (seg, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pk > lit)
|
||||||
|
{
|
||||||
|
const auto seg = juce::Rectangle<float> (row.getX() + pk * (segW + segGap),
|
||||||
|
row.getY() + 2.0f, segW, rowH - 4.0f);
|
||||||
|
g.setColour (MindballColors::meterRed);
|
||||||
|
g.fillRoundedRectangle (seg, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::String name;
|
||||||
|
float levels[2] = { 0.0f, 0.0f };
|
||||||
|
float peaks[2] = { 0.0f, 0.0f };
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PillToggle final : public juce::Button
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PillToggle (const juce::String& text)
|
||||||
|
: juce::Button (text)
|
||||||
|
{
|
||||||
|
setClickingTogglesState (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInverted (bool shouldInvert)
|
||||||
|
{
|
||||||
|
inverted = shouldInvert;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||||||
|
{
|
||||||
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||||||
|
const bool on = inverted ? ! getToggleState() : getToggleState();
|
||||||
|
|
||||||
|
if (on)
|
||||||
|
{
|
||||||
|
g.setColour (MindballColors::accent);
|
||||||
|
g.fillRoundedRectangle (b, b.getHeight() * 0.5f);
|
||||||
|
g.setColour (MindballColors::bg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||||||
|
g.setColour (MindballColors::textDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||||||
|
g.drawText (getButtonText(), getLocalBounds(), juce::Justification::centred, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool inverted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ModeToggle final : public juce::Button, private juce::AudioProcessorParameter::Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ModeToggle (juce::AudioProcessorValueTreeState& apvts)
|
||||||
|
: juce::Button ("Mode"),
|
||||||
|
param (static_cast<juce::AudioParameterChoice*> (apvts.getParameter (ParameterIDs::mode)))
|
||||||
|
{
|
||||||
|
if (param != nullptr)
|
||||||
|
param->addListener (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ModeToggle() override
|
||||||
|
{
|
||||||
|
if (param != nullptr)
|
||||||
|
param->removeListener (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clicked() override
|
||||||
|
{
|
||||||
|
if (param == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int next = (param->getIndex() + 1) % param->choices.size();
|
||||||
|
param->beginChangeGesture();
|
||||||
|
param->setValueNotifyingHost (param->convertTo0to1 (next));
|
||||||
|
param->endChangeGesture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void parameterValueChanged (int, float) override
|
||||||
|
{
|
||||||
|
juce::MessageManager::callAsync ([this] { repaint(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void parameterGestureChanged (int, bool) override {}
|
||||||
|
|
||||||
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||||||
|
{
|
||||||
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||||||
|
const int mode = param != nullptr ? param->getIndex() : 0;
|
||||||
|
const bool on = mode != 0;
|
||||||
|
|
||||||
|
if (on)
|
||||||
|
{
|
||||||
|
g.setColour (MindballColors::accent);
|
||||||
|
g.fillRoundedRectangle (b, b.getHeight() * 0.5f);
|
||||||
|
g.setColour (MindballColors::bg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||||||
|
g.setColour (MindballColors::textDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (9.0f)));
|
||||||
|
g.drawText (param != nullptr ? param->getCurrentChoiceName() : "Mode",
|
||||||
|
getLocalBounds(), juce::Justification::centred, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::AudioParameterChoice* param;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ScaleButton final : public juce::Button
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScaleButton (std::function<void (float)> onPick)
|
||||||
|
: juce::Button ("UI Scale"), pickCallback (std::move (onPick))
|
||||||
|
{
|
||||||
|
setScale (1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setScale (float s)
|
||||||
|
{
|
||||||
|
scale = s;
|
||||||
|
setButtonText (juce::String (juce::roundToInt (scale * 100.0f)) + "%");
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||||||
|
{
|
||||||
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||||||
|
g.setColour (MindballColors::textBright);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||||||
|
g.drawText (getButtonText(), getLocalBounds(), juce::Justification::centred, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clicked() override
|
||||||
|
{
|
||||||
|
const float options[] = { 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f, 2.5f, 3.0f };
|
||||||
|
|
||||||
|
juce::PopupMenu menu;
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
menu.addItem (i + 1, juce::String (juce::roundToInt (options[i] * 100.0f)) + "%",
|
||||||
|
true, scale == options[i]);
|
||||||
|
|
||||||
|
menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this),
|
||||||
|
[this, options] (int result)
|
||||||
|
{
|
||||||
|
if (result >= 1 && result <= 8 && pickCallback != nullptr)
|
||||||
|
pickCallback (options[result - 1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::function<void (float)> pickCallback;
|
||||||
|
float scale = 1.5f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class PresetButton final : public juce::Button
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PresetButton (std::function<void (int)> onPick)
|
||||||
|
: juce::Button ("Presets"), pickCallback (std::move (onPick))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrent (int index)
|
||||||
|
{
|
||||||
|
current = index;
|
||||||
|
setButtonText (juce::String (Presets::all[index].name));
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentProvider (std::function<int()> provider)
|
||||||
|
{
|
||||||
|
currentProvider = std::move (provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paintButton (juce::Graphics& g, bool, bool) override
|
||||||
|
{
|
||||||
|
const auto b = getLocalBounds().toFloat().reduced (1.0f);
|
||||||
|
g.setColour (MindballColors::arcBg);
|
||||||
|
g.drawRoundedRectangle (b, b.getHeight() * 0.5f, 1.0f);
|
||||||
|
g.setColour (MindballColors::textBright);
|
||||||
|
g.setFont (juce::Font (juce::FontOptions (11.0f)));
|
||||||
|
g.drawText (getButtonText(), b, juce::Justification::centred, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clicked() override
|
||||||
|
{
|
||||||
|
const int checked = currentProvider != nullptr ? currentProvider() : current;
|
||||||
|
|
||||||
|
juce::PopupMenu menu;
|
||||||
|
for (int i = 0; i < Presets::count; ++i)
|
||||||
|
menu.addItem (i + 1, juce::String (Presets::all[i].name), true, i == checked);
|
||||||
|
|
||||||
|
menu.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (this),
|
||||||
|
[this] (int result)
|
||||||
|
{
|
||||||
|
if (result >= 1 && result <= Presets::count && pickCallback != nullptr)
|
||||||
|
pickCallback (result - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::function<void (int)> pickCallback;
|
||||||
|
std::function<int()> currentProvider;
|
||||||
|
int current = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UI;
|
||||||
|
|
||||||
|
class MindballAudioProcessorEditor final : public juce::AudioProcessorEditor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit MindballAudioProcessorEditor (MindballAudioProcessor& p);
|
||||||
|
~MindballAudioProcessorEditor() override;
|
||||||
|
|
||||||
|
void paint (juce::Graphics&) override;
|
||||||
|
void resized() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int baseW = 736;
|
||||||
|
static constexpr int baseH = 320;
|
||||||
|
|
||||||
|
void setUiScale (float s);
|
||||||
|
void applyPreset (int index);
|
||||||
|
int findMatchingPreset() const;
|
||||||
|
bool presetMatches (const Presets::Definition& p) const;
|
||||||
|
void setParam (const char* paramID, float value);
|
||||||
|
void setChoiceParam (const char* paramID, int index);
|
||||||
|
|
||||||
|
MindballAudioProcessor& proc;
|
||||||
|
std::unique_ptr<UI> ui;
|
||||||
|
ScaleButton scaleButton;
|
||||||
|
PresetButton presetButton;
|
||||||
|
float uiScale = 1.5f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MindballAudioProcessorEditor)
|
||||||
|
};
|
||||||
196
Source/PluginProcessor.cpp
Normal file
196
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
#include "PluginProcessor.h"
|
||||||
|
#include "PluginEditor.h"
|
||||||
|
|
||||||
|
MindballAudioProcessor::MindballAudioProcessor()
|
||||||
|
: AudioProcessor (BusesProperties()
|
||||||
|
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
|
||||||
|
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
|
||||||
|
apvts (*this, nullptr, "Parameters", createParameterLayout())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessorValueTreeState::ParameterLayout MindballAudioProcessor::createParameterLayout()
|
||||||
|
{
|
||||||
|
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::feedback, "Feedback",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0001f, 1.0f), 0.35f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::locut, "Lo-Cut",
|
||||||
|
juce::NormalisableRange<float> (20.0f, 4000.0f, 1.0f, 0.3f), 100.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::hicut, "Hi-Cut",
|
||||||
|
juce::NormalisableRange<float> (500.0f, 18000.0f, 1.0f, 0.3f), 12000.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::time, "Time",
|
||||||
|
juce::NormalisableRange<float> (10.0f, 3000.0f, 1.0f, 0.4f), 300.0f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterChoice> (
|
||||||
|
ParameterIDs::timeSync, "Time (Sync)",
|
||||||
|
juce::StringArray { "1/16", "1/8", "1/4", "1/3", "1/2", "3/4", "1", "3/2", "2", "3", "4", "8" }, 4));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::drywet, "Dry/Wet",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f), 0.4f));
|
||||||
|
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::sync, "Time Sync", false));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterChoice> (
|
||||||
|
ParameterIDs::mode, "Mode",
|
||||||
|
juce::StringArray { "Normal", "Invert", "Ping-Pong", "SuperPong" }, 0));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::doubleTap, "Double", false));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::center, "Center", false));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::autopan, "Auto-Pan", false));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterFloat> (
|
||||||
|
ParameterIDs::panDepth, "Pan Depth",
|
||||||
|
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 1.0f), 1.0f));
|
||||||
|
layout.add (std::make_unique<juce::AudioParameterBool> (ParameterIDs::bypass, "Bypass", false));
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::String MindballAudioProcessor::getName() const { return JucePlugin_Name; }
|
||||||
|
|
||||||
|
bool MindballAudioProcessor::acceptsMidi() const { return false; }
|
||||||
|
bool MindballAudioProcessor::producesMidi() const { return false; }
|
||||||
|
double MindballAudioProcessor::getTailLengthSeconds() const { return 10.0; }
|
||||||
|
|
||||||
|
int MindballAudioProcessor::getNumPrograms() { return 1; }
|
||||||
|
int MindballAudioProcessor::getCurrentProgram() { return 0; }
|
||||||
|
void MindballAudioProcessor::setCurrentProgram (int) {}
|
||||||
|
const juce::String MindballAudioProcessor::getProgramName (int) { return {}; }
|
||||||
|
void MindballAudioProcessor::changeProgramName (int, const juce::String&) {}
|
||||||
|
|
||||||
|
void MindballAudioProcessor::prepareToPlay (double sampleRate, int /*samplesPerBlock*/)
|
||||||
|
{
|
||||||
|
engine.prepare (sampleRate, maxDelaySeconds);
|
||||||
|
meterDecay = static_cast<float> (std::exp (-1.0 / (0.4 * sampleRate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessor::releaseResources()
|
||||||
|
{
|
||||||
|
engine.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MindballAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
|
||||||
|
{
|
||||||
|
const auto mainIn = layouts.getMainInputChannelSet();
|
||||||
|
const auto mainOut = layouts.getMainOutputChannelSet();
|
||||||
|
|
||||||
|
return mainIn == mainOut
|
||||||
|
&& (mainIn == juce::AudioChannelSet::mono() || mainIn == juce::AudioChannelSet::stereo());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||||
|
{
|
||||||
|
juce::ScopedNoDenormals noDenormals;
|
||||||
|
|
||||||
|
const int numSamples = buffer.getNumSamples();
|
||||||
|
const int numChannels = buffer.getNumChannels();
|
||||||
|
|
||||||
|
if (numSamples == 0 || numChannels == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const bool bypass = *apvts.getRawParameterValue (ParameterIDs::bypass) > 0.5f;
|
||||||
|
if (bypass)
|
||||||
|
{
|
||||||
|
if (!wasBypassed)
|
||||||
|
{
|
||||||
|
engine.clear();
|
||||||
|
wasBypassed = true;
|
||||||
|
}
|
||||||
|
meterDryL = meterDryR = meterWetL = meterWetR = 0.0f;
|
||||||
|
dryMeterL.store (0.0f); dryMeterR.store (0.0f);
|
||||||
|
wetMeterL.store (0.0f); wetMeterR.store (0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wasBypassed = false;
|
||||||
|
|
||||||
|
double bpm = 120.0;
|
||||||
|
if (auto* playHead = getPlayHead())
|
||||||
|
{
|
||||||
|
juce::AudioPlayHead::CurrentPositionInfo pos;
|
||||||
|
if (playHead->getCurrentPosition (pos))
|
||||||
|
bpm = pos.bpm > 0 ? pos.bpm : 120.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mindball::DelayEngine::Params p;
|
||||||
|
p.feedback = *apvts.getRawParameterValue (ParameterIDs::feedback);
|
||||||
|
p.loCutHz = *apvts.getRawParameterValue (ParameterIDs::locut);
|
||||||
|
p.hiCutHz = *apvts.getRawParameterValue (ParameterIDs::hicut);
|
||||||
|
p.timeMs = *apvts.getRawParameterValue (ParameterIDs::time);
|
||||||
|
p.timeSyncIndex = static_cast<int> (*apvts.getRawParameterValue (ParameterIDs::timeSync));
|
||||||
|
p.dryWet = *apvts.getRawParameterValue (ParameterIDs::drywet);
|
||||||
|
p.mode = static_cast<int> (*apvts.getRawParameterValue (ParameterIDs::mode));
|
||||||
|
p.sync = *apvts.getRawParameterValue (ParameterIDs::sync) > 0.5f;
|
||||||
|
p.doubleTap = *apvts.getRawParameterValue (ParameterIDs::doubleTap) > 0.5f;
|
||||||
|
p.center = *apvts.getRawParameterValue (ParameterIDs::center) > 0.5f;
|
||||||
|
p.autoPan = *apvts.getRawParameterValue (ParameterIDs::autopan) > 0.5f;
|
||||||
|
p.panDepth = *apvts.getRawParameterValue (ParameterIDs::panDepth);
|
||||||
|
p.bpm = bpm;
|
||||||
|
|
||||||
|
wetScratch.setSize (2, numSamples);
|
||||||
|
|
||||||
|
if (numChannels >= 2)
|
||||||
|
{
|
||||||
|
engine.process (buffer.getReadPointer (0), buffer.getReadPointer (1),
|
||||||
|
buffer.getWritePointer (0), buffer.getWritePointer (1),
|
||||||
|
numSamples, p,
|
||||||
|
wetScratch.getWritePointer (0), wetScratch.getWritePointer (1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
engine.process (buffer.getReadPointer (0), buffer.getReadPointer (0),
|
||||||
|
buffer.getWritePointer (0), buffer.getWritePointer (0),
|
||||||
|
numSamples, p,
|
||||||
|
wetScratch.getWritePointer (0), wetScratch.getWritePointer (1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float* outL = buffer.getReadPointer (0);
|
||||||
|
const float* outR = numChannels >= 2 ? buffer.getReadPointer (1) : outL;
|
||||||
|
const float* wetL = wetScratch.getReadPointer (0);
|
||||||
|
const float* wetR = wetScratch.getReadPointer (1);
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
const float dryL = outL[i] - wetL[i];
|
||||||
|
const float dryR = outR[i] - wetR[i];
|
||||||
|
|
||||||
|
meterDryL = std::max (std::abs (dryL), meterDryL * meterDecay);
|
||||||
|
meterDryR = std::max (std::abs (dryR), meterDryR * meterDecay);
|
||||||
|
meterWetL = std::max (std::abs (wetL[i]), meterWetL * meterDecay);
|
||||||
|
meterWetR = std::max (std::abs (wetR[i]), meterWetR * meterDecay);
|
||||||
|
}
|
||||||
|
|
||||||
|
dryMeterL.store (meterDryL); dryMeterR.store (meterDryR);
|
||||||
|
wetMeterL.store (meterWetL); wetMeterR.store (meterWetR);
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessorEditor* MindballAudioProcessor::createEditor()
|
||||||
|
{
|
||||||
|
return new MindballAudioProcessorEditor (*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MindballAudioProcessor::hasEditor() const { return true; }
|
||||||
|
|
||||||
|
void MindballAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||||
|
{
|
||||||
|
auto state = apvts.copyState();
|
||||||
|
std::unique_ptr<juce::XmlElement> xml (state.createXml());
|
||||||
|
copyXmlToBinary (*xml, destData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MindballAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||||
|
{
|
||||||
|
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||||
|
if (xml != nullptr)
|
||||||
|
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||||
|
{
|
||||||
|
return new MindballAudioProcessor();
|
||||||
|
}
|
||||||
75
Source/PluginProcessor.h
Normal file
75
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DelayEngine.h"
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
namespace ParameterIDs
|
||||||
|
{
|
||||||
|
inline constexpr const char* feedback = "feedback";
|
||||||
|
inline constexpr const char* locut = "locut";
|
||||||
|
inline constexpr const char* hicut = "hicut";
|
||||||
|
inline constexpr const char* time = "time";
|
||||||
|
inline constexpr const char* timeSync = "timeSync";
|
||||||
|
inline constexpr const char* drywet = "drywet";
|
||||||
|
inline constexpr const char* sync = "sync";
|
||||||
|
inline constexpr const char* mode = "mode";
|
||||||
|
inline constexpr const char* doubleTap = "double";
|
||||||
|
inline constexpr const char* center = "center";
|
||||||
|
inline constexpr const char* autopan = "autopan";
|
||||||
|
inline constexpr const char* panDepth = "pandepth";
|
||||||
|
inline constexpr const char* bypass = "bypass";
|
||||||
|
}
|
||||||
|
|
||||||
|
class MindballAudioProcessor : public juce::AudioProcessor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MindballAudioProcessor();
|
||||||
|
~MindballAudioProcessor() override = default;
|
||||||
|
|
||||||
|
juce::AudioProcessorValueTreeState apvts;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
float getDryMeterL() const { return dryMeterL.load (std::memory_order_relaxed); }
|
||||||
|
float getDryMeterR() const { return dryMeterR.load (std::memory_order_relaxed); }
|
||||||
|
float getWetMeterL() const { return wetMeterL.load (std::memory_order_relaxed); }
|
||||||
|
float getWetMeterR() const { return wetMeterR.load (std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||||
|
|
||||||
|
static constexpr double maxDelaySeconds = 16.0;
|
||||||
|
|
||||||
|
mindball::DelayEngine engine;
|
||||||
|
bool wasBypassed = false;
|
||||||
|
|
||||||
|
juce::AudioBuffer<float> wetScratch;
|
||||||
|
float meterDryL = 0.0f, meterDryR = 0.0f;
|
||||||
|
float meterWetL = 0.0f, meterWetR = 0.0f;
|
||||||
|
float meterDecay = 0.0f;
|
||||||
|
std::atomic<float> dryMeterL { 0.0f }, dryMeterR { 0.0f };
|
||||||
|
std::atomic<float> wetMeterL { 0.0f }, wetMeterR { 0.0f };
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue