added output recording feature. moved settings to a separate popup panel instead of a menu

This commit is contained in:
essej 2022-06-14 19:11:57 -04:00
parent aff633ff39
commit af5efd8b09
21 changed files with 3309 additions and 263 deletions

View File

@ -36,9 +36,9 @@ endif()
# `project()` command. `project()` sets up some helpful variables that describe source/binary
# directories, and the current project version. This is a standard CMake command.
project(PaulXStretch VERSION 1.5.4)
project(PaulXStretch VERSION 1.6.0)
set(BUILDVERSION 111)
set(BUILDVERSION 112)
# If you've installed JUCE somehow (via a package manager, or directly using the CMake install
@ -277,12 +277,20 @@ function(sono_add_custom_plugin_target target_name product_name formats is_instr
Source/CustomLookAndFeel.h
Source/CustomStandaloneFilterApp.cpp
Source/CustomStandaloneFilterWindow.h
Source/GenericItemChooser.cpp
Source/GenericItemChooser.h
Source/SonoChoiceButton.cpp
Source/SonoChoiceButton.h
Source/SonoTextButton.cpp
Source/SonoTextButton.h
Source/PluginEditor.cpp
Source/PluginEditor.h
Source/PluginProcessor.cpp
Source/PluginProcessor.h
Source/RenderSettingsComponent.cpp
Source/RenderSettingsComponent.h
Source/OptionsView.cpp
Source/OptionsView.h
Source/envelope_component.cpp
Source/envelope_component.h
Source/jcdp_envelope.h
@ -401,8 +409,10 @@ function(sono_add_custom_plugin_target target_name product_name formats is_instr
images/play_icon.svg
images/power.svg
images/power_sel.svg
images/record.svg
images/record_active.svg
images/record_input.svg
images/record_input_active.svg
images/record_output.svg
images/record_output_active.svg
images/skipback_icon.svg
)

View File

@ -243,10 +243,12 @@ void CustomLookAndFeel::createTabTextLayout (const TabBarButton& button, float l
Colour colour, TextLayout& textLayout)
{
float hscale = 0.6f;
float xhscale = 0.6f;
#if JUCE_IOS
hscale = 0.5f;
xhscale = 0.5f;
#endif
float fontsize = button.getExtraComponent() != nullptr ? jmin(depth, 32.0f) * hscale : jmin(depth, 32.0f) * hscale;
float fontsize = button.getExtraComponent() != nullptr ? jmin(depth, 32.0f) * xhscale : jmin(depth, 32.0f) * hscale;
Font font = myFont.withHeight(fontsize * fontScale);
font.setUnderline (button.hasKeyboardFocus (false));
@ -1174,7 +1176,7 @@ void CustomLookAndFeel::drawDrawableButton (Graphics& g, DrawableButton& button,
int textH = 0;
int textW = 0;
float imageratio = 0.75f;
float imageratio = 0.85f;
//if (SonoDrawableButton* const sonobutt = dynamic_cast<SonoDrawableButton*> (&button)) {
// imageratio = sonobutt->getForegroundImageRatio();

View File

@ -131,8 +131,8 @@ public:
mainWindow->pluginHolder->saveAudioDeviceState();
if (auto * sonoproc = dynamic_cast<PaulstretchpluginAudioProcessor*>(mainWindow->pluginHolder->processor.get())) {
if (sonoproc->getBoolParameter(cpi_pause_enabled)->get() && !sonoproc->isRecordingEnabled()
&& !mainWindow->pluginHolder->isInterAppAudioConnected()) {
if (sonoproc->getBoolParameter(cpi_pause_enabled)->get() && !sonoproc->isInputRecordingEnabled()
&& !sonoproc->isRecordingToFile() && !mainWindow->pluginHolder->isInterAppAudioConnected()) {
// shutdown audio engine
DBG("not active, shutting down audio");
mainWindow->getDeviceManager().closeAudioDevice();

View File

@ -327,7 +327,7 @@ public:
settings->setValue ("audioSetup", xml.get());
#if ! (JUCE_IOS || JUCE_ANDROID)
settings->setValue ("shouldMuteInput", (bool) shouldMuteInput.getValue());
// settings->setValue ("shouldMuteInput", (bool) shouldMuteInput.getValue());
#endif
}
}
@ -343,7 +343,7 @@ public:
savedState = settings->getXmlValue ("audioSetup");
#if ! (JUCE_IOS || JUCE_ANDROID)
shouldMuteInput.setValue (settings->getBoolValue ("shouldMuteInput", true));
// shouldMuteInput.setValue (settings->getBoolValue ("shouldMuteInput", true));
#endif
}

View File

@ -0,0 +1,354 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#include "GenericItemChooser.h"
enum {
nameTextColourId = 0x1002830,
currentNameTextColourId = 0x1002850,
selectedColourId = 0x1002840,
separatorColourId = 0x1002860,
disabledColourId = 0x1002870
};
CallOutBox& GenericItemChooser::launchPopupChooser(const Array<GenericItemChooserItem> & items, Rectangle<int> targetBounds, Component * targetComponent, GenericItemChooser::Listener * listener, int tag, int selectedIndex, int maxheight, bool dismissSel)
{
auto chooser = std::make_unique<GenericItemChooser>(items, tag);
chooser->dismissOnSelected = dismissSel;
if (selectedIndex >= 0) {
chooser->setCurrentRow(selectedIndex);
}
if (listener) {
chooser->addListener(listener);
}
if (maxheight > 0) {
chooser->setMaxHeight(maxheight);
}
CallOutBox & box = CallOutBox::launchAsynchronously (std::move(chooser), targetBounds, targetComponent);
box.setDismissalMouseClicksAreAlwaysConsumed(true);
// box.setArrowSize(0);
box.grabKeyboardFocus();
return box;
}
CallOutBox& GenericItemChooser::launchPopupChooser(const Array<GenericItemChooserItem> & items, juce::Rectangle<int> targetBounds, Component * targetComponent, std::function<void (GenericItemChooser* chooser,int index)> onSelectedFunction, int selectedIndex, int maxheight, bool dismissSel)
{
auto chooser = std::make_unique<GenericItemChooser>(items, 0);
chooser->dismissOnSelected = dismissSel;
if (selectedIndex >= 0) {
chooser->setCurrentRow(selectedIndex);
}
chooser->onSelected = onSelectedFunction;
if (maxheight > 0) {
chooser->setMaxHeight(maxheight);
}
CallOutBox & box = CallOutBox::launchAsynchronously (std::move(chooser), targetBounds, targetComponent);
box.setDismissalMouseClicksAreAlwaysConsumed(true);
// box.setArrowSize(0);
box.grabKeyboardFocus();
return box;
}
GenericItemChooser::GenericItemChooser(const Array<GenericItemChooserItem> & items_, int tag_) : font (16.0, Font::plain), catFont(15.0, Font::plain), items(items_), tag(tag_)
{
currentIndex = -1;
#if JUCE_IOS || JUCE_ANDROID
rowHeight = 42;
#else
rowHeight = 32;
#endif
numRows = items.size();
// Create our table component and add it to this component..
addAndMakeVisible (table);
table.setModel (this);
// give it a border
table.setColour (ListBox::outlineColourId, Colour::fromFloatRGBA(0.0, 0.0, 0.0, 0.0));
table.setColour (ListBox::backgroundColourId, Colour::fromFloatRGBA(0.1, 0.1, 0.1, 1.0f));
table.setColour (ListBox::textColourId, Colours::whitesmoke.withAlpha(0.8f));
setColour (nameTextColourId, Colour::fromFloatRGBA(1.0f, 1.0f, 1.0f, 0.8f));
setColour (currentNameTextColourId, Colour::fromFloatRGBA(0.4f, 0.8f, 1.0f, 0.9f));
setColour (selectedColourId, Colour (0xff3d70c8).withAlpha(0.5f));
setColour (separatorColourId, Colour::fromFloatRGBA(1.0f, 1.0f, 1.0f, 0.5f));
setColour (disabledColourId, Colour::fromFloatRGBA(0.7f, 0.7f, 0.7f, 0.7f));
table.setOutlineThickness (0);
//table.getViewport()->setScrollOnDragEnabled(true);
table.getViewport()->setScrollBarsShown(true, false);
table.getViewport()->setScrollOnDragEnabled(true);
table.setRowSelectedOnMouseDown(true);
table.setRowClickedOnMouseDown(false);
table.setMultipleSelectionEnabled (false);
table.setRowHeight(rowHeight);
int newh = (rowHeight) * numRows + 4;
setSize(getAutoWidth(), newh);
}
GenericItemChooser::~GenericItemChooser()
{
}
void GenericItemChooser::setCurrentRow(int index)
{
currentIndex = index;
SparseSet<int> selrows;
selrows.addRange(Range<int>(index,index+1));
table.setSelectedRows(selrows);
table.updateContent();
}
void GenericItemChooser::setItems(const Array<GenericItemChooserItem> & items_)
{
items = items_;
numRows = items.size();
table.updateContent();
int newh = (rowHeight) * numRows;
setSize(getAutoWidth(), newh);
}
int GenericItemChooser::getAutoWidth()
{
int targw = 60;
for (int i=0; i < items.size(); ++i) {
int tsize = font.getStringWidth(items[i].name);
if (items[i].image.isValid()) {
tsize += rowHeight - 8;
}
targw = jmax(targw, tsize);
}
return targw + 30;
}
void GenericItemChooser::setRowHeight(int ht)
{
rowHeight = ht;
table.setRowHeight(rowHeight);
int newh = (rowHeight+2) * numRows;
if (maxHeight > 0) {
newh = jmin(newh, maxHeight);
}
setSize(getAutoWidth(), newh);
}
void GenericItemChooser::setMaxHeight(int ht)
{
maxHeight = ht;
int newh = (rowHeight+2) * numRows;
if (maxHeight > 0) {
newh = jmin(newh, maxHeight);
}
setSize(getAutoWidth(), newh);
}
// This is overloaded from TableListBoxModel, and must return the total number of rows in our table
int GenericItemChooser::getNumRows()
{
return numRows;
}
String GenericItemChooser::getNameForRow (int rowNumber)
{
if (rowNumber< items.size()) {
return items[rowNumber].name;
}
return ListBoxModel::getNameForRow(rowNumber);
}
void GenericItemChooser::listBoxItemClicked (int rowNumber, const MouseEvent& e)
{
DBG("listbox clicked");
if (items[rowNumber].disabled) {
// not selectable
return;
}
listeners.call (&GenericItemChooser::Listener::genericItemChooserSelected, this, rowNumber);
if (onSelected) {
onSelected(this, rowNumber);
}
if (dismissOnSelected) {
if (CallOutBox* const cb = findParentComponentOfClass<CallOutBox>()) {
cb->dismiss();
}
} else {
setCurrentRow(rowNumber);
repaint();
}
}
void GenericItemChooser::selectedRowsChanged(int lastRowSelected)
{
// notify listeners
DBG("Selected rows changed");
}
void GenericItemChooser::deleteKeyPressed (int)
{
DBG("delete key pressed");
}
void GenericItemChooser::returnKeyPressed (int rowNumber)
{
DBG("return key pressed: " << rowNumber);
listeners.call (&GenericItemChooser::Listener::genericItemChooserSelected, this, rowNumber);
if (rowNumber < items.size() && items[rowNumber].disabled) {
// not selectable
return;
}
if (onSelected) {
onSelected(this, rowNumber);
}
if (dismissOnSelected) {
if (CallOutBox* const cb = findParentComponentOfClass<CallOutBox>()) {
cb->giveAwayKeyboardFocus();
cb->dismiss();
}
} else {
setCurrentRow(rowNumber);
repaint();
}
}
void GenericItemChooser::paintListBoxItem (int rowNumber, Graphics &g, int width, int height, bool rowIsSelected)
{
if (items[rowNumber].separator) {
g.setColour (findColour(separatorColourId));
g.drawLine(0, 0, width, 0);
}
if (rowIsSelected && !items[rowNumber].disabled) {
g.setColour (findColour(selectedColourId));
g.fillRect(Rectangle<int>(0,0,width,height));
}
if (items[rowNumber].disabled) {
g.setColour (findColour(disabledColourId));
}
else if (rowNumber == currentIndex) {
g.setColour (findColour(currentNameTextColourId));
}
else {
g.setColour (findColour(nameTextColourId));
}
g.setFont (font);
int imagewidth = 0;
if (rowNumber < items.size()) {
if (items[rowNumber].image.isValid()) {
imagewidth = height-8;
//g.drawImage(items[rowNumber].image, Rectangle<float>(2, 2, imagewidth, height - 4));
g.drawImageWithin(items[rowNumber].image, 2, 4, imagewidth, imagewidth, RectanglePlacement(RectanglePlacement::centred|RectanglePlacement::onlyReduceInSize));
}
}
/*
int imagewidth = height * 0.75;
if (rowNumber == 0) {
g.drawImageWithin(wheelImage, 2, 2, imagewidth, height - 4, RectanglePlacement::centred|RectanglePlacement::onlyReduceInSize);
}
else if (rowNumber == 1) {
g.drawImageWithin(stringImage, 2, 2, imagewidth, height - 4, RectanglePlacement::centred|RectanglePlacement::onlyReduceInSize);
}
else {
g.drawImageWithin(keyboardImage, 2, 2, imagewidth, height - 4, RectanglePlacement::centred|RectanglePlacement::onlyReduceInSize);
}
*/
String text = items[rowNumber].name;
//DBG("Paint " << text);
//g.drawFittedText (text, imagewidth + 10, 0, width - (imagewidth+8), height, Justification::centredLeft, 1, 0.5);
g.drawFittedText (text, imagewidth + 8, 0, width - (imagewidth+8), height, Justification::centredLeft, 1, 0.5);
}
void GenericItemChooser::buttonClicked (Button* buttonThatWasClicked)
{
//AppState * app = AppState::getInstance();
}
void GenericItemChooser::paint (Graphics& g)
{
//g.fillAll (Colour (0xff000000));
}
//==============================================================================
void GenericItemChooser::resized()
{
// position our table with a gap around its edge
int keywidth = 50;
int bottomButtHeight = 40;
table.setBoundsInset (BorderSize<int>(2,2,2,2));
// table.setTouchScrollScale(1.0f / AppState::getInstance()->scaleFactor);
//int bottbuttwidth = (int) (0.3333f * (getWidth()-keywidth));
//int addbuttwidth = (getWidth()-keywidth) - 2*bottbuttwidth - 8;
//allButton->setBounds(keywidth+2, getHeight()-bottomButtHeight-4, bottbuttwidth, bottomButtHeight);
//favsButton->setBounds(keywidth+2+bottbuttwidth, getHeight()-bottomButtHeight-4, bottbuttwidth, bottomButtHeight);
//addFavButton->setBounds(getWidth()-addbuttwidth-4, getHeight()-bottomButtHeight-4, addbuttwidth, bottomButtHeight);
}

110
Source/GenericItemChooser.h Normal file
View File

@ -0,0 +1,110 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#pragma once
#include "JuceHeader.h"
struct GenericItemChooserItem
{
struct UserData {
virtual ~UserData() {}
};
GenericItemChooserItem() : image(Image()) {}
GenericItemChooserItem(const String & name_, const Image & image_=Image(), std::shared_ptr<UserData> udata = nullptr, bool withSeparator=false, bool disabled_=false) : name(name_), image(image_), userdata(udata), separator(withSeparator), disabled(disabled_) {}
String name;
Image image;
std::shared_ptr<UserData> userdata;
bool separator = false;
bool disabled = false;
};
class GenericItemChooser : public Component, public ListBoxModel, public Button::Listener
{
public:
GenericItemChooser(const Array<GenericItemChooserItem> & items_, int tag=0);
virtual ~GenericItemChooser();
class Listener {
public:
virtual ~Listener() {}
virtual void genericItemChooserSelected(GenericItemChooser *comp, int index) {}
};
void setItems(const Array<GenericItemChooserItem> & items);
const Array<GenericItemChooserItem> & getItems() const { return items; }
// This is overloaded from TableListBoxModel, and must return the total number of rows in our table
int getNumRows() override;
String getNameForRow (int rowNumber) override;
void deleteKeyPressed (int) override;
void returnKeyPressed (int) override;
void paintListBoxItem (int rowNumber, Graphics &g, int width, int height, bool rowIsSelected) override;
// This is overloaded from TableListBoxModel, and must update any custom components that we're using
//Component* refreshComponentForCell (int rowNumber, int columnId, bool /*isRowSelected*/,
// Component* existingComponentToUpdate) override;
void setCurrentRow(int index);
int getCurrentRow() const { return currentIndex; }
//==============================================================================
void resized() override;
void listBoxItemClicked (int rowNumber, const MouseEvent& e) override;
void selectedRowsChanged(int lastRowSelected) override;
void buttonClicked (Button* buttonThatWasClicked) override;
void paint (Graphics& g) override;
void addListener(Listener * listener) { listeners.add(listener); }
void removeListener(Listener * listener) { listeners.remove(listener); }
void setRowHeight(int ht);
int getRowHeight() const { return rowHeight; }
void setMaxHeight(int ht);
int getMaxHeight() const { return maxHeight; }
void setTag(int tag_) { tag = tag_;}
int getTag() const { return tag; }
static CallOutBox& launchPopupChooser(const Array<GenericItemChooserItem> & items, juce::Rectangle<int> targetBounds, Component * targetComponent, GenericItemChooser::Listener * listener, int tag = 0, int selectedIndex=-1, int maxheight=0, bool dismissSel=true);
static CallOutBox& launchPopupChooser(const Array<GenericItemChooserItem> & items, juce::Rectangle<int> targetBounds, Component * targetComponent, std::function<void (GenericItemChooser* chooser,int index)> onSelectedFunction, int selectedIndex=-1, int maxheight=0, bool dismissSel=true);
std::function<void (GenericItemChooser* chooser,int index)> onSelected;
bool dismissOnSelected = true;
private:
int getAutoWidth();
ListenerList<Listener> listeners;
ListBox table; // the table component itself
Font font;
Font catFont;
int numRows; // The number of rows of data we've got
bool sortDirection;
int selectedRow;
int rowHeight;
int maxHeight = 0;
//StringArray items;
Array<GenericItemChooserItem> items;
int currentIndex;
int tag;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GenericItemChooser)
};

843
Source/OptionsView.cpp Normal file
View File

@ -0,0 +1,843 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2021 Jesse Chappell
#include "OptionsView.h"
class PaulxstretchOptionsTabbedComponent : public TabbedComponent
{
public:
PaulxstretchOptionsTabbedComponent(TabbedButtonBar::Orientation orientation, OptionsView & editor_) : TabbedComponent(orientation), editor(editor_) {
}
void currentTabChanged (int newCurrentTabIndex, const String& newCurrentTabName) override {
editor.optionsTabChanged(newCurrentTabIndex);
}
protected:
OptionsView & editor;
};
enum {
nameTextColourId = 0x1002830,
selectedColourId = 0x1002840,
separatorColourId = 0x1002850,
};
OptionsView::OptionsView(PaulstretchpluginAudioProcessor& proc, std::function<AudioDeviceManager*()> getaudiodevicemanager)
: Component(), getAudioDeviceManager(getaudiodevicemanager), processor(proc), smallLNF(14), sonoSliderLNF(13)
{
setColour (nameTextColourId, Colour::fromFloatRGBA(1.0f, 1.0f, 1.0f, 0.9f));
setColour (selectedColourId, Colour::fromFloatRGBA(0.0f, 0.4f, 0.8f, 0.5f));
setColour (separatorColourId, Colour::fromFloatRGBA(0.3f, 0.3f, 0.3f, 0.3f));
sonoSliderLNF.textJustification = Justification::centredRight;
sonoSliderLNF.sliderTextJustification = Justification::centredRight;
mOptionsComponent = std::make_unique<Component>();
mSettingsTab = std::make_unique<TabbedComponent>(TabbedButtonBar::Orientation::TabsAtTop);
mSettingsTab->setTabBarDepth(36);
mSettingsTab->setOutline(0);
mSettingsTab->getTabbedButtonBar().setMinimumTabScaleFactor(0.1f);
//mSettingsTab->addComponentListener(this);
mOptionsLoadFileWithPluginButton = std::make_unique<ToggleButton>(TRANS("Load file with plugin state"));
mOptionsLoadFileWithPluginButton->addListener(this);
mOptionsLoadFileWithPluginButton->onClick = [this] () {
toggleBool(processor.m_load_file_with_state);
};
mOptionsPlayWithTransportButton = std::make_unique<ToggleButton>(TRANS("Play when host transport running"));
mOptionsPlayWithTransportButton->onClick = [this] () {
toggleBool(processor.m_play_when_host_plays);
};
mOptionsCaptureWithTransportButton = std::make_unique<ToggleButton>(TRANS("Capture when host transport running"));
mOptionsCaptureWithTransportButton->onClick = [this] () {
toggleBool(processor.m_capture_when_host_plays);
};
mOptionsRestorePlayStateButton = std::make_unique<ToggleButton>(TRANS("Restore playing state"));
mOptionsRestorePlayStateButton->onClick = [this] () {
toggleBool(processor.m_restore_playstate);
};
mOptionsMutePassthroughWhenCaptureButton = std::make_unique<ToggleButton>(TRANS("Mute passthrough while capturing"));
mOptionsMutePassthroughWhenCaptureButton->onClick = [this] () {
toggleBool(processor.m_mute_while_capturing);
};
mOptionsMuteProcessedWhenCaptureButton = std::make_unique<ToggleButton>(TRANS("Mute processed audio output while capturing"));
mOptionsMuteProcessedWhenCaptureButton->onClick = [this] () {
toggleBool(processor.m_mute_processed_while_capturing);
};
mOptionsSaveCaptureToDiskButton = std::make_unique<ToggleButton>(TRANS("Save captured audio to disk"));
mOptionsSaveCaptureToDiskButton->onClick = [this] () {
toggleBool(processor.m_save_captured_audio);
};
mOptionsEndRecordingAfterMaxButton = std::make_unique<ToggleButton>(TRANS("End recording after capturing max length"));
mOptionsEndRecordingAfterMaxButton->onClick = [this] () {
toggleBool(processor.m_auto_finish_record);
};
mOptionsSliderSnapToMouseButton = std::make_unique<ToggleButton>(TRANS("Sliders jump to position"));
mOptionsSliderSnapToMouseButton->onClick = [this] () {
toggleBool(processor.m_use_jumpsliders);
if (updateSliderSnap)
updateSliderSnap();
};
mOptionsShowTechnicalInfoButton = std::make_unique<ToggleButton>(TRANS("Show technical info in waveform"));
mOptionsShowTechnicalInfoButton->onClick = [this] () {
toggleBool(processor.m_show_technical_info);
};
mRecFormatChoice = std::make_unique<SonoChoiceButton>();
mRecFormatChoice->addChoiceListener(this);
mRecFormatChoice->addItem(TRANS("FLAC"), PaulstretchpluginAudioProcessor::FileFormatFLAC);
mRecFormatChoice->addItem(TRANS("WAV"), PaulstretchpluginAudioProcessor::FileFormatWAV);
mRecFormatChoice->addItem(TRANS("OGG"), PaulstretchpluginAudioProcessor::FileFormatOGG);
mRecBitsChoice = std::make_unique<SonoChoiceButton>();
mRecBitsChoice->addChoiceListener(this);
mRecBitsChoice->addItem(TRANS("16 bit"), 16);
mRecBitsChoice->addItem(TRANS("24 bit"), 24);
mRecBitsChoice->addItem(TRANS("32 bit"), 32);
mRecFormatStaticLabel = std::make_unique<Label>("", TRANS("Recorded File Format:"));
configLabel(mRecFormatStaticLabel.get(), false);
mRecFormatStaticLabel->setJustificationType(Justification::centredRight);
mRecLocationStaticLabel = std::make_unique<Label>("", TRANS("Record Location:"));
configLabel(mRecLocationStaticLabel.get(), false);
mRecLocationStaticLabel->setJustificationType(Justification::centredRight);
mRecLocationButton = std::make_unique<TextButton>("fileloc");
mRecLocationButton->setButtonText("");
mRecLocationButton->setLookAndFeel(&smallLNF);
mRecLocationButton->addListener(this);
mOptionsCaptureBufferStaticLabel = std::make_unique<Label>("", TRANS("Capture Buffer Length:"));
configLabel(mOptionsCaptureBufferStaticLabel.get(), false);
mOptionsCaptureBufferStaticLabel->setJustificationType(Justification::centredRight);
mCaptureBufferChoice = std::make_unique<SonoChoiceButton>();
mCaptureBufferChoice->addChoiceListener(this);
mCaptureBufferChoice->addItem(TRANS("2 seconds"), 2);
mCaptureBufferChoice->addItem(TRANS("5 seconds"), 5);
mCaptureBufferChoice->addItem(TRANS("10 seconds"), 10);
mCaptureBufferChoice->addItem(TRANS("30 seconds"), 30);
mCaptureBufferChoice->addItem(TRANS("60 seconds"), 60);
mCaptureBufferChoice->addItem(TRANS("120 seconds"), 120);
mOptionsDumpPresetToClipboardButton = std::make_unique<TextButton>("dump");
mOptionsDumpPresetToClipboardButton->setButtonText(TRANS("Dump Preset to clipboard"));
mOptionsDumpPresetToClipboardButton->setLookAndFeel(&smallLNF);
mOptionsDumpPresetToClipboardButton->addListener(this);
mOptionsResetParamsButton = std::make_unique<TextButton>("reset");
mOptionsResetParamsButton->setButtonText(TRANS("Reset Parameters"));
mOptionsResetParamsButton->setLookAndFeel(&smallLNF);
mOptionsResetParamsButton->onClick = [this] () {
processor.resetParameters();
};
addAndMakeVisible(mSettingsTab.get());
mOptionsComponent->addAndMakeVisible(mCaptureBufferChoice.get());
mOptionsComponent->addAndMakeVisible(mOptionsCaptureBufferStaticLabel.get());
mOptionsComponent->addAndMakeVisible(mOptionsLoadFileWithPluginButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsPlayWithTransportButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsCaptureWithTransportButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsRestorePlayStateButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsMutePassthroughWhenCaptureButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsMuteProcessedWhenCaptureButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsSaveCaptureToDiskButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsEndRecordingAfterMaxButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsSliderSnapToMouseButton.get());
#if JUCE_DEBUG
mOptionsComponent->addAndMakeVisible(mOptionsDumpPresetToClipboardButton.get());
#endif
mOptionsComponent->addAndMakeVisible(mOptionsShowTechnicalInfoButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsResetParamsButton.get());
mOptionsComponent->addAndMakeVisible(mOptionsSliderSnapToMouseButton.get());
mOptionsComponent->addAndMakeVisible(mRecFormatChoice.get());
mOptionsComponent->addAndMakeVisible(mRecFormatChoice.get());
mOptionsComponent->addAndMakeVisible(mRecBitsChoice.get());
mOptionsComponent->addAndMakeVisible(mRecFormatStaticLabel.get());
mOptionsComponent->addAndMakeVisible(mRecLocationButton.get());
mOptionsComponent->addAndMakeVisible(mRecLocationStaticLabel.get());
if (JUCEApplicationBase::isStandaloneApp() && getAudioDeviceManager && getAudioDeviceManager())
{
if (!mAudioDeviceSelector) {
int minNumInputs = std::numeric_limits<int>::max(), maxNumInputs = 0,
minNumOutputs = std::numeric_limits<int>::max(), maxNumOutputs = 0;
auto updateMinAndMax = [] (int newValue, int& minValue, int& maxValue)
{
minValue = jmin (minValue, newValue);
maxValue = jmax (maxValue, newValue);
};
/*
if (channelConfiguration.size() > 0)
{
auto defaultConfig = channelConfiguration.getReference (0);
updateMinAndMax ((int) defaultConfig.numIns, minNumInputs, maxNumInputs);
updateMinAndMax ((int) defaultConfig.numOuts, minNumOutputs, maxNumOutputs);
}
*/
if (auto* bus = processor.getBus (true, 0)) {
auto maxsup = bus->getMaxSupportedChannels(128);
updateMinAndMax (maxsup, minNumInputs, maxNumInputs);
updateMinAndMax (bus->getDefaultLayout().size(), minNumInputs, maxNumInputs);
if (bus->isNumberOfChannelsSupported(1)) {
updateMinAndMax (1, minNumInputs, maxNumInputs);
}
if (bus->isNumberOfChannelsSupported(0)) {
updateMinAndMax (0, minNumInputs, maxNumInputs);
}
}
if (auto* bus = processor.getBus (false, 0)) {
auto maxsup = bus->getMaxSupportedChannels(128);
updateMinAndMax (maxsup, minNumOutputs, maxNumOutputs);
updateMinAndMax (bus->getDefaultLayout().size(), minNumOutputs, maxNumOutputs);
if (bus->isNumberOfChannelsSupported(1)) {
updateMinAndMax (1, minNumOutputs, maxNumOutputs);
}
if (bus->isNumberOfChannelsSupported(0)) {
updateMinAndMax (0, minNumOutputs, maxNumOutputs);
}
}
minNumInputs = jmin (minNumInputs, maxNumInputs);
minNumOutputs = jmin (minNumOutputs, maxNumOutputs);
mAudioDeviceSelector = std::make_unique<AudioDeviceSelectorComponent>(*getAudioDeviceManager(),
minNumInputs, maxNumInputs,
minNumOutputs, maxNumOutputs,
false, // show MIDI input
false,
false, false);
#if JUCE_IOS || JUCE_ANDROID
mAudioDeviceSelector->setItemHeight(44);
#endif
mAudioOptionsViewport = std::make_unique<Viewport>();
mAudioOptionsViewport->setViewedComponent(mAudioDeviceSelector.get(), false);
}
if (firsttime) {
mSettingsTab->addTab(TRANS("AUDIO"), Colour::fromFloatRGBA(0.1, 0.1, 0.1, 1.0), mAudioOptionsViewport.get(), false);
}
}
createAbout();
mOtherOptionsViewport = std::make_unique<Viewport>();
mOtherOptionsViewport->setViewedComponent(mOptionsComponent.get(), false);
mSettingsTab->addTab(TRANS("OPTIONS"),Colour::fromFloatRGBA(0.1, 0.1, 0.1, 1.0), mOtherOptionsViewport.get(), false);
mSettingsTab->addTab(TRANS("ABOUT"), Colour::fromFloatRGBA(0.1, 0.1, 0.1, 1.0), mAboutViewport.get(), false);
setFocusContainerType(FocusContainerType::keyboardFocusContainer);
mSettingsTab->setFocusContainerType(FocusContainerType::none);
mSettingsTab->getTabbedButtonBar().setFocusContainerType(FocusContainerType::none);
mSettingsTab->getTabbedButtonBar().setWantsKeyboardFocus(true);
mSettingsTab->setWantsKeyboardFocus(true);
for (int i=0; i < mSettingsTab->getTabbedButtonBar().getNumTabs(); ++i) {
if (auto tabbut = mSettingsTab->getTabbedButtonBar().getTabButton(i)) {
tabbut->setRadioGroupId(3);
tabbut->setWantsKeyboardFocus(true);
}
if (auto tabcomp = mSettingsTab->getTabContentComponent(i)) {
tabcomp->setFocusContainerType(FocusContainerType::focusContainer);
}
}
}
OptionsView::~OptionsView() {}
juce::Rectangle<int> OptionsView::getMinimumContentBounds() const {
int defWidth = 200;
int defHeight = 100;
return Rectangle<int>(0,0,defWidth,defHeight);
}
juce::Rectangle<int> OptionsView::getPreferredContentBounds() const
{
return Rectangle<int> (0, 0, 300, prefHeight);
}
void OptionsView::timerCallback(int timerid)
{
}
void OptionsView::grabInitialFocus()
{
if (auto * butt = mSettingsTab->getTabbedButtonBar().getTabButton(mSettingsTab->getCurrentTabIndex())) {
butt->setWantsKeyboardFocus(true);
butt->grabKeyboardFocus();
}
}
void OptionsView::configLabel(Label *label, bool val)
{
if (val) {
label->setFont(12);
label->setColour(Label::textColourId, Colour(0x90eeeeee));
label->setJustificationType(Justification::centred);
}
else {
label->setFont(14);
//label->setColour(Label::textColourId, Colour(0xaaeeeeee));
label->setJustificationType(Justification::centredLeft);
}
}
void OptionsView::configLevelSlider(Slider * slider)
{
//slider->setVelocityBasedMode(true);
//slider->setVelocityModeParameters(2.5, 1, 0.05);
//slider->setTextBoxStyle(Slider::NoTextBox, true, 40, 18);
slider->setSliderStyle(Slider::SliderStyle::LinearHorizontal);
slider->setTextBoxStyle(Slider::TextBoxAbove, true, 50, 14);
slider->setMouseDragSensitivity(128);
slider->setScrollWheelEnabled(false);
//slider->setPopupDisplayEnabled(true, false, this);
slider->setColour(Slider::textBoxBackgroundColourId, Colours::transparentBlack);
slider->setColour(Slider::textBoxOutlineColourId, Colours::transparentBlack);
slider->setColour(Slider::textBoxTextColourId, Colour(0x90eeeeee));
slider->setColour(TooltipWindow::textColourId, Colour(0xf0eeeeee));
slider->setLookAndFeel(&sonoSliderLNF);
}
void OptionsView::configEditor(TextEditor *editor, bool passwd)
{
editor->addListener(this);
if (passwd) {
editor->setIndents(8, 6);
} else {
editor->setIndents(8, 8);
}
}
void OptionsView::updateState(bool ignorecheck)
{
mRecFormatChoice->setSelectedId((int)processor.getDefaultRecordingFormat(), dontSendNotification);
mRecBitsChoice->setSelectedId((int)processor.getDefaultRecordingBitsPerSample(), dontSendNotification);
File recdir = File(processor.getDefaultRecordingDirectory());
String dispath = recdir.getRelativePathFrom(File::getSpecialLocation (File::userHomeDirectory));
if (dispath.startsWith(".")) dispath = processor.getDefaultRecordingDirectory();
mRecLocationButton->setButtonText(dispath);
mOptionsLoadFileWithPluginButton->setToggleState(processor.m_load_file_with_state, dontSendNotification);
mOptionsPlayWithTransportButton->setToggleState(processor.m_play_when_host_plays, dontSendNotification);
mOptionsCaptureWithTransportButton->setToggleState(processor.m_capture_when_host_plays, dontSendNotification);
mOptionsRestorePlayStateButton->setToggleState(processor.m_restore_playstate, dontSendNotification);
mOptionsMutePassthroughWhenCaptureButton->setToggleState(processor.m_mute_while_capturing, dontSendNotification);
mOptionsMuteProcessedWhenCaptureButton->setToggleState(processor.m_mute_processed_while_capturing, dontSendNotification);
mOptionsSaveCaptureToDiskButton->setToggleState(processor.m_save_captured_audio, dontSendNotification);
mOptionsEndRecordingAfterMaxButton->setToggleState(processor.m_auto_finish_record, dontSendNotification);
mOptionsSliderSnapToMouseButton->setToggleState(processor.m_use_jumpsliders, dontSendNotification);
mOptionsShowTechnicalInfoButton->setToggleState(processor.m_show_technical_info, dontSendNotification);
auto caplen = processor.getFloatParameter(cpi_max_capture_len)->get();
mCaptureBufferChoice->setSelectedId((int)caplen, dontSendNotification);
}
void OptionsView::createAbout()
{
mAboutLabel = std::make_unique<Label>();
String fftlib;
#if PS_USE_VDSP_FFT
fftlib = "vDSP";
#elif PS_USE_PFFFT
fftlib = "pffft";
#else
fftlib = fftwf_version;
#endif
String juceversiontxt = String("JUCE ") + String(JUCE_MAJOR_VERSION) + "." + String(JUCE_MINOR_VERSION);
String title = String(JucePlugin_Name) + " " + String(JucePlugin_VersionString);
#ifdef JUCE_DEBUG
title += " (DEBUG)";
#endif
String vstInfo;
if (processor.wrapperType == AudioProcessor::wrapperType_VST ||
processor.wrapperType == AudioProcessor::wrapperType_VST3)
vstInfo = "VST Plug-In Technology by Steinberg.\n\n";
PluginHostType host;
String text = title + "\n\n" +
"Plugin/Application for extreme time stretching and other sound processing\nBuilt on " + String(__DATE__) + " " + String(__TIME__) + "\n"
"Copyright (C) 2006-2011 Nasca Octavian Paul, Tg. Mures, Romania\n"
"(C) 2017-2021 Xenakios\n"
"(C) 2022 Jesse Chappell\n\n"
+vstInfo;
if (fftlib.isNotEmpty())
text += String("Using ") + fftlib + String(" for FFT\n\n");
#if !JUCE_IOS
if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_AAX) {
text += juceversiontxt + String("\n\n");
}
else {
text += juceversiontxt + String(" used under the GPL license.\n\n");
}
#endif
text += String("GPL licensed source code at : https://github.com/essej/paulxstretch\n");
if (host.type != juce::PluginHostType::UnknownHost) {
text += String("Running in : ") + host.getHostDescription()+ String("\n");
}
mAboutLabel->setJustificationType(Justification::centred);
mAboutLabel->setText(text, dontSendNotification);
mAboutViewport = std::make_unique<Viewport>();
mAboutViewport->setViewedComponent(mAboutLabel.get(), false);
//std::unique_ptr<SettingsComponent> contptr(content);
int defWidth = 450;
int defHeight = 350;
#if JUCE_IOS
defWidth = 320;
defHeight = 350;
#endif
mAboutLabel->setSize (defWidth, defHeight);
}
void OptionsView::resized()
{
int leftmargin = 10;
int minw = 100;
int minKnobWidth = 50;
int minSliderWidth = 50;
int minPannerWidth = 40;
int maxPannerWidth = 100;
int minitemheight = 36;
int knobitemheight = 80;
int minpassheight = 30;
int setitemheight = 36;
int minButtonWidth = 90;
int sliderheight = 44;
int inmeterwidth = 22 ;
int outmeterwidth = 22 ;
int servLabelWidth = 72;
int iconheight = 24;
int iconwidth = iconheight;
int knoblabelheight = 18;
int panbuttwidth = 26;
#if JUCE_IOS || JUCE_ANDROID
// make the button heights a bit more for touchscreen purposes
minitemheight = 44;
knobitemheight = 90;
minpassheight = 38;
panbuttwidth = 32;
#endif
FlexBox mainBox;
mainBox.flexDirection = FlexBox::Direction::column;
mainBox.items.add(FlexItem(100, minitemheight, *mSettingsTab).withMargin(0).withFlex(1));
mainBox.performLayout(getLocalBounds().reduced(2, 2));
auto innerbounds = mSettingsTab->getLocalBounds();
if (mAudioDeviceSelector) {
mAudioDeviceSelector->setBounds(Rectangle<int>(0,0,innerbounds.getWidth() - 10,mAudioDeviceSelector->getHeight()));
}
mOptionsComponent->setBounds(Rectangle<int>(0,0,innerbounds.getWidth() - 10, minOptionsHeight));
if (mAboutLabel) {
mAboutLabel->setBounds(Rectangle<int>(0,0,innerbounds.getWidth() - 10, mAboutLabel->getHeight()));
}
FlexBox lfwpBox;
lfwpBox.flexDirection = FlexBox::Direction::row;
lfwpBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
lfwpBox.items.add(FlexItem(minw, minpassheight, *mOptionsLoadFileWithPluginButton).withMargin(0).withFlex(1));
FlexBox pwtBox;
pwtBox.flexDirection = FlexBox::Direction::row;
pwtBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
pwtBox.items.add(FlexItem(minw, minpassheight, *mOptionsPlayWithTransportButton).withMargin(0).withFlex(1));
FlexBox cwtBox;
cwtBox.flexDirection = FlexBox::Direction::row;
cwtBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
cwtBox.items.add(FlexItem(minw, minpassheight, *mOptionsCaptureWithTransportButton).withMargin(0).withFlex(1));
FlexBox capbufBox;
capbufBox.flexDirection = FlexBox::Direction::row;
capbufBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
capbufBox.items.add(FlexItem(130, minitemheight, *mOptionsCaptureBufferStaticLabel).withMargin(0).withFlex(0));
capbufBox.items.add(FlexItem(5, 12).withFlex(0));
capbufBox.items.add(FlexItem(minw, minitemheight, *mCaptureBufferChoice).withMargin(0).withFlex(1));
FlexBox rpsBox;
rpsBox.flexDirection = FlexBox::Direction::row;
rpsBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
rpsBox.items.add(FlexItem(minw, minpassheight, *mOptionsRestorePlayStateButton).withMargin(0).withFlex(1));
FlexBox mpwcBox;
mpwcBox.flexDirection = FlexBox::Direction::row;
mpwcBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
mpwcBox.items.add(FlexItem(minw, minpassheight, *mOptionsMutePassthroughWhenCaptureButton).withMargin(0).withFlex(1));
FlexBox mprwcBox;
mprwcBox.flexDirection = FlexBox::Direction::row;
mprwcBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
mprwcBox.items.add(FlexItem(minw, minpassheight, *mOptionsMuteProcessedWhenCaptureButton).withMargin(0).withFlex(1));
FlexBox sctdBox;
sctdBox.flexDirection = FlexBox::Direction::row;
sctdBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
sctdBox.items.add(FlexItem(minw, minpassheight, *mOptionsSaveCaptureToDiskButton).withMargin(0).withFlex(1));
FlexBox eramBox;
eramBox.flexDirection = FlexBox::Direction::row;
eramBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
eramBox.items.add(FlexItem(minw, minpassheight, *mOptionsEndRecordingAfterMaxButton).withMargin(0).withFlex(1));
FlexBox ssnapBox;
ssnapBox.flexDirection = FlexBox::Direction::row;
ssnapBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
ssnapBox.items.add(FlexItem(minw, minpassheight, *mOptionsSliderSnapToMouseButton).withMargin(0).withFlex(1));
FlexBox dumpBox;
dumpBox.flexDirection = FlexBox::Direction::row;
dumpBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
dumpBox.items.add(FlexItem(minw, minpassheight, *mOptionsDumpPresetToClipboardButton).withMargin(0).withFlex(1));
FlexBox resetBox;
resetBox.flexDirection = FlexBox::Direction::row;
resetBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
resetBox.items.add(FlexItem(minw, minpassheight, *mOptionsResetParamsButton).withMargin(0).withFlex(1));
FlexBox showtiBox;
showtiBox.flexDirection = FlexBox::Direction::row;
showtiBox.items.add(FlexItem(leftmargin, 12).withFlex(0));
showtiBox.items.add(FlexItem(minw, minpassheight, *mOptionsShowTechnicalInfoButton).withMargin(0).withFlex(1));
FlexBox optionsRecordDirBox;
optionsRecordDirBox.flexDirection = FlexBox::Direction::row;
optionsRecordDirBox.items.add(FlexItem(115, minitemheight, *mRecLocationStaticLabel).withMargin(0).withFlex(0));
optionsRecordDirBox.items.add(FlexItem(minButtonWidth, minitemheight, *mRecLocationButton).withMargin(0).withFlex(3));
FlexBox optionsRecordFormatBox;
optionsRecordFormatBox.flexDirection = FlexBox::Direction::row;
optionsRecordFormatBox.items.add(FlexItem(115, minitemheight, *mRecFormatStaticLabel).withMargin(0).withFlex(0));
optionsRecordFormatBox.items.add(FlexItem(minButtonWidth, minitemheight, *mRecFormatChoice).withMargin(0).withFlex(1));
optionsRecordFormatBox.items.add(FlexItem(2, 4));
optionsRecordFormatBox.items.add(FlexItem(80, minitemheight, *mRecBitsChoice).withMargin(0).withFlex(0.25));
int vgap = 0;
FlexBox optionsBox;
optionsBox.flexDirection = FlexBox::Direction::column;
optionsBox.items.add(FlexItem(4, 6));
optionsBox.items.add(FlexItem(minw, minpassheight, lfwpBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, pwtBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, cwtBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, rpsBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap + 6));
optionsBox.items.add(FlexItem(minw, minitemheight, capbufBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, mpwcBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, mprwcBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, sctdBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, eramBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap + 6));
optionsBox.items.add(FlexItem(minw, minpassheight, ssnapBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(minw, minpassheight, showtiBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap + 6));
#if !(JUCE_IOS || JUCE_ANDROID)
optionsBox.items.add(FlexItem(minw, minitemheight, optionsRecordDirBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap + 2));
#endif
optionsBox.items.add(FlexItem(minw, minitemheight, optionsRecordFormatBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
optionsBox.items.add(FlexItem(4, vgap + 4));
optionsBox.items.add(FlexItem(minw, minpassheight, resetBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
#if JUCE_DEBUG
optionsBox.items.add(FlexItem(4, vgap + 4));
optionsBox.items.add(FlexItem(minw, minpassheight, dumpBox).withMargin(2).withFlex(0));
optionsBox.items.add(FlexItem(4, vgap));
#endif
minOptionsHeight = 0;
for (auto & item : optionsBox.items) {
minOptionsHeight += item.minHeight + item.margin.top + item.margin.bottom;
}
optionsBox.performLayout(mOptionsComponent->getLocalBounds());
prefHeight = minOptionsHeight + mSettingsTab->getTabBarDepth();
}
void OptionsView::showAudioTab()
{
if (mSettingsTab->getNumTabs() == 3) {
mSettingsTab->setCurrentTabIndex(0);
}
}
void OptionsView::showOptionsTab()
{
mSettingsTab->setCurrentTabIndex(mSettingsTab->getNumTabs() == 3 ? 1 : 0);
}
void OptionsView::showAboutTab()
{
mSettingsTab->setCurrentTabIndex(mSettingsTab->getNumTabs() == 3 ? 2 : 1);
}
void OptionsView::optionsTabChanged (int newCurrentTabIndex)
{
}
void OptionsView::showWarnings()
{
}
void OptionsView::textEditorReturnKeyPressed (TextEditor& ed)
{
DBG("Return pressed");
// if (&ed == mOptionsUdpPortEditor.get()) {
// int port = mOptionsUdpPortEditor->getText().getIntValue();
// //changeUdpPort(port);
// }
}
void OptionsView::textEditorEscapeKeyPressed (TextEditor& ed)
{
DBG("escape pressed");
}
void OptionsView::textEditorTextChanged (TextEditor& ed)
{
}
void OptionsView::textEditorFocusLost (TextEditor& ed)
{
// only one we care about live is udp port
//if (&ed == mOptionsUdpPortEditor.get()) {
// int port = mOptionsUdpPortEditor->getText().getIntValue();
// //changeUdpPort(port);
//}
}
void OptionsView::buttonClicked (Button* buttonThatWasClicked)
{
if (buttonThatWasClicked == mRecLocationButton.get()) {
// browse folder chooser
SafePointer<OptionsView> safeThis (this);
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage))
{
RuntimePermissions::request (RuntimePermissions::readExternalStorage,
[safeThis] (bool granted) mutable
{
if (granted)
safeThis->buttonClicked (safeThis->mRecLocationButton.get());
});
return;
}
chooseRecDirBrowser();
}
else if (buttonThatWasClicked == mOptionsDumpPresetToClipboardButton.get()) {
ValueTree tree = processor.getStateTree(true, true);
MemoryBlock destData;
MemoryOutputStream stream(destData, true);
tree.writeToStream(stream);
String txt = Base64::toBase64(destData.getData(), destData.getSize());
SystemClipboard::copyTextToClipboard(txt);
}
}
void OptionsView::choiceButtonSelected(SonoChoiceButton *comp, int index, int ident)
{
if (comp == mRecFormatChoice.get()) {
processor.setDefaultRecordingFormat((PaulstretchpluginAudioProcessor::RecordFileFormat) ident);
}
else if (comp == mRecBitsChoice.get()) {
processor.setDefaultRecordingBitsPerSample(ident);
}
else if (comp == mCaptureBufferChoice.get()) {
*processor.getFloatParameter(cpi_max_capture_len) = (float) ident;
}
}
void OptionsView::chooseRecDirBrowser()
{
SafePointer<OptionsView> safeThis (this);
if (FileChooser::isPlatformDialogAvailable())
{
File recdir = File(processor.getDefaultRecordingDirectory());
mFileChooser.reset(new FileChooser(TRANS("Choose the folder for new recordings"),
recdir,
"",
true, false, getTopLevelComponent()));
mFileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories,
[safeThis] (const FileChooser& chooser) mutable
{
auto results = chooser.getURLResults();
if (safeThis != nullptr && results.size() > 0)
{
auto url = results.getReference (0);
DBG("Chose directory: " << url.toString(false));
if (url.isLocalFile()) {
File lfile = url.getLocalFile();
if (lfile.isDirectory()) {
safeThis->processor.setDefaultRecordingDirectory(lfile.getFullPathName());
} else {
safeThis->processor.setDefaultRecordingDirectory(lfile.getParentDirectory().getFullPathName());
}
safeThis->updateState();
}
}
if (safeThis) {
safeThis->mFileChooser.reset();
}
}, nullptr);
}
else {
DBG("Need to enable code signing");
}
}
void OptionsView::showPopTip(const String & message, int timeoutMs, Component * target, int maxwidth)
{
popTip.reset(new BubbleMessageComponent());
popTip->setAllowedPlacement(BubbleComponent::above);
if (target) {
if (auto * parent = target->findParentComponentOfClass<AudioProcessorEditor>()) {
parent->addChildComponent (popTip.get());
} else {
addChildComponent(popTip.get());
}
}
else {
addChildComponent(popTip.get());
}
AttributedString text(message);
text.setJustification (Justification::centred);
text.setColour (findColour (TextButton::textColourOffId));
text.setFont(Font(12));
if (target) {
popTip->showAt(target, text, timeoutMs);
}
else {
Rectangle<int> topbox(getWidth()/2 - maxwidth/2, 0, maxwidth, 2);
popTip->showAt(topbox, text, timeoutMs);
}
popTip->toFront(false);
//AccessibilityHandler::postAnnouncement(message, AccessibilityHandler::AnnouncementPriority::high);
}
void OptionsView::paint(Graphics & g)
{
/*
//g.fillAll (Colours::black);
Rectangle<int> bounds = getLocalBounds();
bounds.reduce(1, 1);
bounds.removeFromLeft(3);
g.setColour(bgColor);
g.fillRoundedRectangle(bounds.toFloat(), 6.0f);
g.setColour(outlineColor);
g.drawRoundedRectangle(bounds.toFloat(), 6.0f, 0.5f);
*/
}

148
Source/OptionsView.h Normal file
View File

@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2021 Jesse Chappell
#pragma once
#include "JuceHeader.h"
#include "PluginProcessor.h"
#include "CustomLookAndFeel.h"
#include "SonoChoiceButton.h"
#include "GenericItemChooser.h"
class OptionsView :
public Component,
public Button::Listener,
public SonoChoiceButton::Listener,
public GenericItemChooser::Listener,
public TextEditor::Listener,
public MultiTimer
{
public:
OptionsView(PaulstretchpluginAudioProcessor& proc, std::function<AudioDeviceManager*()> getaudiodevicemanager);
virtual ~OptionsView();
class Listener {
public:
virtual ~Listener() {}
virtual void optionsChanged(OptionsView *comp) {}
};
void addListener(Listener * listener) { listeners.add(listener); }
void removeListener(Listener * listener) { listeners.remove(listener); }
void timerCallback(int timerid) override;
void buttonClicked (Button* buttonThatWasClicked) override;
void choiceButtonSelected(SonoChoiceButton *comp, int index, int ident) override;
void textEditorReturnKeyPressed (TextEditor&) override;
void textEditorEscapeKeyPressed (TextEditor&) override;
void textEditorTextChanged (TextEditor&) override;
void textEditorFocusLost (TextEditor&) override;
juce::Rectangle<int> getMinimumContentBounds() const;
juce::Rectangle<int> getPreferredContentBounds() const;
void grabInitialFocus();
void updateState(bool ignorecheck=false);
void paint(Graphics & g) override;
void resized() override;
void showPopTip(const String & message, int timeoutMs, Component * target, int maxwidth=100);
void optionsTabChanged (int newCurrentTabIndex);
void showAudioTab();
void showOptionsTab();
void showAboutTab();
void showWarnings();
std::function<AudioDeviceManager*()> getAudioDeviceManager; // = []() { return 0; };
std::function<void()> updateSliderSnap; // = []() { return 0; };
std::function<void()> updateKeybindings; // = []() { return 0; };
std::function<void()> saveSettingsIfNeeded; // = []() { return 0; };
protected:
void configEditor(TextEditor *editor, bool passwd = false);
void configLabel(Label *label, bool val=false);
void configLevelSlider(Slider *);
void chooseRecDirBrowser();
void createAbout();
PaulstretchpluginAudioProcessor& processor;
CustomBigTextLookAndFeel smallLNF;
CustomBigTextLookAndFeel sonoSliderLNF;
ListenerList<Listener> listeners;
std::unique_ptr<AudioDeviceSelectorComponent> mAudioDeviceSelector;
std::unique_ptr<Viewport> mAudioOptionsViewport;
std::unique_ptr<Viewport> mOtherOptionsViewport;
std::unique_ptr<Viewport> mRecordOptionsViewport;
std::unique_ptr<Component> mOptionsComponent;
std::unique_ptr<Viewport> mAboutViewport;
std::unique_ptr<Label> mAboutLabel;
int minOptionsHeight = 0;
int minRecOptionsHeight = 0;
uint32 settingsClosedTimestamp = 0;
std::unique_ptr<FileChooser> mFileChooser;
std::unique_ptr<SonoChoiceButton> mCaptureBufferChoice;
std::unique_ptr<Label> mOptionsCaptureBufferStaticLabel;
std::unique_ptr<ToggleButton> mOptionsLoadFileWithPluginButton;
std::unique_ptr<ToggleButton> mOptionsPlayWithTransportButton;
std::unique_ptr<ToggleButton> mOptionsCaptureWithTransportButton;
std::unique_ptr<ToggleButton> mOptionsRestorePlayStateButton;
std::unique_ptr<ToggleButton> mOptionsMutePassthroughWhenCaptureButton;
std::unique_ptr<ToggleButton> mOptionsMuteProcessedWhenCaptureButton;
std::unique_ptr<ToggleButton> mOptionsSaveCaptureToDiskButton;
std::unique_ptr<ToggleButton> mOptionsEndRecordingAfterMaxButton;
std::unique_ptr<ToggleButton> mOptionsSliderSnapToMouseButton;
std::unique_ptr<TextButton> mOptionsDumpPresetToClipboardButton;
std::unique_ptr<ToggleButton> mOptionsShowTechnicalInfoButton;
std::unique_ptr<TextButton> mOptionsResetParamsButton;
std::unique_ptr<SonoChoiceButton> mRecFormatChoice;
std::unique_ptr<SonoChoiceButton> mRecBitsChoice;
std::unique_ptr<Label> mRecFormatStaticLabel;
std::unique_ptr<Label> mRecLocationStaticLabel;
std::unique_ptr<TextButton> mRecLocationButton;
std::unique_ptr<TabbedComponent> mSettingsTab;
int minHeight = 0;
int prefHeight = 0;
bool firsttime = true;
// keep this down here, so it gets destroyed early
std::unique_ptr<BubbleMessageComponent> popTip;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OptionsView)
};

View File

@ -281,7 +281,7 @@ inline void callGUI(T* ap, F&& f, bool async)
}
}
inline String secondsToString2(double secs)
inline String secondsToString2(double secs, bool fractional=true)
{
RelativeTime rt(secs);
String result;
@ -292,7 +292,7 @@ inline String secondsToString2(double secs)
result << String((int)rt.inMinutes() % 60).paddedLeft('0', 2) << ':';
result << String((int)rt.inSeconds() % 60).paddedLeft('0', 2);
auto millis = (int)rt.inMilliseconds() % 1000;
if (millis > 0)
if (fractional && millis > 0)
result << '.' << String(millis).paddedLeft('0', 3);
return result.trimEnd();
}

