Move STFX stuff (aside from stfx-library.h) into separate repo
This commit is contained in:
parent
16b2ba955c
commit
e8103a1013
@ -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"
|
||||
)
|
||||
@ -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/
|
||||
@ -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
|
||||
>
|
||||
>
|
||||
>
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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" *
|
||||
@ -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> ¶ms;
|
||||
Crc32 crc;
|
||||
|
||||
ParamScanner(std::vector<Param> ¶ms) : params(params) {}
|
||||
|
||||
template<class PR>
|
||||
RangeParamInfo & range(const char *key, PR ¶m) {
|
||||
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 = ¶m;
|
||||
return entry.rangeInfo.emplace(param);
|
||||
}
|
||||
template<class PS>
|
||||
SteppedParamInfo & stepped(const char *key, PS ¶m) {
|
||||
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 = ¶m;
|
||||
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 ¶m : 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 ¶m : 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 ¶m : 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 ¶m : 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;
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 ¶m) {
|
||||
return {};
|
||||
}
|
||||
template<class PS>
|
||||
SteppedParamIgnore stepped(const char *key, PS ¶m) {
|
||||
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 ¶m) {
|
||||
sub()(key, param);
|
||||
return {};
|
||||
}
|
||||
template<class PS>
|
||||
SteppedParamIgnore stepped(const char *key, PS ¶m) {
|
||||
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 ¶m) {
|
||||
sub()(key, param);
|
||||
return {};
|
||||
}
|
||||
template<class PS>
|
||||
SteppedParamIgnore stepped(const char *key, PS ¶m) {
|
||||
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
|
||||
@ -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
|
||||
1
stfx/ui/html/cbor.min.js
vendored
1
stfx/ui/html/cbor.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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>
|
||||
2
stfx/ui/html/matsui-bundle.min.js
vendored
2
stfx/ui/html/matsui-bundle.min.js
vendored
File diff suppressed because one or more lines are too long
460
stfx/ui/web-ui.h
460
stfx/ui/web-ui.h
@ -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 ¶m) {
|
||||
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 ¶m) {
|
||||
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
|
||||
Loading…
x
Reference in New Issue
Block a user