1
0

Move STFX stuff (aside from stfx-library.h) into separate repo

This commit is contained in:
Geraint 2025-07-08 16:25:03 +01:00
parent 16b2ba955c
commit e8103a1013
16 changed files with 0 additions and 3709 deletions

View File

@ -1,95 +0,0 @@
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)
################ boilerplate config
set(CMAKE_CXX_STANDARD 17) # for string_view
if (APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13)
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64")
enable_language(OBJCXX)
endif ()
include(FetchContent)
FetchContent_Declare(
clap
GIT_REPOSITORY https://github.com/geraintluff/clap.git # https://github.com/free-audio/clap
GIT_TAG 2df92fe17911f15436176b9c37faec370166d14f # draft/web
GIT_SHALLOW OFF
)
FetchContent_MakeAvailable(clap)
################ CLAP wrapper stuff
if(NOT DEFINED VST3_SDK_ROOT)
if(EMSCRIPTEN)
# don't download the VST3 SDK
set(VST3_SDK_ROOT "./dummy/vst3sdk/path")
else()
set(CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES TRUE)
endif()
endif()
set(CLAP_WRAPPER_OUTPUT_NAME clap-wrapper-target)
set(CLAP_WRAPPER_DONT_ADD_TARGETS TRUE)
include(FetchContent)
FetchContent_Declare(
clap-wrapper
GIT_REPOSITORY https://github.com/geraintluff/clap-wrapper.git
GIT_TAG cd666f7d2291d47f810d9c8f123886026a631576
GIT_SHALLOW ON
)
FetchContent_MakeAvailable(clap-wrapper)
################ Helpers
FetchContent_Declare(
clap-helpers
GIT_REPOSITORY https://github.com/free-audio/clap-helpers
GIT_TAG 58ab81b1dc8219e859529c1306f364bb3aedf7d5
GIT_SHALLOW OFF
)
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
signalsmith-basics
)
target_sources(${NAME}_static PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/source/basics.cpp
)
make_clapfirst_plugins(
TARGET_NAME ${NAME}
IMPL_TARGET ${NAME}_static
RESOURCE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
OUTPUT_NAME "${NAME}"
ENTRY_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/source/clap_entry.cpp
BUNDLE_IDENTIFIER "uk.co.signalsmith-audio.plugins.${NAME}"
BUNDLE_VERSION "1.0.0"
WINDOWS_FOLDER_VST3 TRUE
PLUGIN_FORMATS CLAP VST3 WCLAP # AUV2
COPY_AFTER_BUILD FALSE
AUV2_MANUFACTURER_NAME "Signalsmith Audio"
AUV2_MANUFACTURER_CODE "SigA"
AUV2_SUBTYPE_CODE "BdDt"
AUV2_INSTRUMENT_TYPE "aufx"
)

View File

@ -1,52 +0,0 @@
.PHONY: build build-emscripten emsdk
PROJECT := plugins
PLUGIN := basics
CMAKE_PARAMS := -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=.. -G Xcode # -DCMAKE_BUILD_TYPE=Release
mac-installer: mac-installer-$(PLUGIN)
clean:
rm -rf out
build:
cmake . -B out/build $(CMAKE_PARAMS)
release-%: build
cmake --build out/build --target $* --config Release
mac-installer-%: release-%_vst3 release-%_clap emscripten-%_wclap
../stfx/front-end/clap/mac/make-pkg-installer.sh out/$*.pkg $* uk.co.signalsmith.installer.stfx.$* out/Release/$*
####### Emscripten #######
# based on https://stunlock.gg/posts/emscripten_with_cmake/
CURRENT_DIR := $(shell pwd)
EMSDK ?= $(CURRENT_DIR)/emsdk
EMSDK_ENV = unset CMAKE_TOOLCHAIN_FILE; EMSDK_QUIET=1 . "$(EMSDK)/emsdk_env.sh";
emsdk:
@ if ! test -d "$(EMSDK)" ;\
then \
echo "SDK not found - cloning from Github" ;\
git clone https://github.com/emscripten-core/emsdk.git "$(EMSDK)" ;\
cd "$(EMSDK)" && git pull && ./emsdk install latest && ./emsdk activate latest ;\
$(EMSDK_ENV) emcc --check && python3 --version && cmake --version ;\
fi
build-emscripten: emsdk
$(EMSDK_ENV) emcmake cmake . -B out/build-emscripten -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=../Release -DCMAKE_BUILD_TYPE=Release
emscripten-%: build-emscripten
$(EMSDK_ENV) cmake --build out/build-emscripten --target $* --config Release
####### Dev stuff #######
format:
find source -iname \*.cpp -o -iname \*.h | xargs clang-format -i
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/

View File

@ -1,159 +0,0 @@
<REAPER_PROJECT 0.1 "7.24/OSX64-clang" 1750919608
<NOTES 0 2
>
RIPPLE 0
GROUPOVERRIDE 0 0 0
AUTOXFADE 129
ENVATTACH 3
POOLEDENVATTACH 0
MIXERUIFLAGS 11 48
ENVFADESZ10 40
PEAKGAIN 1
FEEDBACK 0
PANLAW 1
PROJOFFS 0 0 0
MAXPROJLEN 0 0
GRID 3199 8 1 8 1 0 0 0
TIMEMODE 1 5 -1 30 0 0 -1
VIDEO_CONFIG 0 0 256
PANMODE 3
PANLAWFLAGS 3
CURSOR 0
ZOOM 100 0 0
VZOOMEX 6 0
USE_REC_CFG 0
RECMODE 1
SMPTESYNC 0 30 100 40 1000 300 0 0 1 0 0
LOOP 0
LOOPGRAN 0 4
RECORD_PATH "Media" ""
<RECORD_CFG
ZXZhdxgAAQ==
>
<APPLYFX_CFG
>
RENDER_FILE ""
RENDER_PATTERN ""
RENDER_FMT 0 2 0
RENDER_1X 0
RENDER_RANGE 1 0 0 18 1000
RENDER_RESAMPLE 3 0 1
RENDER_ADDTOPROJ 0
RENDER_STEMS 0
RENDER_DITHER 0
TIMELOCKMODE 1
TEMPOENVLOCKMODE 1
ITEMMIX 1
DEFPITCHMODE 589824 0
TAKELANE 1
SAMPLERATE 44100 0 0
<RENDER_CFG
ZXZhdxgAAQ==
>
LOCK 1
<METRONOME 6 2
VOL 0.25 0.125
BEATLEN 4
FREQ 1760 880 1
SAMPLES "" ""
SPLIGNORE 0 0
SPLDEF 2 660 "" 0
SPLDEF 3 440 "" 0
PATTERN 0 169
PATTERNSTR ABBB
MULT 1
>
GLOBAL_AUTO -1
TEMPO 120 4 4
PLAYRATE 1 0 0.25 4
SELECTION 0 0
SELECTION2 0 0
MASTERAUTOMODE 0
MASTERTRACKHEIGHT 0 0
MASTERPEAKCOL 16576
MASTERMUTESOLO 0
MASTERTRACKVIEW 1 0.6667 0.5 0.5 0 0 0 0 0 0 0 0 0 0
MASTERHWOUT 0 0 1 0 0 0 0 -1
MASTER_NCH 4 2
MASTER_VOLUME 1 0 -1 -1 1
MASTER_PANMODE 3
MASTER_PANLAWFLAGS 3
MASTER_FX 1
MASTER_SEL 0
<MASTERFXLIST
WNDRECT 848 592 601 240
SHOW 0
LASTSEL 1
DOCKED 0
BYPASS 0 0 0
<VST "VST3: [Basics] Limiter (Signalsmith Audio)" Signalsmith-Audio-signalsmith-basics.vst3 0 "" 767935365{85C8AB4F80EBAD2FB1B22206AC3BB024} ""
hcPFLe5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAEAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAABqAAAAAQAAAP//EAA=
QQAAAAEAAAAAAAAAAAAAEEAAAAAAAADwP5CkT2MBlO0/6sSBcQw8H0CDkljf3IUjQBaXn5dXrjlAAAAAAAAA8D8AAAAAAADgPxkAAAAAAAAAAAAAAAAAAAAAAAAAAACA
gUAAAAAAAEBlQA==
AAAQAAAA
>
FLOATPOS 0 0 0 0
FXID {81F27681-ACFE-1D40-9AAE-F8C9A0035B1F}
WAK 0 0
BYPASS 0 0 0
<VST "VST: ReaStream (Cockos) (8ch)" reastream.vst.dylib 0 "" 1920169074<5653547273747272656173747265616D> ""
cnRzcu5e7f4IAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAgAAAABAAAAAAAAAAIAAAAAAAAA
BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA
AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
AFByb2dyYW0gMQAQAAAA
>
FLOATPOS 0 0 0 0
FXID {11A0D811-6420-CB40-95D6-F9D12E45C862}
WAK 0 0
>
<MASTERPLAYSPEEDENV
EGUID {590DCF42-C4B4-1D40-9C66-B99188BD30ED}
ACT 0 -1
VIS 0 1 1
LANEHEIGHT 0 0
ARM 0
DEFSHAPE 0 -1 -1
>
<TEMPOENVEX
EGUID {9E65AFD8-F464-FB46-AC60-C5553CE5E330}
ACT 0 -1
VIS 1 0 1
LANEHEIGHT 0 0
ARM 0
DEFSHAPE 1 -1 -1
>
<PROJBAY
>
<TRACK {93E699D9-F890-234E-A113-7E0053A85B78}
NAME ""
PEAKCOL 16576
BEAT -1
AUTOMODE 0
PANLAWFLAGS 3
VOLPAN 1 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 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 {93E699D9-F890-234E-A113-7E0053A85B78}
PERF 0
MIDIOUT -1
MAINSEND 1 0
<FXCHAIN
SHOW 0
LASTSEL 0
DOCKED 0
>
>
>

View File

