├── .clang-format ├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE.md ├── README.md ├── cmake └── cpm.cmake ├── docs └── logo.webp ├── plugin ├── CMakeLists.txt ├── include │ └── JuceWebViewTutorial │ │ ├── ParameterIDs.hpp │ │ ├── PluginEditor.h │ │ └── PluginProcessor.h ├── source │ ├── PluginEditor.cpp │ └── PluginProcessor.cpp └── ui │ └── public │ ├── index.html │ └── js │ └── index.js └── scripts └── DownloadWebView2.ps1 /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Chromium 4 | AccessModifierOffset: -2 5 | SortIncludes: false 6 | ... 7 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [macos-14, windows-latest] 14 | build_type: [Debug, Release] 15 | 16 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 17 | # You can convert this to a matrix build if you need cross-platform coverage. 18 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - name: List Xcode installations 23 | if: matrix.os == 'macos-14' 24 | run: sudo ls -1 /Applications | grep "Xcode" 25 | - name: Select Xcode 15.3 26 | if: matrix.os == 'macos-14' 27 | run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer 28 | - uses: actions/checkout@v4 29 | - name: Cache dependencies 30 | id: cache-libs 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{github.workspace}} 34 | key: libs 35 | 36 | - name: Configure CMake 37 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 38 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 39 | run: cmake -S . -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} 40 | 41 | - name: Build 42 | # Build your program with the given configuration 43 | run: cmake --build ${{github.workspace}}/build --config ${{ matrix.build_type }} 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | todo.md 2 | .vs 3 | .vscode 4 | libs/ 5 | build/ 6 | xcode-build/ 7 | release-build/ 8 | vs-build/ 9 | juce/ 10 | 11 | # Prerequisites 12 | *.d 13 | 14 | # Compiled Object files 15 | *.slo 16 | *.lo 17 | *.o 18 | *.obj 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Compiled Dynamic libraries 25 | *.so 26 | *.dylib 27 | *.dll 28 | 29 | # Fortran module files 30 | *.mod 31 | *.smod 32 | 33 | # Compiled Static libraries 34 | *.lai 35 | *.la 36 | *.a 37 | *.lib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app 43 | 44 | .vscode/* 45 | !.vscode/settings.json 46 | !.vscode/tasks.json 47 | !.vscode/launch.json 48 | !.vscode/extensions.json 49 | !.vscode/*.code-snippets 50 | 51 | # Local History for Visual Studio Code 52 | .history/ 53 | 54 | # Built Visual Studio Code Extensions 55 | *.vsix 56 | 57 | ## Ignore Visual Studio temporary files, build results, and 58 | ## files generated by popular Visual Studio add-ons. 59 | ## 60 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 61 | 62 | # User-specific files 63 | *.rsuser 64 | *.suo 65 | *.user 66 | *.userosscache 67 | *.sln.docstates 68 | 69 | # User-specific files (MonoDevelop/Xamarin Studio) 70 | *.userprefs 71 | 72 | # Mono auto generated files 73 | mono_crash.* 74 | 75 | # Build results 76 | [Dd]ebug/ 77 | [Dd]ebugPublic/ 78 | [Rr]elease/ 79 | [Rr]eleases/ 80 | x64/ 81 | x86/ 82 | [Ww][Ii][Nn]32/ 83 | [Aa][Rr][Mm]/ 84 | [Aa][Rr][Mm]64/ 85 | bld/ 86 | [Bb]in/ 87 | [Oo]bj/ 88 | [Ll]og/ 89 | [Ll]ogs/ 90 | 91 | # Visual Studio 2015/2017 cache/options directory 92 | .vs/ 93 | # Uncomment if you have tasks that create the project's static files in wwwroot 94 | #wwwroot/ 95 | 96 | # Visual Studio 2017 auto generated files 97 | Generated\ Files/ 98 | 99 | # MSTest test Results 100 | [Tt]est[Rr]esult*/ 101 | [Bb]uild[Ll]og.* 102 | 103 | # NUnit 104 | *.VisualState.xml 105 | TestResult.xml 106 | nunit-*.xml 107 | 108 | # Build Results of an ATL Project 109 | [Dd]ebugPS/ 110 | [Rr]eleasePS/ 111 | dlldata.c 112 | 113 | # Benchmark Results 114 | BenchmarkDotNet.Artifacts/ 115 | 116 | # .NET Core 117 | project.lock.json 118 | project.fragment.lock.json 119 | artifacts/ 120 | 121 | # ASP.NET Scaffolding 122 | ScaffoldingReadMe.txt 123 | 124 | # StyleCop 125 | StyleCopReport.xml 126 | 127 | # Files built by Visual Studio 128 | *_i.c 129 | *_p.c 130 | *_h.h 131 | *.ilk 132 | *.meta 133 | *.obj 134 | *.iobj 135 | *.pch 136 | *.pdb 137 | *.ipdb 138 | *.pgc 139 | *.pgd 140 | *.rsp 141 | *.sbr 142 | *.tlb 143 | *.tli 144 | *.tlh 145 | *.tmp 146 | *.tmp_proj 147 | *_wpftmp.csproj 148 | *.log 149 | *.tlog 150 | *.vspscc 151 | *.vssscc 152 | .builds 153 | *.pidb 154 | *.svclog 155 | *.scc 156 | 157 | # Chutzpah Test files 158 | _Chutzpah* 159 | 160 | # Visual C++ cache files 161 | ipch/ 162 | *.aps 163 | *.ncb 164 | *.opendb 165 | *.opensdf 166 | *.sdf 167 | *.cachefile 168 | *.VC.db 169 | *.VC.VC.opendb 170 | 171 | # Visual Studio profiler 172 | *.psess 173 | *.vsp 174 | *.vspx 175 | *.sap 176 | 177 | # Visual Studio Trace Files 178 | *.e2e 179 | 180 | # TFS 2012 Local Workspace 181 | $tf/ 182 | 183 | # Guidance Automation Toolkit 184 | *.gpState 185 | 186 | # ReSharper is a .NET coding add-in 187 | _ReSharper*/ 188 | *.[Rr]e[Ss]harper 189 | *.DotSettings.user 190 | 191 | # TeamCity is a build add-in 192 | _TeamCity* 193 | 194 | # DotCover is a Code Coverage Tool 195 | *.dotCover 196 | 197 | # AxoCover is a Code Coverage Tool 198 | .axoCover/* 199 | !.axoCover/settings.json 200 | 201 | # Coverlet is a free, cross platform Code Coverage Tool 202 | coverage*.json 203 | coverage*.xml 204 | coverage*.info 205 | 206 | # Visual Studio code coverage results 207 | *.coverage 208 | *.coveragexml 209 | 210 | # NCrunch 211 | _NCrunch_* 212 | .*crunch*.local.xml 213 | nCrunchTemp_* 214 | 215 | # MightyMoose 216 | *.mm.* 217 | AutoTest.Net/ 218 | 219 | # Web workbench (sass) 220 | .sass-cache/ 221 | 222 | # Installshield output folder 223 | [Ee]xpress/ 224 | 225 | # DocProject is a documentation generator add-in 226 | DocProject/buildhelp/ 227 | DocProject/Help/*.HxT 228 | DocProject/Help/*.HxC 229 | DocProject/Help/*.hhc 230 | DocProject/Help/*.hhk 231 | DocProject/Help/*.hhp 232 | DocProject/Help/Html2 233 | DocProject/Help/html 234 | 235 | # Click-Once directory 236 | publish/ 237 | 238 | # Publish Web Output 239 | *.[Pp]ublish.xml 240 | *.azurePubxml 241 | # Note: Comment the next line if you want to checkin your web deploy settings, 242 | # but database connection strings (with potential passwords) will be unencrypted 243 | *.pubxml 244 | *.publishproj 245 | 246 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 247 | # checkin your Azure Web App publish settings, but sensitive information contained 248 | # in these scripts will be unencrypted 249 | PublishScripts/ 250 | 251 | # NuGet Packages 252 | *.nupkg 253 | # NuGet Symbol Packages 254 | *.snupkg 255 | # The packages folder can be ignored because of Package Restore 256 | **/[Pp]ackages/* 257 | # except build/, which is used as an MSBuild target. 258 | !**/[Pp]ackages/build/ 259 | # Uncomment if necessary however generally it will be regenerated when needed 260 | #!**/[Pp]ackages/repositories.config 261 | # NuGet v3's project.json files produces more ignorable files 262 | *.nuget.props 263 | *.nuget.targets 264 | 265 | # Microsoft Azure Build Output 266 | csx/ 267 | *.build.csdef 268 | 269 | # Microsoft Azure Emulator 270 | ecf/ 271 | rcf/ 272 | 273 | # Windows Store app package directories and files 274 | AppPackages/ 275 | BundleArtifacts/ 276 | Package.StoreAssociation.xml 277 | _pkginfo.txt 278 | *.appx 279 | *.appxbundle 280 | *.appxupload 281 | 282 | # Visual Studio cache files 283 | # files ending in .cache can be ignored 284 | *.[Cc]ache 285 | # but keep track of directories ending in .cache 286 | !?*.[Cc]ache/ 287 | 288 | # Others 289 | ClientBin/ 290 | ~$* 291 | *~ 292 | *.dbmdl 293 | *.dbproj.schemaview 294 | *.jfm 295 | *.pfx 296 | *.publishsettings 297 | orleans.codegen.cs 298 | 299 | # Including strong name files can present a security risk 300 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 301 | #*.snk 302 | 303 | # Since there are multiple workflows, uncomment next line to ignore bower_components 304 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 305 | #bower_components/ 306 | 307 | # RIA/Silverlight projects 308 | Generated_Code/ 309 | 310 | # Backup & report files from converting an old project file 311 | # to a newer Visual Studio version. Backup files are not needed, 312 | # because we have git ;-) 313 | _UpgradeReport_Files/ 314 | Backup*/ 315 | UpgradeLog*.XML 316 | UpgradeLog*.htm 317 | ServiceFabricBackup/ 318 | *.rptproj.bak 319 | 320 | # SQL Server files 321 | *.mdf 322 | *.ldf 323 | *.ndf 324 | 325 | # Business Intelligence projects 326 | *.rdl.data 327 | *.bim.layout 328 | *.bim_*.settings 329 | *.rptproj.rsuser 330 | *- [Bb]ackup.rdl 331 | *- [Bb]ackup ([0-9]).rdl 332 | *- [Bb]ackup ([0-9][0-9]).rdl 333 | 334 | # Microsoft Fakes 335 | FakesAssemblies/ 336 | 337 | # GhostDoc plugin setting file 338 | *.GhostDoc.xml 339 | 340 | # Node.js Tools for Visual Studio 341 | .ntvs_analysis.dat 342 | node_modules/ 343 | 344 | # Visual Studio 6 build log 345 | *.plg 346 | 347 | # Visual Studio 6 workspace options file 348 | *.opt 349 | 350 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 351 | *.vbw 352 | 353 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 354 | *.vbp 355 | 356 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 357 | *.dsw 358 | *.dsp 359 | 360 | # Visual Studio 6 technical files 361 | *.ncb 362 | *.aps 363 | 364 | # Visual Studio LightSwitch build output 365 | **/*.HTMLClient/GeneratedArtifacts 366 | **/*.DesktopClient/GeneratedArtifacts 367 | **/*.DesktopClient/ModelManifest.xml 368 | **/*.Server/GeneratedArtifacts 369 | **/*.Server/ModelManifest.xml 370 | _Pvt_Extensions 371 | 372 | # Paket dependency manager 373 | .paket/paket.exe 374 | paket-files/ 375 | 376 | # FAKE - F# Make 377 | .fake/ 378 | 379 | # CodeRush personal settings 380 | .cr/personal 381 | 382 | # Python Tools for Visual Studio (PTVS) 383 | __pycache__/ 384 | *.pyc 385 | 386 | # Cake - Uncomment if you are using it 387 | # tools/** 388 | # !tools/packages.config 389 | 390 | # Tabs Studio 391 | *.tss 392 | 393 | # Telerik's JustMock configuration file 394 | *.jmconfig 395 | 396 | # BizTalk build output 397 | *.btp.cs 398 | *.btm.cs 399 | *.odx.cs 400 | *.xsd.cs 401 | 402 | # OpenCover UI analysis results 403 | OpenCover/ 404 | 405 | # Azure Stream Analytics local run output 406 | ASALocalRun/ 407 | 408 | # MSBuild Binary and Structured Log 409 | *.binlog 410 | 411 | # NVidia Nsight GPU debugger configuration file 412 | *.nvuser 413 | 414 | # MFractors (Xamarin productivity tool) working folder 415 | .mfractor/ 416 | 417 | # Local History for Visual Studio 418 | .localhistory/ 419 | 420 | # Visual Studio History (VSHistory) files 421 | .vshistory/ 422 | 423 | # BeatPulse healthcheck temp database 424 | healthchecksdb 425 | 426 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 427 | MigrationBackup/ 428 | 429 | # Ionide (cross platform F# VS Code tools) working folder 430 | .ionide/ 431 | 432 | # Fody - auto-generated XML schema 433 | FodyWeavers.xsd 434 | 435 | # VS Code files for those working on multiple tools 436 | .vscode/* 437 | !.vscode/settings.json 438 | !.vscode/tasks.json 439 | !.vscode/launch.json 440 | !.vscode/extensions.json 441 | *.code-workspace 442 | 443 | # Local History for Visual Studio Code 444 | .history/ 445 | 446 | # Windows Installer files from build outputs 447 | *.cab 448 | *.msi 449 | *.msix 450 | *.msm 451 | *.msp 452 | 453 | # JetBrains Rider 454 | *.sln.iml 455 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-clang-format 3 | rev: 'v15.0.6' # Use the sha / tag you want to point at 4 | hooks: 5 | - id: clang-format 6 | files: '.(c|h|hpp|cpp)$' 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Mandatory line, sets the minimum version of CMake that should be used with this repository. 2 | # JUCE requires CMake 3.22 or higher. 3 | # To verify your version run 4 | # $ cmake --version 5 | cmake_minimum_required(VERSION 3.22) 6 | 7 | # Sets a few variables, like PROJECT_NAME 8 | project(Juce8WebViewTutorial) 9 | 10 | # Safe choice 11 | set(CMAKE_CXX_STANDARD 20) 12 | 13 | # I like to download the dependencies to the same folder as the project. 14 | # If you want to install them system wide, set CPM_SOURCE_CACHE with the path to the dependencies 15 | # either as an environment variable or pass it to the cmake script with -DCPM_SOURCE_CACHE=. 16 | set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs) 17 | 18 | # Downloads CPM if not already downloaded. CPM is an easy-to-use package manager nicely integrated with CMake. 19 | include(cmake/cpm.cmake) 20 | 21 | # This commands downloads AND configures JUCE. It sets up some variables, like JUCE_SOURCE_DIR. 22 | CPMAddPackage( 23 | NAME JUCE 24 | GIT_TAG 8.0.6 25 | VERSION 8.0.6 26 | GITHUB_REPOSITORY juce-framework/JUCE 27 | SOURCE_DIR ${LIB_DIR}/juce 28 | ) 29 | 30 | # Install Microsoft.Web.WebView2 NuGet package to allow WebViews on Windows 31 | if (MSVC) 32 | message(STATUS "Setting up WebView dependencies") 33 | execute_process(COMMAND pwsh -NoProfile -File scripts/DownloadWebView2.ps1 34 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 35 | RESULT_VARIABLE DOWNLOAD_WEBVIEW2_RESULT) 36 | 37 | if (NOT DOWNLOAD_WEBVIEW2_RESULT EQUAL 0) 38 | message(FATAL_ERROR "Failed to download Microsoft.Web.WebView2 NuGet package. Result: ${DOWNLOAD_WEBVIEW2_RESULT}") 39 | endif() 40 | endif() 41 | 42 | # Enables strict warnings and treats warnings as errors. 43 | # This needs to be set up only for your projects, not 3rd party 44 | if (MSVC) 45 | set(CXX_PROJECT_WARNINGS "/W4;/WX;/wd4820;/wd4514") 46 | else() 47 | set(CXX_PROJECT_WARNINGS "-Wall;-Werror;-Wextra;-Wpedantic") 48 | endif() 49 | 50 | # Adds all the targets configured in the "plugin" folder. 51 | add_subdirectory(plugin) 52 | 53 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 5, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 22, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "default", 11 | "generator": "Ninja", 12 | "binaryDir": "build", 13 | "cacheVariables": { 14 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", 15 | "CMAKE_BUILD_TYPE": "Debug" 16 | } 17 | }, 18 | { 19 | "name": "release", 20 | "generator": "Ninja", 21 | "binaryDir": "release-build", 22 | "cacheVariables": { 23 | "CMAKE_BUILD_TYPE": "Release" 24 | } 25 | }, 26 | { 27 | "name": "vs", 28 | "generator": "Visual Studio 17 2022", 29 | "binaryDir": "vs-build" 30 | }, 31 | { 32 | "name": "Xcode", 33 | "generator": "Xcode", 34 | "binaryDir": "xcode-build" 35 | } 36 | ], 37 | "buildPresets": [ 38 | { 39 | "name": "default", 40 | "configurePreset": "default" 41 | }, 42 | { 43 | "name": "release", 44 | "configurePreset": "release" 45 | }, 46 | { 47 | "name": "vs", 48 | "configurePreset": "vs" 49 | }, 50 | { 51 | "name": "Xcode", 52 | "configurePreset": "Xcode" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Jan Wilczek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # 🎹 JUCE 8 WebView UI Plugin Tutorial 🎹 6 | 7 | ![Cmake workflow success badge](https://github.com/JanWilczek/juce-webview-tutorial/actions/workflows/cmake.yml/badge.svg) 8 | 9 |
10 | 11 | Welcome to the JUCE 8 WebView UI Plugin Tutorial! This repository accompanies a video tutorial series on creating a WebView UI plugin using JUCE 8. 12 | 13 | Feel free to use this project as a starting point for your own projects. For this, you can click "Use this template" button at the top of this page. 14 | 15 | ## 🚀 Getting Started 16 | 17 | This repository uses CMake. It takes care of all the dependencies using [CPM](https://github.com/cpm-cmake/CPM.cmake). 18 | 19 | After cloning the repo locally, you can proceed with the usual CMake workflow. 20 | 21 | To generate project files execute in the main repo directory 22 | 23 | ```bash 24 | cmake --preset default # uses the Ninja build system 25 | ``` 26 | 27 | Existing presets are `default`, `release`, `vs` (for Visual Studio), and `Xcode`. Check out _CMakePresets.json_ for details. 28 | 29 | The first run will take the most time because the dependencies (CPM and JUCE) need to be downloaded. 30 | 31 | To build the project execute in the main repo directory 32 | 33 | ```bash 34 | cmake --build --preset default # or release, vs, or Xcode 35 | ``` 36 | 37 | ### Additional setup 38 | 39 | To run clang-format on every commit, in the main directory execute 40 | 41 | ```bash 42 | pre-commit install 43 | ``` 44 | 45 | (for this, you may need to install `pre-commit` with `pip`: `pip install pre-commit`). 46 | 47 | ## 📜 License 48 | 49 | This project is licensed under the MIT license. Check out the [LICENSE.md](LICENSE.md) file for details. 50 | 51 | -------------------------------------------------------------------------------- /cmake/cpm.cmake: -------------------------------------------------------------------------------- 1 | set(CPM_DOWNLOAD_VERSION 0.40.2) 2 | 3 | set(CPM_DOWNLOAD_LOCATION "${LIB_DIR}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 4 | 5 | # Expand relative path. This is important if the provided path contains a tilde (~) 6 | get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) 7 | 8 | function(download_cpm) 9 | message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") 10 | file(DOWNLOAD 11 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 12 | ${CPM_DOWNLOAD_LOCATION} 13 | ) 14 | endfunction() 15 | 16 | if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) 17 | download_cpm() 18 | else() 19 | # resume download if it previously failed 20 | file(READ ${CPM_DOWNLOAD_LOCATION} check) 21 | if("${check}" STREQUAL "") 22 | download_cpm() 23 | endif() 24 | unset(check) 25 | endif() 26 | 27 | include(${CPM_DOWNLOAD_LOCATION}) 28 | 29 | -------------------------------------------------------------------------------- /docs/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/juce-webview-tutorial/aa46d8c8aadf8c7163e4ded0c7ea2aab27aea5dc/docs/logo.webp -------------------------------------------------------------------------------- /plugin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22 FATAL_ERROR) 2 | 3 | # Version is needed by JUCE. 4 | project(JuceWebViewPlugin VERSION 0.1.0) 5 | 6 | # Adds a plugin target (that's basically what the Projucer does). 7 | juce_add_plugin(${PROJECT_NAME} 8 | COMPANY_NAME WolfSound 9 | IS_SYNTH FALSE 10 | NEEDS_MIDI_INPUT FALSE 11 | NEEDS_MIDI_OUTPUT FALSE 12 | PLUGIN_MANUFACTURER_CODE WFSD 13 | PLUGIN_CODE JWVT 14 | FORMATS AU VST3 Standalone 15 | PRODUCT_NAME "JuceWebViewPlugin" 16 | # WebViews use web browser features: find necessary packages in the system to link against 17 | NEEDS_WEB_BROWSER TRUE 18 | # This will force JUCE to look for the WebView2 NuGet package on Windows 19 | NEEDS_WEBVIEW2 TRUE 20 | ) 21 | 22 | get_target_property(PRODUCT_NAME ${PROJECT_NAME} JUCE_PRODUCT_NAME) 23 | get_target_property(COMPANY_NAME ${PROJECT_NAME} JUCE_COMPANY_NAME) 24 | target_compile_definitions(${PROJECT_NAME} PRIVATE 25 | JUCE_PRODUCT_NAME="${PRODUCT_NAME}" 26 | JUCE_COMPANY_NAME="${COMPANY_NAME}" 27 | JUCE_PRODUCT_VERSION="${PROJECT_VERSION}") 28 | 29 | # Sets the source files of the plugin project. 30 | set(SOURCES 31 | source/PluginEditor.cpp 32 | source/PluginProcessor.cpp) 33 | 34 | # Adding a directory with the library/application name as a subfolder of the 35 | # include folder is a good practice. It helps avoid name clashes later on. 36 | set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/JuceWebViewTutorial") 37 | 38 | target_sources(${PROJECT_NAME} 39 | PRIVATE 40 | ${SOURCES} 41 | ${INCLUDE_DIR}/PluginEditor.h 42 | ${INCLUDE_DIR}/PluginProcessor.h 43 | ) 44 | 45 | # Sets the include directories of the plugin project. 46 | target_include_directories(${PROJECT_NAME} 47 | PUBLIC 48 | ${CMAKE_CURRENT_SOURCE_DIR}/include 49 | ) 50 | 51 | # Mark JUCE headers as system headers to suppress warnings in them 52 | target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${JUCE_MODULES_DIR}) 53 | 54 | # Folder where web UI data reside 55 | set(WEBVIEW_FILES_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ui/public") 56 | 57 | # Copy JUCE frontend library to plugin UI files 58 | file(COPY "${JUCE_MODULES_DIR}/juce_gui_extra/native/javascript/" DESTINATION "${WEBVIEW_FILES_SOURCE_DIR}/js/juce/") 59 | 60 | # Zip WebView files 61 | set(WEBVIEW_FILES_ZIP_NAME "webview_files.zip") 62 | set(TARGET_WEBVIEW_FILES_ZIP_PATH "${CMAKE_BINARY_DIR}/${WEBVIEW_FILES_ZIP_NAME}") 63 | cmake_path(ABSOLUTE_PATH WEBVIEW_FILES_SOURCE_DIR NORMALIZE OUTPUT_VARIABLE PUBLIC_PATH ) 64 | cmake_path(GET PUBLIC_PATH PARENT_PATH WORKING_DIRECTORY) 65 | 66 | add_custom_target(ZipWebViewFiles 67 | COMMAND 68 | ${CMAKE_COMMAND} -E tar cvf 69 | "${TARGET_WEBVIEW_FILES_ZIP_PATH}" 70 | --format=zip 71 | "${PUBLIC_PATH}" 72 | BYPRODUCTS 73 | "${TARGET_WEBVIEW_FILES_ZIP_PATH}" 74 | WORKING_DIRECTORY 75 | "${WORKING_DIRECTORY}" 76 | COMMENT "Zipping WebView files..." 77 | VERBATIM 78 | ) 79 | 80 | # Pass the prefix of the zipped files (e.g., "public/") to C++ 81 | cmake_path(GET PUBLIC_PATH FILENAME ZIPPED_FILES_PREFIX) 82 | target_compile_definitions(${PROJECT_NAME} PRIVATE ZIPPED_FILES_PREFIX="${ZIPPED_FILES_PREFIX}/") 83 | 84 | # Package web UI sources as binary data 85 | juce_add_binary_data(WebViewFiles 86 | HEADER_NAME WebViewFiles.h 87 | NAMESPACE webview_files 88 | SOURCES ${TARGET_WEBVIEW_FILES_ZIP_PATH} 89 | ) 90 | add_dependencies(WebViewFiles ZipWebViewFiles) 91 | 92 | # Links to all necessary dependencies. The present ones are recommended by JUCE. 93 | # If you use one of the additional modules, like the DSP module, you need to specify it here. 94 | target_link_libraries(${PROJECT_NAME} 95 | PRIVATE 96 | juce::juce_audio_utils 97 | juce::juce_dsp 98 | WebViewFiles 99 | PUBLIC 100 | juce::juce_recommended_config_flags 101 | juce::juce_recommended_lto_flags 102 | juce::juce_recommended_warning_flags 103 | ) 104 | 105 | target_compile_definitions(${PROJECT_NAME} 106 | PUBLIC 107 | # JUCE_WEB_BROWSER=1 is important for using WebBrowserComponent 108 | JUCE_WEB_BROWSER=1 109 | JUCE_USE_CURL=0 110 | JUCE_VST3_CAN_REPLACE_VST2=0 111 | # This will enable WebView2 as the WebView backend on Windows 112 | JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1 113 | ) 114 | 115 | set_source_files_properties(${SOURCES} PROPERTIES COMPILE_OPTIONS "${CXX_PROJECT_WARNINGS}") 116 | 117 | # In Visual Studio this command provides a nice grouping of source files in "filters". 118 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/..) 119 | -------------------------------------------------------------------------------- /plugin/include/JuceWebViewTutorial/ParameterIDs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace webview_plugin::id { 6 | const juce::ParameterID GAIN{"GAIN", 1}; 7 | const juce::ParameterID BYPASS{"BYPASS", 1}; 8 | const juce::ParameterID DISTORTION_TYPE{"DISTORTION_TYPE", 1}; 9 | } // namespace webview_plugin::id 10 | -------------------------------------------------------------------------------- /plugin/include/JuceWebViewTutorial/PluginEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PluginProcessor.h" 4 | #include "juce_gui_basics/juce_gui_basics.h" 5 | #include 6 | #include 7 | 8 | namespace webview_plugin { 9 | 10 | class AudioPluginAudioProcessorEditor : public juce::AudioProcessorEditor, 11 | private juce::Timer { 12 | public: 13 | explicit AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor&); 14 | ~AudioPluginAudioProcessorEditor() override; 15 | 16 | void resized() override; 17 | 18 | void timerCallback() override; 19 | 20 | private: 21 | using Resource = juce::WebBrowserComponent::Resource; 22 | std::optional getResource(const juce::String& url) const; 23 | void nativeFunction( 24 | const juce::Array& args, 25 | juce::WebBrowserComponent::NativeFunctionCompletion completion); 26 | 27 | juce::TextButton runJavaScriptButton{"Run some JavaScript"}; 28 | juce::TextButton emitJavaScriptEventButton{"Emit JavaScript event"}; 29 | juce::Label labelUpdatedFromJavaScript{"label", 30 | "To be updated from JavaScript"}; 31 | 32 | AudioPluginAudioProcessor& processorRef; 33 | 34 | juce::Slider gainSlider{"gain slider"}; 35 | juce::SliderParameterAttachment gainSliderAttachment; 36 | 37 | juce::ToggleButton bypassButton{"Bypass"}; 38 | juce::ButtonParameterAttachment bypassButtonAttachment; 39 | 40 | juce::Label distortionTypeLabel{"distortion type label", "Distortion"}; 41 | juce::ComboBox distortionTypeComboBox{"distortion type combo box"}; 42 | juce::ComboBoxParameterAttachment distortionTypeComboBoxAttachment; 43 | 44 | juce::WebSliderRelay webGainRelay; 45 | juce::WebToggleButtonRelay webBypassRelay; 46 | juce::WebComboBoxRelay webDistortionTypeRelay; 47 | 48 | juce::WebBrowserComponent webView; 49 | 50 | juce::WebSliderParameterAttachment webGainSliderAttachment; 51 | juce::WebToggleButtonParameterAttachment webBypassToggleAttachment; 52 | juce::WebComboBoxParameterAttachment webDistortionTypeComboBoxAttachment; 53 | 54 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessorEditor) 55 | }; 56 | } // namespace webview_plugin 57 | -------------------------------------------------------------------------------- /plugin/include/JuceWebViewTutorial/PluginProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace webview_plugin { 7 | class AudioPluginAudioProcessor : public juce::AudioProcessor { 8 | public: 9 | AudioPluginAudioProcessor(); 10 | ~AudioPluginAudioProcessor() override; 11 | 12 | void prepareToPlay(double sampleRate, int samplesPerBlock) override; 13 | void releaseResources() override; 14 | 15 | bool isBusesLayoutSupported(const BusesLayout& layouts) const override; 16 | 17 | void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; 18 | using AudioProcessor::processBlock; 19 | 20 | juce::AudioProcessorEditor* createEditor() override; 21 | bool hasEditor() const override; 22 | 23 | const juce::String getName() const override; 24 | 25 | bool acceptsMidi() const override; 26 | bool producesMidi() const override; 27 | bool isMidiEffect() const override; 28 | double getTailLengthSeconds() const override; 29 | 30 | int getNumPrograms() override; 31 | int getCurrentProgram() override; 32 | void setCurrentProgram(int index) override; 33 | const juce::String getProgramName(int index) override; 34 | void changeProgramName(int index, const juce::String& newName) override; 35 | 36 | void getStateInformation(juce::MemoryBlock& destData) override; 37 | void setStateInformation(const void* data, int sizeInBytes) override; 38 | 39 | [[nodiscard]] juce::AudioProcessorValueTreeState& getState() noexcept { 40 | return state; 41 | } 42 | 43 | [[nodiscard]] const juce::AudioParameterChoice& getDistortionTypeParameter() 44 | const noexcept { 45 | return *parameters.distortionType; 46 | } 47 | 48 | std::atomic outputLevelLeft; 49 | 50 | private: 51 | struct Parameters { 52 | juce::AudioParameterFloat* gain{nullptr}; 53 | juce::AudioParameterBool* bypass{nullptr}; 54 | juce::AudioParameterChoice* distortionType{nullptr}; 55 | }; 56 | 57 | [[nodiscard]] static juce::AudioProcessorValueTreeState::ParameterLayout 58 | createParameterLayout(Parameters&); 59 | 60 | Parameters parameters; 61 | juce::AudioProcessorValueTreeState state; 62 | juce::dsp::BallisticsFilter envelopeFollower; 63 | juce::AudioBuffer envelopeFollowerOutputBuffer; 64 | 65 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor) 66 | }; 67 | } // namespace webview_plugin 68 | -------------------------------------------------------------------------------- /plugin/source/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "JuceWebViewTutorial/PluginEditor.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "JuceWebViewTutorial/PluginProcessor.h" 7 | #include "juce_core/juce_core.h" 8 | #include "juce_graphics/juce_graphics.h" 9 | #include "juce_gui_extra/juce_gui_extra.h" 10 | #include "JuceWebViewTutorial/ParameterIDs.hpp" 11 | #include 12 | 13 | namespace webview_plugin { 14 | namespace { 15 | std::vector streamToVector(juce::InputStream& stream) { 16 | using namespace juce; 17 | const auto sizeInBytes = static_cast(stream.getTotalLength()); 18 | std::vector result(sizeInBytes); 19 | stream.setPosition(0); 20 | [[maybe_unused]] const auto bytesRead = 21 | stream.read(result.data(), result.size()); 22 | jassert(bytesRead == static_cast(sizeInBytes)); 23 | return result; 24 | } 25 | 26 | static const char* getMimeForExtension(const juce::String& extension) { 27 | static const std::unordered_map mimeMap = { 28 | {{"htm"}, "text/html"}, 29 | {{"html"}, "text/html"}, 30 | {{"txt"}, "text/plain"}, 31 | {{"jpg"}, "image/jpeg"}, 32 | {{"jpeg"}, "image/jpeg"}, 33 | {{"svg"}, "image/svg+xml"}, 34 | {{"ico"}, "image/vnd.microsoft.icon"}, 35 | {{"json"}, "application/json"}, 36 | {{"png"}, "image/png"}, 37 | {{"css"}, "text/css"}, 38 | {{"map"}, "application/json"}, 39 | {{"js"}, "text/javascript"}, 40 | {{"woff2"}, "font/woff2"}}; 41 | 42 | if (const auto it = mimeMap.find(extension.toLowerCase()); 43 | it != mimeMap.end()) 44 | return it->second; 45 | 46 | jassertfalse; 47 | return ""; 48 | } 49 | 50 | juce::Identifier getExampleEventId() { 51 | static const juce::Identifier id{"exampleEvent"}; 52 | return id; 53 | } 54 | 55 | #ifndef ZIPPED_FILES_PREFIX 56 | #error \ 57 | "You must provide the prefix of zipped web UI files' paths, e.g., 'public/', in the ZIPPED_FILES_PREFIX compile definition" 58 | #endif 59 | 60 | /** 61 | * @brief Get a web UI file as bytes 62 | * 63 | * @param filepath path of the form "index.html", "js/index.js", etc. 64 | * @return std::vector with bytes of a read file or an empty vector 65 | * if the file is not contained in webview_files.zip 66 | */ 67 | std::vector getWebViewFileAsBytes(const juce::String& filepath) { 68 | juce::MemoryInputStream zipStream{webview_files::webview_files_zip, 69 | webview_files::webview_files_zipSize, 70 | false}; 71 | juce::ZipFile zipFile{zipStream}; 72 | 73 | if (auto* zipEntry = zipFile.getEntry(ZIPPED_FILES_PREFIX + filepath)) { 74 | const std::unique_ptr entryStream{ 75 | zipFile.createStreamForEntry(*zipEntry)}; 76 | 77 | if (entryStream == nullptr) { 78 | jassertfalse; 79 | return {}; 80 | } 81 | 82 | return streamToVector(*entryStream); 83 | } 84 | 85 | return {}; 86 | } 87 | 88 | constexpr auto LOCAL_DEV_SERVER_ADDRESS = "http://127.0.0.1:8080"; 89 | } // namespace 90 | 91 | AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor( 92 | AudioPluginAudioProcessor& p) 93 | : AudioProcessorEditor(&p), 94 | processorRef(p), 95 | gainSliderAttachment{ 96 | *processorRef.getState().getParameter(id::GAIN.getParamID()), 97 | gainSlider, nullptr}, 98 | bypassButtonAttachment{ 99 | *processorRef.getState().getParameter(id::BYPASS.getParamID()), 100 | bypassButton, nullptr}, 101 | distortionTypeComboBoxAttachment{*processorRef.getState().getParameter( 102 | id::DISTORTION_TYPE.getParamID()), 103 | distortionTypeComboBox, nullptr}, 104 | webGainRelay{id::GAIN.getParamID()}, 105 | webBypassRelay{id::BYPASS.getParamID()}, 106 | webDistortionTypeRelay{id::DISTORTION_TYPE.getParamID()}, 107 | webView{ 108 | juce::WebBrowserComponent::Options{} 109 | .withBackend( 110 | juce::WebBrowserComponent::Options::Backend::webview2) 111 | .withWinWebView2Options( 112 | juce::WebBrowserComponent::Options::WinWebView2{} 113 | .withBackgroundColour(juce::Colours::white) 114 | // this may be necessary for some DAWs; include for safety 115 | .withUserDataFolder(juce::File::getSpecialLocation( 116 | juce::File::SpecialLocationType::tempDirectory))) 117 | .withNativeIntegrationEnabled() 118 | .withResourceProvider( 119 | [this](const auto& url) { return getResource(url); }, 120 | // allowedOriginIn parameter is necessary to 121 | // retrieve resources from the C++ backend even if 122 | // on live server 123 | juce::URL{LOCAL_DEV_SERVER_ADDRESS}.getOrigin()) 124 | .withInitialisationData("vendor", JUCE_COMPANY_NAME) 125 | .withInitialisationData("pluginName", JUCE_PRODUCT_NAME) 126 | .withInitialisationData("pluginVersion", JUCE_PRODUCT_VERSION) 127 | .withUserScript("console.log(\"C++ backend here: This is run " 128 | "before any other loading happens\");") 129 | .withEventListener( 130 | "exampleJavaScriptEvent", 131 | [this](juce::var objectFromFrontend) { 132 | labelUpdatedFromJavaScript.setText( 133 | "example JavaScript event occurred with value " + 134 | objectFromFrontend.getProperty("emittedCount", 0) 135 | .toString(), 136 | juce::dontSendNotification); 137 | }) 138 | .withNativeFunction( 139 | juce::Identifier{"nativeFunction"}, 140 | [this](const juce::Array& args, 141 | juce::WebBrowserComponent::NativeFunctionCompletion 142 | completion) { 143 | nativeFunction(args, std::move(completion)); 144 | }) 145 | .withOptionsFrom(webGainRelay) 146 | .withOptionsFrom(webBypassRelay) 147 | .withOptionsFrom(webDistortionTypeRelay)}, 148 | webGainSliderAttachment{ 149 | *processorRef.getState().getParameter(id::GAIN.getParamID()), 150 | webGainRelay, nullptr}, 151 | webBypassToggleAttachment{ 152 | *processorRef.getState().getParameter(id::BYPASS.getParamID()), 153 | webBypassRelay, nullptr}, 154 | webDistortionTypeComboBoxAttachment{*processorRef.getState().getParameter( 155 | id::DISTORTION_TYPE.getParamID()), 156 | webDistortionTypeRelay, nullptr} { 157 | addAndMakeVisible(webView); 158 | 159 | // WebBrowserComponent can display any URL 160 | // webView.goToURL("https://juce.com"); 161 | 162 | // This is necessary if we want to use a ResourceProvider 163 | webView.goToURL(juce::WebBrowserComponent::getResourceProviderRoot()); 164 | 165 | // This can be used for hot reloading 166 | // webView.goToURL(LOCAL_DEV_SERVER_ADDRESS); 167 | 168 | runJavaScriptButton.onClick = [this] { 169 | constexpr auto JAVASCRIPT_TO_RUN{"console.log(\"Hello from C++!\");"}; 170 | webView.evaluateJavascript( 171 | JAVASCRIPT_TO_RUN, 172 | [](juce::WebBrowserComponent::EvaluationResult result) { 173 | if (const auto* resultPtr = result.getResult()) { 174 | std::cout << "JavaScript evaluation result: " 175 | << resultPtr->toString() << std::endl; 176 | } else { 177 | std::cout << "JavaScript evaluation failed because " 178 | << result.getError()->message << std::endl; 179 | } 180 | }); 181 | }; 182 | addAndMakeVisible(runJavaScriptButton); 183 | 184 | emitJavaScriptEventButton.onClick = [this] { 185 | static const juce::var valueToEmit{42.0}; 186 | webView.emitEventIfBrowserIsVisible(getExampleEventId(), valueToEmit); 187 | }; 188 | addAndMakeVisible(emitJavaScriptEventButton); 189 | 190 | addAndMakeVisible(labelUpdatedFromJavaScript); 191 | 192 | gainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearBar); 193 | addAndMakeVisible(gainSlider); 194 | 195 | addAndMakeVisible(bypassButton); 196 | 197 | addAndMakeVisible(distortionTypeLabel); 198 | 199 | const auto& distortionTypeParameter = 200 | processorRef.getDistortionTypeParameter(); 201 | distortionTypeComboBox.addItemList(distortionTypeParameter.choices, 1); 202 | distortionTypeComboBox.setSelectedItemIndex( 203 | distortionTypeParameter.getIndex(), juce::dontSendNotification); 204 | addAndMakeVisible(distortionTypeComboBox); 205 | 206 | setResizable(true, true); 207 | setSize(800, 600); 208 | 209 | startTimer(60); 210 | } 211 | 212 | AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() {} 213 | 214 | void AudioPluginAudioProcessorEditor::resized() { 215 | auto bounds = getBounds(); 216 | webView.setBounds(bounds.removeFromRight(getWidth() / 2)); 217 | runJavaScriptButton.setBounds(bounds.removeFromTop(50).reduced(5)); 218 | emitJavaScriptEventButton.setBounds(bounds.removeFromTop(50).reduced(5)); 219 | labelUpdatedFromJavaScript.setBounds(bounds.removeFromTop(50).reduced(5)); 220 | gainSlider.setBounds(bounds.removeFromTop(50).reduced(5)); 221 | bypassButton.setBounds(bounds.removeFromTop(50).reduced(10)); 222 | distortionTypeLabel.setBounds(bounds.removeFromTop(50).reduced(5)); 223 | distortionTypeComboBox.setBounds(bounds.removeFromTop(50).reduced(5)); 224 | } 225 | 226 | void AudioPluginAudioProcessorEditor::timerCallback() { 227 | webView.emitEventIfBrowserIsVisible("outputLevel", juce::var{}); 228 | } 229 | 230 | auto AudioPluginAudioProcessorEditor::getResource(const juce::String& url) const 231 | -> std::optional { 232 | std::cout << "ResourceProvider called with " << url << std::endl; 233 | 234 | const auto resourceToRetrieve = 235 | url == "/" ? "index.html" : url.fromFirstOccurrenceOf("/", false, false); 236 | 237 | if (resourceToRetrieve == "outputLevel.json") { 238 | juce::DynamicObject::Ptr levelData{new juce::DynamicObject{}}; 239 | levelData->setProperty("left", processorRef.outputLevelLeft.load()); 240 | const auto jsonString = juce::JSON::toString(levelData.get()); 241 | juce::MemoryInputStream stream{jsonString.getCharPointer(), 242 | jsonString.getNumBytesAsUTF8(), false}; 243 | return juce::WebBrowserComponent::Resource{ 244 | streamToVector(stream), juce::String{"application/json"}}; 245 | } 246 | 247 | const auto resource = getWebViewFileAsBytes(resourceToRetrieve); 248 | if (!resource.empty()) { 249 | const auto extension = 250 | resourceToRetrieve.fromLastOccurrenceOf(".", false, false); 251 | return Resource{std::move(resource), getMimeForExtension(extension)}; 252 | } 253 | 254 | return std::nullopt; 255 | } 256 | 257 | void AudioPluginAudioProcessorEditor::nativeFunction( 258 | const juce::Array& args, 259 | juce::WebBrowserComponent::NativeFunctionCompletion completion) { 260 | using namespace std::views; 261 | juce::String concatenatedString; 262 | for (const auto& string : args | transform(&juce::var::toString)) { 263 | concatenatedString += string; 264 | } 265 | labelUpdatedFromJavaScript.setText( 266 | "Native function called with args: " + concatenatedString, 267 | juce::dontSendNotification); 268 | completion("nativeFunction callback: All OK!"); 269 | } 270 | } // namespace webview_plugin 271 | -------------------------------------------------------------------------------- /plugin/source/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | #include "JuceWebViewTutorial/PluginProcessor.h" 2 | #include 3 | #include "JuceWebViewTutorial/PluginEditor.h" 4 | #include "JuceWebViewTutorial/ParameterIDs.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace webview_plugin { 10 | AudioPluginAudioProcessor::AudioPluginAudioProcessor() 11 | : AudioProcessor( 12 | BusesProperties() 13 | #if !JucePlugin_IsMidiEffect 14 | #if !JucePlugin_IsSynth 15 | .withInput("Input", juce::AudioChannelSet::stereo(), true) 16 | #endif 17 | .withOutput("Output", juce::AudioChannelSet::stereo(), true) 18 | #endif 19 | ), 20 | state{*this, nullptr, "PARAMETERS", createParameterLayout(parameters)} { 21 | } 22 | 23 | AudioPluginAudioProcessor::~AudioPluginAudioProcessor() {} 24 | 25 | const juce::String AudioPluginAudioProcessor::getName() const { 26 | return JucePlugin_Name; 27 | } 28 | 29 | bool AudioPluginAudioProcessor::acceptsMidi() const { 30 | #if JucePlugin_WantsMidiInput 31 | return true; 32 | #else 33 | return false; 34 | #endif 35 | } 36 | 37 | bool AudioPluginAudioProcessor::producesMidi() const { 38 | #if JucePlugin_ProducesMidiOutput 39 | return true; 40 | #else 41 | return false; 42 | #endif 43 | } 44 | 45 | bool AudioPluginAudioProcessor::isMidiEffect() const { 46 | #if JucePlugin_IsMidiEffect 47 | return true; 48 | #else 49 | return false; 50 | #endif 51 | } 52 | 53 | double AudioPluginAudioProcessor::getTailLengthSeconds() const { 54 | return 0.0; 55 | } 56 | 57 | int AudioPluginAudioProcessor::getNumPrograms() { 58 | return 1; // NB: some hosts don't cope very well if you tell them there are 0 59 | // programs, so this should be at least 1, even if you're not 60 | // really implementing programs. 61 | } 62 | 63 | int AudioPluginAudioProcessor::getCurrentProgram() { 64 | return 0; 65 | } 66 | 67 | void AudioPluginAudioProcessor::setCurrentProgram(int index) { 68 | juce::ignoreUnused(index); 69 | } 70 | 71 | const juce::String AudioPluginAudioProcessor::getProgramName(int index) { 72 | juce::ignoreUnused(index); 73 | return {}; 74 | } 75 | 76 | void AudioPluginAudioProcessor::changeProgramName(int index, 77 | const juce::String& newName) { 78 | juce::ignoreUnused(index, newName); 79 | } 80 | 81 | void AudioPluginAudioProcessor::prepareToPlay(double sampleRate, 82 | int samplesPerBlock) { 83 | using namespace juce; 84 | 85 | envelopeFollower.prepare(dsp::ProcessSpec{ 86 | .sampleRate = sampleRate, 87 | .maximumBlockSize = static_cast(samplesPerBlock), 88 | .numChannels = static_cast(getTotalNumOutputChannels())}); 89 | envelopeFollower.setAttackTime(200.f); 90 | envelopeFollower.setReleaseTime(200.f); 91 | envelopeFollower.setLevelCalculationType( 92 | dsp::BallisticsFilter::LevelCalculationType::peak); 93 | 94 | envelopeFollowerOutputBuffer.setSize(getTotalNumOutputChannels(), 95 | samplesPerBlock); 96 | } 97 | 98 | void AudioPluginAudioProcessor::releaseResources() { 99 | // When playback stops, you can use this as an opportunity to free up any 100 | // spare memory, etc. 101 | } 102 | 103 | bool AudioPluginAudioProcessor::isBusesLayoutSupported( 104 | const BusesLayout& layouts) const { 105 | #if JucePlugin_IsMidiEffect 106 | juce::ignoreUnused(layouts); 107 | return true; 108 | #else 109 | // This is the place where you check if the layout is supported. 110 | // In this template code we only support mono or stereo. 111 | // Some plugin hosts, such as certain GarageBand versions, will only 112 | // load plugins that support stereo bus layouts. 113 | if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() && 114 | layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) 115 | return false; 116 | 117 | // This checks if the input layout matches the output layout 118 | #if !JucePlugin_IsSynth 119 | if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) 120 | return false; 121 | #endif 122 | 123 | return true; 124 | #endif 125 | } 126 | 127 | void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer& buffer, 128 | juce::MidiBuffer& midiMessages) { 129 | juce::ignoreUnused(midiMessages); 130 | 131 | juce::ScopedNoDenormals noDenormals; 132 | auto totalNumInputChannels = getTotalNumInputChannels(); 133 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 134 | 135 | // In case we have more outputs than inputs, this code clears any output 136 | // channels that didn't contain input data, (because these aren't 137 | // guaranteed to be empty - they may contain garbage). 138 | // This is here to avoid people getting screaming feedback 139 | // when they first compile a plugin, but obviously you don't need to keep 140 | // this code if your algorithm always overwrites all the output channels. 141 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 142 | buffer.clear(i, 0, buffer.getNumSamples()); 143 | 144 | if (parameters.bypass->get() || buffer.getNumSamples() == 0) { 145 | return; 146 | } 147 | 148 | juce::dsp::AudioBlock block{buffer}; 149 | if (parameters.distortionType->getIndex() == 1) { 150 | // tanh(kx)/tanh(k) 151 | juce::dsp::AudioBlock::process(block, block, [](float sample) { 152 | constexpr auto SATURATION = 5.f; 153 | static const auto normalizationFactor = std::tanh(SATURATION); 154 | sample = std::tanh(SATURATION * sample) / normalizationFactor; 155 | return sample; 156 | }); 157 | } else if (parameters.distortionType->getIndex() == 2) { 158 | // sigmoid 159 | juce::dsp::AudioBlock::process(block, block, [](float sample) { 160 | constexpr auto SATURATION = 5.f; 161 | sample = 2.f / (1.f + std::exp(-SATURATION * sample)) - 1.f; 162 | return sample; 163 | }); 164 | } 165 | 166 | buffer.applyGain(parameters.gain->get()); 167 | 168 | const auto inBlock = 169 | juce::dsp::AudioBlock{buffer}.getSubsetChannelBlock( 170 | 0u, static_cast(getTotalNumOutputChannels())); 171 | auto outBlock = 172 | juce::dsp::AudioBlock{envelopeFollowerOutputBuffer}.getSubBlock( 173 | 0u, static_cast(buffer.getNumSamples())); 174 | envelopeFollower.process( 175 | juce::dsp::ProcessContextNonReplacing{inBlock, outBlock}); 176 | outputLevelLeft = juce::Decibels::gainToDecibels( 177 | outBlock.getSample(0, static_cast(outBlock.getNumSamples()) - 1)); 178 | } 179 | 180 | bool AudioPluginAudioProcessor::hasEditor() const { 181 | return true; // (change this to false if you choose to not supply an editor) 182 | } 183 | 184 | juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor() { 185 | return new AudioPluginAudioProcessorEditor(*this); 186 | } 187 | 188 | void AudioPluginAudioProcessor::getStateInformation( 189 | juce::MemoryBlock& destData) { 190 | // You should use this method to store your parameters in the memory block. 191 | // You could do that either as raw data, or use the XML or ValueTree classes 192 | // as intermediaries to make it easy to save and load complex data. 193 | juce::ignoreUnused(destData); 194 | } 195 | 196 | void AudioPluginAudioProcessor::setStateInformation(const void* data, 197 | int sizeInBytes) { 198 | // You should use this method to restore your parameters from this memory 199 | // block, whose contents will have been created by the getStateInformation() 200 | // call. 201 | juce::ignoreUnused(data, sizeInBytes); 202 | } 203 | 204 | juce::AudioProcessorValueTreeState::ParameterLayout 205 | AudioPluginAudioProcessor::createParameterLayout( 206 | AudioPluginAudioProcessor::Parameters& parameters) { 207 | using namespace juce; 208 | AudioProcessorValueTreeState::ParameterLayout layout; 209 | 210 | { 211 | auto parameter = std::make_unique( 212 | id::GAIN, "gain", NormalisableRange{0.f, 1.f, 0.01f, 0.9f}, 1.f); 213 | parameters.gain = parameter.get(); 214 | layout.add(std::move(parameter)); 215 | } 216 | 217 | { 218 | auto parameter = std::make_unique( 219 | id::BYPASS, "bypass", false, 220 | AudioParameterBoolAttributes{}.withLabel("Bypass")); 221 | parameters.bypass = parameter.get(); 222 | layout.add(std::move(parameter)); 223 | } 224 | 225 | { 226 | auto parameter = std::make_unique( 227 | id::DISTORTION_TYPE, "distortion type", 228 | StringArray{"none", "tanh(kx)/tanh(k)", "sigmoid"}, 0); 229 | parameters.distortionType = parameter.get(); 230 | layout.add(std::move(parameter)); 231 | } 232 | 233 | return layout; 234 | } 235 | } // namespace webview_plugin 236 | 237 | // This creates new instances of the plugin. 238 | // This function definition must be in the global namespace. 239 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { 240 | return new webview_plugin::AudioPluginAudioProcessor(); 241 | } 242 | -------------------------------------------------------------------------------- /plugin/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JUCE 8 WebView Plugin Tutorial 8 | 9 | 10 |
11 |

Hello, World!

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
PropertyValue
Vendor
Plugin name
Plugin version
29 | 30 | 31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /plugin/ui/public/js/index.js: -------------------------------------------------------------------------------- 1 | import * as Juce from "./juce/index.js"; 2 | 3 | console.log("JUCE frontend library successfully imported"); 4 | 5 | window.__JUCE__.backend.addEventListener( 6 | "exampleEvent", 7 | (objectFromBackend) => { 8 | console.log(objectFromBackend); 9 | } 10 | ); 11 | 12 | const data = window.__JUCE__.initialisationData; 13 | 14 | document.getElementById("vendor").innerText = data.vendor; 15 | document.getElementById("pluginName").innerText = data.pluginName; 16 | document.getElementById("pluginVersion").innerText = data.pluginVersion; 17 | 18 | const nativeFunction = Juce.getNativeFunction("nativeFunction"); 19 | 20 | document.addEventListener("DOMContentLoaded", () => { 21 | const button = document.getElementById("nativeFunctionButton"); 22 | button.addEventListener("click", () => { 23 | nativeFunction("one", 2, null).then((result) => { 24 | console.log(result); 25 | }); 26 | }); 27 | 28 | const emitEventButton = document.getElementById("emitEventButton"); 29 | let emittedCount = 0; 30 | emitEventButton.addEventListener("click", () => { 31 | emittedCount++; 32 | window.__JUCE__.backend.emitEvent("exampleJavaScriptEvent", { 33 | emittedCount: emittedCount, 34 | }); 35 | }); 36 | 37 | const slider = document.getElementById("gainSlider"); 38 | const sliderState = Juce.getSliderState("GAIN"); 39 | slider.oninput = function () { 40 | sliderState.setNormalisedValue(this.value); 41 | }; 42 | 43 | slider.step = 1 / sliderState.properties.numSteps; 44 | 45 | sliderState.valueChangedEvent.addListener(() => { 46 | slider.value = sliderState.getNormalisedValue(); 47 | }); 48 | 49 | const bypassCheckbox = document.getElementById("bypassCheckbox"); 50 | const bypassToggleState = Juce.getToggleState("BYPASS"); 51 | bypassCheckbox.oninput = function () { 52 | bypassToggleState.setValue(this.checked); 53 | }; 54 | bypassToggleState.valueChangedEvent.addListener(() => { 55 | bypassCheckbox.checked = bypassToggleState.getValue(); 56 | }); 57 | 58 | const distortionTypeComboBox = document.getElementById( 59 | "distortionTypeComboBox" 60 | ); 61 | const distortionTypeComboBoxState = Juce.getComboBoxState( 62 | "DISTORTION_TYPE" 63 | ); 64 | distortionTypeComboBoxState.propertiesChangedEvent.addListener(() => { 65 | distortionTypeComboBox.innerHTML = ""; 66 | distortionTypeComboBoxState.properties.choices.forEach((choice) => { 67 | distortionTypeComboBox.innerHTML += ``; 68 | }); 69 | }); 70 | distortionTypeComboBoxState.valueChangedEvent.addListener(() => { 71 | distortionTypeComboBox.selectedIndex = 72 | distortionTypeComboBoxState.getChoiceIndex(); 73 | }); 74 | distortionTypeComboBox.oninput = function () { 75 | distortionTypeComboBoxState.setChoiceIndex(this.selectedIndex); 76 | }; 77 | 78 | // Plot with Plotly 79 | const base = -60; 80 | Plotly.newPlot("outputLevelPlot", { 81 | data: [ 82 | { 83 | x: ["left"], 84 | y: [base], 85 | base: [base], 86 | type: "bar", 87 | }, 88 | ], 89 | layout: { width: 200, height: 400, yaxis: { range: [-60, 0] } }, 90 | }); 91 | 92 | window.__JUCE__.backend.addEventListener("outputLevel", () => { 93 | fetch(Juce.getBackendResourceAddress("outputLevel.json")) 94 | .then((response) => response.text()) 95 | .then((outputLevel) => { 96 | const levelData = JSON.parse(outputLevel); 97 | Plotly.animate( 98 | "outputLevelPlot", 99 | { 100 | data: [ 101 | { 102 | y: [levelData.left - base], 103 | }, 104 | ], 105 | traces: [0], 106 | layout: {}, 107 | }, 108 | { 109 | transition: { 110 | duration: 20, 111 | easing: "cubic-in-out", 112 | }, 113 | frame: { 114 | duration: 20, 115 | }, 116 | } 117 | ); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /scripts/DownloadWebView2.ps1: -------------------------------------------------------------------------------- 1 | $packageSourceName = "nugetRepository" 2 | 3 | # Ignore errors: they are meaningless 4 | $ErrorActionPreference = "SilentlyContinue" 5 | 6 | Register-PackageSource -Provider NuGet -Name $packageSourceName -Location https://api.nuget.org/v3/index.json -Force 7 | Install-Package Microsoft.Web.WebView2 -Scope CurrentUser -RequiredVersion 1.0.1901.177 -Source $packageSourceName -Force 8 | 9 | --------------------------------------------------------------------------------