/* ============================================================================== This file is part of the JUCE framework examples. Copyright (c) Raw Material Software Limited The code included in this file is provided under the terms of the ISC license http://www.isc.org/downloads/software-support-policy/isc-license. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ============================================================================== */ /******************************************************************************* The block below describes the properties of this PIP. A PIP is a short snippet of code that can be read by the Projucer and used to generate a JUCE project. BEGIN_JUCE_PIP_METADATA name: WebViewPluginDemo version: 1.0.0 vendor: JUCE website: http://juce.com description: Filtering audio plugin using an HTML/JS user interface dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, juce_audio_processors, juce_dsp, juce_audio_utils, juce_core, juce_data_structures, juce_events, juce_graphics, juce_gui_basics, juce_gui_extra exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1, JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1 type: AudioProcessor mainClass: WebViewPluginAudioProcessorWrapper useLocalCopy: 1 END_JUCE_PIP_METADATA *******************************************************************************/ #pragma once #include "../Assets/DemoUtilities.h" namespace ID { #define PARAMETER_ID(str) static const ParameterID str { #str, 1 }; PARAMETER_ID (cutoffFreqHz) PARAMETER_ID (mute) PARAMETER_ID (filterType) #undef PARAMETER_ID } class CircularBuffer { public: CircularBuffer (int numChannels, int numSamples) : buffer (data, (size_t) numChannels, (size_t) numSamples) { } template void push (dsp::AudioBlock b) { jassert (b.getNumChannels() == buffer.getNumChannels()); const auto trimmed = b.getSubBlock ( b.getNumSamples() - std::min (b.getNumSamples(), buffer.getNumSamples())); const auto bufferLength = (int64) buffer.getNumSamples(); for (auto samplesRemaining = (int64) trimmed.getNumSamples(); samplesRemaining > 0;) { const auto writeOffset = writeIx % bufferLength; const auto numSamplesToWrite = std::min (samplesRemaining, bufferLength - writeOffset); auto destSubBlock = buffer.getSubBlock ((size_t) writeOffset, (size_t) numSamplesToWrite); const auto sourceSubBlock = trimmed.getSubBlock (trimmed.getNumSamples() - (size_t) samplesRemaining, (size_t) numSamplesToWrite); destSubBlock.copyFrom (sourceSubBlock); samplesRemaining -= numSamplesToWrite; writeIx += numSamplesToWrite; } } template void push (Span s) { auto* ptr = s.begin(); dsp::AudioBlock b (&ptr, 1, s.size()); push (b); } void read (int64 readIx, dsp::AudioBlock output) const { const auto numChannelsToUse = std::min (buffer.getNumChannels(), output.getNumChannels()); jassert (output.getNumChannels() == buffer.getNumChannels()); const auto bufferLength = (int64) buffer.getNumSamples(); for (auto outputOffset = (size_t) 0; outputOffset < output.getNumSamples();) { const auto inputOffset = (size_t) ((readIx + (int64) outputOffset) % bufferLength); const auto numSamplesToRead = std::min (output.getNumSamples() - outputOffset, (size_t) bufferLength - inputOffset); auto destSubBlock = output.getSubBlock (outputOffset, numSamplesToRead) .getSubsetChannelBlock (0, numChannelsToUse); destSubBlock.copyFrom (buffer.getSubBlock (inputOffset, numSamplesToRead) .getSubsetChannelBlock (0, numChannelsToUse)); outputOffset += numSamplesToRead; } } int64 getWriteIndex() const noexcept { return writeIx; } private: HeapBlock data; dsp::AudioBlock buffer; int64 writeIx = 0; }; class SpectralBars { public: static constexpr int getNumBars() noexcept { return analysisWindowWidth / 2; } template void push (Span data) { buffer.push (data); } void compute (Span output) { auto* ptr = output.begin(); auto result = dsp::AudioBlock (&ptr, 1, output.size()); result.clear(); auto analysisData = fftTmp.getSubBlock (0, analysisWindowWidth); for (int i = 0; i < numAnalysisWindows; ++i) { buffer.read (0 + i * analysisWindowOverlap, analysisData); fft.performFrequencyOnlyForwardTransform (fftTmp.getChannelPointer (0), true); result.add (analysisData); } result.multiplyBy (1.0f / numAnalysisWindows); } static inline constexpr int64 fftOrder = 5; static inline constexpr int64 analysisWindowWidth = 1 << fftOrder; static inline constexpr int numAnalysisWindows = 16; static inline constexpr int analysisWindowOverlap = analysisWindowWidth / 2; private: dsp::FFT fft { fftOrder }; HeapBlock fftTmpData; dsp::AudioBlock fftTmp { fftTmpData, 1, (size_t) (2 * fft.getSize()) }; CircularBuffer buffer { 1, (int) analysisWindowWidth + (numAnalysisWindows - 1) * analysisWindowOverlap }; }; //============================================================================== class WebViewPluginAudioProcessor : public AudioProcessor { public: //============================================================================== WebViewPluginAudioProcessor (AudioProcessorValueTreeState::ParameterLayout layout); //============================================================================== void prepareToPlay (double sampleRate, int samplesPerBlock) override; void releaseResources() override {} bool isBusesLayoutSupported (const BusesLayout& layouts) const override; void processBlock (AudioBuffer&, MidiBuffer&) override; using AudioProcessor::processBlock; //============================================================================== const String getName() const override { return JucePlugin_Name; } bool acceptsMidi() const override { return false; } bool producesMidi() const override { return false; } bool isMidiEffect() const override { return false; } double getTailLengthSeconds() const override { return 0.0; } //============================================================================== int getNumPrograms() override { return 1; } int getCurrentProgram() override { return 0; } void setCurrentProgram (int) override {} const String getProgramName (int) override { return {}; } void changeProgramName (int, const String&) override {} //============================================================================== void getStateInformation (MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; struct Parameters { public: explicit Parameters (AudioProcessorValueTreeState::ParameterLayout& layout) : cutoffFreqHz (addToLayout (layout, ID::cutoffFreqHz, "Cutoff", NormalisableRange { 200.0f, 14000.0f, 1.0f, 0.5f }, 11000.0f, AudioParameterFloatAttributes{}.withLabel ("Hz"))), mute (addToLayout (layout, ID::mute, "Mute", false)), filterType (addToLayout (layout, ID::filterType, "Filter type", StringArray { "Low-pass", "High-pass", "Band-pass" }, 0)) { } AudioParameterFloat& cutoffFreqHz; AudioParameterBool& mute; AudioParameterChoice& filterType; private: template static void add (AudioProcessorParameterGroup& group, std::unique_ptr param) { group.addChild (std::move (param)); } template static void add (AudioProcessorValueTreeState::ParameterLayout& group, std::unique_ptr param) { group.add (std::move (param)); } template static Param& addToLayout (Group& layout, Ts&&... ts) { auto param = std::make_unique (std::forward (ts)...); auto& ref = *param; add (layout, std::move (param)); return ref; } }; Parameters parameters; AudioProcessorValueTreeState state; std::vector spectrumData = [] { return std::vector (16, 0.0f); }(); SpinLock spectrumDataLock; SpectralBars spectralBars; dsp::LadderFilter filter; private: //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebViewPluginAudioProcessor) }; //============================================================================== WebViewPluginAudioProcessor::WebViewPluginAudioProcessor (AudioProcessorValueTreeState::ParameterLayout layout) : AudioProcessor (BusesProperties() #if ! JucePlugin_IsMidiEffect #if ! JucePlugin_IsSynth .withInput ("Input", juce::AudioChannelSet::stereo(), true) #endif .withOutput ("Output", juce::AudioChannelSet::stereo(), true) #endif ), parameters (layout), state (*this, nullptr, "STATE", std::move (layout)) { } //============================================================================== void WebViewPluginAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { const auto channels = std::max (getTotalNumInputChannels(), getTotalNumOutputChannels()); if (channels == 0) return; filter.prepare ({ sampleRate, (uint32_t) samplesPerBlock, (uint32_t) channels }); filter.reset(); } bool WebViewPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const { if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) return false; if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) return false; return true; } void WebViewPluginAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer&) { juce::ScopedNoDenormals noDenormals; const auto totalNumInputChannels = getTotalNumInputChannels(); const auto totalNumOutputChannels = getTotalNumOutputChannels(); for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) buffer.clear (i, 0, buffer.getNumSamples()); filter.setCutoffFrequencyHz (parameters.cutoffFreqHz.get()); const auto filterMode = [this] { switch (parameters.filterType.getIndex()) { case 0: return dsp::LadderFilter::Mode::LPF12; case 1: return dsp::LadderFilter::Mode::HPF12; default: return dsp::LadderFilter::Mode::BPF12; } }(); filter.setMode (filterMode); auto outBlock = dsp::AudioBlock { buffer }.getSubsetChannelBlock (0, (size_t) getTotalNumOutputChannels()); if (parameters.mute.get()) outBlock.clear(); filter.process (dsp::ProcessContextReplacing (outBlock)); spectralBars.push (Span { buffer.getReadPointer (0), (size_t) buffer.getNumSamples() }); { const SpinLock::ScopedTryLockType lock (spectrumDataLock); if (! lock.isLocked()) return; spectralBars.compute ({ spectrumData.data(), spectrumData.size() }); } } //============================================================================== void WebViewPluginAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { juce::ignoreUnused (destData); } void WebViewPluginAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { juce::ignoreUnused (data, sizeInBytes); } extern const String localDevServerAddress; std::optional getResource (const String& url); //============================================================================== struct SinglePageBrowser : WebBrowserComponent { using WebBrowserComponent::WebBrowserComponent; // Prevent page loads from navigating away from our single page web app bool pageAboutToLoad (const String& newURL) override; }; //============================================================================== class WebViewPluginAudioProcessorEditor : public AudioProcessorEditor, private Timer { public: explicit WebViewPluginAudioProcessorEditor (WebViewPluginAudioProcessor&); std::optional getResource (const String& url); //============================================================================== void paint (Graphics&) override; void resized() override; int getControlParameterIndex (Component&) override { return controlParameterIndexReceiver.getControlParameterIndex(); } void timerCallback() override { static constexpr size_t numFramesBuffered = 5; SpinLock::ScopedLockType lock { processorRef.spectrumDataLock }; Array frame; for (size_t i = 1; i < processorRef.spectrumData.size(); ++i) frame.add (processorRef.spectrumData[i]); spectrumDataFrames.push_back (std::move (frame)); while (spectrumDataFrames.size() > numFramesBuffered) spectrumDataFrames.pop_front(); static int64 callbackCounter = 0; if ( spectrumDataFrames.size() == numFramesBuffered && callbackCounter++ % (int64) numFramesBuffered) { webComponent.emitEventIfBrowserIsVisible ("spectrumData", var{}); } } private: WebViewPluginAudioProcessor& processorRef; WebSliderRelay cutoffSliderRelay { "cutoffSlider" }; WebToggleButtonRelay muteToggleRelay { "muteToggle" }; WebComboBoxRelay filterTypeComboRelay { "filterTypeCombo" }; WebControlParameterIndexReceiver controlParameterIndexReceiver; SinglePageBrowser webComponent { WebBrowserComponent::Options{} .withBackend (WebBrowserComponent::Options::Backend::webview2) .withWinWebView2Options (WebBrowserComponent::Options::WinWebView2{} .withUserDataFolder (File::getSpecialLocation (File::SpecialLocationType::tempDirectory))) .withNativeIntegrationEnabled() .withOptionsFrom (cutoffSliderRelay) .withOptionsFrom (muteToggleRelay) .withOptionsFrom (filterTypeComboRelay) .withOptionsFrom (controlParameterIndexReceiver) .withNativeFunction ("sayHello", [](auto& var, auto complete) { complete ("Hello " + var[0].toString()); }) .withResourceProvider ([this] (const auto& url) { return getResource (url); }, URL { localDevServerAddress }.getOrigin()) }; WebSliderParameterAttachment cutoffAttachment; WebToggleButtonParameterAttachment muteAttachment; WebComboBoxParameterAttachment filterTypeAttachment; std::deque> spectrumDataFrames; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WebViewPluginAudioProcessorEditor) }; static ZipFile* getZipFile() { static auto stream = createAssetInputStream ("webviewplugin-gui_1.0.0.zip", AssertAssetExists::no); if (stream == nullptr) return nullptr; static ZipFile f { stream.get(), false }; return &f; } static const char* getMimeForExtension (const String& extension) { static const std::unordered_map mimeMap = { { { "htm" }, "text/html" }, { { "html" }, "text/html" }, { { "txt" }, "text/plain" }, { { "jpg" }, "image/jpeg" }, { { "jpeg" }, "image/jpeg" }, { { "svg" }, "image/svg+xml" }, { { "ico" }, "image/vnd.microsoft.icon" }, { { "json" }, "application/json" }, { { "png" }, "image/png" }, { { "css" }, "text/css" }, { { "map" }, "application/json" }, { { "js" }, "text/javascript" }, { { "woff2" }, "font/woff2" } }; if (const auto it = mimeMap.find (extension.toLowerCase()); it != mimeMap.end()) return it->second; jassertfalse; return ""; } static String getExtension (String filename) { return filename.fromLastOccurrenceOf (".", false, false); } static auto streamToVector (InputStream& stream) { std::vector result ((size_t) stream.getTotalLength()); stream.setPosition (0); [[maybe_unused]] const auto bytesRead = stream.read (result.data(), result.size()); jassert (bytesRead == (ssize_t) result.size()); return result; } std::optional WebViewPluginAudioProcessorEditor::getResource (const String& url) { const auto urlToRetrive = url == "/" ? String { "index.html" } : url.fromFirstOccurrenceOf ("/", false, false); if (auto* archive = getZipFile()) { if (auto* entry = archive->getEntry (urlToRetrive)) { auto stream = rawToUniquePtr (archive->createStreamForEntry (*entry)); auto v = streamToVector (*stream); auto mime = getMimeForExtension (getExtension (entry->filename).toLowerCase()); return WebBrowserComponent::Resource { std::move (v), std::move (mime) }; } } if (urlToRetrive == "index.html") { auto fallbackIndexHtml = createAssetInputStream ("webviewplugin-gui-fallback.html"); return WebBrowserComponent::Resource { streamToVector (*fallbackIndexHtml), String { "text/html" } }; } if (urlToRetrive == "data.txt") { WebBrowserComponent::Resource resource; static constexpr char testData[] = "testdata"; MemoryInputStream stream { testData, numElementsInArray (testData) - 1, false }; return WebBrowserComponent::Resource { streamToVector (stream), String { "text/html" } }; } if (urlToRetrive == "spectrumData.json") { Array frames; for (const auto& frame : spectrumDataFrames) frames.add (frame); DynamicObject::Ptr d (new DynamicObject()); d->setProperty ("timeResolutionMs", getTimerInterval()); d->setProperty ("frames", std::move (frames)); const auto s = JSON::toString (d.get()); MemoryInputStream stream { s.getCharPointer(), s.getNumBytesAsUTF8(), false }; return WebBrowserComponent::Resource { streamToVector (stream), String { "application/json" } }; } return std::nullopt; } #if JUCE_ANDROID // The localhost is available on this address to the emulator const String localDevServerAddress = "http://10.0.2.2:3000/"; #else const String localDevServerAddress = "http://localhost:3000/"; #endif bool SinglePageBrowser::pageAboutToLoad (const String& newURL) { return newURL == localDevServerAddress || newURL == getResourceProviderRoot(); } //============================================================================== WebViewPluginAudioProcessorEditor::WebViewPluginAudioProcessorEditor (WebViewPluginAudioProcessor& p) : AudioProcessorEditor (&p), processorRef (p), cutoffAttachment (*processorRef.state.getParameter (ID::cutoffFreqHz.getParamID()), cutoffSliderRelay, processorRef.state.undoManager), muteAttachment (*processorRef.state.getParameter (ID::mute.getParamID()), muteToggleRelay, processorRef.state.undoManager), filterTypeAttachment (*processorRef.state.getParameter (ID::filterType.getParamID()), filterTypeComboRelay, processorRef.state.undoManager) { addAndMakeVisible (webComponent); // webComponent.goToURL (localDevServerAddress); webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot()); setSize (500, 500); startTimerHz (20); } //============================================================================== void WebViewPluginAudioProcessorEditor::paint (Graphics& g) { // (Our component is opaque, so we must completely fill the background with a solid colour) g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); } void WebViewPluginAudioProcessorEditor::resized() { webComponent.setBounds (getLocalBounds()); } class WebViewPluginAudioProcessorWrapper : public WebViewPluginAudioProcessor { public: WebViewPluginAudioProcessorWrapper() : WebViewPluginAudioProcessor ({}) {} bool hasEditor() const override { return true; } AudioProcessorEditor* createEditor() override { return new WebViewPluginAudioProcessorEditor (*this); } };