diff --git a/.gitignore b/.gitignore index 1761600..9c3a0ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +.DS_Store + # ignore JUCE **/Builds **/JuceLibraryCode diff --git a/Source/LegacyProject.cpp b/Source/LegacyProject.cpp index 60efa3e..c17ed05 100644 --- a/Source/LegacyProject.cpp +++ b/Source/LegacyProject.cpp @@ -284,4 +284,4 @@ double OscirenderAudioProcessor::valueFromLegacy(double value, const juce::Strin return std::pow(12000.0, value); } return value; -} \ No newline at end of file +} diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp index 0697f89..e8d02e8 100644 --- a/Source/LookAndFeel.cpp +++ b/Source/LookAndFeel.cpp @@ -33,6 +33,11 @@ OscirenderLookAndFeel::OscirenderLookAndFeel() { setColour(juce::CodeEditorComponent::highlightColourId, Colours::grey); setColour(juce::CaretComponent::caretColourId, Dracula::foreground); setColour(juce::TextEditor::highlightColourId, Colours::grey); + setColour(juce::TabbedButtonBar::tabOutlineColourId, Colours::veryDark); + setColour(juce::TabbedButtonBar::frontOutlineColourId, Colours::veryDark); + setColour(juce::TabbedButtonBar::tabTextColourId, juce::Colours::black); + setColour(juce::TabbedButtonBar::frontTextColourId, juce::Colours::black); + setColour(juce::TabbedComponent::outlineColourId, Colours::veryDark); getCurrentColourScheme().setUIColour(ColourScheme::widgetBackground, Colours::veryDark); } diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 4a8bf40..db62e13 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -11,7 +11,8 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess fileButton.onClick = [this] { chooser = std::make_unique("Open", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.obj;*.svg;*.lua;*.txt"); - auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems; + auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems | + juce::FileBrowserComponent::canSelectFiles; chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); diff --git a/Source/MathUtil.h b/Source/MathUtil.h new file mode 100644 index 0000000..2187fe4 --- /dev/null +++ b/Source/MathUtil.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class MathUtil { +public: + + // from https://stackoverflow.com/questions/11980292/how-to-wrap-around-a-range + static inline double wrapAngle(double angle) { + double twoPi = 2.0 * std::numbers::pi; + return angle - twoPi * floor(angle / twoPi); + } +}; diff --git a/Source/MidiComponent.cpp b/Source/MidiComponent.cpp new file mode 100644 index 0000000..fe128c8 --- /dev/null +++ b/Source/MidiComponent.cpp @@ -0,0 +1,24 @@ +#include "MidiComponent.h" +#include "PluginEditor.h" + +MidiComponent::MidiComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { + addAndMakeVisible(midiToggle); + addAndMakeVisible(keyboard); + + midiToggle.onClick = [this]() { + audioProcessor.midiEnabled->setBoolValueNotifyingHost(midiToggle.getToggleState()); + }; +} + + +void MidiComponent::resized() { + auto area = getLocalBounds().reduced(5); + midiToggle.setBounds(area.removeFromTop(50)); + keyboard.setBounds(area.removeFromBottom(100)); +} + +void MidiComponent::paint(juce::Graphics& g) { + auto rect = getLocalBounds().reduced(5); + g.setColour(getLookAndFeel().findColour(groupComponentBackgroundColourId)); + g.fillRect(rect); +} diff --git a/Source/MidiComponent.h b/Source/MidiComponent.h new file mode 100644 index 0000000..1a09d93 --- /dev/null +++ b/Source/MidiComponent.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "PluginProcessor.h" + +class OscirenderAudioProcessorEditor; +class MidiComponent : public juce::Component { +public: + MidiComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); + + void resized() override; + void paint(juce::Graphics& g) override; +private: + OscirenderAudioProcessor& audioProcessor; + OscirenderAudioProcessorEditor& pluginEditor; + + juce::ToggleButton midiToggle{"Enable MIDI"}; + juce::MidiKeyboardState keyboardState; + juce::MidiKeyboardComponent keyboard{keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiComponent) +}; \ No newline at end of file diff --git a/Source/ObjComponent.h b/Source/ObjComponent.h index a178381..3fedcb2 100644 --- a/Source/ObjComponent.h +++ b/Source/ObjComponent.h @@ -6,7 +6,7 @@ #include "components/SvgButton.h" class OscirenderAudioProcessorEditor; -class ObjComponent : public juce::GroupComponent, public juce::MouseListener { +class ObjComponent : public juce::GroupComponent { public: ObjComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); ~ObjComponent(); @@ -32,4 +32,4 @@ private: std::shared_ptr fixedRotateZ = std::make_shared("fixedRotateZ", juce::String(BinaryData::fixed_rotate_svg), "white", "red", audioProcessor.fixedRotateZ); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ObjComponent) -}; \ No newline at end of file +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index ced5801..0f54ba3 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -4,11 +4,11 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioProcessor& p) : AudioProcessorEditor(&p), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white) { - addAndMakeVisible(effects); - addAndMakeVisible(main); - addChildComponent(lua); - addChildComponent(obj); - addChildComponent(txt); + addAndMakeVisible(tabs); + tabs.addTab("Main", juce::Colours::white, &settings, false); + tabs.addTab("MIDI", juce::Colours::white, &midi, false); + tabs.setTabBackgroundColour(0, juce::Colours::white); + tabs.setTabBackgroundColour(1, juce::Colours::white); addAndMakeVisible(volume); menuBar.setModel(&menuBarModel); @@ -82,16 +82,6 @@ void OscirenderAudioProcessorEditor::initialiseCodeEditors() { void OscirenderAudioProcessorEditor::paint(juce::Graphics& g) { g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); - juce::DropShadow ds(juce::Colours::black, 10, juce::Point(0, 0)); - ds.drawForRectangle(g, main.getBounds()); - ds.drawForRectangle(g, effects.getBounds()); - if (lua.isVisible()) { - ds.drawForRectangle(g, lua.getBounds()); - } - if (obj.isVisible()) { - ds.drawForRectangle(g, obj.getBounds()); - } - g.setColour(juce::Colours::white); g.setFont(15.0f); } @@ -134,15 +124,8 @@ void OscirenderAudioProcessorEditor::resized() { collapseButton.setShape(path, false, true, true); } - auto effectsSection = area.removeFromRight(1.2 * getWidth() / sections); - main.setBounds(area.reduced(5)); - if (lua.isVisible() || obj.isVisible() || txt.isVisible()) { - auto altEffectsSection = effectsSection.removeFromBottom(juce::jmin(effectsSection.getHeight() / 2, txt.isVisible() ? 150 : 300)); - lua.setBounds(altEffectsSection.reduced(5)); - obj.setBounds(altEffectsSection.reduced(5)); - txt.setBounds(altEffectsSection.reduced(5)); - } - effects.setBounds(effectsSection.reduced(5)); + settings.sections = sections; + tabs.setBounds(area); repaint(); } @@ -213,20 +196,7 @@ void OscirenderAudioProcessorEditor::updateCodeEditor() { // parsersLock MUST be locked before calling this function void OscirenderAudioProcessorEditor::fileUpdated(juce::String fileName) { - juce::String extension = fileName.fromLastOccurrenceOf(".", true, false); - lua.setVisible(false); - obj.setVisible(false); - txt.setVisible(false); - if (fileName.isEmpty()) { - // do nothing - } else if (extension == ".lua") { - lua.setVisible(true); - } else if (extension == ".obj") { - obj.setVisible(true); - } else if (extension == ".txt") { - txt.setVisible(true); - } - main.updateFileLabel(); + settings.fileUpdated(fileName); updateCodeEditor(); } @@ -237,7 +207,7 @@ void OscirenderAudioProcessorEditor::handleAsyncUpdate() { void OscirenderAudioProcessorEditor::changeListenerCallback(juce::ChangeBroadcaster* source) { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); initialiseCodeEditors(); - txt.update(); + settings.update(); } void OscirenderAudioProcessorEditor::editPerspectiveFunction(bool enable) { @@ -310,7 +280,7 @@ bool OscirenderAudioProcessorEditor::keyPressed(const juce::KeyPress& key) { bool consumeKey2 = true; if (key.isKeyCode(juce::KeyPress::escapeKey)) { - obj.disableMouseRotation(); + settings.disableMouseRotation(); } else if (key.getModifiers().isCommandDown() && key.getModifiers().isShiftDown() && key.getKeyCode() == 'S') { saveProjectAs(); } else if (key.getModifiers().isCommandDown() && key.getKeyCode() == 'S') { @@ -330,7 +300,8 @@ void OscirenderAudioProcessorEditor::newProject() { void OscirenderAudioProcessorEditor::openProject() { chooser = std::make_unique("Load osci-render Project", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.osci"); - auto flags = juce::FileBrowserComponent::openMode; + auto flags = juce::FileBrowserComponent::openMode | + juce::FileBrowserComponent::canSelectFiles; chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { auto file = chooser.getResult(); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 149d0ab..eafa853 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -2,11 +2,8 @@ #include #include "PluginProcessor.h" -#include "EffectsComponent.h" -#include "MainComponent.h" -#include "LuaComponent.h" -#include "ObjComponent.h" -#include "TxtComponent.h" +#include "SettingsComponent.h" +#include "MidiComponent.h" #include "components/VolumeComponent.h" #include "components/MainMenuBarModel.h" #include "LookAndFeel.h" @@ -41,11 +38,9 @@ public: private: OscirenderAudioProcessor& audioProcessor; - MainComponent main{audioProcessor, *this}; - LuaComponent lua{audioProcessor, *this}; - ObjComponent obj{audioProcessor, *this}; - TxtComponent txt{audioProcessor, *this}; - EffectsComponent effects{audioProcessor, *this}; + juce::TabbedComponent tabs{juce::TabbedButtonBar::TabsAtTop}; + MidiComponent midi{audioProcessor, *this}; + SettingsComponent settings{audioProcessor, *this}; VolumeComponent volume{audioProcessor}; std::vector> codeDocuments; std::vector> codeEditors; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 8861b8f..bbd0a7a 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -32,62 +32,60 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() ) #endif { - producer.startThread(); - // locking isn't necessary here because we are in the constructor toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Bit Crush", "bitCrush", 0.0, 0.0, 1.0) + new EffectParameter("Bit Crush", "bitCrush", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Bulge", "bulge", 0.0, 0.0, 1.0) + new EffectParameter("Bulge", "bulge", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("2D Rotate", "2DRotateSpeed", 0.0, 0.0, 1.0) + new EffectParameter("2D Rotate", "2DRotateSpeed", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Vector Cancelling", "vectorCancelling", 0.0, 0.0, 1.0) + new EffectParameter("Vector Cancelling", "vectorCancelling", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(false), - new EffectParameter("Distort X", "distortX", 0.0, 0.0, 1.0) + new EffectParameter("Distort X", "distortX", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( std::make_shared(true), - new EffectParameter("Distort Y", "distortY", 0.0, 0.0, 1.0) + new EffectParameter("Distort Y", "distortY", VERSION_HINT, 0.0, 0.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; - }, std::vector{new EffectParameter("Translate X", "translateX", 0.0, -1.0, 1.0), new EffectParameter("Translate Y", "translateY", 0.0, -1.0, 1.0)} + }, std::vector{new EffectParameter("Translate X", "translateX", VERSION_HINT, 0.0, -1.0, 1.0), new EffectParameter("Translate Y", "translateY", VERSION_HINT, 0.0, -1.0, 1.0)} )); toggleableEffects.push_back(std::make_shared( std::make_shared(), - new EffectParameter("Smoothing", "smoothing", 0.0, 0.0, 1.0) + new EffectParameter("Smoothing", "smoothing", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( wobbleEffect, - new EffectParameter("Wobble", "wobble", 0.0, 0.0, 1.0) + new EffectParameter("Wobble", "wobble", VERSION_HINT, 0.0, 0.0, 1.0) )); toggleableEffects.push_back(std::make_shared( delayEffect, - std::vector{new EffectParameter("Delay Decay", "delayDecay", 0.0, 0.0, 1.0), new EffectParameter("Delay Length", "delayLength", 0.5, 0.0, 1.0)} + std::vector{new EffectParameter("Delay Decay", "delayDecay", VERSION_HINT, 0.0, 0.0, 1.0), new EffectParameter("Delay Length", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0)} )); toggleableEffects.push_back(std::make_shared( perspectiveEffect, std::vector{ - new EffectParameter("3D Perspective", "perspectiveStrength", 0.0, 0.0, 1.0), - new EffectParameter("Depth (z)", "perspectiveZPos", 0.1, 0.0, 1.0), - new EffectParameter("Rotate Speed", "perspectiveRotateSpeed", 0.0, -1.0, 1.0), - new EffectParameter("Rotate X", "perspectiveRotateX", 1.0, -1.0, 1.0), - new EffectParameter("Rotate Y", "perspectiveRotateY", 1.0, -1.0, 1.0), - new EffectParameter("Rotate Z", "perspectiveRotateZ", 0.0, -1.0, 1.0), + new EffectParameter("3D Perspective", "perspectiveStrength", VERSION_HINT, 0.0, 0.0, 1.0), + new EffectParameter("Depth (z)", "perspectiveZPos", VERSION_HINT, 0.1, 0.0, 1.0), + new EffectParameter("Rotate Speed", "perspectiveRotateSpeed", VERSION_HINT, 0.0, -1.0, 1.0), + new EffectParameter("Rotate X", "perspectiveRotateX", VERSION_HINT, 1.0, -1.0, 1.0), + new EffectParameter("Rotate Y", "perspectiveRotateY", VERSION_HINT, 1.0, -1.0, 1.0), + new EffectParameter("Rotate Z", "perspectiveRotateZ", VERSION_HINT, 0.0, -1.0, 1.0), } )); toggleableEffects.push_back(traceMax); @@ -133,10 +131,17 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() booleanParameters.push_back(perspectiveEffect->fixedRotateX); booleanParameters.push_back(perspectiveEffect->fixedRotateY); booleanParameters.push_back(perspectiveEffect->fixedRotateZ); + booleanParameters.push_back(midiEnabled); for (auto parameter : booleanParameters) { addParameter(parameter); } + + for (int i = 0; i < 4; i++) { + synth.addVoice(new ShapeVoice(*this)); + } + + synth.addSound(defaultSound); } OscirenderAudioProcessor::~OscirenderAudioProcessor() {} @@ -194,6 +199,7 @@ void OscirenderAudioProcessor::changeProgramName(int index, const juce::String& void OscirenderAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; pitchDetector.setSampleRate(sampleRate); + synth.setCurrentPlaybackSampleRate(sampleRate); } void OscirenderAudioProcessor::releaseResources() { @@ -240,7 +246,7 @@ void OscirenderAudioProcessor::addLuaSlider() { luaEffects.push_back(std::make_shared( std::make_shared(sliderName, *this), - new EffectParameter("Lua " + sliderName, "lua" + sliderName, 0.0, 0.0, 1.0, 0.001, false) + new EffectParameter("Lua " + sliderName, "lua" + sliderName, VERSION_HINT, 0.0, 0.0, 1.0, 0.001, false) )); auto& effect = luaEffects.back(); @@ -304,7 +310,8 @@ void OscirenderAudioProcessor::updateFileBlock(int index, std::shared_ptr()); fileNames.push_back(file.getFileName()); - parsers.push_back(std::make_unique()); + parsers.push_back(std::make_shared()); + sounds.push_back(new ShapeSound(parsers.back())); file.createInputStream()->readIntoMemoryBlock(*fileBlocks.back()); openFile(fileBlocks.size() - 1); @@ -314,7 +321,8 @@ void OscirenderAudioProcessor::addFile(juce::File file) { void OscirenderAudioProcessor::addFile(juce::String fileName, const char* data, const int size) { fileBlocks.push_back(std::make_shared()); fileNames.push_back(fileName); - parsers.push_back(std::make_unique()); + parsers.push_back(std::make_shared()); + sounds.push_back(new ShapeSound(parsers.back())); fileBlocks.back()->append(data, size); openFile(fileBlocks.size() - 1); @@ -324,7 +332,8 @@ void OscirenderAudioProcessor::addFile(juce::String fileName, const char* data, void OscirenderAudioProcessor::addFile(juce::String fileName, std::shared_ptr data) { fileBlocks.push_back(data); fileNames.push_back(fileName); - parsers.push_back(std::make_unique()); + parsers.push_back(std::make_shared()); + sounds.push_back(new ShapeSound(parsers.back())); openFile(fileBlocks.size() - 1); } @@ -337,6 +346,7 @@ void OscirenderAudioProcessor::removeFile(int index) { fileBlocks.erase(fileBlocks.begin() + index); fileNames.erase(fileNames.begin() + index); parsers.erase(parsers.begin() + index); + sounds.erase(sounds.begin() + index); auto newFileIndex = index; if (newFileIndex >= fileBlocks.size()) { newFileIndex = fileBlocks.size() - 1; @@ -366,18 +376,26 @@ void OscirenderAudioProcessor::openFile(int index) { void OscirenderAudioProcessor::changeCurrentFile(int index) { if (index == -1) { currentFile = -1; - producer.setSource(std::make_shared(), -1); + changeSound(defaultSound); } if (index < 0 || index >= fileBlocks.size()) { return; } - producer.setSource(parsers[index], index); + changeSound(sounds[index]); currentFile = index; - invalidateFrameBuffer = true; updateLuaValues(); updateObjValues(); } +void OscirenderAudioProcessor::changeSound(ShapeSound::Ptr sound) { + synth.clearSounds(); + synth.addSound(sound); + for (int i = 0; i < synth.getNumVoices(); i++) { + auto voice = dynamic_cast(synth.getVoice(i)); + voice->updateSound(sound.get()); + } +} + int OscirenderAudioProcessor::getCurrentFileIndex() { return currentFile; } @@ -402,113 +420,42 @@ std::shared_ptr OscirenderAudioProcessor::getFileBlock(int in return fileBlocks[index]; } -void OscirenderAudioProcessor::addFrame(std::vector> frame, int fileIndex) { - const auto scope = frameFifo.write(1); - - if (scope.blockSize1 > 0) { - frameBuffer[scope.startIndex1].clear(); - for (auto& shape : frame) { - frameBuffer[scope.startIndex1].push_back(std::move(shape)); - } - frameBufferIndices[scope.startIndex1] = fileIndex; - } - - if (scope.blockSize2 > 0) { - frameBuffer[scope.startIndex2].clear(); - for (auto& shape : frame) { - frameBuffer[scope.startIndex2].push_back(std::move(shape)); - } - frameBufferIndices[scope.startIndex2] = fileIndex; - } -} - -void OscirenderAudioProcessor::updateFrame() { - currentShape = 0; - shapeDrawn = 0.0; - frameDrawn = 0.0; - - if (frameFifo.getNumReady() > 0) { - { - const auto scope = frameFifo.read(1); - - if (scope.blockSize1 > 0) { - frame.swap(frameBuffer[scope.startIndex1]); - currentBufferIndex = frameBufferIndices[scope.startIndex1]; - } else if (scope.blockSize2 > 0) { - frame.swap(frameBuffer[scope.startIndex2]); - currentBufferIndex = frameBufferIndices[scope.startIndex2]; - } - - frameLength = Shape::totalLength(frame); - } - } -} - -void OscirenderAudioProcessor::updateLengthIncrement() { - double traceMaxValue = traceMaxEnabled ? actualTraceMax : 1.0; - double traceMinValue = traceMinEnabled ? actualTraceMin : 0.0; - double proportionalLength = (traceMaxValue - traceMinValue) * frameLength; - lengthIncrement = juce::jmax(proportionalLength / (currentSampleRate / frequency), MIN_LENGTH_INCREMENT); -} - -void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) -{ +void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); - // In case we have more outputs than inputs, this code clears any output - // channels that didn't contain input data, (because these aren't - // guaranteed to be empty - they may contain garbage). - // This is here to avoid people getting screaming feedback - // when they first compile a plugin, but obviously you don't need to keep - // this code if your algorithm always overwrites all the output channels. - for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) - buffer.clear (i, 0, buffer.getNumSamples()); - - - auto* channelData = buffer.getArrayOfWritePointers(); - auto numSamples = buffer.getNumSamples(); - - if (invalidateFrameBuffer) { - frameFifo.reset(); - // keeps getting the next frame until the frame comes from the file that we want to render. - // this MIGHT be hacky and cause issues later down the line, but for now it works as a - // solution to get instant changing of current file when pressing j and k. - while (currentBufferIndex != currentFile) { - updateFrame(); - } - invalidateFrameBuffer = false; + buffer.clear(); + bool usingMidi = midiEnabled->getBoolValue(); + if (!usingMidi) { + midiMessages.clear(); } - for (auto sample = 0; sample < numSamples; ++sample) { - updateLengthIncrement(); - - traceMaxEnabled = false; - traceMinEnabled = false; - - Vector2 channels; - double x = 0.0; - double y = 0.0; - - - std::shared_ptr sampleParser; - { - juce::SpinLock::ScopedLockType lock(parsersLock); - if (currentFile >= 0 && parsers[currentFile]->isSample()) { - sampleParser = parsers[currentFile]; - } - } - bool renderingSample = sampleParser != nullptr; - - if (renderingSample) { - channels = sampleParser->nextSample(); - } else if (currentShape < frame.size()) { - auto& shape = frame[currentShape]; - double length = shape->length(); - double drawingProgress = length == 0.0 ? 1 : shapeDrawn / length; - channels = shape->nextVector(drawingProgress); + // if midi enabled has changed state + if (prevMidiEnabled != usingMidi) { + for (int i = 1; i <= 16; i++) { + midiMessages.addEvent(juce::MidiMessage::allNotesOff(i), i); } + } + + // if midi has just been disabled + if (prevMidiEnabled && !usingMidi) { + midiMessages.addEvent(juce::MidiMessage::noteOn(1, 60, 1.0f), 17); + } + + prevMidiEnabled = usingMidi; + + { + juce::SpinLock::ScopedLockType lock1(parsersLock); + juce::SpinLock::ScopedLockType lock2(effectsLock); + synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + } + midiMessages.clear(); + + auto* channelData = buffer.getArrayOfWritePointers(); + + for (auto sample = 0; sample < buffer.getNumSamples(); ++sample) { + Vector2 channels = {buffer.getSample(0, sample), buffer.getSample(1, sample)}; { juce::SpinLock::ScopedLockType lock1(parsersLock); @@ -523,8 +470,8 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju } } - x = channels.x; - y = channels.y; + double x = channels.x; + double y = channels.y; x *= volume; y *= volume; @@ -540,58 +487,17 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju channelData[0][sample] = x; } - audioProducer.write(x, y); - - actualTraceMax = juce::jmax(actualTraceMin + MIN_TRACE, juce::jmin(traceMaxValue, 1.0)); - actualTraceMin = juce::jmax(MIN_TRACE, juce::jmin(traceMinValue, actualTraceMax - MIN_TRACE)); - - if (!renderingSample) { - incrementShapeDrawing(); - } - - double drawnFrameLength = traceMaxEnabled ? actualTraceMax * frameLength : frameLength; - - if (!renderingSample && frameDrawn >= drawnFrameLength) { - updateFrame(); - // TODO: updateFrame already iterates over all the shapes, - // so we can improve performance by calculating frameDrawn - // and shapeDrawn directly. frameDrawn is simply actualTraceMin * frameLength - // but shapeDrawn is the amount of the current shape that has been drawn so - // we need to iterate over all the shapes to calculate it. - if (traceMinEnabled) { - while (frameDrawn < actualTraceMin * frameLength) { - incrementShapeDrawing(); - } + { + juce::SpinLock::ScopedLockType scope(consumerLock); + for (auto consumer : consumers) { + consumer->write(x); + consumer->write(y); + consumer->notifyIfFull(); } } } } -// TODO this is the slowest part of the program - any way to improve this would help! -void OscirenderAudioProcessor::incrementShapeDrawing() { - double length = currentShape < frame.size() ? frame[currentShape]->len : 0.0; - // hard cap on how many times it can be over the length to - // prevent audio stuttering - auto increment = juce::jmin(lengthIncrement, 20 * length); - frameDrawn += increment; - shapeDrawn += increment; - - // Need to skip all shapes that the lengthIncrement draws over. - // This is especially an issue when there are lots of small lines being - // drawn. - while (shapeDrawn > length) { - shapeDrawn -= length; - currentShape++; - if (currentShape >= frame.size()) { - currentShape = 0; - break; - } - // POTENTIAL TODO: Think of a way to make this more efficient when iterating - // this loop many times - length = frame[currentShape]->len; - } -} - //============================================================================== bool OscirenderAudioProcessor::hasEditor() const { return true; // (change this to false if you choose to not supply an editor) @@ -721,6 +627,28 @@ void OscirenderAudioProcessor::setStateInformation(const void* data, int sizeInB } } +std::shared_ptr OscirenderAudioProcessor::consumerRegister(std::vector& buffer) { + std::shared_ptr consumer = std::make_shared(buffer); + juce::SpinLock::ScopedLockType scope(consumerLock); + consumers.push_back(consumer); + + return consumer; +} + +void OscirenderAudioProcessor::consumerRead(std::shared_ptr consumer) { + consumer->waitUntilFull(); + juce::SpinLock::ScopedLockType scope(consumerLock); + consumers.erase(std::remove(consumers.begin(), consumers.end(), consumer), consumers.end()); +} + +void OscirenderAudioProcessor::consumerStop(std::shared_ptr consumer) { + if (consumer != nullptr) { + juce::SpinLock::ScopedLockType scope(consumerLock); + consumer->forceNotify(); + } +} + + //============================================================================== // This creates new instances of the plugin.. juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index c511b99..9966960 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -10,12 +10,11 @@ #include #include "shape/Shape.h" -#include "parser/FileParser.h" -#include "parser/FrameProducer.h" -#include "parser/FrameConsumer.h" +#include "concurrency/BufferConsumer.h" #include "audio/Effect.h" +#include "audio/ShapeSound.h" +#include "audio/ShapeVoice.h" #include -#include "concurrency/BufferProducer.h" #include "audio/AudioWebSocketServer.h" #include "audio/DelayEffect.h" #include "audio/PitchDetector.h" @@ -29,7 +28,6 @@ class OscirenderAudioProcessor : public juce::AudioProcessor #if JucePlugin_Enable_ARA , public juce::AudioProcessorARAExtension #endif - , public FrameConsumer { public: OscirenderAudioProcessor(); @@ -60,6 +58,12 @@ public: void changeProgramName(int index, const juce::String& newName) override; void getStateInformation(juce::MemoryBlock& destData) override; void setStateInformation(const void* data, int sizeInBytes) override; + std::shared_ptr consumerRegister(std::vector& buffer); + void consumerStop(std::shared_ptr consumer); + void consumerRead(std::shared_ptr consumer); + void setMidiEnabled(bool enabled); + + int VERSION_HINT = 1; std::atomic currentSampleRate = 0.0; @@ -71,21 +75,21 @@ public: [this](int index, Vector2 input, const std::vector& values, double sampleRate) { frequency = values[0]; return input; - }, new EffectParameter("Frequency", "frequency", 440.0, 0.0, 12000.0, 0.1) + }, new EffectParameter("Frequency", "frequency", VERSION_HINT, 440.0, 0.0, 12000.0, 0.1) ); std::shared_ptr volumeEffect = std::make_shared( [this](int index, Vector2 input, const std::vector& values, double sampleRate) { volume = values[0]; return input; - }, new EffectParameter("Volume", "volume", 1.0, 0.0, 3.0) + }, new EffectParameter("Volume", "volume", VERSION_HINT, 1.0, 0.0, 3.0) ); std::shared_ptr thresholdEffect = std::make_shared( [this](int index, Vector2 input, const std::vector& values, double sampleRate) { threshold = values[0]; return input; - }, new EffectParameter("Threshold", "threshold", 1.0, 0.0, 1.0) + }, new EffectParameter("Threshold", "threshold", VERSION_HINT, 1.0, 0.0, 1.0) ); std::shared_ptr focalLength = std::make_shared( @@ -96,12 +100,12 @@ public: camera->setFocalLength(values[0]); } return input; - }, new EffectParameter("Focal length", "objFocalLength", 1.0, 0.0, 2.0) + }, new EffectParameter("Focal length", "objFocalLength", VERSION_HINT, 1.0, 0.0, 2.0) ); - BooleanParameter* fixedRotateX = new BooleanParameter("Object Fixed Rotate X", "objFixedRotateX", false); - BooleanParameter* fixedRotateY = new BooleanParameter("Object Fixed Rotate Y", "objFixedRotateY", false); - BooleanParameter* fixedRotateZ = new BooleanParameter("Object Fixed Rotate Z", "objFixedRotateZ", false); + 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) { @@ -115,7 +119,7 @@ public: } } return input; - }, new EffectParameter("Rotate X", "objRotateX", 1.0, -1.0, 1.0) + }, new EffectParameter("Rotate X", "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) { @@ -130,7 +134,7 @@ public: } } return input; - }, new EffectParameter("Rotate Y", "objRotateY", 1.0, -1.0, 1.0) + }, new EffectParameter("Rotate Y", "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) { @@ -145,7 +149,7 @@ public: } } return input; - }, new EffectParameter("Rotate Z", "objRotateZ", 0.0, -1.0, 1.0) + }, new EffectParameter("Rotate Z", "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) { @@ -155,25 +159,41 @@ public: obj->setRotationSpeed(values[0]); } return input; - }, new EffectParameter("Rotate Speed", "objRotateSpeed", 0.0, -1.0, 1.0) + }, new EffectParameter("Rotate Speed", "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) { + return input; + }, new EffectParameter("Trace max", "traceMax", VERSION_HINT, 1.0, 0.0, 1.0) + ); + std::shared_ptr traceMin = std::make_shared( + [this](int index, Vector2 input, const std::vector& values, double sampleRate) { + return input; + }, new EffectParameter("Trace min", "traceMin", VERSION_HINT, 0.0, 0.0, 1.0) ); std::shared_ptr delayEffect = std::make_shared(); - std::shared_ptr perspectiveEffect = std::make_shared(); + std::shared_ptr perspectiveEffect = std::make_shared(VERSION_HINT); + + BooleanParameter* midiEnabled = new BooleanParameter("MIDI Enabled", "midiEnabled", VERSION_HINT, false); + std::atomic frequency = 440.0f; juce::SpinLock parsersLock; std::vector> parsers; + std::vector sounds; std::vector> fileBlocks; std::vector fileNames; std::atomic currentFile = -1; juce::ChangeBroadcaster broadcaster; + +private: + juce::SpinLock consumerLock; + std::vector> consumers; +public: - FrameProducer producer = FrameProducer(*this, std::make_shared()); - - BufferProducer audioProducer; - - PitchDetector pitchDetector{audioProducer}; + PitchDetector pitchDetector{*this}; std::shared_ptr wobbleEffect = std::make_shared(pitchDetector); // shouldn't be accessed by audio thread, but needs to persist when GUI is closed @@ -184,7 +204,6 @@ public: juce::Font font = juce::Font(juce::Font::getDefaultSansSerifFontName(), 1.0f, juce::Font::plain); void addLuaSlider(); - void addFrame(std::vector> frame, int fileIndex) override; void updateEffectPrecedence(); void updateFileBlock(int index, std::shared_ptr block); void addFile(juce::File file); @@ -200,54 +219,20 @@ public: juce::String getFileName(int index); std::shared_ptr getFileBlock(int index); private: - std::atomic frequency = 440.0f; std::atomic volume = 1.0; std::atomic threshold = 1.0; - - juce::AbstractFifo frameFifo{ 10 }; - std::vector> frameBuffer[10]; - int frameBufferIndices[10]; - - int currentShape = 0; - std::vector> frame; - int currentBufferIndex = -1; - double frameLength; - double shapeDrawn = 0.0; - double frameDrawn = 0.0; - double lengthIncrement = 0.0; - bool invalidateFrameBuffer = false; + + bool prevMidiEnabled = !midiEnabled->getBoolValue(); std::vector booleanParameters; std::vector> allEffects; std::vector> permanentEffects; - std::shared_ptr traceMax = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - traceMaxValue = values[0]; - traceMaxEnabled = true; - return input; - }, new EffectParameter("Trace max", "traceMax", 1.0, 0.0, 1.0) - ); - std::shared_ptr traceMin = std::make_shared( - [this](int index, Vector2 input, const std::vector& values, double sampleRate) { - traceMinValue = values[0]; - traceMinEnabled = true; - return input; - }, new EffectParameter("Trace min", "traceMin", 0.0, 0.0, 1.0) - ); - const double MIN_TRACE = 0.005; - double traceMaxValue = traceMax->getValue(); - double traceMinValue = traceMin->getValue(); - double actualTraceMax = traceMaxValue; - double actualTraceMin = traceMinValue; - bool traceMaxEnabled = false; - bool traceMinEnabled = false; + ShapeSound::Ptr defaultSound = new ShapeSound(std::make_shared()); + juce::Synthesiser synth; - AudioWebSocketServer softwareOscilloscopeServer{audioProducer}; + AudioWebSocketServer softwareOscilloscopeServer{*this}; - void updateFrame(); - void updateLengthIncrement(); - void incrementShapeDrawing(); void updateLuaValues(); void updateObjValues(); std::shared_ptr getEffect(juce::String id); @@ -256,6 +241,7 @@ private: std::pair, EffectParameter*> effectFromLegacyId(const juce::String& id, bool updatePrecedence = false); LfoType lfoTypeFromLegacyAnimationType(const juce::String& type); double valueFromLegacy(double value, const juce::String& id); + void changeSound(ShapeSound::Ptr sound); const double MIN_LENGTH_INCREMENT = 0.000001; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp new file mode 100644 index 0000000..ff4ca9a --- /dev/null +++ b/Source/SettingsComponent.cpp @@ -0,0 +1,69 @@ +#include "SettingsComponent.h" +#include "PluginEditor.h" + +SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { + addAndMakeVisible(effects); + addAndMakeVisible(main); + addChildComponent(lua); + addChildComponent(obj); + addChildComponent(txt); +} + + +void SettingsComponent::resized() { + auto area = getLocalBounds(); + auto effectsSection = area.removeFromRight(1.2 * pluginEditor.getWidth() / sections); + area.removeFromLeft(5); + area.removeFromRight(3); + area.removeFromTop(5); + area.removeFromBottom(5); + + main.setBounds(area); + if (lua.isVisible() || obj.isVisible() || txt.isVisible()) { + int height = txt.isVisible() ? 150 : 300; + auto altEffectsSection = effectsSection.removeFromBottom(juce::jmin(effectsSection.getHeight() / 2, height)); + altEffectsSection.removeFromTop(3); + altEffectsSection.removeFromLeft(2); + altEffectsSection.removeFromRight(5); + altEffectsSection.removeFromBottom(5); + + lua.setBounds(altEffectsSection); + obj.setBounds(altEffectsSection); + txt.setBounds(altEffectsSection); + + effectsSection.removeFromBottom(2); + } else { + effectsSection.removeFromBottom(5); + } + + effectsSection.removeFromLeft(2); + effectsSection.removeFromRight(5); + effectsSection.removeFromTop(5); + effects.setBounds(effectsSection); +} + +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()) { + // do nothing + } else if (extension == ".lua") { + lua.setVisible(true); + } else if (extension == ".obj") { + obj.setVisible(true); + } else if (extension == ".txt") { + txt.setVisible(true); + } + main.updateFileLabel(); + resized(); +} + +void SettingsComponent::update() { + txt.update(); +} + +void SettingsComponent::disableMouseRotation() { + obj.disableMouseRotation(); +} diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h new file mode 100644 index 0000000..ab96db6 --- /dev/null +++ b/Source/SettingsComponent.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "MainComponent.h" +#include "LuaComponent.h" +#include "ObjComponent.h" +#include "TxtComponent.h" +#include "EffectsComponent.h" + +class OscirenderAudioProcessorEditor; +class SettingsComponent : public juce::Component { +public: + SettingsComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); + + void resized() override; + void fileUpdated(juce::String fileName); + void update(); + void disableMouseRotation(); + + int sections = 2; +private: + OscirenderAudioProcessor& audioProcessor; + OscirenderAudioProcessorEditor& pluginEditor; + + MainComponent main{audioProcessor, pluginEditor}; + LuaComponent lua{audioProcessor, pluginEditor}; + ObjComponent obj{audioProcessor, pluginEditor}; + TxtComponent txt{audioProcessor, pluginEditor}; + EffectsComponent effects{audioProcessor, pluginEditor}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent) +}; \ No newline at end of file diff --git a/Source/audio/AudioWebSocketServer.cpp b/Source/audio/AudioWebSocketServer.cpp index a6af403..7def241 100644 --- a/Source/audio/AudioWebSocketServer.cpp +++ b/Source/audio/AudioWebSocketServer.cpp @@ -1,6 +1,7 @@ #include "AudioWebSocketServer.h" +#include "../PluginProcessor.h" -AudioWebSocketServer::AudioWebSocketServer(BufferProducer& producer) : juce::Thread("AudioWebSocketServer"), producer(producer) { +AudioWebSocketServer::AudioWebSocketServer(OscirenderAudioProcessor& audioProcessor) : juce::Thread("AudioWebSocketServer"), audioProcessor(audioProcessor) { server.setOnClientMessageCallback([](std::shared_ptr connectionState, ix::WebSocket & webSocket, const ix::WebSocketMessagePtr & msg) { // The ConnectionState object contains information about the connection, // at this point only the client ip address and the port. @@ -40,18 +41,17 @@ AudioWebSocketServer::AudioWebSocketServer(BufferProducer& producer) : juce::Thr AudioWebSocketServer::~AudioWebSocketServer() { server.stop(); ix::uninitNetSystem(); - producer.unregisterConsumer(consumer); + audioProcessor.consumerStop(consumer); stopThread(1000); } void AudioWebSocketServer::run() { - producer.registerConsumer(consumer); - while (!threadShouldExit()) { - auto floatBuffer = consumer->startProcessing(); + consumer = audioProcessor.consumerRegister(floatBuffer); + audioProcessor.consumerRead(consumer); - for (int i = 0; i < floatBuffer->size(); i++) { - short sample = floatBuffer->at(i) * 32767; + for (int i = 0; i < floatBuffer.size(); i++) { + short sample = floatBuffer[i] * 32767; char b0 = sample & 0xff; char b1 = (sample >> 8) & 0xff; buffer[2 * i] = b0; @@ -62,8 +62,6 @@ void AudioWebSocketServer::run() { ix::IXWebSocketSendData data{buffer, sizeof(buffer)}; client->sendBinary(data); } - - consumer->finishedProcessing(); } } diff --git a/Source/audio/AudioWebSocketServer.h b/Source/audio/AudioWebSocketServer.h index 891637f..0cd7697 100644 --- a/Source/audio/AudioWebSocketServer.h +++ b/Source/audio/AudioWebSocketServer.h @@ -1,18 +1,21 @@ #pragma once #include #include "../ixwebsocket/IXWebSocketServer.h" -#include "../concurrency/BufferProducer.h" +#include "../concurrency/BufferConsumer.h" +class OscirenderAudioProcessor; class AudioWebSocketServer : juce::Thread { public: - AudioWebSocketServer(BufferProducer& producer); + AudioWebSocketServer(OscirenderAudioProcessor& audioProcessor); ~AudioWebSocketServer(); void run() override; private: ix::WebSocketServer server{ 42988 }; - BufferProducer& producer; - std::shared_ptr consumer = std::make_shared(4096); + OscirenderAudioProcessor& audioProcessor; + std::vector floatBuffer = std::vector(2 * 4096); char buffer[4096 * 2 * 2]; -}; \ No newline at end of file + + std::shared_ptr consumer; +}; diff --git a/Source/audio/BitCrushEffect.cpp b/Source/audio/BitCrushEffect.cpp index 9efdb15..3f08ab1 100644 --- a/Source/audio/BitCrushEffect.cpp +++ b/Source/audio/BitCrushEffect.cpp @@ -2,8 +2,6 @@ BitCrushEffect::BitCrushEffect() {} -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) { double value = values[0]; diff --git a/Source/audio/BitCrushEffect.h b/Source/audio/BitCrushEffect.h index 9b93e54..edc537d 100644 --- a/Source/audio/BitCrushEffect.h +++ b/Source/audio/BitCrushEffect.h @@ -5,7 +5,6 @@ class BitCrushEffect : public EffectApplication { public: BitCrushEffect(); - ~BitCrushEffect(); Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; -}; \ No newline at end of file +}; diff --git a/Source/audio/BooleanParameter.h b/Source/audio/BooleanParameter.h index 2323438..b2cd44e 100644 --- a/Source/audio/BooleanParameter.h +++ b/Source/audio/BooleanParameter.h @@ -4,7 +4,7 @@ class BooleanParameter : public juce::AudioProcessorParameterWithID { public: - BooleanParameter(juce::String name, juce::String id, bool value) : AudioProcessorParameterWithID(id, name), value(value) {} + BooleanParameter(juce::String name, juce::String id, int versionHint, bool value) : AudioProcessorParameterWithID(juce::ParameterID(id, versionHint), name), value(value) {} juce::String getName(int maximumStringLength) const override { return name.substring(0, maximumStringLength); @@ -86,4 +86,4 @@ public: private: std::atomic value = false; -}; \ No newline at end of file +}; diff --git a/Source/audio/Effect.cpp b/Source/audio/Effect.cpp index a87ee7a..c914b49 100644 --- a/Source/audio/Effect.cpp +++ b/Source/audio/Effect.cpp @@ -33,47 +33,47 @@ void Effect::animateValues() { float percentage = phase / (2 * std::numbers::pi); LfoType type = lfoEnabled ? (LfoType)(int)parameter->lfo->getValueUnnormalised() : LfoType::Static; - switch (type) { - case LfoType::Sine: - actualValues[i] = std::sin(phase) * 0.5 + 0.5; - actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; - break; - case LfoType::Square: - actualValues[i] = (percentage < 0.5) ? maxValue : minValue; - break; - case LfoType::Seesaw: - // modified sigmoid function - actualValues[i] = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; - actualValues[i] = 1 / (1 + std::exp(-16 * (actualValues[i] - 0.5))); - actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; - break; - case LfoType::Triangle: - actualValues[i] = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; - actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; - break; - case LfoType::Sawtooth: - actualValues[i] = percentage * (maxValue - minValue) + minValue; - break; - case LfoType::ReverseSawtooth: - actualValues[i] = (1 - percentage) * (maxValue - minValue) + minValue; - break; - case LfoType::Noise: - actualValues[i] = ((float)rand() / RAND_MAX) * (maxValue - minValue) + minValue; - break; + switch (type) { + case LfoType::Sine: + actualValues[i] = std::sin(phase) * 0.5 + 0.5; + actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; + break; + case LfoType::Square: + actualValues[i] = (percentage < 0.5) ? maxValue : minValue; + break; + case LfoType::Seesaw: + // modified sigmoid function + actualValues[i] = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; + actualValues[i] = 1 / (1 + std::exp(-16 * (actualValues[i] - 0.5))); + actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; + break; + case LfoType::Triangle: + actualValues[i] = (percentage < 0.5) ? percentage * 2 : (1 - percentage) * 2; + actualValues[i] = actualValues[i] * (maxValue - minValue) + minValue; + break; + case LfoType::Sawtooth: + actualValues[i] = percentage * (maxValue - minValue) + minValue; + break; + case LfoType::ReverseSawtooth: + actualValues[i] = (1 - percentage) * (maxValue - minValue) + minValue; + break; + case LfoType::Noise: + actualValues[i] = ((float)rand() / RAND_MAX) * (maxValue - minValue) + minValue; + break; default: double weight = parameter->smoothValueChange ? 0.0005 : 1.0; actualValues[i] = (1.0 - weight) * actualValues[i] + weight * parameter->getValueUnnormalised(); - break; + break; } } } // should only be the audio thread calling this, but either way it's not a big deal float Effect::nextPhase(EffectParameter* parameter) { - parameter->phase += parameter->lfoRate->getValueUnnormalised() / sampleRate; + parameter->phase = parameter->phase + parameter->lfoRate->getValueUnnormalised() / sampleRate; if (parameter->phase > 1) { - parameter->phase -= 1; + parameter->phase = parameter->phase - 1; } return parameter->phase * 2 * std::numbers::pi; @@ -91,6 +91,16 @@ double Effect::getValue() { return getValue(0); } +// Not thread safe! Should only be called from the audio thread +double Effect::getActualValue(int index) { + return actualValues[index]; +} + +// Not thread safe! Should only be called from the audio thread +double Effect::getActualValue() { + return actualValues[0]; +} + void Effect::setValue(int index, double value) { parameters[index]->setUnnormalisedValueNotifyingHost(value); } @@ -137,7 +147,7 @@ void Effect::markEnableable(bool enable) { if (enabled != nullptr) { enabled->setValue(enable); } else { - enabled = new BooleanParameter(getName() + " Enabled", getId() + "Enabled", enable); + enabled = new BooleanParameter(getName() + " Enabled", getId() + "Enabled", parameters[0]->getVersionHint(), enable); } } @@ -150,14 +160,14 @@ juce::String Effect::getName() { } void Effect::save(juce::XmlElement* xml) { - if (enabled != nullptr) { - auto enabledXml = xml->createNewChildElement("enabled"); - enabled->save(enabledXml); - } - xml->setAttribute("id", getId()); - xml->setAttribute("precedence", precedence); - for (auto parameter : parameters) { - parameter->save(xml->createNewChildElement("parameter")); + if (enabled != nullptr) { + auto enabledXml = xml->createNewChildElement("enabled"); + enabled->save(enabledXml); + } + xml->setAttribute("id", getId()); + xml->setAttribute("precedence", precedence); + for (auto parameter : parameters) { + parameter->save(xml->createNewChildElement("parameter")); } } diff --git a/Source/audio/Effect.h b/Source/audio/Effect.h index 1658484..6bfbca6 100644 --- a/Source/audio/Effect.h +++ b/Source/audio/Effect.h @@ -18,6 +18,8 @@ public: void apply(); double getValue(int index); double getValue(); + double getActualValue(int index); + double getActualValue(); void setValue(int index, double value); void setValue(double value); int getPrecedence(); diff --git a/Source/audio/EffectApplication.cpp b/Source/audio/EffectApplication.cpp index 08de7b4..22afa1a 100644 --- a/Source/audio/EffectApplication.cpp +++ b/Source/audio/EffectApplication.cpp @@ -1,16 +1,14 @@ #include "EffectApplication.h" #include +#include "../MathUtil.h" void EffectApplication::resetPhase() { phase = 0.0; } double EffectApplication::nextPhase(double frequency, double sampleRate) { - phase += frequency / sampleRate; + phase += 2 * std::numbers::pi * frequency / sampleRate; + phase = MathUtil::wrapAngle(phase); - if (phase > 1) { - phase -= 1; - } - - return phase * 2 * std::numbers::pi; + return phase; } diff --git a/Source/audio/EffectParameter.h b/Source/audio/EffectParameter.h index 1712d9a..96dedd8 100644 --- a/Source/audio/EffectParameter.h +++ b/Source/audio/EffectParameter.h @@ -5,10 +5,14 @@ class FloatParameter : public juce::AudioProcessorParameterWithID { public: std::atomic min = 0.0; - std::atomic max = 1.0; - std::atomic step = 0.001; + std::atomic max = 0.0; + std::atomic step = 0.0; - FloatParameter(juce::String name, juce::String id, float value, float min, float max, float step = 0.001, juce::String label = "") : juce::AudioProcessorParameterWithID(id, name), value(value), min(min), max(max), step(step), label(label) {} + FloatParameter(juce::String name, juce::String id, int versionHint, float value, float min, float max, float step = 0.69, juce::String label = "") : juce::AudioProcessorParameterWithID(juce::ParameterID(id, versionHint), name), step(step), value(value), label(label) { + // need to initialise here because of naming conflicts on Windows + this->min = min; + this->max = max; + } juce::String getName(int maximumStringLength) const override { return name.substring(0, maximumStringLength); @@ -131,7 +135,11 @@ public: std::atomic min = 0; std::atomic max = 10; - IntParameter(juce::String name, juce::String id, int value, int min, int max) : AudioProcessorParameterWithID(id, name), value(value), min(min), max(max) {} + IntParameter(juce::String name, juce::String id, int versionHint, int value, int min, int max) : AudioProcessorParameterWithID(juce::ParameterID(id, versionHint), name), value(value) { + // need to initialise here because of naming conflicts on Windows + this->min = min; + this->max = max; + } juce::String getName(int maximumStringLength) const override { return name.substring(0, maximumStringLength); @@ -237,7 +245,7 @@ enum class LfoType : int { class LfoTypeParameter : public IntParameter { public: - LfoTypeParameter(juce::String name, juce::String id, int value) : IntParameter(name, id, value, 1, 8) {} + LfoTypeParameter(juce::String name, juce::String id, int versionHint, int value) : IntParameter(name, id, versionHint, value, 1, 8) {} juce::String getText(float value, int maximumStringLength) const override { switch ((LfoType)(int)getUnnormalisedValue(value)) { @@ -298,8 +306,8 @@ public: class EffectParameter : public FloatParameter { public: std::atomic smoothValueChange = true; - LfoTypeParameter* lfo = new LfoTypeParameter(name + " LFO", paramID + "Lfo", 1); - FloatParameter* lfoRate = new FloatParameter(name + " LFO Rate", paramID + "LfoRate", 1.0f, 0.0f, 100.0f, 0.1f, "Hz"); + 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"); std::atomic phase = 0.0f; std::vector getParameters() { @@ -341,5 +349,5 @@ public: } } - EffectParameter(juce::String name, juce::String id, float value, float min, float max, float step = 0.001, bool smoothValueChange = true) : FloatParameter(name, id, value, min, max, step), smoothValueChange(smoothValueChange) {} -}; \ No newline at end of file + EffectParameter(juce::String name, juce::String id, int versionHint, float value, float min, float max, float step = 0.01, bool smoothValueChange = true) : FloatParameter(name, id, versionHint, value, min, max, step), smoothValueChange(smoothValueChange) {} +}; diff --git a/Source/audio/PerspectiveEffect.cpp b/Source/audio/PerspectiveEffect.cpp index d5f50d2..1a56a12 100644 --- a/Source/audio/PerspectiveEffect.cpp +++ b/Source/audio/PerspectiveEffect.cpp @@ -1,7 +1,12 @@ #include "PerspectiveEffect.h" #include +#include "../MathUtil.h" -PerspectiveEffect::PerspectiveEffect() {} +PerspectiveEffect::PerspectiveEffect(int versionHint) : versionHint(versionHint) { + 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); +} Vector2 PerspectiveEffect::apply(int index, Vector2 input, const std::vector& values, double sampleRate) { auto effectScale = values[0]; @@ -27,19 +32,9 @@ Vector2 PerspectiveEffect::apply(int index, Vector2 input, const std::vector std::numbers::pi * 8) { - currentRotateX -= std::numbers::pi * 8; - } - if (currentRotateY > std::numbers::pi * 8) { - currentRotateY -= std::numbers::pi * 8; - } - if (currentRotateZ > std::numbers::pi * 8) { - currentRotateZ -= std::numbers::pi * 8; - } + 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; diff --git a/Source/audio/PerspectiveEffect.h b/Source/audio/PerspectiveEffect.h index 6f8db4b..08f819b 100644 --- a/Source/audio/PerspectiveEffect.h +++ b/Source/audio/PerspectiveEffect.h @@ -6,15 +6,15 @@ class PerspectiveEffect : public EffectApplication { public: - PerspectiveEffect(); + PerspectiveEffect(int versionHint); Vector2 apply(int index, Vector2 input, const std::vector& values, double sampleRate) override; void updateCode(const juce::String& newCode); juce::String getCode(); - BooleanParameter* fixedRotateX = new BooleanParameter("Perspective Fixed Rotate X", "perspectiveFixedRotateX", false); - BooleanParameter* fixedRotateY = new BooleanParameter("Perspective Fixed Rotate Y", "perspectiveFixedRotateY", false); - BooleanParameter* fixedRotateZ = new BooleanParameter("Perspective Fixed Rotate Z", "perspectiveFixedRotateZ", false); + BooleanParameter* fixedRotateX; + BooleanParameter* fixedRotateY; + BooleanParameter* fixedRotateZ; private: const juce::String DEFAULT_SCRIPT = "return { x, y, z }"; juce::String code = DEFAULT_SCRIPT; @@ -25,8 +25,14 @@ private: float currentRotateX = 0; float currentRotateY = 0; float currentRotateZ = 0; + + int versionHint; - float linearSpeedToActualSpeed(float rotateSpeed) { - return (std::exp(3 * juce::jmin(10.0f, std::abs(rotateSpeed))) - 1) / 50000.0; - } -}; \ No newline at end of file + double linearSpeedToActualSpeed(double rotateSpeed) { + double actualSpeed = (std::exp(3 * std::min(10.0, std::abs(rotateSpeed))) - 1) / 50000; + if (rotateSpeed < 0) { + actualSpeed *= -1; + } + return actualSpeed; + } +}; diff --git a/Source/audio/PitchDetector.cpp b/Source/audio/PitchDetector.cpp index b961447..c836ee9 100644 --- a/Source/audio/PitchDetector.cpp +++ b/Source/audio/PitchDetector.cpp @@ -1,24 +1,23 @@ #include "PitchDetector.h" -#include "PitchDetector.h" +#include "../PluginProcessor.h" -PitchDetector::PitchDetector(BufferProducer& producer) : juce::Thread("PitchDetector"), producer(producer) { +PitchDetector::PitchDetector(OscirenderAudioProcessor& audioProcessor) : juce::Thread("PitchDetector"), audioProcessor(audioProcessor) { startThread(); } PitchDetector::~PitchDetector() { - producer.unregisterConsumer(consumer); + audioProcessor.consumerStop(consumer); stopThread(1000); } void PitchDetector::run() { - producer.registerConsumer(consumer); - while (!threadShouldExit()) { - auto buffer = consumer->startProcessing(); + consumer = audioProcessor.consumerRegister(buffer); + audioProcessor.consumerRead(consumer); // buffer is for 2 channels, so we need to only use one for (int i = 0; i < fftSize; i++) { - fftData[i] = buffer->at(2 * i); + fftData[i] = buffer[2 * i]; } forwardFFT.performFrequencyOnlyForwardTransform(fftData.data()); @@ -37,8 +36,6 @@ void PitchDetector::run() { } frequency = frequencyFromIndex(maxIndex); - - consumer->finishedProcessing(); triggerAsyncUpdate(); } } diff --git a/Source/audio/PitchDetector.h b/Source/audio/PitchDetector.h index 04a51ae..94f683e 100644 --- a/Source/audio/PitchDetector.h +++ b/Source/audio/PitchDetector.h @@ -1,12 +1,11 @@ #pragma once #include #include "../concurrency/BufferConsumer.h" -#include "../concurrency/BufferProducer.h" - +class OscirenderAudioProcessor; class PitchDetector : public juce::Thread, public juce::AsyncUpdater { public: - PitchDetector(BufferProducer& producer); + PitchDetector(OscirenderAudioProcessor& audioProcessor); ~PitchDetector(); void run() override; @@ -21,10 +20,11 @@ private: static constexpr int fftOrder = 15; static constexpr int fftSize = 1 << fftOrder; - std::shared_ptr consumer = std::make_shared(fftSize); + std::shared_ptr consumer; + std::vector buffer = std::vector(2 * fftSize); juce::dsp::FFT forwardFFT{fftOrder}; std::array fftData; - BufferProducer& producer; + OscirenderAudioProcessor& audioProcessor; std::vector> callbacks; juce::SpinLock lock; float sampleRate = 192000.0f; @@ -32,4 +32,4 @@ private: float frequencyFromIndex(int index); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PitchDetector) -}; \ No newline at end of file +}; diff --git a/Source/audio/ShapeSound.cpp b/Source/audio/ShapeSound.cpp new file mode 100644 index 0000000..5ad6db9 --- /dev/null +++ b/Source/audio/ShapeSound.cpp @@ -0,0 +1,54 @@ +#include "ShapeSound.h" + +ShapeSound::ShapeSound(std::shared_ptr parser) : parser(parser) { + if (parser->isSample()) { + producer = std::make_unique(*this, std::make_shared()); + } else { + producer = std::make_unique(*this, parser); + } + producer->startThread(); +} + +bool ShapeSound::appliesToNote(int note) { + return true; +} + +bool ShapeSound::appliesToChannel(int channel) { + return true; +} + +void ShapeSound::addFrame(std::vector>& frame) { + const auto scope = frameFifo.write(1); + + if (scope.blockSize1 > 0) { + frameBuffer[scope.startIndex1].clear(); + for (auto& shape : frame) { + frameBuffer[scope.startIndex1].push_back(std::move(shape)); + } + } + + if (scope.blockSize2 > 0) { + frameBuffer[scope.startIndex2].clear(); + for (auto& shape : frame) { + frameBuffer[scope.startIndex2].push_back(std::move(shape)); + } + } +} + +double ShapeSound::updateFrame(std::vector>& frame) { + if (frameFifo.getNumReady() > 0) { + { + const auto scope = frameFifo.read(1); + + if (scope.blockSize1 > 0) { + frame.swap(frameBuffer[scope.startIndex1]); + } else if (scope.blockSize2 > 0) { + frame.swap(frameBuffer[scope.startIndex2]); + } + + frameLength = Shape::totalLength(frame); + } + } + + return frameLength; +} diff --git a/Source/audio/ShapeSound.h b/Source/audio/ShapeSound.h new file mode 100644 index 0000000..bdafebc --- /dev/null +++ b/Source/audio/ShapeSound.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include "../parser/FileParser.h" +#include "../parser/FrameProducer.h" +#include "../parser/FrameConsumer.h" + +class ShapeSound : public juce::SynthesiserSound, public FrameConsumer { +public: + ShapeSound(std::shared_ptr parser); + + bool appliesToNote(int note) override; + bool appliesToChannel(int channel) override; + void addFrame(std::vector>& frame) override; + double updateFrame(std::vector>& frame); + + std::shared_ptr parser; + + using Ptr = juce::ReferenceCountedObjectPtr; + +private: + + juce::AbstractFifo frameFifo{ 10 }; + std::vector> frameBuffer[10]; + std::unique_ptr producer; + double frameLength = 0.0; +}; \ No newline at end of file diff --git a/Source/audio/ShapeVoice.cpp b/Source/audio/ShapeVoice.cpp new file mode 100644 index 0000000..b884ed0 --- /dev/null +++ b/Source/audio/ShapeVoice.cpp @@ -0,0 +1,168 @@ +#include "ShapeVoice.h" +#include "../PluginProcessor.h" + +ShapeVoice::ShapeVoice(OscirenderAudioProcessor& p) : audioProcessor(p) { + actualTraceMin = audioProcessor.traceMin->getValue(); + actualTraceMax = audioProcessor.traceMax->getValue(); +} + +bool ShapeVoice::canPlaySound(juce::SynthesiserSound* sound) { + return dynamic_cast (sound) != nullptr; +} + +void ShapeVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition) { + auto* shapeSound = dynamic_cast(sound); + + currentlyPlaying = true; + this->sound = shapeSound; + if (shapeSound != nullptr) { + int tries = 0; + while (frame.empty() && tries < 50) { + frameLength = shapeSound->updateFrame(frame); + tries++; + } + tailOff = 0.0; + if (audioProcessor.midiEnabled->getBoolValue()) { + frequency = juce::MidiMessage::getMidiNoteInHertz(midiNoteNumber); + } + } +} + +// TODO this is the slowest part of the program - any way to improve this would help! +void ShapeVoice::incrementShapeDrawing() { + double length = currentShape < frame.size() ? frame[currentShape]->len : 0.0; + // hard cap on how many times it can be over the length to + // prevent audio stuttering + auto increment = juce::jmin(lengthIncrement, 20 * length); + frameDrawn += increment; + shapeDrawn += increment; + + // Need to skip all shapes that the lengthIncrement draws over. + // This is especially an issue when there are lots of small lines being + // drawn. + while (shapeDrawn > length) { + shapeDrawn -= length; + currentShape++; + if (currentShape >= frame.size()) { + currentShape = 0; + break; + } + // POTENTIAL TODO: Think of a way to make this more efficient when iterating + // this loop many times + length = frame[currentShape]->len; + } +} + +// should be called if the current file is changed so that we interrupt +// any currently playing sounds / voices +void ShapeVoice::updateSound(juce::SynthesiserSound* sound) { + if (currentlyPlaying) { + this->sound = dynamic_cast(sound); + } +} + +void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int startSample, int numSamples) { + juce::ScopedNoDenormals noDenormals; + + int numChannels = outputBuffer.getNumChannels(); + + if (!audioProcessor.midiEnabled->getBoolValue()) { + frequency = audioProcessor.frequency; + } + + for (auto sample = startSample; sample < startSample + numSamples; ++sample) { + bool traceMinEnabled = audioProcessor.traceMin->enabled->getBoolValue(); + bool traceMaxEnabled = audioProcessor.traceMax->enabled->getBoolValue(); + + // update length increment + double traceMax = traceMaxEnabled ? actualTraceMax : 1.0; + double traceMin = traceMinEnabled ? actualTraceMin : 0.0; + double proportionalLength = (traceMax - traceMin) * frameLength; + // double frequency = audioProcessor.frequencyEffect->getActualValue(); + lengthIncrement = juce::jmax(proportionalLength / (audioProcessor.currentSampleRate / frequency), MIN_LENGTH_INCREMENT); + + Vector2 channels; + double x = 0.0; + double y = 0.0; + + bool renderingSample = true; + + if (sound.load() != nullptr) { + renderingSample = sound.load()->parser->isSample(); + + if (renderingSample) { + channels = sound.load()->parser->nextSample(); + } else if (currentShape < frame.size()) { + auto& shape = frame[currentShape]; + double length = shape->length(); + double drawingProgress = length == 0.0 ? 1 : shapeDrawn / length; + channels = shape->nextVector(drawingProgress); + } + } + + x = channels.x; + y = channels.y; + + if (tailOff > 0.0) { + tailOff *= 0.99999; + + if (tailOff < 0.005) { + clearCurrentNote(); + sound = nullptr; + break; + } + } + + double gain = tailOff == 0.0 ? 1.0 : tailOff; + + if (numChannels >= 2) { + outputBuffer.addSample(0, sample, x * gain); + outputBuffer.addSample(1, sample, y * gain); + } else if (numChannels == 1) { + outputBuffer.addSample(0, sample, x * gain); + } + + double traceMinValue = audioProcessor.traceMin->getActualValue(); + double traceMaxValue = audioProcessor.traceMax->getActualValue(); + actualTraceMax = juce::jmax(actualTraceMin + MIN_TRACE, juce::jmin(traceMaxValue, 1.0)); + actualTraceMin = juce::jmax(MIN_TRACE, juce::jmin(traceMinValue, actualTraceMax - MIN_TRACE)); + + if (!renderingSample) { + incrementShapeDrawing(); + } + + double drawnFrameLength = traceMaxEnabled ? actualTraceMax * frameLength : frameLength; + + if (!renderingSample && frameDrawn >= drawnFrameLength) { + if (sound.load() != nullptr) { + frameLength = sound.load()->updateFrame(frame); + } + // TODO: updateFrame already iterates over all the shapes, + // so we can improve performance by calculating frameDrawn + // and shapeDrawn directly. frameDrawn is simply actualTraceMin * frameLength + // but shapeDrawn is the amount of the current shape that has been drawn so + // we need to iterate over all the shapes to calculate it. + if (traceMinEnabled) { + while (frameDrawn < actualTraceMin * frameLength) { + incrementShapeDrawing(); + } + } + } + } +} + +void ShapeVoice::stopNote(float velocity, bool allowTailOff) { + currentlyPlaying = false; + if (allowTailOff) { + if (tailOff == 0.0) { + tailOff = 1.0; + } + } else { + clearCurrentNote(); + sound = nullptr; + } +} + +void ShapeVoice::pitchWheelMoved(int newPitchWheelValue) {} + +void ShapeVoice::controllerMoved(int controllerNumber, int newControllerValue) {} diff --git a/Source/audio/ShapeVoice.h b/Source/audio/ShapeVoice.h new file mode 100644 index 0000000..d20986f --- /dev/null +++ b/Source/audio/ShapeVoice.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include "ShapeSound.h" + +class OscirenderAudioProcessor; +class ShapeVoice : public juce::SynthesiserVoice { +public: + ShapeVoice(OscirenderAudioProcessor& p); + + bool canPlaySound(juce::SynthesiserSound* sound) override; + void startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition) override; + void updateSound(juce::SynthesiserSound* sound); + void renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int startSample, int numSamples) override; + void stopNote(float velocity, bool allowTailOff) override; + void pitchWheelMoved(int newPitchWheelValue) override; + void controllerMoved(int controllerNumber, int newControllerValue) override; + + void incrementShapeDrawing(); + +private: + const double MIN_TRACE = 0.005; + const double MIN_LENGTH_INCREMENT = 0.000001; + + OscirenderAudioProcessor& audioProcessor; + std::vector> frame; + std::atomic sound = nullptr; + double actualTraceMin; + double actualTraceMax; + + double frameLength = 0.0; + int currentShape = 0; + double shapeDrawn = 0.0; + double frameDrawn = 0.0; + double lengthIncrement = 0.0; + + bool currentlyPlaying = false; + double tailOff = 0.0; + double frequency = 1.0; +}; diff --git a/Source/chinese_postman/Graph.cpp b/Source/chinese_postman/Graph.cpp index 50e9044..d2b4fe4 100644 --- a/Source/chinese_postman/Graph.cpp +++ b/Source/chinese_postman/Graph.cpp @@ -12,8 +12,10 @@ Graph::Graph(int n, list< pair > & edges): { int u = (*it).first; int v = (*it).second; - - AddEdge(u, v); + + if (v < n && u < n) { + AddEdge(u, v); + } } } diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index 86b26d5..318ba2c 100644 --- a/Source/components/EffectComponent.cpp +++ b/Source/components/EffectComponent.cpp @@ -116,16 +116,16 @@ EffectComponent::~EffectComponent() { effect.removeListener(index, this); } -void EffectComponent::resized() { - auto bounds = getLocalBounds(); - auto componentBounds = bounds.removeFromRight(25); - if (component != nullptr) { - component->setBounds(componentBounds); - } - - if (lfoEnabled) { - lfo.setBounds(bounds.removeFromRight(100).reduced(5)); - } +void EffectComponent::resized() { + auto bounds = getLocalBounds(); + auto componentBounds = bounds.removeFromRight(25); + if (component != nullptr) { + component->setBounds(componentBounds); + } + + if (lfoEnabled) { + lfo.setBounds(bounds.removeFromRight(100).reduced(5)); + } auto checkboxLabel = bounds.removeFromLeft(120); diff --git a/Source/components/LuaListComponent.cpp b/Source/components/LuaListComponent.cpp index 85995a5..52a9b17 100644 --- a/Source/components/LuaListComponent.cpp +++ b/Source/components/LuaListComponent.cpp @@ -27,7 +27,8 @@ int LuaListBoxModel::getNumRows() { void LuaListBoxModel::paintListBoxItem(int rowNumber, juce::Graphics& g, int width, int height, bool rowIsSelected) {} juce::Component* LuaListBoxModel::refreshComponentForRow(int rowNum, bool isRowSelected, juce::Component *existingComponentToUpdate) { - juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + // TODO: We should REALLY be locking here but it causes a deadlock :( works fine without..... + // juce::SpinLock::ScopedLockType lock1(audioProcessor.effectsLock); std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); if (juce::isPositiveAndBelow(rowNum, getNumRows())) { item = std::make_unique(audioProcessor, *audioProcessor.luaEffects[rowNum]); diff --git a/Source/components/VisualiserComponent.cpp b/Source/components/VisualiserComponent.cpp index 44b1ba4..add9a15 100644 --- a/Source/components/VisualiserComponent.cpp +++ b/Source/components/VisualiserComponent.cpp @@ -7,7 +7,7 @@ VisualiserComponent::VisualiserComponent(int numChannels, OscirenderAudioProcess } VisualiserComponent::~VisualiserComponent() { - audioProcessor.audioProducer.unregisterConsumer(consumer); + audioProcessor.consumerStop(consumer); stopThread(1000); } @@ -44,12 +44,10 @@ void VisualiserComponent::timerCallback() { } void VisualiserComponent::run() { - audioProcessor.audioProducer.registerConsumer(consumer); - while (!threadShouldExit()) { - auto buffer = consumer->startProcessing(); - setBuffer(*buffer); - consumer->finishedProcessing(); + consumer = audioProcessor.consumerRegister(tempBuffer); + audioProcessor.consumerRead(consumer); + setBuffer(tempBuffer); } } diff --git a/Source/components/VisualiserComponent.h b/Source/components/VisualiserComponent.h index e6533ff..1493b8f 100644 --- a/Source/components/VisualiserComponent.h +++ b/Source/components/VisualiserComponent.h @@ -23,8 +23,10 @@ private: int numChannels = 2; juce::Colour backgroundColour, waveformColour; OscirenderAudioProcessor& audioProcessor; - std::shared_ptr consumer = std::make_shared(4096); + std::vector tempBuffer = std::vector(2 * 4096); int precision = 4; + + std::shared_ptr consumer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VisualiserComponent) -}; \ No newline at end of file +}; diff --git a/Source/components/VolumeComponent.cpp b/Source/components/VolumeComponent.cpp index 3b7d151..9189519 100644 --- a/Source/components/VolumeComponent.cpp +++ b/Source/components/VolumeComponent.cpp @@ -47,7 +47,7 @@ VolumeComponent::VolumeComponent(OscirenderAudioProcessor& p) : audioProcessor(p } VolumeComponent::~VolumeComponent() { - audioProcessor.audioProducer.unregisterConsumer(consumer); + audioProcessor.consumerStop(consumer); stopThread(1000); } @@ -92,29 +92,26 @@ void VolumeComponent::timerCallback() { } void VolumeComponent::run() { - audioProcessor.audioProducer.registerConsumer(consumer); - while (!threadShouldExit()) { - auto buffer = consumer->startProcessing(); + consumer = audioProcessor.consumerRegister(buffer); + audioProcessor.consumerRead(consumer); float leftVolume = 0; float rightVolume = 0; - for (int i = 0; i < buffer->size(); i += 2) { - leftVolume += buffer->at(i) * buffer->at(i); - rightVolume += buffer->at(i + 1) * buffer->at(i + 1); + for (int i = 0; i < buffer.size(); i += 2) { + leftVolume += buffer[i] * buffer[i]; + rightVolume += buffer[i + 1] * buffer[i + 1]; } // RMS - leftVolume = std::sqrt(leftVolume / (buffer->size() / 2)); - rightVolume = std::sqrt(rightVolume / (buffer->size() / 2)); + leftVolume = std::sqrt(leftVolume / (buffer.size() / 2)); + rightVolume = std::sqrt(rightVolume / (buffer.size() / 2)); this->leftVolume = leftVolume; this->rightVolume = rightVolume; avgLeftVolume = (avgLeftVolume * 0.95) + (leftVolume * 0.05); avgRightVolume = (avgRightVolume * 0.95) + (rightVolume * 0.05); - - consumer->finishedProcessing(); } } diff --git a/Source/components/VolumeComponent.h b/Source/components/VolumeComponent.h index 6cb2c6e..291860a 100644 --- a/Source/components/VolumeComponent.h +++ b/Source/components/VolumeComponent.h @@ -1,9 +1,9 @@ #pragma once #include -#include "../concurrency/BufferConsumer.h" #include "../PluginProcessor.h" #include "../LookAndFeel.h" +#include "../concurrency/BufferConsumer.h" class ThumbRadiusLookAndFeel : public OscirenderLookAndFeel { public: @@ -71,7 +71,7 @@ public: private: OscirenderAudioProcessor& audioProcessor; - std::shared_ptr consumer = std::make_shared(1 << 11); + std::vector buffer = std::vector(2 * 1 << 11); std::atomic leftVolume = 0; std::atomic rightVolume = 0; @@ -85,6 +85,8 @@ private: std::unique_ptr volumeIcon; std::unique_ptr thresholdIcon; + + std::shared_ptr consumer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VolumeComponent) -}; \ No newline at end of file +}; diff --git a/Source/concurrency/BufferConsumer.h b/Source/concurrency/BufferConsumer.h index 3988be1..bfa5943 100644 --- a/Source/concurrency/BufferConsumer.h +++ b/Source/concurrency/BufferConsumer.h @@ -1,123 +1,74 @@ #pragma once #include +#include +#include -// This is a helper class for the producer and consumer threads. -// -// ORDER OF OPERATIONS: -// 1. Consumer is created. -// 2. The thread that owns the consumers calls registerConsumer() on the producer, which acquires the lock on the first buffer to be written to by calling getBuffer(). -// LOOP: -// 3. The consumer calls startProcessing() to signal that they want to start processing the current buffer. -// 4. The producer calls finishedWriting() to signal that they have finished writing to the current buffer, which gives the lock to the consumer. -// 5. The consumer calls finishedProcessing() to signal that they have finished processing the current buffer. -// 6. The producer calls getBuffer() to acquire the lock on the next buffer to be written to. -// GOTO LOOP -// 7. The thread that owns the consumer calls unregisterConsumer() on the producer at some point during the loop, which releases the lock on the current buffer. -// -class BufferConsumer { +// FROM https://gist.github.com/Kuxe/6bdd5b748b5f11b303a5cccbb8c8a731 +/** General semaphore with N permissions **/ +class Semaphore { + const size_t num_permissions; + size_t avail; + std::mutex m; + std::condition_variable cv; public: - BufferConsumer(int bufferSize) { - firstBuffer->resize(2 * bufferSize, 0.0); - secondBuffer->resize(2 * bufferSize, 0.0); + /** Default constructor. Default semaphore is a binary semaphore **/ + explicit Semaphore(const size_t& num_permissions = 1) : num_permissions(num_permissions), avail(num_permissions) { } + + /** Copy constructor. Does not copy state of mutex or condition variable, + only the number of permissions and number of available permissions **/ + Semaphore(const Semaphore& s) : num_permissions(s.num_permissions), avail(s.avail) { } + + void acquire() { + std::unique_lock lk(m); + cv.wait(lk, [this] { return avail > 0; }); + avail--; + lk.unlock(); } - ~BufferConsumer() {} - - // Returns the buffer that is ready to be written to. - // This is only called by the producer thread. - // force forces the lock to be acquired. - // Returns nullptr if the lock can't be acquired when force is false. - // It is only called when the global producer lock is held. - std::shared_ptr> getBuffer(bool force) { - auto buffer = firstBufferWriting ? firstBuffer : secondBuffer; - if (lockHeldForWriting) { - return buffer; - } - auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; - - if (force) { - bufferLock->enter(); - lockHeldForWriting = true; - return buffer; - } else if (bufferLock->tryEnter()) { - lockHeldForWriting = true; - return buffer; - } else { - return nullptr; - } + void release() { + m.lock(); + avail++; + m.unlock(); + cv.notify_one(); } - // This is only called by the producer thread. It is only called when the global - // producer lock is held. - void finishedWriting() { - auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; - lockHeldForWriting = false; - firstBufferWriting = !firstBufferWriting; - // Try locking before we unlock the current buffer so that - // the consumer doesn't start processing before we - // unlock the buffer. Ignore if we can't get the lock - // because the consumer is still processing. - getBuffer(false); - bufferLock->exit(); + size_t available() const { + return avail; } - - void releaseLock() { - if (lockHeldForWriting) { - auto bufferLock = firstBufferWriting ? firstBufferLock : secondBufferLock; - bufferLock->exit(); - } - } - - // Returns the buffer that has been written to fully and is ready to be processed. - // This will lock the buffer so that the producer can't write to it while we're processing. - std::shared_ptr> startProcessing() { - auto buffer = firstBufferProcessing ? firstBuffer : secondBuffer; - auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock; - - bufferLock->enter(); - return buffer; - } - - // This should be called after processing has finished. - // It releases the lock on the buffer so that the producer can write to it again. - void finishedProcessing() { - auto bufferLock = firstBufferProcessing ? firstBufferLock : secondBufferLock; - firstBufferProcessing = !firstBufferProcessing; - bufferLock->exit(); - } - - std::shared_ptr> firstBuffer = std::make_shared>(); - std::shared_ptr> secondBuffer = std::make_shared>(); - std::shared_ptr firstBufferLock = std::make_shared(); - std::shared_ptr secondBufferLock = std::make_shared(); -private: - // Indirectly used by the producer to signal whether it holds the lock on the buffer. - // This is accurate if the global producer lock is held as the buffer lock is acquired - // and this is set to true before the global producer lock is released. - bool lockHeldForWriting = false; - bool firstBufferWriting = true; - bool firstBufferProcessing = true; }; -class DummyConsumer : public juce::Thread { + +class BufferConsumer { public: - DummyConsumer(std::shared_ptr consumer) : juce::Thread("DummyConsumer"), consumer(consumer) {} - ~DummyConsumer() {} + BufferConsumer(std::vector& buffer) : buffer(buffer) {} - void run() override { - while (!threadShouldExit()) { - auto buffer = consumer->startProcessing(); + ~BufferConsumer() {} + + void waitUntilFull() { + sema.acquire(); + } - float total = 0.0; - for (int i = 0; i < buffer->size(); i++) { - total += (*buffer)[i]; - } - DBG(total); - - consumer->finishedProcessing(); + void notifyIfFull() { + if (offset >= buffer.size()) { + sema.release(); } } + + // to be used when the audio thread is being destroyed to + // make sure that everything waiting on it stops waiting. + void forceNotify() { + sema.release(); + } + + void write(double d) { + if (offset < buffer.size()) { + buffer[offset++] = d; + } + } + private: - std::shared_ptr consumer; -}; \ No newline at end of file + std::vector& buffer; + Semaphore sema{0}; + int offset = 0; +}; diff --git a/Source/concurrency/BufferProducer.h b/Source/concurrency/BufferProducer.h deleted file mode 100644 index a8f1719..0000000 --- a/Source/concurrency/BufferProducer.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include "BufferConsumer.h" - -class BufferProducer { -public: - BufferProducer() {} - ~BufferProducer() {} - - // This should add the buffers and locks to the vectors - // and then lock the first buffer lock so it can start - // being written to. - // This is only called by the thread that owns the consumer thread. - void registerConsumer(std::shared_ptr consumer) { - juce::CriticalSection::ScopedLockType l(lock); - consumers.push_back(consumer); - bufferPositions.push_back(0); - consumer->getBuffer(true); - } - - // This is only called by the thread that owns the consumer thread. - // This can't happen at the same time as write() it locks the producer lock. - void unregisterConsumer(std::shared_ptr consumer) { - juce::CriticalSection::ScopedLockType l(lock); - for (int i = 0; i < consumers.size(); i++) { - if (consumers[i] == consumer) { - consumer->releaseLock(); - consumers.erase(consumers.begin() + i); - bufferPositions.erase(bufferPositions.begin() + i); - break; - } - } - } - - // Writes a sample to the current buffer for all consumers. - void write(float left, float right) { - juce::CriticalSection::ScopedLockType l(lock); - for (int i = 0; i < consumers.size(); i++) { - std::shared_ptr> buffer = consumers[i]->getBuffer(false); - if (buffer == nullptr) { - continue; - } - - (*buffer)[bufferPositions[i]] = left; - (*buffer)[bufferPositions[i] + 1] = right; - bufferPositions[i] += 2; - - // If we've reached the end of the buffer, switch - // to the other buffer and unlock it. This signals - // to the consumer that it can start processing! - if (bufferPositions[i] >= buffer->size()) { - bufferPositions[i] = 0; - consumers[i]->finishedWriting(); - } - } - } - -private: - juce::CriticalSection lock; - std::vector> consumers; - std::vector bufferPositions; -}; \ No newline at end of file diff --git a/Source/obj/Camera.cpp b/Source/obj/Camera.cpp index cb40857..2153938 100644 --- a/Source/obj/Camera.cpp +++ b/Source/obj/Camera.cpp @@ -47,8 +47,8 @@ std::vector Camera::sampleVerticesInRender(WorldObject& object) { double z = object.vs[j * 3 + 2]; vertices.push_back(project(object.rotateX, object.rotateY, object.rotateZ, x, y, z)); } - object.rotateY += rotation; - object.rotateZ += rotation; + object.rotateY = object.rotateY + rotation; + object.rotateZ = object.rotateY + rotation; } return vertices; diff --git a/Source/obj/WorldObject.cpp b/Source/obj/WorldObject.cpp index 3981a18..6ca2b7c 100644 --- a/Source/obj/WorldObject.cpp +++ b/Source/obj/WorldObject.cpp @@ -1,6 +1,7 @@ #include "WorldObject.h" #include "../chinese_postman/ChinesePostman.h" #include "tiny_obj_loader.h" +#include "../MathUtil.h" struct pair_hash { inline std::size_t operator()(const std::pair& v) const { @@ -238,9 +239,9 @@ void WorldObject::setRotationSpeed(double rotateSpeed) { // called whenever a new frame is drawn, so that the object can update its // rotation void WorldObject::nextFrame() { - currentRotateX += baseRotateX * rotateSpeed; - currentRotateY += baseRotateY * rotateSpeed; - currentRotateZ += baseRotateZ * rotateSpeed; + currentRotateX = MathUtil::wrapAngle(currentRotateX + baseRotateX * rotateSpeed); + currentRotateY = MathUtil::wrapAngle(currentRotateY + baseRotateY * rotateSpeed); + currentRotateZ = MathUtil::wrapAngle(currentRotateZ + baseRotateZ * rotateSpeed); rotateX = baseRotateX + currentRotateX; rotateY = baseRotateY + currentRotateY; rotateZ = baseRotateZ + currentRotateZ; diff --git a/Source/obj/WorldObject.h b/Source/obj/WorldObject.h index e41a9c0..058f6e3 100644 --- a/Source/obj/WorldObject.h +++ b/Source/obj/WorldObject.h @@ -26,4 +26,4 @@ private: std::atomic baseRotateX = 0.0, baseRotateY = 0.0, baseRotateZ = 0.0; std::atomic currentRotateX = 0.0, currentRotateY = 0.0, currentRotateZ = 0.0; std::atomic rotateSpeed; -}; \ No newline at end of file +}; diff --git a/Source/parser/FileParser.h b/Source/parser/FileParser.h index 9d87668..102abf0 100644 --- a/Source/parser/FileParser.h +++ b/Source/parser/FileParser.h @@ -37,4 +37,4 @@ private: std::shared_ptr svg; std::shared_ptr text; std::shared_ptr lua; -}; \ No newline at end of file +}; diff --git a/Source/parser/FrameConsumer.h b/Source/parser/FrameConsumer.h index 5ba4907..b1ab19e 100644 --- a/Source/parser/FrameConsumer.h +++ b/Source/parser/FrameConsumer.h @@ -6,5 +6,5 @@ class FrameConsumer { public: - virtual void addFrame(std::vector> frame, int fileIndex) = 0; + virtual void addFrame(std::vector>& frame) = 0; }; \ No newline at end of file diff --git a/Source/parser/FrameProducer.cpp b/Source/parser/FrameProducer.cpp index 38828bb..755fe0f 100644 --- a/Source/parser/FrameProducer.cpp +++ b/Source/parser/FrameProducer.cpp @@ -9,15 +9,7 @@ FrameProducer::~FrameProducer() { void FrameProducer::run() { while (!threadShouldExit()) { - // this lock is needed so that frameSource isn't deleted whilst nextFrame() is being called - juce::SpinLock::ScopedLockType scope(lock); - frameConsumer.addFrame(frameSource->nextFrame(), sourceFileIndex); + auto frame = frameSource->nextFrame(); + frameConsumer.addFrame(frame); } } - -void FrameProducer::setSource(std::shared_ptr source, int fileIndex) { - juce::SpinLock::ScopedLockType scope(lock); - frameSource->disable(); - frameSource = source; - sourceFileIndex = fileIndex; -} diff --git a/Source/parser/FrameProducer.h b/Source/parser/FrameProducer.h index 0057830..c9e9743 100644 --- a/Source/parser/FrameProducer.h +++ b/Source/parser/FrameProducer.h @@ -10,10 +10,8 @@ public: ~FrameProducer() override; void run() override; - void setSource(std::shared_ptr, int fileIndex); private: juce::SpinLock lock; FrameConsumer& frameConsumer; std::shared_ptr frameSource; - int sourceFileIndex = -1; }; \ No newline at end of file diff --git a/Source/shape/CircleArc.cpp b/Source/shape/CircleArc.cpp index 9303a66..420c25a 100644 --- a/Source/shape/CircleArc.cpp +++ b/Source/shape/CircleArc.cpp @@ -56,11 +56,11 @@ void CircleArc::translate(double x, double y) { double CircleArc::length() { if (len < 0) { len = 0; - double angle = startAngle; - double step = (endAngle - startAngle) / 500; - for (int i = 0; i < 500; i++) { - Vector2 v1 = nextVector(i / 500.0); - Vector2 v2 = nextVector((i + 1) / 500.0); + // TODO: Replace this, it's stupid. Do a real approximation. + int segments = 5; + for (int i = 0; i < segments; i++) { + Vector2 v1 = nextVector(i / (double) segments); + Vector2 v2 = nextVector((i + 1) / (double) segments); len += Line(v1.x, v1.y, v2.x, v2.y).length(); } } diff --git a/Source/shape/Vector2.cpp b/Source/shape/Vector2.cpp index 0aa0273..62b0f8a 100644 --- a/Source/shape/Vector2.cpp +++ b/Source/shape/Vector2.cpp @@ -45,7 +45,7 @@ double Vector2::magnitude() { } std::unique_ptr Vector2::clone() { - return std::unique_ptr(); + return std::make_unique(x, y); } std::string Vector2::type() { diff --git a/Source/svg/SvgParser.cpp b/Source/svg/SvgParser.cpp index 8c5ac4a..a49ef18 100644 --- a/Source/svg/SvgParser.cpp +++ b/Source/svg/SvgParser.cpp @@ -5,17 +5,26 @@ SvgParser::SvgParser(juce::String svgFile) { auto doc = juce::XmlDocument::parse(svgFile); - std::unique_ptr svg = juce::Drawable::createFromSVG(*doc); - juce::DrawableComposite* composite = dynamic_cast(svg.get()); - auto contentArea = composite->getContentArea(); - auto path = svg->getOutlineAsPath(); - // apply transform to path to get the content area in the bounds -1 to 1 - path.applyTransform(juce::AffineTransform::translation(-contentArea.getX(), -contentArea.getY())); - path.applyTransform(juce::AffineTransform::scale(2 / contentArea.getWidth(), 2 / contentArea.getHeight())); - path.applyTransform(juce::AffineTransform::translation(-1, -1)); + if (doc != nullptr) { + std::unique_ptr svg = juce::Drawable::createFromSVG(*doc); + juce::DrawableComposite* composite = dynamic_cast(svg.get()); + if (composite != nullptr) { + auto contentArea = composite->getContentArea(); + auto path = svg->getOutlineAsPath(); + // apply transform to path to get the content area in the bounds -1 to 1 + path.applyTransform(juce::AffineTransform::translation(-contentArea.getX(), -contentArea.getY())); + path.applyTransform(juce::AffineTransform::scale(2 / contentArea.getWidth(), 2 / contentArea.getHeight())); + path.applyTransform(juce::AffineTransform::translation(-1, -1)); - pathToShapes(path, shapes); - Shape::removeOutOfBounds(shapes); + pathToShapes(path, shapes); + Shape::removeOutOfBounds(shapes); + return; + } + } + + // draw an X to indicate an error. + shapes.push_back(std::make_unique(-0.5, -0.5, 0.5, 0.5)); + shapes.push_back(std::make_unique(-0.5, 0.5, 0.5, -0.5)); } SvgParser::~SvgParser() {} @@ -69,4 +78,4 @@ std::vector> SvgParser::draw() { tempShapes.push_back(shape->clone()); } return tempShapes; -} \ No newline at end of file +} diff --git a/osci-render.jucer b/osci-render.jucer index 653d8a3..a905709 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -1,7 +1,7 @@ + + + + @@ -133,8 +137,6 @@ - @@ -378,6 +380,10 @@ + + + @@ -409,6 +415,10 @@ file="Source/PluginProcessor.cpp"/> + +