From f30ac1823e2bfbc5a6c4e505540fee850eb60e92 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Sat, 12 Oct 2024 19:17:35 +0100 Subject: [PATCH] Add support for recording oscilloscope visualiser --- Resources/oscilloscope/oscilloscope.html | 927 ++++++++++++---------- Resources/svg/record_2.svg | 1 + Source/SosciPluginEditor.cpp | 11 + Source/SosciPluginEditor.h | 1 + Source/components/SvgButton.h | 43 + Source/components/VisualiserComponent.cpp | 29 + Source/components/VisualiserComponent.h | 5 + osci-render.jucer | 4 + sosci.jucer | 5 + 9 files changed, 586 insertions(+), 440 deletions(-) create mode 100644 Resources/svg/record_2.svg diff --git a/Resources/oscilloscope/oscilloscope.html b/Resources/oscilloscope/oscilloscope.html index 6f6b1c2..eec572c 100644 --- a/Resources/oscilloscope/oscilloscope.html +++ b/Resources/oscilloscope/oscilloscope.html @@ -2,467 +2,514 @@ - + - -
-
- - let isDebug = true; - let paused = false; - let openInAnotherWindow = false; - let externalSampleRate = 96000; - let externalBufferSize = 1920; - - - - + +
+
Paused
+ +
+ + - const isDebugFn = Juce.getNativeFunction("isDebug"); - - isDebugFn().then(debug => { - isDebug = debug; - if (!debug) { - document.addEventListener('contextmenu', event => event.preventDefault()); + - const isOverlayFn = Juce.getNativeFunction("isOverlay"); - isOverlayFn().then(overlay => { - if (overlay) { - popout.remove(); - fullscreen.remove(); + + + + + + + - Juce.getNativeFunction("isVisualiserOnly")().then(visualiserOnly => { - if (visualiserOnly) { - popout.remove(); - fullscreen.remove(); - settings.remove(); + - window.__JUCE__.backend.addEventListener("childPresent", hasChild => { - openInAnotherWindow = hasChild; - if (hasChild) { - overlay.style.display = "flex"; - overlay.innerText = "Open in separate window"; - } else { - overlay.style.display = "none"; - overlay.innerText = "Paused"; + - document.addEventListener("dblclick", function() { - toggleFullscreen(); - }); - - -
-
Paused
- -
- - - - - - - - + + - - - - - - - - - - - - - - - vec3 desaturate(vec3 color, float factor) { - vec3 lum = vec3(0.299, 0.587, 0.114); - vec3 gray = vec3(dot(lum, color)); - return vec3(mix(color, gray, factor)); - } + - void main (void) - { - vec4 line = texture2D(uTexture0, vTexCoordCanvas); - // r components have grid; g components do not. - vec4 screen = texture2D(uTexture3, vTexCoord); - vec4 tightGlow = texture2D(uTexture1, vTexCoord); - vec4 scatter = texture2D(uTexture2, vTexCoord)+0.35; - float light = line.r + 1.5*screen.g*screen.g*tightGlow.r; - light += 0.4*scatter.g * (2.0+1.0*screen.g + 0.5*screen.r); - float tlight = 1.0-pow(2.0, -uExposure*light); - 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.a = 1.0; - } - - - + diff --git a/Resources/svg/record_2.svg b/Resources/svg/record_2.svg new file mode 100644 index 0000000..32de472 --- /dev/null +++ b/Resources/svg/record_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/SosciPluginEditor.cpp b/Source/SosciPluginEditor.cpp index 775b4a7..2d8577e 100644 --- a/Source/SosciPluginEditor.cpp +++ b/Source/SosciPluginEditor.cpp @@ -39,6 +39,12 @@ SosciPluginEditor::SosciPluginEditor(SosciAudioProcessor& p) openVisualiserSettings(); }; + addAndMakeVisible(record); + record.setPulseAnimation(true); + record.onClick = [this] { + visualiser.toggleRecording(); + }; + addAndMakeVisible(visualiser); visualiser.openSettings = [this] { @@ -48,6 +54,10 @@ SosciPluginEditor::SosciPluginEditor(SosciAudioProcessor& p) visualiser.closeSettings = [this] { visualiserSettingsWindow.setVisible(false); }; + + visualiser.recordingHalted = [this] { + record.setToggleState(false, juce::NotificationType::dontSendNotification); + }; visualiserSettingsWindow.setResizable(false, false); #if JUCE_WINDOWS @@ -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); diff --git a/Source/SosciPluginEditor.h b/Source/SosciPluginEditor.h index 34190f9..1f814a0 100644 --- a/Source/SosciPluginEditor.h +++ b/Source/SosciPluginEditor.h @@ -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; diff --git a/Source/components/SvgButton.h b/Source/components/SvgButton.h index a869828..e58fcd9 100644 --- a/Source/components/SvgButton.h +++ b/Source/components/SvgButton.h @@ -24,6 +24,8 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame downImageOn = juce::Drawable::createFromSVG(*doc); changeSvgColour(doc.get(), colourOn.withBrightness(0.3f)); disabledImageOn = juce::Drawable::createFromSVG(*doc); + + path = normalImage->getOutlineAsPath(); getLookAndFeel().setColour(juce::DrawableButton::backgroundOnColourId, juce::Colours::transparentWhite); @@ -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) {} @@ -69,6 +73,30 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame juce::DrawableButton::mouseExit(e); 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 normalImage; @@ -82,6 +110,21 @@ private: std::unique_ptr disabledImageOn; 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) { diff --git a/Source/components/VisualiserComponent.cpp b/Source/components/VisualiserComponent.cpp index b3f4ab8..6845ec8 100644 --- a/Source/components/VisualiserComponent.cpp +++ b/Source/components/VisualiserComponent.cpp @@ -270,6 +270,9 @@ void VisualiserComponent::paintXY(juce::Graphics& g, juce::Rectangle 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& args, auto complete) { + juce::String base64 = args[0].toString(); + chooser = std::make_unique("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); diff --git a/Source/components/VisualiserComponent.h b/Source/components/VisualiserComponent.h index a95e296..572233c 100644 --- a/Source/components/VisualiserComponent.h +++ b/Source/components/VisualiserComponent.h @@ -42,12 +42,15 @@ public: void setFullScreen(bool fullScreen); void setVisualiserType(bool oldVisualiser); void handleAsyncUpdate() override; + void toggleRecording(); VisualiserComponent* parent = nullptr; VisualiserComponent* child = nullptr; std::unique_ptr popout = nullptr; std::atomic active = true; + + std::function recordingHalted; private: // 60fps @@ -120,6 +123,8 @@ private: // keeping this around for memory management reasons std::unique_ptr oldBrowser = nullptr; + std::unique_ptr chooser; + void initialiseBrowser(); void resetBuffer(); void popoutWindow(); diff --git a/osci-render.jucer b/osci-render.jucer index 3c08563..d310333 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -658,6 +658,7 @@ + @@ -681,6 +682,7 @@ + + + diff --git a/sosci.jucer b/sosci.jucer index 59a7b3d..686e1e2 100644 --- a/sosci.jucer +++ b/sosci.jucer @@ -44,6 +44,7 @@ + @@ -139,6 +140,7 @@ + + + +