View File

@ -8,11 +8,7 @@
#include "RenderSettingsComponent.h"
#include "CrossPlatformUtils.h"
static void handleSettingsMenuModalCallback(int choice, PaulstretchpluginAudioProcessorEditor* ed)
{
ed->executeModalMenuAction(0,choice);
}
#include "OptionsView.h"
enum ParameterGroupIds
{
@ -27,24 +23,6 @@ enum ParameterGroupIds
CompressGroup = 8
};
enum SettingsMenuIds
{
SettingsPlayHostTransId = 1,
SettingsCaptureHostTransId = 2,
SettingsAboutId = 3,
SettingsResetParametersId = 4,
SettingsLoadFileWithStateId = 5,
SettingsDumpPresetClipboardId = 6,
SettingsShowTechInfoId = 7,
SettingsMutePassthruCaptureId = 8,
SettingsSaveCaptureDiskId = 9,
SettingsMuteProcessedCaptureId = 10,
SettingsAudioSettingsId = 11,
SettingsSliderSnapId = 12,
SettingsRestorePlayStateId = 13,
SettingsAutoEndCaptureId = 14,
};
//==============================================================================
PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(PaulstretchpluginAudioProcessor& p)
@ -94,7 +72,9 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau
addAndMakeVisible(&m_settings_button);
m_settings_button.setButtonText("Settings...");
m_settings_button.onClick = [this]() { showSettingsMenu(); };
m_settings_button.onClick = [this]() {
showSettings(true);
};
if (JUCEApplicationBase::isStandaloneApp())
{
@ -125,6 +105,27 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau
m_info_label.setJustificationType(Justification::centredRight);
m_info_label.setFont(14.0f);
m_recordingButton = std::make_unique<DrawableButton>("rewind", DrawableButton::ButtonStyle::ImageFitted);
std::unique_ptr<Drawable> reconimg(Drawable::createFromImageData(BinaryData::record_output_active_svg, BinaryData::record_output_active_svgSize));
std::unique_ptr<Drawable> recoffimg(Drawable::createFromImageData(BinaryData::record_output_svg, BinaryData::record_output_svgSize));
m_recordingButton->setImages(recoffimg.get(), nullptr, nullptr, nullptr, reconimg.get());
m_recordingButton->setColour(DrawableButton::backgroundOnColourId, Colour::fromFloatRGBA(0.6, 0.3, 0.3, 0.5));
addAndMakeVisible(m_recordingButton.get());
//m_recordingButton->setColour(DrawableButton::backgroundOnColourId, Colours::transparentBlack);
m_recordingButton->setTooltip(TRANS("Start/Stop recording output to file"));
m_recordingButton->setTitle(TRANS("Record Output"));
m_recordingButton->setClickingTogglesState(true);
m_recordingButton->onClick = [this]() { toggleOutputRecording(); };
m_fileRecordingLabel = std::make_unique<Label>("rectime", "");
m_fileRecordingLabel->setJustificationType(Justification::centredBottom);
m_fileRecordingLabel->setFont(12);
m_fileRecordingLabel->setColour(Label::textColourId, Colour(0x88ffbbbb));
m_fileRecordingLabel->setAccessible(false);
addAndMakeVisible(m_fileRecordingLabel.get());
m_wavecomponent.GetFileCallback = [this]() { return processor.getAudioFile(); };
const auto& pars = processor.getParameters();
@ -206,9 +207,9 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau
}
if (auto * recbut = m_parcomps[cpi_capture_trigger]->getDrawableButton()) {
std::unique_ptr<Drawable> reconimg(Drawable::createFromImageData(BinaryData::record_active_svg, BinaryData::record_active_svgSize));
std::unique_ptr<Drawable> recoffimg(Drawable::createFromImageData(BinaryData::record_svg, BinaryData::record_svgSize));
recbut->setImages(recoffimg.get(), nullptr, nullptr, nullptr, reconimg.get());
std::unique_ptr<Drawable> ireconimg(Drawable::createFromImageData(BinaryData::record_input_active_svg, BinaryData::record_input_active_svgSize));
std::unique_ptr<Drawable> irecoffimg(Drawable::createFromImageData(BinaryData::record_input_svg, BinaryData::record_input_svgSize));
recbut->setImages(irecoffimg.get(), nullptr, nullptr, nullptr, ireconimg.get());
recbut->setColour(DrawableButton::backgroundOnColourId, Colour::fromFloatRGBA(0.6, 0.3, 0.3, 0.5));
}
@ -586,92 +587,88 @@ void PaulstretchpluginAudioProcessorEditor::showAudioSetup()
}
}
void PaulstretchpluginAudioProcessorEditor::executeModalMenuAction(int menuid, int r)
void PaulstretchpluginAudioProcessorEditor::showSettings(bool flag)
{
enum SettingsMenuIds
{
SettingsPlayHostTransId = 1,
SettingsCaptureHostTransId = 2,
SettingsAboutId = 3,
SettingsResetParametersId = 4,
SettingsLoadFileWithStateId = 5,
SettingsDumpPresetClipboardId = 6,
SettingsShowTechInfoId = 7,
SettingsMutePassthruCaptureId = 8,
SettingsSaveCaptureDiskId = 9,
SettingsMuteProcessedCaptureId = 10,
};
DBG("Got settings click");
if (r >= 200 && r < 210)
{
int caplen = m_capturelens[r - 200];
*processor.getFloatParameter(cpi_max_capture_len) = (float)caplen;
}
else if (r == SettingsPlayHostTransId)
{
toggleBool(processor.m_play_when_host_plays);
}
else if (r == SettingsCaptureHostTransId)
{
toggleBool(processor.m_capture_when_host_plays);
}
else if (r == SettingsMutePassthruCaptureId)
{
toggleBool(processor.m_mute_while_capturing);
}
else if (r == SettingsMuteProcessedCaptureId)
{
toggleBool(processor.m_mute_processed_while_capturing);
}
else if (r == SettingsResetParametersId)
{
processor.resetParameters();
}
else if (r == SettingsLoadFileWithStateId)
{
toggleBool(processor.m_load_file_with_state);
}
else if (r == SettingsSaveCaptureDiskId)
{
toggleBool(processor.m_save_captured_audio);
}
else if (r == SettingsSliderSnapId)
{
toggleBool(processor.m_use_jumpsliders);
}
else if (r == SettingsAutoEndCaptureId)
{
toggleBool(processor.m_auto_finish_record);
}
else if (r == SettingsRestorePlayStateId)
{
toggleBool(processor.m_restore_playstate);
}
else if (r == SettingsAboutId)
{
showAbout();
}
else if (r == SettingsAudioSettingsId)
{
showAudioSetup();
}
if (flag && settingsCalloutBox == nullptr) {
//Viewport * wrap = new Viewport();
Component* dw = this;
#if JUCE_IOS || JUCE_ANDROID
int defWidth = 320;
int defHeight = 420;
#else
int defWidth = 340;
int defHeight = 400;
#endif
else if (r == SettingsDumpPresetClipboardId)
{
ValueTree tree = processor.getStateTree(true, true);
MemoryBlock destData;
MemoryOutputStream stream(destData, true);
tree.writeToStream(stream);
String txt = Base64::toBase64(destData.getData(), destData.getSize());
SystemClipboard::copyTextToClipboard(txt);
}
else if (r == SettingsShowTechInfoId)
{
toggleBool(processor.m_show_technical_info);
processor.m_propsfile->m_props_file->setValue("showtechnicalinfo", processor.m_show_technical_info);
}
bool firsttime = false;
if (!m_optionsView) {
m_optionsView = std::make_unique<OptionsView>(processor, getAudioDeviceManager);
m_optionsView->updateSliderSnap = [this]() { updateAllSliders(); };
//m_optionsView->saveSettingsIfNeeded = [this]() { if (saveSettingsIfNeeded) saveSettingsIfNeeded(); };
m_optionsView->addComponentListener(this);
firsttime = true;
}
// presize it so the preferred gets calculated
m_optionsView->setBounds(Rectangle<int>(0,0,defWidth,defHeight));
auto prefbounds = m_optionsView->getPreferredContentBounds();
defHeight = prefbounds.getHeight();
defWidth = jmin(defWidth + 8, dw->getWidth() - 30);
defHeight = jmin(defHeight + 8, dw->getHeight() - 90); // 24
auto wrap = std::make_unique<Component>();
wrap->addAndMakeVisible(m_optionsView.get());
m_optionsView->setBounds(Rectangle<int>(0,0,defWidth,defHeight));
wrap->setSize(defWidth,defHeight);
m_optionsView->updateState();
Rectangle<int> bounds = dw->getLocalArea(nullptr, m_settings_button.getScreenBounds().reduced(10));
DBG("callout bounds: " << bounds.toString());
settingsCalloutBox = & CallOutBox::launchAsynchronously (std::move(wrap), bounds , dw, false);
if (CallOutBox * box = dynamic_cast<CallOutBox*>(settingsCalloutBox.get())) {
box->setDismissalMouseClicksAreAlwaysConsumed(true);
}
settingsClosedTimestamp = 0;
m_optionsView->grabInitialFocus();
}
else {
// dismiss it
if (CallOutBox * box = dynamic_cast<CallOutBox*>(settingsCalloutBox.get())) {
box->dismiss();
settingsCalloutBox = nullptr;
}
}
}
void PaulstretchpluginAudioProcessorEditor::componentParentHierarchyChanged (Component& component)
{
if (&component == m_optionsView.get()) {
if (component.getParentComponent() == nullptr) {
DBG("setting parent changed: " << (uint64) component.getParentComponent());
settingsClosedTimestamp = Time::getMillisecondCounter();
}
}
}
void PaulstretchpluginAudioProcessorEditor::updateAllSliders()
{
for (auto& e : m_parcomps) {
@ -765,6 +762,10 @@ void PaulstretchpluginAudioProcessorEditor::resized()
//togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_bypass_stretch]).withMargin(margin).withFlex(1).withMaxWidth(150));
togglesbox.items.add(FlexItem(buttminw + 15, buttonrowheight, *m_parcomps[cpi_passthrough]).withMargin(margin).withFlex(1.5).withMaxWidth(85));
if (m_recordingButton) {
togglesbox.items.add(FlexItem(0, buttonrowheight).withFlex(0.1).withMaxWidth(10));
togglesbox.items.add(FlexItem(buttminw, buttonrowheight, *m_recordingButton).withMargin(1).withFlex(1).withMaxWidth(65));
}
togglesbox.items.add(FlexItem(2, buttonrowheight));
@ -1057,6 +1058,9 @@ void PaulstretchpluginAudioProcessorEditor::resized()
m_wavecomponent.setBounds(m_wave_container->getX(), 0, m_wave_container->getWidth(),
m_wave_container->getHeight()-zscrollh-1);
if (m_recordingButton) {
m_fileRecordingLabel->setBounds(m_recordingButton->getBounds().removeFromTop(14).translated(0, -9));
}
m_zs.setBounds(m_wave_container->getX(), m_wavecomponent.getBottom(), m_wave_container->getWidth(), zscrollh);
@ -1072,6 +1076,10 @@ void PaulstretchpluginAudioProcessorEditor::resized()
m_groupcontainer->repaint();
}
if (settingsCalloutBox && settingsCalloutBox->isVisible()) {
settingsCalloutBox->toFront(false);
}
}
void PaulstretchpluginAudioProcessorEditor::timerCallback(int id)
@ -1091,9 +1099,9 @@ void PaulstretchpluginAudioProcessorEditor::timerCallback(int id)
m_parcomps[i]->updateComponent();
}
m_free_filter_component.updateParameterComponents();
if (processor.isRecordingEnabled())
if (processor.isInputRecordingEnabled())
{
m_wavecomponent.setRecordingPosition(processor.getRecordingPositionPercent());
m_wavecomponent.setRecordingPosition(processor.getInputRecordingPositionPercent());
} else
m_wavecomponent.setRecordingPosition(-1.0);
m_wavecomponent.setAudioInfo(processor.getSampleRateChecked(), processor.getStretchSource()->getLastSeekPos(),
@ -1143,6 +1151,10 @@ void PaulstretchpluginAudioProcessorEditor::timerCallback(int id)
m_perfmeter.enabled = !enablepar->get();
}
if (m_recordingButton) {
m_recordingButton->setToggleState(processor.isRecordingToFile(), dontSendNotification);
}
}
if (id == 2)
{
@ -1174,6 +1186,10 @@ void PaulstretchpluginAudioProcessorEditor::timerCallback(int id)
updateAllSliders();
if (processor.isRecordingToFile() && m_fileRecordingLabel) {
m_fileRecordingLabel->setText(secondsToString2(processor.getElapsedRecordTime(), false), dontSendNotification);
}
//m_parcomps[cpi_dryplayrate]->setVisible(*processor.getBoolParameter(cpi_bypass_stretch));
//m_parcomps[cpi_stretchamount]->setVisible(!*processor.getBoolParameter(cpi_bypass_stretch));
@ -1228,123 +1244,6 @@ bool PaulstretchpluginAudioProcessorEditor::keyPressed(const KeyPress & press)
return action && action();
}
void PaulstretchpluginAudioProcessorEditor::showSettingsMenu()
{
PopupMenu m_settings_menu;
if (JUCEApplicationBase::isStandaloneApp()) {
m_settings_menu.addItem(11, "Audio Setup...", true, false);
}
m_settings_menu.addItem(SettingsResetParametersId, "Reset parameters", true, false);
m_settings_menu.addSeparator();
m_settings_menu.addItem(SettingsLoadFileWithStateId, "Load file with plugin state", true, processor.m_load_file_with_state);
m_settings_menu.addItem(SettingsPlayHostTransId, "Play when host transport running", true, processor.m_play_when_host_plays);
m_settings_menu.addItem(SettingsCaptureHostTransId, "Capture when host transport running", true, processor.m_capture_when_host_plays);
m_settings_menu.addItem(SettingsRestorePlayStateId, "Restore playing state", true, processor.m_restore_playstate);
m_settings_menu.addSeparator();
m_settings_menu.addItem(SettingsMutePassthruCaptureId, "Mute passthrough while capturing", true, processor.m_mute_while_capturing);
m_settings_menu.addItem(SettingsMuteProcessedCaptureId, "Mute processed audio output while capturing", true, processor.m_mute_processed_while_capturing);
m_settings_menu.addItem(SettingsSaveCaptureDiskId, "Save captured audio to disk", true, processor.m_save_captured_audio);
int capturelen = *processor.getFloatParameter(cpi_max_capture_len);
PopupMenu capturelenmenu;
for (int i=0;i<m_capturelens.size();++i)
capturelenmenu.addItem(200+i, String(m_capturelens[i])+" seconds", true, capturelen == m_capturelens[i]);
m_settings_menu.addSubMenu("Capture buffer length", capturelenmenu);
m_settings_menu.addItem(SettingsAutoEndCaptureId, "End recording after capturing max length", true, processor.m_auto_finish_record);
m_settings_menu.addSeparator();
m_settings_menu.addItem(SettingsSliderSnapId, "Sliders jump to position", true, processor.m_use_jumpsliders);
#ifdef JUCE_DEBUG
m_settings_menu.addItem(SettingsDumpPresetClipboardId, "Dump preset to clipboard", true, false);
#endif
m_settings_menu.addItem(SettingsShowTechInfoId, "Show technical info in waveform", true, processor.m_show_technical_info);
m_settings_menu.addSeparator();
m_settings_menu.addItem(SettingsAboutId, "About...", true, false);
auto options = PopupMenu::Options().withTargetComponent(&m_settings_button);
#if JUCE_IOS
options = options.withStandardItemHeight(34);
#endif
if (!JUCEApplicationBase::isStandaloneApp()) {
options = options.withParentComponent(this);
}
m_settings_menu.showMenuAsync(options,
ModalCallbackFunction::forComponent(handleSettingsMenuModalCallback, this));
}
void PaulstretchpluginAudioProcessorEditor::showAbout()
{
String fftlib;
#if PS_USE_VDSP_FFT
fftlib = "vDSP";
#elif PS_USE_PFFFT
fftlib = "pffft";
#else
fftlib = fftwf_version;
#endif
String juceversiontxt = String("JUCE ") + String(JUCE_MAJOR_VERSION) + "." + String(JUCE_MINOR_VERSION);
String title = String(JucePlugin_Name) + " " + String(JucePlugin_VersionString);
#ifdef JUCE_DEBUG
title += " (DEBUG)";
#endif
String vstInfo;
if (processor.wrapperType == AudioProcessor::wrapperType_VST ||
processor.wrapperType == AudioProcessor::wrapperType_VST3)
vstInfo = "VST Plug-In Technology by Steinberg.\n\n";
PluginHostType host;
auto * content = new Label();
String text = title + "\n\n" +
"Plugin/Application for extreme time stretching and other sound processing\nBuilt on " + String(__DATE__) + " " + String(__TIME__) + "\n"
"Copyright (C) 2006-2011 Nasca Octavian Paul, Tg. Mures, Romania\n"
"(C) 2017-2021 Xenakios\n"
"(C) 2022 Jesse Chappell\n\n"
+vstInfo;
if (fftlib.isNotEmpty())
text += String("Using ") + fftlib + String(" for FFT\n\n");
#if !JUCE_IOS
if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_AAX) {
text += juceversiontxt + String("\n\n");
}
else {
text += juceversiontxt + String(" used under the GPL license.\n\n");
}
#endif
text += String("GPL licensed source code at : https://github.com/essej/paulxstretch\n");
if (host.type != juce::PluginHostType::UnknownHost) {
text += String("Running in : ") + host.getHostDescription()+ String("\n");
}
content->setJustificationType(Justification::centred);
content->setText(text, dontSendNotification);
auto wrap = std::make_unique<Viewport>();
wrap->setViewedComponent(content, true); // takes ownership of content
//std::unique_ptr<SettingsComponent> contptr(content);
int defWidth = 450;
int defHeight = 350;
#if JUCE_IOS
defWidth = 320;
defHeight = 350;
#endif
content->setSize (defWidth, defHeight);
wrap->setSize(jmin(defWidth, getWidth() - 20), jmin(defHeight, getHeight() - 24));
auto bounds = getLocalArea(nullptr, m_settings_button.getScreenBounds());
auto & cb = CallOutBox::launchAsynchronously(std::move(wrap), bounds, this, false);
cb.setDismissalMouseClicksAreAlwaysConsumed(true);
}
void PaulstretchpluginAudioProcessorEditor::toggleFileBrowser()
{
#if JUCE_IOS
@ -1398,6 +1297,93 @@ void PaulstretchpluginAudioProcessorEditor::toggleFileBrowser()
#endif
}
void PaulstretchpluginAudioProcessorEditor::toggleOutputRecording()
{
if (processor.isRecordingToFile()) {
processor.stopRecordingToFile();
m_recordingButton->setToggleState(false, dontSendNotification);
//updateServerStatusLabel("Stopped Recording");
String filepath;
#if (JUCE_IOS || JUCE_ANDROID)
filepath = m_lastRecordedFile.getRelativePathFrom(File::getSpecialLocation (File::userDocumentsDirectory));
//showPopTip(TRANS("Finished recording to ") + filepath, 4000, mRecordingButton.get(), 130);
#else
filepath = m_lastRecordedFile.getRelativePathFrom(File::getSpecialLocation (File::userHomeDirectory));
#endif
m_recordingButton->setTooltip(TRANS("Last recorded file: ") + filepath);
//mFileRecordingLabel->setText("Total: " + SonoUtility::durationToString(processor.getElapsedRecordTime(), true), dontSendNotification);
m_fileRecordingLabel->setText("", dontSendNotification);
//Timer::callAfterDelay(200, []() {
// AccessibilityHandler::postAnnouncement(TRANS("Recording finished"), AccessibilityHandler::AnnouncementPriority::high);
//});
} else {
SafePointer<PaulstretchpluginAudioProcessorEditor> safeThis (this);
if (! RuntimePermissions::isGranted (RuntimePermissions::writeExternalStorage))
{
RuntimePermissions::request (RuntimePermissions::writeExternalStorage,
[safeThis] (bool granted) mutable
{
if (granted)
safeThis->toggleOutputRecording();
});
return;
}
// create new timestamped filename
String filename = "PaulXStretchSession" + String("_") + Time::getCurrentTime().formatted("%Y-%m-%d_%H.%M.%S");
filename = File::createLegalFileName(filename);
auto parentDir = File(processor.getDefaultRecordingDirectory());
parentDir.createDirectory();
File file (parentDir.getNonexistentChildFile (filename, ".flac"));
if (processor.startRecordingToFile(file)) {
//updateServerStatusLabel("Started recording...");
m_lastRecordedFile = file;
String filepath;
#if (JUCE_IOS || JUCE_ANDROID)
//showPopTip(TRANS("Started recording output"), 2000, mRecordingButton.get());
#else
//Timer::callAfterDelay(200, []() {
// AccessibilityHandler::postAnnouncement(TRANS("Started recording output"), AccessibilityHandler::AnnouncementPriority::high);
//});
#endif
#if (JUCE_IOS || JUCE_ANDROID)
filepath = m_lastRecordedFile.getRelativePathFrom(File::getSpecialLocation (File::userDocumentsDirectory));
#else
filepath = m_lastRecordedFile.getRelativePathFrom(File::getSpecialLocation (File::userHomeDirectory));
#endif
m_recordingButton->setTooltip(TRANS("Recording audio to: ") + filepath);
}
else {
// show error starting record
String lasterr = processor.getLastErrorMessage();
//showPopTip(lasterr, 0, mRecordingButton.get());
}
m_fileRecordingLabel->setText("", dontSendNotification);
m_recordingButton->setToggleState(true, dontSendNotification);
}
}
///============================
WaveformComponent::WaveformComponent(AudioFormatManager* afm, AudioThumbnail* thumb, StretchAudioSource* sas)
: m_sas(sas)
{

View File

@ -12,6 +12,8 @@
#include "envelope_component.h"
#include "CustomLookAndFeel.h"
class OptionsView;
class zoom_scrollbar : public Component
{
public:
@ -510,7 +512,8 @@ private:
};
class PaulstretchpluginAudioProcessorEditor : public AudioProcessorEditor,
public MultiTimer, public FileDragAndDropTarget, public DragAndDropContainer
public MultiTimer, public FileDragAndDropTarget, public DragAndDropContainer, public ComponentListener
{
public:
PaulstretchpluginAudioProcessorEditor (PaulstretchpluginAudioProcessor&);
@ -538,10 +541,12 @@ public:
bool keyPressed(const KeyPress& press) override;
void componentParentHierarchyChanged (Component& component) override;
WaveformComponent m_wavecomponent;
void showRenderDialog();
void executeModalMenuAction(int menuid, int actionid);
//SimpleFFTComponent m_sonogram;
String m_last_err;
@ -557,6 +562,8 @@ private:
void updateAllSliders();
void toggleOutputRecording();
CustomLookAndFeel m_lookandfeel;
PaulstretchpluginAudioProcessor& processor;
@ -581,8 +588,15 @@ private:
double m_lastspec_select_time = 0.0;
int m_lastspec_select_group = -1;
bool m_shortMode = false;
File m_lastRecordedFile;
std::unique_ptr<Label> m_fileRecordingLabel;
std::unique_ptr<DrawableButton> m_recordingButton;
bool settingsWasShownOnDown = false;
uint32 settingsClosedTimestamp = 0;
WeakReference<Component> settingsCalloutBox;
std::unique_ptr<OptionsView> m_optionsView;
void showSettingsMenu();
zoom_scrollbar m_zs;
RatioMixerEditor m_ratiomixeditor{ 8 };
@ -591,7 +605,7 @@ private:
MyTabComponent m_wavefilter_tab;
Component* m_wave_container=nullptr;
void showAudioSetup();
void showAbout();
void showSettings(bool flag);
void toggleFileBrowser();
std::vector<int> m_capturelens{ 2,5,10,30,60,120 };

View File

@ -294,6 +294,20 @@ m_bufferingthread("pspluginprebufferthread"), m_is_stand_alone_offline(is_stand_
startTimer(1, 40);
}
#if (JUCE_IOS)
m_defaultRecordDir = File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName();
#elif (JUCE_ANDROID)
auto parentDir = File::getSpecialLocation (File::userApplicationDataDirectory);
parentDir = parentDir.getChildFile("Recordings");
m_defaultRecordDir = parentDir.getFullPathName();
#else
auto parentDir = File::getSpecialLocation (File::userMusicDirectory);
parentDir = parentDir.getChildFile("PaulXStretch");
m_defaultRecordDir = parentDir.getFullPathName();
#endif
//m_defaultCaptureDir = parentDir.getChildFile("Captures").getFullPathName();
m_show_technical_info = m_propsfile->m_props_file->getBoolValue("showtechnicalinfo", false);
DBG("Constructed PS plugin");
@ -398,6 +412,9 @@ ValueTree PaulstretchpluginAudioProcessor::getStateTree(bool ignoreoptions, bool
storeToTreeProperties(paramtree, nullptr, "restoreplaystate", m_restore_playstate);
storeToTreeProperties(paramtree, nullptr, "autofinishrecord", m_auto_finish_record);
paramtree.setProperty("defRecordDir", m_defaultRecordDir, nullptr);
return paramtree;
}
@ -439,7 +456,11 @@ void PaulstretchpluginAudioProcessor::setStateFromTree(ValueTree tree)
}
getFromTreeProperties(tree, "waveviewrange", m_wave_view_range);
getFromTreeProperties(tree, getParameters());
#if !(JUCE_IOS || JUCE_ANDROID)
setDefaultRecordingDirectory(tree.getProperty("defRecordDir", m_defaultRecordDir));
#endif
}
int prebufamt = tree.getProperty("prebufamount", 2);
if (prebufamt == -1)
@ -676,23 +697,32 @@ void PaulstretchpluginAudioProcessor::saveCaptureBuffer()
int inchans = jmin(getMainBusNumInputChannels(), getIntParameter(cpi_num_inchans)->get());
if (inchans < 1)
return;
WavAudioFormat wavformat;
std::unique_ptr<AudioFormat> audioFormat;
String fextension;
int bitsPerSample = std::min(32, m_defaultRecordingBitsPerSample);
if (m_defaultRecordingFormat == FileFormatWAV) {
audioFormat = std::make_unique<WavAudioFormat>();
fextension = ".wav";
}
else {
audioFormat = std::make_unique<FlacAudioFormat>();
fextension = ".flac";
bitsPerSample = std::min(24, bitsPerSample);
}
String outfn;
String filename = String("pxs_") + Time::getCurrentTime().formatted("%Y-%m-%d_%H.%M.%S");
filename = File::createLegalFileName(filename);
if (m_capture_location.isEmpty()) {
File capdir;
#if JUCE_IOS
capdir = File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory);
outfn = capdir.getChildFile("Captures").getNonexistentChildFile(filename, ".wav").getFullPathName();
#else
capdir = m_propsfile->m_props_file->getFile().getParentDirectory();
outfn = capdir.getChildFile("Captures").getNonexistentChildFile(filename, ".wav").getFullPathName();
#endif
File capdir(m_defaultRecordDir);
outfn = capdir.getChildFile("Captures").getNonexistentChildFile(filename, fextension).getFullPathName();
}
else {
outfn = File(m_capture_location).getNonexistentChildFile(filename, ".wav").getFullPathName();
outfn = File(m_capture_location).getNonexistentChildFile(filename, fextension).getFullPathName();
}
File outfile(outfn);
outfile.create();
@ -700,8 +730,8 @@ void PaulstretchpluginAudioProcessor::saveCaptureBuffer()
{
m_capture_save_state = 1;
auto outstream = outfile.createOutputStream();
auto writer = unique_from_raw(wavformat.createWriterFor(outstream.get(), getSampleRateChecked(),
inchans, 32, {}, 0));
auto writer = unique_from_raw(audioFormat->createWriterFor(outstream.get(), getSampleRateChecked(),
inchans, bitsPerSample, {}, 0));
if (writer != nullptr)
{
outstream.release(); // the writer takes ownership
@ -1274,6 +1304,19 @@ void PaulstretchpluginAudioProcessor::processBlock (AudioSampleBuffer& buffer, M
ed->m_sonogram.addAudioBlock(buffer);
}
*/
// output to file writer if necessary
if (m_writingPossible.load()) {
const ScopedTryLock sl (m_writerLock);
if (sl.isLocked())
{
if (m_activeMixWriter.load() != nullptr) {
m_activeMixWriter.load()->write (buffer.getArrayOfReadPointers(), buffer.getNumSamples());
}
m_elapsedRecordSamples += buffer.getNumSamples();
}
}
}
//==============================================================================
@ -1306,7 +1349,7 @@ void PaulstretchpluginAudioProcessor::setDirty()
toggleBool(getBoolParameter(cpi_markdirty));
}
void PaulstretchpluginAudioProcessor::setRecordingEnabled(bool b)
void PaulstretchpluginAudioProcessor::setInputRecordingEnabled(bool b)
{
ScopedLock locker(m_cs);
int lenbufframes = getSampleRateChecked()*m_max_reclen;
@ -1334,7 +1377,7 @@ void PaulstretchpluginAudioProcessor::setRecordingEnabled(bool b)
}
}
double PaulstretchpluginAudioProcessor::getRecordingPositionPercent()
double PaulstretchpluginAudioProcessor::getInputRecordingPositionPercent()
{
if (m_is_recording_pending==false)
return 0.0;
@ -1411,13 +1454,13 @@ void PaulstretchpluginAudioProcessor::timerCallback(int id)
if (capture == true && m_is_recording_pending == false && !m_is_recording_finished)
{
DBG("start recording");
setRecordingEnabled(true);
setInputRecordingEnabled(true);
return;
}
if (capture == false && m_is_recording_pending == true && !m_is_recording_finished)
{
DBG("stop recording");
setRecordingEnabled(false);
setInputRecordingEnabled(false);
return;
}
@ -1517,6 +1560,145 @@ void PaulstretchpluginAudioProcessor::finishRecording(int lenrecording, bool nos
}
}
bool PaulstretchpluginAudioProcessor::startRecordingToFile(File & file, RecordFileFormat fileformat)
{
if (!m_recordingThread) {
m_recordingThread = std::make_unique<TimeSliceThread>("Recording Thread");
m_recordingThread->startThread();
}
stopRecordingToFile();
bool ret = false;
// Now create a WAV writer object that writes to our output stream...
//WavAudioFormat audioFormat;
std::unique_ptr<AudioFormat> audioFormat;
std::unique_ptr<AudioFormat> wavAudioFormat;
int qualindex = 0;
int bitsPerSample = std::min(32, m_defaultRecordingBitsPerSample);
if (getSampleRate() <= 0)
{
return false;
}
File usefile = file;
if (fileformat == FileFormatDefault) {
fileformat = m_defaultRecordingFormat;
}
m_totalRecordingChannels = getMainBusNumOutputChannels();
if (m_totalRecordingChannels == 0) {
m_totalRecordingChannels = 2;
}
if (fileformat == FileFormatFLAC && m_totalRecordingChannels > 8) {
// flac doesn't support > 8 channels
fileformat = FileFormatWAV;
}
if (fileformat == FileFormatFLAC || (fileformat == FileFormatAuto && file.getFileExtension().toLowerCase() == ".flac")) {
audioFormat = std::make_unique<FlacAudioFormat>();
bitsPerSample = std::min(24, bitsPerSample);
usefile = file.withFileExtension(".flac");
}
else if (fileformat == FileFormatWAV || (fileformat == FileFormatAuto && file.getFileExtension().toLowerCase() == ".wav")) {
audioFormat = std::make_unique<WavAudioFormat>();
usefile = file.withFileExtension(".wav");
}
else if (fileformat == FileFormatOGG || (fileformat == FileFormatAuto && file.getFileExtension().toLowerCase() == ".ogg")) {
audioFormat = std::make_unique<OggVorbisAudioFormat>();
qualindex = 8; // 256k
usefile = file.withFileExtension(".ogg");
}
else {
m_lastError = TRANS("Could not find format for filename");
DBG(m_lastError);
return false;
}
bool userwriting = false;
// Create an OutputStream to write to our destination file...
usefile.deleteFile();
if (auto fileStream = std::unique_ptr<FileOutputStream> (usefile.createOutputStream()))
{
if (auto writer = audioFormat->createWriterFor (fileStream.get(), getSampleRate(), m_totalRecordingChannels, bitsPerSample, {}, qualindex))
{
fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
// Now we'll create one of these helper objects which will act as a FIFO buffer, and will
// write the data to disk on our background thread.
m_threadedMixWriter.reset (new AudioFormatWriter::ThreadedWriter (writer, *m_recordingThread, 65536));
DBG("Started recording only mix file " << usefile.getFullPathName());
file = usefile;
ret = true;
} else {
m_lastError.clear();
m_lastError << TRANS("Error creating writer for ") << usefile.getFullPathName();
DBG(m_lastError);
}
} else {
m_lastError.clear();
m_lastError << TRANS("Error creating output file: ") << usefile.getFullPathName();
DBG(m_lastError);
}
if (ret) {
// And now, swap over our active writer pointers so that the audio callback will start using it..
const ScopedLock sl (m_writerLock);
m_elapsedRecordSamples = 0;
m_activeMixWriter = m_threadedMixWriter.get();
m_writingPossible.store(m_activeMixWriter);
//DBG("Started recording file " << usefile.getFullPathName());
}
return ret;
}
bool PaulstretchpluginAudioProcessor::stopRecordingToFile()
{
// First, clear this pointer to stop the audio callback from using our writer object..
{
const ScopedLock sl (m_writerLock);
m_activeMixWriter = nullptr;
m_writingPossible.store(false);
}
bool didit = false;
if (m_threadedMixWriter) {
// Now we can delete the writer object. It's done in this order because the deletion could
// take a little time while remaining data gets flushed to disk, and we can't be blocking
// the audio callback while this happens.
m_threadedMixWriter.reset();
DBG("Stopped recording mix file");
didit = true;
}
return didit;
}
bool PaulstretchpluginAudioProcessor::isRecordingToFile()
{
return (m_activeMixWriter.load() != nullptr);
}
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new PaulstretchpluginAudioProcessor();

