diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1c8e7a24..cc7ffc9b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,6 +3,7 @@ on: push: branches: - release + - 3d-audio jobs: build-linux: name: Build Linux @@ -18,7 +19,9 @@ jobs: - name: "Run script" run: | export OS="linux" - ./ci/build.sh + source ./ci/setup-env.sh + source ./ci/test.sh + source ./ci/build.sh shell: bash - name: Upload Artifact uses: actions/upload-artifact@v3 @@ -40,7 +43,9 @@ jobs: - name: "Run script" run: | export OS="mac" - ./ci/build.sh + source ./ci/setup-env.sh + source ./ci/test.sh + source ./ci/build.sh shell: bash - name: Upload Artifact uses: actions/upload-artifact@v3 @@ -62,7 +67,9 @@ jobs: - name: "Run script" run: | export OS="win" - ./ci/build.sh + source ./ci/setup-env.sh + source ./ci/test.sh + source ./ci/build.sh shell: bash - name: Upload Artifact uses: actions/upload-artifact@v3 diff --git a/Resources/svg/delete.svg b/Resources/svg/delete.svg new file mode 100644 index 00000000..8b82122a --- /dev/null +++ b/Resources/svg/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/left_arrow.svg b/Resources/svg/left_arrow.svg new file mode 100644 index 00000000..75dcd623 --- /dev/null +++ b/Resources/svg/left_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/random.svg b/Resources/svg/random.svg new file mode 100644 index 00000000..de6f95e9 --- /dev/null +++ b/Resources/svg/random.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/right_arrow.svg b/Resources/svg/right_arrow.svg new file mode 100644 index 00000000..a763cfde --- /dev/null +++ b/Resources/svg/right_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index d0dcd0c8..d682781d 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -23,6 +23,15 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP }; addAndMakeVisible(addBtn);*/ + addAndMakeVisible(randomiseButton); + + randomiseButton.setTooltip("Randomise all effect parameter values, randomise which effects are enabled, and randomise their order."); + + randomiseButton.onClick = [this] { + itemData.randomise(); + listBox.updateContent(); + }; + { juce::MessageManagerLock lock; audioProcessor.broadcaster.addChangeListener(this); @@ -38,7 +47,12 @@ EffectsComponent::~EffectsComponent() { } void EffectsComponent::resized() { - auto area = getLocalBounds().withTrimmedTop(20).reduced(20); + auto area = getLocalBounds(); + auto titleBar = area.removeFromTop(30); + titleBar.removeFromLeft(100); + + randomiseButton.setBounds(titleBar.removeFromLeft(20)); + area = area.reduced(20); frequency.setBounds(area.removeFromTop(30)); area.removeFromTop(6); diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 986bad41..071ee7cd 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -1,6 +1,7 @@ #pragma once #include +#include "LookAndFeel.h" #include "audio/BitCrushEffect.h" #include "PluginProcessor.h" #include "components/DraggableListBox.h" @@ -20,6 +21,8 @@ class EffectsComponent : public juce::GroupComponent, public juce::ChangeListene // juce::TextButton addBtn; + SvgButton randomiseButton{ "randomise", juce::String(BinaryData::random_svg), Colours::accentColor }; + AudioEffectListBoxItemData itemData; EffectsListBoxModel listBoxModel; DraggableListBox listBox; diff --git a/Source/LegacyProject.cpp b/Source/LegacyProject.cpp index c17ed05b..fc48ab14 100644 --- a/Source/LegacyProject.cpp +++ b/Source/LegacyProject.cpp @@ -122,7 +122,7 @@ void OscirenderAudioProcessor::openLegacyProject(const juce::XmlElement* xml) { if (perspectiveFunction != nullptr) { auto stream = juce::MemoryOutputStream(); juce::Base64::convertFromBase64(stream, perspectiveFunction->getAllSubText()); - perspectiveEffect->updateCode(stream.toString()); + customEffect->updateCode(stream.toString()); } auto fontFamilyXml = xml->getChildByName("fontFamily"); diff --git a/Source/LuaComponent.cpp b/Source/LuaComponent.cpp index de6edc92..147773b0 100644 --- a/Source/LuaComponent.cpp +++ b/Source/LuaComponent.cpp @@ -2,7 +2,7 @@ #include "PluginEditor.h" LuaComponent::LuaComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor), slidersModel(sliders, p) { - setText(".lua File Settings"); + setText("Lua Settings"); sliders.setModel(&slidersModel); sliders.setRowHeight(30); diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 0670823f..9493179a 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -32,7 +32,6 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess }; addAndMakeVisible(closeFileButton); - closeFileButton.setButtonText("Close File"); closeFileButton.onClick = [this] { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); @@ -49,9 +48,40 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess inputEnabled.onClick = [this] { audioProcessor.inputEnabled->setBoolValueNotifyingHost(!audioProcessor.inputEnabled->getBoolValue()); }; + inputEnabled.setTooltip("Enable to use input audio, instead of the generated audio."); + addAndMakeVisible(fileLabel); + fileLabel.setJustificationType(juce::Justification::centred); updateFileLabel(); + addAndMakeVisible(leftArrow); + leftArrow.onClick = [this] { + juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); + juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); + + int index = audioProcessor.getCurrentFileIndex(); + + if (index > 0) { + audioProcessor.changeCurrentFile(index - 1); + pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); + } + }; + leftArrow.setTooltip("Change to previous file (k)."); + + addAndMakeVisible(rightArrow); + rightArrow.onClick = [this] { + juce::SpinLock::ScopedLockType parserLock(audioProcessor.parsersLock); + juce::SpinLock::ScopedLockType effectsLock(audioProcessor.effectsLock); + + int index = audioProcessor.getCurrentFileIndex(); + + if (index < audioProcessor.numFiles() - 1) { + audioProcessor.changeCurrentFile(index + 1); + pluginEditor.fileUpdated(audioProcessor.getCurrentFileName()); + } + }; + rightArrow.setTooltip("Change to next file (j)."); + addAndMakeVisible(fileName); fileType.addItem(".lua", 1); @@ -109,6 +139,8 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess pluginEditor.removeChildComponent(&pluginEditor.visualiser); addAndMakeVisible(pluginEditor.visualiser); } + pluginEditor.visualiser.setFullScreen(pluginEditor.visualiserFullScreen); + pluginEditor.resized(); pluginEditor.repaint(); resized(); @@ -139,6 +171,9 @@ MainComponent::~MainComponent() { } void MainComponent::updateFileLabel() { + showLeftArrow = audioProcessor.getCurrentFileIndex() > 0; + showRightArrow = audioProcessor.getCurrentFileIndex() < audioProcessor.numFiles() - 1; + if (audioProcessor.objectServerRendering) { fileLabel.setText("Rendering from Blender", juce::dontSendNotification); } else if (audioProcessor.getCurrentFileIndex() == -1) { @@ -146,6 +181,8 @@ void MainComponent::updateFileLabel() { } else { fileLabel.setText(audioProcessor.getCurrentFileName(), juce::dontSendNotification); } + + resized(); } void MainComponent::resized() { @@ -163,9 +200,30 @@ void MainComponent::resized() { row.removeFromLeft(rowPadding); inputEnabled.setBounds(row.removeFromLeft(20)); row.removeFromLeft(rowPadding); + if (audioProcessor.getCurrentFileIndex() != -1) { + closeFileButton.setBounds(row.removeFromRight(20)); + row.removeFromRight(rowPadding); + } else { + closeFileButton.setBounds(juce::Rectangle()); + } + + auto arrowLeftBounds = row.removeFromLeft(15); + if (showLeftArrow) { + leftArrow.setBounds(arrowLeftBounds); + } else { + leftArrow.setBounds(0, 0, 0, 0); + } + row.removeFromLeft(rowPadding); + + auto arrowRightBounds = row.removeFromRight(15); + if (showRightArrow) { + rightArrow.setBounds(arrowRightBounds); + } else { + rightArrow.setBounds(0, 0, 0, 0); + } + row.removeFromRight(rowPadding); + fileLabel.setBounds(row); - bounds.removeFromTop(padding); - closeFileButton.setBounds(bounds.removeFromTop(buttonHeight).removeFromLeft(buttonWidth)); bounds.removeFromTop(padding); row = bounds.removeFromTop(buttonHeight); diff --git a/Source/MainComponent.h b/Source/MainComponent.h index efb28e4f..9df77308 100644 --- a/Source/MainComponent.h +++ b/Source/MainComponent.h @@ -25,9 +25,13 @@ class MainComponent : public juce::GroupComponent { std::unique_ptr chooser; juce::TextButton fileButton; - juce::TextButton closeFileButton; + SvgButton closeFileButton{"closeFile", juce::String(BinaryData::delete_svg), juce::Colours::red}; SvgButton inputEnabled{"inputEnabled", juce::String(BinaryData::microphone_svg), juce::Colours::white, juce::Colours::red, audioProcessor.inputEnabled}; juce::Label fileLabel; + SvgButton leftArrow{"leftArrow", juce::String(BinaryData::left_arrow_svg), juce::Colours::white}; + SvgButton rightArrow{"rightArrow", juce::String(BinaryData::right_arrow_svg), juce::Colours::white}; + bool showLeftArrow = false; + bool showRightArrow = false; juce::TextEditor fileName; juce::ComboBox fileType; diff --git a/Source/ObjComponent.cpp b/Source/ObjComponent.cpp deleted file mode 100644 index 3bc1c48c..00000000 --- a/Source/ObjComponent.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "ObjComponent.h" -#include "PluginEditor.h" -#include - -ObjComponent::ObjComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { - setText("3D .obj File Settings"); - - juce::Desktop::getInstance().addGlobalMouseListener(this); - - addAndMakeVisible(focalLength); - addAndMakeVisible(rotateX); - addAndMakeVisible(rotateY); - addAndMakeVisible(rotateZ); - addAndMakeVisible(rotateSpeed); - - focalLength.slider.onValueChange = [this] { - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - audioProcessor.focalLength->setValue(focalLength.slider.getValue()); - audioProcessor.focalLength->apply(); - }; - - auto onRotationChange = [this]() { - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - audioProcessor.rotateX->setValue(rotateX.slider.getValue()); - audioProcessor.rotateY->setValue(rotateY.slider.getValue()); - audioProcessor.rotateZ->setValue(rotateZ.slider.getValue()); - - audioProcessor.fixedRotateX->setBoolValueNotifyingHost(fixedRotateX->getToggleState()); - audioProcessor.fixedRotateY->setBoolValueNotifyingHost(fixedRotateY->getToggleState()); - audioProcessor.fixedRotateZ->setBoolValueNotifyingHost(fixedRotateZ->getToggleState()); - }; - - rotateX.slider.onValueChange = onRotationChange; - rotateY.slider.onValueChange = onRotationChange; - rotateZ.slider.onValueChange = onRotationChange; - - rotateSpeed.slider.onValueChange = [this] { - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - audioProcessor.rotateSpeed->setValue(rotateSpeed.slider.getValue()); - audioProcessor.rotateSpeed->apply(); - }; - - addAndMakeVisible(resetRotation); - addAndMakeVisible(mouseRotate); - - resetRotation.onClick = [this] { - fixedRotateX->setToggleState(false, juce::NotificationType::dontSendNotification); - fixedRotateY->setToggleState(false, juce::NotificationType::dontSendNotification); - fixedRotateZ->setToggleState(false, juce::NotificationType::dontSendNotification); - - rotateX.slider.setValue(0); - rotateY.slider.setValue(0); - rotateZ.slider.setValue(0); - rotateSpeed.slider.setValue(0); - - mouseRotate.setToggleState(false, juce::NotificationType::dontSendNotification); - - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - if (audioProcessor.getCurrentFileIndex() != -1) { - auto obj = audioProcessor.getCurrentFileParser()->getObject(); - if (obj != nullptr) { - obj->setCurrentRotationX(0); - obj->setCurrentRotationY(0); - obj->setCurrentRotationZ(0); - } - } - }; - - fixedRotateX->onClick = onRotationChange; - fixedRotateY->onClick = onRotationChange; - fixedRotateZ->onClick = onRotationChange; - - rotateX.setComponent(fixedRotateX); - rotateY.setComponent(fixedRotateY); - rotateZ.setComponent(fixedRotateZ); - - juce::String tooltip = "Toggles whether the rotation around this axis is fixed, or changes according to the rotation speed."; - fixedRotateX->setTooltip(tooltip); - fixedRotateY->setTooltip(tooltip); - fixedRotateZ->setTooltip(tooltip); -} - -ObjComponent::~ObjComponent() { - juce::Desktop::getInstance().removeGlobalMouseListener(this); -} - -// listen for mouse movement and rotate the object if mouseRotate is enabled -void ObjComponent::mouseMove(const juce::MouseEvent& e) { - if (mouseRotate.getToggleState()) { - auto globalEvent = e.getEventRelativeTo(&pluginEditor); - auto width = pluginEditor.getWidth(); - auto height = pluginEditor.getHeight(); - auto x = globalEvent.position.getX(); - auto y = globalEvent.position.getY(); - - rotateX.slider.setValue(2 * x / width - 1); - rotateY.slider.setValue(1 - 2 * y / height); - } -} - -void ObjComponent::disableMouseRotation() { - mouseRotate.setToggleState(false, juce::NotificationType::dontSendNotification); -} - -void ObjComponent::resized() { - auto area = getLocalBounds().withTrimmedTop(20).reduced(20); - double rowHeight = 30; - focalLength.setBounds(area.removeFromTop(rowHeight)); - rotateX.setBounds(area.removeFromTop(rowHeight)); - rotateY.setBounds(area.removeFromTop(rowHeight)); - rotateZ.setBounds(area.removeFromTop(rowHeight)); - rotateSpeed.setBounds(area.removeFromTop(rowHeight)); - - area.removeFromTop(10); - auto row = area.removeFromTop(rowHeight); - resetRotation.setBounds(row.removeFromLeft(120)); - row.removeFromLeft(20); - mouseRotate.setBounds(row); -} diff --git a/Source/ObjComponent.h b/Source/ObjComponent.h deleted file mode 100644 index 3820fc0a..00000000 --- a/Source/ObjComponent.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include "PluginProcessor.h" -#include "components/EffectComponent.h" -#include "components/SvgButton.h" - -class OscirenderAudioProcessorEditor; -class ObjComponent : public juce::GroupComponent { -public: - ObjComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); - ~ObjComponent(); - - void resized() override; - void mouseMove(const juce::MouseEvent& event) override; - void disableMouseRotation(); -private: - OscirenderAudioProcessor& audioProcessor; - OscirenderAudioProcessorEditor& pluginEditor; - - EffectComponent focalLength{audioProcessor, *audioProcessor.focalLength, false}; - EffectComponent rotateX{audioProcessor, *audioProcessor.rotateX, false}; - EffectComponent rotateY{audioProcessor, *audioProcessor.rotateY, false}; - EffectComponent rotateZ{audioProcessor, *audioProcessor.rotateZ, false}; - EffectComponent rotateSpeed{audioProcessor, *audioProcessor.rotateSpeed, false}; - - juce::TextButton resetRotation{"Reset Rotation"}; - juce::ToggleButton mouseRotate{"Rotate with Mouse (Esc to disable)"}; - - std::shared_ptr fixedRotateX = std::make_shared("fixedRotateX", juce::String(BinaryData::fixed_rotate_svg), juce::Colours::white, juce::Colours::red, audioProcessor.fixedRotateX); - std::shared_ptr fixedRotateY = std::make_shared("fixedRotateY", juce::String(BinaryData::fixed_rotate_svg), juce::Colours::white, juce::Colours::red, audioProcessor.fixedRotateY); - std::shared_ptr fixedRotateZ = std::make_shared("fixedRotateZ", juce::String(BinaryData::fixed_rotate_svg), juce::Colours::white, juce::Colours::red, audioProcessor.fixedRotateZ); - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ObjComponent) -}; diff --git a/Source/PerspectiveComponent.cpp b/Source/PerspectiveComponent.cpp new file mode 100644 index 00000000..f418c6d7 --- /dev/null +++ b/Source/PerspectiveComponent.cpp @@ -0,0 +1,22 @@ +#include "PerspectiveComponent.h" +#include "PluginEditor.h" +#include + +PerspectiveComponent::PerspectiveComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { + setText("3D Settings"); + + addAndMakeVisible(perspective); + addAndMakeVisible(focalLength); + + perspective.setSliderOnValueChange(); + focalLength.setSliderOnValueChange(); +} + +PerspectiveComponent::~PerspectiveComponent() {} + +void PerspectiveComponent::resized() { + auto area = getLocalBounds().withTrimmedTop(20).reduced(20); + double rowHeight = 30; + perspective.setBounds(area.removeFromTop(rowHeight)); + focalLength.setBounds(area.removeFromTop(rowHeight)); +} diff --git a/Source/PerspectiveComponent.h b/Source/PerspectiveComponent.h new file mode 100644 index 00000000..a60cc3e1 --- /dev/null +++ b/Source/PerspectiveComponent.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "components/EffectComponent.h" +#include "components/SvgButton.h" + +class OscirenderAudioProcessorEditor; +class PerspectiveComponent : public juce::GroupComponent { +public: + PerspectiveComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); + ~PerspectiveComponent(); + + void resized() override; +private: + OscirenderAudioProcessor& audioProcessor; + OscirenderAudioProcessorEditor& pluginEditor; + + EffectComponent perspective{audioProcessor, *audioProcessor.perspective, 0}; + EffectComponent focalLength{audioProcessor, *audioProcessor.perspective, 1}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PerspectiveComponent) +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 0160b483..d5b476e5 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -29,8 +29,8 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr collapseButton.onClick = [this] { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); int originalIndex = audioProcessor.getCurrentFileIndex(); - int index = editingPerspective ? 0 : audioProcessor.getCurrentFileIndex() + 1; - if (originalIndex != -1 || editingPerspective) { + int index = editingCustomFunction ? 0 : audioProcessor.getCurrentFileIndex() + 1; + if (originalIndex != -1 || editingCustomFunction) { if (codeEditors[index]->isVisible()) { codeEditors[index]->setVisible(false); } else { @@ -80,12 +80,19 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr setResizeLimits(500, 400, 999999, 999999); layout.setItemLayout(0, -0.3, -1.0, -0.7); - layout.setItemLayout(1, 7, 7, 7); + layout.setItemLayout(1, RESIZER_BAR_SIZE, RESIZER_BAR_SIZE, RESIZER_BAR_SIZE); layout.setItemLayout(2, -0.1, -1.0, -0.3); addAndMakeVisible(settings); addAndMakeVisible(resizerBar); + luaLayout.setItemLayout(0, -0.3, -1.0, -0.7); + luaLayout.setItemLayout(1, RESIZER_BAR_SIZE, RESIZER_BAR_SIZE, RESIZER_BAR_SIZE); + luaLayout.setItemLayout(2, -0.1, -1.0, -0.3); + + addAndMakeVisible(lua); + addAndMakeVisible(luaResizerBar); + if (visualiserFullScreen) { addAndMakeVisible(visualiser); } @@ -120,21 +127,22 @@ void OscirenderAudioProcessorEditor::initialiseCodeEditors() { void OscirenderAudioProcessorEditor::paint(juce::Graphics& g) { g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); + auto ds = juce::DropShadow(juce::Colours::black, 5, juce::Point(0, 0)); + if (!usingNativeMenuBar) { // add drop shadow to the menu bar - auto ds = juce::DropShadow(juce::Colours::black, 5, juce::Point(0, 0)); ds.drawForRectangle(g, menuBar.getBounds()); } - // draw drop shadow around code editor if visible - juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - - int originalIndex = audioProcessor.getCurrentFileIndex(); - int index = editingPerspective ? 0 : audioProcessor.getCurrentFileIndex() + 1; - if ((originalIndex != -1 || editingPerspective) && index < codeEditors.size() && codeEditors[index]->isVisible()) { - auto ds = juce::DropShadow(juce::Colours::black, 5, juce::Point(0, 0)); - ds.drawForRectangle(g, codeEditors[index]->getBounds()); - } + for (int i = 0; i < codeEditors.size(); i++) { + if (codeEditors[i]->getBounds().getWidth() > 0 && codeEditors[i]->getBounds().getHeight() > 0) { + ds.drawForRectangle(g, codeEditors[i]->getBounds()); + } + } + + if (lua.getBounds().getWidth() > 0 && lua.getBounds().getHeight() > 0) { + ds.drawForRectangle(g, lua.getBounds()); + } } void OscirenderAudioProcessorEditor::resized() { @@ -156,34 +164,56 @@ void OscirenderAudioProcessorEditor::resized() { area.removeFromLeft(3); bool editorVisible = false; - juce::Component dummy; - { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); int originalIndex = audioProcessor.getCurrentFileIndex(); - int index = editingPerspective ? 0 : audioProcessor.getCurrentFileIndex() + 1; - if (originalIndex != -1 || editingPerspective) { + int index = editingCustomFunction ? 0 : audioProcessor.getCurrentFileIndex() + 1; + if (originalIndex != -1 || editingCustomFunction) { if (codeEditors[index]->isVisible()) { editorVisible = true; - juce::Component* columns[] = { &dummy, &resizerBar, codeEditors[index].get() }; + juce::Component dummy; + juce::Component dummy2; + + juce::Component* columns[] = { &dummy, &resizerBar, &dummy2 }; // offsetting the y position by -1 and the height by +1 is a hack to fix a bug where the code editor // doesn't draw up to the edges of the menu bar above. layout.layOutComponents(columns, 3, area.getX(), area.getY() - 1, area.getWidth(), area.getHeight() + 1, false, true); auto dummyBounds = dummy.getBounds(); collapseButton.setBounds(dummyBounds.removeFromRight(20)); - area = dummyBounds; - + + auto dummy2Bounds = dummy2.getBounds(); + dummy2Bounds.removeFromBottom(5); + dummy2Bounds.removeFromTop(5); + dummy2Bounds.removeFromRight(5); + + juce::String extension; + if (originalIndex >= 0) { + extension = audioProcessor.getFileName(originalIndex).fromLastOccurrenceOf(".", true, false); + } + + if (editingCustomFunction || extension == ".lua") { + juce::Component* rows[] = { codeEditors[index].get(), &luaResizerBar, &lua }; + luaLayout.layOutComponents(rows, 3, dummy2Bounds.getX(), dummy2Bounds.getY(), dummy2Bounds.getWidth(), dummy2Bounds.getHeight(), true, true); + } else { + codeEditors[index]->setBounds(dummy2Bounds); + luaResizerBar.setBounds(0, 0, 0, 0); + lua.setBounds(0, 0, 0, 0); + } } else { codeEditors[index]->setBounds(0, 0, 0, 0); resizerBar.setBounds(0, 0, 0, 0); + luaResizerBar.setBounds(0, 0, 0, 0); + lua.setBounds(0, 0, 0, 0); collapseButton.setBounds(area.removeFromRight(20)); } } else { collapseButton.setBounds(0, 0, 0, 0); + luaResizerBar.setBounds(0, 0, 0, 0); + lua.setBounds(0, 0, 0, 0); } } @@ -208,8 +238,8 @@ void OscirenderAudioProcessorEditor::addCodeEditor(int index) { std::shared_ptr editor; if (index == 0) { - codeDocument = perspectiveCodeDocument; - editor = perspectiveCodeEditor; + codeDocument = customFunctionCodeDocument; + editor = customFunctionCodeEditor; } else { codeDocument = std::make_shared(); juce::String extension = audioProcessor.getFileName(originalIndex).fromLastOccurrenceOf(".", true, false); @@ -250,8 +280,8 @@ void OscirenderAudioProcessorEditor::updateCodeEditor() { } } int originalIndex = audioProcessor.getCurrentFileIndex(); - int index = editingPerspective ? 0 : audioProcessor.getCurrentFileIndex() + 1; - if ((originalIndex != -1 || editingPerspective) && visible) { + int index = editingCustomFunction ? 0 : audioProcessor.getCurrentFileIndex() + 1; + if ((originalIndex != -1 || editingCustomFunction) && visible) { for (int i = 0; i < codeEditors.size(); i++) { codeEditors[i]->setVisible(false); } @@ -262,7 +292,7 @@ void OscirenderAudioProcessorEditor::updateCodeEditor() { // message thread, this is safe. updatingDocumentsWithParserLock = true; if (index == 0) { - codeEditors[index]->loadContent(audioProcessor.perspectiveEffect->getCode()); + codeEditors[index]->loadContent(audioProcessor.customEffect->getCode()); } else { codeEditors[index]->loadContent(juce::MemoryInputStream(*audioProcessor.getFileBlock(originalIndex), false).readEntireStreamAsString()); } @@ -298,8 +328,27 @@ void OscirenderAudioProcessorEditor::changeListenerCallback(juce::ChangeBroadcas } } -void OscirenderAudioProcessorEditor::editPerspectiveFunction(bool enable) { - editingPerspective = enable; +void OscirenderAudioProcessorEditor::toggleLayout(juce::StretchableLayoutManager& layout, double prefSize) { + double minSize, maxSize, preferredSize; + double otherMinSize, otherMaxSize, otherPreferredSize; + layout.getItemLayout(2, minSize, maxSize, preferredSize); + layout.getItemLayout(0, otherMinSize, otherMaxSize, otherPreferredSize); + + if (preferredSize == CLOSED_PREF_SIZE) { + double otherPrefSize = -(1 + prefSize); + if (prefSize > 0) { + otherPrefSize = -1.0; + } + layout.setItemLayout(2, CLOSED_PREF_SIZE, maxSize, prefSize); + layout.setItemLayout(0, CLOSED_PREF_SIZE, otherMaxSize, otherPrefSize); + } else { + layout.setItemLayout(2, CLOSED_PREF_SIZE, maxSize, CLOSED_PREF_SIZE); + layout.setItemLayout(0, CLOSED_PREF_SIZE, otherMaxSize, -1.0); + } +} + +void OscirenderAudioProcessorEditor::editCustomFunction(bool enable) { + editingCustomFunction = enable; juce::SpinLock::ScopedLockType lock1(audioProcessor.parsersLock); juce::SpinLock::ScopedLockType lock2(audioProcessor.effectsLock); codeEditors[0]->setVisible(enable); @@ -328,9 +377,9 @@ void OscirenderAudioProcessorEditor::codeDocumentTextDeleted(int startIndex, int // parsersLock AND effectsLock must be locked before calling this function void OscirenderAudioProcessorEditor::updateCodeDocument() { - if (editingPerspective) { + if (editingCustomFunction) { juce::String file = codeDocuments[0]->getAllContent(); - audioProcessor.perspectiveEffect->updateCode(file); + audioProcessor.customEffect->updateCode(file); audioProcessor.updateLuaValues(); } else { int originalIndex = audioProcessor.getCurrentFileIndex(); @@ -377,9 +426,7 @@ bool OscirenderAudioProcessorEditor::keyPressed(const juce::KeyPress& key) { } } - if (key.isKeyCode(juce::KeyPress::escapeKey)) { - settings.disableMouseRotation(); - } else if (key.getModifiers().isCommandDown() && key.getModifiers().isShiftDown() && key.getKeyCode() == 'S') { + if (key.getModifiers().isCommandDown() && key.getModifiers().isShiftDown() && key.getKeyCode() == 'S') { saveProjectAs(); } else if (key.getModifiers().isCommandDown() && key.getKeyCode() == 'S') { saveProject(); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index e0b44102..82faf0ca 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -24,8 +24,9 @@ class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, privat void fileUpdated(juce::String fileName); void handleAsyncUpdate() override; void changeListenerCallback(juce::ChangeBroadcaster* source) override; + void toggleLayout(juce::StretchableLayoutManager& layout, double prefSize); - void editPerspectiveFunction(bool enabled); + void editCustomFunction(bool enabled); void newProject(); void openProject(); @@ -39,13 +40,17 @@ class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, privat OscirenderAudioProcessor& audioProcessor; public: + const double CLOSED_PREF_SIZE = 30.0; + const double RESIZER_BAR_SIZE = 7.0; + OscirenderLookAndFeel lookAndFeel; - std::atomic editingPerspective = false; + std::atomic editingCustomFunction = false; VisualiserComponent visualiser{2, audioProcessor}; std::atomic visualiserFullScreen = false; SettingsComponent settings{audioProcessor, *this}; + LuaComponent lua{audioProcessor, *this}; VolumeComponent volume{audioProcessor}; std::vector> codeDocuments; @@ -54,8 +59,8 @@ class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, privat juce::LuaTokeniser luaTokeniser; juce::XmlTokeniser xmlTokeniser; juce::ShapeButton collapseButton; - std::shared_ptr perspectiveCodeDocument = std::make_shared(); - std::shared_ptr perspectiveCodeEditor = std::make_shared(*perspectiveCodeDocument, &luaTokeniser, audioProcessor, PerspectiveEffect::FILE_NAME); + std::shared_ptr customFunctionCodeDocument = std::make_shared(); + std::shared_ptr customFunctionCodeEditor = std::make_shared(*customFunctionCodeDocument, &luaTokeniser, audioProcessor, CustomEffect::FILE_NAME); std::unique_ptr chooser; MainMenuBarModel menuBarModel{*this}; @@ -64,6 +69,9 @@ class OscirenderAudioProcessorEditor : public juce::AudioProcessorEditor, privat juce::StretchableLayoutManager layout; juce::StretchableLayoutResizerBar resizerBar{&layout, 1, true}; + juce::StretchableLayoutManager luaLayout; + juce::StretchableLayoutResizerBar luaResizerBar{&luaLayout, 1, false}; + juce::TooltipWindow tooltipWindow{this, 0}; std::atomic updatingDocumentsWithParserLock = false; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 998ee43b..181fd95f 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -10,7 +10,6 @@ #include "PluginEditor.h" #include "parser/FileParser.h" #include "parser/FrameProducer.h" -#include "audio/RotateEffect.h" #include "audio/VectorCancellingEffect.h" #include "audio/DistortEffect.h" #include "audio/SmoothEffect.h" @@ -36,64 +35,107 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the image look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.0, 0.0, 1.0) + new EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.6, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.0, 0.0, 1.0) - )); - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - new EffectParameter("2D Rotate", "Rotates the image, and pans the audio.", "2DRotateSpeed", VERSION_HINT, 0.0, 0.0, 1.0) + new EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.5, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.0, 0.0, 1.0) + new EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.1111111, 0.0, 1.0) )); + toggleableEffects.push_back(std::make_shared( + [this](int index, Point input, const std::vector& values, double sampleRate) { + return input * Point(values[0], values[1], values[2]); + }, std::vector{ + new EffectParameter("Scale X", "Scales the object in the horizontal direction.", "scaleX", VERSION_HINT, 1.0, -5.0, 5.0), + new EffectParameter("Scale Y", "Scales the object in the vertical direction.", "scaleY", VERSION_HINT, 1.0, -5.0, 5.0), + new EffectParameter("Scale Z", "Scales the depth of the object.", "scaleZ", VERSION_HINT, 1.0, -5.0, 5.0), + } + )); toggleableEffects.push_back(std::make_shared( - std::make_shared(false), - new EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0) + [this](int index, Point input, const std::vector& values, double sampleRate) { + int flip = index % 2 == 0 ? 1 : -1; + Point jitter = Point(flip * values[0], flip * values[1], flip * values[2]); + return input + jitter; + }, std::vector{ + new EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0), + new EffectParameter("Distort Y", "Distorts the image in the vertical direction by jittering the audio sample being drawn.", "distortY", VERSION_HINT, 0.0, 0.0, 1.0), + new EffectParameter("Distort Z", "Distorts the depth of the image by jittering the audio sample being drawn.", "distortZ", VERSION_HINT, 0.1, 0.0, 1.0), + } )); + auto rippleEffect = std::make_shared( + [this](int index, Point input, const std::vector& values, double sampleRate) { + double phase = values[1] * std::numbers::pi; + double distance = 100 * values[2] * (input.x * input.x + input.y * input.y); + input.z += values[0] * std::sin(phase + distance); + return input; + }, std::vector{ + new EffectParameter("Ripple Depth", "Controls how large the ripples applied to the image are.", "rippleDepth", VERSION_HINT, 0.2, 0.0, 1.0), + new EffectParameter("Ripple Phase", "Controls the position of the ripple. Animate this to see a moving ripple effect.", "ripplePhase", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0), + } + ); + rippleEffect->getParameter("ripplePhase")->lfo->setUnnormalisedValueNotifyingHost((int) LfoType::Sawtooth); + toggleableEffects.push_back(rippleEffect); + auto rotateEffect = std::make_shared( + [this](int index, Point input, const std::vector& values, double sampleRate) { + input.rotate(values[0] * std::numbers::pi, values[1] * std::numbers::pi, values[2] * std::numbers::pi); + return input; + }, std::vector{ + new EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "rotateX", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "rotateY", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "rotateZ", VERSION_HINT, 0.0, -1.0, 1.0), + } + ); + rotateEffect->getParameter("rotateY")->lfo->setUnnormalisedValueNotifyingHost((int) LfoType::Sawtooth); + rotateEffect->getParameter("rotateY")->lfoRate->setUnnormalisedValueNotifyingHost(0.2); + toggleableEffects.push_back(rotateEffect); toggleableEffects.push_back(std::make_shared( - std::make_shared(true), - new EffectParameter("Distort Y", "Distorts the image in the vertical direction by jittering the audio sample being drawn.", "distortY", VERSION_HINT, 0.0, 0.0, 1.0) + [this](int index, Point input, const std::vector& values, double sampleRate) { + return input + Point(values[0], values[1], values[2]); + }, std::vector{ + new EffectParameter("Translate X", "Moves the object horizontally.", "translateX", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Translate Y", "Moves the object vertically.", "translateY", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Translate Z", "Moves the object away from the camera.", "translateZ", VERSION_HINT, 0.0, -1.0, 1.0), + } )); toggleableEffects.push_back(std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - input.x += values[0]; - input.y += values[1]; - return input; + [this](int index, Point input, const std::vector& values, double sampleRate) { + double length = 10 * values[0] * input.magnitude(); + double newX = input.x * std::cos(length) - input.y * std::sin(length); + double newY = input.x * std::sin(length) + input.y * std::cos(length); + return Point(newX, newY, input.z); }, std::vector{ - new EffectParameter("Translate X", "Moves the image horizontally.", "translateX", VERSION_HINT, 0.0, -1.0, 1.0), - new EffectParameter("Translate Y", "Moves the image vertically.", "translateY", VERSION_HINT, 0.0, -1.0, 1.0) + new EffectParameter("Swirl", "Swirls the image in a spiral pattern.", "swirl", VERSION_HINT, 0.3, -1.0, 1.0), } )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", "smoothing", VERSION_HINT, 0.0, 0.0, 1.0) + new EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", "smoothing", VERSION_HINT, 0.75, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( wobbleEffect, - new EffectParameter("Wobble", "Adds a sine wave of the prominent frequency in the audio currently playing. The sine wave's frequency is slightly offset to create a subtle 'wobble' in the image. Increasing the slider increases the strength of the wobble.", "wobble", VERSION_HINT, 0.0, 0.0, 1.0) + new EffectParameter("Wobble", "Adds a sine wave of the prominent frequency in the audio currently playing. The sine wave's frequency is slightly offset to create a subtle 'wobble' in the image. Increasing the slider increases the strength of the wobble.", "wobble", VERSION_HINT, 0.3, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( delayEffect, std::vector{ - new EffectParameter("Delay Decay", "Adds repetitions, delays, or echos to the audio. This slider controls the volume of the echo.", "delayDecay", VERSION_HINT, 0.0, 0.0, 1.0), + new EffectParameter("Delay Decay", "Adds repetitions, delays, or echos to the audio. This slider controls the volume of the echo.", "delayDecay", VERSION_HINT, 0.4, 0.0, 1.0), new EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0) } )); toggleableEffects.push_back(std::make_shared( - perspectiveEffect, + dashedLineEffect, std::vector{ - new EffectParameter("3D Perspective", "Controls the strength of the 3D perspective effect which treats the image as a 3D object that can be rotated.", "perspectiveStrength", VERSION_HINT, 0.0, 0.0, 1.0), - new EffectParameter("Depth (z)", "Controls how far away the 3D object is drawn away from the camera (the Z position).", "perspectiveZPos", VERSION_HINT, 0.1, 0.0, 1.0), - new EffectParameter("Rotate Speed", "Controls how fast the 3D object rotates in the direction determined by the rotation sliders below.", "perspectiveRotateSpeed", VERSION_HINT, 0.0, -1.0, 1.0), - new EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "perspectiveRotateX", VERSION_HINT, 1.0, -1.0, 1.0), - new EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "perspectiveRotateY", VERSION_HINT, 1.0, -1.0, 1.0), - new EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "perspectiveRotateZ", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Dash Length", "Controls the length of the dashed line.", "dashLength", VERSION_HINT, 0.2, 0.0, 1.0), } )); + toggleableEffects.push_back(std::make_shared( + customEffect, + new EffectParameter("Lua Effect", "Controls the strength of the custom Lua effect applied. You can write your own custom effect using Lua by pressing the edit button on the right.", "customEffectStrength", VERSION_HINT, 1.0, 0.0, 1.0) + )); toggleableEffects.push_back(traceMax); toggleableEffects.push_back(traceMin); @@ -105,14 +147,10 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() effect->setPrecedence(i); } + permanentEffects.push_back(perspective); permanentEffects.push_back(frequencyEffect); permanentEffects.push_back(volumeEffect); permanentEffects.push_back(thresholdEffect); - permanentEffects.push_back(rotateSpeed); - permanentEffects.push_back(rotateX); - permanentEffects.push_back(rotateY); - permanentEffects.push_back(rotateZ); - permanentEffects.push_back(focalLength); for (int i = 0; i < 26; i++) { addLuaSlider(); @@ -135,12 +173,6 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() } } - booleanParameters.push_back(fixedRotateX); - booleanParameters.push_back(fixedRotateY); - booleanParameters.push_back(fixedRotateZ); - booleanParameters.push_back(perspectiveEffect->fixedRotateX); - booleanParameters.push_back(perspectiveEffect->fixedRotateY); - booleanParameters.push_back(perspectiveEffect->fixedRotateZ); booleanParameters.push_back(midiEnabled); booleanParameters.push_back(inputEnabled); @@ -324,15 +356,6 @@ void OscirenderAudioProcessor::removeErrorListener(ErrorListener* listener) { errorListeners.erase(std::remove(errorListeners.begin(), errorListeners.end(), listener), errorListeners.end()); } -// parsersLock should be held when calling this -void OscirenderAudioProcessor::updateObjValues() { - focalLength->apply(); - rotateX->apply(); - rotateY->apply(); - rotateZ->apply(); - rotateSpeed->apply(); -} - // effectsLock should be held when calling this std::shared_ptr OscirenderAudioProcessor::getEffect(juce::String id) { for (auto& effect : allEffects) { @@ -471,7 +494,6 @@ void OscirenderAudioProcessor::changeCurrentFile(int index) { } currentFile = index; updateLuaValues(); - updateObjValues(); changeSound(sounds[index]); } @@ -578,7 +600,14 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju inputBuffer.copyFrom(channel, 0, buffer, channel, 0, buffer.getNumSamples()); } + juce::AudioBuffer outputBuffer3d = juce::AudioBuffer(3, buffer.getNumSamples()); + outputBuffer3d.clear(); + if (usingInput && totalNumInputChannels >= 2) { + for (auto channel = 0; channel < juce::jmin(2, totalNumInputChannels); channel++) { + outputBuffer3d.copyFrom(channel, 0, inputBuffer, channel, 0, buffer.getNumSamples()); + } + // handle all midi messages auto midiIterator = midiMessages.cbegin(); std::for_each(midiIterator, @@ -586,11 +615,16 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju [&] (const juce::MidiMessageMetadata& meta) { synth.publicHandleMidiEvent(meta.getMessage()); } ); } else { - // only clear buffer if we aren't using input, since we keep the input audio. - buffer.clear(); juce::SpinLock::ScopedLockType lock1(parsersLock); juce::SpinLock::ScopedLockType lock2(effectsLock); - synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + synth.renderNextBlock(outputBuffer3d, midiMessages, 0, buffer.getNumSamples()); + for (int i = 0; i < synth.getNumVoices(); i++) { + auto voice = dynamic_cast(synth.getVoice(i)); + if (voice->isVoiceActive()) { + customEffect->frequency = voice->getFrequency(); + break; + } + } } midiMessages.clear(); @@ -617,12 +651,7 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju currentVolume = std::sqrt(squaredVolume); currentVolume = juce::jlimit(0.0, 1.0, currentVolume); - Vector2 channels; - if (totalNumOutputChannels >= 2) { - channels = {buffer.getSample(0, sample), buffer.getSample(1, sample)}; - } else if (totalNumOutputChannels == 1) { - channels = {buffer.getSample(0, sample), buffer.getSample(0, sample)}; - } + Point channels = { outputBuffer3d.getSample(0, sample), outputBuffer3d.getSample(1, sample), outputBuffer3d.getSample(2, sample) }; { juce::SpinLock::ScopedLockType lock1(parsersLock); @@ -713,8 +742,8 @@ void OscirenderAudioProcessor::getStateInformation(juce::MemoryBlock& destData) parameter->save(parameterXml); } - auto perspectiveFunction = xml->createNewChildElement("perspectiveFunction"); - perspectiveFunction->addTextElement(juce::Base64::toBase64(perspectiveEffect->getCode())); + auto customFunction = xml->createNewChildElement("customFunction"); + customFunction->addTextElement(juce::Base64::toBase64(customEffect->getCode())); auto fontXml = xml->createNewChildElement("font"); fontXml->setAttribute("family", font.getTypefaceName()); @@ -797,11 +826,14 @@ void OscirenderAudioProcessor::setStateInformation(const void* data, int sizeInB } } - auto perspectiveFunction = xml->getChildByName("perspectiveFunction"); - if (perspectiveFunction != nullptr) { + auto customFunction = xml->getChildByName("customFunction"); + if (customFunction == nullptr) { + customFunction = xml->getChildByName("perspectiveFunction"); + } + if (customFunction != nullptr) { auto stream = juce::MemoryOutputStream(); - juce::Base64::convertFromBase64(stream, perspectiveFunction->getAllSubText()); - perspectiveEffect->updateCode(stream.toString()); + juce::Base64::convertFromBase64(stream, customFunction->getAllSubText()); + customEffect->updateCode(stream.toString()); } auto fontXml = xml->getChildByName("font"); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 89bba058..4fc147d5 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -24,6 +24,8 @@ #include "obj/ObjectServer.h" #include "UGen/Env.h" #include "UGen/ugen_JuceEnvelopeComponent.h" +#include "audio/CustomEffect.h" +#include "audio/DashedLineEffect.h" //============================================================================== /** @@ -71,7 +73,7 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override; void envelopeChanged(EnvelopeComponent* changedEnvelope) override; - int VERSION_HINT = 1; + int VERSION_HINT = 2; std::atomic currentSampleRate = 0.0; @@ -80,7 +82,7 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces std::vector> luaEffects; std::shared_ptr frequencyEffect = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + [this](int index, Point input, const std::vector& values, double sampleRate) { frequency = values[0]; return input; }, new EffectParameter( @@ -92,7 +94,7 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces ); std::shared_ptr volumeEffect = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + [this](int index, Point input, const std::vector& values, double sampleRate) { volume = values[0]; return input; }, new EffectParameter( @@ -104,7 +106,7 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces ); std::shared_ptr thresholdEffect = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + [this](int index, Point input, const std::vector& values, double sampleRate) { threshold = values[0]; return input; }, new EffectParameter( @@ -115,125 +117,42 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces ) ); - std::shared_ptr focalLength = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - if (getCurrentFileIndex() != -1) { - auto camera = getCurrentFileParser()->getCamera(); - if (camera == nullptr) return input; - camera->setFocalLength(values[0]); - } - return input; - }, new EffectParameter( - "Focal length", - "Controls the focal length of the camera being used to render the 3D object. A lower focal length results in a wider field of view, distorting the image, and making the image smaller.", - "objFocalLength", - VERSION_HINT, 1.0, 0.0, 2.0 - ) - ); - - BooleanParameter* fixedRotateX = new BooleanParameter("Object Fixed Rotate X", "objFixedRotateX", VERSION_HINT, false); - BooleanParameter* fixedRotateY = new BooleanParameter("Object Fixed Rotate Y", "objFixedRotateY", VERSION_HINT, false); - BooleanParameter* fixedRotateZ = new BooleanParameter("Object Fixed Rotate Z", "objFixedRotateZ", VERSION_HINT, false); - std::shared_ptr rotateX = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - if (getCurrentFileIndex() != -1) { - auto obj = getCurrentFileParser()->getObject(); - if (obj == nullptr) return input; - auto rotation = values[0] * std::numbers::pi; - if (fixedRotateX->getBoolValue()) { - obj->setCurrentRotationX(rotation); - } else { - obj->setBaseRotationX(rotation); - } - } - return input; - }, new EffectParameter( - "Object Rotate X", - "Controls the rotation of the 3D object around the X axis. When Object Fixed Rotate X is enabled, the object is unaffected by the rotation speed, and remains in a fixed position.", - "objRotateX", - VERSION_HINT, 1.0, -1.0, 1.0 - ) - ); - std::shared_ptr rotateY = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - if (getCurrentFileIndex() != -1) { - auto obj = getCurrentFileParser()->getObject(); - if (obj == nullptr) return input; - auto rotation = values[0] * std::numbers::pi; - if (fixedRotateY->getBoolValue()) { - obj->setCurrentRotationY(rotation); - } else { - obj->setBaseRotationY(rotation); - } - } - return input; - }, new EffectParameter( - "Object Rotate Y", - "Controls the rotation of the 3D object around the Y axis. When Object Fixed Rotate Y is enabled, the object is unaffected by the rotation speed, and remains in a fixed position.", - "objRotateY", - VERSION_HINT, 1.0, -1.0, 1.0 - ) - ); - std::shared_ptr rotateZ = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - if (getCurrentFileIndex() != -1) { - auto obj = getCurrentFileParser()->getObject(); - if (obj == nullptr) return input; - auto rotation = values[0] * std::numbers::pi; - if (fixedRotateZ->getBoolValue()) { - obj->setCurrentRotationZ(rotation); - } else { - obj->setBaseRotationZ(rotation); - } - } - return input; - }, new EffectParameter( - "Object Rotate Z", - "Controls the rotation of the 3D object around the Z axis. When Object Fixed Rotate Z is enabled, the object is unaffected by the rotation speed, and remains in a fixed position.", - "objRotateZ", - VERSION_HINT, 0.0, -1.0, 1.0 - ) - ); - std::shared_ptr rotateSpeed = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - if (getCurrentFileIndex() != -1) { - auto obj = getCurrentFileParser()->getObject(); - if (obj == nullptr) return input; - obj->setRotationSpeed(values[0]); - } - return input; - }, new EffectParameter( - "Rotate Speed", - "Controls the speed at which the 3D object rotates. A negative value results in the object rotating in the opposite direction. The rotate speed is scaled by the different Object Rotate Axis values to rotate the object.", - "objRotateSpeed", - VERSION_HINT, 0.0, -1.0, 1.0 - ) - ); - std::shared_ptr traceMax = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + [this](int index, Point input, const std::vector& values, double sampleRate) { return input; }, new EffectParameter( "Trace max", "Defines the maximum proportion of the image that is drawn before skipping to the next frame. This has the effect of 'tracing' out the image from a single dot when animated. By default, we draw until the end of the frame, so this value is 1.0.", "traceMax", - VERSION_HINT, 1.0, 0.0, 1.0 + VERSION_HINT, 0.75, 0.0, 1.0 ) ); std::shared_ptr traceMin = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + [this](int index, Point input, const std::vector& values, double sampleRate) { return input; }, new EffectParameter( "Trace min", "Defines the proportion of the image that drawing starts from. This has the effect of 'tracing' out the image from a single dot when animated. By default, we start drawing from the beginning of the frame, so this value is 0.0.", "traceMin", - VERSION_HINT, 0.0, 0.0, 1.0 + VERSION_HINT, 0.25, 0.0, 1.0 ) ); std::shared_ptr delayEffect = std::make_shared(); + + std::shared_ptr dashedLineEffect = std::make_shared(); + std::function errorCallback = [this](int lineNum, juce::String fileName, juce::String error) { notifyErrorListeners(lineNum, fileName, error); }; - std::shared_ptr perspectiveEffect = std::make_shared(VERSION_HINT, errorCallback); + std::shared_ptr customEffect = std::make_shared(errorCallback); + + std::shared_ptr perspectiveEffect = std::make_shared(); + std::shared_ptr perspective = std::make_shared( + perspectiveEffect, + std::vector{ + new EffectParameter("3D Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0), + new EffectParameter("Focal Length", "Controls the focal length of the 3D perspective effect. A higher focal length makes the image look more flat, and a lower focal length makes the image look more 3D.", "perspectiveFocalLength", VERSION_HINT, 2.0, 0.0, 10.0), + } + ); BooleanParameter* midiEnabled = new BooleanParameter("MIDI Enabled", "midiEnabled", VERSION_HINT, false); BooleanParameter* inputEnabled = new BooleanParameter("Audio Input Enabled", "inputEnabled", VERSION_HINT, false); @@ -344,7 +263,6 @@ class OscirenderAudioProcessor : public juce::AudioProcessor, juce::AudioProces double squaredVolume = 0; double currentVolume = 0; - void updateObjValues(); std::shared_ptr getEffect(juce::String id); BooleanParameter* getBooleanParameter(juce::String id); FloatParameter* getFloatParameter(juce::String id); diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index bcabdf73..4a8c5734 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -4,25 +4,19 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { addAndMakeVisible(effects); addAndMakeVisible(main); + addAndMakeVisible(perspective); addAndMakeVisible(midiResizerBar); addAndMakeVisible(mainResizerBar); - addAndMakeVisible(effectResizerBar); addAndMakeVisible(midi); - addChildComponent(lua); - addChildComponent(obj); addChildComponent(txt); midiLayout.setItemLayout(0, -0.1, -1.0, -1.0); - midiLayout.setItemLayout(1, 7, 7, 7); - midiLayout.setItemLayout(2, CLOSED_PREF_SIZE, -0.9, CLOSED_PREF_SIZE); + midiLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); + midiLayout.setItemLayout(2, pluginEditor.CLOSED_PREF_SIZE, -0.9, pluginEditor.CLOSED_PREF_SIZE); mainLayout.setItemLayout(0, -0.1, -0.9, -0.4); - mainLayout.setItemLayout(1, 7, 7, 7); + mainLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); mainLayout.setItemLayout(2, -0.1, -0.9, -0.6); - - effectLayout.setItemLayout(0, -0.1, -1.0, -0.63); - effectLayout.setItemLayout(1, 7, 7, 7); - effectLayout.setItemLayout(2, -0.1, -0.9, -0.37); } @@ -34,47 +28,43 @@ void SettingsComponent::resized() { area.removeFromBottom(5); juce::Component dummy; + juce::Component dummy2; juce::Component* midiComponents[] = { &dummy, &midiResizerBar, &midi }; midiLayout.layOutComponents(midiComponents, 3, area.getX(), area.getY(), area.getWidth(), area.getHeight(), true, true); - juce::Component* columns[] = { &main, &mainResizerBar, &dummy }; + juce::Component* columns[] = { &dummy2, &mainResizerBar, &dummy }; mainLayout.layOutComponents(columns, 3, dummy.getX(), dummy.getY(), dummy.getWidth(), dummy.getHeight(), false, true); + auto bounds = dummy2.getBounds(); + perspective.setBounds(bounds.removeFromBottom(120)); + bounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); + main.setBounds(bounds); + juce::Component* effectSettings = nullptr; - if (lua.isVisible()) { - effectSettings = &lua; - } else if (obj.isVisible()) { - effectSettings = &obj; - } else if (txt.isVisible()) { + if (txt.isVisible()) { effectSettings = &txt; } - juce::Component* rows[] = { &effects, &effectResizerBar, effectSettings }; + auto dummyBounds = dummy.getBounds(); - // use the dummy component to work out the bounds of the rows if (effectSettings != nullptr) { - effectLayout.layOutComponents(rows, 3, dummy.getX(), dummy.getY(), dummy.getWidth(), dummy.getHeight(), true, true); - } else { - effects.setBounds(dummy.getBounds()); + effectSettings->setBounds(dummyBounds.removeFromBottom(150)); + dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); } + effects.setBounds(dummyBounds); + repaint(); } void SettingsComponent::fileUpdated(juce::String fileName) { juce::String extension = fileName.fromLastOccurrenceOf(".", true, false); - lua.setVisible(false); - obj.setVisible(false); txt.setVisible(false); if (fileName.isEmpty() || audioProcessor.objectServerRendering) { // do nothing - } else if (extension == ".lua") { - lua.setVisible(true); - } else if (extension == ".obj") { - obj.setVisible(true); - } else if (extension == ".txt") { + } if (extension == ".txt") { txt.setVisible(true); } main.updateFileLabel(); @@ -85,37 +75,23 @@ void SettingsComponent::update() { txt.update(); } -void SettingsComponent::disableMouseRotation() { - obj.disableMouseRotation(); -} - -void SettingsComponent::toggleMidiComponent() { - double minSize, maxSize, preferredSize; - midiLayout.getItemLayout(2, minSize, maxSize, preferredSize); - if (preferredSize == CLOSED_PREF_SIZE) { - midiLayout.setItemLayout(0, -0.1, -1.0, -0.7); - midiLayout.setItemLayout(2, CLOSED_PREF_SIZE, -0.9, -0.3); - } else { - midiLayout.setItemLayout(0, -0.1, -1.0, -1.0); - midiLayout.setItemLayout(2, CLOSED_PREF_SIZE, -0.9, CLOSED_PREF_SIZE); - } - - resized(); -} - void SettingsComponent::mouseMove(const juce::MouseEvent& event) { - // if mouse over midi component, change cursor to link cursor - if (midi.getBounds().removeFromTop(CLOSED_PREF_SIZE).contains(event.getPosition())) { - setMouseCursor(juce::MouseCursor::PointingHandCursor); - } else { - setMouseCursor(juce::MouseCursor::NormalCursor); + for (int i = 0; i < 2; i++) { + if (toggleComponents[i]->getBounds().removeFromTop(pluginEditor.CLOSED_PREF_SIZE).contains(event.getPosition())) { + setMouseCursor(juce::MouseCursor::PointingHandCursor); + return; + } } + setMouseCursor(juce::MouseCursor::NormalCursor); } void SettingsComponent::mouseDown(const juce::MouseEvent& event) { - // if mouse over midi component, toggle midi component - if (midi.getBounds().removeFromTop(CLOSED_PREF_SIZE).contains(event.getPosition())) { - toggleMidiComponent(); + for (int i = 0; i < 1; i++) { + if (toggleComponents[i]->getBounds().removeFromTop(pluginEditor.CLOSED_PREF_SIZE).contains(event.getPosition())) { + pluginEditor.toggleLayout(*toggleLayouts[i], prefSizes[i]); + resized(); + return; + } } } @@ -125,12 +101,9 @@ void SettingsComponent::paint(juce::Graphics& g) { dc.drawForRectangle(g, main.getBounds()); dc.drawForRectangle(g, effects.getBounds()); dc.drawForRectangle(g, midi.getBounds()); + dc.drawForRectangle(g, perspective.getBounds()); - if (lua.isVisible()) { - dc.drawForRectangle(g, lua.getBounds()); - } else if (obj.isVisible()) { - dc.drawForRectangle(g, obj.getBounds()); - } else if (txt.isVisible()) { + if (txt.isVisible()) { dc.drawForRectangle(g, txt.getBounds()); } } diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h index 37caa04f..7d5b5fbf 100644 --- a/Source/SettingsComponent.h +++ b/Source/SettingsComponent.h @@ -4,7 +4,7 @@ #include "PluginProcessor.h" #include "MainComponent.h" #include "LuaComponent.h" -#include "ObjComponent.h" +#include "PerspectiveComponent.h" #include "TxtComponent.h" #include "EffectsComponent.h" #include "MidiComponent.h" @@ -17,8 +17,6 @@ class SettingsComponent : public juce::Component { void resized() override; void fileUpdated(juce::String fileName); void update(); - void disableMouseRotation(); - void toggleMidiComponent(); void mouseMove(const juce::MouseEvent& event) override; void mouseDown(const juce::MouseEvent& event) override; void paint(juce::Graphics& g) override; @@ -28,20 +26,19 @@ class SettingsComponent : public juce::Component { OscirenderAudioProcessorEditor& pluginEditor; MainComponent main{audioProcessor, pluginEditor}; - LuaComponent lua{audioProcessor, pluginEditor}; - ObjComponent obj{audioProcessor, pluginEditor}; + PerspectiveComponent perspective{audioProcessor, pluginEditor}; TxtComponent txt{audioProcessor, pluginEditor}; EffectsComponent effects{audioProcessor, pluginEditor}; MidiComponent midi{audioProcessor, pluginEditor}; - const double CLOSED_PREF_SIZE = 30.0; - juce::StretchableLayoutManager midiLayout; juce::StretchableLayoutResizerBar midiResizerBar{&midiLayout, 1, false}; juce::StretchableLayoutManager mainLayout; juce::StretchableLayoutResizerBar mainResizerBar{&mainLayout, 1, true}; - juce::StretchableLayoutManager effectLayout; - juce::StretchableLayoutResizerBar effectResizerBar{&effectLayout, 1, false}; + + juce::Component* toggleComponents[1] = { &midi }; + juce::StretchableLayoutManager* toggleLayouts[1] = { &midiLayout }; + double prefSizes[1] = { 300 }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent) }; \ No newline at end of file diff --git a/Source/TestMain.cpp b/Source/TestMain.cpp new file mode 100644 index 00000000..14240777 --- /dev/null +++ b/Source/TestMain.cpp @@ -0,0 +1,133 @@ +#include +#include "obj/Camera.h" +#include "mathter/Common/Approx.hpp" + +class FrustumTest : public juce::UnitTest { +public: + FrustumTest() : juce::UnitTest("Frustum Culling") {} + + void runTest() override { + double focalLength = 1; + + Camera camera; + camera.setFocalLength(focalLength); + Vec3 position = Vec3(0, 0, -focalLength); + camera.setPosition(position); + Frustum frustum = camera.getFrustum(); + + beginTest("Focal Plane Frustum In-Bounds"); + + // Focal plane is at z = 0 + Vec3 vecs[] = { + Vec3(0, 0, 0), Vec3(0, 0, 0), + Vec3(1, 1, 0), Vec3(1, 1, 0), + Vec3(-1, -1, 0), Vec3(-1, -1, 0), + Vec3(1, -1, 0), Vec3(1, -1, 0), + Vec3(-1, 1, 0), Vec3(-1, 1, 0), + Vec3(0.5, 0.5, 0), Vec3(0.5, 0.5, 0), + }; + + testFrustumClippedEqualsExpected(vecs, camera, 6); + + beginTest("Focal Plane Frustum Out-Of-Bounds"); + + // Focal plane is at z = 0 + Vec3 vecs2[] = { + Vec3(1.1, 1.1, 0), Vec3(1, 1, 0), + Vec3(-1.1, -1.1, 0), Vec3(-1, -1, 0), + Vec3(1.1, -1.1, 0), Vec3(1, -1, 0), + Vec3(-1.1, 1.1, 0), Vec3(-1, 1, 0), + Vec3(1.1, 0.5, 0), Vec3(1, 0.5, 0), + Vec3(-1.1, 0.5, 0), Vec3(-1, 0.5, 0), + Vec3(0.5, -1.1, 0), Vec3(0.5, -1, 0), + Vec3(0.5, 1.1, 0), Vec3(0.5, 1, 0), + Vec3(10, 10, 0), Vec3(1, 1, 0), + Vec3(-10, -10, 0), Vec3(-1, -1, 0), + Vec3(10, -10, 0), Vec3(1, -1, 0), + Vec3(-10, 10, 0), Vec3(-1, 1, 0), + }; + + testFrustumClippedEqualsExpected(vecs2, camera, 12); + + beginTest("Behind Camera Out-Of-Bounds"); + + double minZWorldCoords = -focalLength + camera.getFrustum().nearDistance; + + Vec3 vecs3[] = { + Vec3(0, 0, -focalLength), Vec3(0, 0, minZWorldCoords), + Vec3(0, 0, -100), Vec3(0, 0, minZWorldCoords), + Vec3(0.5, 0.5, -focalLength), Vec3(0.1, 0.1, minZWorldCoords), + Vec3(10, -10, -focalLength), Vec3(0.1, -0.1, minZWorldCoords), + Vec3(-0.5, 0.5, -100), Vec3(-0.1, 0.1, minZWorldCoords), + Vec3(-10, 10, -100), Vec3(-0.1, 0.1, minZWorldCoords), + }; + + testFrustumClippedEqualsExpected(vecs3, camera, 6); + + beginTest("3D Point Out-Of-Bounds"); + + Vec3 vecs4[] = { + Vec3(1, 1, -0.1), + Vec3(-1, -1, -0.1), + Vec3(1, -1, -0.1), + Vec3(-1, 1, -0.1), + Vec3(0.5, 0.5, minZWorldCoords), + }; + + testFrustumClipOccurs(vecs4, camera, 5); + } + + Vec3 project(Vec3& p, double focalLength) { + return Vec3( + p.x * focalLength / p.z, + p.y * focalLength / p.z, + 0 + ); + } + + juce::String vec3ToString(Vec3& p) { + return "(" + juce::String(p.x) + ", " + juce::String(p.y) + ", " + juce::String(p.z) + ")"; + } + + juce::String errorMessage(Vec3& actual, Vec3& expected) { + return "Expected: " + vec3ToString(expected) + ", Actual: " + vec3ToString(actual); + } + + void testFrustumClippedEqualsExpected(Vec3 vecs[], Camera& camera, int length) { + for (int i = 0; i < length; i++) { + Vec3 p = vecs[2 * i]; + p = camera.toCameraSpace(p); + camera.getFrustum().clipToFrustum(p); + p = camera.toWorldSpace(p); + expect(mathter::AlmostEqual(p, vecs[2 * i + 1]), errorMessage(p, vecs[2 * i + 1])); + } + } + + void testFrustumClipOccurs(Vec3 vecs[], Camera& camera, int length) { + for (int i = 0; i < length; i++) { + Vec3 p = vecs[i]; + p = camera.toCameraSpace(p); + camera.getFrustum().clipToFrustum(p); + p = camera.toWorldSpace(p); + expect(!mathter::AlmostEqual(p, vecs[i]), errorMessage(p, vecs[i])); + } + } +}; + +static FrustumTest frustumTest; + +int main(int argc, char* argv[]) { + juce::UnitTestRunner runner; + runner.setAssertOnFailure(false); + + runner.runAllTests(); + + for (int i = 0; i < runner.getNumResults(); ++i) { + const juce::UnitTestRunner::TestResult* result = runner.getResult(i); + if (result->failures > 0) { + return 1; + } + } + + return 0; +} diff --git a/Source/TxtComponent.cpp b/Source/TxtComponent.cpp index b6df2093..bd4eeae3 100644 --- a/Source/TxtComponent.cpp +++ b/Source/TxtComponent.cpp @@ -2,7 +2,7 @@ #include "PluginEditor.h" TxtComponent::TxtComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { - setText(".txt File Settings"); + setText("Text Settings"); addAndMakeVisible(font); addAndMakeVisible(bold); diff --git a/Source/audio/BitCrushEffect.cpp b/Source/audio/BitCrushEffect.cpp index 3f08ab1a..8336a859 100644 --- a/Source/audio/BitCrushEffect.cpp +++ b/Source/audio/BitCrushEffect.cpp @@ -3,7 +3,7 @@ BitCrushEffect::BitCrushEffect() {} // algorithm from https://www.kvraudio.com/forum/viewtopic.php?t=163880 -Vector2 BitCrushEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point BitCrushEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { double value = values[0]; // change rage of value from 0-1 to 0.0-0.78 double rangedValue = value * 0.78; @@ -12,5 +12,5 @@ Vector2 BitCrushEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; }; diff --git a/Source/audio/BooleanParameter.h b/Source/audio/BooleanParameter.h index b2cd44e3..9bad39a3 100644 --- a/Source/audio/BooleanParameter.h +++ b/Source/audio/BooleanParameter.h @@ -1,5 +1,5 @@ #pragma once -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include class BooleanParameter : public juce::AudioProcessorParameterWithID { diff --git a/Source/audio/BulgeEffect.cpp b/Source/audio/BulgeEffect.cpp index af2e649f..965fc42a 100644 --- a/Source/audio/BulgeEffect.cpp +++ b/Source/audio/BulgeEffect.cpp @@ -4,13 +4,13 @@ BulgeEffect::BulgeEffect() {} BulgeEffect::~BulgeEffect() {} -Vector2 BulgeEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point BulgeEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { double value = values[0]; double translatedBulge = -value + 1; - double r = input.magnitude(); + double r = sqrt(pow(input.x, 2) + pow(input.y, 2)); double theta = atan2(input.y, input.x); double rn = pow(r, translatedBulge); - return Vector2(rn * cos(theta), rn * sin(theta)); + return Point(rn * cos(theta), rn * sin(theta), input.z); } diff --git a/Source/audio/BulgeEffect.h b/Source/audio/BulgeEffect.h index c9803d0d..2bdd86b0 100644 --- a/Source/audio/BulgeEffect.h +++ b/Source/audio/BulgeEffect.h @@ -1,11 +1,11 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" class BulgeEffect : public EffectApplication { public: BulgeEffect(); ~BulgeEffect(); - Vector2 apply(int index, Vector2 input, const std::vector&, double sampleRate) override; + Point apply(int index, Point input, const std::vector&, double sampleRate) override; }; \ No newline at end of file diff --git a/Source/audio/CustomEffect.cpp b/Source/audio/CustomEffect.cpp new file mode 100644 index 00000000..b2a49f5d --- /dev/null +++ b/Source/audio/CustomEffect.cpp @@ -0,0 +1,68 @@ +#include "CustomEffect.h" +#include +#include "../MathUtil.h" + +const juce::String CustomEffect::FILE_NAME = "6a3580b0-c5fc-4b28-a33e-e26a487f052f"; + +CustomEffect::CustomEffect(std::function errorCallback) : errorCallback(errorCallback) {} + +CustomEffect::~CustomEffect() { + parser->close(L); +} + +Point CustomEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { + auto effectScale = values[0]; + + auto x = input.x; + auto y = input.y; + auto z = input.z; + + { + juce::SpinLock::ScopedLockType lock(codeLock); + if (!defaultScript) { + parser->setVariable("x", x); + parser->setVariable("y", y); + parser->setVariable("z", z); + + auto result = parser->run(L, LuaVariables{sampleRate, frequency}, step, phase); + if (result.size() >= 2) { + x = result[0]; + y = result[1]; + if (result.size() >= 3) { + z = result[2]; + } + } + } else { + parser->resetErrors(); + } + } + + step++; + phase += 2 * std::numbers::pi * frequency / sampleRate; + phase = MathUtil::wrapAngle(phase); + + return Point( + (1 - effectScale) * input.x + effectScale * x, + (1 - effectScale) * input.y + effectScale * y, + (1 - effectScale) * input.z + effectScale * z + ); +} + +void CustomEffect::updateCode(const juce::String& newCode) { + juce::SpinLock::ScopedLockType lock(codeLock); + defaultScript = newCode == DEFAULT_SCRIPT; + code = newCode; + parser = std::make_unique(FILE_NAME, code, errorCallback); +} + +void CustomEffect::setVariable(juce::String variableName, double value) { + juce::SpinLock::ScopedLockType lock(codeLock); + if (!defaultScript) { + parser->setVariable(variableName, value); + } +} + +juce::String CustomEffect::getCode() { + juce::SpinLock::ScopedLockType lock(codeLock); + return code; +} diff --git a/Source/audio/CustomEffect.h b/Source/audio/CustomEffect.h new file mode 100644 index 00000000..65ac12e1 --- /dev/null +++ b/Source/audio/CustomEffect.h @@ -0,0 +1,36 @@ +#pragma once +#include "EffectApplication.h" +#include "../shape/Point.h" +#include "../audio/Effect.h" +#include "../lua/LuaParser.h" + +class CustomEffect : public EffectApplication { +public: + CustomEffect(std::function errorCallback); + ~CustomEffect(); + + // arbitrary UUID + static const juce::String FILE_NAME; + + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; + void updateCode(const juce::String& newCode); + void setVariable(juce::String variableName, double value); + + juce::String getCode(); + + double frequency = 0; + +private: + const juce::String DEFAULT_SCRIPT = "return { x, y, z }"; + juce::String code = DEFAULT_SCRIPT; + std::function errorCallback; + std::unique_ptr parser = std::make_unique(FILE_NAME, code, errorCallback); + juce::SpinLock codeLock; + + bool defaultScript = true; + + lua_State *L = nullptr; + + long step = 1; + double phase = 0; +}; diff --git a/Source/audio/DashedLineEffect.cpp b/Source/audio/DashedLineEffect.cpp new file mode 100644 index 00000000..ac586a2b --- /dev/null +++ b/Source/audio/DashedLineEffect.cpp @@ -0,0 +1,28 @@ +#include "DashedLineEffect.h" + +DashedLineEffect::DashedLineEffect() {} + +DashedLineEffect::~DashedLineEffect() {} + +Point DashedLineEffect::apply(int index, Point vector, const std::vector& values, double sampleRate) { + // dash length in seconds + double dashLength = values[0] / 400; + int dashLengthSamples = (int)(dashLength * sampleRate); + dashLengthSamples = juce::jmin(dashLengthSamples, MAX_BUFFER); + + if (dashIndex >= dashLengthSamples) { + dashIndex = 0; + bufferIndex = 0; + } + + buffer[bufferIndex] = vector; + bufferIndex++; + + vector = buffer[dashIndex]; + + if (index % 2 == 0) { + dashIndex++; + } + + return vector; +} diff --git a/Source/audio/DashedLineEffect.h b/Source/audio/DashedLineEffect.h new file mode 100644 index 00000000..c6351e1d --- /dev/null +++ b/Source/audio/DashedLineEffect.h @@ -0,0 +1,17 @@ +#pragma once +#include "EffectApplication.h" +#include "../shape/Point.h" + +class DashedLineEffect : public EffectApplication { +public: + DashedLineEffect(); + ~DashedLineEffect(); + + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; + +private: + const static int MAX_BUFFER = 192000; + std::vector buffer = std::vector(MAX_BUFFER); + int dashIndex = 0; + int bufferIndex = 0; +}; \ No newline at end of file diff --git a/Source/audio/DelayEffect.cpp b/Source/audio/DelayEffect.cpp index ea77eb61..cdd16342 100644 --- a/Source/audio/DelayEffect.cpp +++ b/Source/audio/DelayEffect.cpp @@ -4,7 +4,7 @@ DelayEffect::DelayEffect() {} DelayEffect::~DelayEffect() {} -Vector2 DelayEffect::apply(int index, Vector2 vector, const std::vector& values, double sampleRate) { +Point DelayEffect::apply(int index, Point vector, const std::vector& values, double sampleRate) { double decay = values[0]; double decayLength = values[1]; int delayBufferLength = (int)(sampleRate * decayLength); @@ -22,10 +22,11 @@ Vector2 DelayEffect::apply(int index, Vector2 vector, const std::vector& } } - Vector2 echo = delayBuffer[position]; - vector = Vector2( + Point echo = delayBuffer[position]; + vector = Point( vector.x + echo.x * decay, - vector.y + echo.y * decay + vector.y + echo.y * decay, + vector.z + echo.z * decay ); delayBuffer[head] = vector; diff --git a/Source/audio/DelayEffect.h b/Source/audio/DelayEffect.h index e8921d45..5c77726e 100644 --- a/Source/audio/DelayEffect.h +++ b/Source/audio/DelayEffect.h @@ -1,17 +1,17 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" class DelayEffect : public EffectApplication { public: DelayEffect(); ~DelayEffect(); - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: const static int MAX_DELAY = 192000 * 10; - std::vector delayBuffer = std::vector(MAX_DELAY); + std::vector delayBuffer = std::vector(MAX_DELAY); int head = 0; int position = 0; int samplesSinceLastDelay = 0; diff --git a/Source/audio/DistortEffect.cpp b/Source/audio/DistortEffect.cpp index 38e48be0..8a0f9c74 100644 --- a/Source/audio/DistortEffect.cpp +++ b/Source/audio/DistortEffect.cpp @@ -4,13 +4,13 @@ DistortEffect::DistortEffect(bool vertical) : vertical(vertical) {} DistortEffect::~DistortEffect() {} -Vector2 DistortEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point DistortEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { double value = values[0]; int vertical = (int)this->vertical; if (index % 2 == 0) { - input.translate((1 - vertical) * value, vertical * value); + input.translate((1 - vertical) * value, vertical * value, 0); } else { - input.translate((1 - vertical) * -value, vertical * -value); + input.translate((1 - vertical) * -value, vertical * -value, 0); } return input; } diff --git a/Source/audio/DistortEffect.h b/Source/audio/DistortEffect.h index f92e6580..edcbd4dd 100644 --- a/Source/audio/DistortEffect.h +++ b/Source/audio/DistortEffect.h @@ -1,13 +1,13 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" class DistortEffect : public EffectApplication { public: DistortEffect(bool vertical); ~DistortEffect(); - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: bool vertical; }; \ No newline at end of file diff --git a/Source/audio/Effect.cpp b/Source/audio/Effect.cpp index d8f7d2b3..e9e1a20b 100644 --- a/Source/audio/Effect.cpp +++ b/Source/audio/Effect.cpp @@ -17,7 +17,7 @@ Effect::Effect(EffectApplicationType application, const std::vector{parameter}) {} -Vector2 Effect::apply(int index, Vector2 input, double volume) { +Point Effect::apply(int index, Point input, double volume) { animateValues(volume); if (application) { return application(index, input, actualValues, sampleRate); @@ -90,7 +90,7 @@ float Effect::nextPhase(EffectParameter* parameter) { } void Effect::apply() { - apply(0, Vector2()); + apply(0, Point()); } double Effect::getValue(int index) { diff --git a/Source/audio/Effect.h b/Source/audio/Effect.h index 0b20b5da..1108961b 100644 --- a/Source/audio/Effect.h +++ b/Source/audio/Effect.h @@ -1,11 +1,11 @@ #pragma once -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include #include "EffectApplication.h" #include "EffectParameter.h" #include "BooleanParameter.h" -typedef std::function& values, double sampleRate)> EffectApplicationType; +typedef std::function& values, double sampleRate)> EffectApplicationType; class Effect { public: @@ -14,7 +14,7 @@ class Effect { Effect(EffectApplicationType application, const std::vector& parameters); Effect(EffectApplicationType application, EffectParameter* parameter); - Vector2 apply(int index, Vector2 input, double volume = 0.0); + Point apply(int index, Point input, double volume = 0.0); void apply(); double getValue(int index); diff --git a/Source/audio/EffectApplication.h b/Source/audio/EffectApplication.h index 7573e48b..ac944ca6 100644 --- a/Source/audio/EffectApplication.h +++ b/Source/audio/EffectApplication.h @@ -1,12 +1,12 @@ #pragma once -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include class EffectApplication { public: EffectApplication() {}; - virtual Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) = 0; + virtual Point apply(int index, Point input, const std::vector& values, double sampleRate) = 0; void resetPhase(); double nextPhase(double frequency, double sampleRate); diff --git a/Source/audio/EffectParameter.h b/Source/audio/EffectParameter.h index 88fc2aaa..254ba4ea 100644 --- a/Source/audio/EffectParameter.h +++ b/Source/audio/EffectParameter.h @@ -1,5 +1,5 @@ #pragma once -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include #include "BooleanParameter.h" @@ -328,7 +328,7 @@ class EffectParameter : public FloatParameter { public: std::atomic smoothValueChange = true; LfoTypeParameter* lfo = new LfoTypeParameter(name + " LFO", paramID + "Lfo", getVersionHint(), 1); - FloatParameter* lfoRate = new FloatParameter(name + " LFO Rate", paramID + "LfoRate", getVersionHint(), 1.0f, 0.0f, 100.0f, 0.1f, "Hz"); + FloatParameter* lfoRate = new FloatParameter(name + " LFO Rate", paramID + "LfoRate", getVersionHint(), 1.0f, 0.0f, 1000.0f, 0.1f, "Hz"); BooleanParameter* sidechain = new BooleanParameter(name + " Sidechain Enabled", paramID + "Sidechain", getVersionHint(), false); std::atomic phase = 0.0f; juce::String description; diff --git a/Source/audio/LuaEffect.cpp b/Source/audio/LuaEffect.cpp index c62691a0..792f3271 100644 --- a/Source/audio/LuaEffect.cpp +++ b/Source/audio/LuaEffect.cpp @@ -1,7 +1,7 @@ #include "LuaEffect.h" #include "../lua/LuaParser.h" -Vector2 LuaEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point LuaEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { int fileIndex = audioProcessor.getCurrentFileIndex(); if (fileIndex == -1) { return input; @@ -11,7 +11,7 @@ Vector2 LuaEffect::apply(int index, Vector2 input, const std::vector& va parser->setVariable("slider_" + name.toLowerCase(), values[0]); } - audioProcessor.perspectiveEffect->setVariable("slider_" + name.toLowerCase(), values[0]); + audioProcessor.customEffect->setVariable("slider_" + name.toLowerCase(), values[0]); return input; } diff --git a/Source/audio/LuaEffect.h b/Source/audio/LuaEffect.h index ae5e4808..86bb221d 100644 --- a/Source/audio/LuaEffect.h +++ b/Source/audio/LuaEffect.h @@ -1,6 +1,6 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include "../audio/Effect.h" #include "../PluginProcessor.h" @@ -8,7 +8,7 @@ class LuaEffect : public EffectApplication { public: LuaEffect(juce::String name, OscirenderAudioProcessor& p) : audioProcessor(p), name(name) {}; - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: OscirenderAudioProcessor& audioProcessor; juce::String name; diff --git a/Source/audio/PerspectiveEffect.cpp b/Source/audio/PerspectiveEffect.cpp index bc6f1cf0..32ed99ec 100644 --- a/Source/audio/PerspectiveEffect.cpp +++ b/Source/audio/PerspectiveEffect.cpp @@ -1,114 +1,26 @@ #include "PerspectiveEffect.h" #include #include "../MathUtil.h" +#include "../obj/Camera.h" -const juce::String PerspectiveEffect::FILE_NAME = "6a3580b0-c5fc-4b28-a33e-e26a487f052f"; +PerspectiveEffect::PerspectiveEffect() {} -PerspectiveEffect::PerspectiveEffect(int versionHint, std::function errorCallback) : versionHint(versionHint), errorCallback(errorCallback) { - fixedRotateX = new BooleanParameter("Perspective Fixed Rotate X", "perspectiveFixedRotateX", versionHint, false); - fixedRotateY = new BooleanParameter("Perspective Fixed Rotate Y", "perspectiveFixedRotateY", versionHint, false); - fixedRotateZ = new BooleanParameter("Perspective Fixed Rotate Z", "perspectiveFixedRotateZ", versionHint, false); -} - -PerspectiveEffect::~PerspectiveEffect() { - parser->close(L); -} +PerspectiveEffect::~PerspectiveEffect() {} -Vector2 PerspectiveEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point PerspectiveEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { auto effectScale = values[0]; - auto depth = 1.0 + (values[1] - 0.1) * 3; - auto rotateSpeed = linearSpeedToActualSpeed(values[2]); - double baseRotateX, baseRotateY, baseRotateZ; - if (fixedRotateX->getBoolValue()) { - baseRotateX = 0; - currentRotateX = values[3] * std::numbers::pi; - } else { - baseRotateX = values[3] * std::numbers::pi; - } - if (fixedRotateY->getBoolValue()) { - baseRotateY = 0; - currentRotateY = values[4] * std::numbers::pi; - } else { - baseRotateY = values[4] * std::numbers::pi; - } - if (fixedRotateZ->getBoolValue()) { - baseRotateZ = 0; - currentRotateZ = values[5] * std::numbers::pi; - } else { - baseRotateZ = values[5] * std::numbers::pi; - } - - currentRotateX = MathUtil::wrapAngle(currentRotateX + baseRotateX * rotateSpeed); - currentRotateY = MathUtil::wrapAngle(currentRotateY + baseRotateY * rotateSpeed); - currentRotateZ = MathUtil::wrapAngle(currentRotateZ + baseRotateZ * rotateSpeed); - - auto x = input.x; - auto y = input.y; - auto z = 0.0; - - { - juce::SpinLock::ScopedLockType lock(codeLock); - if (!defaultScript) { - parser->setVariable("x", x); - parser->setVariable("y", y); - parser->setVariable("z", z); - - auto result = parser->run(L, LuaVariables{sampleRate, 0}, step, phase); - if (result.size() >= 3) { - x = result[0]; - y = result[1]; - z = result[2]; - } - } else { - parser->resetErrors(); - } - } - - auto rotateX = baseRotateX + currentRotateX; - auto rotateY = baseRotateY + currentRotateY; - auto rotateZ = baseRotateZ + currentRotateZ; + auto focalLength = juce::jmax(values[1], 0.001); - // rotate around x-axis - double cosValue = std::cos(rotateX); - double sinValue = std::sin(rotateX); - double y2 = cosValue * y - sinValue * z; - double z2 = sinValue * y + cosValue * z; + Vec3 origin = Vec3(0, 0, -focalLength); + camera.setPosition(origin); + camera.setFocalLength(focalLength); + Vec3 vec = Vec3(input.x, input.y, input.z); - // rotate around y-axis - cosValue = std::cos(rotateY); - sinValue = std::sin(rotateY); - double x2 = cosValue * x + sinValue * z2; - double z3 = -sinValue * x + cosValue * z2; + Vec3 projected = camera.project(vec); - // rotate around z-axis - cosValue = cos(rotateZ); - sinValue = sin(rotateZ); - double x3 = cosValue * x2 - sinValue * y2; - double y3 = sinValue * x2 + cosValue * y2; - - // perspective projection - auto focalLength = 1.0; - return Vector2( - (1 - effectScale) * input.x + effectScale * (x3 * focalLength / (z3 - depth)), - (1 - effectScale) * input.y + effectScale * (y3 * focalLength / (z3 - depth)) + return Point( + (1 - effectScale) * input.x + effectScale * projected.x, + (1 - effectScale) * input.y + effectScale * projected.y, + 0 ); } - -void PerspectiveEffect::updateCode(const juce::String& newCode) { - juce::SpinLock::ScopedLockType lock(codeLock); - defaultScript = newCode == DEFAULT_SCRIPT; - code = newCode; - parser = std::make_unique(FILE_NAME, code, errorCallback); -} - -void PerspectiveEffect::setVariable(juce::String variableName, double value) { - juce::SpinLock::ScopedLockType lock(codeLock); - if (!defaultScript) { - parser->setVariable(variableName, value); - } -} - -juce::String PerspectiveEffect::getCode() { - juce::SpinLock::ScopedLockType lock(codeLock); - return code; -} diff --git a/Source/audio/PerspectiveEffect.h b/Source/audio/PerspectiveEffect.h index 05a57cfb..97ce0c52 100644 --- a/Source/audio/PerspectiveEffect.h +++ b/Source/audio/PerspectiveEffect.h @@ -1,52 +1,17 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include "../audio/Effect.h" -#include "../lua/LuaParser.h" +#include "../obj/Camera.h" class PerspectiveEffect : public EffectApplication { public: - PerspectiveEffect(int versionHint, std::function errorCallback); + PerspectiveEffect(); ~PerspectiveEffect(); - // arbitrary UUID - static const juce::String FILE_NAME; - - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; - void updateCode(const juce::String& newCode); - void setVariable(juce::String variableName, double value); - - juce::String getCode(); - - BooleanParameter* fixedRotateX; - BooleanParameter* fixedRotateY; - BooleanParameter* fixedRotateZ; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: - const juce::String DEFAULT_SCRIPT = "return { x, y, z }"; - juce::String code = DEFAULT_SCRIPT; - std::function errorCallback; - std::unique_ptr parser = std::make_unique(FILE_NAME, code, errorCallback); - juce::SpinLock codeLock; - - bool defaultScript = true; - - float currentRotateX = 0; - float currentRotateY = 0; - float currentRotateZ = 0; - - lua_State *L = nullptr; - - int versionHint; - - long step = 1; - double phase = 0; - double linearSpeedToActualSpeed(double rotateSpeed) { - double actualSpeed = (std::exp(3 * juce::jmin(10.0, std::abs(rotateSpeed))) - 1) / 50000; - if (rotateSpeed < 0) { - actualSpeed *= -1; - } - return actualSpeed; - } + Camera camera; }; diff --git a/Source/audio/RotateEffect.cpp b/Source/audio/RotateEffect.cpp deleted file mode 100644 index 876c1c84..00000000 --- a/Source/audio/RotateEffect.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "RotateEffect.h" - -RotateEffect::RotateEffect() {} - -RotateEffect::~RotateEffect() {} - -Vector2 RotateEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { - input.rotate(nextPhase(values[0], sampleRate)); - return input; -} diff --git a/Source/audio/RotateEffect.h b/Source/audio/RotateEffect.h deleted file mode 100644 index 282e64e7..00000000 --- a/Source/audio/RotateEffect.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include "EffectApplication.h" -#include "../shape/Vector2.h" - -class RotateEffect : public EffectApplication { -public: - RotateEffect(); - ~RotateEffect(); - - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; -}; \ No newline at end of file diff --git a/Source/audio/ShapeVoice.cpp b/Source/audio/ShapeVoice.cpp index 6a895719..a63555cc 100644 --- a/Source/audio/ShapeVoice.cpp +++ b/Source/audio/ShapeVoice.cpp @@ -63,6 +63,10 @@ void ShapeVoice::incrementShapeDrawing() { } } +double ShapeVoice::getFrequency() { + return actualFrequency; +} + // should be called if the current file is changed so that we interrupt // any currently playing sounds / voices void ShapeVoice::updateSound(juce::SynthesiserSound* sound) { @@ -76,9 +80,9 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star int numChannels = outputBuffer.getNumChannels(); - float actualFrequency = frequency * pitchWheelAdjustment; - - if (!audioProcessor.midiEnabled->getBoolValue()) { + if (audioProcessor.midiEnabled->getBoolValue()) { + actualFrequency = frequency * pitchWheelAdjustment; + } else { actualFrequency = audioProcessor.frequency; } @@ -92,9 +96,10 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star double proportionalLength = (traceMax - traceMin) * frameLength; lengthIncrement = juce::jmax(proportionalLength / (audioProcessor.currentSampleRate / actualFrequency), MIN_LENGTH_INCREMENT); - Vector2 channels; + Point channels; double x = 0.0; double y = 0.0; + double z = 0.0; bool renderingSample = true; @@ -114,6 +119,7 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star x = channels.x; y = channels.y; + z = channels.z; time += 1.0 / audioProcessor.currentSampleRate; @@ -127,7 +133,11 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star double gain = audioProcessor.midiEnabled->getBoolValue() ? adsr.lookup(time) : 1.0; gain *= velocity; - if (numChannels >= 2) { + if (numChannels >= 3) { + outputBuffer.addSample(0, sample, x * gain); + outputBuffer.addSample(1, sample, y * gain); + outputBuffer.addSample(2, sample, z * gain); + } else if (numChannels == 2) { outputBuffer.addSample(0, sample, x * gain); outputBuffer.addSample(1, sample, y * gain); } else if (numChannels == 1) { @@ -136,7 +146,9 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star double traceMinValue = audioProcessor.traceMin->getActualValue(); double traceMaxValue = audioProcessor.traceMax->getActualValue(); - actualTraceMax = juce::jmax(actualTraceMin + MIN_TRACE, juce::jmin(traceMaxValue, 1.0)); + traceMaxValue = traceMaxEnabled ? traceMaxValue : 1.0; + traceMinValue = traceMinEnabled ? traceMinValue : 0.0; + actualTraceMax = juce::jmax(actualTraceMin, juce::jmin(traceMaxValue, 1.0)); actualTraceMin = juce::jmax(MIN_TRACE, juce::jmin(traceMinValue, actualTraceMax - MIN_TRACE)); if (!renderingSample) { @@ -152,6 +164,10 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star shapeDrawn = 0.0; currentShape = 0; } + frameDrawn = 0.0; + shapeDrawn = 0.0; + currentShape = 0; + // TODO: updateFrame already iterates over all the shapes, // so we can improve performance by calculating frameDrawn // and shapeDrawn directly. frameDrawn is simply actualTraceMin * frameLength diff --git a/Source/audio/ShapeVoice.h b/Source/audio/ShapeVoice.h index 6412673d..9f4880b9 100644 --- a/Source/audio/ShapeVoice.h +++ b/Source/audio/ShapeVoice.h @@ -18,6 +18,7 @@ class ShapeVoice : public juce::SynthesiserVoice { void incrementShapeDrawing(); + double getFrequency(); private: const double MIN_TRACE = 0.005; @@ -37,6 +38,7 @@ class ShapeVoice : public juce::SynthesiserVoice { bool currentlyPlaying = false; double frequency = 1.0; + std::atomic actualFrequency = 1.0; double velocity = 1.0; double pitchWheelAdjustment = 1.0; diff --git a/Source/audio/SmoothEffect.cpp b/Source/audio/SmoothEffect.cpp index c3ad0006..066998a5 100644 --- a/Source/audio/SmoothEffect.cpp +++ b/Source/audio/SmoothEffect.cpp @@ -4,13 +4,13 @@ SmoothEffect::SmoothEffect() {} SmoothEffect::~SmoothEffect() {} -Vector2 SmoothEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { - double weight = values[0]; +Point SmoothEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { + double weight = juce::jmax(values[0], 0.00001); weight *= 0.95; double strength = 10; weight = std::log(strength * weight + 1) / std::log(strength + 1); // TODO: This doesn't consider the sample rate! - leftAvg = weight * leftAvg + (1 - weight) * input.x; - rightAvg = weight * rightAvg + (1 - weight) * input.y; - return Vector2(leftAvg, rightAvg); + avg = weight * avg + (1 - weight) * input; + + return avg; } diff --git a/Source/audio/SmoothEffect.h b/Source/audio/SmoothEffect.h index 9992d354..7067b651 100644 --- a/Source/audio/SmoothEffect.h +++ b/Source/audio/SmoothEffect.h @@ -1,14 +1,13 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" class SmoothEffect : public EffectApplication { public: SmoothEffect(); ~SmoothEffect(); - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: - double leftAvg = 0; - double rightAvg = 0; + Point avg; }; \ No newline at end of file diff --git a/Source/audio/VectorCancellingEffect.cpp b/Source/audio/VectorCancellingEffect.cpp index 561e80b8..ead3ad09 100644 --- a/Source/audio/VectorCancellingEffect.cpp +++ b/Source/audio/VectorCancellingEffect.cpp @@ -4,7 +4,7 @@ VectorCancellingEffect::VectorCancellingEffect() {} VectorCancellingEffect::~VectorCancellingEffect() {} -Vector2 VectorCancellingEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point VectorCancellingEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { double value = values[0]; if (value < 0.001) { return input; @@ -17,7 +17,7 @@ Vector2 VectorCancellingEffect::apply(int index, Vector2 input, const std::vecto if (index >= nextInvert) { nextInvert += frequency; } else { - input.scale(-1, -1); + input.scale(-1, -1, 1); } return input; } diff --git a/Source/audio/VectorCancellingEffect.h b/Source/audio/VectorCancellingEffect.h index 6c94161d..38aac064 100644 --- a/Source/audio/VectorCancellingEffect.h +++ b/Source/audio/VectorCancellingEffect.h @@ -1,13 +1,13 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" class VectorCancellingEffect : public EffectApplication { public: VectorCancellingEffect(); ~VectorCancellingEffect(); - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: int lastIndex = 0; double nextInvert = 0; diff --git a/Source/audio/WobbleEffect.cpp b/Source/audio/WobbleEffect.cpp index 938a1264..f7619d6c 100644 --- a/Source/audio/WobbleEffect.cpp +++ b/Source/audio/WobbleEffect.cpp @@ -4,13 +4,11 @@ WobbleEffect::WobbleEffect(PitchDetector& pitchDetector) : pitchDetector(pitchDe WobbleEffect::~WobbleEffect() {} -Vector2 WobbleEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { +Point WobbleEffect::apply(int index, Point input, const std::vector& values, double sampleRate) { // TODO: this doesn't consider sample rate smoothedFrequency = smoothedFrequency * 0.99995 + pitchDetector.frequency * 0.00005; double theta = nextPhase(smoothedFrequency, sampleRate); - double delta = values[0] * std::sin(theta); - double x = input.x + delta; - double y = input.y + delta; + double delta = 0.5 * values[0] * std::sin(theta); - return Vector2(x, y); + return input + delta; } diff --git a/Source/audio/WobbleEffect.h b/Source/audio/WobbleEffect.h index dd8ba807..2d30ca6a 100644 --- a/Source/audio/WobbleEffect.h +++ b/Source/audio/WobbleEffect.h @@ -1,6 +1,6 @@ #pragma once #include "EffectApplication.h" -#include "../shape/Vector2.h" +#include "../shape/Point.h" #include "PitchDetector.h" class WobbleEffect : public EffectApplication { @@ -8,7 +8,7 @@ class WobbleEffect : public EffectApplication { WobbleEffect(PitchDetector& pitchDetector); ~WobbleEffect(); - Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; + Point apply(int index, Point input, const std::vector& values, double sampleRate) override; private: PitchDetector& pitchDetector; diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index 4a1fe15d..b6b87b0b 100644 --- a/Source/components/EffectComponent.cpp +++ b/Source/components/EffectComponent.cpp @@ -69,6 +69,7 @@ void EffectComponent::setupComponent() { lfoSlider.setRange(parameter->lfoRate->min, parameter->lfoRate->max, parameter->lfoRate->step); lfoSlider.setValue(parameter->lfoRate->getValueUnnormalised(), juce::dontSendNotification); + lfoSlider.setSkewFactorFromMidPoint(parameter->lfoRate->min + 0.2 * (parameter->lfoRate->max - parameter->lfoRate->min)); if (lfo.getSelectedId() == static_cast(LfoType::Static)) { lfoSlider.setVisible(false); @@ -197,3 +198,9 @@ void EffectComponent::setComponent(std::shared_ptr component) { this->component = component; addAndMakeVisible(component.get()); } + +void EffectComponent::setSliderOnValueChange() { + slider.onValueChange = [this] { + effect.setValue(index, slider.getValue()); + }; +} diff --git a/Source/components/EffectComponent.h b/Source/components/EffectComponent.h index 7d9ffd1c..c1da63f1 100644 --- a/Source/components/EffectComponent.h +++ b/Source/components/EffectComponent.h @@ -19,6 +19,7 @@ class EffectComponent : public juce::Component, public juce::AudioProcessorParam void handleAsyncUpdate() override; void setComponent(std::shared_ptr component); + void setSliderOnValueChange(); juce::Slider slider; juce::Slider lfoSlider; diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp index a2c6764d..b2b0a1bc 100644 --- a/Source/components/EffectsListComponent.cpp +++ b/Source/components/EffectsListComponent.cpp @@ -84,34 +84,15 @@ void EffectsListComponent::resized() { } std::shared_ptr EffectsListComponent::createComponent(EffectParameter* parameter) { - if (parameter->paramID == "perspectiveRotateX" || parameter->paramID == "perspectiveRotateY" || parameter->paramID == "perspectiveRotateZ") { - BooleanParameter* toggle; - juce::String axis; - if (parameter->paramID == "perspectiveRotateX") { - toggle = audioProcessor.perspectiveEffect->fixedRotateX; - axis = "X"; - } else if (parameter->paramID == "perspectiveRotateY") { - toggle = audioProcessor.perspectiveEffect->fixedRotateY; - axis = "Y"; - } else if (parameter->paramID == "perspectiveRotateZ") { - toggle = audioProcessor.perspectiveEffect->fixedRotateZ; - axis = "Z"; - } - std::shared_ptr button = std::make_shared(parameter->name, BinaryData::fixed_rotate_svg, juce::Colours::white, juce::Colours::red, toggle); - button->setTooltip("Toggles whether the rotation around the " + axis + " axis is fixed, or changes according to the rotation speed."); - button->onClick = [this, toggle] { - toggle->setBoolValueNotifyingHost(!toggle->getBoolValue()); - }; - return button; - } else if (parameter->paramID == "perspectiveStrength") { + if (parameter->paramID == "customEffectStrength") { std::shared_ptr button = std::make_shared(parameter->name, BinaryData::pencil_svg, juce::Colours::white, juce::Colours::red); std::weak_ptr weakButton = button; button->setEdgeIndent(5); - button->setToggleState(editor.editingPerspective, juce::dontSendNotification); - button->setTooltip("Toggles whether the text editor is editing the currently open file, or the Lua 3D perspective function."); + button->setToggleState(editor.editingCustomFunction, juce::dontSendNotification); + button->setTooltip("Toggles whether the text editor is editing the currently open file, or the custom Lua effect."); button->onClick = [this, weakButton] { if (auto button = weakButton.lock()) { - editor.editPerspectiveFunction(button->getToggleState()); + editor.editCustomFunction(button->getToggleState()); } }; return button; diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h index 8b1e202a..00406989 100644 --- a/Source/components/EffectsListComponent.h +++ b/Source/components/EffectsListComponent.h @@ -5,6 +5,7 @@ #include "../audio/Effect.h" #include "EffectComponent.h" #include "ComponentList.h" +#include // Application-specific data container class OscirenderAudioProcessorEditor; @@ -18,6 +19,44 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData resetData(); } + void randomise() { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + + for (int i = 0; i < data.size(); i++) { + auto effect = data[i]; + auto id = effect->getId().toLowerCase(); + + if (id.contains("scale") || id.contains("translate") || id.contains("trace")) { + continue; + } + + for (auto& parameter : effect->parameters) { + parameter->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat()); + if (parameter->lfo != nullptr) { + parameter->lfo->setUnnormalisedValueNotifyingHost((int) LfoType::Static); + parameter->lfoRate->setUnnormalisedValueNotifyingHost(1); + + if (juce::Random::getSystemRandom().nextFloat() > 0.8) { + parameter->lfo->setUnnormalisedValueNotifyingHost((int)(juce::Random::getSystemRandom().nextFloat() * (int)LfoType::Noise)); + parameter->lfoRate->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() * 0.1); + } + } + } + effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7); + } + + // shuffle precedence + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(data.begin(), data.end(), g); + + for (int i = 0; i < data.size(); i++) { + data[i]->setPrecedence(i); + } + + audioProcessor.updateEffectPrecedence(); + } + void resetData() { juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); data.clear(); diff --git a/Source/components/SvgButton.h b/Source/components/SvgButton.h index 4dbece9f..f66854b2 100644 --- a/Source/components/SvgButton.h +++ b/Source/components/SvgButton.h @@ -5,6 +5,8 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame public: SvgButton(juce::String name, juce::String svg, juce::Colour colour, juce::Colour colourOn, BooleanParameter* toggle = nullptr) : juce::DrawableButton(name, juce::DrawableButton::ButtonStyle::ImageFitted), toggle(toggle) { auto doc = juce::XmlDocument::parse(svg); + + setMouseCursor(juce::MouseCursor::PointingHandCursor); changeSvgColour(doc.get(), colour); normalImage = juce::Drawable::createFromSVG(*doc); diff --git a/Source/components/VisualiserComponent.cpp b/Source/components/VisualiserComponent.cpp index 3d6687a2..0ccac56a 100644 --- a/Source/components/VisualiserComponent.cpp +++ b/Source/components/VisualiserComponent.cpp @@ -5,6 +5,8 @@ VisualiserComponent::VisualiserComponent(int numChannels, OscirenderAudioProcess resetBuffer(); startTimerHz(60); startThread(); + + setFullScreen(false); roughness.textBox.setValue(4); intensity.textBox.setValue(1.0); @@ -119,6 +121,14 @@ bool VisualiserComponent::keyPressed(const juce::KeyPress& key) { return false; } +void VisualiserComponent::setFullScreen(bool fullScreen) { + if (fullScreen) { + setTooltip(""); + } else { + setTooltip("Click to pause. Double click to toggle full screen. Right click to change quality and intensity settings."); + } +} + void VisualiserComponent::paintChannel(juce::Graphics& g, juce::Rectangle area, int channel) { juce::Path path; diff --git a/Source/components/VisualiserComponent.h b/Source/components/VisualiserComponent.h index 960a95e0..5a081a10 100644 --- a/Source/components/VisualiserComponent.h +++ b/Source/components/VisualiserComponent.h @@ -13,7 +13,7 @@ enum class FullScreenMode { MAIN_COMPONENT, }; -class VisualiserComponent : public juce::Component, public juce::Timer, public juce::Thread, public juce::MouseListener { +class VisualiserComponent : public juce::Component, public juce::Timer, public juce::Thread, public juce::MouseListener, public juce::SettableTooltipClient { public: VisualiserComponent(int numChannels, OscirenderAudioProcessor& p); ~VisualiserComponent() override; @@ -29,6 +29,8 @@ class VisualiserComponent : public juce::Component, public juce::Timer, public j void run() override; void mouseDown(const juce::MouseEvent& event) override; bool keyPressed(const juce::KeyPress& key) override; + + void setFullScreen(bool fullScreen); private: diff --git a/Source/components/VolumeComponent.cpp b/Source/components/VolumeComponent.cpp index 375fd8f7..1fd83498 100644 --- a/Source/components/VolumeComponent.cpp +++ b/Source/components/VolumeComponent.cpp @@ -117,6 +117,11 @@ void VolumeComponent::run() { leftVolume = std::sqrt(leftVolume / (buffer.size() / 2)); rightVolume = std::sqrt(rightVolume / (buffer.size() / 2)); + if (std::isnan(leftVolume) || std::isnan(rightVolume)) { + leftVolume = 0; + rightVolume = 0; + } + this->leftVolume = leftVolume; this->rightVolume = rightVolume; diff --git a/Source/lua/LuaParser.cpp b/Source/lua/LuaParser.cpp index b6c2db8e..056aee6c 100644 --- a/Source/lua/LuaParser.cpp +++ b/Source/lua/LuaParser.cpp @@ -18,33 +18,33 @@ void LuaParser::reset(lua_State*& L, juce::String script) { void LuaParser::reportError(const char* errorChars) { std::string error = errorChars; - std::regex nilRegex = std::regex(R"(attempt to.*nil value.*'slider_\w')"); - // ignore nil errors about global variables, these are likely caused by other errors - if (std::regex_search(error, nilRegex)) { - return; + std::regex nilRegex = std::regex(R"(attempt to.*nil value.*'slider_\w')"); + // ignore nil errors about global variables, these are likely caused by other errors + if (std::regex_search(error, nilRegex)) { + return; } // remove any newlines from error message error = std::regex_replace(error, std::regex(R"(\n|\r)"), ""); // remove script content from error message - error = std::regex_replace(error, std::regex(R"(^\[string ".*"\]:)"), ""); - // extract line number from start of error message - std::regex lineRegex(R"(^(\d+): )"); - std::smatch lineMatch; - std::regex_search(error, lineMatch, lineRegex); - - if (lineMatch.size() > 1) { - int line = std::stoi(lineMatch[1]); - // remove line number from error message - error = std::regex_replace(error, lineRegex, ""); - errorCallback(line, fileName, error); + error = std::regex_replace(error, std::regex(R"(^\[string ".*"\]:)"), ""); + // extract line number from start of error message + std::regex lineRegex(R"(^(\d+): )"); + std::smatch lineMatch; + std::regex_search(error, lineMatch, lineRegex); + + if (lineMatch.size() > 1) { + int line = std::stoi(lineMatch[1]); + // remove line number from error message + error = std::regex_replace(error, lineRegex, ""); + errorCallback(line, fileName, error); } } void LuaParser::parse(lua_State*& L) { const int ret = luaL_loadstring(L, script.toUTF8()); - if (ret != 0) { - const char* error = lua_tostring(L, -1); + if (ret != 0) { + const char* error = lua_tostring(L, -1); reportError(error); lua_pop(L, 1); functionRef = -1; @@ -96,8 +96,8 @@ std::vector LuaParser::run(lua_State*& L, const LuaVariables vars, long& if (lua_isfunction(L, -1)) { const int ret = lua_pcall(L, 0, LUA_MULTRET, 0); - if (ret != LUA_OK) { - const char* error = lua_tostring(L, -1); + if (ret != LUA_OK) { + const char* error = lua_tostring(L, -1); reportError(error); functionRef = -1; usingFallbackScript = true; diff --git a/Source/mathter/CMakeLists.txt b/Source/mathter/CMakeLists.txt new file mode 100644 index 00000000..07ea4004 --- /dev/null +++ b/Source/mathter/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(Mathter INTERFACE) + +message("${CMAKE_CURRENT_SOURCE_DIR}/..") +target_include_directories(Mathter INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/..") + +target_sources(Mathter + INTERFACE + "Mathter.natvis" +) + +target_compile_features(Mathter INTERFACE cxx_std_17) + +if (${MATHTER_USE_XSIMD}) + find_package(xsimd) + target_link_libraries(Mathter INTERFACE xsimd) +endif() \ No newline at end of file diff --git a/Source/mathter/Common/Approx.hpp b/Source/mathter/Common/Approx.hpp new file mode 100644 index 00000000..190cf312 --- /dev/null +++ b/Source/mathter/Common/Approx.hpp @@ -0,0 +1,125 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +// Specialization for floats. +#include "../IoStream.hpp" +#include "../Matrix.hpp" +#include "../Vector.hpp" + +#include + +namespace mathter { + + +template +bool AlmostEqual(T d1, T d2, std::true_type) { + if (std::abs(d1) < 1e-38 && std::abs(d2) < 1e-38) { + return true; + } + if ((d1 == 0 && d2 < 1e-4) || (d2 == 0 && d1 < 1e-4)) { + return true; + } + T scaler = pow(T(10), floor(std::log10(std::abs(d1)))); + d1 /= scaler; + d2 /= scaler; + d1 *= T(1000.0); + d2 *= T(1000.0); + return round(d1) == round(d2); +} + +// Specialization for int, complex and custom types: simple equality. +template +bool AlmostEqual(T d1, T d2, std::false_type) { + return d1 == d2; +} + +// Check equivalence with tolerance. +template ::value && traits::NotMatrix::value && traits::NotQuaternion::value>> +bool AlmostEqual(T d1, U d2) { + using P = traits::MatMulElemT; + return AlmostEqual(P(d1), P(d2), std::integral_constant::value>()); +} + +template +bool AlmostEqual(const Vector& lhs, const Vector& rhs) { + bool eq = true; + for (auto i : impl::Range(Dim)) { + eq = eq && AlmostEqual(lhs[i], rhs[i]); + } + return eq; +} + +template +bool AlmostEqual(const Quaternion& lhs, const Quaternion& rhs) { + bool eq = true; + for (auto i : impl::Range(4)) { + eq = eq && AlmostEqual(lhs.vec[i], rhs.vec[i]); + } + return eq; +} + +template +bool AlmostEqual(const Matrix& lhs, const Matrix& rhs) { + bool eq = true; + for (auto i : impl::Range(Rows)) { + for (auto j : impl::Range(Columns)) { + eq = eq && AlmostEqual(lhs(i, j), rhs(i, j)); + } + } + return eq; +} + + +// Floating point comparison helper class, works like Catch2 units testing framework's float Approx. +template +struct ApproxHelper { + ApproxHelper() {} + explicit ApproxHelper(LinalgClass object) { + this->object = object; + } + LinalgClass object; +}; + + +template +bool operator==(const ApproxHelper& lhs, const LinalgClass2& rhs) { + return AlmostEqual(lhs.object, rhs); +} + +template +bool operator==(const LinalgClass1& lhs, const ApproxHelper& rhs) { + return AlmostEqual(rhs.object, lhs); +} + +template +bool operator==(const ApproxHelper& lhs, const ApproxHelper& rhs) { + return AlmostEqual(lhs.object, rhs.object); +} + +template +std::ostream& operator<<(std::ostream& os, const ApproxHelper& arg) { + os << arg.object; + return os; +} + +template +ApproxHelper ApproxVec(const LinalgClass& arg) { + return ApproxHelper{ arg }; +} + + + +} // namespace mathter \ No newline at end of file diff --git a/Source/mathter/Common/Definitions.hpp b/Source/mathter/Common/Definitions.hpp new file mode 100644 index 00000000..4a8b1cbf --- /dev/null +++ b/Source/mathter/Common/Definitions.hpp @@ -0,0 +1,75 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +namespace mathter { + +//------------------------------------------------------------------------------ +// Enums +//------------------------------------------------------------------------------ + +/// Determines if you want to left- or right-multiply your matrices with vectors. +/// +/// This flag affects the generated transformation matrices. If you want to write M2*M1*v in your code, +/// then choose PRECEDE_VECTOR, if you want v*M1*M2, choose FOLLOW_VECTOR. Matrices generated by Transform, Scale, +/// Rotation and similar functions will match your order of multiplication. (I.e. bottom row is translation if you +/// choose FOLLOW_VECTOR). +/// You can still use M*v and v*M in your code. +/// +enum class eMatrixOrder { + PRECEDE_VECTOR, + FOLLOW_VECTOR, +}; + +/// Determines the memory layout of matrices. +/// +/// For ROW_MAJOR layout, the matrix's first row comes first in memory, followed immediately by +/// the second row's elements. For COLUMN_MAJOR matrices, the memory region begins with the first column. +/// This does not affect arithmetic or matrix generator function in any way. Your arithmetic will work +/// the same way if you change this. +/// Please note that changing this flag may affect performance of arithmetic operations. Profile your +/// code to determine optimal settings. Performance may depend on multiple factors. +/// +enum class eMatrixLayout { + ROW_MAJOR, + COLUMN_MAJOR, +}; + + + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +/// Specify this as Vector or Matrix dimension template parameter to set size at runtime. +/// PLEASE NOTE THAT DYNAMICALLY SIZED VECTORS AND MATRICES ARE NOT SUPPORTED YET. +constexpr int DYNAMIC = -1; + + +//------------------------------------------------------------------------------ +// Classes +//------------------------------------------------------------------------------ + +template +struct VectorData; + +template +class Vector; + +template +class Swizzle; + +template +class Matrix; + +template +class SubmatrixHelper; + +template +class Quaternion; + + +} // namespace mathter diff --git a/Source/mathter/Common/DeterministicInitializer.hpp b/Source/mathter/Common/DeterministicInitializer.hpp new file mode 100644 index 00000000..cfa6c265 --- /dev/null +++ b/Source/mathter/Common/DeterministicInitializer.hpp @@ -0,0 +1,112 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +#include +#include +#include + +namespace mathter::impl { + + +/// Return a value initialized to zero, or, if not possible, a value-initialized object. +/// If you want special treatment for a particular type, specialize this method. +template +struct NullScalarInitializer { + static constexpr T Get() { + if constexpr (std::is_convertible_v) { + return T(0); + } + else { + return T{}; + } + } +}; + + +/// Return a value initialized to zero, or, if not possible, a value-initialized object. +template +constexpr T NullScalar() { + return NullScalarInitializer::Get(); +} + + +/// Initializes an object of type T to invalid (NaN) value. +/// Returns a signaling NaN, +/// if not possible, a quiet NaN, +/// if not possible, an infinity, +/// if not possible, the highest value for T, +/// if not possible, then a null-initialized value. +/// If you want special treatment for a particular type, specialize this method. +template +struct InvalidScalarInitializer { + static constexpr T Get() { + if constexpr (std::numeric_limits::is_specialized) { + if constexpr (std::numeric_limits::has_signaling_NaN) { + return std::numeric_limits::signaling_NaN(); + } + else if constexpr (std::numeric_limits::has_quiet_NaN) { + return std::numeric_limits::quiet_NaN(); + } + else if constexpr (std::numeric_limits::has_infinity) { + return std::numeric_limits::infinity(); + } + else { + return std::numeric_limits::max(); + } + } + return NullScalar(); + } +}; + + +/// Return a signaling NaN complex number. Same as the unspecialized version. +template +struct InvalidScalarInitializer> { + static constexpr std::complex Get() { + return { InvalidScalarInitializer::Get(), InvalidScalarInitializer::Get() }; + } +}; + + +/// Returns a signaling NaN, +/// if not possible, a quiet NaN, +/// if not possible, an infinity, +/// if not possible, the highest value for T, +/// if not possible, then a null-initialized value. +template +constexpr T InvalidScalar() { + return InvalidScalarInitializer::Get(); +} + + +#if !defined(MATHTER_NULL_INITIALIZE) && !defined(MATHTER_INVALID_INITIALIZE) && !defined(MATHTER_DONT_INITIALIZE) +#ifdef NDEBUG +#define MATHTER_DONT_INITIALIZE 1 +#else +#define MATHTER_INVALID_INITIALIZE 1 +#endif +#endif + + +#if defined(MATHTER_NULL_INITIALIZE) +#define MATHTER_SCALAR_INIT_EXPRESSION(T) mathter::impl::NullScalar() +#elif defined(MATHTER_INVALID_INITIALIZE) +#define MATHTER_SCALAR_INIT_EXPRESSION(T) mathter::impl::InvalidScalar() +#elif defined(MATHTER_DONT_INITIALIZE) +#define MATHTER_SCALAR_INIT_EXPRESSION(T) +#else +#error Set at least one of the MATHTER_NULL/INVALID/DONT_INITIALIZE macros. +#endif + +#if defined(MATHTER_NULL_INITIALIZE) || defined(MATHTER_INVALID_INITIALIZE) +#define MATHTER_VECTOR_INITIALIZER(T) : Vector(MATHTER_SCALAR_INIT_EXPRESSION(T)) +#else +#define MATHTER_VECTOR_INITIALIZER(T) +#endif + + +} // namespace mathter::impl \ No newline at end of file diff --git a/Source/mathter/Common/MathUtil.hpp b/Source/mathter/Common/MathUtil.hpp new file mode 100644 index 00000000..e80f137d --- /dev/null +++ b/Source/mathter/Common/MathUtil.hpp @@ -0,0 +1,34 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +#include + +namespace mathter::impl { + + +template +T sign(T arg) { + return T(arg > T(0)) - (arg < T(0)); +} + +template +T sign_nonzero(T arg) { + return std::copysign(T(1), arg); +} + +template +constexpr T ConstexprExp10(int exponent) { + return exponent == 0 ? T(1) : T(10) * ConstexprExp10(exponent - 1); +} + +template +constexpr T ConstexprAbs(T arg) { + return arg >= T(0) ? arg : -arg; +} + + +} // namespace mathter::impl \ No newline at end of file diff --git a/Source/mathter/Common/Range.hpp b/Source/mathter/Common/Range.hpp new file mode 100644 index 00000000..de5ee385 --- /dev/null +++ b/Source/mathter/Common/Range.hpp @@ -0,0 +1,79 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +#include +#include + + +namespace mathter::impl { + +// Helper for writing for loops as for (auto i : Range(0,10)) +template +class RangeHelper { +public: + class iterator { + friend class RangeHelper; + iterator(T value, T step) : value(value), step(step) {} + + public: + iterator() : value(std::numeric_limits::lowest()) {} + + using value_type = T; + using difference_type = ptrdiff_t; + using reference = T&; + using pointer = T*; + using iterator_category = std::forward_iterator_tag; + + void operator++() { + value += step; + } + T operator*() const { + return value; + } + bool operator==(const iterator& rhs) const { + return value == rhs.value; + } + bool operator!=(const iterator& rhs) const { + return !(*this == rhs); + } + + private: + T value; + T step; + }; + + RangeHelper(T first, T last, T step) : first(first), last(last), step(step) {} + + iterator begin() const { return iterator(first, step); } + iterator end() const { return iterator(last, step); } + iterator cbegin() const { return iterator(first, step); } + iterator cend() const { return iterator(last, step); } + +private: + T first, last, step; +}; + + +template +RangeHelper Range(T first, T last, T step) { + return RangeHelper(first, last, step); +} + +template +RangeHelper Range(T first, T last) { + T step = last >= first ? T(1) : T(-1); + return Range(first, last, step); +} + +template +RangeHelper Range(T last) { + T first = T(0); + T step = last >= first ? T(1) : T(-1); + return Range(first, last, step); +} + +} // namespace mathter::impl \ No newline at end of file diff --git a/Source/mathter/Common/Traits.hpp b/Source/mathter/Common/Traits.hpp new file mode 100644 index 00000000..8f815fba --- /dev/null +++ b/Source/mathter/Common/Traits.hpp @@ -0,0 +1,309 @@ +// L============================================================================= +// L This software is distributed under the MIT license. +// L Copyright 2021 Péter Kardos +// L============================================================================= + +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include +#include +#include +#include + + +namespace mathter::traits { + +// Vector properties +template +class VectorTraitsHelper {}; + +template +class VectorTraitsHelper> { +public: + using Type = T_; + static constexpr int Dim = Dim_; + static constexpr bool Packed = Packed_; +}; + +template +class VectorTraitsHelper> { +public: + using Type = T_; + static constexpr int Dim = Dim_; + static constexpr bool Packed = Packed_; +}; + +template +class VectorTraits : public VectorTraitsHelper::type> {}; + + +// Matrix properties +template +class MatrixTraitsHelper {}; + +template +class MatrixTraitsHelper> { +public: + using Type = T_; + static constexpr int Rows = Rows_; + static constexpr int Columns = Columns_; + static constexpr eMatrixOrder Order = Order_; + static constexpr eMatrixLayout Layout = Layout_; + static constexpr bool Packed = Packed_; +}; + +template +class MatrixTraits : public MatrixTraitsHelper::type> {}; + + +template +class OppositeOrder { +public: + static constexpr eMatrixOrder value = (Order == eMatrixOrder::FOLLOW_VECTOR ? eMatrixOrder::PRECEDE_VECTOR : eMatrixOrder::FOLLOW_VECTOR); +}; + +template +class OppositeLayout { +public: + static constexpr eMatrixLayout value = (Layout == eMatrixLayout::ROW_MAJOR ? eMatrixLayout::COLUMN_MAJOR : eMatrixLayout::ROW_MAJOR); +}; + + +// Common utility +template +using MatMulElemT = decltype(T() * U() + T() * U()); + + + +// Template metaprogramming utilities +template