From cdf3e88e17d2be2a13650a75912acae51e3b6d51 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Fri, 4 Apr 2025 23:00:38 +0100 Subject: [PATCH] Make volume button mutable, and add ability to save global settings --- Resources/svg/mute.svg | 1 + Source/CommonPluginProcessor.cpp | 60 ++++++++++++++++++++++++++- Source/CommonPluginProcessor.h | 16 ++++++- Source/PluginProcessor.cpp | 10 ++++- Source/SosciPluginProcessor.cpp | 6 +++ Source/components/SvgButton.h | 31 ++++++++++---- Source/components/VolumeComponent.cpp | 23 ++++++---- Source/components/VolumeComponent.h | 3 +- Source/txt/TextParser.cpp | 5 ++- osci-render.jucer | 1 + sosci.jucer | 1 + 11 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 Resources/svg/mute.svg diff --git a/Resources/svg/mute.svg b/Resources/svg/mute.svg new file mode 100644 index 0000000..ff402f0 --- /dev/null +++ b/Resources/svg/mute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/CommonPluginProcessor.cpp b/Source/CommonPluginProcessor.cpp index 59297e6..6d3b866 100644 --- a/Source/CommonPluginProcessor.cpp +++ b/Source/CommonPluginProcessor.cpp @@ -17,6 +17,20 @@ CommonAudioProcessor::CommonAudioProcessor(const BusesProperties& busesPropertie : AudioProcessor(busesProperties) #endif { + // Initialize the global settings with the plugin name + juce::PropertiesFile::Options options; + options.applicationName = JucePlugin_Name + juce::String("_globals"); + options.filenameSuffix = ".settings"; + options.osxLibrarySubFolder = "Application Support"; + + #if JUCE_LINUX || JUCE_BSD + options.folderName = "~/.config"; + #else + options.folderName = ""; + #endif + + globalSettings = std::make_unique(options); + // locking isn't necessary here because we are in the constructor for (auto effect : visualiserParameters.effects) { @@ -36,6 +50,9 @@ CommonAudioProcessor::CommonAudioProcessor(const BusesProperties& busesPropertie intParameters.push_back(parameter); } + muteParameter = new BooleanParameter("Mute", "mute", VERSION_HINT, false, "Mute audio output"); + booleanParameters.push_back(muteParameter); + permanentEffects.push_back(volumeEffect); permanentEffects.push_back(thresholdEffect); effects.push_back(volumeEffect); @@ -67,7 +84,10 @@ void CommonAudioProcessor::addAllParameters() { } } -CommonAudioProcessor::~CommonAudioProcessor() {} +CommonAudioProcessor::~CommonAudioProcessor() +{ + saveGlobalSettings(); +} const juce::String CommonAudioProcessor::getName() const { return JucePlugin_Name; @@ -301,3 +321,41 @@ void CommonAudioProcessor::loadProperties(juce::XmlElement& xml) { } } } + +bool CommonAudioProcessor::getGlobalBoolValue(const juce::String& keyName, bool defaultValue) const +{ + return globalSettings != nullptr ? globalSettings->getBoolValue(keyName, defaultValue) : defaultValue; +} + +int CommonAudioProcessor::getGlobalIntValue(const juce::String& keyName, int defaultValue) const +{ + return globalSettings != nullptr ? globalSettings->getIntValue(keyName, defaultValue) : defaultValue; +} + +double CommonAudioProcessor::getGlobalDoubleValue(const juce::String& keyName, double defaultValue) const +{ + return globalSettings != nullptr ? globalSettings->getDoubleValue(keyName, defaultValue) : defaultValue; +} + +juce::String CommonAudioProcessor::getGlobalStringValue(const juce::String& keyName, const juce::String& defaultValue) const +{ + return globalSettings != nullptr ? globalSettings->getValue(keyName, defaultValue) : defaultValue; +} + +void CommonAudioProcessor::setGlobalValue(const juce::String& keyName, const juce::var& value) +{ + if (globalSettings != nullptr) + globalSettings->setValue(keyName, value); +} + +void CommonAudioProcessor::removeGlobalValue(const juce::String& keyName) +{ + if (globalSettings != nullptr) + globalSettings->removeValue(keyName); +} + +void CommonAudioProcessor::saveGlobalSettings() +{ + if (globalSettings != nullptr) + globalSettings->saveIfNeeded(); +} diff --git a/Source/CommonPluginProcessor.h b/Source/CommonPluginProcessor.h index 9d3443a..5d50fda 100644 --- a/Source/CommonPluginProcessor.h +++ b/Source/CommonPluginProcessor.h @@ -4,6 +4,7 @@ This file contains the basic framework code for a JUCE plugin processor. ============================================================================== + */ #pragma once @@ -63,12 +64,22 @@ public: std::any getProperty(const std::string& key); std::any getProperty(const std::string& key, std::any defaultValue); void setProperty(const std::string& key, std::any value); - + + // Global settings methods + bool getGlobalBoolValue(const juce::String& keyName, bool defaultValue = false) const; + int getGlobalIntValue(const juce::String& keyName, int defaultValue = 0) const; + double getGlobalDoubleValue(const juce::String& keyName, double defaultValue = 0.0) const; + juce::String getGlobalStringValue(const juce::String& keyName, const juce::String& defaultValue = "") const; + void setGlobalValue(const juce::String& keyName, const juce::var& value); + void removeGlobalValue(const juce::String& keyName); + void saveGlobalSettings(); + juce::SpinLock audioPlayerListenersLock; std::vector audioPlayerListeners; std::atomic volume = 1.0; std::atomic threshold = 1.0; + BooleanParameter* muteParameter = nullptr; std::shared_ptr volumeEffect = std::make_shared( [this](int index, OsciPoint input, const std::vector>& values, double sampleRate) { @@ -132,6 +143,9 @@ protected: juce::SpinLock propertiesLock; std::unordered_map properties; + + // Global settings that persist across plugin instances + std::unique_ptr globalSettings; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CommonAudioProcessor) diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index cddd555..da15906 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -612,14 +612,20 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju x = juce::jmax(-threshold, juce::jmin(threshold.load(), x)); y = juce::jmax(-threshold, juce::jmin(threshold.load(), y)); + threadManager.write(OsciPoint(x, y, 1)); + + // Apply mute if active + if (muteParameter->getBoolValue()) { + x = 0.0; + y = 0.0; + } + if (totalNumOutputChannels >= 2) { channelData[0][sample] = x; channelData[1][sample] = y; } else if (totalNumOutputChannels == 1) { channelData[0][sample] = x; } - - threadManager.write(OsciPoint(x, y, 1)); if (isPlaying) { playTimeSeconds += sTimeSec; diff --git a/Source/SosciPluginProcessor.cpp b/Source/SosciPluginProcessor.cpp index 3f23edd..6e130ac 100644 --- a/Source/SosciPluginProcessor.cpp +++ b/Source/SosciPluginProcessor.cpp @@ -69,6 +69,12 @@ void SosciAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::M point.x = juce::jmax(-threshold, juce::jmin(threshold.load(), point.x)); point.y = juce::jmax(-threshold, juce::jmin(threshold.load(), point.y)); + // Apply mute if active + if (muteParameter->getBoolValue()) { + point.x = 0.0; + point.y = 0.0; + } + // this is the point that the volume component will draw (i.e. post scale/clipping) threadManager.write(point, "VolumeComponent"); } diff --git a/Source/components/SvgButton.h b/Source/components/SvgButton.h index fe5a1ea..428a8f7 100644 --- a/Source/components/SvgButton.h +++ b/Source/components/SvgButton.h @@ -4,7 +4,7 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParameter::Listener, public juce::AsyncUpdater { public: - SvgButton(juce::String name, juce::String svg, juce::Colour colour, juce::Colour colourOn, BooleanParameter* toggle = nullptr) : juce::DrawableButton(name, juce::DrawableButton::ButtonStyle::ImageFitted), toggle(toggle) { + SvgButton(juce::String name, juce::String svg, juce::Colour colour, juce::Colour colourOn, BooleanParameter* toggle = nullptr, juce::String toggledSvg = "") : juce::DrawableButton(name, juce::DrawableButton::ButtonStyle::ImageFitted), toggle(toggle) { auto doc = juce::XmlDocument::parse(svg); changeSvgColour(doc.get(), colour); @@ -16,14 +16,27 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame changeSvgColour(doc.get(), colour.withBrightness(0.3f)); disabledImage = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colourOn); - normalImageOn = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colourOn.withBrightness(0.7f)); - overImageOn = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colourOn.withBrightness(0.5f)); - downImageOn = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colourOn.withBrightness(0.3f)); - disabledImageOn = juce::Drawable::createFromSVG(*doc); + // If a toggled SVG is provided, use it for the "on" state images + if (toggledSvg.isNotEmpty()) { + auto toggledDoc = juce::XmlDocument::parse(toggledSvg); + changeSvgColour(toggledDoc.get(), colourOn); + normalImageOn = juce::Drawable::createFromSVG(*toggledDoc); + changeSvgColour(toggledDoc.get(), colourOn.withBrightness(0.7f)); + overImageOn = juce::Drawable::createFromSVG(*toggledDoc); + changeSvgColour(toggledDoc.get(), colourOn.withBrightness(0.5f)); + downImageOn = juce::Drawable::createFromSVG(*toggledDoc); + changeSvgColour(toggledDoc.get(), colourOn.withBrightness(0.3f)); + disabledImageOn = juce::Drawable::createFromSVG(*toggledDoc); + } else { + changeSvgColour(doc.get(), colourOn); + normalImageOn = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colourOn.withBrightness(0.7f)); + overImageOn = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colourOn.withBrightness(0.5f)); + downImageOn = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colourOn.withBrightness(0.3f)); + disabledImageOn = juce::Drawable::createFromSVG(*doc); + } basePath = normalImage->getOutlineAsPath(); diff --git a/Source/components/VolumeComponent.cpp b/Source/components/VolumeComponent.cpp index 79e3657..984d952 100644 --- a/Source/components/VolumeComponent.cpp +++ b/Source/components/VolumeComponent.cpp @@ -1,6 +1,9 @@ #include "VolumeComponent.h" -VolumeComponent::VolumeComponent(CommonAudioProcessor& p) : AudioBackgroundThread("VolumeComponent", p.threadManager), audioProcessor(p) { +VolumeComponent::VolumeComponent(CommonAudioProcessor& p) + : AudioBackgroundThread("VolumeComponent", p.threadManager), + audioProcessor(p) +{ setOpaque(false); setShouldBeRunning(true); @@ -38,12 +41,14 @@ VolumeComponent::VolumeComponent(CommonAudioProcessor& p) : AudioBackgroundThrea audioProcessor.thresholdEffect->setValue(thresholdSlider.getValue()); }; - auto doc = juce::XmlDocument::parse(BinaryData::volume_svg); - volumeIcon = juce::Drawable::createFromSVG(*doc); - doc = juce::XmlDocument::parse(BinaryData::threshold_svg); + addAndMakeVisible(volumeButton); + volumeButton.onClick = [this] { + audioProcessor.muteParameter->setBoolValueNotifyingHost(!audioProcessor.muteParameter->getBoolValue()); + }; + + auto doc = juce::XmlDocument::parse(BinaryData::threshold_svg); thresholdIcon = juce::Drawable::createFromSVG(*doc); - addAndMakeVisible(*volumeIcon); addAndMakeVisible(*thresholdIcon); } @@ -122,9 +127,11 @@ void VolumeComponent::stopTask() {} void VolumeComponent::resized() { auto r = getLocalBounds(); - auto iconRow = r.removeFromTop(20).toFloat(); - volumeIcon->setTransformToFit(iconRow.removeFromLeft(iconRow.getWidth() / 2).reduced(1), juce::RectanglePlacement::centred); - thresholdIcon->setTransformToFit(iconRow.reduced(2), juce::RectanglePlacement::centred); + auto iconRow = r.removeFromTop(20); + auto volumeRect = iconRow.removeFromLeft(iconRow.getWidth() / 2); + volumeButton.setBounds(volumeRect.expanded(3)); + thresholdIcon->setTransformToFit(iconRow.reduced(2).toFloat(), juce::RectanglePlacement::centred); + volumeSlider.setBounds(r.removeFromLeft(r.getWidth() / 2)); auto radius = volumeSlider.getLookAndFeel().getSliderThumbRadius(volumeSlider); thresholdSlider.setBounds(r.reduced(0, radius / 2)); diff --git a/Source/components/VolumeComponent.h b/Source/components/VolumeComponent.h index f2bcbd9..314e16a 100644 --- a/Source/components/VolumeComponent.h +++ b/Source/components/VolumeComponent.h @@ -4,6 +4,7 @@ #include "../CommonPluginProcessor.h" #include "../LookAndFeel.h" #include "../concurrency/AudioBackgroundThread.h" +#include "SvgButton.h" class ThumbRadiusLookAndFeel : public OscirenderLookAndFeel { public: @@ -88,7 +89,7 @@ private: ThresholdLookAndFeel thresholdLookAndFeel{7}; juce::Slider thresholdSlider; - std::unique_ptr volumeIcon; + SvgButton volumeButton = SvgButton("VolumeButton", BinaryData::volume_svg, juce::Colours::white, juce::Colours::red, audioProcessor.muteParameter, BinaryData::mute_svg); std::unique_ptr thresholdIcon; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VolumeComponent) diff --git a/Source/txt/TextParser.cpp b/Source/txt/TextParser.cpp index 3dfbd3a..cc31d4d 100644 --- a/Source/txt/TextParser.cpp +++ b/Source/txt/TextParser.cpp @@ -36,7 +36,7 @@ void TextParser::parse(juce::String text, juce::Font font) { juce::String displayText = attributedString.getText(); // remove all whitespace - displayText = displayText.replaceCharacters(" \t\n\r", ""); + displayText = displayText.removeCharacters(" \t\n\r"); int index = 0; // Iterate through all lines and all runs in each line @@ -50,6 +50,9 @@ void TextParser::parse(juce::String text, juce::Font font) { juce::GlyphArrangement glyphs; for (int k = 0; k < run->glyphs.size(); ++k) { + if (index >= displayText.length()) { + break; + } juce::juce_wchar character = displayText[index]; juce::TextLayout::Glyph glyph = run->glyphs.getUnchecked(k); juce::PositionedGlyph positionedGlyph = juce::PositionedGlyph( diff --git a/osci-render.jucer b/osci-render.jucer index 6c6b6a5..fc90e6d 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -54,6 +54,7 @@ + diff --git a/sosci.jucer b/sosci.jucer index 61b4355..71e89db 100644 --- a/sosci.jucer +++ b/sosci.jucer @@ -58,6 +58,7 @@ +