View File

@ -193,9 +193,33 @@ public:
void setDirty();
void setRecordingEnabled(bool b);
bool isRecordingEnabled() { return m_is_recording_pending; }
double getRecordingPositionPercent();
void setInputRecordingEnabled(bool b);
bool isInputRecordingEnabled() { return m_is_recording_pending; }
double getInputRecordingPositionPercent();
enum RecordFileFormat {
FileFormatDefault = 0,
FileFormatAuto,
FileFormatFLAC,
FileFormatWAV,
FileFormatOGG
};
bool startRecordingToFile(File & file, RecordFileFormat fileformat=FileFormatDefault);
bool stopRecordingToFile();
bool isRecordingToFile();
double getElapsedRecordTime() const { return m_elapsedRecordSamples / getSampleRate(); }
String getLastErrorMessage() const { return m_lastError; }
void setDefaultRecordingDirectory(String recdir) {
m_defaultRecordDir = recdir;
}
String getDefaultRecordingDirectory() const { return m_defaultRecordDir; }
RecordFileFormat getDefaultRecordingFormat() const { return m_defaultRecordingFormat; }
void setDefaultRecordingFormat(RecordFileFormat fmt) { m_defaultRecordingFormat = fmt; }
int getDefaultRecordingBitsPerSample() const { return m_defaultRecordingBitsPerSample; }
void setDefaultRecordingBitsPerSample(int fmt) { m_defaultRecordingBitsPerSample = fmt; }
String setAudioFile(const URL& url);
URL getAudioFile() { return m_current_file; }
Range<double> getTimeSelection();
@ -316,6 +340,23 @@ private:
int m_midinote_to_use = -1;
ADSR m_adsr;
bool m_is_stand_alone_offline = false;
// recording stuff
RecordFileFormat m_defaultRecordingFormat = FileFormatFLAC;
int m_defaultRecordingBitsPerSample = 24;
String m_defaultRecordDir;
String m_defaultCaptureDir;
String m_lastError;
std::atomic<bool> m_writingPossible = { false };
int m_totalRecordingChannels = 2;
int64 m_elapsedRecordSamples = 0;
CriticalSection m_writerLock;
std::unique_ptr<TimeSliceThread> m_recordingThread;
std::unique_ptr<AudioFormatWriter::ThreadedWriter> m_threadedMixWriter;
std::atomic<AudioFormatWriter::ThreadedWriter*> m_activeMixWriter { nullptr };
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PaulstretchpluginAudioProcessor)
};

