diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..42360bd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.24) + +add_library(signalsmith-basics INTERFACE) +target_include_directories(signalsmith-basics INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/modules +) + +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/modules/linear) +target_link_libraries(signalsmith-basics INTERFACE + signalsmith-linear +) diff --git a/analyser.h b/analyser.h index aedb097..47e7cee 100644 --- a/analyser.h +++ b/analyser.h @@ -4,6 +4,12 @@ Released under the Boost Software License (see LICENSE.txt) */ #include "stfx/stfx-library.h" +#include "dsp/curves.h" +#include "linear/stft.h" +#include "linear/linear.h" + +#include + namespace signalsmith { namespace basics { template @@ -15,22 +21,45 @@ using AnalyserDouble = stfx::LibraryEffect; template struct AnalyserSTFX : public BaseEffect { using typename BaseEffect::Sample; + using Complex = std::complex; using typename BaseEffect::ParamRange; using typename BaseEffect::ParamStepped; + + static constexpr Sample stftBlockMs = 30, stftIntervalMs = 5; + + ParamRange barkResolution = 10; template void state(Storage &storage) { storage.info("[Basics] Analyser", "A Bark-scale spectrum analyser"); storage.version(0); + + storage.range("barkResolution", barkResolution) + .info("res.", "in Bark scale") + .range(1, 10, 25) + .unit("", 0); + + if (storage.extra()) { + storage("spectrum", spectrum); + } } template void configureSTFX(Config &config) { - config.outputChannels = config.inputChannels; + sampleRate = config.sampleRate; + channels = config.outputChannels = config.inputChannels; config.auxInputs = config.auxOutputs = {}; + + stft.configure(channels, 0, stftBlockMs*0.001*sampleRate, stftIntervalMs*0.001*sampleRate); + subRate = sampleRate/stft.defaultInterval(); + bands = spectrum.resize(channels, barkResolution, sampleRate); + updateBands(); + tmp.resize(stft.defaultInterval()); } - void reset() {} + void reset() { + spectrum.reset(); + } template void processSTFX(Io &io, Config &config, Block &block) { @@ -42,7 +71,167 @@ struct AnalyserSTFX : public BaseEffect { } } + bool processedSpectrum = false; + size_t index = 0; + while (index < block.length) { + size_t remaining = stft.defaultInterval() - stft.samplesSinceAnalysis(); + size_t consume = std::min(remaining, block.length - index); + // copy input + for (size_t c = 0; c < config.inputChannels; ++c) { + auto &input = io.input[c]; + for (size_t i = 0; i < consume; ++i) { + tmp[i] = input[index + i]; + } + stft.writeInput(c, consume, tmp.data()); + } + stft.moveInput(consume); + if (remaining == consume) { + stft.analyse(); + stftStep(); + processedSpectrum = true; + } + index += consume; + } + + if (processedSpectrum && block.wantsMeters()) { + for (size_t c = 0; c < channels; ++c) { + size_t offset = c*bands; + auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands); + linear.wrap(spectrum.energy[c]) = state2.norm(); + } + } } + + template + void meterState(Storage &storage) { + storage("spectrum", spectrum); + } + +private: + signalsmith::linear::DynamicSTFT stft; + signalsmith::linear::Linear linear; + std::vector tmp; + double prevBarkResolution = -1; + + Sample sampleRate, subRate; + size_t channels = 0; + size_t bands = 0; + std::vector inputBin; + std::vector bandInputReal, bandInputImag; + std::vector state1Real, state1Imag; + std::vector state2Real, state2Imag; + std::vector twistReal, twistImag, inputGain; + + void updateBands() { + linear.reserve(bands); + + bandInputReal.resize(bands*channels); + bandInputImag.resize(bands*channels); + state1Real.resize(bands*channels); + state1Imag.resize(bands*channels); + linear.wrap(state1Real) = 0; + linear.wrap(state1Imag) = 0; + state2Real.resize(bands*channels); + state2Imag.resize(bands*channels); + linear.wrap(state2Real) = 0; + linear.wrap(state2Imag) = 0; + + inputBin.resize(0); + twistReal.resize(0); + twistImag.resize(0); + + for (size_t b = 0; b < bands; ++b) { + Sample hz = spectrum.hz[b]; + Sample bwHz = spectrum.bwHz[b]; + + inputBin.push_back(stft.freqToBin(hz/sampleRate)); + Sample freq = Sample(2*M_PI/subRate)*hz; + Sample bw = Sample(2*M_PI/subRate)*bwHz; + Complex twist = std::exp(Complex{-bw, freq}); + twistReal.push_back(twist.real()); + twistImag.push_back(twist.imag()); + } + inputGain.resize(bands); + linear.wrap(inputGain) = 1 - linear.wrap(twistReal, twistImag).abs(); + } + + void stftStep() { + if (barkResolution != prevBarkResolution) { + prevBarkResolution = barkResolution; + bands = spectrum.resize(channels, barkResolution, sampleRate); + updateBands(); + } + + // Load inputs from the spectrum + Sample scalingFactor = Sample(1)/stft.defaultInterval(); + for (size_t c = 0; c < channels; ++c) { + auto *spectrum = stft.spectrum(c); + auto *inputR = bandInputReal.data() + bands*c; + auto *inputI = bandInputImag.data() + bands*c; + for (size_t b = 0; b < bands; ++b) { + Sample index = std::min(inputBin[b], stft.bands() - Sample(1.001)); + size_t indexLow = std::floor(index); + Sample indexFrac = index - std::floor(index); + Complex v = spectrum[indexLow] + (spectrum[indexLow + 1] - spectrum[indexLow])*indexFrac; + v *= scalingFactor; + inputR[b] = v.real(); + inputI[b] = v.imag(); + } + } + + auto twist = linear.wrap(twistReal, twistImag); + auto gain = linear.wrap(inputGain); + for (size_t c = 0; c < channels; ++c) { + size_t offset = c*bands; + auto state1 = linear.wrap(state1Real.data() + offset, state1Imag.data() + offset, bands); + auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands); + auto input = linear.wrap(bandInputReal.data() + offset, bandInputImag.data() + offset, bands); + state1 = state1*twist + input*gain; + state2 = state2*twist + state1*gain; + } + } + + struct Spectrum { + bool bandsChanged = false; + std::vector hz, bwHz; + std::vector> energy; + + size_t resize(size_t channels, Sample barkResolution, Sample sampleRate) { + bandsChanged = true; + hz.resize(0); + bwHz.resize(0); + + auto barkScale = signalsmith::curves::Reciprocal::barkScale(); + Sample barkStep = 1/barkResolution, barkEnd = barkScale.inverse(sampleRate/2); + for (Sample bark = barkScale.inverse(0); bark < barkEnd; bark += barkStep) { + hz.push_back(barkScale(bark)); + bwHz.push_back(barkScale.dx(bark)*barkStep); + } + hz.push_back(sampleRate/2); + bwHz.push_back(barkScale.dx(barkEnd)*barkStep); + + energy.resize(channels); + for (auto &e : energy) e.resize(hz.size()); + + return hz.size(); + } + + void reset() { + for (auto &e : energy) { + for (auto &v : e) v = 0; + } + } + + template + void state(Storage &storage) { + if (bandsChanged) { + bandsChanged = false; + storage.extra("$type", "Spectrum"); + storage("hz", hz); + } + storage("energy", energy); + } + } spectrum; }; }} // namespace diff --git a/clap/CMakeLists.txt b/clap/CMakeLists.txt index b64f579..b7b73fe 100644 --- a/clap/CMakeLists.txt +++ b/clap/CMakeLists.txt @@ -1,6 +1,7 @@ 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) diff --git a/clap/Makefile b/clap/Makefile index 2df44f6..7259d1a 100644 --- a/clap/Makefile +++ b/clap/Makefile @@ -48,4 +48,5 @@ format: dev: release-$(PLUGIN)_clap /Applications/REAPER.app/Contents/MacOS/REAPER REAPER/$(PLUGIN)/$(PLUGIN).RPP -wclap: emscripten-$(PLUGIN)_wclap \ No newline at end of file +wclap: emscripten-$(PLUGIN)_wclap + ./wclap-tar.sh out/Release/$(PLUGIN).wclap out/ \ No newline at end of file diff --git a/clap/wclap-tar.sh b/clap/wclap-tar.sh new file mode 100755 index 0000000..09ecf3a --- /dev/null +++ b/clap/wclap-tar.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +name="$(basename $1)" + +outDir="${2:-..}" +pushd "${outDir}" +outDir=`pwd` +popd + +echo $name + +cd $1 +rm -f "${outDir}/${name}.tar.gz" +tar --exclude=".*" -vczf "${outDir}/${name}.tar.gz" * diff --git a/modules/linear b/modules/linear index 157b448..4c89993 160000 --- a/modules/linear +++ b/modules/linear @@ -1 +1 @@ -Subproject commit 157b448e390663e78d845ca9f8dad65140f686ad +Subproject commit 4c8999385c725e619723346bf8f415010fed9c4c diff --git a/stfx/stfx-library.h b/stfx/stfx-library.h index a87a797..4932ce6 100644 --- a/stfx/stfx-library.h +++ b/stfx/stfx-library.h @@ -164,7 +164,9 @@ namespace stfx { // per-block automation gradient rate double blockFade; // we might need some additional setup on the fiand/or after a directly - bool firstBlockAfterReset = false; + bool firstBlockAfterReset; + bool metersRequested; + bool &metersChecked; template bool paramsChanging(Param ¶m) const { @@ -178,7 +180,7 @@ namespace stfx { // Block length in samples int length; - Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset=false) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), length(length) {} + Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset, bool wantsMeters, bool &metersChecked) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), metersRequested(wantsMeters), metersChecked(metersChecked), length(length) {} // Not copyable, because that's probably a mistake Block(const Block &) = delete; Block & operator =(const Block&) = delete; @@ -278,8 +280,9 @@ namespace stfx { callback(0, length); } - constexpr bool wantsMeters() const { - return false; + bool wantsMeters() const { + metersChecked = true; + return metersRequested; } }; @@ -358,6 +361,22 @@ namespace stfx { template void presets(Presets &) {} + + // passes ownership of any meter values back to the audio thread + void wantsMeters(bool meters=true) { + metersReady.clear(); + if (meters) { + metersRequested.test_and_set(); + } else { + metersRequested.clear(); + } + } + // whether the meter values can be read + bool hasMeters() const { + return metersReady.test(); + } + protected: + std::atomic_flag metersRequested = ATOMIC_FLAG_INIT, metersReady = ATOMIC_FLAG_INIT; }; /// Creates an effect class from an effect template, with optional extra config. @@ -493,7 +512,7 @@ namespace stfx { process(buffers, buffers, blockLength); } - /// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`. + /// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.processSTFX(io, config, block)`. /// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc. template void process(Inputs &&inputs, Outputs &&outputs, int blockLength) { @@ -522,7 +541,8 @@ namespace stfx { Outputs output; }; Io io{inputs, outputs}; - Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset); + bool metersChecked = false; + Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset, this->metersRequested.test(), metersChecked); ((EffectClass *)this)->processSTFX(io, (const Config &)config, (const Block &)block); @@ -535,6 +555,12 @@ namespace stfx { justHadReset = false; for (auto param : params.rangeParams) param->_libEndBlock(); for (auto param : params.stepParams) param->_libEndBlock(); + + // Meters are filled - pass ownership of meter values to the main thread + if (this->metersRequested.test() && metersChecked) { + this->metersRequested.clear(); + this->metersReady.test_and_set(); + } } }; diff --git a/stfx/storage/storage.h b/stfx/storage/storage.h index f809bc2..8adc62a 100644 --- a/stfx/storage/storage.h +++ b/stfx/storage/storage.h @@ -204,7 +204,7 @@ private: } template - void readValue(std::vector &array) { + void readVector(std::vector &array) { if (!cbor.isArray()) return; size_t length = 0; cbor = cbor.forEach([&](Cbor item, size_t index){ @@ -217,13 +217,18 @@ private: array.resize(length); } + template + void readValue(std::vector &array) { + readVector(array); + } + #define STORAGE_TYPED_ARRAY(T) \ void readValue(std::vector &array) { \ if (cbor.isTypedArray()) { \ array.resize(cbor.typedArrayLength()); \ cbor.readTypedArray(array); \ } else { \ - readValue>(array); \ + readVector(array); \ } \ } STORAGE_TYPED_ARRAY(uint8_t) diff --git a/stfx/ui/html/generic.html b/stfx/ui/html/generic.html index 8d8ccff..0d448d9 100644 --- a/stfx/ui/html/generic.html +++ b/stfx/ui/html/generic.html @@ -7,15 +7,15 @@ box-sizing: border-box; } body { - display: grid; - grid-template-areas: "header" "params"; - grid-template-rows: min-content 1fr; + display: flex; + flex-direction: column; margin: 0; padding: 0; width: 100vw; max-width: 100vw; height: 100vh; + min-height: 100vh; max-height: 100vh; overflow: hidden; @@ -46,7 +46,7 @@ transform: scale(0.95, 1); } #params { - grid-area: params; + flex-grow: 1; display: flex; align-content: space-around; justify-content: space-evenly; @@ -96,6 +96,27 @@ .param-range-name { grid-area: name; } + + #plots { + display: flex; + flex-grow: 10; + + } + .plot { + display: block; + width: 100%; + flex-grow: 100; + + position: relative; + background: linear-gradient(#222, #000 2rem); + } + .plot canvas { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } @@ -128,55 +149,188 @@ + + diff --git a/stfx/ui/html/matsui-bundle.min.js b/stfx/ui/html/matsui-bundle.min.js index 634fe9a..99017d6 100644 --- a/stfx/ui/html/matsui-bundle.min.js +++ b/stfx/ui/html/matsui-bundle.min.js @@ -1,2 +1,2 @@ -(e=>{"undefined"==typeof self&&"object"==typeof module?module.exports=e():self.Matsui=e("function"==typeof Matsui?Matsui:self.Matsui)})(e=>{Object.hasOwn||(Object.hasOwn=(e,t)=>Object.prototype.hasOwnProperty.call(e,t));let t=[],d=e=>e&&"object"==typeof e,h=()=>document.createTextNode("");function n(e,t){for(;e.nextSibling&&e.nextSibling!=t;)e.nextSibling.remove()}function a(){let e=document.createDocumentFragment(),t=h(),r=h();return e.append(t,r),{t:e,o:(...e)=>{n(t,r),t.after(...e)}}}let c=Symbol();function f(t){if(d(t)){let e=t[c];for(;e&&e!=t;)e=(t=e)[c]}return t}let o=Symbol(),i=Symbol(),p=Symbol("no change"),s=Symbol("replace"),y={apply(r,n,o){return d(n)?d(r)?Array.isArray(n)?n:(n[s]&&(o?r[s]=!0:delete n[s]),Object.keys(n).forEach(e=>{var t=n[e];Object.hasOwn(r,e)?null!=t||o?r[e]=y.apply(r[e],t,o):delete r[e]:null==t&&!o||(r[e]=t)}),r):(o&&(n[s]=!0),n):n},make(n,o,e){if(!e||n!==o){if(!d(o))return o;if(!d(n))return o[s]=!0,o;if(Array.isArray(o))return o;let r={};return Object.keys(o).forEach(e=>{var t;Object.hasOwn(n,e)?void 0!==(t=y.make(n[e],o[e],!0))&&(r[e]=t):r[e]=o[e]}),Object.keys(n).forEach(e=>{Object.hasOwn(o,e)||(r[e]=null)}),e&&0==Object.keys(r).length?void 0:r}},tracked(e,t,r){if(!d(e))return e;let n=t,o=!1,i=p,l=null,a=()=>{var e;clearTimeout(l),i!==p&&(e=i,i=p,n(e))},u=(e,o)=>new Proxy(e,{get(e,t){var r=e[t];return t===c?e:d(r)?u(r,e=>o({[t]:e})):r},set(e,t,r,n){if(null==r)return delete n[t];if((r=f(r))===e[t])return!0;if(n=y.make(e[t],r),d(n))n[s]=!0;else if(r===e[t])return!0;return!!Reflect.set(e,t,r)&&(o({[t]:n}),!0)},deleteProperty(e,t){return!(t in e)||delete e[t]&&(o({[t]:null}),!0)}});return u(e,t=e=>{if(i!==p)i=y.apply(i,e,!0);else if(i=e,r)requestAnimationFrame(a),clearTimeout(l),l=setTimeout(a,0);else if(!o)for(;i!==p;)o=!0,a(),o=!1})},addHidden(e,n){return d(e)?new Proxy(e,{get(e,t){var r;return t===o?n:t===i||t===c?e:(e=e[t],r=d(n)&&t in n,y.addHidden(e,r?n[t]:p))},has(e,t){return t===o||t in e}}):e},getHidden(e,t){var r;return d(e)&&void 0!==(r=f(e[o]))?r===p?t:r:e},withoutHidden(e){return d(e)&&e[i]||e}},l=Symbol(),u=Symbol(),m=Symbol("accessed"),v=Symbol("list-keys"),w={tracked(e,n){if(!d(e))return n[m]=m,e;let o=Array.isArray(e);return new Proxy(e,{get(e,t){var r=e[t];return t===c||t===u?e:t===l?(n[m]=m,e):o&&"length"===t?(n[v]=v,r):("function"!=typeof r||r.prototype||(n[m]=m),t in n||(n[t]={}),w.tracked(r,n[t]))},ownKeys(e){return n[v]=v,Reflect.ownKeys(e)}})},pierce(e,t){return e&&e[t?u:l]||e},accessed:m},g=Symbol(),b=(i,r)=>{for(let e=0;et(r(e))]}Object.freeze(i);let t=!0,l=[],n=null,e=o=>{o=w.pierce(o);let r=y.withoutHidden(o),e=f(r);if(t||n!==e)n=d(e)?e:null,t=!1,i.forEach((e,t)=>{t=l[t]={},e(w.tracked(r,t))});else{let n=y.getHidden(o,p);i.forEach((e,t)=>{var r;!function e(t,r){if(r!==p){if(t[m])return 1;if(!d(r)||Array.isArray(r)||r[s])return 1;if(t[v]){for(var n in r)if(e(t[n]||{},r[n]))return 1}else for(var o in r)if(t[o]&&e(t[o],r[o]))return 1}}(l[t],n)||(r=l[t]={},e(w.tracked(o,r,t)))})}};return e[g]=i,e},E=/\$\{/g;function j(n,o){let i=0;for(var l,a=[];l=E.exec(n);){a.push(n.slice(i,l.index));let e=l.index+2,t=e+1,r;for(;te=>e;else{let r=t.split(".");1==r.length?(r=r[0],o[n]=()=>e=>e?.[r]):o[n]=()=>t=>(r.forEach(e=>{t&&"object"==typeof t&&(t=t[e])}),t)}else o[n]=e=>e[t]}return t=>{let e=o.map(e=>"function"==typeof e?e(t):e).filter(e=>""!=e);return 1==e.length?e[0]:e.some(e=>"function"==typeof e)?t=>e.map(e=>"function"==typeof e?e(t):e).join(""):e.join("")}}function S(e){if(/^template$/i.test(e.tagName))return 1;for(var t of e.attributes||[])if("@"==t.name[0])return 1}function _(e){return/^script$/i.test(e.tagName)}function A(e){return e.slice(1).toLowerCase().replace(/-+(.)/g,(e,t)=>t.toUpperCase())}let L=/(\{[a-z0-9_=\.-]+\}|\uF74A[0-9]+\uF74B)/giu,$=/((\$[a-z0-9_-]+)*)(\{([a-z0-9_=\.-]+)\}|\uF74A([0-9]+)\uF74B)/giu,T=Symbol();function k(e){let f=e.content||e,s=[],d={},p=!1,m=(Array.from(f.childNodes).forEach(t=>{if("TEMPLATE"==t.tagName){var r=t.getAttribute("name");if(r){p=!0;let e=e=>null;t.hasAttribute("$filter")&&(e=O(t.getAttribute("$filter"))),d[r]={i:k(t),l:t.getAttribute("@scoped"),u:t[T],p:e},t.remove()}}}),function r(t,a){if(3==t.nodeType){var u=a,o=t.nodeValue;let e,n=0;for(;e=$.exec(o);){let i=o.slice(n,e.index),l=(n=$.lastIndex,e[1].split("$").slice(1)),t=e[4],r=e[5],a=null;if(t){let e="="==t?[]:t.split(".");a=t=>(e.forEach(e=>{t&&"object"==typeof t&&(t=t[e])}),t)}s.push((n,e)=>{let o=a||e[r];if("function"==typeof o)if("template"===l[0])(n=n.extend()).add("template",o),o=e=>e;else if("scoped"===l[0]){n=n.extend();let t=o;n.add("scoped",x(e=>t(e,n))),o=e=>e}return{m:u,v:(e,t,r)=>{i&&e.before(i),r=((n,e,o)=>{let i=e.map(e=>{var t=n.named[e];if(t)return t;let r="Template not found: "+e;return console.error(r),e=>({node:document.createTextNode(r),updates:[]})});return function t(r){if(r>=e.length)return o;let n=i[r];return e=>n(t(r+1))}(0)(n.dynamic)})(n,l,r),e.before(r.node),"function"==typeof o?t.push(b(r.updates,o)):r.updates.forEach(e=>e(o))}}})}if(0e.nodeValue=t:e=>e.remove();s.push(e=>({m:u,v:r}))}}else if(1===t.nodeType){if(S(t)&&a.length){if("TEMPLATE"==t.tagName&&t.hasAttribute("name"))throw Error('