1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
Geraint
6fcdd0a158 Analyser with updates via metering path 2025-07-01 11:55:12 +01:00
Geraint
a1b9153bdc Start analyser 2025-06-29 09:53:18 +01:00
18 changed files with 744 additions and 404 deletions

6
.gitmodules vendored
View File

@ -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
View 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
View 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

View File

@ -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(

View File

@ -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/

View File

@ -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"
>
> >
> >
> >

View File

@ -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() {

View File

@ -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
View 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" *

View File

@ -0,0 +1 @@
#include "../../analyser.h"

1
modules/linear Submodule

@ -0,0 +1 @@
Subproject commit 4c8999385c725e619723346bf8f415010fed9c4c

View File

@ -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;

View File

@ -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 &param) const { bool paramsChanging(Param &param) 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();
}
} }
}; };

View File

@ -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 {

View File

@ -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)

View File

@ -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>

File diff suppressed because one or more lines are too long

View File

@ -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