245
Source/SonoChoiceButton.cpp Normal file
View File

@ -0,0 +1,245 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#include "SonoChoiceButton.h"
SonoChoiceButton::SonoChoiceButton()
{
textLabel = std::make_unique<Label>();
addAndMakeVisible(textLabel.get());
textLabel->setJustificationType(Justification::centredLeft);
textLabel->setAccessible(false);
selIndex = 0;
addListener(this);
}
SonoChoiceButton::~SonoChoiceButton()
{
}
void SonoChoiceButton::clearItems()
{
items.clear();
idList.clear();
textLabel->setText("", dontSendNotification);
}
void SonoChoiceButton::addItem(const String & name, int ident, bool separator, bool disabled)
{
items.add(GenericItemChooserItem(name, Image(), nullptr, separator, disabled));
idList.add(ident);
}
void SonoChoiceButton::addItem(const String & name, int ident, const Image & newItemImage, bool separator, bool disabled)
{
items.add(GenericItemChooserItem(name, newItemImage, nullptr, separator, disabled));
idList.add(ident);
}
void SonoChoiceButton::setSelectedItemIndex(int index, NotificationType notification)
{
selIndex = index;
if (selIndex < items.size()) {
textLabel->setText(items[selIndex].name, dontSendNotification);
}
if (notification != dontSendNotification) {
int ident = index < idList.size() ? idList[index] : 0;
listeners.call (&SonoChoiceButton::Listener::choiceButtonSelected, this, index, ident);
}
repaint();
}
void SonoChoiceButton::setSelectedId(int ident, NotificationType notification)
{
for (int i=0; i < idList.size(); ++i) {
if (idList[i] == ident) {
setSelectedItemIndex(i, notification);
break;
}
}
}
String SonoChoiceButton::getItemText(int index) const
{
if (selIndex < items.size()) {
return items[selIndex].name;
}
return "";
}
String SonoChoiceButton::getCurrentText() const
{
return textLabel->getText();
}
void SonoChoiceButton::genericItemChooserSelected(GenericItemChooser *comp, int index)
{
setSelectedItemIndex(index);
int ident = index < idList.size() ? idList[index] : 0;
listeners.call (&SonoChoiceButton::Listener::choiceButtonSelected, this, index, ident);
if (CallOutBox* const dw = comp->findParentComponentOfClass<CallOutBox>()) {
dw->dismiss();
}
setWantsKeyboardFocus(true);
Timer::callAfterDelay(200, [this](){
if (isShowing()) grabKeyboardFocus();
});
}
void SonoChoiceButton::resized()
{
SonoTextButton::resized();
int xoff = 0;
bool validImage = false;
if (selIndex < items.size()) {
if (items[selIndex].image.isValid()) {
float imagesize = getHeight() - 8;
xoff = imagesize;
validImage = true;
}
}
if (showArrow && getWidth()-(20 + xoff + 4) > 40) {
textLabel->setBounds(4 + xoff, 2, getWidth() - 22, getHeight()-4 - xoff);
}
else if (!showArrow && getWidth()-(xoff + 4) > 40) {
textLabel->setBounds(4 + xoff, 2, getWidth() - 8 - xoff, getHeight()-4 - xoff);
}
else {
textLabel->setSize(0, 0);
}
}
void SonoChoiceButton::paint(Graphics & g)
{
int width = getWidth();
int height = getHeight();
SonoTextButton::paint(g);
if (showArrow) {
Rectangle<int> arrowZone (width - 20, 0, 16, height);
Path path;
path.startNewSubPath (arrowZone.getX() + 3.0f, arrowZone.getCentreY() - 2.0f);
path.lineTo (static_cast<float> (arrowZone.getCentreX()), arrowZone.getCentreY() + 3.0f);
path.lineTo (arrowZone.getRight() - 3.0f, arrowZone.getCentreY() - 2.0f);
g.setColour (findColour (ComboBox::arrowColourId).withAlpha ((isEnabled() ? 0.9f : 0.2f)));
g.strokePath (path, PathStrokeType (2.0f));
}
if (selIndex < items.size()) {
if (items[selIndex].image.isValid()) {
float imagesize = height - 8;
//g.drawImage(items[selIndex].image, Rectangle<float>(2, 4, imagesize, imagesize));
g.drawImageWithin(items[selIndex].image, 2, 4, imagesize, imagesize, RectanglePlacement(RectanglePlacement::centred|RectanglePlacement::onlyReduceInSize));
}
}
}
void SonoChoiceButton::buttonClicked (Button* buttonThatWasClicked)
{
if (items.isEmpty()) {
listeners.call (&SonoChoiceButton::Listener::choiceButtonEmptyClick, this);
}
else {
showPopup();
}
}
void SonoChoiceButton::showPopup()
{
auto chooser = std::make_unique<GenericItemChooser>(items);
chooser->setRowHeight(std::min(getHeight(), 40));
chooser->addListener(this);
chooser->setCurrentRow(selIndex);
//DocumentWindow* const dw = this->findParentComponentOfClass<DocumentWindow>();
Component* dw = this->findParentComponentOfClass<DocumentWindow>();
if (!dw) {
dw = this->findParentComponentOfClass<AudioProcessorEditor>();
}
if (!dw) {
dw = this->findParentComponentOfClass<Component>();
}
chooser->setSize(jmin(chooser->getWidth(), dw->getWidth() - 16), jmin(chooser->getHeight(), dw->getHeight() - 20));
Rectangle<int> bounds = dw->getLocalArea(nullptr, getScreenBounds());
CallOutBox & box = CallOutBox::launchAsynchronously (std::move(chooser), bounds , dw);
box.setDismissalMouseClicksAreAlwaysConsumed(true);
//box.setArrowSize(0);
box.grabKeyboardFocus();
activeCalloutBox = &box;
}
bool SonoChoiceButton::isPopupActive() const
{
return activeCalloutBox.get() != nullptr;
}
//==============================================================================
class SonoChoiceButtonAccessibilityHandler : public AccessibilityHandler
{
public:
explicit SonoChoiceButtonAccessibilityHandler (SonoChoiceButton& comboBoxToWrap)
: AccessibilityHandler (comboBoxToWrap,
AccessibilityRole::comboBox,
getAccessibilityActions (comboBoxToWrap)),
comboBox (comboBoxToWrap)
{
}
AccessibleState getCurrentState() const override
{
auto state = AccessibilityHandler::getCurrentState().withExpandable();
return comboBox.isPopupActive() ? state.withExpanded() : state.withCollapsed();
}
String getTitle() const override { return comboBox.getTitle() + ", " + comboBox.getCurrentText(); }
private:
static AccessibilityActions getAccessibilityActions (SonoChoiceButton& comboBox)
{
return AccessibilityActions().addAction (AccessibilityActionType::press, [&comboBox] { comboBox.showPopup(); })
.addAction (AccessibilityActionType::showMenu, [&comboBox] { comboBox.showPopup(); });
}
SonoChoiceButton& comboBox;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SonoChoiceButtonAccessibilityHandler)
};
std::unique_ptr<AccessibilityHandler> SonoChoiceButton::createAccessibilityHandler()
{
return std::make_unique<SonoChoiceButtonAccessibilityHandler> (*this);
}

