Compare commits
2 Commits
5c31f6dbf8
...
6fcdd0a158
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fcdd0a158 | ||
|
|
a1b9153bdc |
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -4,3 +4,9 @@
|
|||||||
[submodule "modules/hilbert-iir"]
|
[submodule "modules/hilbert-iir"]
|
||||||
path = modules/hilbert-iir
|
path = modules/hilbert-iir
|
||||||
url = https://github.com/Signalsmith-Audio/hilbert-iir.git
|
url = https://github.com/Signalsmith-Audio/hilbert-iir.git
|
||||||
|
[submodule "clap/modules/linear"]
|
||||||
|
path = modules/linear
|
||||||
|
url = https://github.com/Signalsmith-Audio/linear.git
|
||||||
|
[submodule "modules/linear"]
|
||||||
|
path = modules/linear
|
||||||
|
url = https://github.com/Signalsmith-Audio/linear.git
|
||||||
|
|||||||
12
CMakeLists.txt
Normal file
12
CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
add_library(signalsmith-basics INTERFACE)
|
||||||
|
target_include_directories(signalsmith-basics INTERFACE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/modules
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/modules/linear)
|
||||||
|
target_link_libraries(signalsmith-basics INTERFACE
|
||||||
|
signalsmith-linear
|
||||||
|
)
|
||||||
237
analyser.h
Normal file
237
analyser.h
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
|
||||||
|
Released under the Boost Software License (see LICENSE.txt) */
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "stfx/stfx-library.h"
|
||||||
|
|
||||||
|
#include "dsp/curves.h"
|
||||||
|
#include "linear/stft.h"
|
||||||
|
#include "linear/linear.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace signalsmith { namespace basics {
|
||||||
|
|
||||||
|
template<class BaseEffect>
|
||||||
|
struct AnalyserSTFX;
|
||||||
|
|
||||||
|
using AnalyserFloat = stfx::LibraryEffect<float, AnalyserSTFX>;
|
||||||
|
using AnalyserDouble = stfx::LibraryEffect<double, AnalyserSTFX>;
|
||||||
|
|
||||||
|
template<class BaseEffect>
|
||||||
|
struct AnalyserSTFX : public BaseEffect {
|
||||||
|
using typename BaseEffect::Sample;
|
||||||
|
using Complex = std::complex<Sample>;
|
||||||
|
using typename BaseEffect::ParamRange;
|
||||||
|
using typename BaseEffect::ParamStepped;
|
||||||
|
|
||||||
|
static constexpr Sample stftBlockMs = 30, stftIntervalMs = 5;
|
||||||
|
|
||||||
|
ParamRange barkResolution = 10;
|
||||||
|
|
||||||
|
template<class Storage>
|
||||||
|
void state(Storage &storage) {
|
||||||
|
storage.info("[Basics] Analyser", "A Bark-scale spectrum analyser");
|
||||||
|
storage.version(0);
|
||||||
|
|
||||||
|
storage.range("barkResolution", barkResolution)
|
||||||
|
.info("res.", "in Bark scale")
|
||||||
|
.range(1, 10, 25)
|
||||||
|
.unit("", 0);
|
||||||
|
|
||||||
|
if (storage.extra()) {
|
||||||
|
storage("spectrum", spectrum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Config>
|
||||||
|
void configureSTFX(Config &config) {
|
||||||
|
sampleRate = config.sampleRate;
|
||||||
|
channels = config.outputChannels = config.inputChannels;
|
||||||
|
config.auxInputs = config.auxOutputs = {};
|
||||||
|
|
||||||
|
stft.configure(channels, 0, stftBlockMs*0.001*sampleRate, stftIntervalMs*0.001*sampleRate);
|
||||||
|
subRate = sampleRate/stft.defaultInterval();
|
||||||
|
bands = spectrum.resize(channels, barkResolution, sampleRate);
|
||||||
|
updateBands();
|
||||||
|
tmp.resize(stft.defaultInterval());
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
spectrum.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Io, class Config, class Block>
|
||||||
|
void processSTFX(Io &io, Config &config, Block &block) {
|
||||||
|
for (size_t c = 0; c < config.inputChannels; ++c) {
|
||||||
|
auto &input = io.input[c];
|
||||||
|
auto &output = io.output[c];
|
||||||
|
for (size_t i = 0; i < block.length; ++i) {
|
||||||
|
output[i] = input[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool processedSpectrum = false;
|
||||||
|
size_t index = 0;
|
||||||
|
while (index < block.length) {
|
||||||
|
size_t remaining = stft.defaultInterval() - stft.samplesSinceAnalysis();
|
||||||
|
size_t consume = std::min<size_t>(remaining, block.length - index);
|
||||||
|
// copy input
|
||||||
|
for (size_t c = 0; c < config.inputChannels; ++c) {
|
||||||
|
auto &input = io.input[c];
|
||||||
|
for (size_t i = 0; i < consume; ++i) {
|
||||||
|
tmp[i] = input[index + i];
|
||||||
|
}
|
||||||
|
stft.writeInput(c, consume, tmp.data());
|
||||||
|
}
|
||||||
|
stft.moveInput(consume);
|
||||||
|
if (remaining == consume) {
|
||||||
|
stft.analyse();
|
||||||
|
stftStep();
|
||||||
|
processedSpectrum = true;
|
||||||
|
}
|
||||||
|
index += consume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedSpectrum && block.wantsMeters()) {
|
||||||
|
for (size_t c = 0; c < channels; ++c) {
|
||||||
|
size_t offset = c*bands;
|
||||||
|
auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands);
|
||||||
|
linear.wrap(spectrum.energy[c]) = state2.norm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Storage>
|
||||||
|
void meterState(Storage &storage) {
|
||||||
|
storage("spectrum", spectrum);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
signalsmith::linear::DynamicSTFT<Sample> stft;
|
||||||
|
signalsmith::linear::Linear linear;
|
||||||
|
std::vector<Sample> tmp;
|
||||||
|
double prevBarkResolution = -1;
|
||||||
|
|
||||||
|
Sample sampleRate, subRate;
|
||||||
|
size_t channels = 0;
|
||||||
|
size_t bands = 0;
|
||||||
|
std::vector<Sample> inputBin;
|
||||||
|
std::vector<Sample> bandInputReal, bandInputImag;
|
||||||
|
std::vector<Sample> state1Real, state1Imag;
|
||||||
|
std::vector<Sample> state2Real, state2Imag;
|
||||||
|
std::vector<Sample> twistReal, twistImag, inputGain;
|
||||||
|
|
||||||
|
void updateBands() {
|
||||||
|
linear.reserve<Sample>(bands);
|
||||||
|
|
||||||
|
bandInputReal.resize(bands*channels);
|
||||||
|
bandInputImag.resize(bands*channels);
|
||||||
|
state1Real.resize(bands*channels);
|
||||||
|
state1Imag.resize(bands*channels);
|
||||||
|
linear.wrap(state1Real) = 0;
|
||||||
|
linear.wrap(state1Imag) = 0;
|
||||||
|
state2Real.resize(bands*channels);
|
||||||
|
state2Imag.resize(bands*channels);
|
||||||
|
linear.wrap(state2Real) = 0;
|
||||||
|
linear.wrap(state2Imag) = 0;
|
||||||
|
|
||||||
|
inputBin.resize(0);
|
||||||
|
twistReal.resize(0);
|
||||||
|
twistImag.resize(0);
|
||||||
|
|
||||||
|
for (size_t b = 0; b < bands; ++b) {
|
||||||
|
Sample hz = spectrum.hz[b];
|
||||||
|
Sample bwHz = spectrum.bwHz[b];
|
||||||
|
|
||||||
|
inputBin.push_back(stft.freqToBin(hz/sampleRate));
|
||||||
|
Sample freq = Sample(2*M_PI/subRate)*hz;
|
||||||
|
Sample bw = Sample(2*M_PI/subRate)*bwHz;
|
||||||
|
Complex twist = std::exp(Complex{-bw, freq});
|
||||||
|
twistReal.push_back(twist.real());
|
||||||
|
twistImag.push_back(twist.imag());
|
||||||
|
}
|
||||||
|
inputGain.resize(bands);
|
||||||
|
linear.wrap(inputGain) = 1 - linear.wrap(twistReal, twistImag).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stftStep() {
|
||||||
|
if (barkResolution != prevBarkResolution) {
|
||||||
|
prevBarkResolution = barkResolution;
|
||||||
|
bands = spectrum.resize(channels, barkResolution, sampleRate);
|
||||||
|
updateBands();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load inputs from the spectrum
|
||||||
|
Sample scalingFactor = Sample(1)/stft.defaultInterval();
|
||||||
|
for (size_t c = 0; c < channels; ++c) {
|
||||||
|
auto *spectrum = stft.spectrum(c);
|
||||||
|
auto *inputR = bandInputReal.data() + bands*c;
|
||||||
|
auto *inputI = bandInputImag.data() + bands*c;
|
||||||
|
for (size_t b = 0; b < bands; ++b) {
|
||||||
|
Sample index = std::min<Sample>(inputBin[b], stft.bands() - Sample(1.001));
|
||||||
|
size_t indexLow = std::floor(index);
|
||||||
|
Sample indexFrac = index - std::floor(index);
|
||||||
|
Complex v = spectrum[indexLow] + (spectrum[indexLow + 1] - spectrum[indexLow])*indexFrac;
|
||||||
|
v *= scalingFactor;
|
||||||
|
inputR[b] = v.real();
|
||||||
|
inputI[b] = v.imag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto twist = linear.wrap(twistReal, twistImag);
|
||||||
|
auto gain = linear.wrap(inputGain);
|
||||||
|
for (size_t c = 0; c < channels; ++c) {
|
||||||
|
size_t offset = c*bands;
|
||||||
|
auto state1 = linear.wrap(state1Real.data() + offset, state1Imag.data() + offset, bands);
|
||||||
|
auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands);
|
||||||
|
auto input = linear.wrap(bandInputReal.data() + offset, bandInputImag.data() + offset, bands);
|
||||||
|
state1 = state1*twist + input*gain;
|
||||||
|
state2 = state2*twist + state1*gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Spectrum {
|
||||||
|
bool bandsChanged = false;
|
||||||
|
std::vector<Sample> hz, bwHz;
|
||||||
|
std::vector<std::vector<Sample>> energy;
|
||||||
|
|
||||||
|
size_t resize(size_t channels, Sample barkResolution, Sample sampleRate) {
|
||||||
|
bandsChanged = true;
|
||||||
|
hz.resize(0);
|
||||||
|
bwHz.resize(0);
|
||||||
|
|
||||||
|
auto barkScale = signalsmith::curves::Reciprocal<Sample>::barkScale();
|
||||||
|
Sample barkStep = 1/barkResolution, barkEnd = barkScale.inverse(sampleRate/2);
|
||||||
|
for (Sample bark = barkScale.inverse(0); bark < barkEnd; bark += barkStep) {
|
||||||
|
hz.push_back(barkScale(bark));
|
||||||
|
bwHz.push_back(barkScale.dx(bark)*barkStep);
|
||||||
|
}
|
||||||
|
hz.push_back(sampleRate/2);
|
||||||
|
bwHz.push_back(barkScale.dx(barkEnd)*barkStep);
|
||||||
|
|
||||||
|
energy.resize(channels);
|
||||||
|
for (auto &e : energy) e.resize(hz.size());
|
||||||
|
|
||||||
|
return hz.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
for (auto &e : energy) {
|
||||||
|
for (auto &v : e) v = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Storage>
|
||||||
|
void state(Storage &storage) {
|
||||||
|
if (bandsChanged) {
|
||||||
|
bandsChanged = false;
|
||||||
|
storage.extra("$type", "Spectrum");
|
||||||
|
storage("hz", hz);
|
||||||
|
}
|
||||||
|
storage("energy", energy);
|
||||||
|
}
|
||||||
|
} spectrum;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace
|
||||||
@ -1,7 +1,10 @@
|
|||||||
cmake_minimum_required(VERSION 3.28)
|
cmake_minimum_required(VERSION 3.28)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||||
|
set(CMAKE_C_VISIBILITY_PRESET hidden)
|
||||||
|
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
||||||
|
|
||||||
project(plugins VERSION 1.0.0)
|
project(plugins VERSION 1.0.0)
|
||||||
set(NAME basics)
|
|
||||||
|
|
||||||
################ boilerplate config
|
################ boilerplate config
|
||||||
|
|
||||||
@ -57,17 +60,16 @@ FetchContent_MakeAvailable(clap-helpers)
|
|||||||
|
|
||||||
################ The actual plugin(s)
|
################ The actual plugin(s)
|
||||||
|
|
||||||
|
add_subdirectory(../ signalsmith-basics/) # need explicit path since it's not a subdir
|
||||||
|
|
||||||
|
set(NAME basics)
|
||||||
add_library(${NAME}_static STATIC)
|
add_library(${NAME}_static STATIC)
|
||||||
target_link_libraries(${NAME}_static PUBLIC
|
target_link_libraries(${NAME}_static PUBLIC
|
||||||
clap
|
clap
|
||||||
clap-helpers
|
signalsmith-basics
|
||||||
)
|
|
||||||
target_include_directories(${NAME}_static PRIVATE
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../modules
|
|
||||||
)
|
)
|
||||||
target_sources(${NAME}_static PRIVATE
|
target_sources(${NAME}_static PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/source/${NAME}.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/source/basics.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
make_clapfirst_plugins(
|
make_clapfirst_plugins(
|
||||||
|
|||||||
@ -48,4 +48,5 @@ format:
|
|||||||
dev: release-$(PLUGIN)_clap
|
dev: release-$(PLUGIN)_clap
|
||||||
/Applications/REAPER.app/Contents/MacOS/REAPER REAPER/$(PLUGIN)/$(PLUGIN).RPP
|
/Applications/REAPER.app/Contents/MacOS/REAPER REAPER/$(PLUGIN)/$(PLUGIN).RPP
|
||||||
|
|
||||||
wclap: emscripten-$(PLUGIN)_wclap
|
wclap: emscripten-$(PLUGIN)_wclap
|
||||||
|
./wclap-tar.sh out/Release/$(PLUGIN).wclap out/
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<REAPER_PROJECT 0.1 "7.20/macOS-arm64" 1747749064
|
<REAPER_PROJECT 0.1 "7.24/OSX64-clang" 1750919608
|
||||||
<NOTES 0 2
|
<NOTES 0 2
|
||||||
>
|
>
|
||||||
RIPPLE 0
|
RIPPLE 0
|
||||||
@ -101,7 +101,7 @@
|
|||||||
BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA
|
BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA
|
||||||
AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
AAAQAAAA
|
AFByb2dyYW0gMQAQAAAA
|
||||||
>
|
>
|
||||||
FLOATPOS 0 0 0 0
|
FLOATPOS 0 0 0 0
|
||||||
FXID {11A0D811-6420-CB40-95D6-F9D12E45C862}
|
FXID {11A0D811-6420-CB40-95D6-F9D12E45C862}
|
||||||
@ -125,54 +125,8 @@
|
|||||||
>
|
>
|
||||||
<PROJBAY
|
<PROJBAY
|
||||||
>
|
>
|
||||||
<TRACK {6D299D8B-E766-F540-80D3-DA6147A064BA}
|
<TRACK {93E699D9-F890-234E-A113-7E0053A85B78}
|
||||||
NAME "Sidechain Distortion"
|
NAME ""
|
||||||
PEAKCOL 16576
|
|
||||||
BEAT -1
|
|
||||||
AUTOMODE 0
|
|
||||||
PANLAWFLAGS 3
|
|
||||||
VOLPAN 0.56085967561807 0 -1 -1 1
|
|
||||||
MUTESOLO 0 0 0
|
|
||||||
IPHASE 0
|
|
||||||
PLAYOFFS 0 1
|
|
||||||
ISBUS 1 1
|
|
||||||
BUSCOMP 0 0 0 0 0
|
|
||||||
SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0
|
|
||||||
FIXEDLANES 9 0 0 0 0
|
|
||||||
SEL 1
|
|
||||||
REC 0 0 1 0 0 0 0 0
|
|
||||||
VU 2
|
|
||||||
TRACKHEIGHT 0 0 0 0 0 0 0
|
|
||||||
INQ 0 0 0 0.5 100 0 0 100
|
|
||||||
NCHAN 4
|
|
||||||
FX 1
|
|
||||||
TRACKID {6D299D8B-E766-F540-80D3-DA6147A064BA}
|
|
||||||
PERF 0
|
|
||||||
AUXRECV 2 3 1 0 0 0 0 0 2 -1:U 0 -1 ''
|
|
||||||
AUXRECV 3 3 1 0 0 0 0 0 2 -1:U 0 -1 ''
|
|
||||||
MIDIOUT -1
|
|
||||||
MAINSEND 1 0
|
|
||||||
<FXCHAIN
|
|
||||||
WNDRECT 184 282 862 150
|
|
||||||
SHOW 1
|
|
||||||
LASTSEL 0
|
|
||||||
DOCKED 0
|
|
||||||
BYPASS 0 0 0
|
|
||||||
<CLAP "CLAP: Sidechain Distortion (example) (Signalsmith Audio)" uk.co.signalsmith.dev.sidechain-distortion ""
|
|
||||||
CFG 0 0 0 ""
|
|
||||||
<OUT_PINS
|
|
||||||
>
|
|
||||||
<STATE
|
|
||||||
v2d2ZXJzaW9uAGVzdGF0Zb9ndmVyc2lvbgBnbGltaXREYr9ldmFsdWX7wDrCUmkT/3z/Z3RvbmVrSHq/ZXZhbHVl+0AVARbxhJ0S////
|
|
||||||
>
|
|
||||||
>
|
|
||||||
FLOATPOS 0 0 0 0
|
|
||||||
FXID {1F02CE63-5B2C-DD40-B8DA-6796E8E4CD70}
|
|
||||||
WAK 0 0
|
|
||||||
>
|
|
||||||
>
|
|
||||||
<TRACK {D098F2BD-6569-F946-9F66-5C0548DFE5B1}
|
|
||||||
NAME "Welcome Pad"
|
|
||||||
PEAKCOL 16576
|
PEAKCOL 16576
|
||||||
BEAT -1
|
BEAT -1
|
||||||
AUTOMODE 0
|
AUTOMODE 0
|
||||||
@ -181,180 +135,25 @@
|
|||||||
MUTESOLO 0 0 0
|
MUTESOLO 0 0 0
|
||||||
IPHASE 0
|
IPHASE 0
|
||||||
PLAYOFFS 0 1
|
PLAYOFFS 0 1
|
||||||
ISBUS 2 -1
|
|
||||||
BUSCOMP 0 0 0 0 0
|
|
||||||
SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0
|
|
||||||
FIXEDLANES 9 0 0 0 0
|
|
||||||
SEL 0
|
|
||||||
REC 0 0 1 0 0 0 0 0
|
|
||||||
VU 2
|
|
||||||
TRACKHEIGHT 0 0 0 0 0 0 0
|
|
||||||
INQ 0 0 0 0.5 100 0 0 100
|
|
||||||
NCHAN 2
|
|
||||||
FX 1
|
|
||||||
TRACKID {D098F2BD-6569-F946-9F66-5C0548DFE5B1}
|
|
||||||
PERF 0
|
|
||||||
MIDIOUT -1
|
|
||||||
MAINSEND 1 0
|
|
||||||
<ITEM
|
|
||||||
POSITION 0
|
|
||||||
SNAPOFFS 0
|
|
||||||
LENGTH 12.5
|
|
||||||
LOOP 0
|
|
||||||
ALLTAKES 0
|
|
||||||
FADEIN 1 0 0 1 0 0 0
|
|
||||||
FADEOUT 1 0 0 1 0 0 0
|
|
||||||
MUTE 0 0
|
|
||||||
SEL 0
|
|
||||||
IGUID {96281C1B-0CD8-7A4C-BC7B-83E34DB38B24}
|
|
||||||
IID 4
|
|
||||||
NAME "Welcome Pad - stem"
|
|
||||||
VOLPAN 1 0 1 -1
|
|
||||||
SOFFS 0
|
|
||||||
PLAYRATE 1 1 0 -1 0 0.0025
|
|
||||||
CHANMODE 0
|
|
||||||
GUID {D46D1639-7253-D040-86A3-47C6E3A0FBCB}
|
|
||||||
<SOURCE WAVE
|
|
||||||
FILE "Media/sidechain-distortion_stems_Welcome Pad.wav"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
>
|
|
||||||
<TRACK {F4D9FA0E-571B-E24E-B158-4F619AA188D8}
|
|
||||||
NAME Filtered
|
|
||||||
PEAKCOL 16576
|
|
||||||
BEAT -1
|
|
||||||
AUTOMODE 0
|
|
||||||
PANLAWFLAGS 3
|
|
||||||
VOLPAN 0 0 -1 -1 1
|
|
||||||
MUTESOLO 0 0 0
|
|
||||||
IPHASE 0
|
|
||||||
PLAYOFFS 0 1
|
|
||||||
ISBUS 0 0
|
ISBUS 0 0
|
||||||
BUSCOMP 0 0 0 0 0
|
BUSCOMP 0 0 0 0 0
|
||||||
SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0
|
SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0
|
||||||
FIXEDLANES 9 0 0 0 0
|
FIXEDLANES 9 0 0 0 0
|
||||||
SEL 0
|
SEL 1
|
||||||
REC 0 0 1 0 0 0 0 0
|
REC 0 5088 1 7 0 0 0 0
|
||||||
VU 2
|
VU 2
|
||||||
TRACKHEIGHT 0 0 0 0 0 0 0
|
TRACKHEIGHT 0 0 0 0 0 0 0
|
||||||
INQ 0 0 0 0.5 100 0 0 100
|
INQ 0 0 0 0.5 100 0 0 100
|
||||||
NCHAN 2
|
NCHAN 2
|
||||||
FX 1
|
FX 1
|
||||||
TRACKID {F4D9FA0E-571B-E24E-B158-4F619AA188D8}
|
TRACKID {93E699D9-F890-234E-A113-7E0053A85B78}
|
||||||
PERF 0
|
PERF 0
|
||||||
MIDIOUT -1
|
MIDIOUT -1
|
||||||
MAINSEND 1 0
|
MAINSEND 1 0
|
||||||
<FXCHAIN
|
<FXCHAIN
|
||||||
WNDRECT 847 421 815 444
|
|
||||||
SHOW 2
|
|
||||||
LASTSEL 1
|
|
||||||
DOCKED 0
|
|
||||||
BYPASS 0 0 0
|
|
||||||
<JS "Geraint's JSFX/Utility/Panalysis/Panalysis.jsfx" ""
|
|
||||||
1 0 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
>
|
|
||||||
<JS_SER
|
|
||||||
zczMPQAAAAAAAAAA
|
|
||||||
>
|
|
||||||
FLOATPOS 0 0 0 0
|
|
||||||
FXID {6A5C4484-079E-F34D-8A18-CB456B094205}
|
|
||||||
WAK 0 0
|
|
||||||
BYPASS 0 0 0
|
|
||||||
<VST "VST: ReaEQ (Cockos)" reaeq.vst.dylib 0 "" 1919247729<56535472656571726561657100000000> ""
|
|
||||||
cWVlcu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAagAAAAEAAAAAABAA
|
|
||||||
IQAAAAIAAAAEAAAAAQAAAMS7LCro8zZAAAAAAAAA8D9GtvP91Hj1PwEDAAAAAQAAAHj7zCB2ucJAdouc1Fqk9D8QWDm0yHbyPwEBAAAAAQAAAAAAAAAAAPA/AAAAAEAC
|
|
||||||
AABrAQAAAgAAAA==
|
|
||||||
AAAQAAAA
|
|
||||||
>
|
|
||||||
FLOATPOS 0 0 0 0
|
|
||||||
FXID {5262179C-F519-424F-8C34-4765E9AE04E7}
|
|
||||||
WAK 0 0
|
|
||||||
>
|
|
||||||
<ITEM
|
|
||||||
POSITION 0
|
|
||||||
SNAPOFFS 0
|
|
||||||
LENGTH 12.5
|
|
||||||
LOOP 0
|
|
||||||
ALLTAKES 0
|
|
||||||
FADEIN 1 0 0 1 0 0 0
|
|
||||||
FADEOUT 1 0 0 1 0 0 0
|
|
||||||
MUTE 0 0
|
|
||||||
SEL 1
|
|
||||||
IGUID {102A7029-390F-704B-AE82-A4E1AA02CD03}
|
|
||||||
IID 8
|
|
||||||
NAME "Welcome Pad - stem"
|
|
||||||
VOLPAN 1 0 1 -1
|
|
||||||
SOFFS 0
|
|
||||||
PLAYRATE 1 1 0 -1 0 0.0025
|
|
||||||
CHANMODE 0
|
|
||||||
GUID {6D40E4DA-D9A6-5149-915A-965CEB60DCAE}
|
|
||||||
<SOURCE WAVE
|
|
||||||
FILE "Media/sidechain-distortion_stems_Welcome Pad.wav"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
>
|
|
||||||
<TRACK {F52BCBA5-0B54-6B40-8D30-4E09891F36EC}
|
|
||||||
NAME Drums
|
|
||||||
PEAKCOL 16576
|
|
||||||
BEAT -1
|
|
||||||
AUTOMODE 0
|
|
||||||
PANLAWFLAGS 3
|
|
||||||
VOLPAN 0 0 -1 -1 1
|
|
||||||
MUTESOLO 1 0 0
|
|
||||||
IPHASE 0
|
|
||||||
PLAYOFFS 0 1
|
|
||||||
ISBUS 0 0
|
|
||||||
BUSCOMP 0 0 0 0 0
|
|
||||||
SHOWINMIX 1 0.6667 0.5 1 0.5 0 0 0
|
|
||||||
FIXEDLANES 9 0 0 0 0
|
|
||||||
SEL 0
|
|
||||||
REC 0 0 1 0 0 0 0 0
|
|
||||||
VU 2
|
|
||||||
TRACKHEIGHT 0 0 0 0 0 0 0
|
|
||||||
INQ 0 0 0 0.5 100 0 0 100
|
|
||||||
NCHAN 2
|
|
||||||
FX 1
|
|
||||||
TRACKID {F52BCBA5-0B54-6B40-8D30-4E09891F36EC}
|
|
||||||
PERF 0
|
|
||||||
MIDIOUT -1
|
|
||||||
MAINSEND 1 0
|
|
||||||
<FXCHAIN
|
|
||||||
WNDRECT 900 170 815 444
|
|
||||||
SHOW 0
|
SHOW 0
|
||||||
LASTSEL 0
|
LASTSEL 0
|
||||||
DOCKED 0
|
DOCKED 0
|
||||||
BYPASS 0 0 0
|
|
||||||
<VST "VST: ReaEQ (Cockos)" reaeq.vst.dylib 0 "" 1919247729<56535472656571726561657100000000> ""
|
|
||||||
cWVlcu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAagAAAAEAAAAAABAA
|
|
||||||
IQAAAAIAAAADAAAAAQAAAJ0Q6aODFJFAAAAAAAAA8D+amZmZmZnpPwEEAAAAAQAAADNAfmGLZFZANFOMBg1H7z9/arx0kxjsPwEBAAAAAQAAAModDNkGk/o/AAAAAEAC
|
|
||||||
AABrAQAAAgAAAA==
|
|
||||||
AAAQAAAA
|
|
||||||
>
|
|
||||||
FLOATPOS 0 0 0 0
|
|
||||||
FXID {4CCB9CFD-8185-AE45-8B5D-ADB8ED3C4344}
|
|
||||||
WAK 0 0
|
|
||||||
>
|
|
||||||
<ITEM
|
|
||||||
POSITION 0
|
|
||||||
SNAPOFFS 0
|
|
||||||
LENGTH 12.5
|
|
||||||
LOOP 1
|
|
||||||
ALLTAKES 0
|
|
||||||
FADEIN 1 0 0 1 0 0 0
|
|
||||||
FADEOUT 1 0 0 1 0 0 0
|
|
||||||
MUTE 0 0
|
|
||||||
SEL 0
|
|
||||||
IGUID {AEF812EA-814E-294B-8BFA-79DB6C6C36A2}
|
|
||||||
IID 1
|
|
||||||
NAME Drums.wav
|
|
||||||
VOLPAN 1 0 1 -1
|
|
||||||
SOFFS 0
|
|
||||||
PLAYRATE 1 1 0 -1 0 0.0025
|
|
||||||
CHANMODE 0
|
|
||||||
GUID {DE8AA9D2-A674-E747-9071-19071A8A8C7E}
|
|
||||||
<SOURCE WAVE
|
|
||||||
FILE "Media/Drums.wav"
|
|
||||||
>
|
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
# define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl;
|
# define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "signalsmith-basics/analyser.h"
|
||||||
#include "signalsmith-basics/crunch.h"
|
#include "signalsmith-basics/crunch.h"
|
||||||
#include "signalsmith-basics/limiter.h"
|
#include "signalsmith-basics/limiter.h"
|
||||||
#include "signalsmith-basics/reverb.h"
|
#include "signalsmith-basics/reverb.h"
|
||||||
@ -24,7 +25,7 @@ bool clap_init(const char *path) {
|
|||||||
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
||||||
CLAP_PLUGIN_FEATURE_DISTORTION,
|
CLAP_PLUGIN_FEATURE_DISTORTION,
|
||||||
});
|
});
|
||||||
|
|
||||||
plugins.add<signalsmith::basics::LimiterSTFX>({
|
plugins.add<signalsmith::basics::LimiterSTFX>({
|
||||||
.clap_version = CLAP_VERSION,
|
.clap_version = CLAP_VERSION,
|
||||||
.id = "uk.co.signalsmith.basics.limiter",
|
.id = "uk.co.signalsmith.basics.limiter",
|
||||||
@ -38,7 +39,7 @@ bool clap_init(const char *path) {
|
|||||||
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
||||||
CLAP_PLUGIN_FEATURE_LIMITER,
|
CLAP_PLUGIN_FEATURE_LIMITER,
|
||||||
});
|
});
|
||||||
|
|
||||||
plugins.add<signalsmith::basics::ReverbSTFX>({
|
plugins.add<signalsmith::basics::ReverbSTFX>({
|
||||||
.clap_version = CLAP_VERSION,
|
.clap_version = CLAP_VERSION,
|
||||||
.id = "uk.co.signalsmith.basics.reverb",
|
.id = "uk.co.signalsmith.basics.reverb",
|
||||||
@ -52,6 +53,21 @@ bool clap_init(const char *path) {
|
|||||||
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
||||||
CLAP_PLUGIN_FEATURE_REVERB,
|
CLAP_PLUGIN_FEATURE_REVERB,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
plugins.add<signalsmith::basics::AnalyserSTFX>({
|
||||||
|
.clap_version = CLAP_VERSION,
|
||||||
|
.id = "uk.co.signalsmith.basics.analyser",
|
||||||
|
.name = "[Basics] Analyser",
|
||||||
|
.vendor = "Signalsmith Audio",
|
||||||
|
.url = "",
|
||||||
|
.manual_url = "",
|
||||||
|
.support_url = "",
|
||||||
|
.version = "1.0.0"
|
||||||
|
}, {
|
||||||
|
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
||||||
|
CLAP_PLUGIN_FEATURE_DISTORTION,
|
||||||
|
});
|
||||||
|
|
||||||
return plugins.clap_init(path);
|
return plugins.clap_init(path);
|
||||||
}
|
}
|
||||||
void clap_deinit() {
|
void clap_deinit() {
|
||||||
|
|||||||
@ -1,8 +1,34 @@
|
|||||||
#include "clap/entry.h"
|
#include "clap/entry.h"
|
||||||
|
/*
|
||||||
|
#include "../../stfx/clap/stfx-clap.h"
|
||||||
|
|
||||||
extern bool clap_init(const char *);
|
stfx::clap::Plugins stfxPlugins;
|
||||||
|
extern void addAnalyser();
|
||||||
|
extern void addCrunch();
|
||||||
|
extern void addLimiter();
|
||||||
|
extern void addReverb();
|
||||||
|
|
||||||
|
bool clap_init(const char *path) {
|
||||||
|
static bool added = false;
|
||||||
|
if (!added) {
|
||||||
|
addAnalyser();
|
||||||
|
addCrunch();
|
||||||
|
addLimiter();
|
||||||
|
addReverb();
|
||||||
|
}
|
||||||
|
return added = stfxPlugins.clap_init(path);
|
||||||
|
}
|
||||||
|
void clap_deinit() {
|
||||||
|
stfxPlugins.clap_deinit();
|
||||||
|
}
|
||||||
|
const void * clap_get_factory(const char *id) {
|
||||||
|
return stfxPlugins.clap_get_factory(id);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
extern bool clap_init(const char *path);
|
||||||
extern void clap_deinit();
|
extern void clap_deinit();
|
||||||
extern const void * clap_get_factory(const char *);
|
extern const void * clap_get_factory(const char *id);
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
const CLAP_EXPORT clap_plugin_entry clap_entry{
|
const CLAP_EXPORT clap_plugin_entry clap_entry{
|
||||||
|
|||||||
14
clap/wclap-tar.sh
Executable file
14
clap/wclap-tar.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
name="$(basename $1)"
|
||||||
|
|
||||||
|
outDir="${2:-..}"
|
||||||
|
pushd "${outDir}"
|
||||||
|
outDir=`pwd`
|
||||||
|
popd
|
||||||
|
|
||||||
|
echo $name
|
||||||
|
|
||||||
|
cd $1
|
||||||
|
rm -f "${outDir}/${name}.tar.gz"
|
||||||
|
tar --exclude=".*" -vczf "${outDir}/${name}.tar.gz" *
|
||||||
1
include/signalsmith-basics/analyser.h
Normal file
1
include/signalsmith-basics/analyser.h
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "../../analyser.h"
|
||||||
1
modules/linear
Submodule
1
modules/linear
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 4c8999385c725e619723346bf8f415010fed9c4c
|
||||||
@ -20,7 +20,7 @@ struct Plugin;
|
|||||||
// A helper to make a CLAP plugin factory from STFX templates
|
// A helper to make a CLAP plugin factory from STFX templates
|
||||||
struct Plugins {
|
struct Plugins {
|
||||||
template<template<class> class EffectSTFX, class ...Args>
|
template<template<class> class EffectSTFX, class ...Args>
|
||||||
void add(clap_plugin_descriptor desc, std::initializer_list<const char *> features, Args ...args) {
|
size_t add(clap_plugin_descriptor desc, std::initializer_list<const char *> features, Args ...args) {
|
||||||
size_t index = featureLists.size();
|
size_t index = featureLists.size();
|
||||||
|
|
||||||
featureLists.emplace_back(features);
|
featureLists.emplace_back(features);
|
||||||
@ -31,6 +31,7 @@ struct Plugins {
|
|||||||
creates.push_back([=](const clap_host *host){
|
creates.push_back([=](const clap_host *host){
|
||||||
return new Plugin<EffectSTFX>(*this, &descriptors[index], host, args...);
|
return new Plugin<EffectSTFX>(*this, &descriptors[index], host, args...);
|
||||||
});
|
});
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool clap_init(const char *path) {
|
bool clap_init(const char *path) {
|
||||||
@ -651,3 +652,5 @@ struct Plugin : public clap_plugin {
|
|||||||
};
|
};
|
||||||
|
|
||||||
}} // namespace
|
}} // namespace
|
||||||
|
|
||||||
|
extern stfx::clap::Plugins stfxPlugins;
|
||||||
|
|||||||
@ -164,7 +164,9 @@ namespace stfx {
|
|||||||
// per-block automation gradient rate
|
// per-block automation gradient rate
|
||||||
double blockFade;
|
double blockFade;
|
||||||
// we might need some additional setup on the fiand/or after a directly
|
// we might need some additional setup on the fiand/or after a directly
|
||||||
bool firstBlockAfterReset = false;
|
bool firstBlockAfterReset;
|
||||||
|
bool metersRequested;
|
||||||
|
bool &metersChecked;
|
||||||
|
|
||||||
template<class Param>
|
template<class Param>
|
||||||
bool paramsChanging(Param ¶m) const {
|
bool paramsChanging(Param ¶m) const {
|
||||||
@ -178,7 +180,7 @@ namespace stfx {
|
|||||||
// Block length in samples
|
// Block length in samples
|
||||||
int length;
|
int length;
|
||||||
|
|
||||||
Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset=false) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), length(length) {}
|
Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset, bool wantsMeters, bool &metersChecked) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), metersRequested(wantsMeters), metersChecked(metersChecked), length(length) {}
|
||||||
// Not copyable, because that's probably a mistake
|
// Not copyable, because that's probably a mistake
|
||||||
Block(const Block &) = delete;
|
Block(const Block &) = delete;
|
||||||
Block & operator =(const Block&) = delete;
|
Block & operator =(const Block&) = delete;
|
||||||
@ -278,8 +280,9 @@ namespace stfx {
|
|||||||
callback(0, length);
|
callback(0, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr bool wantsMeters() const {
|
bool wantsMeters() const {
|
||||||
return false;
|
metersChecked = true;
|
||||||
|
return metersRequested;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -358,6 +361,22 @@ namespace stfx {
|
|||||||
|
|
||||||
template<class Presets>
|
template<class Presets>
|
||||||
void presets(Presets &) {}
|
void presets(Presets &) {}
|
||||||
|
|
||||||
|
// passes ownership of any meter values back to the audio thread
|
||||||
|
void wantsMeters(bool meters=true) {
|
||||||
|
metersReady.clear();
|
||||||
|
if (meters) {
|
||||||
|
metersRequested.test_and_set();
|
||||||
|
} else {
|
||||||
|
metersRequested.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// whether the meter values can be read
|
||||||
|
bool hasMeters() const {
|
||||||
|
return metersReady.test();
|
||||||
|
}
|
||||||
|
protected:
|
||||||
|
std::atomic_flag metersRequested = ATOMIC_FLAG_INIT, metersReady = ATOMIC_FLAG_INIT;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Creates an effect class from an effect template, with optional extra config.
|
/// Creates an effect class from an effect template, with optional extra config.
|
||||||
@ -493,7 +512,7 @@ namespace stfx {
|
|||||||
process(buffers, buffers, blockLength);
|
process(buffers, buffers, blockLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`.
|
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.processSTFX(io, config, block)`.
|
||||||
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
|
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
|
||||||
template<class Inputs, class Outputs>
|
template<class Inputs, class Outputs>
|
||||||
void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
|
void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
|
||||||
@ -522,7 +541,8 @@ namespace stfx {
|
|||||||
Outputs output;
|
Outputs output;
|
||||||
};
|
};
|
||||||
Io io{inputs, outputs};
|
Io io{inputs, outputs};
|
||||||
Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset);
|
bool metersChecked = false;
|
||||||
|
Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset, this->metersRequested.test(), metersChecked);
|
||||||
|
|
||||||
((EffectClass *)this)->processSTFX(io, (const Config &)config, (const Block &)block);
|
((EffectClass *)this)->processSTFX(io, (const Config &)config, (const Block &)block);
|
||||||
|
|
||||||
@ -535,6 +555,12 @@ namespace stfx {
|
|||||||
justHadReset = false;
|
justHadReset = false;
|
||||||
for (auto param : params.rangeParams) param->_libEndBlock();
|
for (auto param : params.rangeParams) param->_libEndBlock();
|
||||||
for (auto param : params.stepParams) param->_libEndBlock();
|
for (auto param : params.stepParams) param->_libEndBlock();
|
||||||
|
|
||||||
|
// Meters are filled - pass ownership of meter values to the main thread
|
||||||
|
if (this->metersRequested.test() && metersChecked) {
|
||||||
|
this->metersRequested.clear();
|
||||||
|
this->metersReady.test_and_set();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "./storage.h"
|
#include "./storage.h"
|
||||||
|
#include "../stfx-library.h" // for the ...ParamIgnore classes
|
||||||
|
|
||||||
namespace stfx { namespace storage {
|
namespace stfx { namespace storage {
|
||||||
|
|
||||||
|
|||||||
@ -204,7 +204,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<class Item>
|
template<class Item>
|
||||||
void readValue(std::vector<Item> &array) {
|
void readVector(std::vector<Item> &array) {
|
||||||
if (!cbor.isArray()) return;
|
if (!cbor.isArray()) return;
|
||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
cbor = cbor.forEach([&](Cbor item, size_t index){
|
cbor = cbor.forEach([&](Cbor item, size_t index){
|
||||||
@ -217,13 +217,18 @@ private:
|
|||||||
array.resize(length);
|
array.resize(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<class Item>
|
||||||
|
void readValue(std::vector<Item> &array) {
|
||||||
|
readVector(array);
|
||||||
|
}
|
||||||
|
|
||||||
#define STORAGE_TYPED_ARRAY(T) \
|
#define STORAGE_TYPED_ARRAY(T) \
|
||||||
void readValue(std::vector<T> &array) { \
|
void readValue(std::vector<T> &array) { \
|
||||||
if (cbor.isTypedArray()) { \
|
if (cbor.isTypedArray()) { \
|
||||||
array.resize(cbor.typedArrayLength()); \
|
array.resize(cbor.typedArrayLength()); \
|
||||||
cbor.readTypedArray(array); \
|
cbor.readTypedArray(array); \
|
||||||
} else { \
|
} else { \
|
||||||
readValue<std::vector<T>>(array); \
|
readVector<T>(array); \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
STORAGE_TYPED_ARRAY(uint8_t)
|
STORAGE_TYPED_ARRAY(uint8_t)
|
||||||
|
|||||||
@ -7,15 +7,15 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-areas: "header" "params";
|
flex-direction: column;
|
||||||
grid-template-rows: min-content 1fr;
|
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
transform: scale(0.95, 1);
|
transform: scale(0.95, 1);
|
||||||
}
|
}
|
||||||
#params {
|
#params {
|
||||||
grid-area: params;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: space-around;
|
align-content: space-around;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
@ -96,6 +96,27 @@
|
|||||||
.param-range-name {
|
.param-range-name {
|
||||||
grid-area: name;
|
grid-area: name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#plots {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 10;
|
||||||
|
|
||||||
|
}
|
||||||
|
.plot {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 100;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(#222, #000 2rem);
|
||||||
|
}
|
||||||
|
.plot canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -128,55 +149,188 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
<script>
|
||||||
|
function drawDial(data, canvas) {
|
||||||
|
let scale = window.devicePixelRatio;
|
||||||
|
let pixels = canvas.offsetWidth*scale;
|
||||||
|
if (canvas.width != pixels) canvas.width = pixels;
|
||||||
|
if (canvas.height != pixels) canvas.height = pixels;
|
||||||
|
|
||||||
|
let context = canvas.getContext('2d');
|
||||||
|
context.resetTransform();
|
||||||
|
context.clearRect(0, 0, pixels, pixels);
|
||||||
|
|
||||||
|
context.scale(pixels/2, pixels/2);
|
||||||
|
context.translate(1, 1);
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(0, 0, 0.8, 0.8, 0, 0, Math.PI*2);
|
||||||
|
context.fillStyle = '#FFF';
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
context.lineWidth = 0.2;
|
||||||
|
context.lineCap = 'round';
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*0.25);
|
||||||
|
context.strokeStyle = '#0001';
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
let rangeUnit = data.rangeUnit;
|
||||||
|
if (data.gesture) rangeUnit = data._gestureUnit;
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*(-1.249 + rangeUnit*1.499));
|
||||||
|
context.strokeStyle = '#000';
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</section>
|
</section>
|
||||||
|
<template @foreach>
|
||||||
|
<div class="plot" @if="${d => d.$type == 'Spectrum'}">
|
||||||
|
<canvas class="plot-grid" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
||||||
|
<canvas class="plot-data" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
||||||
|
<canvas class="plot-labels" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<script>
|
<script>
|
||||||
function drawDial(data, canvas) {
|
let plotColours = ['#8CF', '#FC8', '#8D8', '#F9B'];
|
||||||
let scale = window.devicePixelRatio;
|
function freqScale(width, lowHz, highHz) {
|
||||||
let pixels = canvas.offsetWidth*scale;
|
// let a = -289.614, b = 1176.76, c = 15.3385, d = -0.552833; // Bark
|
||||||
if (canvas.width != pixels) canvas.width = pixels;
|
let low = lowHz/(1500 + lowHz);
|
||||||
if (canvas.height != pixels) canvas.height = pixels;
|
let high = highHz/(1500 + highHz);
|
||||||
|
|
||||||
|
let scale = width/(high - low);
|
||||||
|
return hz => {
|
||||||
|
let v = hz/(1500 + hz);
|
||||||
|
return scale*(v - low);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function drawSpectrum(data, canvas) {
|
||||||
let context = canvas.getContext('2d');
|
let context = canvas.getContext('2d');
|
||||||
context.resetTransform();
|
let width = canvas.offsetWidth, height = canvas.offsetHeight;
|
||||||
context.clearRect(0, 0, pixels, pixels);
|
{
|
||||||
|
let pixelWidth = Math.round(width*window.devicePixelRatio);
|
||||||
|
let pixelHeight = Math.round(height*devicePixelRatio);
|
||||||
|
if (canvas.width != pixelWidth) canvas.width = pixelWidth;
|
||||||
|
if (canvas.height != pixelHeight) canvas.height = pixelHeight;
|
||||||
|
|
||||||
|
context.resetTransform();
|
||||||
|
context.clearRect(0, 0, pixelWidth, pixelHeight);
|
||||||
|
context.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
context.scale(pixels/2, pixels/2);
|
let baseHz = 100, maxHz = data.hz[data.hz.length - 1];
|
||||||
context.translate(1, 1);
|
let xScale = freqScale(width, data.hz[0], maxHz);
|
||||||
|
let yScale = e => height*Math.log10(e + 1e-30)*10/-105;
|
||||||
|
yScale = e => {
|
||||||
|
e = Math.pow(e, 0.12);
|
||||||
|
e = e*0.6/(1 - 0.6 - e + 2*e*0.6);
|
||||||
|
return height*(1 - e);
|
||||||
|
};
|
||||||
|
|
||||||
context.beginPath();
|
if (canvas.classList.contains('plot-grid')) {
|
||||||
context.ellipse(0, 0, 0.8, 0.8, 0, 0, Math.PI*2);
|
context.beginPath();
|
||||||
context.fillStyle = '#FFF';
|
context.lineWidth = 1;
|
||||||
context.fill();
|
context.strokeStyle = '#555';
|
||||||
|
for (let baseHz = 100; baseHz < maxHz; baseHz *= 10) {
|
||||||
context.lineWidth = 0.2;
|
for (let i = 1; i < 10; ++i) {
|
||||||
context.lineCap = 'round';
|
let hz = baseHz*i;
|
||||||
context.beginPath();
|
let x = xScale(hz);
|
||||||
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*0.25);
|
context.moveTo(x, 0);
|
||||||
context.strokeStyle = '#0001';
|
context.lineTo(x, height);
|
||||||
context.stroke();
|
}
|
||||||
|
}
|
||||||
let rangeUnit = data.rangeUnit;
|
for (let db = 0; db >= -240; db -= 12) {
|
||||||
if (data.gesture) rangeUnit = data._gestureUnit;
|
let e = Math.pow(10, db/10);
|
||||||
|
let y = yScale(e);
|
||||||
context.beginPath();
|
context.moveTo(0, y);
|
||||||
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*(-1.249 + rangeUnit*1.499));
|
context.lineTo(width, y);
|
||||||
context.strokeStyle = '#000';
|
}
|
||||||
context.stroke();
|
context.stroke();
|
||||||
|
} else if (canvas.classList.contains('plot-data')) {
|
||||||
|
context.lineWidth = 1.5;
|
||||||
|
context.lineCap = 'round';
|
||||||
|
context.lineJoin = 'round';
|
||||||
|
|
||||||
|
let xMapped = Matsui.getRaw(data.hz).map(xScale);
|
||||||
|
data.energy.forEach((line, index) => {
|
||||||
|
context.strokeStyle = plotColours[index%plotColours.length];
|
||||||
|
context.globalAlpha = 6/(6 + index);
|
||||||
|
context.beginPath();
|
||||||
|
for (let i = 0; i < xMapped.length; ++i) {
|
||||||
|
let e = line[i];
|
||||||
|
let y = yScale(e);
|
||||||
|
context.lineTo(xMapped[i], y);
|
||||||
|
}
|
||||||
|
context.stroke();
|
||||||
|
if (1) {
|
||||||
|
context.fillStyle = plotColours[index%plotColours.length];
|
||||||
|
context.lineTo(width, height);
|
||||||
|
context.lineTo(0, height);
|
||||||
|
context.globalAlpha = 2/(10 + index);
|
||||||
|
context.fill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeStyle = '#0006';
|
||||||
|
context.fillStyle = '#DDD';
|
||||||
|
for (let baseHz = 100; baseHz < maxHz; baseHz *= 10) {
|
||||||
|
let text = (baseHz < 1000) ? baseHz + "Hz" : baseHz/1000 + "kHz";
|
||||||
|
context.strokeText(text, xScale(baseHz) + 2, height - 2);
|
||||||
|
context.fillText(text, xScale(baseHz) + 2, height - 2);
|
||||||
|
}
|
||||||
|
for (let db = 0; db > -105; db -= (db <= -48 ? 24 : 12)) {
|
||||||
|
let e = Math.pow(10, db/10);
|
||||||
|
let y = yScale(e);
|
||||||
|
context.strokeText(db + "dB", 2, y + 4 + 4*(db == 0));
|
||||||
|
context.fillText(db + "dB", 2, y + 4 + 4*(db == 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="cbor.min.js"></script>
|
<script src="cbor.min.js"></script>
|
||||||
<script src="matsui-bundle.min.js"></script>
|
<script src="matsui-bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
Matsui.global.attributes.resize = (element, fn) => {
|
||||||
|
element.classList.add('_matsuiResize');
|
||||||
|
element._matsuiResize = fn;
|
||||||
|
};
|
||||||
|
addEventListener('resize', e => {
|
||||||
|
document.querySelectorAll('._matsuiResize').forEach(e => {
|
||||||
|
let fn = e._matsuiResize;
|
||||||
|
if (fn) fn(e, e.offsetWidth, e.offsetHeight);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let state = Matsui.replace(document.body, {name: "..."});
|
let state = Matsui.replace(document.body, {name: "..."});
|
||||||
state.trackMerges(merge => {
|
state.trackMerges(merge => {
|
||||||
window.parent.postMessage(CBOR.encode(merge), '*');
|
window.parent.postMessage(CBOR.encode(merge), '*');
|
||||||
});
|
});
|
||||||
addEventListener('message', e => {
|
addEventListener('message', e => {
|
||||||
state.merge(CBOR.decode(e.data));
|
let merge = CBOR.decode(e.data);
|
||||||
|
window.merge = merge;//console.log(merge);
|
||||||
|
state.merge(merge);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.parent.postMessage(CBOR.encode("ready"), '*');
|
window.parent.postMessage(CBOR.encode("ready"), '*');
|
||||||
|
|
||||||
|
// Monitor framerate and send every 200ms
|
||||||
|
let frameMs = 100, prevFrame = Date.now(), prevSent = 100;
|
||||||
|
requestAnimationFrame(function nextFrame() {
|
||||||
|
let now = Date.now(), delta = now - prevFrame;
|
||||||
|
frameMs += (delta - frameMs)*frameMs/1000;
|
||||||
|
prevFrame = now;
|
||||||
|
|
||||||
|
prevSent += delta;
|
||||||
|
if (prevSent > 200) {
|
||||||
|
window.parent.postMessage(CBOR.encode(["meters", frameMs/1000, 0.3]));
|
||||||
|
prevSent = 0;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(nextFrame);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4
stfx/ui/html/matsui-bundle.min.js
vendored
4
stfx/ui/html/matsui-bundle.min.js
vendored
File diff suppressed because one or more lines are too long
316
stfx/ui/web-ui.h
316
stfx/ui/web-ui.h
@ -9,6 +9,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <iostream> // we log to stderr if our queue gets full
|
||||||
|
|
||||||
namespace stfx { namespace web {
|
namespace stfx { namespace web {
|
||||||
|
|
||||||
@ -79,29 +80,20 @@ This produces a template with inheritance like:
|
|||||||
The WebBase template replaces the ParamRange/ParamStepped classes, so that the UI can be updated when they change. The outer WebSTFX class adds methods for forwarding messages between the effect and its webview.
|
The WebBase template replaces the ParamRange/ParamStepped classes, so that the UI can be updated when they change. The outer WebSTFX class adds methods for forwarding messages between the effect and its webview.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
template<template<class, class...> class EffectSTFX>
|
template<template<class, class...> class EffectSTFX, class SubClass>
|
||||||
struct WebUIHelper {
|
struct WebUIHelper {
|
||||||
|
|
||||||
template<class Effect, class SubClass>
|
template<class Effect>
|
||||||
class WebBase : public Effect {
|
class WebBase : public Effect {
|
||||||
using SuperRange = typename Effect::ParamRange;
|
using SuperRange = typename Effect::ParamRange;
|
||||||
using SuperStepped = typename Effect::ParamStepped;
|
using SuperStepped = typename Effect::ParamStepped;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct WebParamScanner {
|
struct WebParamScanner : public storage::STFXStorageScanner<WebParamScanner>{
|
||||||
SubClass *effect;
|
SubClass *effect;
|
||||||
std::vector<std::string> scope;
|
std::vector<std::string> scope;
|
||||||
|
|
||||||
WebParamScanner(SubClass *effect) : effect(effect) {}
|
WebParamScanner(SubClass *effect) : effect(effect) {}
|
||||||
|
|
||||||
// Do nothing for most types
|
|
||||||
template<class V>
|
|
||||||
void operator()(const char *key, V &v) {
|
|
||||||
scope.emplace_back(key);
|
|
||||||
// TODO: detect objects with .state() somehow
|
|
||||||
v.state(*this);
|
|
||||||
scope.pop_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra methods we add for STFX storage
|
// Extra methods we add for STFX storage
|
||||||
void info(const char *, const char *) {}
|
void info(const char *, const char *) {}
|
||||||
@ -278,140 +270,184 @@ struct WebUIHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<class Storage>
|
||||||
|
void meterState(Storage &storage) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
template<class Effect, class... ExtraArgs>
|
template<class Effect, class... ExtraArgs>
|
||||||
struct WebSTFX : public EffectSTFX<WebBase<Effect, WebSTFX<Effect, ExtraArgs...>>, ExtraArgs...> {
|
struct WebSTFX : public EffectSTFX<WebBase<Effect>, ExtraArgs...> {
|
||||||
/* TODO: without the ExtraArgs, it would be a bit neater:
|
using Super = EffectSTFX<WebBase<Effect>, ExtraArgs...>;
|
||||||
EffectSTFX<WebBase<Effect, WebSTFX<Effect>>>
|
|
||||||
*/
|
|
||||||
using Super = EffectSTFX<WebBase<Effect, WebSTFX<Effect, ExtraArgs...>>, ExtraArgs...>;
|
|
||||||
|
|
||||||
template<class... Args>
|
|
||||||
WebSTFX(Args... args) : Super(args...) {
|
|
||||||
typename Super::WebParamScanner scanner{this};
|
|
||||||
this->state(scanner);
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are called when the parameter is changed from the web UI
|
|
||||||
std::function<void(ParamContext, double)> paramListenerRange;
|
|
||||||
std::function<void(ParamContext, bool)> paramListenerGesture;
|
|
||||||
std::function<void(ParamContext, int)> paramListenerStepped;
|
|
||||||
|
|
||||||
// This state includes all the parameters
|
|
||||||
void saveState(std::vector<unsigned char> &bytes) {
|
|
||||||
stfx::storage::STFXStorageWriter<> storage(bytes);
|
|
||||||
this->state(storage);
|
|
||||||
}
|
|
||||||
void loadState(const std::vector<unsigned char> &bytes) {
|
|
||||||
stfx::storage::STFXStorageReader<> storage(bytes);
|
|
||||||
this->state(storage);
|
|
||||||
requestEntireState();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WebMessage {
|
|
||||||
std::vector<unsigned char> bytes;
|
|
||||||
|
|
||||||
WebMessage() {
|
|
||||||
bytes.reserve(256);
|
|
||||||
}
|
|
||||||
|
|
||||||
void sent() {
|
|
||||||
readyToSend.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class WebSTFX;
|
|
||||||
friend struct Super::ParamRange;
|
|
||||||
friend struct Super::ParamStepped;
|
|
||||||
std::atomic_flag readyToSend = ATOMIC_FLAG_INIT;
|
|
||||||
|
|
||||||
void markReady() {
|
|
||||||
readyToSend.test_and_set();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
bool hasPendingWebMessage() const {
|
|
||||||
auto &message = queue[readIndex];
|
|
||||||
return message.readyToSend.test();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll on the main thread (and directly after any `.webReceive()`), calling `.sent()` after you've sent it, repeat until you get `nullptr`
|
|
||||||
WebMessage * getPendingWebMessage() {
|
|
||||||
auto &message = queue[readIndex];
|
|
||||||
if (!message.readyToSend.test()) return nullptr;
|
|
||||||
if (++readIndex == queue.size()) readIndex = 0;
|
|
||||||
|
|
||||||
if (resetQueue.test()) {
|
|
||||||
// Clear ("send") every message aside from this one
|
|
||||||
for (auto &m : queue) {
|
|
||||||
if (&m == &message) continue;
|
|
||||||
m.sent();
|
|
||||||
}
|
|
||||||
// Ready to start sending messages again, starting from the next index
|
|
||||||
writeIndex.store(readIndex);
|
|
||||||
resetQueue.clear();
|
|
||||||
// any messages (state updates) sent after this won't assume a *newer* state than what we serialise below
|
|
||||||
|
|
||||||
// Replace this one's message with the complete plugin state
|
|
||||||
WebStateWriter storage(message.bytes);
|
|
||||||
this->state(storage);
|
|
||||||
}
|
|
||||||
return &message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call when the webview posts any message back
|
|
||||||
void webReceive(const void *message, size_t size) {
|
|
||||||
auto *bytes = (const unsigned char *)message;
|
|
||||||
signalsmith::cbor::TaggedCborWalker cbor(bytes, bytes + size);
|
|
||||||
|
|
||||||
if (cbor.isUtf8()) {
|
|
||||||
if (cbor.utf8View() == "ready") {
|
|
||||||
requestEntireState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (cbor.isMap()) {
|
|
||||||
// Apply it as a merge
|
|
||||||
WebStateReader reader(cbor);
|
|
||||||
this->state(reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend struct Super::ParamRange;
|
|
||||||
friend struct Super::ParamStepped;
|
|
||||||
|
|
||||||
std::vector<WebMessage> queue = std::vector<WebMessage>(64); // power of 2 so that overflow doesn't mess with the modulo
|
|
||||||
size_t readIndex = 0;
|
|
||||||
std::atomic_flag resetQueue = ATOMIC_FLAG_INIT;
|
|
||||||
|
|
||||||
// Atomic because multiple threads might write
|
|
||||||
std::atomic<size_t> writeIndex = 0;
|
|
||||||
WebMessage * getEmptyMessage() {
|
|
||||||
if (resetQueue.test()) return nullptr;
|
|
||||||
auto reservedIndex = writeIndex.fetch_add(1);
|
|
||||||
auto &message = queue[reservedIndex%queue.size()];
|
|
||||||
if (message.readyToSend.test()) {
|
|
||||||
std::cerr << "Web message queue full - sending entire state instead\n";
|
|
||||||
// When our queue is full, we drop the entire thing and re-send the entire state
|
|
||||||
resetQueue.test_and_set();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
message.bytes.resize(0);
|
|
||||||
return &message;
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestEntireState() {
|
|
||||||
if (auto *m = getEmptyMessage()) {
|
|
||||||
resetQueue.test_and_set();
|
|
||||||
m->markReady();
|
|
||||||
} // if this fails, then the queue is full and we're doing a reset anyway
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use this instead of a plain LibraryEffect
|
// Use this instead of a plain LibraryEffect
|
||||||
template<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
|
template<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
|
||||||
using WebUILibraryEffect = LibraryEffect<Sample, WebUIHelper<EffectSTFX>::template WebSTFX, ExtraArgs...>;
|
struct WebUILibraryEffect : public LibraryEffect<Sample, WebUIHelper<EffectSTFX, WebUILibraryEffect<Sample, EffectSTFX, ExtraArgs...>>::template WebSTFX, ExtraArgs...> {
|
||||||
|
using Super = LibraryEffect<Sample, WebUIHelper<EffectSTFX, WebUILibraryEffect<Sample, EffectSTFX, ExtraArgs...>>::template WebSTFX, ExtraArgs...>;
|
||||||
|
|
||||||
|
template<class... Args>
|
||||||
|
WebUILibraryEffect(Args... args) : Super(args...) {
|
||||||
|
typename Super::WebParamScanner scanner{this};
|
||||||
|
this->state(scanner);
|
||||||
|
};
|
||||||
|
|
||||||
|
// These are called when the parameter is changed from the web UI
|
||||||
|
std::function<void(ParamContext, double)> paramListenerRange;
|
||||||
|
std::function<void(ParamContext, bool)> paramListenerGesture;
|
||||||
|
std::function<void(ParamContext, int)> paramListenerStepped;
|
||||||
|
|
||||||
|
// This state includes all the parameters
|
||||||
|
void saveState(std::vector<unsigned char> &bytes) {
|
||||||
|
stfx::storage::STFXStorageWriter<> storage(bytes);
|
||||||
|
this->state(storage);
|
||||||
|
}
|
||||||
|
void loadState(const std::vector<unsigned char> &bytes) {
|
||||||
|
stfx::storage::STFXStorageReader<> storage(bytes);
|
||||||
|
this->state(storage);
|
||||||
|
requestEntireState();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebMessage {
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
|
||||||
|
WebMessage() {
|
||||||
|
bytes.reserve(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sent() {
|
||||||
|
readyToSend.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WebUILibraryEffect;
|
||||||
|
friend struct Super::ParamRange;
|
||||||
|
friend struct Super::ParamStepped;
|
||||||
|
std::atomic_flag readyToSend = ATOMIC_FLAG_INIT;
|
||||||
|
|
||||||
|
void markReady() {
|
||||||
|
readyToSend.test_and_set();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool hasPendingWebMessage() const {
|
||||||
|
auto &message = queue[readIndex];
|
||||||
|
return message.readyToSend.test();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll on the main thread (and directly after any `.webReceive()`), calling `.sent()` after you've sent it, repeat until you get `nullptr`
|
||||||
|
WebMessage * getPendingWebMessage() {
|
||||||
|
auto &message = queue[readIndex];
|
||||||
|
if (!message.readyToSend.test()) return nullptr;
|
||||||
|
if (++readIndex == queue.size()) readIndex = 0;
|
||||||
|
|
||||||
|
if (resetQueue.test()) {
|
||||||
|
// Clear ("send") every message aside from this one
|
||||||
|
for (auto &m : queue) {
|
||||||
|
if (&m == &message) continue;
|
||||||
|
m.sent();
|
||||||
|
}
|
||||||
|
// Ready to start sending messages again, starting from the next index
|
||||||
|
writeIndex.store(readIndex);
|
||||||
|
resetQueue.clear();
|
||||||
|
// any messages (state updates) sent after this won't assume a *newer* state than what we serialise below
|
||||||
|
|
||||||
|
// Replace this one's message with the complete plugin state
|
||||||
|
WebStateWriter storage(message.bytes);
|
||||||
|
this->state(storage);
|
||||||
|
}
|
||||||
|
return &message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call when the webview posts any message back
|
||||||
|
void webReceive(const void *message, size_t size) {
|
||||||
|
auto *bytes = (const unsigned char *)message;
|
||||||
|
signalsmith::cbor::TaggedCborWalker cbor(bytes, bytes + size);
|
||||||
|
|
||||||
|
if (cbor.isUtf8()) {
|
||||||
|
if (cbor.utf8View() == "ready") {
|
||||||
|
requestEntireState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (cbor.isArray()) {
|
||||||
|
size_t length = cbor.length();
|
||||||
|
cbor = cbor.enter();
|
||||||
|
if (cbor.utf8View() == "meters" && length == 3) {
|
||||||
|
double interval = ++cbor;
|
||||||
|
double duration = ++cbor;
|
||||||
|
metersInterval.store(interval*this->config.sampleRate);
|
||||||
|
metersDuration.store(duration*this->config.sampleRate);
|
||||||
|
}
|
||||||
|
} else if (cbor.isMap()) {
|
||||||
|
// Apply it as a merge
|
||||||
|
WebStateReader reader(cbor);
|
||||||
|
this->state(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace `.process()` to add meter messages if they exist
|
||||||
|
template<class Buffers>
|
||||||
|
void process(Buffers &&buffers, int blockLength) {
|
||||||
|
process(buffers, buffers, blockLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Inputs, class Outputs>
|
||||||
|
void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
|
||||||
|
Super::process(inputs, outputs, blockLength);
|
||||||
|
|
||||||
|
if (this->hasMeters()) {
|
||||||
|
auto *m = getEmptyMessage();
|
||||||
|
if (m) {
|
||||||
|
signalsmith::cbor::CborWriter cbor{m->bytes};
|
||||||
|
WebStateWriter storage{cbor};
|
||||||
|
this->meterState(storage);
|
||||||
|
m->markReady();
|
||||||
|
}
|
||||||
|
this->wantsMeters(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metersDuration.load() > 0) {
|
||||||
|
metersDuration -= blockLength;
|
||||||
|
samplesSinceMeters += blockLength;
|
||||||
|
if (samplesSinceMeters > metersInterval.load()) {
|
||||||
|
this->wantsMeters();
|
||||||
|
samplesSinceMeters = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend struct Super::ParamRange;
|
||||||
|
friend struct Super::ParamStepped;
|
||||||
|
|
||||||
|
std::atomic<int> metersInterval = 0, metersDuration = 0;
|
||||||
|
int samplesSinceMeters = 0; // only used from `.process()`
|
||||||
|
|
||||||
|
std::vector<WebMessage> queue = std::vector<WebMessage>(64); // power of 2 so that overflow doesn't mess with the modulo
|
||||||
|
size_t readIndex = 0;
|
||||||
|
std::atomic_flag resetQueue = ATOMIC_FLAG_INIT;
|
||||||
|
|
||||||
|
// Atomic because multiple threads might write
|
||||||
|
std::atomic<size_t> writeIndex = 0;
|
||||||
|
WebMessage * getEmptyMessage() {
|
||||||
|
if (resetQueue.test()) return nullptr;
|
||||||
|
auto reservedIndex = writeIndex.fetch_add(1);
|
||||||
|
auto &message = queue[reservedIndex%queue.size()];
|
||||||
|
if (message.readyToSend.test()) {
|
||||||
|
std::cerr << "Web message queue full - sending entire state instead\n";
|
||||||
|
// When our queue is full, we drop the entire thing and re-send the entire state
|
||||||
|
resetQueue.test_and_set();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
message.bytes.resize(0);
|
||||||
|
return &message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestEntireState() {
|
||||||
|
if (auto *m = getEmptyMessage()) {
|
||||||
|
resetQueue.test_and_set();
|
||||||
|
m->markReady();
|
||||||
|
} // if this fails, then the queue is full and we're doing a reset anyway
|
||||||
|
}};
|
||||||
|
|
||||||
}} // namespace
|
}} // namespace
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user