From 13a6a6e2e3bc8acdc599578b9c623270bae08e14 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 28 Aug 2023 22:06:21 +0100 Subject: [PATCH 1/3] Get basic MIDI input working --- Source/PluginProcessor.cpp | 182 +++++--------------------------- Source/PluginProcessor.h | 59 +++-------- Source/audio/Effect.cpp | 10 ++ Source/audio/Effect.h | 2 + Source/audio/ShapeSound.cpp | 54 ++++++++++ Source/audio/ShapeSound.h | 26 +++++ Source/audio/ShapeVoice.cpp | 150 ++++++++++++++++++++++++++ Source/audio/ShapeVoice.h | 37 +++++++ Source/parser/FrameConsumer.h | 2 +- Source/parser/FrameProducer.cpp | 12 +-- Source/parser/FrameProducer.h | 2 - osci-render.jucer | 4 + 12 files changed, 325 insertions(+), 215 deletions(-) create mode 100644 Source/audio/ShapeSound.cpp create mode 100644 Source/audio/ShapeSound.h create mode 100644 Source/audio/ShapeVoice.cpp create mode 100644 Source/audio/ShapeVoice.h diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 8861b8f..b072e61 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -32,8 +32,6 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() ) #endif { - producer.startThread(); - // locking isn't necessary here because we are in the constructor toggleableEffects.push_back(std::make_shared( @@ -137,6 +135,10 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() for (auto parameter : booleanParameters) { addParameter(parameter); } + + for (int i = 0; i < 4; i++) { + synth.addVoice(new ShapeVoice(*this)); + } } OscirenderAudioProcessor::~OscirenderAudioProcessor() {} @@ -194,6 +196,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() { @@ -304,7 +307,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 +318,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 +329,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 +343,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; @@ -363,17 +370,18 @@ void OscirenderAudioProcessor::openFile(int index) { // used ONLY for changing the current file to an EXISTING file. // much faster than openFile(int index) because it doesn't reparse any files. // parsersLock AND effectsLock must be locked before calling this function + +// TODO: This should change whatever the ShapeSound is to the new index void OscirenderAudioProcessor::changeCurrentFile(int index) { + synth.clearSounds(); if (index == -1) { currentFile = -1; - producer.setSource(std::make_shared(), -1); } if (index < 0 || index >= fileBlocks.size()) { return; } - producer.setSource(parsers[index], index); + synth.addSound(sounds[index]); currentFile = index; - invalidateFrameBuffer = true; updateLuaValues(); updateObjValues(); } @@ -402,113 +410,19 @@ 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()); - + buffer.clear(); + synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + midiMessages.clear(); 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; - } - 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); - } + 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 +437,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; @@ -541,57 +455,9 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju } 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(); - } - } - } } } -// 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) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index c511b99..be7999f 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -10,10 +10,9 @@ #include #include "shape/Shape.h" -#include "parser/FileParser.h" -#include "parser/FrameProducer.h" -#include "parser/FrameConsumer.h" #include "audio/Effect.h" +#include "audio/ShapeSound.h" +#include "audio/ShapeVoice.h" #include #include "concurrency/BufferProducer.h" #include "audio/AudioWebSocketServer.h" @@ -29,7 +28,6 @@ class OscirenderAudioProcessor : public juce::AudioProcessor #if JucePlugin_Enable_ARA , public juce::AudioProcessorARAExtension #endif - , public FrameConsumer { public: OscirenderAudioProcessor(); @@ -158,18 +156,28 @@ public: }, new EffectParameter("Rotate Speed", "objRotateSpeed", 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", 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", 0.0, 0.0, 1.0) + ); + std::shared_ptr delayEffect = std::make_shared(); std::shared_ptr perspectiveEffect = std::make_shared(); juce::SpinLock parsersLock; std::vector> parsers; + std::vector sounds; std::vector> fileBlocks; std::vector fileNames; std::atomic currentFile = -1; juce::ChangeBroadcaster broadcaster; - - FrameProducer producer = FrameProducer(*this, std::make_shared()); BufferProducer audioProducer; @@ -184,7 +192,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); @@ -204,50 +211,14 @@ private: 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; - 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; + juce::Synthesiser synth; AudioWebSocketServer softwareOscilloscopeServer{audioProducer}; - void updateFrame(); - void updateLengthIncrement(); - void incrementShapeDrawing(); void updateLuaValues(); void updateObjValues(); std::shared_ptr getEffect(juce::String id); diff --git a/Source/audio/Effect.cpp b/Source/audio/Effect.cpp index a87ee7a..dafb6e9 100644 --- a/Source/audio/Effect.cpp +++ b/Source/audio/Effect.cpp @@ -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); } 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/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..ac5c4bf --- /dev/null +++ b/Source/audio/ShapeVoice.cpp @@ -0,0 +1,150 @@ +#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); + + if (shapeSound != nullptr) { + this->sound = shapeSound; + while (frame.empty()) { + frameLength = shapeSound->updateFrame(frame); + } + tailOff = 0.0; + 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; + } +} + +void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int startSample, int numSamples) { + juce::ScopedNoDenormals noDenormals; + + int numChannels = outputBuffer.getNumChannels(); + + 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 != nullptr) { + renderingSample = sound->parser->isSample(); + + if (renderingSample) { + channels = sound->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 != nullptr) { + frameLength = sound->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) { + 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..ecd33a8 --- /dev/null +++ b/Source/audio/ShapeVoice.h @@ -0,0 +1,37 @@ +#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 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; + ShapeSound* 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; + + double tailOff = 0.0; + double frequency = 1.0; +}; \ 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/osci-render.jucer b/osci-render.jucer index 653d8a3..2900487 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -66,6 +66,10 @@ + + + + From 4667019163ea291de8bbed587e0aa58ac00b896b Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 28 Aug 2023 22:09:34 +0100 Subject: [PATCH 2/3] Add failsafe when fetching new frame in loop --- Source/audio/ShapeVoice.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/audio/ShapeVoice.cpp b/Source/audio/ShapeVoice.cpp index ac5c4bf..332d05e 100644 --- a/Source/audio/ShapeVoice.cpp +++ b/Source/audio/ShapeVoice.cpp @@ -15,8 +15,10 @@ void ShapeVoice::startNote(int midiNoteNumber, float velocity, juce::Synthesiser if (shapeSound != nullptr) { this->sound = shapeSound; - while (frame.empty()) { + int tries = 0; + while (frame.empty() && tries < 20) { frameLength = shapeSound->updateFrame(frame); + tries++; } tailOff = 0.0; frequency = juce::MidiMessage::getMidiNoteInHertz(midiNoteNumber); From 8107c521b8d757567f7505735280d3c4ceccdb87 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 29 Aug 2023 20:47:13 +0100 Subject: [PATCH 3/3] Add tabs for main settings and MIDI settings --- Source/LookAndFeel.cpp | 5 +++ Source/MidiComponent.cpp | 20 +++++++++++ Source/MidiComponent.h | 22 ++++++++++++ Source/PluginEditor.cpp | 50 ++++++-------------------- Source/PluginEditor.h | 15 +++----- Source/SettingsComponent.cpp | 69 ++++++++++++++++++++++++++++++++++++ Source/SettingsComponent.h | 33 +++++++++++++++++ osci-render.jucer | 7 ++++ 8 files changed, 171 insertions(+), 50 deletions(-) create mode 100644 Source/MidiComponent.cpp create mode 100644 Source/MidiComponent.h create mode 100644 Source/SettingsComponent.cpp create mode 100644 Source/SettingsComponent.h 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/MidiComponent.cpp b/Source/MidiComponent.cpp new file mode 100644 index 0000000..6120547 --- /dev/null +++ b/Source/MidiComponent.cpp @@ -0,0 +1,20 @@ +#include "MidiComponent.h" +#include "PluginEditor.h" + +MidiComponent::MidiComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { + addAndMakeVisible(midiToggle); + addAndMakeVisible(keyboard); +} + + +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/PluginEditor.cpp b/Source/PluginEditor.cpp index ced5801..ec769ca 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') { 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/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/osci-render.jucer b/osci-render.jucer index 2900487..594ea79 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -382,6 +382,9 @@ + + @@ -413,6 +416,10 @@ file="Source/PluginProcessor.cpp"/> + +