90
Source/SonoChoiceButton.h Normal file
View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#pragma once
#include "JuceHeader.h"
#include "SonoTextButton.h"
#include "GenericItemChooser.h"
#include "CustomLookAndFeel.h"
class SonoChoiceLookAndFeel : public CustomLookAndFeel
{
public:
int getCallOutBoxBorderSize (const CallOutBox&) override {
return 40;
}
};
class SonoChoiceButton : public SonoTextButton, public GenericItemChooser::Listener, public Button::Listener
{
public:
SonoChoiceButton();
virtual ~SonoChoiceButton();
class Listener {
public:
virtual ~Listener() {}
virtual void choiceButtonEmptyClick(SonoChoiceButton *comp) {}
virtual void choiceButtonSelected(SonoChoiceButton *comp, int index, int ident) {}
};
void paint(Graphics & g) override;
void resized() override;
void genericItemChooserSelected(GenericItemChooser *comp, int index) override;
void clearItems();
void addItem(const String & name, int ident, bool separator=false, bool disabled=false);
void addItem(const String & name, int ident, const Image & newItemImage, bool separator=false, bool disabled=false);
//void setItems(const StringArray & items);
//const StringArray & getItems() const { return items; }
void setSelectedItemIndex(int index, NotificationType notification = dontSendNotification);
int getSelectedItemIndex() const { return selIndex; }
void setSelectedId(int ident, NotificationType notification = dontSendNotification);
void setShowArrow(bool flag) { showArrow = flag; }
bool getShowArrow() const { return showArrow; }
String getItemText(int index) const;
String getCurrentText() const;
void buttonClicked (Button* buttonThatWasClicked) override;
void addChoiceListener(Listener * listener) { listeners.add(listener); }
void removeChoiceListener(Listener * listener) { listeners.remove(listener); }
void showPopup();
bool isPopupActive() const;
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
protected:
ListenerList<Listener> listeners;
std::unique_ptr<Label> textLabel;
Array<GenericItemChooserItem> items;
Array<int> idList;
int selIndex;
bool showArrow = true;
WeakReference<Component> activeCalloutBox;
SonoChoiceLookAndFeel lnf;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SonoChoiceButton)
};