@ -1,7 +0,0 @@
<!DOCTYPE html>
<html><head><title></title><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{display: flex;flex-direction:column;justify-content:stretch;align-items:center;min-height:100vh;justify-content:center;font:12pt sans-serif;color:#FFF;background:#222;margin:0;padding:0}img,video{max-width:100vw;max-height:100vh;flex-shrink:1}a{color:#8AF;text-decoration:none;margin-top:1em}iframe{background:#FFF;width:100%;flex-grow:0.8}</style>
</head><body><form onsubmit="return false"><input id="p" type="password" autofocus><input type="submit" value="go" onclick="d(document.querySelector('#p').value).catch(e=>document.body.textContent='failed')"></form><script>
let balloonOptions={"rounds":5,"buffers":32768},s64="Ho9CMRCSYNw84y+8VRdnwVrm5eZBlEWlC8LK0oP2AlM=",e64="xGiUxaRUjqura2lejNiFdvZs6SQB1P3wY9COVVzbHk6X5Z84KO_YaoeUq0hathjnPiVsUNHBvnsEmrP8q06pElSZeo8brf2bZWxSf6NxG3TL2Ghh_2Rw1Gl7qSpeJZ6vr3jnd-fL5hzC5nAbD2Hnx6mgH0sW3DMarGYnBItq3O8TE9LzvWSWceBGhD518fyjDKThWJEzvkTG5f04mcixVMdOQQj09zgO0Msd0CG-S65WFRqGzfPmnuPFCzyknhCdjHy8At1GtssIib3h-pBeWwyCnezZX5iprn8gnPhxgSHZgMVp082L18XqcaVAiiry8XGi3B-QGmRW5V2xuJGz3s-1GMJxl1-4Od7X-9yRwsmA2LplZtATa5zpF50pUspSXGGn1ZDWpTft34SW3Tf-cIEJ_PfitsV-w7k1OGMKgst6X5XxITQwhGYiO6GiO6T189ZDgswexN9jb-AZTFrLI_9U_53kJj-wjar2a8a1wSaBXmNivtPGDmALAFyS-zSj9Y_yllulg1h482G8MhTE4mOp3uIxOw";
let cryptoUtils=(function r(){let n={factory:r};var e=[];for(let t=2;e.length<64;++t)e.some(r=>!(t%r))||e.push(t);let g=4294967296,p=e.slice(0,8).map(r=>Math.sqrt(r)*g|0),D=e.map(r=>Math.cbrt(r)*g|0),h=new Int32Array(64),d=new Int32Array(32),x=n.sha256=function(r,t){"string"==typeof r&&(r=(new TextEncoder).encode(r));var e="hex"==t,n=(t=(t=e?null:t)||new Uint8Array(32),r.length),a=16*Math.ceil((n+1+8)/64),i=32==a?d.fill(0):new Int32Array(a),o=new Uint8Array(i.buffer),y=(o.set(r),o[n]=128,new DataView(i.buffer));for(let r=0;r<a;++r)i[r]=y.getInt32(4*r);i[a-1]=8*n|0,i[a-2]=8*n/g|0;var f=new Int32Array(t.buffer);for(let r=0;r<8;++r)f[r]=p[r];var l=f.slice(0,8);for(let t=0;t<a;t+=16){for(let r=0;r<16;++r)h[r]=i[t+r];for(let r=16;r<64;++r){var w=h[r-15],c=h[r-2];h[r]=h[r-16]+h[r-7]+((w>>>7|w<<25)^(w>>>18|w<<14)^w>>>3)+((c>>>17|c<<15)^(c>>>19|c<<13)^c>>>10)|0}for(let r=0;r<64;r++){var A=l[0],u=l[4],v=l[7]+((u>>>6|u<<26)^(u>>>11|u<<21)^(u>>>25|u<<7))+(u&l[5]^~u&l[6])+D[r]+h[r],s=(A&l[1]^A&l[2]^l[1]&l[2])+((A>>>2|A<<30)^(A>>>13|A<<19)^(A>>>22|A<<10));l[7]=l[6],l[6]=l[5],l[5]=u,l[4]=l[3]+v|0,l[3]=l[2],l[2]=l[1],l[1]=A,l[0]=v+s|0}for(let r=0;r<8;r++)f[r]=l[r]=f[r]+l[r]|0}var U=new DataView(t.buffer);for(let r=0;r<8;r++)U.setInt32(4*r,f[r]);return e?Array.from(t,r=>(r>>>4).toString(16)+(15&r).toString(16)).join(""):t};return n.balloon=async function(n,r,t={}){var e=new TextEncoder,a=("string"==typeof n&&(n=x(e.encode(n))),"string"==typeof r&&(text=x(e.encode(r))),t.buffers=t.buffers||32768),i=t.rounds=t.rounds||4,o=t.delta=t.delta||3,y=[],f=t.workPeriod||100;let l=f*(1/(t.workRatio||1)-1);var w=t.progress||(r=>r);32!=n.length&&(n=x(n)),32!=r.length&&(r=x(r));let c=new Uint8Array(68),A=new DataView(c.buffer);var u=a*(1+i*(1+2*o));let v=0;function s(t,e){A.setUint32(0,v++,!0);for(let r=0;r<32;++r)c[4+r]=t[r];for(let r=0;r<32;++r)c[36+r]=e[r];return x(c)}y[0]=s(r,n);for(let r=1;r<a;++r)y[r]=s(y[r-1],n);let U=Date.now()+f;var g=new Uint8Array(32),p=new DataView(g.buffer);for(let e=0;e<i;++e)for(let t=0;t<a;++t){Date.now()>U&&(w(v,u),e=await new Promise((t,r)=>{setTimeout(r=>t(e),l)}),U=Date.now()+f);var D=0==t?y[a-1]:y[t-1];y[t]=s(D,y[t]);for(let r=0;r<o;++r){p.setUint32(0,e,!0),p.setUint32(4,t,!0),p.setUint32(8,r,!0);var h=s(n,g),h=new DataView(h.buffer).getUint32(0,!0);y[t]=s(y[t],y[h%a])}}return w(v,u),y[a-1]},n.aesPair=async function(r){return"string"==typeof r&&(r=Uint8Array.fromBase64(r)),crypto.subtle.importKey("raw",r,"AES-GCM",!1,["encrypt","decrypt"]).then(a=>({key:a,encrypt:(r,e)=>{"string"==typeof r&&(r=(new TextEncoder).encode(r));let n=new Uint8Array(12);return self.crypto.getRandomValues(n),crypto.subtle.encrypt({name:"AES-GCM",iv:n,tagLength:128},a,r).then(r=>{r=new Uint8Array(r);var t=new Uint8Array(12+r.length);return t.set(n),t.set(r,12),e?t.toBase64(!0):t})},decrypt:(r,t)=>{var e=(r="string"==typeof r?Uint8Array.fromBase64(r):r).subarray(0,12),r=r.subarray(12);return crypto.subtle.decrypt({name:"AES-GCM",iv:e,tagLength:128},a,r).then(r=>t?new TextDecoder("utf-8",{fatal:!0}).decode(r):r)}}))},n.balloonPair=async function(r,t,e={}){return n.balloon(r,t,e).then(n.aesPair)},Uint8Array.prototype.toBase64=function(r){let t="";return this.forEach(r=>t+=String.fromCharCode(r)),t=btoa(t),t=r?t.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+/,""):t},Uint8Array.fromBase64=r=>Uint8Array.from(atob(r.replace(/_/g,"/").replace(/-/g,"+")),r=>r.charCodeAt(0)),Uint8Array.prototype.toHex=function(){return Array.from(this,r=>(r>>>4).toString(16)+(15&r).toString(16)).join("")},Uint8Array.fromHex=t=>{var e=t.length/2,n=new Uint8Array(e);for(let r=0;r<e;++r)n[r]=parseInt(t.slice(2*r,2*r+2),16)||0;return n},n})();
async function d(t){let e=document.body;balloonOptions.progress=((t,n)=>e.textContent=Math.round(100*t/n)+"%");let n=await cryptoUtils.balloonPair(Uint8Array.fromBase64(s64),t,balloonOptions),a=await n.decrypt(e64);e.textContent="";let o=new DataView(a),r=new Uint8Array(a),i=0;function l(t){let e=4294967296*o.getUint32(i)+o.getUint32(i+4);i+=8;let n=r.subarray(i,i+e);return i+=e,t?new TextDecoder("utf-8",{fatal:!0}).decode(n):n}let d={image:"img",audio:"audio",video:"video",text:"iframe"};for(;i<r.length;){let t=l(1),n=l(1),a=document.createElement("a");a.href=URL.createObjectURL(new File([l()],t,{type:n})),a.setAttribute("download",a.textContent=t),e.append(a);let o=n.split("/")[0];if(d[o]){let t=document.createElement(d[o]);t.src=t.data=a.href,t.controls=!0,e.append(t)}}}
</script></body></html>

View File

@ -1,114 +0,0 @@
#ifndef LOG_EXPR
# include <iostream>
# define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl;
#endif
#include "signalsmith-basics/analyser.h"
#include "signalsmith-basics/chorus.h"
#include "signalsmith-basics/crunch.h"
#include "signalsmith-basics/freq-shifter.h"
#include "signalsmith-basics/limiter.h"
#include "signalsmith-basics/reverb.h"
#include "../../stfx/clap/stfx-clap.h"
template<class Effect>
struct AnalyserSTFX : public signalsmith::basics::AnalyserSTFX<Effect> {
AnalyserSTFX() {
this->webPage += "?compact&columns";
}
};
static stfx::clap::Plugins plugins;
bool clap_init(const char *path) {
plugins.add<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_ANALYZER,
});
plugins.add<signalsmith::basics::ChorusSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.chorus",
.name = "[Basics] Chorus",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_CHORUS,
});
plugins.add<signalsmith::basics::CrunchSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.crunch",
.name = "[Basics] Crunch",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_DISTORTION,
});
plugins.add<signalsmith::basics::FreqShifterSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.freq-shifter",
.name = "[Basics] Frequency Shifter",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_FREQUENCY_SHIFTER,
});
plugins.add<signalsmith::basics::LimiterSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.limiter",
.name = "[Basics] Limiter",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_LIMITER,
});
plugins.add<signalsmith::basics::ReverbSTFX>({
.clap_version = CLAP_VERSION,
.id = "uk.co.signalsmith.basics.reverb",
.name = "[Basics] Reverb",
.vendor = "Signalsmith Audio",
.url = "",
.manual_url = "",
.support_url = "",
.version = "1.0.0"
}, {
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
CLAP_PLUGIN_FEATURE_REVERB,
});
return plugins.clap_init(path);
}
void clap_deinit() {
plugins.clap_deinit();
}
const void * clap_get_factory(const char *id) {
return plugins.clap_get_factory(id);
}

View File

