Add support for recording oscilloscope visualiser

pull/263/head
James H Ball 2024-10-12 19:17:35 +01:00
rodzic 925f2a7b80
commit f30ac1823e
9 zmienionych plików z 586 dodań i 440 usunięć

Wyświetl plik

@ -2,7 +2,7 @@
<!DOCTYPE html>
<head>
<style>
<style>
body {
font-family: Sans-Serif;
font-size: 14px;
@ -73,6 +73,10 @@
filter: brightness(50%);
}
#download {
background: url(download.svg) no-repeat;
}
#fullscreen {
background: url(fullscreen.svg) no-repeat;
}
@ -84,18 +88,19 @@
#settings {
background: url(cog.svg) no-repeat;
}
</style>
</style>
</head>
<body bgcolor="black" text="white" autocomplete="off" style="margin: 0px;">
<div id="buttonRow">
<div id="buttonRow">
<button onClick="toggleRecording()" id="download"/>
<button id="fullscreen"/>
<button id="popout"/>
<button id="settings"/>
</div>
</div>
<script>
<script>
var controls=
{
swapXY : false,
@ -132,10 +137,38 @@
let openInAnotherWindow = false;
let externalSampleRate = 96000;
let externalBufferSize = 1920;
let recording = false;
let mediaRecorder = undefined;
let downloadCallback = undefined;
</script>
const toggleRecording = () => {
recording = !recording;
if (recording) {
const canvas = document.getElementById("crtCanvas");
const data = [];
const stream = canvas.captureStream(60);
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => data.push(e.data);
mediaRecorder.onstop = (e) => {
const div = document.getElementById("buttonRow");
var a = document.createElement("a");
const video = new Blob(data, { type: "video/webm;codecs=h264" });
var reader = new FileReader();
reader.readAsDataURL(video);
reader.onloadend = function() {
var dataUrl = reader.result;
var base64 = dataUrl.split(',')[1];
downloadCallback(base64);
}
};
mediaRecorder.start();
} else {
mediaRecorder.stop();
}
};
</script>
<script type="module">
<script type="module">
import * as Juce from "./index.js";
const fullscreen = document.getElementById('fullscreen');
@ -201,37 +234,45 @@
}
});
window.__JUCE__.backend.addEventListener("toggleRecording", hasChild => {
toggleRecording();
});
document.addEventListener("dblclick", function() {
toggleFullscreen();
});
</script>
<div id="mainScreen">
downloadCallback = (base64) => {
Juce.getNativeFunction("downloadVideo")(base64);
};
</script>
<div id="mainScreen">
<div id="overlay">Paused</div>
<canvas id="crtCanvas" width="800" height="800"></canvas>
</div>
</div>
<script id="vertex" type="x-shader">
<script id="vertex" type="x-shader">
attribute vec2 vertexPosition;
void main()
{
gl_Position = vec4(vertexPosition, 0.0, 1.0);
}
</script>
</script>
<script id="fragment" type="x-shader">
<script id="fragment" type="x-shader">
precision highp float;
uniform vec4 colour;
void main()
{
gl_FragColor = colour;
}
</script>
</script>
<!-- The Gaussian line-drawing code, the next two shaders, is adapted
<!-- The Gaussian line-drawing code, the next two shaders, is adapted
from woscope by e1ml : https://github.com/m1el/woscope -->
<script id="gaussianVertex" type="x-shader">
<script id="gaussianVertex" type="x-shader">
#define EPS 1E-6
uniform float uInvert;
uniform float uSize;
@ -298,9 +339,9 @@
//seed = mod(sin(seed*seed), 7.0);
//if (mod(seed/2.0, 1.0)<0.5) gl_Position = vec4(10.0);
}
</script>
</script>
<script id="gaussianFragment" type="x-shader">
<script id="gaussianFragment" type="x-shader">
#define EPS 1E-6
#define TAU 6.283185307179586
#define TAUR 2.5066282746310002
@ -351,9 +392,9 @@
gl_FragColor = 2.0 * texture2D(uScreen, vTexCoord) * brightness;
gl_FragColor.a = 1.0;
}
</script>
</script>
<script id="texturedVertex" type="x-shader">
<script id="texturedVertex" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
@ -362,9 +403,9 @@
gl_Position = vec4(aPos, 0.0, 1.0);
vTexCoord = (0.5*aPos+0.5);
}
</script>
</script>
<script id="texturedVertexWithResize" type="x-shader">
<script id="texturedVertexWithResize" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
@ -374,9 +415,9 @@
gl_Position = vec4(aPos, 0.0, 1.0);
vTexCoord = (0.5*aPos+0.5)*uResizeForCanvas;
}
</script>
</script>
<script id="texturedFragment" type="x-shader">
<script id="texturedFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0;
varying vec2 vTexCoord;
@ -385,9 +426,9 @@
gl_FragColor = texture2D(uTexture0, vTexCoord);
gl_FragColor.a= 1.0;
}
</script>
</script>
<script id="blurFragment" type="x-shader">
<script id="blurFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0;
uniform vec2 uOffset;
@ -414,9 +455,9 @@
sum += texture2D(uTexture0, vTexCoord + uOffset*8.0) * 0.000078;
gl_FragColor = sum;
}
</script>
</script>
<script id="outputVertex" type="x-shader">
<script id="outputVertex" type="x-shader">
precision highp float;
attribute vec2 aPos;
varying vec2 vTexCoord;
@ -428,9 +469,9 @@
vTexCoord = (0.5*aPos+0.5);
vTexCoordCanvas = vTexCoord*uResizeForCanvas;
}
</script>
</script>
<script id="outputFragment" type="x-shader">
<script id="outputFragment" type="x-shader">
precision highp float;
uniform sampler2D uTexture0; //line
uniform sampler2D uTexture1; //tight glow
@ -448,8 +489,13 @@
return vec3(mix(color, gray, factor));
}
void main (void)
{
/* Gradient noise from Jorge Jimenez's presentation: */
/* http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare */
float gradientNoise(in vec2 uv) {
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
}
void main (void) {
vec4 line = texture2D(uTexture0, vTexCoordCanvas);
// r components have grid; g components do not.
vec4 screen = texture2D(uTexture3, vTexCoord);
@ -461,8 +507,9 @@
float tlight2 = tlight*tlight*tlight;
gl_FragColor.rgb = mix(uColour, vec3(1.0), 0.3+tlight2*tlight2*0.5)*tlight;
gl_FragColor.rgb = desaturate(gl_FragColor.rgb, 1.0 - uSaturation);
gl_FragColor.rgb += (1.0 / 255.0) * gradientNoise(gl_FragCoord.xy) - (0.5 / 255.0);
gl_FragColor.a = 1.0;
}
</script>
</script>
<script src="oscilloscope.js" type="module"></script>
<script src="oscilloscope.js" type="module"></script>

Wyświetl plik

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0,12a12,12 0 1,0 24,0a12,12 0 1,0 -24,0Z" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 120 B

Wyświetl plik

@ -39,6 +39,12 @@ SosciPluginEditor::SosciPluginEditor(SosciAudioProcessor& p)
openVisualiserSettings();
};
addAndMakeVisible(record);
record.setPulseAnimation(true);
record.onClick = [this] {
visualiser.toggleRecording();
};
addAndMakeVisible(visualiser);
visualiser.openSettings = [this] {
@ -49,6 +55,10 @@ SosciPluginEditor::SosciPluginEditor(SosciAudioProcessor& p)
visualiserSettingsWindow.setVisible(false);
};
visualiser.recordingHalted = [this] {
record.setToggleState(false, juce::NotificationType::dontSendNotification);
};
visualiserSettingsWindow.setResizable(false, false);
#if JUCE_WINDOWS
// if not standalone, use native title bar for compatibility with DAWs
@ -80,6 +90,7 @@ void SosciPluginEditor::resized() {
auto topBar = area.removeFromTop(25);
settings.setBounds(topBar.removeFromRight(25));
record.setBounds(topBar.removeFromRight(25));
menuBar.setBounds(topBar);
visualiser.setBounds(area);

Wyświetl plik

@ -39,6 +39,7 @@ public:
juce::TooltipWindow tooltipWindow{nullptr, 0};
SvgButton record{"Record", BinaryData::record_2_svg, juce::Colours::red, juce::Colours::red.withAlpha(0.01f)};
SvgButton settings{"Settings", BinaryData::cog_svg, juce::Colours::white, juce::Colours::white};
bool usingNativeMenuBar = false;

Wyświetl plik

@ -25,6 +25,8 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame
changeSvgColour(doc.get(), colourOn.withBrightness(0.3f));
disabledImageOn = juce::Drawable::createFromSVG(*doc);
path = normalImage->getOutlineAsPath();
getLookAndFeel().setColour(juce::DrawableButton::backgroundOnColourId, juce::Colours::transparentWhite);
if (colour != colourOn) {
@ -37,6 +39,8 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame
setToggleState(toggle->getBoolValue(), juce::NotificationType::dontSendNotification);
setTooltip(toggle->getDescription());
}
updater.addAnimator(pulse);
}
SvgButton(juce::String name, juce::String svg, juce::Colour colour) : SvgButton(name, svg, colour, colour) {}
@ -70,6 +74,30 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame
setMouseCursor(juce::MouseCursor::NormalCursor);
}
void setPulseAnimation(bool pulseUsed) {
this->pulseUsed = pulseUsed;
}
void paintOverChildren(juce::Graphics& g) override {
if (pulseUsed && getToggleState()) {
g.setColour(juce::Colours::black.withAlpha(colourFade / 1.5f));
g.fillPath(path);
}
}
void buttonStateChanged() override {
juce::DrawableButton::buttonStateChanged();
if (pulseUsed && getToggleState() != prevToggleState) {
if (getToggleState()) {
pulse.start();
} else {
pulse.complete();
colourFade = 1.0;
}
prevToggleState = getToggleState();
}
}
private:
std::unique_ptr<juce::Drawable> normalImage;
std::unique_ptr<juce::Drawable> overImage;
@ -83,6 +111,21 @@ private:
BooleanParameter* toggle;
juce::VBlankAnimatorUpdater updater{this};
float colourFade = 0.0;
bool pulseUsed = false;
bool prevToggleState = false;
juce::Path path;
juce::Animator pulse = juce::ValueAnimatorBuilder {}
.withEasing([] (float t) { return std::sin(3.14159 * t) / 2 + 0.5; })
.withDurationMs(500)
.runningInfinitely()
.withValueChangedCallback([this] (auto value) {
colourFade = value;
repaint();
})
.build();
void changeSvgColour(juce::XmlElement* xml, juce::Colour colour) {
forEachXmlChildElement(*xml, xmlnode) {
xmlnode->setAttribute("fill", '#' + colour.toDisplayString(false));

Wyświetl plik

@ -270,6 +270,9 @@ void VisualiserComponent::paintXY(juce::Graphics& g, juce::Rectangle<float> area
}
void VisualiserComponent::initialiseBrowser() {
if (recordingHalted != nullptr) {
recordingHalted();
}
oldBrowser = std::move(browser);
if (oldBrowser != nullptr) {
removeChildComponent(oldBrowser.get());
@ -323,6 +326,21 @@ void VisualiserComponent::initialiseBrowser() {
.withNativeFunction("isVisualiserOnly", [this](auto& var, auto complete) {
complete(visualiserOnly);
})
.withNativeFunction("downloadVideo", [this](const juce::Array<juce::var>& args, auto complete) {
juce::String base64 = args[0].toString();
chooser = std::make_unique<juce::FileChooser>("Save video", juce::File::getSpecialLocation(juce::File::SpecialLocationType::userDesktopDirectory).getChildFile("osci-render.webm"), "*.webm");
chooser->launchAsync(juce::FileBrowserComponent::saveMode,
[base64](const juce::FileChooser& chooser) {
juce::File result = chooser.getResult();
if (result.getFullPathName().isNotEmpty()) {
juce::FileOutputStream stream(result);
stream.setPosition(0);
stream.truncate();
juce::Base64::convertFromBase64(stream, base64);
stream.flush();
}
});
})
);
addAndMakeVisible(*browser);
@ -366,6 +384,13 @@ void VisualiserComponent::handleAsyncUpdate() {
}
}
void VisualiserComponent::toggleRecording() {
if (oldVisualiser) {
return;
}
browser->emitEventIfBrowserIsVisible("toggleRecording", juce::var());
}
void VisualiserComponent::resized() {
if (!oldVisualiser) {
browser->setBounds(getLocalBounds());
@ -390,10 +415,14 @@ void VisualiserComponent::childChanged() {
}
void VisualiserComponent::popoutWindow() {
if (recordingHalted != nullptr) {
recordingHalted();
}
auto visualiser = new VisualiserComponent(sampleRateManager, consumerManager, settings, this, oldVisualiser);
visualiser->settings.setLookAndFeel(&getLookAndFeel());
visualiser->openSettings = openSettings;
visualiser->closeSettings = closeSettings;
visualiser->recordingHalted = recordingHalted;
child = visualiser;
childChanged();
popOutButton.setVisible(false);

Wyświetl plik

@ -42,6 +42,7 @@ public:
void setFullScreen(bool fullScreen);
void setVisualiserType(bool oldVisualiser);
void handleAsyncUpdate() override;
void toggleRecording();
VisualiserComponent* parent = nullptr;
VisualiserComponent* child = nullptr;
@ -49,6 +50,8 @@ public:
std::atomic<bool> active = true;
std::function<void()> recordingHalted;
private:
// 60fps
const double BUFFER_LENGTH_SECS = 1/60.0;
@ -120,6 +123,8 @@ private:
// keeping this around for memory management reasons
std::unique_ptr<juce::WebBrowserComponent> oldBrowser = nullptr;
std::unique_ptr<juce::FileChooser> chooser;
void initialiseBrowser();
void resetBuffer();
void popoutWindow();

Wyświetl plik

@ -658,6 +658,7 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../JUCE/modules"/>
</MODULEPATHS>
</LINUX_MAKE>
<VS2022 targetFolder="Builds/VisualStudio2022" smallIcon="pSc1mq" bigIcon="pSc1mq">
@ -681,6 +682,7 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../JUCE/modules"/>
</MODULEPATHS>
</VS2022>
<XCODE_MAC targetFolder="Builds/MacOSX" extraLinkerFlags="-Wl,-weak_reference_mismatches,weak"
@ -705,10 +707,12 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../JUCE/modules"/>
</MODULEPATHS>
</XCODE_MAC>
</EXPORTFORMATS>
<MODULES>
<MODULE id="juce_animation" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_devices" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_formats" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>

Wyświetl plik

@ -44,6 +44,7 @@
<FILE id="PFc2q2" name="random.svg" compile="0" resource="1" file="Resources/svg/random.svg"/>
<FILE id="CE6di2" name="range.svg" compile="0" resource="1" file="Resources/svg/range.svg"/>
<FILE id="n79IAy" name="record.svg" compile="0" resource="1" file="Resources/svg/record.svg"/>
<FILE id="TWt5MY" name="record_2.svg" compile="0" resource="1" file="Resources/svg/record_2.svg"/>
<FILE id="OaqZb1" name="right_arrow.svg" compile="0" resource="1" file="Resources/svg/right_arrow.svg"/>
<FILE id="rXjNlx" name="threshold.svg" compile="0" resource="1" file="Resources/svg/threshold.svg"/>
<FILE id="rFYmV8" name="timer.svg" compile="0" resource="1" file="Resources/svg/timer.svg"/>
@ -139,6 +140,7 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../../../JUCE/modules"/>
</MODULEPATHS>
</LINUX_MAKE>
<VS2022 targetFolder="Builds/sosci/VisualStudio2022" smallIcon="pSc1mq"
@ -163,6 +165,7 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../../../JUCE/modules"/>
</MODULEPATHS>
</VS2022>
<XCODE_MAC targetFolder="Builds/sosci/MacOSX" extraLinkerFlags="-Wl,-weak_reference_mismatches,weak"
@ -187,10 +190,12 @@
<MODULEPATH id="juce_gui_extra" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_opengl" path="../../../JUCE/modules"/>
<MODULEPATH id="juce_dsp" path="../../JUCE/modules"/>
<MODULEPATH id="juce_animation" path="../../../JUCE/modules"/>
</MODULEPATHS>
</XCODE_MAC>
</EXPORTFORMATS>
<MODULES>
<MODULE id="juce_animation" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_devices" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
<MODULE id="juce_audio_formats" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>