diff --git a/.clang-format b/.clang-format index e6a8760e2..3db7236c0 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,5 @@ --- +Language: Cpp AccessModifierOffset: -4 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false @@ -42,7 +43,6 @@ IndentWrappedFunctionNames: true IndentPPDirectives: BeforeHash KeepEmptyLinesAtTheStartOfBlocks: false LambdaBodyIndentation: Signature -Language: Cpp MaxEmptyLinesToKeep: 1 NamespaceIndentation: None PackConstructorInitializers: Never @@ -70,6 +70,7 @@ SpacesInSquareBrackets: false Standard: "c++17" TabWidth: 4 UseTab: Never +WhitespaceSensitiveMacros: ['EM_ASM', 'EM_JS', 'EM_ASM_INT', 'EM_ASM_DOUBLE', 'EM_ASM_PTR', 'MAIN_THREAD_EM_ASM', 'MAIN_THREAD_EM_ASM_INT', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_ASYNC_EM_ASM'] --- Language: ObjC BasedOnStyle: Chromium @@ -84,3 +85,10 @@ PointerAlignment: Left SpacesBeforeTrailingComments: 1 TabWidth: 4 UseTab: Never +--- +Language: JavaScript +BasedOnStyle: Chromium +ColumnLimit: 0 +IndentWidth: 4 +TabWidth: 4 +UseTab: Never diff --git a/README.md b/README.md index fc69cb73b..e7ec4904b 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,14 @@ UI courtesy from https://www.drywestdesign.com/: ![SeaSynth](./images/seasynth_prototype.png) -Example rive animations ([source code](./examples/render/source/main.cpp)): -[![Web Render Example 1](./images/web_render_1.png)](https://kunitoki.github.io/yup/demos/web_render_1/) -[![Web Render Example 2](./images/web_render_2.png)](https://kunitoki.github.io/yup/demos/web_render_2/) -[![Web Render Example 3](./images/web_render_3.png)](https://kunitoki.github.io/yup/demos/web_render_3/) - +

+ + + + +

+ +Example Rive animation display ([source code](./examples/render/source/main.cpp)): [Renderer Youtube Video](https://youtube.com/shorts/3XC4hyDlrVs) [![Build And Test MacOS](https://github.com/kunitoki/yup/actions/workflows/build_macos.yml/badge.svg)](https://github.com/kunitoki/yup/actions/workflows/build_macos.yml) @@ -33,14 +36,28 @@ YUP brings a suite of powerful features, including: ## Supported Rendering Backends | | **Windows** | **macOS** | **Linux** | **WASM** | **Android**(1) | **iOS**(1) | |--------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:-------------------------:|:---------------------:| -| **OpenGL 4.2** | :white_check_mark: | | :white_check_mark: | | :white_check_mark: | | -| **OpenGL ES2.0** | | | | :white_check_mark: | | | -| **Metal** | | :white_check_mark: | | | | :white_check_mark: | +| **OpenGL 4.2** | :white_check_mark: | | :white_check_mark: | | | | +| **OpenGL ES2.0** | | | | :white_check_mark: | :construction: | | +| **OpenGL ES3.0** | | | | | :construction: | | +| **Metal** | | :white_check_mark: | | | | :construction: | | **Direct3D 11** | :white_check_mark: | | | | | | -| **Vulkan**(2) | | | | | | | +| **Vulkan** | :construction: | :construction: | :construction: | | :construction: | | +| **WebGPU** | | | | | | | 1. Platforms not fully supported by the windowing system -2. Renderer currently work in progress + +## Supported Audio Backends +| | **Windows** | **macOS** | **Linux** | **WASM** | **Android** | **iOS** | +|--------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:-------------------------:|:---------------------:| +| **CoreAudio** | | :white_check_mark: | | | | :white_check_mark: | +| **ASIO** | :white_check_mark: | | | | | | +| **DirectSound** | :white_check_mark: | | | | | | +| **WASAPI** | :white_check_mark: | | | | | | +| **ALSA** | | | :white_check_mark: | | | | +| **JACK** | | | :white_check_mark: | | | | +| **Oboe** | | | | | :white_check_mark: | | +| **OpenSL** | | | | | :white_check_mark: | | +| **AudioWorklet** | | | | :white_check_mark: | | | ## Development > [!IMPORTANT] diff --git a/cmake/platforms/emscripten/mini-coi.js b/cmake/platforms/emscripten/mini-coi.js new file mode 100644 index 000000000..9fbfc47cc --- /dev/null +++ b/cmake/platforms/emscripten/mini-coi.js @@ -0,0 +1,28 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */ +(({ document: d, navigator: { serviceWorker: s } }) => { + if (d) { + const { currentScript: c } = d; + s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => { + r.addEventListener('updatefound', () => location.reload()); + if (r.active && !s.controller) location.reload(); + }); + } + else { + addEventListener('install', () => skipWaiting()); + addEventListener('activate', e => e.waitUntil(clients.claim())); + addEventListener('fetch', e => { + const { request: r } = e; + if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return; + e.respondWith(fetch(r).then(r => { + const { body, status, statusText } = r; + if (!status || status > 399) return r; + const h = new Headers(r.headers); + h.set('Cross-Origin-Opener-Policy', 'same-origin'); + h.set('Cross-Origin-Embedder-Policy', 'require-corp'); + h.set('Cross-Origin-Resource-Policy', 'cross-origin'); + return new Response(body, { status, statusText, headers: h }); + })); + }); + } + })(self); \ No newline at end of file diff --git a/cmake/platforms/emscripten/shell.html b/cmake/platforms/emscripten/shell.html index 680dec4b1..332f1fdef 100644 --- a/cmake/platforms/emscripten/shell.html +++ b/cmake/platforms/emscripten/shell.html @@ -2,9 +2,8 @@ + - - YUP! On Emscripten
Downloading...

Resize canvasLock/hide mouse pointer
\ No newline at end of file diff --git a/docs/demos/web_render_4/mini-coi.js b/docs/demos/web_render_4/mini-coi.js new file mode 100644 index 000000000..9fbfc47cc --- /dev/null +++ b/docs/demos/web_render_4/mini-coi.js @@ -0,0 +1,28 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */ +(({ document: d, navigator: { serviceWorker: s } }) => { + if (d) { + const { currentScript: c } = d; + s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => { + r.addEventListener('updatefound', () => location.reload()); + if (r.active && !s.controller) location.reload(); + }); + } + else { + addEventListener('install', () => skipWaiting()); + addEventListener('activate', e => e.waitUntil(clients.claim())); + addEventListener('fetch', e => { + const { request: r } = e; + if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return; + e.respondWith(fetch(r).then(r => { + const { body, status, statusText } = r; + if (!status || status > 399) return r; + const h = new Headers(r.headers); + h.set('Cross-Origin-Opener-Policy', 'same-origin'); + h.set('Cross-Origin-Embedder-Policy', 'require-corp'); + h.set('Cross-Origin-Resource-Policy', 'cross-origin'); + return new Response(body, { status, statusText, headers: h }); + })); + }); + } + })(self); \ No newline at end of file diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index 03a2e012a..5749a1eae 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -23,10 +23,16 @@ cmake_minimum_required(VERSION 3.28) set (target_name example_graphics) set (target_version "1.0.0") +set (link_options "") +if ("${yup_platform}" MATCHES "^(emscripten)$") + set (link_options --preload-file ${CMAKE_CURRENT_LIST_DIR}/data/Roboto-Regular.ttf@data/Roboto-Regular.ttf) +endif() + yup_standalone_app ( TARGET_NAME ${target_name} TARGET_VERSION ${target_version} TARGET_IDE_GROUP "Examples" + LINK_OPTIONS ${link_options} MODULES juce_core juce_events diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 501744c78..9e46e01e7 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -26,6 +26,51 @@ #include #include +#include // For sine wave generation + +//============================================================================== + +class SineWaveGenerator +{ +public: + SineWaveGenerator() + : sampleRate (44100.0) + , currentAngle (0.0) + , angleDelta (0.0) + , amplitude (0.0) + { + } + + void setFrequency (double frequency, double newSampleRate) + { + sampleRate = newSampleRate; + angleDelta = (juce::MathConstants::twoPi * frequency) / sampleRate; + + amplitude.reset (sampleRate, 0.1); + } + + void setAmplitude (float newAmplitude) + { + amplitude.setTargetValue (newAmplitude); + } + + float getNextSample() + { + auto sample = std::sin (currentAngle) * amplitude.getNextValue(); + + currentAngle += angleDelta; + if (currentAngle >= juce::MathConstants::twoPi) + currentAngle -= juce::MathConstants::twoPi; + + return static_cast (sample); + } + +private: + double sampleRate; + double currentAngle; + double angleDelta; + yup::SmoothedValue amplitude; +}; //============================================================================== @@ -47,28 +92,51 @@ class CustomWindow return; } + setTitle ("main"); + + // Load the font + yup::File fontFilePath = #if JUCE_WASM - yup::File dataPath = yup::File ("/data"); + yup::File ("/data") #else - yup::File dataPath = yup::File (__FILE__).getParentDirectory().getSiblingFile ("data"); + yup::File (__FILE__).getParentDirectory().getSiblingFile ("data") #endif - - yup::File fontFilePath = dataPath.getChildFile ("Roboto-Regular.ttf"); + .getChildFile ("Roboto-Regular.ttf"); if (auto result = font.loadFromFile (fontFilePath, factory); result.failed()) yup::Logger::outputDebugString (result.getErrorMessage()); - setTitle ("main"); + // Initialize the audio device + deviceManager.addAudioCallback (this); + deviceManager.initialiseWithDefaultDevices (1, 0); + + // Initialize sine wave generators + double sampleRate = deviceManager.getAudioDeviceSetup().sampleRate; + sineWaveGenerators.resize (totalRows * totalColumns); + for (size_t i = 0; i < sineWaveGenerators.size(); ++i) + { + sineWaveGenerators[i] = std::make_unique(); + sineWaveGenerators[i]->setFrequency(440.0 * std::pow(1.1, i), sampleRate); + } + + // Add sliders and buttons for (int i = 0; i < totalRows * totalColumns; ++i) - addAndMakeVisible (sliders.add (std::make_unique (yup::String (i), font))); + { + auto slider = sliders.add (std::make_unique (yup::String (i), font)); - //button = std::make_unique ("xyz", font); - //addAndMakeVisible (*button); + slider->onValueChanged = [this, i](float value) + { + sineWaveGenerators[i]->setAmplitude (value); + }; - deviceManager.addAudioCallback (this); - deviceManager.initialiseWithDefaultDevices (1, 0); + addAndMakeVisible (slider); + } + + //button = std::make_unique ("Randomize", font); + //addAndMakeVisible (*button); + // Start the timer startTimerHz (1); } @@ -174,6 +242,7 @@ class CustomWindow int numSamples, const yup::AudioIODeviceCallbackContext& context) override { + /* int copiedSamples = 0; while (copiedSamples < numSamples) { @@ -184,6 +253,19 @@ class CustomWindow readPos %= renderData.size(); } + */ + + for (int sample = 0; sample < numSamples; ++sample) + { + float mixedSample = 0.0f; + + for (int i = 0; i < sineWaveGenerators.size(); ++i) + mixedSample += sineWaveGenerators[i]->getNextSample(); + + for (int channel = 0; channel < numOutputChannels; ++channel) + outputChannelData[channel][sample] = + mixedSample / static_cast (sineWaveGenerators.size()); + } //inputReady.signal(); //renderReady.wait(); @@ -191,7 +273,7 @@ class CustomWindow void audioDeviceAboutToStart (yup::AudioIODevice* device) override { - DBG ("audioDeviceAboutToStart"); + yup::Logger::outputDebugString ("audioDeviceAboutToStart"); inputData.resize (device->getDefaultBufferSize()); renderData.resize (device->getDefaultBufferSize()); @@ -200,7 +282,7 @@ class CustomWindow void audioDeviceStopped() override { - DBG ("audioDeviceStopped"); + yup::Logger::outputDebugString ("audioDeviceStopped"); } private: @@ -232,6 +314,8 @@ class CustomWindow std::unique_ptr button; + std::vector> sineWaveGenerators; + yup::Font font; yup::StyledText styleText; }; @@ -244,7 +328,7 @@ struct Application : yup::YUPApplication const yup::String getApplicationName() override { - return "yup graphics!"; + return "yup! graphics"; } const yup::String getApplicationVersion() override diff --git a/examples/render/source/main.cpp b/examples/render/source/main.cpp index 92b25d61e..2ce8ce695 100644 --- a/examples/render/source/main.cpp +++ b/examples/render/source/main.cpp @@ -188,7 +188,7 @@ struct Application : yup::YUPApplication const yup::String getApplicationName() override { - return "yup!"; + return "yup! render"; } const yup::String getApplicationVersion() override @@ -198,6 +198,8 @@ struct Application : yup::YUPApplication void initialise (const yup::String& commandLineParameters) override { + YUP_PROFILE_START(); + yup::Logger::outputDebugString ("Starting app " + commandLineParameters); window = std::make_unique(); @@ -210,6 +212,8 @@ struct Application : yup::YUPApplication yup::Logger::outputDebugString ("Shutting down"); window.reset(); + + YUP_PROFILE_STOP(); } private: diff --git a/justfile b/justfile index 99cfe206f..3f9578b48 100644 --- a/justfile +++ b/justfile @@ -31,8 +31,9 @@ ios: emscripten: emcc -v emcmake cmake -G "Ninja Multi-Config" -B build - cmake --build build - python3 -m http.server -d build/examples/render/Debug + cmake --build build --config Debug + python3 -m http.server -d . + #python3 serve.py -p 8000 -d . #run: # @just make diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index 98aa82404..78e5cac42 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -268,6 +268,7 @@ void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_Oboe()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_OpenSLES()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_Android()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_AudioWorklet()); } void AudioDeviceManager::addAudioDeviceType (std::unique_ptr newDeviceType) diff --git a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp index e143e013d..ac3ec2bc6 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp @@ -219,4 +219,16 @@ AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Oboe() } #endif +#if JUCE_EMSCRIPTEN +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_AudioWorklet() +{ + return new AudioWorkletAudioIODeviceType(); +} +#else +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_AudioWorklet() +{ + return nullptr; +} +#endif + } // namespace juce diff --git a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h index 2d3d63c38..a60e2df77 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h +++ b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h @@ -183,6 +183,8 @@ class JUCE_API AudioIODeviceType static AudioIODeviceType* createAudioIODeviceType_Oboe(); /** Creates a Bela device type if it's available on this platform, or returns null. */ static AudioIODeviceType* createAudioIODeviceType_Bela(); + /** Creates a AudioWorklet device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_AudioWorklet(); #ifndef DOXYGEN [[deprecated ("You should call the method which takes a WASAPIDeviceMode instead.")]] static AudioIODeviceType* createAudioIODeviceType_WASAPI (bool exclusiveMode); diff --git a/modules/juce_audio_devices/juce_audio_devices.cpp b/modules/juce_audio_devices/juce_audio_devices.cpp index ac5cf32a8..74b3624e5 100644 --- a/modules/juce_audio_devices/juce_audio_devices.cpp +++ b/modules/juce_audio_devices/juce_audio_devices.cpp @@ -262,6 +262,13 @@ RealtimeThreadFactory getAndroidRealtimeThreadFactory() { return nullptr; } #endif #elif JUCE_WASM +#if JUCE_EMSCRIPTEN +#include +#include + +#include "native/juce_AudioWorklet_emscripten.cpp" +#endif + #include "native/juce_Midi_wasm.cpp" #endif diff --git a/modules/juce_audio_devices/native/juce_AudioWorklet_emscripten.cpp b/modules/juce_audio_devices/native/juce_AudioWorklet_emscripten.cpp new file mode 100644 index 000000000..f42db1f2e --- /dev/null +++ b/modules/juce_audio_devices/native/juce_AudioWorklet_emscripten.cpp @@ -0,0 +1,479 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + 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. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +namespace +{ + +template +using hasAudioSampleFrameSamplesPerChannel = decltype(T::samplesPerChannel); + +template +int getNumSamplesPerChannel (const T& frame) +{ + if constexpr (isDetected) + return frame.samplesPerChannel; + else + return 128; +} + +} // namespace + +//============================================================================== +class AudioWorkletAudioIODevice final : public AudioIODevice +{ +public: + AudioWorkletAudioIODevice() + : AudioIODevice (AudioWorkletAudioIODevice::audioWorkletTypeName, + AudioWorkletAudioIODevice::audioWorkletTypeName) + { + context = emscripten_create_audio_context (0); + } + + ~AudioWorkletAudioIODevice() + { + close(); + + emscripten_destroy_audio_context (context); + } + + //============================================================================== + StringArray getOutputChannelNames() override + { + StringArray result; + + for (int i = 1; i <= actualNumberOfOutputs; i++) + result.add ("Out #" + String (i)); + + return result; + } + + StringArray getInputChannelNames() override + { + StringArray result; + + for (int i = 1; i <= actualNumberOfInputs; i++) + result.add ("In #" + String (i)); + + return result; + } + + Array getAvailableSampleRates() override + { + return { getDefaultSampleRate() }; + } + + double getDefaultSampleRate() + { + int outputSampleRate = EM_ASM_INT ({ + return emscriptenGetAudioObject ($0).sampleRate; + }, context); + + if (outputSampleRate == 0) + outputSampleRate = 44100; + + return static_cast (outputSampleRate); + } + + Array getAvailableBufferSizes() override + { + return { getDefaultBufferSize() }; + } + + int getDefaultBufferSize() override + { + return 128; + } + + //============================================================================== + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, + int bufferSizeSamples) override + { + if (sampleRate != getDefaultSampleRate() || bufferSizeSamples != getDefaultBufferSize()) + { + lastError = "Browser audio outputs only support 44.1 kHz sample rate and 128 samples buffer size."; + return lastError; + } + + auto numIns = getNumContiguousSetBits (inputChannels); + auto numOuts = getNumContiguousSetBits (outputChannels); + actualNumberOfInputs = jmax (numIns, 1); + actualNumberOfOutputs = jmax (numOuts, 1); + actualSampleRate = sampleRate; + actualBufferSize = (uint32) bufferSizeSamples; + + channelInBuffer.calloc (actualNumberOfInputs); + channelOutBuffer.calloc (actualNumberOfOutputs); + + isDeviceOpen = true; + isRunning = false; + callback = nullptr; + underruns = 0; + + emscripten_start_wasm_audio_worklet_thread_async ( + context, audioThreadStack, sizeof (audioThreadStack), &audioThreadInitializedCallback, this); + + return {}; + } + + void close() override + { + stop(); + + if (isDeviceOpen) + { + EM_ASM ({ + emscriptenGetAudioObject ($0).disconnect(); + }, audioWorkletNode); + audioWorkletNode = {}; + + isDeviceOpen = false; + callback = nullptr; + underruns = 0; + + actualBufferSize = 0; + actualNumberOfInputs = 0; + actualNumberOfOutputs = 0; + actualSampleRate = 44100.0; + actualBufferSize = 128u; + + channelInBuffer.free(); + channelOutBuffer.free(); + } + } + + bool isOpen() override { return isDeviceOpen; } + + void start (AudioIODeviceCallback* newCallback) override + { + if (! isDeviceOpen) + return; + + if (isRunning) + { + if (newCallback != callback) + { + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + { + ScopedLock lock (callbackLock); + std::swap (callback, newCallback); + } + + if (newCallback != nullptr) + newCallback->audioDeviceStopped(); + } + } + else + { + callback = newCallback; + isRunning = emscripten_audio_context_state (context) == AUDIO_CONTEXT_STATE_RUNNING; + + if (! isRunning && hasBeenActivatedAlreadyByUser) + { + emscripten_resume_audio_context_sync (context); + isRunning = emscripten_audio_context_state (context) == AUDIO_CONTEXT_STATE_RUNNING; + } + + firstCallback = true; + + if (callback != nullptr) + { + if (isRunning) + callback->audioDeviceAboutToStart (this); + } + } + } + + void stop() override + { + AudioIODeviceCallback* oldCallback = nullptr; + + if (callback != nullptr) + { + ScopedLock lock (callbackLock); + std::swap (callback, oldCallback); + } + + isRunning = false; + + EM_ASM ({ + emscriptenGetAudioObject ($0).suspend(); + }, context); + + if (oldCallback != nullptr) + oldCallback->audioDeviceStopped(); + } + + bool isPlaying() override { return isRunning; } + + String getLastError() override { return lastError; } + + //============================================================================== + int getCurrentBufferSizeSamples() override { return (int) actualBufferSize; } + + double getCurrentSampleRate() override { return actualSampleRate; } + + int getCurrentBitDepth() override { return 16; } + + BigInteger getActiveOutputChannels() const override + { + BigInteger b; + b.setRange (0, actualNumberOfOutputs, true); + return b; + } + + BigInteger getActiveInputChannels() const override + { + BigInteger b; + b.setRange (0, actualNumberOfInputs, true); + return b; + } + + int getOutputLatencyInSamples() override + { + return 0; + } + + int getInputLatencyInSamples() override + { + return 0; + } + + int getXRunCount() const noexcept override { return underruns; } + + //============================================================================== + static const char* const audioWorkletTypeName; + +private: + //============================================================================== + + void audioThreadInitialized() + { + WebAudioWorkletProcessorCreateOptions opts = + { + .name = audioWorkletTypeName + }; + + emscripten_create_wasm_audio_worklet_processor_async ( + context, &opts, &audioWorkletProcessorCreatedCallback, this); + } + + void audioWorkletProcessorCreated() + { + int outputChannelCounts[1] = { actualNumberOfOutputs }; + EmscriptenAudioWorkletNodeCreateOptions options = + { + .numberOfInputs = actualNumberOfInputs, + .numberOfOutputs = 1, + .outputChannelCounts = outputChannelCounts + }; + + // Create node + audioWorkletNode = emscripten_create_wasm_audio_worklet_node ( + context, audioWorkletTypeName, &options, renderAudioCallback, this); + + // Connect it to audio context destination + // emscripten_audio_node_connect (audioWorkletNode, context, 0, 0); + EM_ASM ({ + emscriptenGetAudioObject ($0).connect (emscriptenGetAudioObject ($1).destination); + }, audioWorkletNode, context); + + emscripten_set_click_callback ("canvas", reinterpret_cast (this), 0, canvasClickCallback); + } + + EM_BOOL renderAudio (int numInputs, const AudioSampleFrame* inputs, + int numOutputs, AudioSampleFrame* outputs, + int numParams, const AudioParamFrame* params) + { + const int samplesPerChannel = [&] + { + if (numOutputs > 0) + return getNumSamplesPerChannel (outputs[0]); + + else if (numInputs > 0) + return getNumSamplesPerChannel (inputs[0]); + + return 128; + }(); + + // check for xruns + calculateXruns (samplesPerChannel); + + ScopedLock lock (callbackLock); + + if (callback != nullptr) + { + // Setup channelInBuffers + for (int ch = 0; ch < actualNumberOfInputs; ++ch) + channelInBuffer[ch] = &(inputs[ch].data[0]); + + // Setup channelOutBuffers (assume a single worklet output) + for (int ch = 0; ch < actualNumberOfOutputs; ++ch) + channelOutBuffer[ch] = &(outputs[0].data[ch * samplesPerChannel]); + + callback->audioDeviceIOCallbackWithContext (channelInBuffer.getData(), + actualNumberOfInputs, + channelOutBuffer.getData(), + actualNumberOfOutputs, + samplesPerChannel, + {}); + + audioFramesElapsed += samplesPerChannel; + } + + return EM_TRUE; // keep going ! + } + + void canvasClick() + { + if (emscripten_audio_context_state (context) != AUDIO_CONTEXT_STATE_RUNNING) + { + emscripten_resume_audio_context_sync (context); + + isRunning = true; + hasBeenActivatedAlreadyByUser = true; + + ScopedLock lock (callbackLock); + + if (callback != nullptr) + callback->audioDeviceAboutToStart (this); + } + } + + //============================================================================== + + static void audioThreadInitializedCallback (EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void* userData) + { + if (! success) return; // Check browser console in a debug build for detailed errors + + static_cast (userData)->audioThreadInitialized(); + } + + static void audioWorkletProcessorCreatedCallback (EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void* userData) + { + if (! success) return; // Check browser console in a debug build for detailed errors + + static_cast (userData)->audioWorkletProcessorCreated(); + } + + static EM_BOOL renderAudioCallback (int numInputs, const AudioSampleFrame* inputs, + int numOutputs, AudioSampleFrame* outputs, + int numParams, const AudioParamFrame* params, + void* userData) + { + return static_cast (userData)->renderAudio (numInputs, inputs, numOutputs, outputs, numParams, params); + } + + static EM_BOOL canvasClickCallback (int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) + { + static_cast (userData)->canvasClick(); + + return EM_FALSE; + } + + //============================================================================== + uint64_t expectedElapsedAudioSamples = 0; + uint64_t audioFramesElapsed = 0; + int underruns = 0; + bool firstCallback = false; + + void calculateXruns (uint32_t numSamples) + { + if (audioFramesElapsed > expectedElapsedAudioSamples && ! firstCallback) + ++underruns; + + firstCallback = false; + expectedElapsedAudioSamples = audioFramesElapsed + numSamples; + } + + //============================================================================== + static int getNumContiguousSetBits (const BigInteger& value) noexcept + { + int bit = 0; + + while (value[bit]) + ++bit; + + return bit; + } + + //============================================================================== + EMSCRIPTEN_WEBAUDIO_T context{}; + EMSCRIPTEN_AUDIO_WORKLET_NODE_T audioWorkletNode{}; + + bool isDeviceOpen = false; + bool isRunning = false; + bool hasBeenActivatedAlreadyByUser = false; + + CriticalSection callbackLock; + AudioIODeviceCallback* callback = nullptr; + + String lastError; + uint32 actualBufferSize = 0; + int actualNumberOfInputs = 0, actualNumberOfOutputs = 0; + double actualSampleRate = 44100.0; + + HeapBlock channelInBuffer; + HeapBlock channelOutBuffer; + + alignas(16) uint8 audioThreadStack[4096] = {}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioWorkletAudioIODevice) +}; + +const char* const AudioWorkletAudioIODevice::audioWorkletTypeName = "Audio Worklet"; + +//============================================================================== +struct AudioWorkletAudioIODeviceType final : public AudioIODeviceType +{ + AudioWorkletAudioIODeviceType() + : AudioIODeviceType ("AudioWorklet") + { + } + + StringArray getDeviceNames (bool) const override { return StringArray (AudioWorkletAudioIODevice::audioWorkletTypeName); } + + void scanForDevices() override {} + + int getDefaultDeviceIndex (bool) const override { return 0; } + + int getIndexOfDevice (AudioIODevice* device, bool) const override { return device != nullptr ? 0 : -1; } + + bool hasSeparateInputsAndOutputs() const override { return false; } + + AudioIODevice* createDevice (const String& outputName, const String& inputName) override + { + if (outputName == AudioWorkletAudioIODevice::audioWorkletTypeName || inputName == AudioWorkletAudioIODevice::audioWorkletTypeName) + return new AudioWorkletAudioIODevice(); + + return nullptr; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioWorkletAudioIODeviceType) +}; + +} // namespace juce diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index dbccc194b..14be4503b 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -102,6 +102,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC #if JUCE_EMSCRIPTEN #include +#include #endif #if JUCE_LINUX || JUCE_BSD @@ -290,8 +291,10 @@ extern char** environ; //============================================================================== #elif JUCE_WASM +#include "native/juce_WebAssemblyHelpers.h" #include "native/juce_SystemStats_wasm.cpp" #include "native/juce_Files_wasm.cpp" +#include "native/juce_Network_wasm.cpp" #include "native/juce_Threads_wasm.cpp" #include "native/juce_PlatformTimer_generic.cpp" #endif diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index 5f42d8621..8aef38fd8 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -57,7 +57,7 @@ dependencies: zlib osxFrameworks: Cocoa Foundation IOKit Security - iosFrameworks: Foundation + iosFrameworks: Foundation UIKit linuxLibs: rt dl pthread mingwLibs: ws2_32 uuid wininet version kernel32 user32 wsock32 advapi32 ole32 oleaut32 imm32 comdlg32 shlwapi rpcrt4 winmm diff --git a/modules/juce_core/misc/juce_MetaProgramming.h b/modules/juce_core/misc/juce_MetaProgramming.h index 2eaea6f72..8ab6c32a6 100644 --- a/modules/juce_core/misc/juce_MetaProgramming.h +++ b/modules/juce_core/misc/juce_MetaProgramming.h @@ -22,10 +22,67 @@ namespace juce { +//============================================================================== template inline constexpr bool dependentBoolValue = Value; template inline constexpr bool dependentFalse = dependentBoolValue; +//============================================================================== +struct NoneSuch +{ + NoneSuch() = delete; + ~NoneSuch() = delete; + NoneSuch(NoneSuch const&) = delete; + void operator=(NoneSuch const&) = delete; + NoneSuch(NoneSuch&&) = delete; + void operator=(NoneSuch&&) = delete; +}; + +namespace detail { +template class Op, class... Args> +struct Detector +{ + using ValueType = std::false_type; + using Type = Default; +}; + +template class Op, class... Args> +struct Detector>, Op, Args...> +{ + using ValueType = std::true_type; + using Type = Op; +}; + +template