mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
WebViewPluginDemo: Add demo of WebBrowserComponent with a React GUI
This commit is contained in:
parent
ee0aac2c75
commit
bc6295d7b5
12 changed files with 2147 additions and 32 deletions
|
|
@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept
|
|||
#endif
|
||||
}
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath)
|
||||
enum class AssertAssetExists
|
||||
{
|
||||
no,
|
||||
yes
|
||||
};
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath,
|
||||
[[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes)
|
||||
{
|
||||
#if JUCE_ANDROID
|
||||
ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile));
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath))));
|
||||
const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath));
|
||||
|
||||
if (fileIndex == -1)
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (fileIndex));
|
||||
#else
|
||||
#if JUCE_IOS
|
||||
auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
|
||||
|
|
@ -106,7 +121,12 @@ inline std::unique_ptr<InputStream> createAssetInputStream (const char* resource
|
|||
#endif
|
||||
|
||||
auto resourceFile = assetsDir.getChildFile (resourcePath);
|
||||
jassert (resourceFile.existsAsFile());
|
||||
|
||||
if (! resourceFile.existsAsFile())
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return resourceFile.createInputStream();
|
||||
#endif
|
||||
|
|
|
|||
39
examples/Assets/webviewplugin-gui-fallback.html
Normal file
39
examples/Assets/webviewplugin-gui-fallback.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WebViewPluginDemo</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebViewPluginDemo</h1>
|
||||
<p>
|
||||
This document is a placeholder for the GUI component of the
|
||||
WebViewPluginDemo.
|
||||
</p>
|
||||
<p>
|
||||
To build the fully fledged user interface you need to install
|
||||
<a href="https://nodejs.org">node.js</a>
|
||||
</p>
|
||||
<p>
|
||||
Then navigate into the
|
||||
<code>examples/GUI/WebViewPluginDemoGUI</code> directory inside your JUCE
|
||||
directory, and issue the following commands.
|
||||
</p>
|
||||
<pre>
|
||||
npm install
|
||||
npm run build
|
||||
npm run zip
|
||||
</pre
|
||||
>
|
||||
<p>
|
||||
This will build the full GUI package and place it in the
|
||||
<code>Assets</code> directory.
|
||||
</p>
|
||||
<p>After this, rebuild and restart this demo.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -51,6 +51,30 @@ function(_juce_add_pips)
|
|||
list(REMOVE_ITEM headers "${CMAKE_CURRENT_SOURCE_DIR}/ReaperEmbeddedViewPluginDemo.h")
|
||||
endif()
|
||||
|
||||
if((CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/WebViewPluginDemo.h")
|
||||
|
||||
if(NOT ("${JUCE_CMAKE_UTILS_DIR}" IN_LIST CMAKE_MODULE_PATH))
|
||||
list(APPEND CMAKE_MODULE_PATH "${JUCE_CMAKE_UTILS_DIR}")
|
||||
endif()
|
||||
|
||||
find_package(WebView2 QUIET)
|
||||
|
||||
if(NOT WebView2_FOUND)
|
||||
list(REMOVE_ITEM headers "${CMAKE_CURRENT_SOURCE_DIR}/WebViewPluginDemo.h")
|
||||
|
||||
message(WARNING "The WebViewPluginDemo was not enabled because WebView2 wasn't found"
|
||||
" in the the local NuGet folder"
|
||||
"\n"
|
||||
"To install NuGet and the WebView2 package containing the statically linked library, "
|
||||
"open a PowerShell and issue the following commands"
|
||||
"\n"
|
||||
"> Register-PackageSource -provider NuGet -name nugetRepository -location https://www.nuget.org/api/v2\n"
|
||||
"> Install-Package NuGet.CommandLine -Scope CurrentUser\n"
|
||||
"> Install-Package Microsoft.Web.WebView2 -Scope CurrentUser -RequiredVersion 1.0.1901.177\n")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
foreach(header IN ITEMS ${headers})
|
||||
juce_add_pip(${header} added_target)
|
||||
target_link_libraries(${added_target} PUBLIC
|
||||
|
|
|
|||
|
|
@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept
|
|||
#endif
|
||||
}
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath)
|
||||
enum class AssertAssetExists
|
||||
{
|
||||
no,
|
||||
yes
|
||||
};
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath,
|
||||
[[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes)
|
||||
{
|
||||
#if JUCE_ANDROID
|
||||
ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile));
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath))));
|
||||
const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath));
|
||||
|
||||
if (fileIndex == -1)
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (fileIndex));
|
||||
#else
|
||||
#if JUCE_IOS
|
||||
auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
|
||||
|
|
@ -106,7 +121,12 @@ inline std::unique_ptr<InputStream> createAssetInputStream (const char* resource
|
|||
#endif
|
||||
|
||||
auto resourceFile = assetsDir.getChildFile (resourcePath);
|
||||
jassert (resourceFile.existsAsFile());
|
||||
|
||||
if (! resourceFile.existsAsFile())
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return resourceFile.createInputStream();
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WebViewPluginDemo</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebViewPluginDemo</h1>
|
||||
<p>
|
||||
This document is a placeholder for the GUI component of the
|
||||
WebViewPluginDemo.
|
||||
</p>
|
||||
<p>
|
||||
To build the fully fledged user interface you need to install
|
||||
<a href="https://nodejs.org">node.js</a>
|
||||
</p>
|
||||
<p>
|
||||
Then navigate into the
|
||||
<code>examples/GUI/WebViewPluginDemoGUI</code> directory inside your JUCE
|
||||
directory, and issue the following commands.
|
||||
</p>
|
||||
<pre>
|
||||
npm install
|
||||
npm run build
|
||||
npm run zip
|
||||
</pre
|
||||
>
|
||||
<p>
|
||||
This will build the full GUI package and place it in the
|
||||
<code>Assets</code> directory.
|
||||
</p>
|
||||
<p>After this, rebuild and restart this demo.</p>
|
||||
</body>
|
||||
</html>
|
||||
624
examples/GUI/WebViewPluginDemo.h
Normal file
624
examples/GUI/WebViewPluginDemo.h
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
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 <typename T>
|
||||
void push (dsp::AudioBlock<T> 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 <typename T>
|
||||
void push (Span<T> s)
|
||||
{
|
||||
auto* ptr = s.begin();
|
||||
dsp::AudioBlock<T> b (&ptr, 1, s.size());
|
||||
push (b);
|
||||
}
|
||||
|
||||
void read (int64 readIx, dsp::AudioBlock<float> 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<char> data;
|
||||
dsp::AudioBlock<float> buffer;
|
||||
int64 writeIx = 0;
|
||||
};
|
||||
|
||||
class SpectralBars
|
||||
{
|
||||
public:
|
||||
static constexpr int getNumBars() noexcept
|
||||
{
|
||||
return analysisWindowWidth / 2;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void push (Span<T> data)
|
||||
{
|
||||
buffer.push (data);
|
||||
}
|
||||
|
||||
void compute (Span<float> output)
|
||||
{
|
||||
auto* ptr = output.begin();
|
||||
auto result = dsp::AudioBlock<float> (&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<char> fftTmpData;
|
||||
dsp::AudioBlock<float> 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<float>&, 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<AudioParameterFloat> (layout,
|
||||
ID::cutoffFreqHz,
|
||||
"Cutoff",
|
||||
NormalisableRange<float> { 200.0f, 14000.0f, 1.0f, 0.5f },
|
||||
11000.0f,
|
||||
AudioParameterFloatAttributes{}.withLabel ("Hz"))),
|
||||
mute (addToLayout<AudioParameterBool> (layout, ID::mute, "Mute", false)),
|
||||
filterType (addToLayout<AudioParameterChoice> (layout,
|
||||
ID::filterType,
|
||||
"Filter type",
|
||||
StringArray { "Low-pass", "High-pass", "Band-pass" },
|
||||
0))
|
||||
{
|
||||
}
|
||||
|
||||
AudioParameterFloat& cutoffFreqHz;
|
||||
AudioParameterBool& mute;
|
||||
AudioParameterChoice& filterType;
|
||||
|
||||
private:
|
||||
template <typename Param>
|
||||
static void add (AudioProcessorParameterGroup& group, std::unique_ptr<Param> param)
|
||||
{
|
||||
group.addChild (std::move (param));
|
||||
}
|
||||
|
||||
template <typename Param>
|
||||
static void add (AudioProcessorValueTreeState::ParameterLayout& group, std::unique_ptr<Param> param)
|
||||
{
|
||||
group.add (std::move (param));
|
||||
}
|
||||
|
||||
template <typename Param, typename Group, typename... Ts>
|
||||
static Param& addToLayout (Group& layout, Ts&&... ts)
|
||||
{
|
||||
auto param = std::make_unique<Param> (std::forward<Ts> (ts)...);
|
||||
auto& ref = *param;
|
||||
add (layout, std::move (param));
|
||||
return ref;
|
||||
}
|
||||
};
|
||||
|
||||
Parameters parameters;
|
||||
AudioProcessorValueTreeState state;
|
||||
|
||||
std::vector<float> spectrumData = [] { return std::vector<float> (16, 0.0f); }();
|
||||
SpinLock spectrumDataLock;
|
||||
|
||||
SpectralBars spectralBars;
|
||||
|
||||
dsp::LadderFilter<float> 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<float>& 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<float>::Mode::LPF12;
|
||||
|
||||
case 1:
|
||||
return dsp::LadderFilter<float>::Mode::HPF12;
|
||||
|
||||
default:
|
||||
return dsp::LadderFilter<float>::Mode::BPF12;
|
||||
}
|
||||
}();
|
||||
|
||||
filter.setMode (filterMode);
|
||||
|
||||
auto outBlock = dsp::AudioBlock<float> { buffer }.getSubsetChannelBlock (0, (size_t) getTotalNumOutputChannels());
|
||||
|
||||
if (parameters.mute.get())
|
||||
outBlock.clear();
|
||||
|
||||
filter.process (dsp::ProcessContextReplacing<float> (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<WebBrowserComponent::Resource> 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<WebBrowserComponent::Resource> getResource (const String& url);
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
static constexpr size_t numFramesBuffered = 5;
|
||||
|
||||
SpinLock::ScopedLockType lock { processorRef.spectrumDataLock };
|
||||
|
||||
Array<var> 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 { webComponent, "cutoffSlider" };
|
||||
WebToggleButtonRelay muteToggleRelay { webComponent, "muteToggle" };
|
||||
WebComboBoxRelay filterTypeComboRelay { webComponent, "filterTypeCombo" };
|
||||
|
||||
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)
|
||||
.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<Array<var>> 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<String, const char*> 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<std::byte> 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<WebBrowserComponent::Resource> 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<var> 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); }
|
||||
};
|
||||
903
examples/GUI/WebViewPluginDemoGUI/package-lock.json
generated
903
examples/GUI/WebViewPluginDemoGUI/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ladder-filter-gui",
|
||||
"version": "0.1.0",
|
||||
"name": "webviewplugin-gui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"juce-framework-frontend": "file:../../../modules/juce_gui_extra/native/javascript",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
|
@ -20,7 +21,8 @@
|
|||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"zip": "npm-build-zip --destination=../../Assets"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
@ -42,6 +44,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-plugin-react": "^7.32.2"
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"npm-build-zip": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,416 @@
|
|||
import "@fontsource/roboto/300.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
|
||||
import Box from "@mui/material/Container";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Container from "@mui/material/Container";
|
||||
import Slider from "@mui/material/Slider";
|
||||
import Button from "@mui/material/Button";
|
||||
import CardActions from "@mui/material/CardActions";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Select from "@mui/material/Select";
|
||||
import FormGroup from "@mui/material/FormGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
|
||||
import { React, useState, useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as Juce from "juce-framework-frontend";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
import { React } from "react";
|
||||
function JuceSlider({ identifier, title }) {
|
||||
JuceSlider.propTypes = {
|
||||
identifier: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
const sliderState = Juce.getSliderState(identifier);
|
||||
|
||||
const [value, setValue] = useState(sliderState.getNormalisedValue());
|
||||
const [properties, setProperties] = useState(sliderState.properties);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
sliderState.setNormalisedValue(newValue);
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const mouseDown = () => {
|
||||
sliderState.sliderDragStarted();
|
||||
};
|
||||
|
||||
const changeCommitted = (event, newValue) => {
|
||||
sliderState.setNormalisedValue(newValue);
|
||||
sliderState.sliderDragEnded();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const valueListenerId = sliderState.valueChangedEvent.addListener(() => {
|
||||
setValue(sliderState.getNormalisedValue());
|
||||
});
|
||||
const propertiesListenerId = sliderState.propertiesChangedEvent.addListener(
|
||||
() => setProperties(sliderState.properties)
|
||||
);
|
||||
|
||||
return function cleanup() {
|
||||
sliderState.valueChangedEvent.removeListener(valueListenerId);
|
||||
sliderState.propertiesChangedEvent.removeListener(propertiesListenerId);
|
||||
};
|
||||
});
|
||||
|
||||
function calculateValue() {
|
||||
return sliderState.getScaledValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography sx={{ mt: 1.5 }}>
|
||||
{properties.name}: {sliderState.getScaledValue()} {properties.label}
|
||||
</Typography>
|
||||
<Slider
|
||||
aria-label={title}
|
||||
value={value}
|
||||
scale={calculateValue}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={1 / (properties.numSteps - 1)}
|
||||
onChangeCommitted={changeCommitted}
|
||||
onMouseDown={mouseDown}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function JuceCheckbox({ identifier }) {
|
||||
JuceCheckbox.propTypes = {
|
||||
identifier: PropTypes.string,
|
||||
};
|
||||
|
||||
const checkboxState = Juce.getToggleState(identifier);
|
||||
|
||||
const [value, setValue] = useState(checkboxState.getValue());
|
||||
const [properties, setProperties] = useState(checkboxState.properties);
|
||||
|
||||
const handleChange = (event) => {
|
||||
checkboxState.setValue(event.target.checked);
|
||||
setValue(event.target.checked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const valueListenerId = checkboxState.valueChangedEvent.addListener(() => {
|
||||
setValue(checkboxState.getValue());
|
||||
});
|
||||
const propertiesListenerId =
|
||||
checkboxState.propertiesChangedEvent.addListener(() =>
|
||||
setProperties(checkboxState.properties)
|
||||
);
|
||||
|
||||
return function cleanup() {
|
||||
checkboxState.valueChangedEvent.removeListener(valueListenerId);
|
||||
checkboxState.propertiesChangedEvent.removeListener(propertiesListenerId);
|
||||
};
|
||||
});
|
||||
|
||||
const cb = <Checkbox checked={value} onChange={handleChange} />;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormGroup>
|
||||
<FormControlLabel control={cb} label={properties.name} />
|
||||
</FormGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function JuceComboBox({ identifier }) {
|
||||
JuceComboBox.propTypes = {
|
||||
identifier: PropTypes.string,
|
||||
};
|
||||
|
||||
const comboBoxState = Juce.getComboBoxState(identifier);
|
||||
|
||||
const [value, setValue] = useState(comboBoxState.getChoiceIndex());
|
||||
const [properties, setProperties] = useState(comboBoxState.properties);
|
||||
|
||||
const handleChange = (event) => {
|
||||
comboBoxState.setChoiceIndex(event.target.value);
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const valueListenerId = comboBoxState.valueChangedEvent.addListener(() => {
|
||||
setValue(comboBoxState.getChoiceIndex());
|
||||
});
|
||||
const propertiesListenerId =
|
||||
comboBoxState.propertiesChangedEvent.addListener(() => {
|
||||
setProperties(comboBoxState.properties);
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
comboBoxState.valueChangedEvent.removeListener(valueListenerId);
|
||||
comboBoxState.propertiesChangedEvent.removeListener(propertiesListenerId);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id={identifier}>{properties.name}</InputLabel>
|
||||
<Select
|
||||
labelId={identifier}
|
||||
value={value}
|
||||
label={properties.name}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{properties.choices.map((choice, i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{choice}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const sayHello = Juce.getNativeFunction("sayHello");
|
||||
|
||||
const SpectrumDataReceiver_eventId = "spectrumData";
|
||||
|
||||
function interpolate(a, b, s) {
|
||||
let result = new Array(a.length).fill(0);
|
||||
|
||||
for (const [i, val] of a.entries()) result[i] += (1 - s) * val;
|
||||
|
||||
for (const [i, val] of b.entries()) result[i] += s * val;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mod(dividend, divisor) {
|
||||
const quotient = Math.floor(dividend / divisor);
|
||||
return dividend - divisor * quotient;
|
||||
}
|
||||
|
||||
class SpectrumDataReceiver {
|
||||
constructor(bufferLength) {
|
||||
this.bufferLength = bufferLength;
|
||||
this.buffer = new Array(this.bufferLength);
|
||||
this.readIndex = 0;
|
||||
this.writeIndex = 0;
|
||||
this.lastTimeStampMs = 0;
|
||||
this.timeResolutionMs = 0;
|
||||
|
||||
let self = this;
|
||||
this.spectrumDataRegistrationId = window.__JUCE__.backend.addEventListener(
|
||||
SpectrumDataReceiver_eventId,
|
||||
() => {
|
||||
fetch(Juce.getBackendResourceAddress("spectrumData.json"))
|
||||
.then((response) => response.text())
|
||||
.then((text) => {
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (self.timeResolutionMs == 0) {
|
||||
self.timeResolutionMs = data.timeResolutionMs;
|
||||
|
||||
// We want to stay behind the write index by a full batch plus one
|
||||
// so that we can keep reading buffered frames until we receive the
|
||||
// new batch
|
||||
self.readIndex = -data.frames.length - 1;
|
||||
|
||||
self.buffer.fill(new Array(data.frames[0].length).fill(0));
|
||||
}
|
||||
|
||||
for (const f of data.frames)
|
||||
self.buffer[mod(self.writeIndex++, self.bufferLength)] = f;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getBufferItem(index) {
|
||||
return this.buffer[mod(index, this.buffer.length)];
|
||||
}
|
||||
|
||||
getLevels(timeStampMs) {
|
||||
if (this.timeResolutionMs == 0) return null;
|
||||
|
||||
const previousTimeStampMs = this.lastTimeStampMs;
|
||||
this.lastTimeStampMs = timeStampMs;
|
||||
|
||||
if (previousTimeStampMs == 0) return this.buffer[0];
|
||||
|
||||
const timeAdvance =
|
||||
(timeStampMs - previousTimeStampMs) / this.timeResolutionMs;
|
||||
this.readIndex += timeAdvance;
|
||||
|
||||
const integralPart = Math.floor(this.readIndex);
|
||||
const fractionalPart = this.readIndex - integralPart;
|
||||
|
||||
return interpolate(
|
||||
this.getBufferItem(integralPart),
|
||||
this.getBufferItem(integralPart + 1),
|
||||
fractionalPart
|
||||
);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
window.__JUCE__.backend.removeEventListener(
|
||||
this.spectrumDataRegistrationId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function FreqBandInfo() {
|
||||
const canvasRef = useRef(null);
|
||||
let dataReceiver = null;
|
||||
let isActive = true;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const render = (timeStampMs) => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var grd = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
grd.addColorStop(0, "#1976d2");
|
||||
grd.addColorStop(1, "#dae9f8");
|
||||
ctx.fillStyle = grd;
|
||||
|
||||
if (dataReceiver != null) {
|
||||
const levels = dataReceiver.getLevels(timeStampMs);
|
||||
|
||||
if (levels != null) {
|
||||
const numBars = levels.length;
|
||||
const barWidth = canvas.width / numBars;
|
||||
const barHeight = canvas.height;
|
||||
|
||||
for (const [i, l] of levels.entries()) {
|
||||
ctx.fillRect(
|
||||
i * barWidth,
|
||||
barHeight - l * barHeight,
|
||||
barWidth,
|
||||
l * barHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isActive) window.requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dataReceiver = new SpectrumDataReceiver(10);
|
||||
isActive = true;
|
||||
window.requestAnimationFrame(render);
|
||||
|
||||
return function cleanup() {
|
||||
isActive = false;
|
||||
dataReceiver.unregister();
|
||||
};
|
||||
});
|
||||
|
||||
const canvasStyle = {
|
||||
marginLeft: "0",
|
||||
marginRight: "0",
|
||||
marginTop: "1em",
|
||||
display: "block",
|
||||
width: "94%",
|
||||
bottom: "0",
|
||||
position: "absolute",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<canvas height={90} style={canvasStyle} ref={canvasRef}></canvas>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [snackbarMessage, setMessage] = useState("No message received yet");
|
||||
|
||||
const openSnackbar = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = (event, reason) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const action = (
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<div>
|
||||
<Container>
|
||||
<JuceSlider identifier="cutoffSlider" title="Cutoff" />
|
||||
</Container>
|
||||
<CardActions style={{ justifyContent: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ marginTop: 2 }}
|
||||
onClick={() => {
|
||||
sayHello("JUCE").then((result) => {
|
||||
setMessage(result);
|
||||
openSnackbar();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
Call backend function
|
||||
</Button>
|
||||
</CardActions>
|
||||
<CardActions style={{ justifyContent: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ marginTop: 2 }}
|
||||
onClick={() => {
|
||||
fetch(Juce.getBackendResourceAddress("data.txt"))
|
||||
.then((response) => response.text())
|
||||
.then((text) => {
|
||||
setMessage("Data fetched: " + text);
|
||||
openSnackbar();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Fetch data from backend
|
||||
</Button>
|
||||
</CardActions>
|
||||
<JuceCheckbox identifier="muteToggle" />
|
||||
<br></br>
|
||||
<JuceComboBox identifier="filterTypeCombo" />
|
||||
<FreqBandInfo></FreqBandInfo>
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={6000}
|
||||
onClose={handleClose}
|
||||
message={snackbarMessage}
|
||||
action={action}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ body {
|
|||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
|
|||
|
|
@ -86,11 +86,26 @@ inline File getExamplesDirectory() noexcept
|
|||
#endif
|
||||
}
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath)
|
||||
enum class AssertAssetExists
|
||||
{
|
||||
no,
|
||||
yes
|
||||
};
|
||||
|
||||
inline std::unique_ptr<InputStream> createAssetInputStream (const char* resourcePath,
|
||||
[[maybe_unused]] AssertAssetExists assertExists = AssertAssetExists::yes)
|
||||
{
|
||||
#if JUCE_ANDROID
|
||||
ZipFile apkZip (File::getSpecialLocation (File::invokedExecutableFile));
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (apkZip.getIndexOfFileName ("assets/" + String (resourcePath))));
|
||||
const auto fileIndex = apkZip.getIndexOfFileName ("assets/" + String (resourcePath));
|
||||
|
||||
if (fileIndex == -1)
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::unique_ptr<InputStream> (apkZip.createStreamForEntry (fileIndex));
|
||||
#else
|
||||
#if JUCE_IOS
|
||||
auto assetsDir = File::getSpecialLocation (File::currentExecutableFile)
|
||||
|
|
@ -106,7 +121,12 @@ inline std::unique_ptr<InputStream> createAssetInputStream (const char* resource
|
|||
#endif
|
||||
|
||||
auto resourceFile = assetsDir.getChildFile (resourcePath);
|
||||
jassert (resourceFile.existsAsFile());
|
||||
|
||||
if (! resourceFile.existsAsFile())
|
||||
{
|
||||
jassert (assertExists == AssertAssetExists::no);
|
||||
return {};
|
||||
}
|
||||
|
||||
return resourceFile.createInputStream();
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WebViewPluginDemo</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebViewPluginDemo</h1>
|
||||
<p>
|
||||
This document is a placeholder for the GUI component of the
|
||||
WebViewPluginDemo.
|
||||
</p>
|
||||
<p>
|
||||
To build the fully fledged user interface you need to install
|
||||
<a href="https://nodejs.org">node.js</a>
|
||||
</p>
|
||||
<p>
|
||||
Then navigate into the
|
||||
<code>examples/GUI/WebViewPluginDemoGUI</code> directory inside your JUCE
|
||||
directory, and issue the following commands.
|
||||
</p>
|
||||
<pre>
|
||||
npm install
|
||||
npm run build
|
||||
npm run zip
|
||||
</pre
|
||||
>
|
||||
<p>
|
||||
This will build the full GUI package and place it in the
|
||||
<code>Assets</code> directory.
|
||||
</p>
|
||||
<p>After this, rebuild and restart this demo.</p>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue