1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Move the WebViewPluginDemo from the GUI to the Plugins category

This commit is contained in:
attila 2024-04-25 13:07:06 +02:00
parent d9095fccb2
commit 8a9ce51b2a
20 changed files with 3 additions and 3 deletions

View 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); }
};

View file

@ -0,0 +1,49 @@
/*
==============================================================================
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.
==============================================================================
*/
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ["eslint:recommended", "plugin:react/recommended"],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["react"],
rules: {
"no-extra-semi": "error",
},
};

View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
{
"name": "webviewplugin-gui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.3",
"@mui/icons-material": "^5.13.7",
"@mui/material": "^5.13.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",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"zip": "npm-build-zip --destination=../../Assets"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint": "^8.43.0",
"eslint-plugin-react": "^7.32.2",
"npm-build-zip": "^1.0.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Ladder Filter</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -0,0 +1,25 @@
{
"short_name": "Ladder Filter",
"name": "Ladder Filter",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -0,0 +1,441 @@
/*
==============================================================================
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.
==============================================================================
*/
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";
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>
<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();
});
}}
>
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>
);
}
export default App;

View file

@ -0,0 +1,31 @@
/*
==============================================================================
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.
==============================================================================
*/
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -0,0 +1,14 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: white;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -0,0 +1,40 @@
/*
==============================================================================
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.
==============================================================================
*/
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View file

@ -0,0 +1,36 @@
/*
==============================================================================
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.
==============================================================================
*/
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View file

@ -0,0 +1,28 @@
/*
==============================================================================
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.
==============================================================================
*/
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';