diff --git a/Resources/oscilloscope/juce/check_native_interop.js b/Resources/oscilloscope/juce/check_native_interop.js new file mode 100644 index 0000000..6e27088 --- /dev/null +++ b/Resources/oscilloscope/juce/check_native_interop.js @@ -0,0 +1,146 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +if ( + typeof window.__JUCE__ !== "undefined" && + typeof window.__JUCE__.getAndroidUserScripts !== "undefined" && + typeof window.inAndroidUserScriptEval === "undefined" +) { + window.inAndroidUserScriptEval = true; + eval(window.__JUCE__.getAndroidUserScripts()); + delete window.inAndroidUserScriptEval; +} + +{ + if (typeof window.__JUCE__ === "undefined") { + console.warn( + "The 'window.__JUCE__' object is undefined." + + " Native integration features will not work." + + " Defining a placeholder 'window.__JUCE__' object." + ); + + window.__JUCE__ = { + postMessage: function () {}, + }; + } + + if (typeof window.__JUCE__.initialisationData === "undefined") { + window.__JUCE__.initialisationData = { + __juce__platform: [], + __juce__functions: [], + __juce__registeredGlobalEventIds: [], + __juce__sliders: [], + __juce__toggles: [], + __juce__comboBoxes: [], + }; + } + + class ListenerList { + constructor() { + this.listeners = new Map(); + this.listenerId = 0; + } + + addListener(fn) { + const newListenerId = this.listenerId++; + this.listeners.set(newListenerId, fn); + return newListenerId; + } + + removeListener(id) { + if (this.listeners.has(id)) { + this.listeners.delete(id); + } + } + + callListeners(payload) { + for (const [, value] of this.listeners) { + value(payload); + } + } + } + + class EventListenerList { + constructor() { + this.eventListeners = new Map(); + } + + addEventListener(eventId, fn) { + if (!this.eventListeners.has(eventId)) + this.eventListeners.set(eventId, new ListenerList()); + + const id = this.eventListeners.get(eventId).addListener(fn); + + return [eventId, id]; + } + + removeEventListener([eventId, id]) { + if (this.eventListeners.has(eventId)) { + this.eventListeners.get(eventId).removeListener(id); + } + } + + emitEvent(eventId, object) { + if (this.eventListeners.has(eventId)) + this.eventListeners.get(eventId).callListeners(object); + } + } + + class Backend { + constructor() { + this.listeners = new EventListenerList(); + } + + addEventListener(eventId, fn) { + return this.listeners.addEventListener(eventId, fn); + } + + removeEventListener([eventId, id]) { + this.listeners.removeEventListener(eventId, id); + } + + emitEvent(eventId, object) { + window.__JUCE__.postMessage( + JSON.stringify({ eventId: eventId, payload: object }) + ); + } + + emitByBackend(eventId, object) { + this.listeners.emitEvent(eventId, JSON.parse(object)); + } + } + + if (typeof window.__JUCE__.backend === "undefined") + window.__JUCE__.backend = new Backend(); +} diff --git a/Resources/oscilloscope/juce/index.js b/Resources/oscilloscope/juce/index.js new file mode 100644 index 0000000..ddeaea8 --- /dev/null +++ b/Resources/oscilloscope/juce/index.js @@ -0,0 +1,492 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +import "./check_native_interop.js"; + +class PromiseHandler { + constructor() { + this.lastPromiseId = 0; + this.promises = new Map(); + + window.__JUCE__.backend.addEventListener( + "__juce__complete", + ({ promiseId, result }) => { + if (this.promises.has(promiseId)) { + this.promises.get(promiseId).resolve(result); + this.promises.delete(promiseId); + } + } + ); + } + + createPromise() { + const promiseId = this.lastPromiseId++; + const result = new Promise((resolve, reject) => { + this.promises.set(promiseId, { resolve: resolve, reject: reject }); + }); + return [promiseId, result]; + } +} + +const promiseHandler = new PromiseHandler(); + +/** + * Returns a function object that calls a function registered on the JUCE backend and forwards all + * parameters to it. + * + * The provided name should be the same as the name argument passed to + * WebBrowserComponent::Options.withNativeFunction() on the backend. + * + * @param {String} name + */ +function getNativeFunction(name) { + if (!window.__JUCE__.initialisationData.__juce__functions.includes(name)) + console.warn( + `Creating native function binding for '${name}', which is unknown to the backend` + ); + + const f = function () { + const [promiseId, result] = promiseHandler.createPromise(); + + window.__JUCE__.backend.emitEvent("__juce__invoke", { + name: name, + params: Array.prototype.slice.call(arguments), + resultId: promiseId, + }); + + return result; + }; + + return f; +} + +//============================================================================== + +class ListenerList { + constructor() { + this.listeners = new Map(); + this.listenerId = 0; + } + + addListener(fn) { + const newListenerId = this.listenerId++; + this.listeners.set(newListenerId, fn); + return newListenerId; + } + + removeListener(id) { + if (this.listeners.has(id)) { + this.listeners.delete(id); + } + } + + callListeners(payload) { + for (const [, value] of this.listeners) { + value(payload); + } + } +} + +const BasicControl_valueChangedEventId = "valueChanged"; +const BasicControl_propertiesChangedId = "propertiesChanged"; + +class SliderState { + constructor(name) { + if (!window.__JUCE__.initialisationData.__juce__sliders.includes(name)) + console.warn( + "Creating SliderState for '" + + name + + "', which is unknown to the backend" + ); + + this.name = name; + this.identifier = "__juce__slider" + this.name; + this.scaledValue = 0; + this.properties = { + start: 0, + end: 1, + skew: 1, + name: "", + label: "", + numSteps: 100, + interval: 0, + parameterIndex: -1, + }; + this.valueChangedEvent = new ListenerList(); + this.propertiesChangedEvent = new ListenerList(); + + window.__JUCE__.backend.addEventListener(this.identifier, (event) => + this.handleEvent(event) + ); + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: "requestInitialUpdate", + }); + } + + setNormalisedValue(newValue) { + this.scaledValue = this.snapToLegalValue( + this.normalisedToScaledValue(newValue) + ); + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: BasicControl_valueChangedEventId, + value: this.scaledValue, + }); + } + + sliderDragStarted() {} + + sliderDragEnded() {} + + handleEvent(event) { + if (event.eventType == BasicControl_valueChangedEventId) { + this.scaledValue = event.value; + this.valueChangedEvent.callListeners(); + } + if (event.eventType == BasicControl_propertiesChangedId) { + // eslint-disable-next-line no-unused-vars + let { eventType: _, ...rest } = event; + this.properties = rest; + this.propertiesChangedEvent.callListeners(); + } + } + + getScaledValue() { + return this.scaledValue; + } + + getNormalisedValue() { + return Math.pow( + (this.scaledValue - this.properties.start) / + (this.properties.end - this.properties.start), + this.properties.skew + ); + } + + normalisedToScaledValue(normalisedValue) { + return ( + Math.pow(normalisedValue, 1 / this.properties.skew) * + (this.properties.end - this.properties.start) + + this.properties.start + ); + } + + snapToLegalValue(value) { + const interval = this.properties.interval; + + if (interval == 0) return value; + + const start = this.properties.start; + const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val)); + + return clamp( + start + interval * Math.floor((value - start) / interval + 0.5), + this.properties.start, + this.properties.end + ); + } +} + +const sliderStates = new Map(); + +for (const sliderName of window.__JUCE__.initialisationData.__juce__sliders) + sliderStates.set(sliderName, new SliderState(sliderName)); + +/** + * Returns a SliderState object that is connected to the backend WebSliderRelay object that was + * created with the same name argument. + * + * To register a WebSliderRelay object create one with the right name and add it to the + * WebBrowserComponent::Options struct using withOptionsFrom. + * + * @param {String} name + */ +function getSliderState(name) { + if (!sliderStates.has(name)) sliderStates.set(name, new SliderState(name)); + + return sliderStates.get(name); +} + +class ToggleState { + constructor(name) { + if (!window.__JUCE__.initialisationData.__juce__toggles.includes(name)) + console.warn( + "Creating ToggleState for '" + + name + + "', which is unknown to the backend" + ); + + this.name = name; + this.identifier = "__juce__toggle" + this.name; + this.value = false; + this.properties = { + name: "", + parameterIndex: -1, + }; + this.valueChangedEvent = new ListenerList(); + this.propertiesChangedEvent = new ListenerList(); + + window.__JUCE__.backend.addEventListener(this.identifier, (event) => + this.handleEvent(event) + ); + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: "requestInitialUpdate", + }); + } + + getValue() { + return this.value; + } + + setValue(newValue) { + this.value = newValue; + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: BasicControl_valueChangedEventId, + value: this.value, + }); + } + + handleEvent(event) { + if (event.eventType == BasicControl_valueChangedEventId) { + this.value = event.value; + this.valueChangedEvent.callListeners(); + } + if (event.eventType == BasicControl_propertiesChangedId) { + // eslint-disable-next-line no-unused-vars + let { eventType: _, ...rest } = event; + this.properties = rest; + this.propertiesChangedEvent.callListeners(); + } + } +} + +const toggleStates = new Map(); + +for (const name of window.__JUCE__.initialisationData.__juce__toggles) + toggleStates.set(name, new ToggleState(name)); + +/** + * Returns a ToggleState object that is connected to the backend WebToggleButtonRelay object that was + * created with the same name argument. + * + * To register a WebToggleButtonRelay object create one with the right name and add it to the + * WebBrowserComponent::Options struct using withOptionsFrom. + * + * @param {String} name + */ +function getToggleState(name) { + if (!toggleStates.has(name)) toggleStates.set(name, new ToggleState(name)); + + return toggleStates.get(name); +} + +class ComboBoxState { + constructor(name) { + if (!window.__JUCE__.initialisationData.__juce__comboBoxes.includes(name)) + console.warn( + "Creating ComboBoxState for '" + + name + + "', which is unknown to the backend" + ); + + this.name = name; + this.identifier = "__juce__comboBox" + this.name; + this.value = 0.0; + this.properties = { + name: "", + parameterIndex: -1, + choices: [], + }; + this.valueChangedEvent = new ListenerList(); + this.propertiesChangedEvent = new ListenerList(); + + window.__JUCE__.backend.addEventListener(this.identifier, (event) => + this.handleEvent(event) + ); + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: "requestInitialUpdate", + }); + } + + getChoiceIndex() { + return Math.round(this.value * (this.properties.choices.length - 1)); + } + + setChoiceIndex(index) { + const numItems = this.properties.choices.length; + this.value = numItems > 1 ? index / (numItems - 1) : 0.0; + + window.__JUCE__.backend.emitEvent(this.identifier, { + eventType: BasicControl_valueChangedEventId, + value: this.value, + }); + } + + handleEvent(event) { + if (event.eventType == BasicControl_valueChangedEventId) { + this.value = event.value; + this.valueChangedEvent.callListeners(); + } + if (event.eventType == BasicControl_propertiesChangedId) { + // eslint-disable-next-line no-unused-vars + let { eventType: _, ...rest } = event; + this.properties = rest; + this.propertiesChangedEvent.callListeners(); + } + } +} + +const comboBoxStates = new Map(); + +for (const name of window.__JUCE__.initialisationData.__juce__comboBoxes) + comboBoxStates.set(name, new ComboBoxState(name)); + +/** + * Returns a ComboBoxState object that is connected to the backend WebComboBoxRelay object that was + * created with the same name argument. + * + * To register a WebComboBoxRelay object create one with the right name and add it to the + * WebBrowserComponent::Options struct using withOptionsFrom. + * + * @param {String} name + */ +function getComboBoxState(name) { + if (!comboBoxStates.has(name)) + comboBoxStates.set(name, new ComboBoxState(name)); + + return comboBoxStates.get(name); +} + +/** + * Appends a platform-specific prefix to the path to ensure that a request sent to this address will + * be received by the backend's ResourceProvider. + * @param {String} path + */ +function getBackendResourceAddress(path) { + const platform = + window.__JUCE__.initialisationData.__juce__platform.length > 0 + ? window.__JUCE__.initialisationData.__juce__platform[0] + : ""; + + if (platform == "windows" || platform == "android") + return "https://juce.backend/" + path; + + if (platform == "macos" || platform == "ios" || platform == "linux") + return "juce://juce.backend/" + path; + + console.warn( + "getBackendResourceAddress() called, but no JUCE native backend is detected." + ); + return path; +} + +/** + * This helper class is intended to aid the implementation of + * AudioProcessorEditor::getControlParameterIndex() for editors using a WebView interface. + * + * Create an instance of this class and call its handleMouseMove() method in each mousemove event. + * + * This class can be used to continuously report the controlParameterIndexAnnotation attribute's + * value related to the DOM element that is currently under the mouse pointer. + * + * This value is defined at all times as follows + * * the annotation attribute's value for the DOM element directly under the mouse, if it has it, + * * the annotation attribute's value for the first parent element, that has it, + * * -1 otherwise. + * + * Whenever there is a change in this value, an event is emitted to the frontend with the new value. + * You can use a ControlParameterIndexReceiver object on the backend to listen to these events. + * + * @param {String} controlParameterIndexAnnotation + */ +class ControlParameterIndexUpdater { + constructor(controlParameterIndexAnnotation) { + this.controlParameterIndexAnnotation = controlParameterIndexAnnotation; + this.lastElement = null; + this.lastControlParameterIndex = null; + } + + handleMouseMove(event) { + const currentElement = document.elementFromPoint( + event.clientX, + event.clientY + ); + + if (currentElement === this.lastElement) return; + this.lastElement = currentElement; + + let controlParameterIndex = -1; + + if (currentElement !== null) + controlParameterIndex = this.#getControlParameterIndex(currentElement); + + if (controlParameterIndex === this.lastControlParameterIndex) return; + this.lastControlParameterIndex = controlParameterIndex; + + window.__JUCE__.backend.emitEvent( + "__juce__controlParameterIndexChanged", + controlParameterIndex + ); + } + + //============================================================================== + #getControlParameterIndex(element) { + const isValidNonRootElement = (e) => { + return e !== null && e !== document.documentElement; + }; + + while (isValidNonRootElement(element)) { + if (element.hasAttribute(this.controlParameterIndexAnnotation)) { + return element.getAttribute(this.controlParameterIndexAnnotation); + } + + element = element.parentElement; + } + + return -1; + } +} + +export { + getNativeFunction, + getSliderState, + getToggleState, + getComboBoxState, + getBackendResourceAddress, + ControlParameterIndexUpdater, +}; diff --git a/Resources/oscilloscope/juce/package.json b/Resources/oscilloscope/juce/package.json new file mode 100644 index 0000000..49409b4 --- /dev/null +++ b/Resources/oscilloscope/juce/package.json @@ -0,0 +1,4 @@ +{ + "name": "juce-framework-frontend", + "version": "7.0.7" +} diff --git a/Resources/oscilloscope/oscilloscope.html b/Resources/oscilloscope/oscilloscope.html index ffd3015..769f1d8 100644 --- a/Resources/oscilloscope/oscilloscope.html +++ b/Resources/oscilloscope/oscilloscope.html @@ -1,72 +1,146 @@
+ + +- - [CLICK TO START] - |
@@ -659,4 +733,4 @@ var Controls = {
}
-
+
diff --git a/Resources/oscilloscope/oscilloscope.js b/Resources/oscilloscope/oscilloscope.js
index 7c2343b..f426282 100644
--- a/Resources/oscilloscope/oscilloscope.js
+++ b/Resources/oscilloscope/oscilloscope.js
@@ -1,3 +1,4 @@
+import * as Juce from "./index.js";
var AudioSystem =
{
@@ -5,160 +6,19 @@ var AudioSystem =
init : function (bufferSize)
{
- window.AudioContext = window.AudioContext||window.webkitAudioContext;
- this.audioContext = new window.AudioContext();
- this.sampleRate = this.audioContext.sampleRate;
+ this.sampleRate = 96000;
this.bufferSize = bufferSize;
this.timePerSample = 1/this.sampleRate;
this.oldXSamples = new Float32Array(this.bufferSize);
this.oldYSamples = new Float32Array(this.bufferSize);
this.smoothedXSamples = new Float32Array(Filter.nSmoothedSamples);
this.smoothedYSamples = new Float32Array(Filter.nSmoothedSamples);
-
- if (!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia))
- {
- microphoneOutput.value = " unavailable in this browser";
- }
},
startSound : function()
{
- var audioElement = document.getElementById("audioElement");
- this.source = this.audioContext.createMediaElementSource(audioElement);
- this.audioVolumeNode = this.audioContext.createGain();
-
- this.generator = this.audioContext.createScriptProcessor(this.bufferSize, 0, 2);
- this.generator.onaudioprocess = SignalGenerator.generate;
-
- this.scopeNode = this.audioContext.createScriptProcessor(this.bufferSize, 2, 2);
- this.scopeNode.onaudioprocess = doScriptProcessor;
- this.source.connect(this.scopeNode);
- this.generator.connect(this.scopeNode);
-
- this.scopeNode.connect(this.audioVolumeNode);
- this.audioVolumeNode.connect(this.audioContext.destination);
- },
-
- tryToGetMicrophone : function()
- {
- if (this.microphoneActive)
- {
- AudioSystem.microphone.connect(AudioSystem.scopeNode);
- audioVolume.value = 0.0;
- audioVolume.oninput();
- return;
- }
-
- var constraints = {audio: { mandatory: { echoCancellation: false }}};
- //var constraints = {audio: {echoCancellation: false} };
- navigator.getUserMedia = navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia;
- if (navigator.getUserMedia)
- {
- navigator.getUserMedia(constraints, onStream, function(){micCheckbox.checked = false;});
- }
- else
- {
- micCheckbox.checked = false;
- }
- },
-
- disconnectMicrophone : function()
- {
- if (this.microphone) this.microphone.disconnect();
- }
-}
-
-
-
-onStream = function(stream)
-{
- AudioSystem.microphoneActive = true;
- AudioSystem.microphone = AudioSystem.audioContext.createMediaStreamSource(stream);
- AudioSystem.microphone.connect(AudioSystem.scopeNode);
-
- audioVolume.value = 0.0;
- audioVolume.oninput();
-};
-
-var SignalGenerator =
-{
- oldA : 1.0,
- oldB : 1.0,
- timeInSamples : 0,
-
- generate : function(event)
- {
- var xOut = event.outputBuffer.getChannelData(0);
- var yOut = event.outputBuffer.getChannelData(1);
- var newA = controls.aValue * Math.pow(10.0, controls.aExponent);
- var newB = controls.bValue * Math.pow(10.0, controls.bExponent);
- var oldA = SignalGenerator.oldA;
- var oldB = SignalGenerator.oldB;
- var PI = Math.PI;
- var cos = Math.cos;
- var sin = Math.sin;
- var xFunc = eval("(function xFunc(){return "+controls.xExpression+";})");
- var yFunc = eval("(function yFunc(){return "+controls.yExpression+";})");
- var bufferSize = AudioSystem.bufferSize;
- var timeInSamples = SignalGenerator.timeInSamples;
- var sampleRate = AudioSystem.sampleRate;
- var x = 0.0;
- var y = 0.0;
- if (!controls.signalGeneratorOn)
- {
- for (var i=0; i |