@ -1,40 +0,0 @@
#include "clap/entry.h"
/*
#include "../../stfx/clap/stfx-clap.h"
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 *id);
extern "C" {
const CLAP_EXPORT clap_plugin_entry clap_entry{
.clap_version = CLAP_VERSION,
.init = clap_init,
.deinit = clap_deinit,
.get_factory = clap_get_factory
};
}

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,656 +0,0 @@
#include "clap/clap.h"
#include <vector>
#include <string>
#include <functional>
#include <initializer_list>
#include <cstdio>
#include <optional>
#include "../param-info.h"
#include "../storage/stfx-storage.h"
#include "../ui/web-ui.h"
namespace stfx { namespace clap {
// A CLAP plugin made from an STFX template
template<template<class> class EffectSTFX>
struct Plugin;
// A helper to make a CLAP plugin factory from STFX templates
struct Plugins {
template<template<class> class EffectSTFX, class ...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);
featureLists[index].push_back(nullptr);
desc.features = featureLists[index].data();
descriptors.push_back(desc);
creates.push_back([=](const clap_host *host){
return new Plugin<EffectSTFX>(*this, &descriptors[index], host, args...);
});
return index;
}
bool clap_init(const char *path) {
modulePath = path;
return true;
}
void clap_deinit() {}
const void * clap_get_factory(const char *id) {
if (!std::strcmp(id, CLAP_PLUGIN_FACTORY_ID)) {
// static variables like this are thread-safe (since C++11)
// https://en.cppreference.com/w/cpp/language/storage_duration.html#Static_block_variables
static PluginFactory factory{*this};
return &factory;
}
return nullptr;
}
std::string modulePath;
private:
std::vector<std::vector<const char *>> featureLists;
std::vector<clap_plugin_descriptor> descriptors;
std::vector<std::function<const clap_plugin_t *(const clap_host *)>> creates;
struct PluginFactory : public clap_plugin_factory {
Plugins &plugins;
PluginFactory(Plugins &plugins) : plugins(plugins) {
get_plugin_count = static_get_plugin_count;
get_plugin_descriptor = static_get_plugin_descriptor;
create_plugin = static_create_plugin;
}
static uint32_t static_get_plugin_count(const clap_plugin_factory *factory) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
return uint32_t(plugins.creates.size());
}
static const clap_plugin_descriptor * static_get_plugin_descriptor(const clap_plugin_factory *factory, uint32_t index) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
if (index >= plugins.descriptors.size()) return nullptr;
return &plugins.descriptors[index];
}
static const clap_plugin * static_create_plugin(const clap_plugin_factory *factory, const clap_host *host, const char *pluginId) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
for (size_t index = 0; index < plugins.descriptors.size(); ++index) {
auto &desc = plugins.descriptors[index];
if (!std::strcmp(pluginId, desc.id)) {
return plugins.creates[index](host);
}
}
return nullptr;
}
};
};
struct Crc32 {
void add(uint8_t byte) {
uint32_t val = (crc^byte)&0xFF;
for (int i = 0; i < 8; ++i) {
val = (val&1) ? (val>>1)^0xEDB88320 : (val>>1);
}
crc = val^(crc>>8);
}
Crc32 & addString(const char *str, bool includeNull=true) {
while (*str) {
add(uint8_t(*str));
++str;
}
if (includeNull) add(0);
return *this;
}
Crc32 copy() {
return {*this};
}
uint32_t done() const {
return crc^0xFFFFFFFFu;
}
private:
uint32_t crc = 0xFFFFFFFFu;
};
// Just include the proposed draft structs here
static constexpr const char *CLAP_EXT_WEBVIEW1 = "clap.webview/1";
struct clap_plugin_webview1 {
bool(CLAP_ABI *provide_starting_uri)(const clap_plugin_t *plugin, char *out_buffer, uint32_t out_buffer_capacity);
bool(CLAP_ABI *receive)(const clap_plugin_t *plugin, const void *buffer, uint32_t size);
};
struct clap_host_webview1 {
bool(CLAP_ABI *is_open)(const clap_host_t *host);
bool(CLAP_ABI *send)(const clap_host_t *host, const void *buffer, uint32_t size);
};
template<template<class> class EffectSTFX>
struct Plugin : public clap_plugin {
const Plugins &plugins;
const clap_host *host;
const clap_host_params *hostParams = nullptr;
const clap_host_webview1 *hostWebview1 = nullptr;
using Effect = stfx::web::WebUILibraryEffect<float, EffectSTFX>;
Effect effect;
template<class ...Args>
Plugin(const Plugins &plugins, const clap_plugin_descriptor *desc, const clap_host *host, Args ...args) : plugins(plugins), host(host), effect(args...) {
this->desc = desc;
this->plugin_data = nullptr;
this->init = plugin_init;
this->destroy = plugin_destroy;
this->activate = plugin_activate;
this->deactivate = plugin_deactivate;
this->start_processing = plugin_start_processing;
this->stop_processing = plugin_stop_processing;
this->reset = plugin_reset;
this->process = plugin_process;
this->get_extension = plugin_get_extension;
this->on_main_thread = plugin_on_main_thread;
scanParams();
}
// plugin state
bool isConfigured = false;
// for library STFX, all inputs/outputs (main and aux) get concatenated together
std::vector<const float *> inputBuffers;
std::vector<float *> outputBuffers;
void processEvent(const clap_event_header *header) {
if (header->space_id != CLAP_CORE_EVENT_SPACE_ID) return; // only core events supported atm
if (header->type == CLAP_EVENT_PARAM_VALUE) {
auto *event = (clap_event_param_value *)header;
auto *param = (Param *)event->cookie;
if (!param) {
auto paramId = event->param_id;
for (auto &p : params) {
if (p.id == paramId) {
param = &p;
break;
}
}
if (!param) return; // invalid parameter
} else if (param->id != event->param_id) {
return; // inconsistent ID / cookie
}
if (param->rangeParam) {
*param->rangeParam = param->rangeInfo->fromUnit(event->value);
} else {
*param->steppedParam = int(std::round(event->value));
}
} else {
LOG_EXPR(header->size);
LOG_EXPR(header->time);
LOG_EXPR(header->space_id);
LOG_EXPR(header->type);
LOG_EXPR(header->flags);
}
}
// CLAP plugin methods
static bool plugin_init(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
#define STFX_GET_EXT(field, extId) \
plugin.field = (decltype(plugin.field))plugin.host->get_extension(plugin.host, extId);
STFX_GET_EXT(hostParams, CLAP_EXT_PARAMS)
STFX_GET_EXT(hostWebview1, CLAP_EXT_WEBVIEW1)
#undef STFX_GET_EXT
return true;
}
static void plugin_destroy(const clap_plugin *obj) {
delete (Plugin *)obj;
}
static bool plugin_activate(const clap_plugin *obj, double sampleRate, uint32_t minFrames, uint32_t maxFrames) {
auto &plugin = *(Plugin *)obj;
auto &config = plugin.effect.config;
if (!plugin.isConfigured || sampleRate != plugin.effect.config.sampleRate) {
auto prevConfig = config;
plugin.isConfigured = plugin.effect.configure();
if (!plugin.isConfigured) {
// Can't change config (e.g. sample-rate/ports/etc.) here, return to previous
config = prevConfig;
}
}
size_t inputChannels = config.inputChannels;
for (auto &a : config.auxInputs) inputChannels += a;
plugin.inputBuffers.reserve(inputChannels);
size_t outputChannels = config.outputChannels;
for (auto &a : config.auxOutputs) a += outputChannels;
plugin.outputBuffers.reserve(outputChannels);
return plugin.isConfigured;
}
static void plugin_deactivate(const clap_plugin *obj) {}
static bool plugin_start_processing(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
return plugin.isConfigured;
}
static void plugin_stop_processing(const clap_plugin *obj) {}
static void plugin_reset(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
if (plugin.isConfigured) plugin.effect.reset();
}
static const void * plugin_get_extension(const clap_plugin *obj, const char *extId) {
if (!std::strcmp(extId, CLAP_EXT_PARAMS)) {
static struct clap_plugin_params ext{
params_count,
params_get_info,
params_get_value,
params_value_to_text,
params_text_to_value,
params_flush
};
return &ext;
} else if (!std::strcmp(extId, CLAP_EXT_AUDIO_PORTS)) {
static struct clap_plugin_audio_ports ext{
audio_ports_count,
audio_ports_get
};
return &ext;
} else if (!std::strcmp(extId, CLAP_EXT_STATE)) {
static struct clap_plugin_state ext{
state_save,
state_load
};
return &ext;
} else if (!std::strcmp(extId, CLAP_EXT_WEBVIEW1)) {
static struct clap_plugin_webview1 ext{
webview1_provide_starting_uri,
webview1_receive
};
return &ext;
}
return nullptr;
}
static clap_process_status plugin_process(const clap_plugin *obj, const clap_process *process) {
auto &plugin = *(Plugin *)obj;
auto &inputBuffers = plugin.inputBuffers;
for (uint32_t i = 0; i < process->audio_inputs_count; ++i) {
auto &buffer = process->audio_inputs[i];
for (uint32_t c = 0; c < buffer.channel_count; ++c) {
inputBuffers.push_back(buffer.data32[c]);
}
}
auto &outputBuffers = plugin.outputBuffers;
for (uint32_t i = 0; i < process->audio_outputs_count; ++i) {
auto &buffer = process->audio_outputs[i];
for (uint32_t c = 0; c < buffer.channel_count; ++c) {
outputBuffers.push_back(buffer.data32[c]);
}
}
size_t length = process->frames_count;
auto inputEventCount = process->in_events->size(process->in_events);
size_t offset = 0, nextEventIndex = 0;
while (offset < length) {
size_t nextEventOffset = length;
const clap_event_header *event = nullptr;
if (nextEventIndex < inputEventCount) {
event = process->in_events->get(process->in_events, nextEventIndex);
nextEventOffset = std::max<size_t>(offset, event->time);
}
// process up until the next event (or end)
if (nextEventOffset > offset) {
auto delta = nextEventOffset - offset;
plugin.effect.process(inputBuffers.data(), outputBuffers.data(), delta);
offset = nextEventOffset;
for (auto &p : inputBuffers) p += delta;
for (auto &p : outputBuffers) p += delta;
}
if (event) {
plugin.processEvent(event);
++nextEventIndex;
}
}
inputBuffers.resize(0);
outputBuffers.resize(0);
if (plugin.effect.hasPendingWebMessage()) {
plugin.host->request_callback(plugin.host);
}
plugin.sendParamEvents(process->out_events);
return CLAP_PROCESS_CONTINUE;
}
static void plugin_on_main_thread(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
plugin.sendWebMessages();
}
// parameters
struct Param : public clap_param_info {
typename Effect::ParamRange *rangeParam = nullptr;
std::optional<RangeParamInfo> rangeInfo;
typename Effect::ParamStepped *steppedParam = nullptr;
std::optional<SteppedParamInfo> steppedInfo;
std::atomic_flag hostValueSent = ATOMIC_FLAG_INIT;
std::atomic_flag hostStartGestureSent = ATOMIC_FLAG_INIT;
std::atomic_flag hostStopGestureSent = ATOMIC_FLAG_INIT;
Param() {}
Param(Param &&other) : clap_param_info(other), rangeParam(other.rangeParam), rangeInfo(std::move(other.rangeInfo)), steppedParam(other.steppedParam), steppedInfo(std::move(other.steppedInfo)) {}
void setClapInfo() {
flags |= CLAP_PARAM_IS_AUTOMATABLE;
cookie = this;
if (rangeParam) {
std::strncpy(name, rangeInfo->name.c_str(), CLAP_NAME_SIZE);
// STFX range params are mapped to [0, 1] so we can give them a nonlinear shape
min_value = 0;
max_value = 1;
default_value = rangeInfo->toUnit(rangeInfo->defaultValue);
} else {
std::strncpy(name, steppedInfo->name.c_str(), CLAP_NAME_SIZE);
flags |= CLAP_PARAM_IS_STEPPED;
min_value = steppedInfo->low;
max_value = steppedInfo->high;
default_value = steppedInfo->defaultValue;
}
hostValueSent.test_and_set();
hostStartGestureSent.test_and_set();
hostStopGestureSent.test_and_set();
}
};
std::vector<Param> params;
struct ParamScanner : public storage::STFXStorageScanner<ParamScanner> {
std::vector<Param> &params;
Crc32 crc;
ParamScanner(std::vector<Param> &params) : params(params) {}
template<class PR>
RangeParamInfo & range(const char *key, PR &param) {
param.context.index = params.size(); // so we can find it in the listeners below
params.emplace_back();
auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done();
entry.rangeParam = &param;
return entry.rangeInfo.emplace(param);
}
template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) {
param.context.index = params.size(); // so we can find it in the listeners below
params.emplace_back();
auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done();
entry.steppedParam = &param;
return entry.steppedInfo.emplace(param);
}
};
void scanParams() {
params.clear();
ParamScanner scanner{params};
effect.state(scanner);
for (auto &entry : params) entry.setClapInfo();
// Listen for changes from the effect UI
effect.paramListenerRange = [&](stfx::web::ParamContext context, double value){
params[context.index].hostValueSent.clear();
if (hostParams) hostParams->request_flush(host);
};
effect.paramListenerGesture = [&](stfx::web::ParamContext context, bool gesture){
if (gesture) {
params[context.index].hostStartGestureSent.clear();
} else {
params[context.index].hostStopGestureSent.clear();
}
if (hostParams) hostParams->request_flush(host);
};
effect.paramListenerStepped = [&](stfx::web::ParamContext context, int value){
params[context.index].hostValueSent.clear();
if (hostParams) hostParams->request_flush(host);
};
}
void sendParamEvents(const clap_output_events *events) {
for (auto &param : params) {
auto sendGesture = [&](uint16_t eventType){
clap_event_param_gesture event{
.header={
.size=sizeof(event),
.time=0,
.space_id=CLAP_CORE_EVENT_SPACE_ID,
.type=eventType
},
.param_id=param.id,
};
// Not *super* bothered if this fails
events->try_push(events, &event.header);
};
if (!param.hostStartGestureSent.test_and_set()) {
sendGesture(CLAP_EVENT_PARAM_GESTURE_BEGIN);
}
if (!param.hostValueSent.test_and_set()) {
clap_event_param_value event{
.header={
.size=sizeof(event),
.time=0,
.space_id=CLAP_CORE_EVENT_SPACE_ID,
.type=CLAP_EVENT_PARAM_VALUE
},
.param_id=param.id,
.cookie=param.cookie,
.note_id=-1,
.port_index=-1,
.channel=-1,
.key=-1
};
if (param.rangeParam) {
event.value = param.rangeInfo->toUnit((double)*param.rangeParam);
} else {
event.value = (int)*param.steppedParam;
}
if (!events->try_push(events, &event.header)) {
// failed, try again later
param.hostValueSent.clear();
continue;
}
}
if (!param.hostStopGestureSent.test_and_set()) {
sendGesture(CLAP_EVENT_PARAM_GESTURE_END);
}
}
}
// CLAP parameter methods
static uint32_t params_count(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
return plugin.params.size();
}
static bool params_get_info(const clap_plugin *obj, uint32_t index, clap_param_info *info) {
auto &plugin = *(Plugin *)obj;
if (index >= plugin.params.size()) return false;
*info = plugin.params[index];
return true;
}
static bool params_get_value(const clap_plugin *obj, clap_id paramId, double *value) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
if (param.rangeParam) {
*value = param.rangeInfo->toUnit((double)*param.rangeParam);
} else {
*value = (int)*param.steppedParam;
}
param.hostValueSent.test_and_set();
return true;
}
}
return false;
}
static bool params_value_to_text(const clap_plugin *obj, clap_id paramId, double value, char *text, uint32_t textCapacity) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
if (param.rangeParam) {
auto str = param.rangeInfo->toString(param.rangeInfo->fromUnit(value));
std::strncpy(text, str.c_str(), textCapacity);
} else {
auto str = param.steppedInfo->toString(int(std::round(value)));
std::strncpy(text, str.c_str(), textCapacity);
}
return true;
}
}
return false;
}
static bool params_text_to_value(const clap_plugin *obj, clap_id paramId, const char *text, double *value) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
std::string str = text;
if (param.rangeParam) {
*value = param.rangeInfo->toUnit(param.rangeInfo->fromString(str));
} else {
*value = param.steppedInfo->fromString(str);
}
return true;
}
}
return false;
}
static void params_flush(const clap_plugin *obj, const clap_input_events *inEvents, const clap_output_events *outEvents) {
auto &plugin = *(Plugin *)obj;
auto count = inEvents->size(inEvents);
for (uint32_t i = 0; i < count; ++i) {
auto *header = inEvents->get(inEvents, i);
plugin.processEvent(header);
}
plugin.sendParamEvents(outEvents);
}
// CLAP audio port methods
static uint32_t audio_ports_count(const clap_plugin *obj, bool inputPorts) {
auto &plugin = *(Plugin *)obj;
if (!plugin.isConfigured) {
plugin.isConfigured = plugin.effect.configurePersistent();
if (!plugin.isConfigured) return 0;
}
auto &config = plugin.effect.config;
if (inputPorts) {
return config.auxInputs.size() + (config.inputChannels > 0);
} else {
return config.auxOutputs.size() + (config.outputChannels > 0);
}
}
static bool audio_ports_get(const clap_plugin *obj, uint32_t index, bool inputPorts, clap_audio_port_info *info) {
auto &plugin = *(Plugin *)obj;
if (!plugin.isConfigured) return false;
auto &config = plugin.effect.config;
clap_id portIdBase = (inputPorts ? 0x1000000 : 0x2000000);
auto main = uint32_t(inputPorts ? config.inputChannels : config.outputChannels);
auto &aux = (inputPorts ? config.auxInputs : config.auxOutputs);
auto auxIndex = index;
if (main) {
if (index == 0) {
*info = {
.id=portIdBase,
.name={'m', 'a', 'i', 'n'},
.flags=CLAP_AUDIO_PORT_IS_MAIN,
.channel_count=main,
.port_type=nullptr,
.in_place_pair=CLAP_INVALID_ID
};
return true;
}
--auxIndex;
}
if (auxIndex < aux.size()) {
*info = {
.id=portIdBase + index,
.name={'a', 'u', 'x'},
.flags=CLAP_AUDIO_PORT_IS_MAIN,
.channel_count=main,
.port_type=nullptr,
.in_place_pair=CLAP_INVALID_ID
};
if (aux.size() > 1) {
info->name[3] = '1' + auxIndex;
}
return true;
}
return false;
}
static bool writeToStream(const unsigned char *bytes, size_t length, const clap_ostream *stream) {
size_t index = 0;
while (index < length) {
size_t remaining = length - index;
auto written = stream->write(stream, bytes + index, remaining);
if (written <= 0) return false;
index += written;
}
return true;
}
static bool fillFromStream(const clap_istream *stream, std::vector<unsigned char> &buffer) {
buffer.resize(0);
size_t chunkSize = 1024;
size_t length = 0;
while (1) {
buffer.resize(length + chunkSize);
auto read = stream->read(stream, buffer.data() + length, chunkSize);
length += read;
if (read < 0) {
buffer.resize(0);
return false;
} else if (read == 0) {
buffer.resize(length);
return true;
}
chunkSize = buffer.size();
}
}
std::vector<unsigned char> stateBuffer;
static bool state_save(const clap_plugin *obj, const clap_ostream *stream) {
auto &plugin = *(Plugin *)obj;
auto &buffer = plugin.stateBuffer;
plugin.effect.saveState(buffer);
return writeToStream(buffer.data(), buffer.size(), stream);
}
static bool state_load(const clap_plugin *obj, const clap_istream *stream) {
auto &plugin = *(Plugin *)obj;
auto &buffer = plugin.stateBuffer;
if (!fillFromStream(stream, buffer)) return false;
plugin.effect.loadState(buffer);
plugin.sendWebMessages(); // should already be on the main thread
return true;
}
static bool webview1_provide_starting_uri(const clap_plugin_t *obj, char *startingUri, uint32_t capacity) {
auto &plugin = *(Plugin *)obj;
if (!plugin.hostWebview1) return false;
if (!plugin.effect.webPage.size() + 1 > capacity) return false;
std::strcpy(startingUri, plugin.effect.webPage.c_str());
return true;
}
static bool webview1_receive(const clap_plugin_t *obj, const void *buffer, uint32_t size) {
auto &plugin = *(Plugin *)obj;
plugin.effect.webReceive(buffer, size);
// TODO: *double* check we're on the main thread, for safety
plugin.sendWebMessages();
return true;
}
void sendWebMessages() {
if (!hostWebview1) return;
while (auto *m = effect.getPendingWebMessage()) {
hostWebview1->send(host, m->bytes.data(), m->bytes.size());
m->sent();
}
}
};
}} // namespace
extern stfx::clap::Plugins stfxPlugins;

View File

@ -1,336 +0,0 @@
#ifndef SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
#define SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
#include <vector>
#include <cmath>
#include <string>
#include <sstream>
#include <iomanip>
#include <map>
namespace stfx {
class UnitRangeMap {
double a, b, d;
public:
UnitRangeMap() : a(0), b(1), d(0) {} // identity
UnitRangeMap(double min, double mid, double max) {
double k = (mid - min)/(max - mid);
a = min;
b = max*k - min;
d = k - 1;
}
double toUnit(double value) const {
return (value - a)/(b - value*d);
}
double fromUnit(double unit) const {
return (a + unit*b)/(1 + unit*d);
}
};
struct RangeParamInfo {
double defaultValue;
double low, mid, high;
std::string name, description;
RangeParamInfo(double defaultValue) : defaultValue(defaultValue), low(defaultValue), mid(defaultValue), high(defaultValue) {}
RangeParamInfo(const RangeParamInfo &other) = delete;
RangeParamInfo(RangeParamInfo &&other) = default;
RangeParamInfo & info(std::string pName, std::string pDescription) {
name = pName;
description = pDescription;
return *this;
}
RangeParamInfo & range(double pLow, double pHigh) {
return range(pLow, pHigh, (pLow + pHigh)*0.5);
}
RangeParamInfo & range(double pLow, double pMid, double pHigh) {
low = pLow;
mid = pMid;
high = pHigh;
// sanity check in case we mix the argument order up
if ((mid < high) != (low < high)) std::swap(mid, high); // middle crosses the high
if ((low < mid) != (low < high)) std::swap(low, mid); // middle crosses the low
rangeMap = {low, mid, high};
return *this;
}
typedef double((*DoubleMap)(double));
RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromDisplay=identityMap, DoubleMap toDisplay=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) {
unitOptions.emplace_back(suffix, fromDisplay, toDisplay, validLow, validHigh, precision);
return *this;
}
RangeParamInfo & unit(std::string suffix, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow=doubleLowest, double validHigh=doubleMax) {
return unit(suffix, 2, fromDisplay, toDisplay, validLow, validHigh);
}
RangeParamInfo & unit(std::string suffix, int precision, double validLow, double validHigh) {
return unit(suffix, precision, identityMap, identityMap, validLow, validHigh);
}
RangeParamInfo & unit(std::string suffix, double validLow, double validHigh) {
return unit(suffix, identityMap, identityMap, validLow, validHigh);
}
RangeParamInfo & exact(double v, std::string valueName) {
exactOptions.push_back({v, valueName});
return *this;
}
std::string toString(double value) const {
for (auto &option : exactOptions) {
if (value == option.value) return option.name;
}
for (const auto &unit : unitOptions) {
if (unit.valid(value)) return unit.toString(value);
}
if (unitOptions.empty()) return std::to_string(value);
return unitOptions[0].toString(value);
}
void toString(double value, std::string &valueString, std::string &unitString) const {
for (auto &option : exactOptions) {
if (value == option.value) {
valueString = option.name;
unitString = "";
return;
}
}
for (const auto &unit : unitOptions) {
if (unit.valid(value)) return unit.toString(value, valueString, unitString);
}
if (unitOptions.empty()) {
valueString = std::to_string(value);
unitString = "";
}
unitOptions[0].toString(value, valueString, unitString);
}
double fromString(const std::string &str) const {
bool hasDecimal = false, hasDigit = false;;
{ // check for a parseable number
size_t pos = 0;
while (str[pos] == '\t' || str[pos] == ' ') ++pos; // strip leading whitespace
if (str[pos] == '-') ++pos;
while (pos < str.length()) {
if (!hasDecimal && (str[pos] == '.' || str[pos] == ',')) {
hasDecimal = true;
} else if (str[pos] >= '0' && str[pos] <= '9') {
hasDigit = true;
} else {
break;
}
++pos;
}
}
// It's not a number - look for an exact match
if (!hasDigit) {
int longestMatch = -1;
double result = defaultValue;
for (auto &option : exactOptions) {
for (size_t i = 0; i < option.name.size() && i < str.size(); ++i) {
if (option.name[i] == str[i]) {
if (int(i) > longestMatch) {
longestMatch = i;
result = option.value;
}
} else {
break;
}
}
}
return result;
}
size_t pos = 0;
double result = std::stod(str, 0);
// Skip whitespace after the number
while (pos < str.length() && (str[pos] == ' ' || str[pos] == '\t')) ++pos;
size_t end = str.length(); // and at the end
while (end > pos && end > 0 && (str[end - 1] == ' ' || str[end - 1] == '\t')) --end;
std::string unit = str.substr(pos, end - pos);
for (const auto &u : unitOptions) {
if (u.unit == unit) {
return u.fromDisplay(result);
}
}
return result;
}
std::vector<std::string> getUnits() const {
std::vector<std::string> result;
for (size_t i = 0; i < unitOptions.size(); ++i) {
bool duplicate = false;
for (size_t j = 0; j < i; ++j) {
if (unitOptions[i].unit == unitOptions[j].unit) {
duplicate = true;
break;
}
}
if (!duplicate) result.push_back(unitOptions[i].unit);
}
return result;
}
double toUnit(double value) const {
return rangeMap.toUnit(value);
}
double fromUnit(double unit) const {
return rangeMap.fromUnit(unit);
}
private:
static constexpr double doubleLowest = std::numeric_limits<double>::lowest();
static constexpr double doubleMax = std::numeric_limits<double>::max();
static double identityMap(double v) {
return v;
}
// A map from [0, 1] to another range with specified midpoint, based on a 1/x curve
UnitRangeMap rangeMap;
protected:
struct ExactEntry {
double value;
std::string name;
};
std::vector<ExactEntry> exactOptions;
struct UnitEntry {
std::string fixedDisplay = "";
std::string unit;
bool addSpace = false, useFixed = false, keepZeros = false;
DoubleMap fromDisplay, toDisplay;
double validLow, validHigh;
int precision;
double precisionOffset = 0;
UnitEntry(std::string unit, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow, double validHigh, int precision=2) : unit(unit), fromDisplay(fromDisplay), toDisplay(toDisplay), validLow(validLow), validHigh(validHigh), precision(precision) {
if (validLow > validHigh) {
std::swap(validLow, validHigh);
keepZeros = true;
}
if (unit[0] == ' ') {
addSpace = true;
this->unit = unit.substr(1, unit.size() - 1);
}
precisionOffset = 0.4999*std::pow(10, -precision);
}
bool valid(double value) const {
return value >= validLow && value <= validHigh;
}
void toString(double value, std::string &valueString, std::string &unitString) const {
std::ostringstream oss;
oss.precision(precision);
oss << std::fixed;
double offset = 0;
if (precision > 0) {
oss << toDisplay(value) + offset;
} else {
oss << (int)(toDisplay(value) + offset);
}
valueString = oss.str();
// Strip trailing zeroes
if (!keepZeros) for (int i = 0; i < (int)valueString.size(); ++i) {
if (valueString[i] == '.') {
int zeros = valueString.size();
while (zeros > i && valueString[zeros - 1] == '0') --zeros;
if (zeros == i + 1) --zeros;
valueString = valueString.substr(0, zeros);
break;
}
}
unitString = unit;
}
std::string toString(double value) const {
std::string valueString, unitString;
toString(value, valueString, unitString);
return valueString + (addSpace ? " " : "") + unitString;
}
};
std::vector<UnitEntry> unitOptions;
};
struct SteppedParamInfo {
int defaultValue;
int low, high;
std::string name, description;
SteppedParamInfo(int defaultValue) : defaultValue(defaultValue), low(defaultValue), high(defaultValue), nameIndex(defaultValue) {}
SteppedParamInfo(const SteppedParamInfo &other) = delete;
SteppedParamInfo(SteppedParamInfo &&other) = default;
SteppedParamInfo & info(std::string pName, std::string pDescription) {
name = pName;
description = pDescription;
return *this;
}
SteppedParamInfo & range(int pLow, int pHigh) {
low = pLow;
high = pHigh;
return *this;
}
SteppedParamInfo & label(const char *n) {
if (nameIndex > high && high >= low) high = nameIndex;
if (nameIndex > low && low > high) low = nameIndex;
nameMap[nameIndex] = n;
valueMap[n] = nameIndex;
return *this;
}
bool fullyLabelled() const {
return int(nameMap.size()) == (high - low + 1);
}
template<class ...Args>
SteppedParamInfo & label(const char *first, Args... others) {
label(first);
++nameIndex;
return label(others...);
}
template<class ...Args>
SteppedParamInfo & label(int start, const char *first, Args... others) {
nameIndex = start;
return label(first, others...);
}
const std::string * getLabel(int value) const {
auto pair = nameMap.find(value);
if (pair != nameMap.end()) {
return &pair->second;
}
return nullptr;
}
std::string toString(int value) const {
const std::string *maybeLabel = getLabel(value);
if (maybeLabel) return *maybeLabel;
return std::to_string(value);
}
int fromString(const std::string &str) const {
auto pair = valueMap.find(str);
if (pair != valueMap.end()) return pair->second;
size_t pos = 0;
if (str[0] == '-') ++pos;
while (pos < str.length() && str[pos] >= '0' && str[pos] <= '9') ++pos;
if (pos >= str.length() || pos == 0 || (pos == 1 && str[0] == '-')) {
return defaultValue;
}
int result = std::stoi(str, &pos);
if (high >= low) {
return std::max<int>(std::min<int>(high, result), low);
} else {
return std::max<int>(std::min<int>(low, result), high);
}
}
private:
int nameIndex;
protected:
std::map<int, std::string> nameMap;
std::map<std::string, int> valueMap;
};
} // namespace
#endif // include guard

View File

@ -1,990 +0,0 @@
#ifndef SIGNALSMITH_CBOR_WALKER_H
#define SIGNALSMITH_CBOR_WALKER_H
#include <cstdint>
#include <cmath>
#include <cstring>
#ifndef UINT64_MAX
# define UINT64_MAX 0xFFFFFFFFFFFFFFFFull;
#endif
#include <vector>
#include <string>
#if __cplusplus >= 201703L
# define CBOR_WALKER_USE_STRING_VIEW
# include <string_view>
#endif
#if __cplusplus >= 202002L
# define CBOR_WALKER_USE_BIT_CAST
# include <bit>
#endif
namespace signalsmith { namespace cbor {
struct CborWalker {
CborWalker(uint64_t errorCode=ERROR_NOT_INITIALISED) : CborWalker(nullptr, nullptr, errorCode) {}
CborWalker(const std::vector<unsigned char> &vector) : CborWalker(vector.data(), vector.data() + vector.size()) {}
CborWalker(const unsigned char *data, const unsigned char *dataEnd) : data(data), dataEnd(dataEnd) {
if (data >= dataEnd) {
typeCode = TypeCode::error;
additional = ERROR_END_OF_DATA;
return;
}
unsigned char head = *data;
typeCode = (TypeCode)(head>>5);
unsigned char remainder = head&0x1F;
switch (remainder) {
case 24:
additional = data[1];
dataNext = data + 2;
break;
case 25:
if (typeCode == TypeCode::simple) {
#ifdef CBOR_WALKER_HALF_PRECISION_FLOAT
// Translated from RFC 8949 Appendix D
uint16_t half = ((uint16_t)data[1]<<8) + data[2];
uint16_t exponent = (half>>10)&0x001F;
uint16_t mantissa = half&0x03FF;
double value;
if (exponent == 0) {
value = std::ldexpf(mantissa, -24);
} else if (exponent == 31) {
value = (mantissa == 0) ? INFINITY : NAN;
} else {
value = std::ldexpf(mantissa + 1024, exponent - 25);
}
typeCode = TypeCode::float32;
float32 = (half&0x8000) ? -value : value;
#else
float32 = 0;
#endif
} else {
additional = (uint64_t(data[1])<<8)|uint64_t(data[2]);
}
dataNext = data + 3;
break;
case 26:
if (typeCode == TypeCode::simple) {
typeCode = TypeCode::float32;
}
additional = (uint64_t(data[1])<<24)|(uint64_t(data[2])<<16)|(uint64_t(data[3])<<8)|uint64_t(data[4]);
dataNext = data + 5;
break;
case 27:
if (typeCode == TypeCode::simple) {
typeCode = TypeCode::float64;
}
additional = (uint64_t(data[1])<<56)|(uint64_t(data[2])<<48)|(uint64_t(data[3])<<40)|(uint64_t(data[4])<<32)|(uint64_t(data[5])<<24)|(uint64_t(data[6])<<16)|(uint64_t(data[7])<<8)|uint64_t(data[8]);
dataNext = data + 9;
break;
case 28:
case 29:
case 30:
typeCode = TypeCode::error;
additional = ERROR_INVALID_ADDITIONAL;
dataNext = data;
case 31:
additional = 0; // returns 0 length for indefinite values
switch (typeCode) {
case TypeCode::integerP:
case TypeCode::integerN:
case TypeCode::tag:
typeCode = TypeCode::error;
additional = ERROR_INVALID_ADDITIONAL;
break;
case TypeCode::bytes:
typeCode = TypeCode::indefiniteBytes;
break;
case TypeCode::utf8:
typeCode = TypeCode::indefiniteUtf8;
break;
case TypeCode::array:
typeCode = TypeCode::indefiniteArray;
break;
case TypeCode::map:
typeCode = TypeCode::indefiniteMap;
break;
case TypeCode::simple:
typeCode = TypeCode::indefiniteBreak;
break;
default:
typeCode = TypeCode::error;
additional = ERROR_SHOULD_BE_IMPOSSIBLE;
break;
}
default:
additional = remainder;
dataNext = data + 1;
break;
}
}
// All error codes are non-zero, so can be checked with `.error()`
static constexpr uint64_t ERROR_END_OF_DATA = 1;
static constexpr uint64_t ERROR_INVALID_ADDITIONAL = 2;
static constexpr uint64_t ERROR_INVALID_VALUE = 3;
static constexpr uint64_t ERROR_INCONSISTENT_INDEFINITE = 4;
static constexpr uint64_t ERROR_NOT_INITIALISED = 5;
static constexpr uint64_t ERROR_METHOD_TYPE_MISMATCH = 6;
static constexpr uint64_t ERROR_SHOULD_BE_IMPOSSIBLE = 7;
CborWalker next(size_t count) const {
CborWalker result = *this;
for (size_t i = 0; i < count; ++i) {
++result;
}
return result;
}
CborWalker next() const {
switch (typeCode) {
case TypeCode::integerP:
case TypeCode::integerN:
case TypeCode::simple:
case TypeCode::float32:
case TypeCode::float64:
case TypeCode::indefiniteBreak:
return nextBasic();
case TypeCode::bytes:
case TypeCode::utf8:
return {dataNext + additional, dataEnd};
return {dataNext + additional, dataEnd};
case TypeCode::array: {
auto result = nextBasic();
auto length = additional;
for (uint64_t i = 0; i < length; ++i) {
++result;
}
return result;
}
case TypeCode::map: {
auto result = nextBasic();
auto length = additional;
for (uint64_t i = 0; i < length; ++i) {
++result;
++result;
}
return result;
}
case TypeCode::indefiniteBytes: {
auto result = nextBasic();
while (!result.error() && result.typeCode != TypeCode::indefiniteBreak) {
if (result.typeCode != TypeCode::bytes) {
return {data, dataEnd, ERROR_INCONSISTENT_INDEFINITE};
}
++result;
}
return result.nextBasic();
}
case TypeCode::indefiniteUtf8: {
auto result = nextBasic();
while (!result.error() && result.typeCode != TypeCode::indefiniteBreak) {
if (result.typeCode != TypeCode::utf8) {
return {data, dataEnd, ERROR_INCONSISTENT_INDEFINITE};
}
++result;
}
return result.nextBasic();
}
case TypeCode::indefiniteArray: {
auto result = nextBasic();
while (!result.error() && result.typeCode != TypeCode::indefiniteBreak) {
result = result.next();
}
return result.nextBasic();
}
case TypeCode::indefiniteMap: {
auto result = nextBasic();
while (!result.error() && result.typeCode != TypeCode::indefiniteBreak) {
++result;
++result;
}
return result.nextBasic();
}
case TypeCode::tag: {
// Skip all the tags first
auto result = nextBasic();
while (result.isTagged()) result = nextBasic();
return result.next();
}
case TypeCode::error:
return *this;
}
}
// ++Prefix increments the position, and returns itself
CborWalker & operator++() {
*this = next();
return *this;
}
// Postfix++ increments the position, but returns the old position
CborWalker operator++(int) {
CborWalker result = *this;
*this = next();
return result;
}
CborWalker enter() const {
switch (typeCode) {
case TypeCode::integerP:
case TypeCode::integerN:
case TypeCode::simple:
case TypeCode::float32:
case TypeCode::float64:
case TypeCode::indefiniteBreak:
case TypeCode::bytes:
case TypeCode::utf8:
return next();
case TypeCode::tag:
case TypeCode::array:
case TypeCode::map:
case TypeCode::indefiniteBytes:
case TypeCode::indefiniteUtf8:
case TypeCode::indefiniteArray:
case TypeCode::indefiniteMap:
return nextBasic();
case TypeCode::error:
return *this;
}
}
CborWalker nextExit() const {
CborWalker result = *this;
while (!result.error() && !result.isExit()) {
++result;
}
return result.nextBasic();
}
uint64_t error() const {
return typeCode == TypeCode::error ? additional : 0;
}
bool isSimple() const {
return typeCode == TypeCode::simple;
}
bool isBool() const {
if (typeCode != TypeCode::simple) return false;
return (additional == 20 || additional == 21);
}
explicit operator bool() const {
return (additional == 21);
}
bool isNull() const {
return typeCode == TypeCode::simple && additional == 22;
}
bool isUndefined() const {
return typeCode == TypeCode::simple && additional == 23;
}
bool isExit() const {
return typeCode == TypeCode::indefiniteBreak;
}
bool atEnd() const {
return typeCode == TypeCode::error && additional == ERROR_END_OF_DATA;
}
bool isNumber() const {
return isFloat() || isInt();
}
bool isInt() const {
return typeCode == TypeCode::integerP || typeCode == TypeCode::integerN;
}
operator uint64_t() const {
switch (typeCode) {
case TypeCode::integerP:
case TypeCode::bytes:
case TypeCode::utf8:
case TypeCode::array:
case TypeCode::map:
case TypeCode::tag:
case TypeCode::simple:
case TypeCode::error:
return (int64_t)additional;
case TypeCode::integerN:
return (uint64_t)-1 - (uint64_t)additional;
return additional;
case TypeCode::float32:
return (uint64_t)float32;
case TypeCode::float64:
return (uint64_t)float64;
default:
return 0;
}
}
operator int64_t() const {
switch (typeCode) {
case TypeCode::integerP:
case TypeCode::bytes:
case TypeCode::utf8:
case TypeCode::array:
case TypeCode::map:
case TypeCode::tag:
case TypeCode::simple:
case TypeCode::error:
return (int64_t)additional;
case TypeCode::integerN:
return -1 - (int64_t)additional;
case TypeCode::float32:
return (uint64_t)float32;
case TypeCode::float64:
return (uint64_t)float64;
default:
return 0;
}
}
operator uint32_t() const {
return (uint32_t)(uint64_t)(*this);
}
operator uint16_t() const {
return (uint16_t)(uint64_t)(*this);
}
operator uint8_t() const {
return (uint32_t)(uint64_t)(*this);
}
operator size_t() const {
return (size_t)(uint64_t)(*this);
}
// For the signed ones, we cast from the signed 64-bit
operator int32_t() const {
return (int32_t)(int64_t)(*this);
}
operator int16_t() const {
return (int16_t)(int64_t)(*this);
}
operator int8_t() const {
return (int8_t)(int64_t)(*this);
}
bool isFloat() const {
return typeCode == TypeCode::float32 || typeCode == TypeCode::float64;
}
operator double() const {
switch (typeCode) {
case TypeCode::float32:
return float32;
case TypeCode::float64:
return float64;
case TypeCode::integerP:
return (uint64_t)(*this);
case TypeCode::integerN:
return (int64_t)(*this);
default:
return 0;
}
}
operator float() const {
return (float)(double)(*this);
}
bool isBytes() const {
return typeCode == TypeCode::bytes || typeCode == TypeCode::indefiniteBytes;
}
bool isUtf8() const {
return typeCode == TypeCode::utf8 || typeCode == TypeCode::indefiniteUtf8;
}
bool hasLength() const {
return typeCode != TypeCode::indefiniteBytes && typeCode != TypeCode::indefiniteUtf8 && typeCode != TypeCode::indefiniteArray && typeCode != TypeCode::indefiniteMap;
}
size_t length() const {
return (size_t)(*this);
}
const unsigned char * bytes() const {
return dataNext;
}
std::string utf8() const {
if (typeCode != TypeCode::utf8) return "";
return {(const char *)dataNext, length()};
}
#ifdef CBOR_WALKER_USE_STRING_VIEW
std::string_view utf8View() const {
if (typeCode != TypeCode::utf8) return {nullptr, 0};
return {(const char *)dataNext, length()};
}
#endif
bool isArray() const {
return typeCode == TypeCode::array || typeCode == TypeCode::indefiniteArray;
}
template<class Fn>
CborWalker forEach(Fn &&fn, bool mapValues=true) const {
if (typeCode == TypeCode::array) {
size_t count = length();
CborWalker item = enter();
for (size_t i = 0; i < count; ++i) {
if (item.error()) return item;
CborWalker value = item++;
fn(value, i);
}
return item;
} else if (typeCode == TypeCode::indefiniteArray) {
CborWalker item = enter();
size_t i = 0;
while (!item.error() && !item.isExit()) {
fn(item++, i++);
}
return item.next(); // move past the exit
} else if (typeCode == TypeCode::indefiniteBytes) {
CborWalker item = enter();
size_t i = 0;
while (!item.error() && !item.isExit()) {
if (item.typeCode != TypeCode::bytes) return {data, dataEnd, ERROR_INVALID_VALUE};
fn(item++, i++);
}
return item.next(); // move past the exit
} else if (typeCode == TypeCode::indefiniteUtf8) {
CborWalker item = enter();
size_t i = 0;
while (!item.error() && !item.isExit()) {
if (item.typeCode != TypeCode::utf8) return {data, dataEnd, ERROR_INVALID_VALUE};
fn(item++, i++);
}
return item.next(); // move past the exit
} else if (typeCode == TypeCode::map) {
size_t count = length();
CborWalker item = enter();
for (size_t i = 0; i < count; ++i) {
if (item.error()) return item;
CborWalker key = item++;
if (item.error()) return item;
CborWalker value = item++;
fn(mapValues ? value : key, i);
}
return item;
} else if (typeCode == TypeCode::indefiniteMap) {
CborWalker item = enter();
size_t i = 0;
while (!item.error() && !item.isExit()) {
CborWalker key = item++;
if (item.error()) return item;
if (item.isExit()) return {item.data, item.dataEnd, ERROR_INVALID_VALUE};
CborWalker value = item++;
fn(mapValues ? value : key, i);
++i;
}
return item.next(); // move past the exit
}
return {data, dataEnd, ERROR_METHOD_TYPE_MISMATCH};
}
bool isMap() const {
return typeCode == TypeCode::map || typeCode == TypeCode::indefiniteMap;
}
template<class Fn>
CborWalker forEachPair(Fn &&fn) const {
if (typeCode == TypeCode::map) {
size_t count = length();
CborWalker item = enter();
for (size_t i = 0; i < count; ++i) {
auto key = item++;
if (key.error() || item.error()) return item;
auto value = item++;
fn(key, value);
}
return item;
} else if (typeCode == TypeCode::indefiniteMap) {
CborWalker item = enter();
while (!item.error() && !item.isExit()) {
auto key = item++;
if (key.error() || item.error()) return item;
if (item.isExit()) return {item.data, item.dataEnd, ERROR_INVALID_VALUE};
auto value = item++;
fn(CborWalker(key), CborWalker(value));
}
return item.next(); // move past the exit
}
return {data, dataEnd, ERROR_METHOD_TYPE_MISMATCH};
}
bool isEnd() const {
return typeCode == TypeCode::array || typeCode == TypeCode::indefiniteArray;
}
bool isTagged() const {
return typeCode == TypeCode::tag;
}
protected:
CborWalker(const unsigned char *data, const unsigned char *dataEnd, uint64_t errorCode) : data(data), dataEnd(dataEnd), dataNext(nullptr), typeCode(TypeCode::error), additional(errorCode) {}
// The next *core* value - but doesn't check whether the current value is the header for a string/array/etc.
CborWalker nextBasic() const {
return {dataNext, dataEnd};
}
const unsigned char *data, *dataEnd, *dataNext;
enum class TypeCode {
integerP, integerN, bytes, utf8, array, map, tag, simple, float32, float64,
error, indefiniteBreak, indefiniteBytes, indefiniteUtf8, indefiniteArray, indefiniteMap
};
TypeCode typeCode;
union {
uint64_t additional;
float float32;
double float64;
unsigned char additionalBytes[8];
};
};
// Automatically skips over tags, but still lets you query them
struct TaggedCborWalker : public CborWalker {
TaggedCborWalker() {}
TaggedCborWalker(const CborWalker& basic) : CborWalker(basic), tagStart(data) {
consumeTags();
}
TaggedCborWalker(const unsigned char *dataStart, const unsigned char *dataEnd) : CborWalker(dataStart, dataEnd), tagStart(dataStart) {
consumeTags();
}
TaggedCborWalker next(size_t i=1) const {
return CborWalker::next(i);
}
TaggedCborWalker & operator++() {
CborWalker::operator++();
return *this;
}
TaggedCborWalker operator++(int _) {
return CborWalker::operator++(_);
}
TaggedCborWalker enter() const {
return CborWalker::enter();
}
TaggedCborWalker nextExit() const {
return CborWalker::nextExit();
}
template<class Fn>
TaggedCborWalker forEach(Fn &&fn) const {
return CborWalker::forEach([&](const CborWalker &item, size_t i){
fn(TaggedCborWalker{item}, i);
});
}
template<class Fn>
TaggedCborWalker forEachPair(Fn &&fn) const {
return CborWalker::forEachPair([&](const CborWalker &key, const CborWalker &value){
fn(TaggedCborWalker{key}, TaggedCborWalker{value});
});
}
size_t tagCount() const {
return nTags;
}
uint64_t tag(size_t tagIndex) const {
CborWalker tagWalker(tagStart, dataEnd);
for (size_t i = 0; i < tagIndex; ++i) {
tagWalker = tagWalker.enter();
}
return tagWalker;
}
bool isTypedArray() const {
return isBytes() && typedArrayTag;
}
size_t typedArrayLength() const {
uint8_t widthLog2 = typedArrayTag&0x03;
uint8_t elementType = (typedArrayTag&0x18)>>3; // unsigned, signed, float
widthLog2 += (elementType == 2); // int sizes are 8-64 bits, float sizes are 16-128
size_t stride = 1<<widthLog2;
return length()/stride;
}
template<class Array>
size_t readTypedArray(Array &&array) const {
return readTypedArray(array, 0, typedArrayLength());
}
template<class Array>
size_t readTypedArray(Array &&array, size_t offset, size_t maxCount) const {
size_t byteLength = length();
bool bigEndian = !(typedArrayTag&0x04);
switch (typedArrayTag&0xFB) { // without endian flag
// unsigned int
case 64: {
size_t count = std::min(maxCount, byteLength);
const uint8_t *bytes = dataNext + offset;
for (size_t i = 0; i < count; ++i) {
array[i] = bytes[i];
}
return count;
}
case 65:
return typedArrayReadInner<Array, uint16_t, uint16_t>(array, offset, maxCount, bigEndian);
case 66:
return typedArrayReadInner<Array, uint32_t, uint32_t>(array, offset, maxCount, bigEndian);
case 67:
return typedArrayReadInner<Array, uint64_t, uint64_t>(array, offset, maxCount, bigEndian);
// signed int
case 72: {
size_t count = std::min(maxCount, byteLength);
const uint8_t *bytes = dataNext + offset;
for (size_t i = 0; i < count; ++i) {
array[i] = (int8_t)bytes[i]; // cast to signed here first, to make sure negatives behave correctly
}
return count;
}
case 73:
return typedArrayReadInner<Array, uint16_t, int16_t>(array, offset, maxCount, bigEndian);
case 74:
return typedArrayReadInner<Array, uint32_t, int32_t>(array, offset, maxCount, bigEndian);
case 75:
return typedArrayReadInner<Array, uint64_t, int64_t>(array, offset, maxCount, bigEndian);
// floating-point
case 80:
// TODO: half-precision float support
return 0;
case 81:
return typedArrayReadInner<Array, uint32_t, float, true>(array, offset, maxCount, bigEndian);
case 82:
return typedArrayReadInner<Array, uint64_t, double, true>(array, offset, maxCount, bigEndian);
case 83:
// TODO: quad-precision float support
return 0;
default:
return 0;
}
}
private:
size_t nTags = 0;
const unsigned char *tagStart;
uint8_t typedArrayTag = 0;
void consumeTags() {
while (isTagged() && data < dataEnd) {
++nTags;
uint64_t tag = (*this);
if (tag >= 64 && tag < 87) { // RFC-8746 range
typedArrayTag = tag;
}
// Move "into" the tag
CborWalker::operator=(enter());
}
}
template<class Array, typename UIntType, typename ResultT, bool bitcast=false>
size_t typedArrayReadInner(Array &&array, size_t offset, size_t maxCount, bool bigEndian) const {
constexpr size_t B = sizeof(UIntType);
if (offset*B > length()) return 0;
const uint8_t *bytes = dataNext + offset*B;
size_t count = std::min(maxCount, length()/B - offset);
if (bigEndian) {
for (size_t i = 0; i < count; ++i) {
UIntType v = 0;
for (size_t b = 0; b < B; ++b) {
UIntType bv = bytes[i*B + b];
v += bv<<((B - 1 - b)*8);
}
if (bitcast) {
#ifdef CBOR_WALKER_USE_BIT_CAST
array[i] = std::bit_cast<ResultT>(v);
#else
ResultT r;
std::memcpy(&r, &v, B);
array[i] = r;
#endif
} else {
array[i] = (ResultT)v;
}
}
} else {
for (size_t i = 0; i < count; ++i) {
UIntType v = 0;
for (size_t b = 0; b < B; ++b) {
UIntType bv = bytes[i*B + b];
v += bv<<(b*8);
}
if (bitcast) {
#ifdef CBOR_WALKER_USE_BIT_CAST
array[i] = std::bit_cast<ResultT>(v);
#else
ResultT r;
std::memcpy(&r, &v, B);
array[i] = r;
#endif
} else {
array[i] = (ResultT)v;
}
}
}
return count;
}
};
struct CborWriter {
CborWriter(std::vector<unsigned char> &bytes) : bytes(bytes) {}
CborWriter & addUInt(uint64_t u) {
writeHead(0, u);
return *this;
}
CborWriter & addInt(int64_t u) {
if (u >= 0) {
writeHead(0, u);
} else {
writeHead(1, -1 - u);
}
return *this;
}
CborWriter & addTag(uint64_t u) {
writeHead(6, u);
return *this;
}
CborWriter & addBool(bool b) {
writeHead(7, 20 + b);
return *this;
}
CborWriter & openArray() {
bytes.push_back(0x9F);
return *this;
}
CborWriter & openArray(size_t items) {
writeHead(4, items);
return *this;
}
CborWriter & openMap() {
bytes.push_back(0xBF);
return *this;
}
CborWriter & openMap(size_t pairs) {
writeHead(5, pairs);
return *this;
}
CborWriter & close() {
bytes.push_back(0xFF);
return *this;
}
CborWriter & addBytes(const void *ptr, size_t length) {
addBytes((const unsigned char *)ptr, length);
return *this;
}
CborWriter & addBytes(const unsigned char *ptr, size_t length) {
writeHead(2, length);
bytes.insert(bytes.end(), ptr, ptr + length);
return *this;
}
CborWriter & openBytes() {
bytes.push_back(0x5F);
return *this;
}
CborWriter & addUtf8(const char *ptr, size_t length) {
writeHead(3, length);
bytes.insert(bytes.end(), ptr, ptr + length);
return *this;
}
CborWriter & addUtf8(const char *str) {
addUtf8(str, std::strlen(str));
return *this;
}
CborWriter & addUtf8(const std::string &str) {
addUtf8(str.c_str());
return *this;
}
#ifdef CBOR_WALKER_USE_STRING_VIEW
CborWriter & addUtf8(const std::string_view &str) {
addUtf8(str.data(), str.size());
return *this;
}
#endif
CborWriter & openUtf8() {
bytes.push_back(0x7F);
return *this;
}
CborWriter & addNull() {
bytes.push_back(0xF6);
return *this;
}
CborWriter & addUndefined() {
bytes.push_back(0xF7);
return *this;
}
CborWriter & addSimple(unsigned char k) {
writeHead(7, k);
return *this;
}
CborWriter & addFloat(float v) {
bytes.push_back(0xFA);
#ifdef CBOR_WALKER_USE_BIT_CAST
uint32_t vi = std::bit_cast<uint32_t>(v);
#else
uint32_t vi;
std::memcpy(&vi, &v, 4);
#endif
for (size_t i = 0; i < 4; ++i) {
auto shift = (3 - i)*8;
bytes.push_back((vi>>shift)&0xFF);
}
return *this;
}
CborWriter & addFloat(double v) {
bytes.push_back(0xFB);
#ifdef CBOR_WALKER_USE_BIT_CAST
uint64_t vi = std::bit_cast<uint64_t>(v);
#else
uint64_t vi;
std::memcpy(&vi, &v, 8);
#endif
for (size_t i = 0; i < 8; ++i) {
auto shift = (7 - i)*8;
bytes.push_back((vi>>shift)&0xFF);
}
return *this;
}
// RFC-8746 tags for typed arrays
// bits: [1, 0] = log2(elementBytes), [2] = isLittleEndian, [3, 4] = [unsigned, signed, float]
CborWriter & addTypedArray(const uint8_t *arr, size_t length) {
addTag(64);
addBytes((const void *)arr, length);
return *this;
}
CborWriter & addTypedArray(const int8_t *arr, size_t length) {
addTag(72);
addBytes((const void *)arr, length);
return *this;
}
CborWriter & addTypedArray(const uint16_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 65 : 69);
writeTypedBlock<uint16_t>(arr, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const uint32_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 66 : 70);
writeTypedBlock<uint32_t>(arr, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const uint64_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 67 : 71);
writeTypedBlock<uint64_t>(arr, length, bigEndian);
return *this;
}
// For signed ints, we make a proxy struct which casts them on-the-fly
CborWriter & addTypedArray(const int16_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 73 : 77);
struct {
const int16_t *arr;
uint16_t operator[](size_t i) const {
return (uint16_t)(arr[i]);
}
} unsignedArray{arr};
writeTypedBlock<uint16_t>(unsignedArray, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const int32_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 74 : 78);
struct {
const int32_t *arr;
uint32_t operator[](size_t i) const {
return (uint32_t)(arr[i]);
}
} unsignedArray{arr};
writeTypedBlock<uint32_t>(unsignedArray, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const int64_t *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 75 : 79);
struct {
const int64_t *arr;
uint64_t operator[](size_t i) const {
return (uint64_t)(arr[i]);
}
} unsignedArray{arr};
writeTypedBlock<uint64_t>(unsignedArray, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const float *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 81 : 85);
struct {
const float *arr;
uint32_t operator[](size_t i) const {
#ifdef CBOR_WALKER_USE_BIT_CAST
return std::bit_cast<uint32_t>(arr[i]);
#else
float v = arr[i];
uint32_t vi;
std::memcpy(&vi, &v, 4);
return vi;
#endif
}
} unsignedArray{arr};
writeTypedBlock<uint32_t>(unsignedArray, length, bigEndian);
return *this;
}
CborWriter & addTypedArray(const double *arr, size_t length, bool bigEndian=false) {
addTag(bigEndian ? 82 : 86);
struct {
const double *arr;
uint64_t operator[](size_t i) const {
#ifdef CBOR_WALKER_USE_BIT_CAST
return std::bit_cast<uint64_t>(arr[i]);
#else
double v = arr[i];
uint64_t vi;
std::memcpy(&vi, &v, 8);
return vi;
#endif
}
} unsignedArray{arr};
writeTypedBlock<uint64_t>(unsignedArray, length, bigEndian);
return *this;
}
private:
void writeHead(unsigned char type, uint64_t argument) {
type <<= 5;
if (argument >= 4294967296ul) {
bytes.push_back(type|27);
for (size_t i = 0; i < 8; ++i) {
bytes.push_back(argument>>(56 - i*8));
}
} else if (argument >= 65536) {
bytes.push_back(type|26);
for (size_t i = 0; i < 4; ++i) {
bytes.push_back(argument>>(24 - i*8));
}
} else if (argument >= 256) {
bytes.push_back(type|25);
bytes.push_back(argument>>8);
bytes.push_back(argument);
} else if (argument >= 24) {
bytes.push_back(type|24);
bytes.push_back(argument);
} else {
bytes.push_back(type|argument);
}
}
template<typename UIntType, class Array>
void writeTypedBlock(Array &&array, size_t length, bool bigEndian) {
constexpr size_t B = sizeof(UIntType);
writeHead(2, length*B);
if (bigEndian) {
for (size_t i = 0; i < length; ++i) {
UIntType v = array[i];
for (size_t b = 0; b < B; ++b) bytes.push_back((v>>((B-1-b)*8))&0xFF);
}
} else {
for (size_t i = 0; i < length; ++i) {
UIntType v = array[i];
for (size_t b = 0; b < B; ++b) bytes.push_back((v>>(b*8))&0xFF);
}
}
}
std::vector<unsigned char> &bytes;
};
}} // namespace
#endif // include guard

View File

@ -1,136 +0,0 @@
#pragma once
#include "./storage.h"
#include "../stfx-library.h" // for the ...ParamIgnore classes
namespace stfx { namespace storage {
template<class SubClassCRTP>
struct STFXStorageScanner : public signalsmith::storage::StorageScanner<SubClassCRTP> {
void info(const char *, const char *) {}
int version(int v) {
return v;
}
template<class V>
bool changed(const char *key, V &v) {
auto &sub = *(SubClassCRTP *)this;
sub(key, v);
return false;
}
void invalidate(const char *) {}
bool extra() {
return false;
}
template<class V>
void extra(const char *, V) {}
template<class PR>
RangeParamIgnore range(const char *key, PR &param) {
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
return {};
}
};
template<class SubClassCRTP=void>
struct STFXStorageWriter : public signalsmith::storage::StorageCborWriter<SubClassCRTP> {
using signalsmith::storage::StorageCborWriter<SubClassCRTP>::StorageCborWriter;
void info(const char *name, const char *desc) {
sub().extra("name", name);
sub().extra("desc", desc);
}
template<class V>
bool changed(const char *key, V &v) {
sub()(key, v);
return false;
}
void invalidate(const char *) {}
int version(int v) {
sub()("version", v);
return v;
}
bool extra() {
return false;
}
template<class V>
void extra(const char *key, V v) {}
template<class PR>
RangeParamIgnore range(const char *key, PR &param) {
sub()(key, param);
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
sub()(key, param);
return {};
}
private:
SubClassCRTP & sub() {
return *(SubClassCRTP *)this;
}
};
template<class SubClassCRTP=void>
struct STFXStorageReader : public signalsmith::storage::StorageCborReader<SubClassCRTP> {
using signalsmith::storage::StorageCborReader<SubClassCRTP>::StorageCborReader;
// This is supplemental
void info(const char *, const char *) {}
int version(int v) {
sub()("version", v);
return v;
}
bool extra() {
return false;
}
template<class V>
void extra(const char *key, V v) {}
template<class V>
bool changed(const char *key, V &v) {
V prev = v;
sub()(key, v);
return v != prev;
}
void invalidate(const char *invalidatedKey) {
//LOG_EXPR(invalidatedKey);
}
template<class PR>
RangeParamIgnore range(const char *key, PR &param) {
sub()(key, param);
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
sub()(key, param);
return {};
}
private:
SubClassCRTP & sub() {
return *(SubClassCRTP *)this;
}
};
// If void, use itself for CRTP
template<>
struct STFXStorageWriter<void> : public STFXStorageWriter<STFXStorageWriter<void>> {
using STFXStorageWriter<STFXStorageWriter<void>>::STFXStorageWriter;
};
template<>
struct STFXStorageReader<void> : public STFXStorageReader<STFXStorageReader<void>> {
using STFXStorageReader<STFXStorageReader<void>>::STFXStorageReader;
};
}} // namespace

View File

@ -1,264 +0,0 @@
#pragma once
#include "./cbor-walker.h"
#include <string>
#include <vector>
namespace signalsmith { namespace storage {
struct StorageDummy {
template<class V>
void operator()(const char *, V &) {}
};
template<class SubClassCRTP>
struct StorageScanner {
template<class V>
void operator()(const char */*key*/, V &v) {
value(v);
}
private:
void value(int64_t &v) {};
void value(uint64_t &v) {};
void value(int32_t &v) {};
void value(uint32_t &v) {};
void value(int16_t &v) {};
void value(uint16_t &v) {};
void value(int8_t &v) {};
void value(uint8_t &v) {};
void value(float &v) {};
void value(double &v) {};
void value(bool &v) {};
void value(std::string &str) {};
template<class Item>
void value(std::vector<Item> &array) {
for (auto &item : array) {
value(item);
}
}
template<class V>
void value(V &v) {
v.state(*(SubClassCRTP *)this);
}
};
template<class SubClassCRTP=void>
struct StorageCborWriter {
StorageCborWriter(const signalsmith::cbor::CborWriter &writer, std::vector<unsigned char> *buffer=nullptr) : cbor(writer) {
if (buffer) buffer->resize(0);
cbor.openMap();
}
StorageCborWriter(std::vector<unsigned char> &cborBuffer) : StorageCborWriter(signalsmith::cbor::CborWriter(cborBuffer), &cborBuffer) {}
~StorageCborWriter() {
cbor.close();
}
template<class V>
void operator()(const char *key, V &value) {
cbor.addUtf8(key);
writeValue(value);
}
private:
signalsmith::cbor::CborWriter cbor;
#define STORAGE_BASIC_INT(V) \
void writeValue(V &value) { \
cbor.addInt(value); \
}
STORAGE_BASIC_INT(int64_t)
STORAGE_BASIC_INT(uint64_t)
STORAGE_BASIC_INT(int32_t)
STORAGE_BASIC_INT(uint32_t)
STORAGE_BASIC_INT(int16_t)
STORAGE_BASIC_INT(uint16_t)
STORAGE_BASIC_INT(int8_t)
STORAGE_BASIC_INT(uint8_t)
#undef STORAGE_BASIC_INT
void writeValue(float &value) {
cbor.addFloat(value);
}
void writeValue(double &value) {
cbor.addFloat(value);
}
void writeValue(bool &value) {
cbor.addBool(value);
}
// Support writing C strings (but not reading them), so inherited writers can add supplemental hints/info without allocating
void writeValue(const char *cStr) {
cbor.addUtf8(cStr);
}
void writeValue(std::string &str) {
writeValue(str.c_str());
}
template<class Item>
void writeValue(std::vector<Item> &array) {
cbor.openArray(array.size());
for (auto &item : array) {
writeValue(item);
}
}
#define STORAGE_TYPED_ARRAY(T) \
void writeValue(std::vector<T> &array) { \
cbor.addTypedArray(array.data(), array.size()); \
}
STORAGE_TYPED_ARRAY(uint8_t)
STORAGE_TYPED_ARRAY(int8_t)
STORAGE_TYPED_ARRAY(uint16_t)
STORAGE_TYPED_ARRAY(int16_t)
STORAGE_TYPED_ARRAY(uint32_t)
STORAGE_TYPED_ARRAY(int32_t)
STORAGE_TYPED_ARRAY(uint64_t)
STORAGE_TYPED_ARRAY(int64_t)
STORAGE_TYPED_ARRAY(float)
STORAGE_TYPED_ARRAY(double)
#undef STORAGE_TYPED_ARRAY
template<class Obj>
void writeValue(Obj &obj) {
cbor.openMap();
obj.state(*(SubClassCRTP *)this);
cbor.close();
}
};
template<class SubClassCRTP=void>
struct StorageCborReader {
using Cbor = signalsmith::cbor::TaggedCborWalker;
StorageCborReader(Cbor c) : cbor(c) {
if (cbor.isMap()) cbor = cbor.enter();
}
StorageCborReader(const std::vector<unsigned char> &v) : StorageCborReader(Cbor(v)) {}
template<class V>
void operator()(const char *key, V &v) {
if (filterKeyBytes != nullptr) {
if (!keyMatch(key, filterKeyBytes, filterKeyLength)) return;
}
if (!cbor.isUtf8()) return; // We expect a string key
// If we have a filter, we *should* be just in front of the appropriate key, but check anyway
if (!keyMatch(key, (const char *)cbor.bytes(), cbor.length())) {
return; // key doesn't match
}
cbor++;
readValue(v);
}
private:
Cbor cbor;
const char *filterKeyBytes = nullptr;
size_t filterKeyLength = 0;
template<class Obj>
void readValue(Obj &obj) {
if (!cbor.isMap()) return;
cbor = cbor.forEachPair([&](Cbor key, Cbor value){
if (!key.isUtf8()) return;
const char *fkb = filterKeyBytes;
size_t fkl = filterKeyLength;
// Temporarily set key, and scan the object for that property
filterKeyBytes = (const char *)key.bytes();
filterKeyLength = key.length();
cbor = key;
obj.state(*(SubClassCRTP *)this);
filterKeyBytes = fkb;
filterKeyLength = fkl;
});
}
#define STORAGE_BASIC_TYPE(V) \
void readValue(V &v) { \
v = V(cbor++); \
}
STORAGE_BASIC_TYPE(int64_t)
STORAGE_BASIC_TYPE(uint64_t)
STORAGE_BASIC_TYPE(int32_t)
STORAGE_BASIC_TYPE(uint32_t)
STORAGE_BASIC_TYPE(int16_t)
STORAGE_BASIC_TYPE(uint16_t)
STORAGE_BASIC_TYPE(int8_t)
STORAGE_BASIC_TYPE(uint8_t)
STORAGE_BASIC_TYPE(float)
STORAGE_BASIC_TYPE(double)
STORAGE_BASIC_TYPE(bool)
#undef STORAGE_BASIC_TYPE
void readValue(std::string &v) {
if (!cbor.isUtf8()) v.clear();
v.assign((const char *)cbor.bytes(), cbor.length());
++cbor;
}
template<class Item>
void readVector(std::vector<Item> &array) {
if (!cbor.isArray()) return;
size_t length = 0;
cbor = cbor.forEach([&](Cbor item, size_t index){
length = index + 1;
if (array.size() < length) array.resize(length);
cbor = item;
readValue(array[index]);
});
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 { \
readVector<T>(array); \
} \
}
STORAGE_TYPED_ARRAY(uint8_t)
STORAGE_TYPED_ARRAY(int8_t)
STORAGE_TYPED_ARRAY(uint16_t)
STORAGE_TYPED_ARRAY(int16_t)
STORAGE_TYPED_ARRAY(uint32_t)
STORAGE_TYPED_ARRAY(int32_t)
STORAGE_TYPED_ARRAY(uint64_t)
STORAGE_TYPED_ARRAY(int64_t)
STORAGE_TYPED_ARRAY(float)
STORAGE_TYPED_ARRAY(double)
#undef STORAGE_TYPED_ARRAY
static bool keyMatch(const char *key, const char *filterKeyBytes, size_t filterKeyLength) {
for (size_t i = 0; i < filterKeyLength; ++i) {
if (key[i] != filterKeyBytes[i]) return false;
}
return key[filterKeyLength] == 0;
}
};
// If void, use itself for CRTP
template<>
struct StorageCborWriter<void> : public StorageCborWriter<StorageCborWriter<void>> {
using StorageCborWriter<StorageCborWriter<void>>::StorageCborWriter;
};
template<>
struct StorageCborReader<void> : public StorageCborReader<StorageCborReader<void>> {
using StorageCborReader<StorageCborReader<void>>::StorageCborReader;
};
}} // namespace

