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