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"]
path = modules/hilbert-iir
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)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
project(plugins VERSION 1.0.0)
set(NAME basics)
################ boilerplate config
@ -57,17 +60,16 @@ FetchContent_MakeAvailable(clap-helpers)
################ 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)
target_link_libraries(${NAME}_static PUBLIC
clap
clap-helpers
)
target_include_directories(${NAME}_static PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
${CMAKE_CURRENT_SOURCE_DIR}/../modules
signalsmith-basics
)
target_sources(${NAME}_static PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/source/${NAME}.cpp
${CMAKE_CURRENT_SOURCE_DIR}/source/basics.cpp
)
make_clapfirst_plugins(

View File

@ -48,4 +48,5 @@ format:
dev: release-$(PLUGIN)_clap
/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
>
RIPPLE 0
@ -101,7 +101,7 @@
BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA
AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
AAAQAAAA
AFByb2dyYW0gMQAQAAAA
>
FLOATPOS 0 0 0 0
FXID {11A0D811-6420-CB40-95D6-F9D12E45C862}
@ -125,54 +125,8 @@
>
<PROJBAY
>
<TRACK {6D299D8B-E766-F540-80D3-DA6147A064BA}
NAME "Sidechain Distortion"
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"
<TRACK {93E699D9-F890-234E-A113-7E0053A85B78}
NAME ""
PEAKCOL 16576
BEAT -1
AUTOMODE 0
@ -181,180 +135,25 @@
MUTESOLO 0 0 0
IPHASE 0
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
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
SEL 1
REC 0 5088 1 7 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 {F4D9FA0E-571B-E24E-B158-4F619AA188D8}
TRACKID {93E699D9-F890-234E-A113-7E0053A85B78}
PERF 0
MIDIOUT -1
MAINSEND 1 0
<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
LASTSEL 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;
#endif
#include "signalsmith-basics/analyser.h"
#include "signalsmith-basics/crunch.h"
#include "signalsmith-basics/limiter.h"
#include "signalsmith-basics/reverb.h"
@ -24,7 +25,7 @@ bool clap_init(const char *path) {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_DISTORTION,
});
plugins.add<signalsmith::basics::LimiterSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.limiter",
@ -38,7 +39,7 @@ bool clap_init(const char *path) {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_LIMITER,
});
plugins.add<signalsmith::basics::ReverbSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.reverb",
@ -52,6 +53,21 @@ bool clap_init(const char *path) {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
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);
}
void clap_deinit() {

View File

@ -1,8 +1,34 @@
#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 const void * clap_get_factory(const char *);
extern const void * clap_get_factory(const char *id);
extern "C" {
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
struct Plugins {
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();
featureLists.emplace_back(features);
@ -31,6 +31,7 @@ struct Plugins {
creates.push_back([=](const clap_host *host){
return new Plugin<EffectSTFX>(*this, &descriptors[index], host, args...);
});
return index;
}
bool clap_init(const char *path) {
@ -651,3 +652,5 @@ struct Plugin : public clap_plugin {
};
}} // namespace
extern stfx::clap::Plugins stfxPlugins;

View File

@ -164,7 +164,9 @@ namespace stfx {
// per-block automation gradient rate
double blockFade;
// 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>
bool paramsChanging(Param &param) const {
@ -178,7 +180,7 @@ namespace stfx {
// Block length in samples
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
Block(const Block &) = delete;
Block & operator =(const Block&) = delete;
@ -278,8 +280,9 @@ namespace stfx {
callback(0, length);
}
constexpr bool wantsMeters() const {
return false;
bool wantsMeters() const {
metersChecked = true;
return metersRequested;
}
};
@ -358,6 +361,22 @@ namespace stfx {
template<class 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.
@ -493,7 +512,7 @@ namespace stfx {
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.
template<class Inputs, class Outputs>
void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
@ -522,7 +541,8 @@ namespace stfx {
Outputs output;
};
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);
@ -535,6 +555,12 @@ namespace stfx {
justHadReset = false;
for (auto param : params.rangeParams) 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
#include "./storage.h"
#include "../stfx-library.h" // for the ...ParamIgnore classes
namespace stfx { namespace storage {

View File

@ -204,7 +204,7 @@ private:
}
template<class Item>
void readValue(std::vector<Item> &array) {
void readVector(std::vector<Item> &array) {
if (!cbor.isArray()) return;
size_t length = 0;
cbor = cbor.forEach([&](Cbor item, size_t index){
@ -217,13 +217,18 @@ private:
array.resize(length);
}
template<class Item>
void readValue(std::vector<Item> &array) {
readVector(array);
}
#define STORAGE_TYPED_ARRAY(T) \
void readValue(std::vector<T> &array) { \
if (cbor.isTypedArray()) { \
array.resize(cbor.typedArrayLength()); \
cbor.readTypedArray(array); \
} else { \
readValue<std::vector<T>>(array); \
readVector<T>(array); \
} \
}
STORAGE_TYPED_ARRAY(uint8_t)

View File

@ -7,15 +7,15 @@
box-sizing: border-box;
}
body {
display: grid;
grid-template-areas: "header" "params";
grid-template-rows: min-content 1fr;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
width: 100vw;
max-width: 100vw;
height: 100vh;
min-height: 100vh;
max-height: 100vh;
overflow: hidden;
@ -46,7 +46,7 @@
transform: scale(0.95, 1);
}
#params {
grid-area: params;
flex-grow: 1;
display: flex;
align-content: space-around;
justify-content: space-evenly;
@ -96,6 +96,27 @@
.param-range-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>
</head>
<body>
@ -128,55 +149,188 @@
</div>
</label>
</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>
<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>
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 plotColours = ['#8CF', '#FC8', '#8D8', '#F9B'];
function freqScale(width, lowHz, highHz) {
// let a = -289.614, b = 1176.76, c = 15.3385, d = -0.552833; // Bark
let low = lowHz/(1500 + lowHz);
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');
context.resetTransform();
context.clearRect(0, 0, pixels, pixels);
let width = canvas.offsetWidth, height = canvas.offsetHeight;
{
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);
context.translate(1, 1);
let baseHz = 100, maxHz = data.hz[data.hz.length - 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();
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();
if (canvas.classList.contains('plot-grid')) {
context.beginPath();
context.lineWidth = 1;
context.strokeStyle = '#555';
for (let baseHz = 100; baseHz < maxHz; baseHz *= 10) {
for (let i = 1; i < 10; ++i) {
let hz = baseHz*i;
let x = xScale(hz);
context.moveTo(x, 0);
context.lineTo(x, height);
}
}
for (let db = 0; db >= -240; db -= 12) {
let e = Math.pow(10, db/10);
let y = yScale(e);
context.moveTo(0, y);
context.lineTo(width, y);
}
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 src="cbor.min.js"></script>
<script src="matsui-bundle.min.js"></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: "..."});
state.trackMerges(merge => {
window.parent.postMessage(CBOR.encode(merge), '*');
});
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"), '*');
// 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>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@
#include <functional>
#include <atomic>
#include <memory>
#include <iostream> // we log to stderr if our queue gets full
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.
*/
template<template<class, class...> class EffectSTFX>
template<template<class, class...> class EffectSTFX, class SubClass>
struct WebUIHelper {
template<class Effect, class SubClass>
template<class Effect>
class WebBase : public Effect {
using SuperRange = typename Effect::ParamRange;
using SuperStepped = typename Effect::ParamStepped;
protected:
struct WebParamScanner {
struct WebParamScanner : public storage::STFXStorageScanner<WebParamScanner>{
SubClass *effect;
std::vector<std::string> scope;
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
void info(const char *, const char *) {}
@ -278,140 +270,184 @@ struct WebUIHelper {
}
}
};
template<class Storage>
void meterState(Storage &storage) {}
};
template<class Effect, class... ExtraArgs>
struct WebSTFX : public EffectSTFX<WebBase<Effect, WebSTFX<Effect, ExtraArgs...>>, ExtraArgs...> {
/* TODO: without the ExtraArgs, it would be a bit neater:
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
}
struct WebSTFX : public EffectSTFX<WebBase<Effect>, ExtraArgs...> {
using Super = EffectSTFX<WebBase<Effect>, ExtraArgs...>;
};
};
// Use this instead of a plain LibraryEffect
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