File diff suppressed because one or more lines are too long

View File

@ -1,383 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Generic STFX UI</title>
<style>
:root {
font-size:12pt;
}
* {
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
width: 100vw;
max-width: 100vw;
height: 100vh;
min-height: 100vh;
max-height: 100vh;
overflow: hidden;
background: linear-gradient(#FFF, #EEE, #CCC);
color: #000;
text-align: center;
word-wrap: break-word;
font-family: Bahnschrift, 'DIN Alternate', 'Alte DIN 1451 Mittelschrift', 'D-DIN', 'OpenDin', 'Clear Sans', 'Barlow', 'Abel', 'Franklin Gothic Medium', system-ui, sans-serif;
/* no text selectable by default */
user-select: none;
}
output {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
font-weight: normal;
color: #000;
}
#header {
grid-area: header;
display: flex;
justify-content: center;
margin: 0.5rem;
font-weight: normal;
letter-spacing: 0.1ex;
transform: scale(0.95, 1);
}
#params {
flex-grow: 1;
display: flex;
align-content: space-around;
justify-content: space-evenly;
flex-wrap: wrap;
padding: 1rem;
gap: 1rem;
}
.param-range {
display: grid;
width: 3.5rem;
grid-template-areas: "dial" "name";
grid-template-rows: 3.5rem 1fr;
}
.param-range-value {
position: relative;
grid-area: dial;
width: 3.5rem;
height: 3.5rem;
}
.param-range-dial {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
.param-range-text {
position: absolute;
top: 30%;
left: 0;
width: 100%;
letter-spacing: -0.1ex;
}
.param-range-units {
position: absolute;
top: 60%;
left: 0;
width: 100%;
color: #666;
font-size: 0.85rem;
}
.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%;
}
/* Alternative layouts */
/* compact: more compact, */
:root.compact {
font-size: 9pt;
}
:root.compact #params {
padding: 0.5rem;
}
/* columns: horizontal layout, not vertical */
:root.columns body {
flex-direction: row;
}
:root.columns #params {
flex-direction: column;
}
:root.columns #header {
display: none;
}
</style>
</head>
<body>
<h1 id="header">{name}</h1>
<section id="params">
<template @foreach>
<label class="param-range" @if="${d => d.$type == 'ParamRange'}">
<div class="param-range-name">{name}</div>
<script>
let gestureUnit = Symbol(), scrollTimeout = Symbol();
function move(data, dx, dy, element) {
let prev = data.gesture ? data[gestureUnit] : data.rangeUnit;
data[gestureUnit] = data.rangeUnit = Math.min(1, Math.max(0, prev - dy/250));
}
function scroll(data, dx, dy, element) {
let prev = data.gesture ? data[gestureUnit] : data.rangeUnit;
clearTimeout(element[scrollTimeout]);
data.gesture = true;
element[scrollTimeout] = setTimeout(_ => data.gesture = false, 500);
data[gestureUnit] = data.rangeUnit = Math.min(1, Math.max(0, prev + dy/250));
}
function press(data, count, event, element) {
clearTimeout(element[scrollTimeout]);
data.gesture = true;
data[gestureUnit] = data.rangeUnit;
if (count == 2) {
data.value = data.defaultValue;
data.gesture = false;
}
}
function unpress(data) {
data.gesture = false;
}
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>
<div class="param-range-value" $move="${move}" $scroll="${scroll}" $press="${press}" $unpress="${unpress}">
<canvas class="param-range-dial" $update="${drawDial}"></canvas>
<output class="param-range-text">{text}</output><br>
<div class="param-range-units">{textUnits}</div>
</div>
</label>
</template>
</section>
<template @foreach>
<div class="plot" @if="${d => d.$type == 'Spectrum'}" $fullscreentoggle='foo'>
<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>
// Query keys set CSS class on the root
new URL(location).searchParams.forEach((value, key) => {
document.body.parentNode.classList.add(key);
});
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.fullscreentoggle = (element) => {
Matsui.global.attributes.press(element, count => {
if (count == 2) {
if (document.fullscreenElement == element) {
document.exitFullscreen();
} else {
element.requestFullscreen();
}
}
});
};
let state = Matsui.replace(document.body, {name: "..."});
state.trackMerges(merge => {
console.log(JSON.stringify(merge));
window.parent.postMessage(CBOR.encode(merge), '*');
});
let pendingMerge = null;
addEventListener('message', e => {
let merge = CBOR.decode(e.data);
if (pendingMerge !== null) {
Matsui.merge.apply(pendingMerge, merge);
} else {
pendingMerge = merge;
requestAnimationFrame(_ => {
pendingMerge = null;
state.merge(merge);
});
}
});
if (window.parent !== window) 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

@ -1,460 +0,0 @@
#pragma once
#include "../storage/cbor-walker.h"
#include "../storage/stfx-storage.h"
#include "../param-info.h"
#include <string>
#include <vector>
#include <functional>
#include <atomic>
#include <memory>
#include <iostream> // we log to stderr if our queue gets full
namespace stfx { namespace web {
struct ParamContext {
union {
void *pointer;
size_t index;
};
ParamContext() : index(0) {}
};
struct WebStateWriter : public storage::STFXStorageWriter<WebStateWriter> {
using Super = storage::STFXStorageWriter<WebStateWriter>;
using Super::Super;
// There should never be a version mismatch, ignore this
int version(int v) {return v;}
// Include extra info
bool extra() {
return true;
}
template<class V>
void extra(const char *key, V v) {
(*this)(key, v);
}
};
struct WebStateReader : public storage::STFXStorageReader<WebStateReader> {
using Super = storage::STFXStorageReader<WebStateReader>;
using Super::Super;
// There should never be a version mismatch, ignore this
int version(int v) {return v;}
// We're interested in an extended set of values being sent back
bool extra() {
return true;
}
// But anything read-only gets skipped
template<class V>
void extra(const char *key, V v) {}
void invalidate(const char *invalidatedKey) {
// TODO: right now this is hacked together, by the params re-sending themselves whenever they would otherwise invalidate.
// Instead, this would let us reply with only the keys which changed
}
};
template<class S>
constexpr bool storageIsWeb() {return false;}
template<>
constexpr bool storageIsWeb<WebStateWriter>() {return true;}
template<>
constexpr bool storageIsWeb<WebStateReader>() {return true;}
/* Given an STFX template which (when insantiated) has inheritance like:
EffectSTFX : Effect
This produces a template with inheritance like:
WebSTFX : EffectSTFX : WebBase : Effect
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>
struct WebUIHelper {
template<class Effect>
class WebBase : public Effect {
using SuperRange = typename Effect::ParamRange;
using SuperStepped = typename Effect::ParamStepped;
protected:
struct WebParamScanner : public storage::STFXStorageScanner<WebParamScanner>{
SubClass *effect;
std::vector<std::string> scope;
WebParamScanner(SubClass *effect) : effect(effect) {}
// Extra methods we add for STFX storage
void info(const char *, const char *) {}
int version(int v) {
return v;
}
template<class PR>
RangeParamInfo & range(const char *key, PR &param) {
param.effect = effect;
param.scope = scope;
param.scope.emplace_back(key);
param.info = std::unique_ptr<RangeParamInfo>{
new RangeParamInfo(param)
};
return *param.info;
}
template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) {
param.effect = effect;
param.scope = scope;
param.scope.emplace_back(key);
param.info = std::unique_ptr<SteppedParamInfo>{
new SteppedParamInfo(param)
};
return *param.info;
}
template<class V>
bool changed(const char *key, V &v) {
(*this)(key, v);
return false;
}
void invalidate(const char *) {}
};
public:
using Effect::Effect;
std::string webPage = "html/generic.html";
int webWidth = 640, webHeight = 480;
struct ParamRange : public SuperRange {
using SuperRange::SuperRange;
ParamContext context;
ParamRange & operator=(double v) {
bool changed = (v != *this);
SuperRange::operator=(v);
if (changed) sendUpdateMessage();
return *this;
}
template<class Storage>
void state(Storage &storage) {
double prevV = *this;
bool prevGesture = gesture;
SuperRange::state(storage);
constexpr bool isWeb = storageIsWeb<Storage>();
if (isWeb) { // a bunch of extra state
storage.extra("$type", "ParamRange");
storage.extra("name", info->name);
storage.extra("defaultValue", info->defaultValue);
std::string text, units;
info->toString((double)*this, text, units);
storage.extra("text", text);
storage.extra("textUnits", units);
storage("gesture", gesture);
double unit = info->toUnit(*this);
if (storage.changed("rangeUnit", unit)) {
SuperRange::operator=(info->fromUnit(unit));
storage.invalidate("value");
}
}
if (prevV != *this) {
storage.invalidate("rangeUnit");
storage.invalidate("text");
storage.invalidate("textUnits");
if (isWeb) {
if (effect->paramListenerRange) {
effect->paramListenerRange(context, *this);
}
sendUpdateMessage(); // TODO: shouldn't be necessary once we have `.invalidate()` working
} else {
sendUpdateMessage();
}
}
if (gesture != prevGesture) {
if (isWeb && effect->paramListenerGesture) {
effect->paramListenerGesture(context, gesture);
}
}
}
private:
friend struct WebParamScanner;
SubClass *effect = nullptr;
std::vector<std::string> scope;
std::unique_ptr<RangeParamInfo> info;
bool gesture = false;
void sendUpdateMessage() {
if (auto *m = effect->getEmptyMessage()) {
signalsmith::cbor::CborWriter cbor{m->bytes};
for (auto &key : scope) {
cbor.openMap(1);
cbor.addUtf8(key);
}
WebStateWriter storage{cbor};
state(storage);
m->markReady();
}
}
};
struct ParamStepped : public SuperStepped {
using SuperStepped::SuperStepped;
ParamContext context;
ParamStepped & operator=(int v) {
bool changed = (v != *this);
SuperStepped::operator=(v);
if (changed) sendUpdateMessage();
return *this;
}
template<class Storage>
void state(Storage &storage) {
int prevV = *this;
SuperStepped::state(storage);
constexpr bool isWeb = storageIsWeb<Storage>();
if (isWeb) {
storage.extra("$type", "ParamStepped");
storage.extra("low", info->low);
storage.extra("high", info->high);
storage.extra("name", info->name);
std::string text = info->toString((int)*this);
storage.extra("text", text);
}
if (prevV != *this) {
storage.invalidate("text");
if (isWeb) {
if (effect->paramListenerStepped) {
effect->paramListenerStepped(context, *this);
}
sendUpdateMessage(); // TODO: shouldn't be necessary once we have `.invalidate()` working
} else {
sendUpdateMessage();
}
}
}
private:
friend struct WebParamScanner;
SubClass *effect = nullptr;
std::vector<std::string> scope;
std::unique_ptr<SteppedParamInfo> info;
void sendUpdateMessage() {
if (auto *m = effect->getEmptyMessage()) {
signalsmith::cbor::CborWriter cbor{m->bytes};
for (auto &key : scope) {
cbor.openMap(1);
cbor.addUtf8(key);
}
WebStateWriter storage{cbor};
state(storage);
m->markReady();
}
}
};
template<class Storage>
void meterState(Storage &storage) {}
};
template<class Effect, class... 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>
WebUILibraryEffect(Args... args) : Super(args...) {
typename Super::WebParamScanner scanner{this};
this->state(scanner);
// The web-message queue starts in a reset state
requestEntireState();
};
// 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);
}
}
void webClosed() {
// stops new messages from being queued up until the reset message is sent
requestEntireState();
}
// 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