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"]
|
||||
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
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)
|
||||
|
||||
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(
|
||||
|
||||
@ -49,3 +49,4 @@ dev: release-$(PLUGIN)_clap
|
||||
/Applications/REAPER.app/Contents/MacOS/REAPER REAPER/$(PLUGIN)/$(PLUGIN).RPP
|
||||
|
||||
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
|
||||
>
|
||||
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
|
||||
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}
|
||||
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
|
||||
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 {F52BCBA5-0B54-6B40-8D30-4E09891F36EC}
|
||||
TRACKID {93E699D9-F890-234E-A113-7E0053A85B78}
|
||||
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"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
|
||||
@ -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"
|
||||
@ -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() {
|
||||
|
||||
@ -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
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
|
||||
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;
|
||||
|
||||
@ -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 ¶m) 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "./storage.h"
|
||||
#include "../stfx-library.h" // for the ...ParamIgnore classes
|
||||
|
||||
namespace stfx { namespace storage {
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,7 +149,6 @@
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</section>
|
||||
<script>
|
||||
function drawDial(data, canvas) {
|
||||
let scale = window.devicePixelRatio;
|
||||
@ -164,19 +184,153 @@
|
||||
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>
|
||||
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');
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
2
stfx/ui/html/matsui-bundle.min.js
vendored
2
stfx/ui/html/matsui-bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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,30 +80,21 @@ 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 *) {}
|
||||
int version(int v) {
|
||||
@ -278,17 +270,24 @@ 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...>;
|
||||
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>
|
||||
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>
|
||||
WebSTFX(Args... args) : Super(args...) {
|
||||
WebUILibraryEffect(Args... args) : Super(args...) {
|
||||
typename Super::WebParamScanner scanner{this};
|
||||
this->state(scanner);
|
||||
};
|
||||
@ -321,7 +320,7 @@ struct WebUIHelper {
|
||||
}
|
||||
|
||||
private:
|
||||
friend class WebSTFX;
|
||||
friend class WebUILibraryEffect;
|
||||
friend struct Super::ParamRange;
|
||||
friend struct Super::ParamStepped;
|
||||
std::atomic_flag readyToSend = ATOMIC_FLAG_INIT;
|
||||
@ -370,6 +369,15 @@ struct WebUIHelper {
|
||||
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);
|
||||
@ -377,10 +385,44 @@ struct WebUIHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// 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;
|
||||
@ -406,12 +448,6 @@ struct WebUIHelper {
|
||||
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
|
||||
template<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
|
||||
using WebUILibraryEffect = LibraryEffect<Sample, WebUIHelper<EffectSTFX>::template WebSTFX, ExtraArgs...>;
|
||||
}};
|
||||
|
||||
}} // namespace
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user