409
Source/SonoTextButton.cpp Normal file
View File

@ -0,0 +1,409 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#include "SonoTextButton.h"
#include "CustomLookAndFeel.h"
#include <math.h>
SonoTextButton::SonoTextButton(const String & name)
: TextButton(name), _buttonStyle(SonoButtonStyleNormal)
{
}
SonoTextButton::~SonoTextButton()
{
}
void SonoTextButton::setButtonStyle(SonoButtonStyle style)
{
_buttonStyle = style;
}
void SonoTextButton::drawButtonBackground(Graphics& g, bool isMouseOverButton, bool isButtonDown)
{
Colour bgcolor = findColour (getToggleState() ? buttonOnColourId : buttonColourId);
Colour bordcolor = isColourSpecified(outlineColourId) ? findColour (outlineColourId) : Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5);
if (isButtonDown) {
bgcolor = bgcolor.withMultipliedBrightness(1.8);
} else if (isMouseOverButton) {
bgcolor = bgcolor.withMultipliedBrightness(1.3);
}
g.setColour(bgcolor);
g.fillPath(fillPath);
g.setColour(bordcolor);
g.strokePath(borderPath, PathStrokeType(1.0));
}
void SonoTextButton::paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown)
{
LookAndFeel& lf = getLookAndFeel();
drawButtonBackground(g, isMouseOverButton, isButtonDown);
if (auto * tetlf = dynamic_cast<CustomLookAndFeel*>(&lf)) {
Justification just = textJustification;
switch (_buttonStyle) {
case SonoButtonStyleLowerLeftCorner: just = Justification::centredLeft; break;
case SonoButtonStyleLowerRightCorner: just = Justification::centredRight; break;
default: break;
}
tetlf->drawButtonTextWithAlignment (g, *this, isMouseOverButton, isButtonDown, just);
}
else {
lf.drawButtonText (g, *this, isMouseOverButton, isButtonDown);
}
}
bool SonoTextButton::hitTest (int x, int y)
{
return fillPath.contains(x, y);
}
void SonoTextButton::resized()
{
TextButton::resized();
setupPath();
}
void SonoTextButton::setupPath() {
float borderOffset = 0.5; // _borderWidth*1.5f;
float width = getWidth();
float height = getHeight();
fillPath.clear();
borderPath.clear();
if (_buttonStyle == SonoButtonStyleTop) {
fillPath.startNewSubPath(borderOffset, borderOffset);
fillPath.quadraticTo(width/2.0, height-borderOffset, width - borderOffset, borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
borderPath.startNewSubPath(borderOffset, borderOffset);
borderPath.quadraticTo(width/2.0, height-borderOffset, width - borderOffset, borderOffset);
//[fillPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(width - borderOffset, borderOffset) controlPoint:CGPointMake(width/2.0, height-borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(width - borderOffset, borderOffset) controlPoint:CGPointMake(width/2.0, height-borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleBottom) {
// H /2 + W^2 / 8H
float maxdim = jmax(width, height);
float radius = circleRadius > 0.0f ? circleRadius : height * 0.5 + (width*width)/(8.0f * height);
float startAng = atan2(height - radius, maxdim/2 - width);
float endAng = atan2(height - radius, width - maxdim/2);
startAng += M_PI_2;
endAng += M_PI_2;
DBG("Start ang " << radiansToDegrees(startAng) << " end " << radiansToDegrees(endAng) << " h: " << height << " rad: " << radius);
fillPath.startNewSubPath(borderOffset, height - borderOffset);
fillPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
fillPath.lineTo(borderOffset, height - borderOffset);
borderPath.startNewSubPath(borderOffset, height - borderOffset);
borderPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
borderPath.lineTo(borderOffset, height - borderOffset);
//[fillPath moveToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[fillPath addArcWithCenter:CGPointMake(maxdim/2, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, height -borderOffset)];
//[borderPath addArcWithCenter:CGPointMake(maxdim/2, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[borderPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
}
else if (_buttonStyle == SonoButtonStyleLeft) {
fillPath.startNewSubPath(borderOffset, borderOffset);
fillPath.quadraticTo(width - borderOffset, borderOffset, width - borderOffset, height/2);
fillPath.quadraticTo(width - borderOffset, height-borderOffset, borderOffset, height-borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
borderPath.startNewSubPath(borderOffset, borderOffset);
borderPath.quadraticTo(width - borderOffset, borderOffset, width - borderOffset, height/2);
borderPath.quadraticTo(width - borderOffset, height-borderOffset, borderOffset, height-borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(width - borderOffset, height/2) controlPoint:CGPointMake(width - borderOffset , borderOffset) ];
//[fillPath addQuadCurveToPoint:CGPointMake(borderOffset, height-borderOffset) controlPoint:CGPointMake(width - borderOffset , height-borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(width - borderOffset, height/2) controlPoint:CGPointMake(width - borderOffset , borderOffset) ];
//[borderPath addQuadCurveToPoint:CGPointMake(borderOffset, height-borderOffset) controlPoint:CGPointMake(width - borderOffset , height-borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleRight) {
fillPath.startNewSubPath(width - borderOffset, borderOffset);
fillPath.quadraticTo(borderOffset, borderOffset, borderOffset, height/2);
fillPath.quadraticTo(borderOffset, height-borderOffset, width - borderOffset, height-borderOffset);
fillPath.lineTo(width - borderOffset, borderOffset);
borderPath.startNewSubPath(width - borderOffset, borderOffset);
borderPath.quadraticTo(borderOffset, borderOffset, borderOffset, height/2);
borderPath.quadraticTo(borderOffset, height-borderOffset, width - borderOffset, height-borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(width - borderOffset, borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(borderOffset, height/2) controlPoint:CGPointMake(borderOffset, borderOffset) ];
//[fillPath addQuadCurveToPoint:CGPointMake(width - borderOffset, height - borderOffset) controlPoint:CGPointMake(borderOffset, height - borderOffset)];
//[fillPath addLineToPoint:CGPointMake(width - borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(width -borderOffset, borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(borderOffset, height/2) controlPoint:CGPointMake(borderOffset, borderOffset) ];
//[borderPath addQuadCurveToPoint:CGPointMake(width - borderOffset, height - borderOffset) controlPoint:CGPointMake(borderOffset, height - borderOffset)];
}
else if (_buttonStyle == SonoButtonStyleUpperLeftCorner) {
fillPath.startNewSubPath(borderOffset, borderOffset);
fillPath.lineTo(width - borderOffset, borderOffset);
fillPath.quadraticTo(width - borderOffset, height - borderOffset, borderOffset, height - borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
borderPath.startNewSubPath(width - borderOffset, borderOffset);
borderPath.quadraticTo(width - borderOffset, height - borderOffset, borderOffset, height - borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addLineToPoint:CGPointMake(width-borderOffset, borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(borderOffset, height-borderOffset) controlPoint:CGPointMake(width-borderOffset, height-borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(width-borderOffset, borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(borderOffset, height-borderOffset) controlPoint:CGPointMake(width-borderOffset, height-borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleUpperRightCorner) {
fillPath.startNewSubPath(width - borderOffset, borderOffset);
fillPath.lineTo(width - borderOffset, height - borderOffset);
fillPath.quadraticTo(borderOffset, height - borderOffset, borderOffset, borderOffset);
fillPath.lineTo(width - borderOffset, borderOffset);
borderPath.startNewSubPath(width - borderOffset, height - borderOffset);
borderPath.quadraticTo(borderOffset, height - borderOffset, borderOffset, borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(width - borderOffset, borderOffset)];
//[fillPath addLineToPoint:CGPointMake(width - borderOffset, height - borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(borderOffset, borderOffset) controlPoint:CGPointMake(borderOffset, height-borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(width - borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(width - borderOffset, height - borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(borderOffset, borderOffset) controlPoint:CGPointMake(borderOffset, height-borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleLowerLeftCorner) {
fillPath.startNewSubPath(borderOffset, height - borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
fillPath.quadraticTo(width-borderOffset, borderOffset, width-borderOffset, height-borderOffset);
fillPath.lineTo(borderOffset, height-borderOffset);
borderPath.startNewSubPath(borderOffset, borderOffset);
borderPath.quadraticTo(width-borderOffset, borderOffset, width-borderOffset, height-borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, height-borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(width-borderOffset, height-borderOffset) controlPoint:CGPointMake(width-borderOffset, borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height-borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(width-borderOffset, height-borderOffset) controlPoint:CGPointMake(width-borderOffset, borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleLowerRightCorner) {
fillPath.startNewSubPath(width-borderOffset, height - borderOffset);
fillPath.lineTo(borderOffset, height-borderOffset);
fillPath.quadraticTo(borderOffset, borderOffset, width-borderOffset, borderOffset);
fillPath.lineTo(width - borderOffset, height-borderOffset);
borderPath.startNewSubPath(borderOffset, height - borderOffset);
borderPath.quadraticTo(borderOffset, borderOffset, width-borderOffset, borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(width-borderOffset, height-borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height-borderOffset)];
//[fillPath addQuadCurveToPoint:CGPointMake(width-borderOffset, borderOffset) controlPoint:CGPointMake(borderOffset, borderOffset) ];
//[fillPath addLineToPoint:CGPointMake(width-borderOffset, height-borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, height-borderOffset)];
//[borderPath addQuadCurveToPoint:CGPointMake(width-borderOffset, borderOffset) controlPoint:CGPointMake(borderOffset, borderOffset) ];
}
else if (_buttonStyle == SonoButtonStyleLowerRightCornerRound) {
float maxdim = jmax(width*2, height);
// H /2 + W^2 / 8H (but double the width here)
float radius = circleRadius > 0.0f ? circleRadius : height * 0.5 + (width*width*4.0)/(8.0f * height);
float startAng = atan2(height - radius, maxdim/2 - width*2);
float endAng = atan2(height - radius, width - maxdim/2) ;
startAng += M_PI_2;
endAng += M_PI_2;
fillPath.startNewSubPath(borderOffset, height - borderOffset);
fillPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
fillPath.lineTo(width - borderOffset, height - borderOffset);
fillPath.lineTo(borderOffset, height - borderOffset);
borderPath.startNewSubPath(borderOffset, height - borderOffset);
borderPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
borderPath.lineTo(width - borderOffset, height - borderOffset);
borderPath.lineTo(borderOffset, height - borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[fillPath addArcWithCenter:CGPointMake(maxdim/2, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[fillPath addLineToPoint:CGPointMake(width - borderOffset, height - borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, height -borderOffset)];
//[borderPath addArcWithCenter:CGPointMake(maxdim/2, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[borderPath addLineToPoint:CGPointMake(width - borderOffset, height - borderOffset)];
//[borderPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
}
else if (_buttonStyle == SonoButtonStyleLowerLeftCornerRound) {
float maxdim = jmax(width*2, height);
// H /2 + W^2 / 8H (but double the width here)
float radius = circleRadius > 0.0f ? circleRadius : height * 0.5 + (width*width*4.0)/(8.0f * height);
float startAng = atan2(height - radius, width - maxdim/2) ;
float endAng = atan2(height - radius, width*2 - maxdim/2);
startAng += M_PI_2;
endAng += M_PI_2;
fillPath.startNewSubPath(borderOffset, height - borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
fillPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
fillPath.lineTo(borderOffset, height - borderOffset);
borderPath.startNewSubPath(borderOffset, height - borderOffset);
borderPath.lineTo(borderOffset, borderOffset);
borderPath.addCentredArc(maxdim/2, radius, radius, radius, 0.0f, startAng, endAng);
borderPath.lineTo(borderOffset, height - borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addArcWithCenter:CGPointMake(maxdim/2 - width, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, height -borderOffset)];
//[borderPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath addArcWithCenter:CGPointMake(maxdim/2 - width, radius) radius:radius startAngle:startAng endAngle:endAng clockwise:YES];
//[borderPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
}
else if (_buttonStyle == SonoButtonStyleUpperRightCornerRound) {
float maxdim = jmax(width*2, height);
// H /2 + W^2 / 8H (but double the width here)
float radius = circleRadius > 0.0f ? circleRadius : height * 0.5 + (width*width*4.0)/(8.0f * height);
float startAng = atan2(radius - height, maxdim/2 - width*2);
float endAng = atan2(radius - height , width - maxdim/2) ;
startAng += M_PI_2;
endAng += M_PI_2;
DBG("URC Start ang " << radiansToDegrees(startAng) << " end " << radiansToDegrees(endAng) << " h: " << height << " rad: " << radius);
fillPath.startNewSubPath(borderOffset, borderOffset);
fillPath.addCentredArc(maxdim/2, height-radius, radius, radius, 0.0f, startAng, endAng);
fillPath.lineTo(width - borderOffset, borderOffset);
fillPath.lineTo(borderOffset, borderOffset);
borderPath.startNewSubPath(borderOffset, borderOffset);
borderPath.addCentredArc(maxdim/2, height-radius, radius, radius, 0.0f, startAng, endAng);
borderPath.startNewSubPath(width - borderOffset, borderOffset);
borderPath.lineTo(borderOffset, borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addArcWithCenter:CGPointMake(maxdim/2, height -radius) radius:radius startAngle:startAng endAngle:endAng clockwise:NO];
//[fillPath addLineToPoint:CGPointMake(width - borderOffset, borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
//[borderPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[borderPath addArcWithCenter:CGPointMake(maxdim/2, height -radius) radius:radius startAngle:startAng endAngle:endAng clockwise:NO];
//[borderPath addLineToPoint:CGPointMake(width - borderOffset, borderOffset)];
//[borderPath moveToPoint:CGPointMake(width - borderOffset, borderOffset)];
//[borderPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
}
else if (_buttonStyle == SonoButtonStyleUpperLeftCornerRound) {
float maxdim = jmax(width*2, height);
// H /2 + W^2 / 8H (but double the width here)
float radius = circleRadius > 0.0f ? circleRadius : height * 0.5 + (width*width*4.0)/(8.0f * height);
float startAng = atan2(radius - height, width - maxdim/2) ;
float endAng = atan2(radius-height, width*2 - maxdim/2);
startAng += M_PI_2;
endAng += M_PI_2;
DBG("URC Start ang " << radiansToDegrees(startAng) << " end " << radiansToDegrees(endAng) << " h: " << height << " rad: " << radius);
fillPath.startNewSubPath(borderOffset, borderOffset);
fillPath.lineTo(borderOffset, height - borderOffset);
fillPath.addCentredArc(maxdim/2 - width, height-radius, radius, radius, 0.0f, startAng, endAng);
fillPath.lineTo(borderOffset, borderOffset);
borderPath.startNewSubPath(borderOffset, height - borderOffset);
borderPath.lineTo(borderOffset, height - borderOffset);
borderPath.addCentredArc(maxdim/2 - width, height-radius, radius, radius, 0.0f, startAng, endAng);
borderPath.lineTo(borderOffset, borderOffset);
//fillPath = [UIBezierPath bezierPath];
//[fillPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
//[fillPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
////[fillPath moveToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[fillPath addArcWithCenter:CGPointMake(maxdim/2 - width, height -radius) radius:radius startAngle:startAng endAngle:endAng clockwise:NO];
//[fillPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
//borderPath = [UIBezierPath bezierPath];
////[borderPath moveToPoint:CGPointMake(borderOffset, borderOffset)];
////[borderPath addLineToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[borderPath moveToPoint:CGPointMake(borderOffset, height - borderOffset)];
//[borderPath addArcWithCenter:CGPointMake(maxdim/2 - width, height -radius) radius:radius startAngle:startAng endAngle:endAng clockwise:NO];
//[borderPath addLineToPoint:CGPointMake(borderOffset, borderOffset)];
}
else
{
auto bounds = getLocalBounds().toFloat().reduced(borderOffset);
fillPath.addRoundedRectangle(bounds, cornerRadius);
borderPath.addRoundedRectangle(bounds, cornerRadius);
}
}

74
Source/SonoTextButton.h Normal file
View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception
// Copyright (C) 2020 Jesse Chappell
#pragma once
#include "JuceHeader.h"
enum SonoButtonStyle {
SonoButtonStyleNormal=0,
SonoButtonStyleUpperLeftCorner,
SonoButtonStyleUpperRightCorner,
SonoButtonStyleLowerLeftCorner,
SonoButtonStyleLowerRightCorner,
SonoButtonStyleUpperLeftCornerRound,
SonoButtonStyleUpperRightCornerRound,
SonoButtonStyleLowerLeftCornerRound,
SonoButtonStyleLowerRightCornerRound,
SonoButtonStyleLeft,
SonoButtonStyleRight,
SonoButtonStyleTop,
SonoButtonStyleBottom
};
class SonoTextButton : public TextButton
{
public:
SonoTextButton(const String & name="");
virtual ~SonoTextButton();
enum SonoColourIds
{
outlineColourId = 0x1008015,
};
void paintButton (Graphics&, bool isMouseOverButton, bool isButtonDown) override;
void resized() override;
void setCornerRadius(float rad) { cornerRadius = rad; }
float getCornerRadius() const { return cornerRadius; }
void setButtonStyle(SonoButtonStyle style);
SonoButtonStyle getButtonStyle() const { return _buttonStyle; }
void setCircleRadius(float rad) { circleRadius = rad; }
float getCircleRadius() const { return circleRadius; }
float getTextHeightRatio() const { return textHeightRatio; }
void setTextHeightRatio(float ratio) { textHeightRatio = ratio; }
void setTextJustification(Justification just) { textJustification = just; }
Justification getTextJustification() const { return textJustification; }
protected:
bool hitTest (int x, int y) override;
void setupPath();
void drawButtonBackground(Graphics& g, bool isMouseOverButton, bool isButtonDown);
SonoButtonStyle _buttonStyle;
float cornerRadius = 6.0f;
Path borderPath;
Path fillPath;
float circleRadius = 0.0f;
float textHeightRatio = 0.8f;
Justification textJustification = Justification::centred;
};

132
images/record_input.svg Normal file
View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="26"
height="26"
id="svg1437"
sodipodi:docname="record_input.svg"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
inkscape:export-filename="/Users/jesse/src/sonoaudio/tonalenergy/images/circleslash_icon.png"
inkscape:export-xdpi="102.4"
inkscape:export-ydpi="102.4"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata4595">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:snap-bbox="false"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1124"
inkscape:window-height="686"
id="namedview4593"
showgrid="false"
inkscape:snap-nodes="true"
inkscape:snap-global="true"
inkscape:zoom="5.5056667"
inkscape:cx="-3.7234364"
inkscape:cy="-0.36326209"
inkscape:window-x="2807"
inkscape:window-y="192"
inkscape:window-maximized="0"
inkscape:current-layer="svg1437"
inkscape:pagecheckerboard="true"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
id="grid11889"
type="xygrid"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs1440">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="EmptyTriangleInS"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="EmptyTriangleInS">
<path
transform="matrix(-0.2,0,0,-0.2,0.6,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path1125" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="Arrow1Send"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Send">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path983" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2864"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
transform="scale(-0.4)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path2862" />
</marker>
</defs>
<rect
y="0"
x="0"
height="26"
width="26"
id="rect9748"
style="opacity:1;fill:#000000;fill-opacity:0;stroke:none;stroke-width:0.185546;stroke-linecap:round;stroke-opacity:1" />
<g
id="g1193"
transform="translate(-2,-2)">
<circle
style="fill:#cd2828;fill-opacity:0.495968;stroke:#cccccc;stroke-width:1.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path886"
cx="15"
cy="15"
r="10.826572" />
<path
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:0.304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.459071,19.707728 v 3.337127 c 0,0.256023 -0.207467,0.459082 -0.459071,0.459082 -0.256024,0 -0.459081,-0.207469 -0.459081,-0.459082 V 19.707728 C 13.132798,19.597377 11.865921,18.974975 10.921291,18.030335 9.8751216,16.984175 9.22624,15.540738 9.22624,13.951625 c 0,-0.256022 0.2074688,-0.459072 0.4590716,-0.459072 0.2560236,0 0.4590824,0.207469 0.4590824,0.459072 0,1.333089 0.547361,2.546992 1.42578,3.429828 0.878426,0.878427 2.092326,1.42578 3.429826,1.42578 1.333089,0 2.54699,-0.547353 3.429827,-1.42578 0.878418,-0.878426 1.42578,-2.092329 1.42578,-3.429828 0,-0.256022 0.207468,-0.459072 0.459073,-0.459072 0.256031,0 0.45908,0.207469 0.45908,0.459072 0,1.589113 -0.64889,3.03255 -1.69505,4.07871 -0.944631,0.94464 -2.211508,1.567044 -3.619639,1.677393 z M 15.030896,6.496063 c 0.882838,0 1.686223,0.361962 2.2689,0.94464 0.582669,0.582669 0.944631,1.386054 0.944631,2.268891 v 4.15375 c 0,0.882838 -0.361962,1.686223 -0.944631,2.268891 -0.582677,0.582678 -1.386062,0.94464 -2.2689,0.94464 -0.882836,0 -1.68622,-0.361962 -2.26889,-0.94464 -0.582677,-0.582668 -0.94464,-1.386053 -0.94464,-2.268891 v -4.15375 c 0,-0.882837 0.361963,-1.686222 0.94464,-2.268891 0.58267,-0.582678 1.386054,-0.94464 2.26889,-0.94464 z m 1.615599,1.593522 C 16.23156,7.674649 15.65771,7.418626 15.030896,7.418626 c -0.631223,0 -1.200653,0.256023 -1.615588,0.670959 -0.414937,0.414936 -0.670959,0.988775 -0.670959,1.61559 v 4.153759 c 0,0.631224 0.256022,1.200654 0.670959,1.61559 0.414935,0.414936 0.988775,0.670959 1.615588,0.670959 0.631234,0 1.200664,-0.256023 1.615599,-0.670959 0.414927,-0.414936 0.67095,-0.988776 0.67095,-1.61559 V 9.705175 c 0,-0.631225 -0.256023,-1.200654 -0.67095,-1.61559 z"
fill-rule="nonzero"
id="path7563" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="26"
height="26"
id="svg1437"
sodipodi:docname="record_input_active.svg"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
inkscape:export-filename="/Users/jesse/src/sonoaudio/tonalenergy/images/circleslash_icon.png"
inkscape:export-xdpi="102.4"
inkscape:export-ydpi="102.4"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata4595">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:snap-bbox="false"
pagecolor="#616161"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1145"
inkscape:window-height="686"
id="namedview4593"
showgrid="false"
inkscape:snap-nodes="true"
inkscape:snap-global="true"
inkscape:zoom="8.2114431"
inkscape:cx="11.569221"
inkscape:cy="16.440472"
inkscape:window-x="1576"
inkscape:window-y="160"
inkscape:window-maximized="0"
inkscape:current-layer="svg1437"
inkscape:pagecheckerboard="true"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
id="grid11889"
type="xygrid"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs1440">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="EmptyTriangleInS"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="EmptyTriangleInS">
<path
transform="matrix(-0.2,0,0,-0.2,0.6,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path1125" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="Arrow1Send"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Send">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path983" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2864"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
transform="scale(-0.4)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path2862" />
</marker>
</defs>
<rect
y="0"
x="0"
height="26"
width="26"
id="rect9748"
style="opacity:1;fill:#000000;fill-opacity:0;stroke:none;stroke-width:0.185546;stroke-linecap:round;stroke-opacity:1" />
<g
id="g1189"
transform="translate(-2,-2)">
<circle
style="fill:#f74848;fill-opacity:1;stroke:#cccccc;stroke-width:1.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path886"
cx="15"
cy="15"
r="10.826573" />
<path
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:0.304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.459071,19.707728 v 3.337127 c 0,0.256023 -0.207467,0.459082 -0.459071,0.459082 -0.256024,0 -0.459081,-0.207469 -0.459081,-0.459082 v -3.337127 c -1.408121,-0.110351 -2.674998,-0.732753 -3.619628,-1.677393 -1.0461695,-1.04616 -1.6950511,-2.489597 -1.6950511,-4.07871 0,-0.256022 0.2074688,-0.459072 0.4590716,-0.459072 0.2560236,0 0.4590825,0.207469 0.4590825,0.459072 0,1.333089 0.547361,2.546992 1.42578,3.429828 0.878426,0.878427 2.092326,1.42578 3.429826,1.42578 1.333089,0 2.54699,-0.547353 3.429827,-1.42578 0.878418,-0.878426 1.42578,-2.092329 1.42578,-3.429828 0,-0.256022 0.207468,-0.459072 0.459073,-0.459072 0.256031,0 0.45908,0.207469 0.45908,0.459072 0,1.589113 -0.64889,3.03255 -1.69505,4.07871 -0.944631,0.94464 -2.211508,1.567044 -3.619639,1.677393 z M 15.030896,6.4960629 c 0.882838,0 1.686223,0.361962 2.2689,0.9446399 0.582669,0.5826688 0.944631,1.3860537 0.944631,2.2688914 v 4.1537498 c 0,0.882838 -0.361962,1.686223 -0.944631,2.268891 -0.582677,0.582678 -1.386062,0.94464 -2.2689,0.94464 -0.882836,0 -1.68622,-0.361962 -2.26889,-0.94464 -0.582677,-0.582668 -0.94464,-1.386053 -0.94464,-2.268891 V 9.7095942 c 0,-0.8828377 0.361963,-1.6862226 0.94464,-2.2688914 0.58267,-0.5826779 1.386054,-0.9446399 2.26889,-0.9446399 z m 1.615599,1.5935217 C 16.23156,7.6746488 15.65771,7.4186258 15.030896,7.4186258 c -0.631223,0 -1.200653,0.256023 -1.615588,0.6709588 -0.414937,0.414936 -0.670959,0.9887751 -0.670959,1.6155898 v 4.1537596 c 0,0.631224 0.256022,1.200654 0.670959,1.61559 0.414935,0.414936 0.988775,0.670959 1.615588,0.670959 0.631234,0 1.200664,-0.256023 1.615599,-0.670959 0.414927,-0.414936 0.67095,-0.988776 0.67095,-1.61559 V 9.7051744 c 0,-0.6312247 -0.256023,-1.2006538 -0.67095,-1.6155898 z"
fill-rule="nonzero"
id="path7563" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

137
images/record_output.svg Normal file
View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="25.998236"
height="25.998236"
id="svg1437"
sodipodi:docname="record_output.svg"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
inkscape:export-filename="/Users/jesse/src/sonoaudio/tonalenergy/images/circleslash_icon.png"
inkscape:export-xdpi="102.4"
inkscape:export-ydpi="102.4"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata4595">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:snap-bbox="false"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1124"
inkscape:window-height="686"
id="namedview4593"
showgrid="false"
inkscape:snap-nodes="true"
inkscape:snap-global="true"
inkscape:zoom="8.9964123"
inkscape:cx="13.060762"
inkscape:cy="17.451401"
inkscape:window-x="2640"
inkscape:window-y="144"
inkscape:window-maximized="0"
inkscape:current-layer="svg1437"
inkscape:pagecheckerboard="true"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
id="grid11889"
type="xygrid"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs1440">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="EmptyTriangleInS"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="EmptyTriangleInS">
<path
transform="matrix(-0.2,0,0,-0.2,0.6,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path1125" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="Arrow1Send"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Send">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path983" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2864"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
transform="scale(-0.4)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path2862" />
</marker>
</defs>
<rect
y="0"
x="0"
height="25.998236"
width="25.998236"
id="rect9748"
style="opacity:1;fill:#000000;fill-opacity:0;stroke:none;stroke-width:0.185534;stroke-linecap:round;stroke-opacity:1" />
<g
id="g1203"
transform="translate(-2.0008821,-2.0008821)">
<circle
style="fill:#cd2828;fill-opacity:0.495968;stroke:#cccccc;stroke-width:1.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path886"
cx="15"
cy="15"
r="10.826572" />
<g
id="Layer_x0020_1"
transform="matrix(0.00449817,0,0,0.00449817,7.1242974,9.0587762)"
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:67.583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<path
id="path7296"
fill-rule="nonzero"
d="m 2268,282 c -53,0 -95,-43 -95,-95 0,-53 43,-95 95,-95 356,0 678,144 911,377 233,233 377,555 377,911 0,356 -144,678 -377,911 -233,233 -555,377 -911,377 -53,0 -95,-43 -95,-95 0,-53 43,-95 95,-95 303,0 578,-123 776,-322 199,-199 322,-473 322,-776 0,-303 -123,-578 -322,-776 C 2845,405 2571,282 2268,282 Z M 303,630 h 702 L 1603,28 c 37,-37 97,-37 134,0 19,19 28,43 28,67 v 2474 c 0,53 -43,95 -95,95 -27,0 -51,-11 -68,-29 L 1010,2136 H 302 C 219,2136 143,2102 88,2047 33,1992 -1,1916 -1,1833 V 932 c 0,-83 34,-159 89,-214 55,-55 131,-89 214,-89 z m 742,190 H 303 c -31,0 -59,13 -80,33 -20,20 -33,49 -33,80 v 901 c 0,31 13,59 33,80 20,20 49,33 80,33 h 742 c 22,0 43,7 61,22 l 470,396 V 325 l -459,462 c -17,20 -43,33 -72,33 z m 1176,5 c -43,0 -79,-35 -79,-79 0,-43 35,-79 79,-79 202,0 385,82 517,214 132,132 214,315 214,517 0,202 -82,385 -214,517 -132,132 -315,214 -517,214 -43,0 -79,-35 -79,-79 0,-43 35,-79 79,-79 159,0 302,-64 406,-168 104,-104 168,-247 168,-406 0,-159 -64,-302 -168,-406 C 2523,887 2380,823 2221,823 Z"
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:67.583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="26"
height="26"
id="svg1437"
sodipodi:docname="record_output_active.svg"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
inkscape:export-filename="/Users/jesse/src/sonoaudio/tonalenergy/images/circleslash_icon.png"
inkscape:export-xdpi="102.4"
inkscape:export-ydpi="102.4"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata4595">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:snap-bbox="false"
pagecolor="#616161"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1145"
inkscape:window-height="686"
id="namedview4593"
showgrid="false"
inkscape:snap-nodes="true"
inkscape:snap-global="true"
inkscape:zoom="11.275529"
inkscape:cx="9.6669523"
inkscape:cy="18.447028"
inkscape:window-x="2002"
inkscape:window-y="770"
inkscape:window-maximized="0"
inkscape:current-layer="svg1437"
inkscape:pagecheckerboard="true"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
id="grid11889"
type="xygrid"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs1440">
<marker
inkscape:isstock="true"
style="overflow:visible"
id="EmptyTriangleInS"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="EmptyTriangleInS">
<path
transform="matrix(-0.2,0,0,-0.2,0.6,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path1125" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="Arrow1Send"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Send">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path983" />
</marker>
<marker
inkscape:isstock="true"
style="overflow:visible"
id="marker2864"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="TriangleInM">
<path
transform="scale(-0.4)"
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1pt;stroke-opacity:1"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path2862" />
</marker>
</defs>
<rect
y="0"
x="0"
height="26"
width="26"
id="rect9748"
style="opacity:1;fill:#000000;fill-opacity:0;stroke:none;stroke-width:0.185546;stroke-linecap:round;stroke-opacity:1" />
<g
id="g1198"
transform="translate(-2,-2)">
<circle
style="fill:#f74848;fill-opacity:1;stroke:#cccccc;stroke-width:1.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path886"
cx="15"
cy="15"
r="10.826573" />
<g
id="Layer_x0020_1"
transform="matrix(0.00449817,0,0,0.00449817,7.0045028,8.9984055)"
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:67.583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<path
id="path7296"
fill-rule="nonzero"
d="m 2268,282 c -53,0 -95,-43 -95,-95 0,-53 43,-95 95,-95 356,0 678,144 911,377 233,233 377,555 377,911 0,356 -144,678 -377,911 -233,233 -555,377 -911,377 -53,0 -95,-43 -95,-95 0,-53 43,-95 95,-95 303,0 578,-123 776,-322 199,-199 322,-473 322,-776 0,-303 -123,-578 -322,-776 C 2845,405 2571,282 2268,282 Z M 303,630 h 702 L 1603,28 c 37,-37 97,-37 134,0 19,19 28,43 28,67 v 2474 c 0,53 -43,95 -95,95 -27,0 -51,-11 -68,-29 L 1010,2136 H 302 C 219,2136 143,2102 88,2047 33,1992 -1,1916 -1,1833 V 932 c 0,-83 34,-159 89,-214 55,-55 131,-89 214,-89 z m 742,190 H 303 c -31,0 -59,13 -80,33 -20,20 -33,49 -33,80 v 901 c 0,31 13,59 33,80 20,20 49,33 80,33 h 742 c 22,0 43,7 61,22 l 470,396 V 325 l -459,462 c -17,20 -43,33 -72,33 z m 1176,5 c -43,0 -79,-35 -79,-79 0,-43 35,-79 79,-79 202,0 385,82 517,214 132,132 214,315 214,517 0,202 -82,385 -214,517 -132,132 -315,214 -517,214 -43,0 -79,-35 -79,-79 0,-43 35,-79 79,-79 159,0 302,-64 406,-168 104,-104 168,-247 168,-406 0,-159 -64,-302 -168,-406 C 2523,887 2380,823 2221,823 Z"
style="fill:#e6e6e6;fill-opacity:1;stroke:#d4d4d4;stroke-width:67.583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB