From 87cf67dc86e4a242867edc57d453e11893ea24ed Mon Sep 17 00:00:00 2001 From: dpezdirc Date: Tue, 10 May 2022 14:21:27 +0200 Subject: [PATCH] - Fix source position not synced with UI - Fix reflectivity and dry/wet sliders not working - Fix dry signal pitched down due to extra write() to delay buffer - Fix non-standard C++ that won't compile on all compilers - Fix allocated memory not deleted - Change output to stereo by default - Add ability to change room size during runtime - Add ability to change mic position via mouse drag - Sync room height between UI and processor --- Source/PluginProcessor.cpp | 247 ++++++++++++++++++++----------------- Source/PluginProcessor.h | 9 +- Source/Reverb/Delay.cpp | 23 ++-- Source/Reverb/Delay.h | 2 +- Source/Reverb/Network.cpp | 8 +- Source/Reverb/Network.h | 2 +- Source/Reverb/Node.cpp | 6 +- Source/XYContainer.cpp | 66 ++++++++-- Source/XYContainer.h | 5 + Source/XYPoint.h | 3 + 10 files changed, 233 insertions(+), 138 deletions(-) diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 2bfdb5a..ab35239 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -11,6 +11,12 @@ #include "PluginProcessor.h" #include "PluginEditor.h" +#define STEREO_OUTPUT +//#define MUTE_OVER_0DBFS + +const float ScatteringDelayReverbAudioProcessor::ROOM_HEIGHT = 3.f; +const float ScatteringDelayReverbAudioProcessor::MAX_ROOM_SIZE = 100.f; + //============================================================================== ScatteringDelayReverbAudioProcessor::ScatteringDelayReverbAudioProcessor() #ifndef JucePlugin_PreferredChannelConfigurations @@ -23,43 +29,50 @@ ScatteringDelayReverbAudioProcessor::ScatteringDelayReverbAudioProcessor() #endif ) #endif + , + roomSizeLast(8.0f) { - - addParameter (roomSize = new AudioParameterFloat ("roomSize", // parameter ID - "Room size", // parameter name - NormalisableRange (0.0f, 100.0f), - 8.0f)); // default value - - - addParameter (absorption = new AudioParameterFloat ("absorption", // parameter ID - "Wall Reflectivity", // parameter name - NormalisableRange (0.0f, 1.0), - 0.5f)); // default value - - addParameter (dryWet = new AudioParameterFloat ("dryWet", // parameter ID - "Dry/Wet", // parameter name - NormalisableRange (0.0f, 1.0f), - 0.5f)); // default value - - addParameter (sourceXPosition = new AudioParameterFloat ("sourceXPosition", // parameter ID - "Source X Position", // parameter name - NormalisableRange (0.0f, roomSize->get()), - roomSize->get()/2.0)); // default value - - addParameter (sourceYPosition = new AudioParameterFloat ("sourceYPosition", // parameter ID - "Source Y Position", // parameter name - NormalisableRange (0.0f, roomSize->get()), - 6.0)); // default value - - addParameter (micXPosition = new AudioParameterFloat ("micXPosition", // parameter ID - "Mic X Position", // parameter name - NormalisableRange (0.0f, roomSize->get()), - 2.0)); // default value - - addParameter (micYPosition = new AudioParameterFloat ("micYPosition", // parameter ID - "Mic Y Position", // parameter name - NormalisableRange (0.0f, roomSize->get()), - roomSize->get()/2.0)); // default value + network = new SDN::Network(44100); // placeholder network - to be reset before playback + + const float initSrcPosX = roomSizeLast / 2; + const float initSrcPosY = roomSizeLast / 2; + const float initMicPosX = roomSizeLast / 4; + const float initMicPosY = roomSizeLast / 2; + + addParameter(roomSize = new AudioParameterFloat("roomSize", // parameter ID + "Room size", // parameter name + NormalisableRange(0.0f, MAX_ROOM_SIZE), + roomSizeLast)); // default value + + addParameter(absorption = new AudioParameterFloat("absorption", // parameter ID + "Wall Reflectivity", // parameter name + NormalisableRange(0.0f, 1.0), + 0.5f)); // default value + + addParameter(dryWet = new AudioParameterFloat("dryWet", // parameter ID + "Source X Position", // parameter name + NormalisableRange(0.0f, 1.0f), + 0.5f)); // default value + + addParameter(sourceXPosition = new AudioParameterFloat("sourceXPosition", // parameter ID + "Source X Position", // parameter name + NormalisableRange(0.0f, MAX_ROOM_SIZE), + initSrcPosX)); // default value + + addParameter(sourceYPosition = new AudioParameterFloat("sourceYPosition", // parameter ID + "Source Y Position", // parameter name + NormalisableRange(0.0f, MAX_ROOM_SIZE), + initSrcPosY)); // default value + + addParameter(micXPosition = new AudioParameterFloat("micXPosition", // parameter ID + "Mic X Position", // parameter name + NormalisableRange(0.0f, MAX_ROOM_SIZE), + initMicPosX)); // default value + + addParameter(micYPosition = new AudioParameterFloat("micYPosition", // parameter ID + "Mic Y Position", // parameter name + NormalisableRange(0.0f, MAX_ROOM_SIZE), + initMicPosY)); // default value } ScatteringDelayReverbAudioProcessor::~ScatteringDelayReverbAudioProcessor() @@ -131,18 +144,8 @@ void ScatteringDelayReverbAudioProcessor::changeProgramName (int index, const St //============================================================================== void ScatteringDelayReverbAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { - // Use this method as the place to do any pre-playback - // initialisation that you need.. - float width, length, height; - width = 8; - length = 8; - height = 8; - - network = new SDN::Network(sampleRate, width, length, height); - network->setSourcePosition(6.0, length/2, height/2); - network->setMicPosition(2.0, length/2, height/2); -// network->setAbsorptionAmount(absorption->get()); - + resetNetwork(); + DBG("azimuths"); DBG(network->getSourceAzimuth()); for(int node = 0; node < 6; node++) { @@ -192,69 +195,58 @@ void ScatteringDelayReverbAudioProcessor::processBlock (AudioBuffer& buff auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); const int numSamples = buffer.getNumSamples(); // How many samples in the buffer - + + if (roomSize->get() != roomSizeLast) + { + resetNetwork(); + roomSizeLast = roomSize->get(); + } + for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) - buffer.clear (i, 0, numSamples); - - - // 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, numSamples); - - // This is the place where you'd normally do the guts of your plugin's - // audio processing... - // Make sure to reset the state if your inner loop is processing - // the samples and the outer loop is handling the channels. - // Alternatively, you can process the samples with the channels - // interleaved by keeping the same state. - - for (int i = 0; i < numSamples; ++i) + buffer.clear(i, 0, numSamples); + + for (int i = 0; i < numSamples; ++i) + { + float in = 0.0; + for (int channel = 0; channel < totalNumInputChannels; ++channel) { - float in = 0.0; - for (int channel = 0; channel < totalNumInputChannels; ++channel) - { - in += buffer.getReadPointer (channel)[i]; - } - - in /= totalNumInputChannels; // sum to mono - -// auto outDry = network->positionSource(in); // get stereo positioned source output (DRY) -// auto outWet = network->scatterStereo(in); // get stereo reverb output (WET) - - // if position changes, get new positions - // get struct of streams - // set source/mic position - - auto outMono = network->scatterMono(in/3.0); - - buffer.getWritePointer (0)[i] = 0.0; - buffer.getWritePointer (1)[i] = 0.0; - - float dryMix = fmin(dryWet->get() * 2, 1.0); // crossfade dry wet amounts (at 0.5 both have gain of 1) - float wetMix = fmin((1 - dryWet->get()) * 2, 1.0); - -// float outL = dryMix * outDry.L + wetMix * outWet.L; -// float outR = dryMix * outDry.R + wetMix * outWet.R; - - buffer.getWritePointer (0)[i] = outMono; - buffer.getWritePointer (1)[i] = outMono; - -// if (outL <= 1 && outR <= 1) { -// buffer.getWritePointer (0)[i] = outL; -// buffer.getWritePointer (1)[i] = outR; -// } -// else { -// DBG("PEAKING"); // TO SAVE YOUR EARS -// } + in += buffer.getReadPointer(channel)[i]; } - // ..do something to the data... -// } + in /= totalNumInputChannels; // sum to mono + + SDN::StereoOutput outDry = network->positionSource(in); // get stereo positioned source output (DRY) + SDN::StereoOutput outWet; + float outL, outR; + +#ifdef STEREO_OUTPUT + outWet = network->scatterStereo(in); // get stereo reverb output (WET) +#else + outWet.L = outWet.R = network->scatterMono(in / 3.0); +#endif + + float dryMix = fmin(dryWet->get() * 2, 1.0); // crossfade dry wet amounts (at 0.5 both have gain of 1) + float wetMix = fmin((1 - dryWet->get()) * 2, 1.0); + + outL = dryMix * outDry.L + wetMix * outWet.L; + outR = dryMix * outDry.R + wetMix * outWet.R; + +#ifdef MUTE_OVER_0DBFS + if (outL <= 1 && outR <= 1) { +#endif + + buffer.getWritePointer(0)[i] = outL; + buffer.getWritePointer(1)[i] = outR; + +#ifdef MUTE_OVER_0DBFS + } + else { + buffer.getWritePointer(0)[i] = 0.0f; + buffer.getWritePointer(1)[i] = 0.0f; + DBG("PEAKING"); // TO SAVE YOUR EARS + } +#endif + } } //============================================================================== @@ -282,12 +274,38 @@ void ScatteringDelayReverbAudioProcessor::setStateInformation (const void* data, // whose contents will have been created by the getStateInformation() call. } +void ScatteringDelayReverbAudioProcessor::resetNetwork() +{ + delete network; + + const float width = roomSize->get(); + const float length = roomSize->get(); + const float height = ROOM_HEIGHT; + + // ensure that source & mic are within room bounds + const float srcX = std::min(length, sourceXPosition->get()); + const float srcY = std::min(width, sourceYPosition->get()); + const float micX = std::min(length, micXPosition->get()); + const float micY = std::min(width, micYPosition->get()); + + sourceXPosition->setValueNotifyingHost(sourceXPosition->getNormalisableRange().convertTo0to1(srcX)); + sourceYPosition->setValueNotifyingHost(sourceYPosition->getNormalisableRange().convertTo0to1(srcY)); + micXPosition->setValueNotifyingHost(micXPosition->getNormalisableRange().convertTo0to1(micX)); + micYPosition->setValueNotifyingHost(micYPosition->getNormalisableRange().convertTo0to1(micY)); + + network = new SDN::Network(getSampleRate(), width, length, height); + network->setSourcePosition(srcX, srcY, height / 2); + network->setMicPosition(micX, micY, height / 2); + network->setAbsorptionAmount(absorption->get()); +} + // method for updating source position only when slider changes void ScatteringDelayReverbAudioProcessor::updateSourcePosition(float x, float y, float z) { - sourceXPosition->setValueNotifyingHost(x); - sourceYPosition->setValueNotifyingHost(y); -// network->setSourcePosition(sourceXPosition->get(), sourceYPosition->get(), roomSize->get()/2); + sourceXPosition->setValueNotifyingHost(sourceXPosition->getNormalisableRange().convertTo0to1(x)); + sourceYPosition->setValueNotifyingHost(sourceYPosition->getNormalisableRange().convertTo0to1(y)); + network->setSourcePosition(x, y, z); + DBG("azimuths"); DBG(network->getSourceAzimuth()); for(int node = 0; node < 6; node++) { @@ -301,9 +319,16 @@ void ScatteringDelayReverbAudioProcessor::updateSourcePosition(float x, float y, } } +void ScatteringDelayReverbAudioProcessor::updateMicPosition(float x, float y, float z) +{ + micXPosition->setValueNotifyingHost(micXPosition->getNormalisableRange().convertTo0to1(x)); + micYPosition->setValueNotifyingHost(micYPosition->getNormalisableRange().convertTo0to1(y)); + network->setMicPosition(x, y, z); +} + void ScatteringDelayReverbAudioProcessor::setAbsorption(const float amount) { absorption->setValueNotifyingHost(amount); -// network->setAbsorptionAmount(amount); + network->setAbsorptionAmount(amount); } //============================================================================== diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 687e2a0..336385c 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -11,8 +11,9 @@ #pragma once #include "../JuceLibraryCode/JuceHeader.h" -#include +#include "Reverb/Network.h" +using namespace juce; //============================================================================== /** @@ -57,7 +58,9 @@ class ScatteringDelayReverbAudioProcessor : public AudioProcessor void getStateInformation (MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; + void resetNetwork(); void updateSourcePosition(float x, float y, float z); + void updateMicPosition(float x, float y, float z); void setAbsorption(const float amount); AudioParameterFloat* sourceXPosition; @@ -70,6 +73,10 @@ class ScatteringDelayReverbAudioProcessor : public AudioProcessor AudioParameterFloat* absorption; AudioParameterFloat* dryWet; + static const float ROOM_HEIGHT; + static const float MAX_ROOM_SIZE; + float roomSizeLast; + private: SDN::Network *network; diff --git a/Source/Reverb/Delay.cpp b/Source/Reverb/Delay.cpp index 4214f89..3470ed1 100644 --- a/Source/Reverb/Delay.cpp +++ b/Source/Reverb/Delay.cpp @@ -38,6 +38,11 @@ namespace SDN { setAirAbsorption(); } + Delay::~Delay() + { + delete[] buffer; + } + Delay* Delay::fromDistance(float sampleRate, float distance, float maxDistance) { return new Delay(sampleRate, (sampleRate * distance) / SDN::c, (sampleRate * maxDistance) / SDN::c); @@ -91,8 +96,10 @@ namespace SDN { // reads next sample float Delay::read() { float out = 0.0; + + const bool isReadPointerFractional = floor(readPointer) != readPointer; -// if(fractional) { + if(isReadPointerFractional) { // get high sample for linearly interpolation int highPointer = floor(readPointer + 1.0); if(highPointer >= bufferLength) highPointer -= bufferLength; @@ -107,12 +114,14 @@ namespace SDN { readPointer += bufferLength; else if (readPointer >= bufferLength) readPointer -= bufferLength; -// } else { -// out = buffer[readPointer]; -// readPointer++; -// -// readPointer = (readPointer + bufferLength) % bufferLength; -// } + } + else { + out = buffer[(int)readPointer]; + incrementReadPointer(); + + if (readPointer >= bufferLength) + readPointer -= bufferLength; + } return out; } diff --git a/Source/Reverb/Delay.h b/Source/Reverb/Delay.h index cc55c97..f46fa72 100644 --- a/Source/Reverb/Delay.h +++ b/Source/Reverb/Delay.h @@ -55,7 +55,7 @@ namespace SDN { Delay() {}; Delay(float sampleRate, int delayInSamples, int maxBuffer); // Delay(float sampleRate, float distance); - virtual ~Delay() {} + virtual ~Delay(); }; } diff --git a/Source/Reverb/Network.cpp b/Source/Reverb/Network.cpp index 46218ca..8f2bee5 100644 --- a/Source/Reverb/Network.cpp +++ b/Source/Reverb/Network.cpp @@ -70,6 +70,12 @@ namespace SDN nodes[5].setAbsorption(0.7); // ceiling } + Network::~Network() + { + delete[] connections; + delete[] sourceMicDelay; + } + // returns a stereo output of the direct line between the source and mic, based on their relative positions StereoOutput Network::positionSource(float sourceInput) { @@ -148,8 +154,6 @@ namespace SDN // main reverberation method void Network::scatter(float in) { - sourceMicDelay->write(in); - // for each node, gather inputs for(int node = 0; node < nodeCount; node++) { diff --git a/Source/Reverb/Network.h b/Source/Reverb/Network.h index df818c9..2626264 100644 --- a/Source/Reverb/Network.h +++ b/Source/Reverb/Network.h @@ -78,7 +78,7 @@ namespace SDN Network(float sampleRate); Network(float sampleRate, float width, float length, float height); - ~Network() {}; + ~Network(); }; } diff --git a/Source/Reverb/Node.cpp b/Source/Reverb/Node.cpp index 1dcaf5f..95509d8 100644 --- a/Source/Reverb/Node.cpp +++ b/Source/Reverb/Node.cpp @@ -78,7 +78,7 @@ namespace SDN { void Node::distributeOutput(float *outputWaveVector) { - float outs[numberOfOtherNodes]; + float* outs = (float*)alloca(numberOfOtherNodes * sizeof(float)); output = 0.0; for(int terminal = 0; terminal < numberOfOtherNodes; terminal++) { @@ -96,7 +96,7 @@ namespace SDN { void Node::scatter(float sourceInput) { - float inputWaveVector[numberOfOtherNodes]; // assign temporary vector for calculation + float* inputWaveVector = (float*)alloca(numberOfOtherNodes * sizeof(float)); // assign temporary vector for calculation prepareInput(inputWaveVector, sourceInput); @@ -106,7 +106,7 @@ namespace SDN { } sum *= weightingFactor; - float outputWaveVector[numberOfOtherNodes]; // assign temporary vector for calculation + float* outputWaveVector = (float*)alloca(numberOfOtherNodes * sizeof(float)); // assign temporary vector for calculation for(int node = 0; node < numberOfOtherNodes; node++) { outputWaveVector[node] = sum - inputWaveVector[node]; diff --git a/Source/XYContainer.cpp b/Source/XYContainer.cpp index ba78c3e..b537ec6 100644 --- a/Source/XYContainer.cpp +++ b/Source/XYContainer.cpp @@ -11,6 +11,18 @@ #include "../JuceLibraryCode/JuceHeader.h" #include "XYContainer.h" +template +T clamp(T value, T min, T max) +{ + return std::max(std::min(value, max), min); +} + +template +T square(T a, T b) +{ + return a * b; +} + //============================================================================== XYContainer::XYContainer(ScatteringDelayReverbAudioProcessor &p) : roomSizeLabel("", "Room size"), @@ -59,20 +71,18 @@ void XYContainer::paint (Graphics& g) String label; label << processor.roomSize->get() << " x "; - label << processor.roomSize->get() << " x 3m"; + label << processor.roomSize->get() << " x "; + label << processor.ROOM_HEIGHT << "m"; roomSizeLabel.setText(label, dontSendNotification); float pointSize = 0.05 * getWidth(); source.setSize(pointSize, pointSize); // set the size of the circle - - float x = processor.sourceXPosition->convertTo0to1(processor.sourceXPosition->get()); - float y = 1 - processor.sourceYPosition->convertTo0to1(processor.sourceYPosition->get()); // flip so y = 0 is at bottom - source.setCentreRelative(x, y); // update sourceXY position based on processor + source.setCentrePosition(getSourceXCoordinate(), getSourceYCoordinate()); // update sourceXY position based on processor // draw mic as cross g.setColour (Colours::white); - float micX = processor.micXPosition->convertTo0to1(processor.micXPosition->get()) * getWidth(); - float micY = (1 - processor.micYPosition->convertTo0to1(processor.micYPosition->get())) * getHeight(); + float micX = getMicXCoordinate(); + float micY = getMicYCoordinate(); g.drawLine(micX - pointSize/2, micY - pointSize/2, micX + pointSize/2, micY + pointSize/2); g.drawLine(micX - pointSize/2, micY + pointSize/2, micX + pointSize/2, micY - pointSize/2); @@ -93,9 +103,41 @@ void XYContainer::timerCallback() void XYContainer::mouseDrag(const MouseEvent& event) { - - float x = (event.getMouseDownX() + event.getDistanceFromDragStartX()) / (float) getWidth(); - float y = 1 - ((event.getMouseDownY() + event.getDistanceFromDragStartY()) / (float) getHeight()); - - processor.updateSourcePosition(x, y, 1.5); // hard code z position to be 1.5 metres + const int xCoordinate = event.getMouseDownX() + event.getDistanceFromDragStartX(); + const int yCoordinate = event.getMouseDownY() + event.getDistanceFromDragStartY(); + + const float xPosition = clamp(static_cast(xCoordinate) / getWidth(), 0.f, 1.f) * processor.roomSize->get(); + const float yPosition = clamp(1.f - static_cast(yCoordinate) / getHeight(), 0.f, 1.f) * processor.roomSize->get(); + + const float distanceToSource = square(xCoordinate - getSourceXCoordinate()) + square(yCoordinate - getSourceYCoordinate()); + const float distanceToMic = square(xCoordinate - getMicXCoordinate()) + square(yCoordinate - getMicYCoordinate()); + + if (distanceToSource < distanceToMic) + { + processor.updateSourcePosition(xPosition, yPosition, 1.5); // hard code z position to be 1.5 metres + } + else + { + processor.updateMicPosition(xPosition, yPosition, 1.5); + } +} + +float XYContainer::getSourceXCoordinate() +{ + return clamp(processor.sourceXPosition->get() / processor.roomSize->get(), 0.f, 1.f) * getWidth(); +} + +float XYContainer::getSourceYCoordinate() +{ + return clamp(1.f - processor.sourceYPosition->get() / processor.roomSize->get(), 0.f, 1.f) * getWidth(); +} + +float XYContainer::getMicXCoordinate() +{ + return clamp(processor.micXPosition->get() / processor.roomSize->get(), 0.f, 1.f) * getWidth(); +} + +float XYContainer::getMicYCoordinate() +{ + return clamp(1.f - processor.micYPosition->get() / processor.roomSize->get(), 0.f, 1.f) * getWidth(); } diff --git a/Source/XYContainer.h b/Source/XYContainer.h index bfc7585..5d0c0bb 100644 --- a/Source/XYContainer.h +++ b/Source/XYContainer.h @@ -34,6 +34,11 @@ class XYContainer : public Component, public Timer virtual void mouseDrag (const MouseEvent& event) override; // handle mousedrag + float getSourceXCoordinate(); + float getSourceYCoordinate(); + float getMicXCoordinate(); + float getMicYCoordinate(); + private: XYPoint source; diff --git a/Source/XYPoint.h b/Source/XYPoint.h index aa99e52..dd6ddf9 100644 --- a/Source/XYPoint.h +++ b/Source/XYPoint.h @@ -17,6 +17,9 @@ Simple circle class that is moved around the XY pad */ + +using namespace juce; + class XYPoint : public Component { public: