1
0

Compare commits

..

No commits in common. "6fcdd0a158e0ef2adc1fe936b5a97ceb31398fc6" and "5c31f6dbf8c92657918ee0bddffa2f9cb850cb62" have entirely different histories.

18 changed files with 405 additions and 745 deletions

6
.gitmodules vendored
View File

@ -4,9 +4,3 @@
[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

View File

@ -1,12 +0,0 @@
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
)

View File

@ -1,237 +0,0 @@
/* 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,10 +1,7 @@
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
@ -60,16 +57,17 @@ 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
signalsmith-basics clap-helpers
)
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/basics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/${NAME}.cpp
) )
make_clapfirst_plugins( make_clapfirst_plugins(

View File

@ -49,4 +49,3 @@ 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.24/OSX64-clang" 1750919608 <REAPER_PROJECT 0.1 "7.20/macOS-arm64" 1747749064
<NOTES 0 2 <NOTES 0 2
> >
RIPPLE 0 RIPPLE 0
@ -101,7 +101,7 @@
BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA
AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
AFByb2dyYW0gMQAQAAAA AAAQAAAA
> >
FLOATPOS 0 0 0 0 FLOATPOS 0 0 0 0
FXID {11A0D811-6420-CB40-95D6-F9D12E45C862} FXID {11A0D811-6420-CB40-95D6-F9D12E45C862}
@ -125,8 +125,54 @@
> >
<PROJBAY <PROJBAY
> >
<TRACK {93E699D9-F890-234E-A113-7E0053A85B78} <TRACK {6D299D8B-E766-F540-80D3-DA6147A064BA}
NAME "" 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"
PEAKCOL 16576 PEAKCOL 16576
BEAT -1 BEAT -1
AUTOMODE 0 AUTOMODE 0
@ -135,25 +181,180 @@
MUTESOLO 0 0 0 MUTESOLO 0 0 0
IPHASE 0 IPHASE 0
PLAYOFFS 0 1 PLAYOFFS 0 1
ISBUS 0 0 ISBUS 2 -1
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 1 SEL 0
REC 0 5088 1 7 0 0 0 0 REC 0 0 1 0 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 {93E699D9-F890-234E-A113-7E0053A85B78} 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 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,7 +3,6 @@
# 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"
@ -53,21 +52,6 @@ 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,34 +1,8 @@
#include "clap/entry.h" #include "clap/entry.h"
/*
#include "../../stfx/clap/stfx-clap.h"
stfx::clap::Plugins stfxPlugins; extern bool clap_init(const char *);
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 *id); extern const void * clap_get_factory(const char *);
extern "C" { extern "C" {
const CLAP_EXPORT clap_plugin_entry clap_entry{ const CLAP_EXPORT clap_plugin_entry clap_entry{

View File

@ -1,14 +0,0 @@
#!/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

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

@ -1 +0,0 @@
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>
size_t add(clap_plugin_descriptor desc, std::initializer_list<const char *> features, Args ...args) { void 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,7 +31,6 @@ 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) {
@ -652,5 +651,3 @@ struct Plugin : public clap_plugin {
}; };
}} // namespace }} // namespace
extern stfx::clap::Plugins stfxPlugins;

View File

@ -164,9 +164,7 @@ 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; bool firstBlockAfterReset = false;
bool metersRequested;
bool &metersChecked;
template<class Param> template<class Param>
bool paramsChanging(Param &param) const { bool paramsChanging(Param &param) const {
@ -180,7 +178,7 @@ namespace stfx {
// Block length in samples // Block length in samples
int length; int 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) {} Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset=false) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), 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;
@ -280,9 +278,8 @@ namespace stfx {
callback(0, length); callback(0, length);
} }
bool wantsMeters() const { constexpr bool wantsMeters() const {
metersChecked = true; return false;
return metersRequested;
} }
}; };
@ -361,22 +358,6 @@ 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.
@ -512,7 +493,7 @@ namespace stfx {
process(buffers, buffers, blockLength); process(buffers, buffers, blockLength);
} }
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.processSTFX(io, config, block)`. /// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(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) {
@ -541,8 +522,7 @@ namespace stfx {
Outputs output; Outputs output;
}; };
Io io{inputs, outputs}; Io io{inputs, outputs};
bool metersChecked = false; Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset);
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);
@ -555,12 +535,6 @@ 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,7 +1,6 @@
#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 readVector(std::vector<Item> &array) { void readValue(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,18 +217,13 @@ 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 { \
readVector<T>(array); \ readValue<std::vector<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: flex; display: grid;
flex-direction: column; grid-template-areas: "header" "params";
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 {
flex-grow: 1; grid-area: params;
display: flex; display: flex;
align-content: space-around; align-content: space-around;
justify-content: space-evenly; justify-content: space-evenly;
@ -96,27 +96,6 @@
.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>
@ -149,6 +128,7 @@
</div> </div>
</label> </label>
</template> </template>
</section>
<script> <script>
function drawDial(data, canvas) { function drawDial(data, canvas) {
let scale = window.devicePixelRatio; let scale = window.devicePixelRatio;
@ -184,153 +164,19 @@
context.stroke(); context.stroke();
} }
</script> </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="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 => {
let merge = CBOR.decode(e.data); state.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,7 +9,6 @@
#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 {
@ -80,21 +79,30 @@ 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, class SubClass> template<template<class, class...> class EffectSTFX>
struct WebUIHelper { struct WebUIHelper {
template<class Effect> template<class Effect, class SubClass>
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 : public storage::STFXStorageScanner<WebParamScanner>{ struct 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 *) {}
int version(int v) { int version(int v) {
@ -270,24 +278,17 @@ 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>, ExtraArgs...> { struct WebSTFX : public EffectSTFX<WebBase<Effect, WebSTFX<Effect, ExtraArgs...>>, ExtraArgs...> {
using Super = EffectSTFX<WebBase<Effect>, 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...>;
// 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> template<class... Args>
WebUILibraryEffect(Args... args) : Super(args...) { WebSTFX(Args... args) : Super(args...) {
typename Super::WebParamScanner scanner{this}; typename Super::WebParamScanner scanner{this};
this->state(scanner); this->state(scanner);
}; };
@ -320,7 +321,7 @@ struct WebUILibraryEffect : public LibraryEffect<Sample, WebUIHelper<EffectSTFX,
} }
private: private:
friend class WebUILibraryEffect; friend class WebSTFX;
friend struct Super::ParamRange; friend struct Super::ParamRange;
friend struct Super::ParamStepped; friend struct Super::ParamStepped;
std::atomic_flag readyToSend = ATOMIC_FLAG_INIT; std::atomic_flag readyToSend = ATOMIC_FLAG_INIT;
@ -369,15 +370,6 @@ struct WebUILibraryEffect : public LibraryEffect<Sample, WebUIHelper<EffectSTFX,
requestEntireState(); requestEntireState();
return; 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()) { } else if (cbor.isMap()) {
// Apply it as a merge // Apply it as a merge
WebStateReader reader(cbor); WebStateReader reader(cbor);
@ -385,44 +377,10 @@ struct WebUILibraryEffect : public LibraryEffect<Sample, WebUIHelper<EffectSTFX,
} }
} }
// Replace `.process()` to add meter messages if they exist private:
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::ParamRange;
friend struct Super::ParamStepped; 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 std::vector<WebMessage> queue = std::vector<WebMessage>(64); // power of 2 so that overflow doesn't mess with the modulo
size_t readIndex = 0; size_t readIndex = 0;
std::atomic_flag resetQueue = ATOMIC_FLAG_INIT; std::atomic_flag resetQueue = ATOMIC_FLAG_INIT;
@ -448,6 +406,12 @@ private:
resetQueue.test_and_set(); resetQueue.test_and_set();
m->markReady(); m->markReady();
} // if this fails, then the queue is full and we're doing a reset anyway } // 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 }} // namespace