From c8f366d477896d1bc6af271536314813247a2322 Mon Sep 17 00:00:00 2001 From: Artur Gorbachev Date: Sun, 4 Oct 2026 00:58:31 +0200 Subject: [PATCH] Update changelog for version 1.3.6 and enhance resource handling in PluginEditor and PolyPitchDetector --- CHANGELOG.md | 13 ++++ Source/PluginEditor.cpp | 13 ++-- Source/PolyPitchDetector.h | 137 ++++++++++++------------------------- UI/vite.config.ts | 7 +- 4 files changed, 71 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba22ae..3ef5123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to ElectroBow are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.6] - 2026-10-04 + +### Changed + +- Finalized the realtime MVP review with the existing per-voice pitch stability, + contextual harmonic handling, physical-pluck trigger separation, polyphonic + continuity, and continuous input-dynamics behavior preserved. +- Made embedded UI asset URLs relative and hardened JUCE resource-provider path + handling across WebView2/WebKit URL forms, preventing valid bundled assets + from being reported as missing during page startup. +- Confirmed the existing implementation with the automated pitch, trigger, + dynamics, analyzer, voice-manager, and processor tests. + ## [1.3.5] - 2026-10-03 ### Added diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index e2a4680..1907d4f 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -55,10 +55,15 @@ ElectroBowAudioProcessorEditor::~ElectroBowAudioProcessorEditor() { std::optional ElectroBowAudioProcessorEditor::getResource(const juce::String& url) const { - const auto path = - url == "/" - ? juce::String("index.html") - : url.fromLastOccurrenceOf("/", false, false).upToFirstOccurrenceOf("?", false, false); + // Resource-provider URLs differ slightly between JUCE backends. Normalize + // the final path component so query strings/fragments and leading slashes + // cannot make an embedded asset look missing. + auto path = url.fromLastOccurrenceOf("/", false, false); + + if (path.isEmpty()) + path = "index.html"; + + path = path.upToFirstOccurrenceOf("?", false, false).upToFirstOccurrenceOf("#", false, false); if (path == "index.html") return makeResource("index_html", "text/html"); diff --git a/Source/PolyPitchDetector.h b/Source/PolyPitchDetector.h index 8b43ae5..9bfe9eb 100644 --- a/Source/PolyPitchDetector.h +++ b/Source/PolyPitchDetector.h @@ -8,27 +8,23 @@ #include #include -class PitchDetector -{ -public: +class PitchDetector { + public: static constexpr int kWindowSize = 4096; static constexpr int kHopSize = 1024; - PitchDetector() - { + PitchDetector() { reset(); } - ~PitchDetector() - { + ~PitchDetector() { destroyAubio(); } PitchDetector(const PitchDetector&) = delete; PitchDetector& operator=(const PitchDetector&) = delete; - void prepare(double newSampleRate) - { + void prepare(double newSampleRate) { sampleRate = std::max(1.0, newSampleRate); destroyAubio(); @@ -37,13 +33,11 @@ class PitchDetector reset(); } - void push(const float* samples, int numSamples) - { + void push(const float* samples, int numSamples) { if (samples == nullptr || numSamples <= 0) return; - for (int i = 0; i < numSamples; ++i) - { + for (int i = 0; i < numSamples; ++i) { inputBuffer[static_cast(writePosition)] = samples[i]; writePosition = (writePosition + 1) % kWindowSize; @@ -57,41 +51,34 @@ class PitchDetector // Analyse immediately on the first complete window, // then every kHopSize samples afterwards. - if (samplesSinceAnalysis >= kHopSize) - { + if (samplesSinceAnalysis >= kHopSize) { samplesSinceAnalysis = 0; analyseCurrentWindow(); } } } - float getFrequencyHz() const noexcept - { + float getFrequencyHz() const noexcept { return frequencyHz; } - float getConfidence() const noexcept - { + float getConfidence() const noexcept { return confidence; } - float getBestFrequencyHz() const noexcept - { + float getBestFrequencyHz() const noexcept { return frequencyHz; } - float getBestConfidence() const noexcept - { + float getBestConfidence() const noexcept { return confidence; } - int getMidiNote() const noexcept - { + int getMidiNote() const noexcept { return midiNote; } - void reset() noexcept - { + void reset() noexcept { inputBuffer.fill(0.0f); analysisBuffer.fill(0.0f); @@ -104,11 +91,9 @@ class PitchDetector midiNote = 0; } -private: - void createAubio() - { - aubioPitchYin = - new_aubio_pitchyin(static_cast(kWindowSize)); + private: + void createAubio() { + aubioPitchYin = new_aubio_pitchyin(static_cast(kWindowSize)); if (aubioPitchYin == nullptr) return; @@ -117,44 +102,34 @@ class PitchDetector // 0.15 was already used successfully in the previous version. aubio_pitchyin_set_tolerance(aubioPitchYin, 0.15f); - aubioInput = - new_fvec(static_cast(kWindowSize)); + aubioInput = new_fvec(static_cast(kWindowSize)); aubioOutput = new_fvec(1); - if (aubioInput == nullptr || aubioOutput == nullptr) - { + if (aubioInput == nullptr || aubioOutput == nullptr) { destroyAubio(); } } - void destroyAubio() noexcept - { - if (aubioOutput != nullptr) - { + void destroyAubio() noexcept { + if (aubioOutput != nullptr) { del_fvec(aubioOutput); aubioOutput = nullptr; } - if (aubioInput != nullptr) - { + if (aubioInput != nullptr) { del_fvec(aubioInput); aubioInput = nullptr; } - if (aubioPitchYin != nullptr) - { + if (aubioPitchYin != nullptr) { del_aubio_pitchyin(aubioPitchYin); aubioPitchYin = nullptr; } } - void analyseCurrentWindow() - { - if (aubioPitchYin == nullptr || - aubioInput == nullptr || - aubioOutput == nullptr) - { + void analyseCurrentWindow() { + if (aubioPitchYin == nullptr || aubioInput == nullptr || aubioOutput == nullptr) { clearDetection(); return; } @@ -170,10 +145,8 @@ class PitchDetector at writePosition. */ - for (int i = 0; i < kWindowSize; ++i) - { - const int sourceIndex = - (writePosition + i) % kWindowSize; + for (int i = 0; i < kWindowSize; ++i) { + const int sourceIndex = (writePosition + i) % kWindowSize; analysisBuffer[static_cast(i)] = inputBuffer[static_cast(sourceIndex)]; @@ -192,47 +165,31 @@ class PitchDetector for (float sample : analysisBuffer) energy += static_cast(sample) * sample; - const double rms = - std::sqrt(energy / static_cast(kWindowSize)); + const double rms = std::sqrt(energy / static_cast(kWindowSize)); - if (rms < 0.000001) - { + if (rms < 0.000001) { clearDetection(); return; } - for (int i = 0; i < kWindowSize; ++i) - { - fvec_set_sample( - aubioInput, - analysisBuffer[static_cast(i)], - static_cast(i)); + for (int i = 0; i < kWindowSize; ++i) { + fvec_set_sample(aubioInput, analysisBuffer[static_cast(i)], + static_cast(i)); } - aubio_pitchyin_do( - aubioPitchYin, - aubioInput, - aubioOutput); + aubio_pitchyin_do(aubioPitchYin, aubioInput, aubioOutput); - const float period = - fvec_get_sample(aubioOutput, 0); + const float period = fvec_get_sample(aubioOutput, 0); const float detectedConfidence = - std::clamp( - aubio_pitchyin_get_confidence(aubioPitchYin), - 0.0f, - 1.0f); - - if (period <= 0.0f || - !std::isfinite(period) || - detectedConfidence <= 0.0f) - { + std::clamp(aubio_pitchyin_get_confidence(aubioPitchYin), 0.0f, 1.0f); + + if (period <= 0.0f || !std::isfinite(period) || detectedConfidence <= 0.0f) { clearDetection(); return; } - const float detectedFrequency = - static_cast(sampleRate / period); + const float detectedFrequency = static_cast(sampleRate / period); /* This is deliberately continuous Hz. @@ -242,10 +199,8 @@ class PitchDetector must survive. */ - if (!std::isfinite(detectedFrequency) || - detectedFrequency < 25.0f || - detectedFrequency > 1200.0f) - { + if (!std::isfinite(detectedFrequency) || detectedFrequency < 25.0f || + detectedFrequency > 1200.0f) { clearDetection(); return; } @@ -259,17 +214,12 @@ class PitchDetector It must never be used as the synthesis frequency. */ - const float midi = - 69.0f + - 12.0f * - std::log2(detectedFrequency / 440.0f); + const float midi = 69.0f + 12.0f * std::log2(detectedFrequency / 440.0f); - midiNote = - static_cast(std::lround(midi)); + midiNote = static_cast(std::lround(midi)); } - void clearDetection() noexcept - { + void clearDetection() noexcept { frequencyHz = 0.0f; confidence = 0.0f; midiNote = 0; @@ -292,4 +242,3 @@ class PitchDetector fvec_t* aubioInput = nullptr; fvec_t* aubioOutput = nullptr; }; - diff --git a/UI/vite.config.ts b/UI/vite.config.ts index d053081..83e0948 100644 --- a/UI/vite.config.ts +++ b/UI/vite.config.ts @@ -3,6 +3,10 @@ import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ + // The bundle is served from JUCE's in-memory resource provider rather than + // a real site root. Relative asset URLs work consistently across WebView2, + // WebKit, and hosts that rewrite the resource-provider URL. + base: "./", plugins: [react(), tailwindcss()], build: { outDir: "dist", @@ -12,7 +16,8 @@ export default defineConfig({ rollupOptions: { output: { entryFileNames: "app.js", - assetFileNames: (asset) => (asset.name?.endsWith(".css") ? "app.css" : "[name][extname]"), + assetFileNames: (asset) => + asset.names?.some((name) => name.endsWith(".css")) ? "app.css" : "[name][extname]", }, }, },