├── README.md ├── RTConvolve.jucer └── Source ├── ConvolutionManager.h ├── PluginEditor.cpp ├── PluginEditor.h ├── PluginProcessor.cpp ├── PluginProcessor.h ├── RefCountedAudioBuffer.h ├── TimeDistributedFFTConvolver.h ├── TimeDistributedFFTConvolver.hpp ├── UniformPartitionConvolver.h ├── UniformPartitionConvolver.hpp └── util ├── SincFilter.hpp ├── fft.hpp ├── util.cpp └── util.h /README.md: -------------------------------------------------------------------------------- 1 | RTCONVOLVE 2 | ========== 3 | 4 | ##Author 5 | Graham Barab 6 | 7 | gbarab@gmail.com 8 | 9 | ##About 10 | RTConvolve is a zero-latency real-time audio effect plugin written in C++ and built on the JUCE framework. It outputs the convolution an input signal with an arbitrary impulse response provided by the user. The goal of this project was to produce a working implementation of an algorithm that performs the computationally expensive operation of convolution with a long impulse response with the constraints that it run in real-time without latency, and that it use only a single thread. It is able to do this by using a combination of uniform and non-uniform partitioning of the impulse response, and by implementing a time-distributed version of the fast Fourier Transform such as that described by Jeffrey R. Hurchalla in his paper "A Time Distributed FFT for Efficient Low Latency Convolution." The plugin is compatible with mono and stereo inputs, and with mono and stereo impulse responses. 11 | 12 | ##Usage 13 | Use the Projucer application to set the paths for the Juce library modules, then select "Save Project and Open in IDE". 14 | 15 | ##OS Support 16 | Currently, the jucer file only contains an exporter for a Mac OSX Xcode project. However, aside from the Juce framework, this project uses standard C++11 features. It should therefore be straightforward to create exporters for other operating systems. 17 | -------------------------------------------------------------------------------- /RTConvolve.jucer: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 34 | 36 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Source/ConvolutionManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // ConvolutionManager.h 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/7/17. 6 | // 7 | // 8 | 9 | #ifndef ConvolutionManager_h 10 | #define ConvolutionManager_h 11 | 12 | #include "UniformPartitionConvolver.h" 13 | #include "TimeDistributedFFTConvolver.h" 14 | #include "../JuceLibraryCode/JuceHeader.h" 15 | #include "util/util.h" 16 | #include "util/SincFilter.hpp" 17 | 18 | static const int DEFAULT_NUM_SAMPLES = 512; 19 | static const int DEFAULT_BUFFER_SIZE = 512; 20 | 21 | template 22 | class ConvolutionManager 23 | { 24 | public: 25 | ConvolutionManager(FLOAT_TYPE *impulseResponse = nullptr, int numSamples = 0, int bufferSize = 0) 26 | : mBufferSize(bufferSize) 27 | , mTimeDistributedConvolver(nullptr) 28 | { 29 | if (impulseResponse == nullptr) 30 | { 31 | mBufferSize = DEFAULT_BUFFER_SIZE; 32 | mImpulseResponse = new juce::AudioBuffer(1, DEFAULT_NUM_SAMPLES); 33 | checkNull(mImpulseResponse); 34 | 35 | FLOAT_TYPE *ir = mImpulseResponse->getWritePointer(0); 36 | genImpulse(ir, DEFAULT_NUM_SAMPLES); 37 | 38 | init(ir, DEFAULT_NUM_SAMPLES); 39 | } 40 | else 41 | { 42 | mImpulseResponse = new juce::AudioBuffer(1, numSamples); 43 | checkNull(mImpulseResponse); 44 | mImpulseResponse->clear(); 45 | FLOAT_TYPE *ir = mImpulseResponse->getWritePointer(0); 46 | 47 | memcpy(ir, impulseResponse, numSamples * sizeof(FLOAT_TYPE)); 48 | init(ir, numSamples); 49 | } 50 | } 51 | 52 | /** 53 | Perform one base time period's worth of work for the convolution. 54 | @param input 55 | The input is expected to hold a number of samples equal to the 'bufferSize' 56 | specified in the constructor. 57 | */ 58 | void processInput(FLOAT_TYPE *input) 59 | { 60 | mUniformConvolver->processInput(input); 61 | const FLOAT_TYPE *out1 = mUniformConvolver->getOutputBuffer(); 62 | FLOAT_TYPE *output = mOutput->getWritePointer(0); 63 | 64 | /* Prepare output */ 65 | 66 | if (mTimeDistributedConvolver != nullptr) 67 | { 68 | mTimeDistributedConvolver->processInput(input); 69 | const FLOAT_TYPE *out2 = mTimeDistributedConvolver->getOutputBuffer(); 70 | 71 | for (int i = 0; i < mBufferSize; ++i) 72 | { 73 | output[i] = out1[i] + out2[i]; 74 | } 75 | } 76 | else 77 | { 78 | for (int i = 0; i < mBufferSize; ++i) 79 | { 80 | output[i] = out1[i]; 81 | } 82 | } 83 | } 84 | 85 | const FLOAT_TYPE *getOutputBuffer() const 86 | { 87 | return mOutput->getReadPointer(0); 88 | } 89 | 90 | void setBufferSize(int bufferSize) 91 | { 92 | FLOAT_TYPE *ir = mImpulseResponse->getWritePointer(0); 93 | int numSamples = mImpulseResponse->getNumSamples(); 94 | mBufferSize = bufferSize; 95 | 96 | init(ir, numSamples); 97 | } 98 | 99 | void setImpulseResponse(const FLOAT_TYPE *impulseResponse, int numSamples) 100 | { 101 | mImpulseResponse = new juce::AudioBuffer(1, numSamples); 102 | checkNull(mImpulseResponse); 103 | 104 | mImpulseResponse->clear(); 105 | 106 | FLOAT_TYPE *ir = mImpulseResponse->getWritePointer(0); 107 | memcpy(ir, impulseResponse, numSamples * sizeof(FLOAT_TYPE)); 108 | init(ir, numSamples); 109 | } 110 | 111 | private: 112 | int mBufferSize; 113 | juce::ScopedPointer > mUniformConvolver; 114 | juce::ScopedPointer > mTimeDistributedConvolver; 115 | juce::ScopedPointer > mOutput; 116 | juce::ScopedPointer > mImpulseResponse; 117 | 118 | void init(FLOAT_TYPE *impulseResponse, int numSamples) 119 | { 120 | mUniformConvolver = new UPConvolver(impulseResponse, numSamples, mBufferSize, 8); 121 | checkNull(mUniformConvolver); 122 | 123 | FLOAT_TYPE *subIR = impulseResponse + (8 * mBufferSize); 124 | int subNumSamples = numSamples - (8 * mBufferSize); 125 | 126 | if (subNumSamples > 0) 127 | { 128 | mTimeDistributedConvolver = new TimeDistributedFFTConvolver(subIR, subNumSamples, mBufferSize); 129 | checkNull(mTimeDistributedConvolver); 130 | } 131 | else 132 | { 133 | mTimeDistributedConvolver = nullptr; 134 | } 135 | 136 | mOutput = new juce::AudioBuffer(1, mBufferSize); 137 | checkNull(mOutput); 138 | } 139 | }; 140 | 141 | #endif /* ConvolutionManager_h */ 142 | -------------------------------------------------------------------------------- /Source/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | It contains the basic framework code for a JUCE plugin editor. 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "PluginProcessor.h" 12 | #include "PluginEditor.h" 13 | 14 | 15 | //============================================================================== 16 | RtconvolveAudioProcessorEditor::RtconvolveAudioProcessorEditor (RtconvolveAudioProcessor& p) 17 | : AudioProcessorEditor (&p) 18 | , processor (p) 19 | , mButtonChooseIR("Choose Impulse Response") 20 | { 21 | 22 | mButtonChooseIR.changeWidthToFitText(); 23 | setSize (400, 300); 24 | 25 | Rectangle boundingBox = getBounds(); 26 | mButtonChooseIR.setBounds(boundingBox); 27 | 28 | addAndMakeVisible(&mButtonChooseIR); 29 | 30 | mButtonChooseIR.addListener(this); 31 | 32 | } 33 | 34 | RtconvolveAudioProcessorEditor::~RtconvolveAudioProcessorEditor() 35 | { 36 | } 37 | 38 | void RtconvolveAudioProcessorEditor::buttonClicked(juce::Button* b) 39 | { 40 | FileChooser fchooser("Select Impulse Response"); 41 | if (fchooser.browseForFileToOpen()) 42 | { 43 | File ir = fchooser.getResult(); 44 | FileInputStream irInputStream(ir); 45 | AudioFormatManager manager; 46 | manager.registerBasicFormats(); 47 | juce::ScopedPointer formatReader = manager.createReaderFor(ir); 48 | AudioSampleBuffer sampleBuffer(formatReader->numChannels, formatReader->lengthInSamples); 49 | formatReader->read(&sampleBuffer, 0, formatReader->lengthInSamples, 0, 1, 1); 50 | 51 | processor.setImpulseResponse(sampleBuffer, ir.getFullPathName()); 52 | } 53 | } 54 | 55 | //============================================================================== 56 | void RtconvolveAudioProcessorEditor::paint (Graphics& g) 57 | { 58 | g.fillAll (Colours::white); 59 | 60 | g.setColour (Colours::black); 61 | g.setFont (15.0f); 62 | 63 | } 64 | 65 | void RtconvolveAudioProcessorEditor::resized() 66 | { 67 | // This is generally where you'll want to lay out the positions of any 68 | // subcomponents in your editor.. 69 | } 70 | -------------------------------------------------------------------------------- /Source/PluginEditor.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | It contains the basic framework code for a JUCE plugin editor. 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #ifndef PLUGINEDITOR_H_INCLUDED 12 | #define PLUGINEDITOR_H_INCLUDED 13 | 14 | #include "../JuceLibraryCode/JuceHeader.h" 15 | #include "PluginProcessor.h" 16 | #include "RefCountedAudioBuffer.h" 17 | 18 | //============================================================================== 19 | /** 20 | */ 21 | class RtconvolveAudioProcessorEditor : public AudioProcessorEditor, public Button::Listener 22 | { 23 | public: 24 | RtconvolveAudioProcessorEditor (RtconvolveAudioProcessor&); 25 | ~RtconvolveAudioProcessorEditor(); 26 | 27 | //============================================================================== 28 | void paint (Graphics&) override; 29 | void resized() override; 30 | void buttonClicked(juce::Button*) override; 31 | private: 32 | // This reference is provided as a quick way for your editor to 33 | // access the processor object that created it. 34 | RtconvolveAudioProcessor& processor; 35 | 36 | juce::TextButton mButtonChooseIR; 37 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RtconvolveAudioProcessorEditor) 38 | }; 39 | 40 | 41 | #endif // PLUGINEDITOR_H_INCLUDED 42 | -------------------------------------------------------------------------------- /Source/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | It contains the basic framework code for a JUCE plugin processor. 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "PluginProcessor.h" 12 | #include "PluginEditor.h" 13 | #include "util/SincFilter.hpp" 14 | #include "util/util.h" 15 | 16 | //============================================================================== 17 | RtconvolveAudioProcessor::RtconvolveAudioProcessor() 18 | : mSampleRate(0.0) 19 | , mBufferSize(0) 20 | , mImpulseResponseFilePath("") 21 | { 22 | 23 | } 24 | 25 | RtconvolveAudioProcessor::~RtconvolveAudioProcessor() 26 | { 27 | } 28 | 29 | //============================================================================== 30 | const String RtconvolveAudioProcessor::getName() const 31 | { 32 | return JucePlugin_Name; 33 | } 34 | 35 | bool RtconvolveAudioProcessor::acceptsMidi() const 36 | { 37 | #if JucePlugin_WantsMidiInput 38 | return true; 39 | #else 40 | return false; 41 | #endif 42 | } 43 | 44 | bool RtconvolveAudioProcessor::producesMidi() const 45 | { 46 | #if JucePlugin_ProducesMidiOutput 47 | return true; 48 | #else 49 | return false; 50 | #endif 51 | } 52 | 53 | double RtconvolveAudioProcessor::getTailLengthSeconds() const 54 | { 55 | return 0.0; 56 | } 57 | 58 | int RtconvolveAudioProcessor::getNumPrograms() 59 | { 60 | return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, 61 | // so this should be at least 1, even if you're not really implementing programs. 62 | } 63 | 64 | int RtconvolveAudioProcessor::getCurrentProgram() 65 | { 66 | return 0; 67 | } 68 | 69 | void RtconvolveAudioProcessor::setCurrentProgram (int index) 70 | { 71 | } 72 | 73 | const String RtconvolveAudioProcessor::getProgramName (int index) 74 | { 75 | return String(); 76 | } 77 | 78 | void RtconvolveAudioProcessor::changeProgramName (int index, const String& newName) 79 | { 80 | } 81 | 82 | void RtconvolveAudioProcessor::setImpulseResponse(const AudioSampleBuffer& impulseResponseBuffer, const juce::String pathToImpulse) 83 | { 84 | juce::ScopedLock lock(mLoadingLock); 85 | mImpulseResponseFilePath = pathToImpulse; 86 | AudioSampleBuffer impulseResponse(impulseResponseBuffer); 87 | 88 | if (impulseResponseBuffer.getNumChannels() == 2) 89 | { 90 | float *impulseResponseLeft = impulseResponse.getWritePointer(0); 91 | float *impulseResponseRight = impulseResponse.getWritePointer(1); 92 | 93 | normalizeStereoImpulseResponse(impulseResponseLeft, impulseResponseRight, impulseResponse.getNumSamples()); 94 | mConvolutionManager[0].setImpulseResponse(impulseResponseLeft, impulseResponse.getNumSamples()); 95 | mConvolutionManager[1].setImpulseResponse(impulseResponseRight, impulseResponse.getNumSamples()); 96 | } 97 | else 98 | { 99 | float *ir = impulseResponse.getWritePointer(0); 100 | 101 | normalizeMonoImpulseResponse(ir, impulseResponse.getNumSamples()); 102 | mConvolutionManager[0].setImpulseResponse(ir, impulseResponse.getNumSamples()); 103 | mConvolutionManager[1].setImpulseResponse(ir, impulseResponse.getNumSamples()); 104 | } 105 | } 106 | 107 | //============================================================================== 108 | void RtconvolveAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) 109 | { 110 | mConvolutionManager[0].setBufferSize(samplesPerBlock); 111 | mConvolutionManager[1].setBufferSize(samplesPerBlock); 112 | } 113 | 114 | void RtconvolveAudioProcessor::releaseResources() 115 | { 116 | } 117 | 118 | #ifndef JucePlugin_PreferredChannelConfigurations 119 | bool RtconvolveAudioProcessor::setPreferredBusArrangement (bool isInput, int bus, const AudioChannelSet& preferredSet) 120 | { 121 | // Reject any bus arrangements that are not compatible with your plugin 122 | 123 | const int numChannels = preferredSet.size(); 124 | 125 | #if JucePlugin_IsMidiEffect 126 | if (numChannels != 0) 127 | return false; 128 | #elif JucePlugin_IsSynth 129 | if (isInput || (numChannels != 1 && numChannels != 2)) 130 | return false; 131 | #else 132 | if (numChannels != 1 && numChannels != 2) 133 | return false; 134 | 135 | if (! AudioProcessor::setPreferredBusArrangement (! isInput, bus, preferredSet)) 136 | return false; 137 | #endif 138 | 139 | return AudioProcessor::setPreferredBusArrangement (isInput, bus, preferredSet); 140 | } 141 | #endif 142 | 143 | void RtconvolveAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages) 144 | { 145 | const int totalNumInputChannels = getTotalNumInputChannels(); 146 | const int totalNumOutputChannels = getTotalNumOutputChannels(); 147 | 148 | for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 149 | buffer.clear (i, 0, buffer.getNumSamples()); 150 | 151 | juce::ScopedTryLock tryLock(mLoadingLock); 152 | 153 | if (tryLock.isLocked()) 154 | { 155 | for (int channel = 0; channel < 1; ++channel) 156 | { 157 | float* channelData = buffer.getWritePointer (channel); 158 | mConvolutionManager[channel].processInput(channelData); 159 | const float* y = mConvolutionManager[channel].getOutputBuffer(); 160 | memcpy(channelData, y, buffer.getNumSamples() * sizeof(float)); 161 | 162 | if (buffer.getNumChannels() == 2) 163 | { 164 | float *channelDataR = buffer.getWritePointer(1); 165 | const float* y = mConvolutionManager[0].getOutputBuffer(); 166 | memcpy(channelDataR, y, buffer.getNumSamples() * sizeof(float)); 167 | } 168 | } 169 | } 170 | else 171 | { 172 | buffer.clear(); 173 | } 174 | } 175 | 176 | //============================================================================== 177 | bool RtconvolveAudioProcessor::hasEditor() const 178 | { 179 | return true; // (change this to false if you choose to not supply an editor) 180 | } 181 | 182 | AudioProcessorEditor* RtconvolveAudioProcessor::createEditor() 183 | { 184 | return new RtconvolveAudioProcessorEditor (*this); 185 | } 186 | 187 | //============================================================================== 188 | void RtconvolveAudioProcessor::getStateInformation (MemoryBlock& destData) 189 | { 190 | XmlElement xml("STATEINFO"); 191 | xml.setAttribute("impulseResponseFilePath", mImpulseResponseFilePath); 192 | copyXmlToBinary(xml, destData); 193 | } 194 | 195 | void RtconvolveAudioProcessor::setStateInformation (const void* data, int sizeInBytes) 196 | { 197 | juce::ScopedPointer xml(getXmlFromBinary(data, sizeInBytes)); 198 | 199 | String impulseResponseFilePath = xml->getStringAttribute("impulseResponseFilePath", ""); 200 | juce::File ir(impulseResponseFilePath); 201 | AudioFormatManager manager; 202 | manager.registerBasicFormats(); 203 | juce::ScopedPointer formatReader = manager.createReaderFor(ir); 204 | AudioSampleBuffer sampleBuffer(formatReader->numChannels, formatReader->lengthInSamples); 205 | formatReader->read(&sampleBuffer, 0, formatReader->lengthInSamples, 0, 1, 1); 206 | setImpulseResponse(sampleBuffer, impulseResponseFilePath); 207 | } 208 | 209 | //============================================================================== 210 | // This creates new instances of the plugin.. 211 | AudioProcessor* JUCE_CALLTYPE createPluginFilter() 212 | { 213 | return new RtconvolveAudioProcessor(); 214 | } 215 | -------------------------------------------------------------------------------- /Source/PluginProcessor.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file was auto-generated! 5 | 6 | It contains the basic framework code for a JUCE plugin processor. 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #ifndef PLUGINPROCESSOR_H_INCLUDED 12 | #define PLUGINPROCESSOR_H_INCLUDED 13 | 14 | #include "../JuceLibraryCode/JuceHeader.h" 15 | #include "UniformPartitionConvolver.h" 16 | #include "TimeDistributedFFTConvolver.h" 17 | #include "ConvolutionManager.h" 18 | 19 | //============================================================================== 20 | /** 21 | */ 22 | class RtconvolveAudioProcessor : public AudioProcessor 23 | { 24 | public: 25 | //============================================================================== 26 | RtconvolveAudioProcessor(); 27 | ~RtconvolveAudioProcessor(); 28 | 29 | //============================================================================== 30 | void prepareToPlay (double sampleRate, int samplesPerBlock) override; 31 | void releaseResources() override; 32 | 33 | #ifndef JucePlugin_PreferredChannelConfigurations 34 | bool setPreferredBusArrangement (bool isInput, int bus, const AudioChannelSet& preferredSet) override; 35 | #endif 36 | 37 | void processBlock (AudioSampleBuffer&, MidiBuffer&) override; 38 | 39 | //============================================================================== 40 | AudioProcessorEditor* createEditor() override; 41 | bool hasEditor() const override; 42 | 43 | //============================================================================== 44 | const String getName() const override; 45 | 46 | bool acceptsMidi() const override; 47 | bool producesMidi() const override; 48 | double getTailLengthSeconds() const override; 49 | 50 | //============================================================================== 51 | int getNumPrograms() override; 52 | int getCurrentProgram() override; 53 | void setCurrentProgram (int index) override; 54 | const String getProgramName (int index) override; 55 | void changeProgramName (int index, const String& newName) override; 56 | 57 | //============================================================================== 58 | void getStateInformation (MemoryBlock& destData) override; 59 | void setStateInformation (const void* data, int sizeInBytes) override; 60 | 61 | //================= CUSTOM ======================= 62 | void setImpulseResponse(const AudioSampleBuffer& impulseResponseBuffer, const juce::String pathToImpulse = ""); 63 | private: 64 | // juce::ScopedPointer > mConvolutionManager[2]; 65 | ConvolutionManager mConvolutionManager[2]; 66 | juce::CriticalSection mLoadingLock; 67 | float mSampleRate; 68 | int mBufferSize; 69 | juce::String mImpulseResponseFilePath; 70 | //============================================================================== 71 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RtconvolveAudioProcessor) 72 | }; 73 | 74 | 75 | #endif // PLUGINPROCESSOR_H_INCLUDED 76 | -------------------------------------------------------------------------------- /Source/RefCountedAudioBuffer.h: -------------------------------------------------------------------------------- 1 | // 2 | // RefCountedAudioBuffer.h 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/9/17. 6 | // 7 | // 8 | 9 | #ifndef RefCountedAudioBuffer_h 10 | #define RefCountedAudioBuffer_h 11 | 12 | #include "../JuceLibraryCode/JuceHeader.h" 13 | 14 | template 15 | class RefCountedAudioBuffer: public juce::AudioBuffer, public juce::ReferenceCountedObject 16 | { 17 | public: 18 | RefCountedAudioBuffer(int numChannels, int size) 19 | : juce::AudioBuffer(numChannels, size) 20 | { 21 | } 22 | }; 23 | 24 | #endif /* RefCountedAudioBuffer_h */ 25 | -------------------------------------------------------------------------------- /Source/TimeDistributedFFTConvolver.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimeDistributedFFTConvolver.h 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/6/17. 6 | // 7 | // 8 | 9 | #ifndef TimeDistributedFFTConvolver_h 10 | #define TimeDistributedFFTConvolver_h 11 | 12 | #include "../JuceLibraryCode/JuceHeader.h" 13 | #include "RefCountedAudioBuffer.h" 14 | 15 | 16 | /** 17 | The TimeDistributedFFTConvolver class computes the convolution of 18 | the input with an impulse response using a slightly modified version 19 | of the time-distributed fast Fourier Transform described in Jeffrey R. Hurchalla's 20 | paper 'A Time Distributed FFT for Efficient Low Latency Convolution'. 21 | The convolved output will be available at the beginning of the output buffer 22 | 8 'base time periods' (host audio buffer size) from when the corresponding input 23 | was supplied in the call to 'processInput()'. There is therefore an input-output 24 | delay of eight times the number of samples in one base time period. 25 | 26 | */ 27 | template 28 | class TimeDistributedFFTConvolver 29 | { 30 | public: 31 | 32 | /** 33 | Convenience enum for expressing the state of the object. 34 | */ 35 | enum 36 | { 37 | kPhase0 = 0, 38 | kPhase1, 39 | kPhase2, 40 | kPhase3, 41 | kNumPhases 42 | }; 43 | 44 | /** 45 | Construct a Time Distributed FFT-based object. 46 | @param impulseResponse 47 | A pointer to a buffer holding the impulse response with which 48 | the subsequent input will be convolved. 49 | @param numSamplesImpulseResponse 50 | The length in samples of the impulse response. 51 | @param bufferSize 52 | The host audio application's audio buffer size, or 'base time period'. 53 | */ 54 | TimeDistributedFFTConvolver(FLOAT_TYPE *impulseResponse, int numSamplesImpulseResponse, int bufferSize); 55 | 56 | /** 57 | Perform one base time period's worth of work for the convolution. The convolved 58 | output corresponding to this input will be ready 8 base time periods from when this 59 | method is called. 60 | @param input 61 | The input is expected to hold a number of samples equal to the 'bufferSize' 62 | specified in the constructor. 63 | */ 64 | void processInput(FLOAT_TYPE *input); 65 | 66 | /** 67 | Obtain a pointer to one base time period's worth of output samples. 68 | @returns 69 | A pointer to the beginning of the output buffer. 70 | */ 71 | const FLOAT_TYPE *getOutputBuffer() const 72 | { 73 | int startIndex = mCurrentPhase * mNumSamplesBaseTimePeriod; 74 | return mOutputReal->getReadPointer(0) + startIndex; 75 | } 76 | 77 | private: 78 | int mNumSamplesBaseTimePeriod; 79 | juce::ReferenceCountedObjectPtr > mBuffersReal[3]; 80 | juce::ReferenceCountedObjectPtr > mBuffersImag[3]; 81 | juce::OwnedArray > mImpulsePartitionsReal; 82 | juce::OwnedArray > mImpulsePartitionsImag; 83 | juce::OwnedArray > mInputReal; 84 | juce::OwnedArray > mInputImag; 85 | juce::ScopedPointer > mOutputReal; 86 | juce::ScopedPointer > mOutputImag; 87 | juce::ScopedPointer > mPreviousTail; 88 | 89 | int mNumPartitions; 90 | int mCurrentPhase; 91 | int mCurrentInputIndex; 92 | 93 | /** 94 | Perform the 'decimation in frequency' forward decomposotition work for a quarter of the input. 95 | 96 | @param rex 97 | The real part of the length-N input. 98 | @param imx 99 | The imaginary part of the length-N input. 100 | @param N 101 | The length (in samples) of the input. The last N/2 samples are assumed to be 102 | a padding of zeros. 103 | @param whichQuarter
104 | 0 - Perform decomposition for 1st quarter of the non-pad input (first half of input buffer)
105 | 1 - Perform decomposition for 2nd quarter of the non-pad input (first half of input buffer)
106 | 2 - Perform decomposition for 3rd quarter of the non-pad input (first half of input buffer)
107 | 3 - Perform decomposition for 4th quarter of the non-pad input (first half of input buffer)
108 | Parameter must be one of these four values, or an exception will be thrown. 109 | */ 110 | void forwardDecomposition(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int N, int whichQuarter); 111 | 112 | /** 113 | Convenience function to perform the forward decomposition work for all four quarters 114 | of the input buffer. 115 | 116 | @param rex 117 | The real part of the length-N input. 118 | @param imx 119 | The imaginary part of the length-N input. 120 | @param N 121 | The length (in samples) of the input. The last N/2 samples are assumed to be 122 | a padding of zeros. 123 | */ 124 | void forwardDecompositionComplete(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int N); 125 | 126 | /** 127 | Perform the 'decimation in frequency' inverse decomposotition work for a quarter of the input. 128 | 129 | @param rex 130 | The real part of the length-N input. 131 | @param imx 132 | The imaginary part of the length-N input. 133 | @param N 134 | The length (in samples) of the input. The last N/2 samples are assumed to be 135 | a padding of zeros. 136 | @param whichQuarter
137 | 0 - Perform decomposition for 1st quarter of the non-pad input (first half of input buffer)
138 | 1 - Perform decomposition for 2nd quarter of the non-pad input (first half of input buffer)
139 | 2 - Perform decomposition for 3rd quarter of the non-pad input (first half of input buffer)
140 | 3 - Perform decomposition for 4th quarter of the non-pad input (first half of input buffer)
141 | Parameter must be one of these four values, or an exception will be thrown. 142 | */ 143 | void inverseDecomposition(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int N, int whichQuarter); 144 | 145 | /** 146 | Convenience function to perform the inverse decomposition work for all four quarters 147 | of the input buffer. 148 | 149 | @param rex 150 | The real part of the length-N input. 151 | @param imx 152 | The imaginary part of the length-N input. 153 | @param N 154 | The length (in samples) of the input. The last N/2 samples are assumed to be 155 | a padding of zeros. 156 | */ 157 | void inverseDecompositionComplete(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int N); 158 | 159 | /** 160 | Computes complex multiplications in the frequency domain for half of a sub-fft's 161 | convolutions. 162 | @param subArray
163 | 0 - the X(2k), ie. 'even' frequency bins 164 | 1 - the X(2k+1), ie. 'odd' frequency bins 165 | @param whichHalf
166 | 0 - 1st half of 'subArray'. 167 | 1 - 2nd half of 'subArray'. 168 | */ 169 | void performConvolutions(int subArray, int whichHalf); 170 | 171 | /** 172 | Internal helper function for updating internal data structures for a new phase of the 173 | overall operation. 174 | */ 175 | void promoteBuffers(); 176 | 177 | /** 178 | Add the previous convolution tail to the output buffer. 179 | */ 180 | void prepareOutput(); 181 | 182 | /** 183 | Custom version of the fast fourier transform in which the frequency bins of the output are arranged 184 | in the order needed for frequency domain convolution implemented by this class. 185 | */ 186 | void fft_priv(FLOAT_TYPE *rex, FLOAT_TYPE *imx, FLOAT_TYPE *trx, FLOAT_TYPE *tix, int N); 187 | }; 188 | 189 | #include "TimeDistributedFFTConvolver.hpp" 190 | 191 | #endif /* TimeDistributedFFTConvolver_h */ 192 | -------------------------------------------------------------------------------- /Source/TimeDistributedFFTConvolver.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // TimeDistributedFFTConvolver.hpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/6/17. 6 | // 7 | // 8 | 9 | #include "util/util.h" 10 | #include "util/fft.hpp" 11 | #include 12 | 13 | static const float TWOPI = 2.0 * M_PI; 14 | static const float NTWOPI = -2.0 * M_PI; 15 | 16 | template 17 | TimeDistributedFFTConvolver::TimeDistributedFFTConvolver(FLOAT_TYPE *impulseResponse, int numSamplesImpulseResponse, int bufferSize) 18 | : mCurrentPhase(kPhase3) 19 | , mCurrentInputIndex(0) 20 | { 21 | mNumSamplesBaseTimePeriod = bufferSize; 22 | 23 | int partitionSize = 4 * mNumSamplesBaseTimePeriod; 24 | 25 | if (isPowerOfTwo(bufferSize) == false) 26 | { 27 | throw std::invalid_argument("bufferSize must be a power of 2"); 28 | } 29 | 30 | mNumPartitions = (numSamplesImpulseResponse / partitionSize) + !!(numSamplesImpulseResponse % partitionSize); 31 | 32 | for (int i = 0; i < mNumPartitions; ++i) 33 | { 34 | int samplesToCopy = std::min((numSamplesImpulseResponse - (i * partitionSize)), partitionSize); 35 | 36 | /* Allocate and fill real array */ 37 | 38 | mImpulsePartitionsReal.add(new juce::AudioBuffer(1, 2 * partitionSize)); 39 | checkNull(mImpulsePartitionsReal[i]); 40 | mImpulsePartitionsReal[i]->clear(); 41 | 42 | FLOAT_TYPE *partition = mImpulsePartitionsReal[i]->getWritePointer(0); 43 | memcpy(partition, impulseResponse + (i * partitionSize), samplesToCopy * sizeof(FLOAT_TYPE)); 44 | 45 | /* Allocate imag array */ 46 | 47 | mImpulsePartitionsImag.add(new juce::AudioBuffer(1, 2 * partitionSize)); 48 | checkNull(mImpulsePartitionsReal[i]); 49 | 50 | mImpulsePartitionsImag[i]->clear(); 51 | FLOAT_TYPE *partitionImag = mImpulsePartitionsImag[i]->getWritePointer(0); 52 | 53 | FLOAT_TYPE *tempReal = new FLOAT_TYPE[2 * partitionSize]; 54 | checkNull(tempReal); 55 | memset(tempReal, 0, 2 * partitionSize * sizeof(FLOAT_TYPE)); 56 | 57 | FLOAT_TYPE *tempImag = new FLOAT_TYPE[2 * partitionSize]; 58 | checkNull(tempImag); 59 | memset(tempImag, 0, 2 * partitionSize * sizeof(FLOAT_TYPE)); 60 | 61 | /* Calculate transform */ 62 | fft_priv(partition, partitionImag, tempReal, tempImag, 2 * partitionSize); 63 | 64 | delete [] tempReal; 65 | delete [] tempImag; 66 | 67 | /* Allocate an input buffer */ 68 | mInputReal.add(new juce::AudioBuffer(1, 2 * partitionSize)); 69 | mInputImag.add(new juce::AudioBuffer(1, 2 * partitionSize)); 70 | mInputReal[i]->clear(); 71 | mInputImag[i]->clear(); 72 | } 73 | 74 | mOutputReal = new juce::AudioBuffer(1, 2 * partitionSize); 75 | checkNull(mOutputReal); 76 | mOutputReal->clear(); 77 | mOutputImag = new juce::AudioBuffer(1, 2 * partitionSize); 78 | checkNull(mOutputImag); 79 | mOutputImag->clear(); 80 | 81 | mBuffersReal[0] = new RefCountedAudioBuffer(1, 2 * partitionSize); 82 | mBuffersReal[1] = new RefCountedAudioBuffer(1, 2 * partitionSize); 83 | mBuffersReal[2] = new RefCountedAudioBuffer(1, 2 * partitionSize); 84 | 85 | mBuffersImag[0] = new RefCountedAudioBuffer(1, 2 * partitionSize); 86 | mBuffersImag[1] = new RefCountedAudioBuffer(1, 2 * partitionSize); 87 | mBuffersImag[2] = new RefCountedAudioBuffer(1, 2 * partitionSize); 88 | 89 | for (int i = 0; i < 3; ++i) 90 | { 91 | mBuffersReal[i]->clear(); 92 | mBuffersImag[i]->clear(); 93 | } 94 | 95 | mPreviousTail = new juce::AudioBuffer(1, partitionSize); 96 | checkNull(mPreviousTail); 97 | mPreviousTail->clear(); 98 | } 99 | 100 | template 101 | void TimeDistributedFFTConvolver::processInput(FLOAT_TYPE *input) 102 | { 103 | ReferenceCountedObjectPtr > temp; 104 | int partitionSize = 4 * mNumSamplesBaseTimePeriod; 105 | mCurrentPhase = trueMod((mCurrentPhase + 1), 4); 106 | int Q = mCurrentPhase * mNumSamplesBaseTimePeriod; 107 | 108 | switch (mCurrentPhase) 109 | { 110 | case kPhase0: 111 | { 112 | promoteBuffers(); 113 | 114 | /* Buffer 'C' */ 115 | mBuffersReal[2]->clear(); 116 | mBuffersImag[2]->clear(); 117 | 118 | FLOAT_TYPE *cr = mBuffersReal[2]->getWritePointer(0); 119 | memcpy(cr + Q, input, mNumSamplesBaseTimePeriod * sizeof(FLOAT_TYPE)); 120 | 121 | FLOAT_TYPE *ci = mBuffersImag[2]->getWritePointer(0); 122 | 123 | forwardDecomposition(cr, ci, 2 * partitionSize, 0); 124 | 125 | /* Buffer 'B' */ 126 | 127 | FLOAT_TYPE *br = mBuffersReal[1]->getWritePointer(0); 128 | FLOAT_TYPE *bi = mBuffersImag[1]->getWritePointer(0); 129 | 130 | fft(br, bi, partitionSize); /* X(2k) */ 131 | 132 | FLOAT_TYPE *rex0 = mInputReal[mCurrentInputIndex]->getWritePointer(0); 133 | FLOAT_TYPE *imx0 = mInputImag[mCurrentInputIndex]->getWritePointer(0); 134 | 135 | memcpy(rex0, br, partitionSize * sizeof(FLOAT_TYPE)); 136 | memcpy(imx0, bi, partitionSize * sizeof(FLOAT_TYPE)); 137 | 138 | performConvolutions(0, 0); /* Perform first half of convolutions for vector Y(2k) */ 139 | 140 | /* Buffer 'A' */ 141 | FLOAT_TYPE *ar = mBuffersReal[0]->getWritePointer(0); 142 | FLOAT_TYPE *ai = mBuffersImag[0]->getWritePointer(0); 143 | 144 | inverseDecomposition(ar, ai, 2 * partitionSize, 0); 145 | prepareOutput(); 146 | break; 147 | } 148 | case kPhase1: 149 | { 150 | /* Buffer 'C' */ 151 | FLOAT_TYPE *cr = mBuffersReal[2]->getWritePointer(0); 152 | memcpy(cr + Q, input, mNumSamplesBaseTimePeriod * sizeof(FLOAT_TYPE)); 153 | 154 | FLOAT_TYPE *ci = mBuffersImag[2]->getWritePointer(0); 155 | forwardDecomposition(cr, ci, 2 * partitionSize, 1); 156 | 157 | /* Buffer 'B' */ 158 | FLOAT_TYPE *br = mBuffersReal[1]->getWritePointer(0); 159 | FLOAT_TYPE *bi = mBuffersImag[1]->getWritePointer(0); 160 | performConvolutions(0, 1); 161 | ifft(br, bi, partitionSize); /* Y(2k) sub-ifft */ 162 | 163 | /* Buffer 'A' */ 164 | FLOAT_TYPE *ar = mBuffersReal[0]->getWritePointer(0); 165 | FLOAT_TYPE *ai = mBuffersImag[0]->getWritePointer(0); 166 | 167 | inverseDecomposition(ar, ai, 2 * partitionSize, 1); 168 | prepareOutput(); 169 | break; 170 | } 171 | case kPhase2: 172 | { 173 | /* Buffer 'C' */ 174 | FLOAT_TYPE *cr = mBuffersReal[2]->getWritePointer(0); 175 | memcpy(cr + Q, input, mNumSamplesBaseTimePeriod * sizeof(FLOAT_TYPE)); 176 | 177 | FLOAT_TYPE *ci = mBuffersImag[2]->getWritePointer(0); 178 | forwardDecomposition(cr, ci, 2 * partitionSize, 2); 179 | 180 | /* Buffer 'B' */ 181 | FLOAT_TYPE *br = mBuffersReal[1]->getWritePointer(0); 182 | FLOAT_TYPE *bi = mBuffersImag[1]->getWritePointer(0); 183 | FLOAT_TYPE *rex0 = mInputReal[mCurrentInputIndex]->getWritePointer(0); 184 | FLOAT_TYPE *imx0 = mInputImag[mCurrentInputIndex]->getWritePointer(0); 185 | 186 | fft(br + partitionSize, bi + partitionSize, partitionSize); 187 | memcpy(rex0 + partitionSize, br + partitionSize, partitionSize * sizeof(FLOAT_TYPE)); 188 | memcpy(imx0 + partitionSize, bi + partitionSize, partitionSize * sizeof(FLOAT_TYPE)); 189 | performConvolutions(1, 0); 190 | 191 | /* Buffer 'A' */ 192 | FLOAT_TYPE *ar = mBuffersReal[0]->getWritePointer(0); 193 | FLOAT_TYPE *ai = mBuffersImag[0]->getWritePointer(0); 194 | 195 | inverseDecomposition(ar, ai, 2 * partitionSize, 2); 196 | prepareOutput(); 197 | break; 198 | } 199 | case kPhase3: 200 | { 201 | /* Buffer 'C' */ 202 | FLOAT_TYPE *cr = mBuffersReal[2]->getWritePointer(0); 203 | memcpy(cr + Q, input, mNumSamplesBaseTimePeriod * sizeof(FLOAT_TYPE)); 204 | 205 | FLOAT_TYPE *ci = mBuffersImag[2]->getWritePointer(0); 206 | forwardDecomposition(cr, ci, 2 * partitionSize, 3); 207 | 208 | /* Buffer 'B' */ 209 | FLOAT_TYPE *br = mBuffersReal[1]->getWritePointer(0); 210 | FLOAT_TYPE *bi = mBuffersImag[1]->getWritePointer(0); 211 | performConvolutions(1, 1); 212 | ifft(br + partitionSize, bi + partitionSize, partitionSize); /* Y(2k+1) sub-ifft */ 213 | 214 | /* Buffer 'A' */ 215 | FLOAT_TYPE *ar = mBuffersReal[0]->getWritePointer(0); 216 | FLOAT_TYPE *ai = mBuffersImag[0]->getWritePointer(0); 217 | 218 | inverseDecomposition(ar, ai, 2 * partitionSize, 3); 219 | 220 | prepareOutput(); 221 | break; 222 | } 223 | } 224 | } 225 | 226 | template 227 | void TimeDistributedFFTConvolver::prepareOutput() 228 | { 229 | FLOAT_TYPE *out = mOutputReal->getWritePointer(0); 230 | FLOAT_TYPE *ar = mBuffersReal[0]->getWritePointer(0); 231 | FLOAT_TYPE *tail = mPreviousTail->getWritePointer(0); 232 | int partitionSize = 4 * mNumSamplesBaseTimePeriod; 233 | int startIndex = mCurrentPhase * mNumSamplesBaseTimePeriod; 234 | 235 | for (int i = 0; i < mNumSamplesBaseTimePeriod; ++i) 236 | { 237 | int j = startIndex + i; 238 | out[j] = ar[j] + tail[j]; 239 | tail[j] = ar[j + partitionSize]; 240 | } 241 | } 242 | 243 | template 244 | void TimeDistributedFFTConvolver::promoteBuffers() 245 | { 246 | ReferenceCountedObjectPtr > temp = mBuffersReal[0]; 247 | mBuffersReal[0] = mBuffersReal[1]; 248 | mBuffersReal[1] = mBuffersReal[2]; 249 | mBuffersReal[2] = temp; 250 | 251 | temp = mBuffersImag[0]; 252 | mBuffersImag[0] = mBuffersImag[1]; 253 | mBuffersImag[1] = mBuffersImag[2]; 254 | mBuffersImag[2] = temp; 255 | 256 | mCurrentInputIndex = trueMod((mCurrentInputIndex + 1), mNumPartitions); 257 | } 258 | 259 | template 260 | void TimeDistributedFFTConvolver::forwardDecomposition(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int length, int whichQuarter) 261 | { 262 | if (whichQuarter > 3) 263 | { 264 | throw std::invalid_argument("Parameter \"whichQuarter\" must be between 0 and 3"); 265 | } 266 | 267 | int N = length; 268 | FLOAT_TYPE *re = rex; 269 | FLOAT_TYPE *im = imx; 270 | 271 | int N8 = N >> 3; 272 | int N2 = N >> 1; 273 | int Q = whichQuarter * N8; 274 | 275 | for (int i = 0; i < N8; ++i) 276 | { 277 | int j = i + Q; 278 | FLOAT_TYPE frac = j / (float)N; 279 | FLOAT_TYPE zr = cos(TWOPI * frac); /* Twiddle factor real part */ 280 | FLOAT_TYPE zi = sin(NTWOPI * frac); /* Twiddle factor imag part */ 281 | FLOAT_TYPE rea; /* an real part */ 282 | FLOAT_TYPE ima; /* an imag part */ 283 | FLOAT_TYPE reb; /* bn real part */ 284 | FLOAT_TYPE imb; /* bn imag part */ 285 | 286 | rea = re[j] + re[j+N2]; 287 | ima = im[j] + im[j+N2]; 288 | reb = re[j] - re[j+N2]; 289 | imb = im[j] - im[j+N2]; 290 | 291 | re[j] = rea; 292 | im[j] = ima; 293 | re[j+N2] = (reb * zr) - (imb * zi); 294 | im[j+N2] = (reb * zi) - (imb * zr); 295 | } 296 | } 297 | 298 | template 299 | void TimeDistributedFFTConvolver::forwardDecompositionComplete(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int length) 300 | { 301 | for (int i = 0; i < 4; ++i) 302 | { 303 | forwardDecomposition(rex, imx, length, i); 304 | } 305 | } 306 | 307 | 308 | template 309 | void TimeDistributedFFTConvolver::inverseDecomposition(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int N, int whichQuarter) 310 | { 311 | if (whichQuarter > 3) 312 | { 313 | throw std::invalid_argument("Parameter \"whichQuarter\" must be between 0 and 3"); 314 | } 315 | 316 | FLOAT_TYPE *re = rex; 317 | FLOAT_TYPE *im = imx; 318 | 319 | int N8 = N >> 3; 320 | int N2 = N >> 1; 321 | int Q = whichQuarter * N8; 322 | 323 | for (int i = 0; i < N8; ++i) 324 | { 325 | int j = i + Q; 326 | FLOAT_TYPE frac = j / (float)N; 327 | FLOAT_TYPE twr = cos(TWOPI * frac); 328 | FLOAT_TYPE twi = sin(TWOPI * frac); 329 | FLOAT_TYPE rea = re[j]; 330 | FLOAT_TYPE ima = im[j]; 331 | FLOAT_TYPE reb = (re[j + N2] * twr) - (im[j + N2] * twi); 332 | FLOAT_TYPE imb = (re[j + N2] * twi) + (im[j + N2] * twr); 333 | re[j] = (rea + reb) * 0.5; 334 | im[j] = (ima + imb) * 0.5; 335 | re[j+N2] = (rea - reb) * 0.5; 336 | im[j+N2] = (ima - imb) * 0.5; 337 | } 338 | } 339 | 340 | 341 | template 342 | void TimeDistributedFFTConvolver::inverseDecompositionComplete(FLOAT_TYPE *rex, FLOAT_TYPE *imx, int length) 343 | { 344 | for (int i = 0; i < 4; ++i) 345 | { 346 | inverseDecomposition(rex, imx, length, i); 347 | } 348 | } 349 | 350 | template 351 | void TimeDistributedFFTConvolver::performConvolutions(int subArray, int whichHalf) 352 | { 353 | int N = 2 * mNumSamplesBaseTimePeriod; 354 | int startIndex = (subArray * 2 * N) + (whichHalf * N); 355 | 356 | FLOAT_TYPE *rex = nullptr; 357 | FLOAT_TYPE *imx = nullptr; 358 | 359 | FLOAT_TYPE *rey = mBuffersReal[1]->getWritePointer(0) + startIndex; 360 | FLOAT_TYPE *imy = mBuffersImag[1]->getWritePointer(0) + startIndex; 361 | FLOAT_TYPE *reh = nullptr; 362 | FLOAT_TYPE *imh = nullptr; 363 | 364 | memset(rey, 0, N * sizeof(FLOAT_TYPE)); 365 | memset(imy, 0, N * sizeof(FLOAT_TYPE)); 366 | 367 | for (int i = 0; i < mNumPartitions; ++i) 368 | { 369 | int k = trueMod((mCurrentInputIndex - i), mNumPartitions); 370 | 371 | rex = mInputReal[k]->getWritePointer(0) + startIndex; 372 | imx = mInputImag[k]->getWritePointer(0) + startIndex; 373 | reh = mImpulsePartitionsReal[i]->getWritePointer(0) + startIndex; 374 | imh = mImpulsePartitionsImag[i]->getWritePointer(0) + startIndex; 375 | 376 | for (int j = 0; j < N; ++j) 377 | { 378 | rey[j] += (rex[j] * reh[j]) - (imx[j] * imh[j]); 379 | imy[j] += (rex[j] * imh[j]) + (imx[j] * reh[j]); 380 | } 381 | } 382 | } 383 | 384 | /** 385 | This function computes the FFT with the frequency domain bins arranged in 386 | the specific order needed for the frequency domain convolutions 387 | */ 388 | template 389 | void TimeDistributedFFTConvolver::fft_priv(FLOAT_TYPE *rex, FLOAT_TYPE *imx, FLOAT_TYPE *trx, FLOAT_TYPE *tix, int N) 390 | { 391 | forwardDecompositionComplete(rex, imx, N); 392 | int N2 = N >> 1; 393 | 394 | fft(rex, imx, N2); 395 | fft(rex+N2, imx+N2, N2); 396 | } 397 | 398 | -------------------------------------------------------------------------------- /Source/UniformPartitionConvolver.h: -------------------------------------------------------------------------------- 1 | // 2 | // UniformPartitionConvolver.hpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/5/17. 6 | // 7 | // 8 | 9 | #ifndef UniformPartitionConvolver_hpp 10 | #define UniformPartitionConvolver_hpp 11 | 12 | #include 13 | #include "../JuceLibraryCode/JuceHeader.h" 14 | 15 | /** 16 | The UPConvolver class computes the convolution via FFT of the input 17 | with some impulse response using the uniform-partition method. 18 | */ 19 | template 20 | class UPConvolver 21 | { 22 | public: 23 | 24 | /** 25 | Construct a UPConvolver object. 26 | @param impulseResponse 27 | A pointer to the samples of an impulse response. 28 | @param numSamples 29 | The number of samples in the impulse response. 30 | @param bufferSize 31 | The host audio applications buffer size. 32 | @param maxPartitions 33 | The maximum number of partitions to use. If the full impulse response 34 | requires a number of partions greater than 'maxPartitions', only the first 35 | 'maxPartitions' sections of the IR will be used. 36 | */ 37 | UPConvolver(FLOAT_TYPE *impulseResponse, int numSamples, int bufferSize, int maxPartitions); 38 | 39 | /** 40 | Perform one base time period's worth of work for the convolution. 41 | @param input 42 | The input is expected to hold a number of samples equal to the 'bufferSize' 43 | specified in the constructor. 44 | */ 45 | void processInput(FLOAT_TYPE *input); 46 | 47 | /** 48 | @returns 49 | A pointer to the output buffer 50 | */ 51 | const FLOAT_TYPE *getOutputBuffer() const 52 | { 53 | return mOutputReal->getReadPointer(0); 54 | }; 55 | 56 | private: 57 | juce::OwnedArray > mImpulsePartitionsReal; 58 | juce::OwnedArray > mImpulsePartitionsImag; 59 | 60 | juce::OwnedArray > mInputReal; 61 | juce::OwnedArray > mInputImag; 62 | 63 | juce::ScopedPointer > mPreviousOutputTail; 64 | juce::ScopedPointer > mOutputReal; 65 | juce::ScopedPointer > mOutputImag; 66 | 67 | int mBufferSize; 68 | int mNumPartitions; 69 | 70 | int mCurrentInputSegment; 71 | 72 | void process(); 73 | 74 | }; 75 | 76 | #include "UniformPartitionConvolver.hpp" 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /Source/UniformPartitionConvolver.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // UniformPartitionConvolver.cpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/5/17. 6 | // 7 | // 8 | 9 | #include "util/util.h" 10 | #include "util/fft.hpp" 11 | #include 12 | 13 | template 14 | UPConvolver::UPConvolver(FLOAT_TYPE *impulseResponse, int numSamples, int bufferSize, int maxPartitions) 15 | : mCurrentInputSegment(0) 16 | { 17 | if (isPowerOfTwo(bufferSize) == false) 18 | { 19 | throw std::invalid_argument("bufferSize must be a power of 2"); 20 | } 21 | 22 | int numPartitions = (numSamples / bufferSize) + !!(numSamples % bufferSize); 23 | 24 | if (numPartitions > maxPartitions) 25 | { 26 | numPartitions = maxPartitions; 27 | } 28 | 29 | mNumPartitions = numPartitions; 30 | mBufferSize = bufferSize; 31 | 32 | for (int i = 0; i < numPartitions; ++i) 33 | { 34 | int samplesToCopy = std::min((numSamples - (i * mBufferSize)), mBufferSize); 35 | 36 | /* Allocate and fill real array */ 37 | 38 | mImpulsePartitionsReal.add(new juce::AudioBuffer(1, 2 * mBufferSize)); 39 | checkNull(mImpulsePartitionsReal[i]); 40 | mImpulsePartitionsReal[i]->clear(); 41 | 42 | FLOAT_TYPE *partition = mImpulsePartitionsReal[i]->getWritePointer(0); 43 | memcpy(partition, impulseResponse + (i * mBufferSize), samplesToCopy * sizeof(FLOAT_TYPE)); 44 | 45 | /* Allocate imag array */ 46 | 47 | mImpulsePartitionsImag.add(new juce::AudioBuffer(1, 2 * mBufferSize)); 48 | checkNull(mImpulsePartitionsReal[i]); 49 | 50 | mImpulsePartitionsImag[i]->clear(); 51 | FLOAT_TYPE *partitionImag = mImpulsePartitionsImag[i]->getWritePointer(0); 52 | 53 | /* Calculate transform */ 54 | fft(partition, partitionImag, 2 * mBufferSize); 55 | 56 | /* Allocate an input buffer */ 57 | mInputReal.add(new juce::AudioBuffer(1, 2 * mBufferSize)); 58 | mInputImag.add(new juce::AudioBuffer(1, 2 * mBufferSize)); 59 | mInputReal[i]->clear(); 60 | mInputImag[i]->clear(); 61 | } 62 | 63 | mOutputReal = new juce::AudioBuffer(1, 2 * mBufferSize); 64 | checkNull(mOutputReal); 65 | 66 | mOutputImag = new juce::AudioBuffer(1, 2 * mBufferSize); 67 | checkNull(mOutputImag); 68 | mOutputImag->clear(); 69 | 70 | mPreviousOutputTail = new juce::AudioBuffer(1, mBufferSize); 71 | checkNull(mPreviousOutputTail); 72 | mPreviousOutputTail->clear(); 73 | } 74 | 75 | template 76 | void UPConvolver::processInput(FLOAT_TYPE *input) 77 | { 78 | FLOAT_TYPE *currentSegmentReal = mInputReal[mCurrentInputSegment]->getWritePointer(0); 79 | FLOAT_TYPE *currentSegmentImag = mInputImag[mCurrentInputSegment]->getWritePointer(0); 80 | 81 | mInputReal[mCurrentInputSegment]->clear(); 82 | mInputImag[mCurrentInputSegment]->clear(); 83 | 84 | memcpy(currentSegmentReal, input, mBufferSize * sizeof(FLOAT_TYPE)); 85 | fft(currentSegmentReal, currentSegmentImag, 2 * mBufferSize); 86 | 87 | process(); 88 | } 89 | 90 | 91 | template 92 | void UPConvolver::process() 93 | { 94 | mOutputReal->clear(); 95 | mOutputImag->clear(); 96 | 97 | FLOAT_TYPE *rey = mOutputReal->getWritePointer(0); 98 | FLOAT_TYPE *imy = mOutputImag->getWritePointer(0); 99 | 100 | for (int j = 0; j < mNumPartitions; ++j) 101 | { 102 | int k = trueMod(mCurrentInputSegment - j, mNumPartitions); 103 | 104 | const FLOAT_TYPE *rex = mInputReal[k]->getReadPointer(0); 105 | const FLOAT_TYPE *imx = mInputImag[k]->getReadPointer(0); 106 | const FLOAT_TYPE *reh = mImpulsePartitionsReal[j]->getReadPointer(0); 107 | const FLOAT_TYPE *imh = mImpulsePartitionsImag[j]->getReadPointer(0); 108 | 109 | for (int i = 0; i < (2 * mBufferSize); ++i) 110 | { 111 | rey[i] += (rex[i] * reh[i]) - (imx[i] * imh[i]); 112 | imy[i] += (rex[i] * imh[i]) + (imx[i] * reh[i]); 113 | } 114 | } 115 | 116 | ifft(rey, imy, 2 * mBufferSize); 117 | FLOAT_TYPE *tail = mPreviousOutputTail->getWritePointer(0); 118 | 119 | for (int i = 0; i < mBufferSize; ++i) 120 | { 121 | rey[i] += tail[i]; 122 | tail[i] = rey[i + mBufferSize]; 123 | } 124 | 125 | mCurrentInputSegment = (mCurrentInputSegment + 1) % mNumPartitions; 126 | } 127 | -------------------------------------------------------------------------------- /Source/util/SincFilter.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // SincFilter.hpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/5/17. 6 | // 7 | // 8 | 9 | #ifndef SincFilter_hpp 10 | #define SincFilter_hpp 11 | 12 | #include 13 | #include 14 | 15 | template 16 | void genSincFilter(FLOAT_TYPE *x, int N, FLOAT_TYPE normalizedCutoff) 17 | { 18 | FLOAT_TYPE omega = 2.0 * M_PI * normalizedCutoff; 19 | FLOAT_TYPE N2 = N / 2.0; 20 | FLOAT_TYPE sum = 0; 21 | 22 | for (int i = 0; i < N; ++i) 23 | { 24 | if (i == N/2) 25 | { 26 | x[i] = 1; 27 | } 28 | else 29 | { 30 | x[i] = sin(omega * (i - N2)) / (i - N2); 31 | } 32 | 33 | sum += x[i]; 34 | } 35 | 36 | /* Normalize */ 37 | FLOAT_TYPE scale = 1.0 / sum; 38 | 39 | for (int i = 0; i < N; ++i) 40 | { 41 | x[i] *= scale; 42 | } 43 | } 44 | 45 | template 46 | void genImpulse(FLOAT_TYPE *x, int N) 47 | { 48 | x[0] = 1.0; 49 | 50 | for (int i = 1; i < N; ++i) { x[i] = 0; } 51 | } 52 | 53 | #endif /* SincFilter_hpp */ 54 | -------------------------------------------------------------------------------- /Source/util/fft.hpp: -------------------------------------------------------------------------------- 1 | /* Adapted from Stephen Smith's algorithm (www.dspguide.com) */ 2 | 3 | #ifndef __FFT__ 4 | #define __FFT__ 5 | 6 | /* Fast Fourier Transform */ 7 | #ifdef _WIN32 8 | #define _USE_MATH_DEFINES 9 | #ifndef M_PI 10 | #define M_PI 3.14159265358979323846 11 | #endif 12 | static double log2( double n ) 13 | { 14 | // log(n)/log(2) is log2. 15 | return log( n ) / log( 2 ); 16 | } 17 | #endif 18 | #include 19 | template 20 | void fft(T *REX, T *IMX, unsigned int N) 21 | { 22 | const unsigned int NM1 = N - 1; 23 | const unsigned int ND2 = N / 2; 24 | const unsigned int M = (unsigned int)log2((float)N); 25 | unsigned int k; 26 | unsigned int LE, LE2; 27 | unsigned int IP; 28 | int j = ND2; 29 | int i, l; /* Loop counters */ 30 | int jm1; 31 | T TR, TI, UR, UI, SR, SI; 32 | 33 | /* Bit reversal sorting */ 34 | for (i = 1; i < N - 1; ++i) { 35 | if (i >= j) 36 | goto SKIP; 37 | TR = REX[j]; 38 | TI = IMX[j]; 39 | REX[j] = REX[i]; 40 | IMX[j] = IMX[i]; 41 | REX[i] = TR; 42 | IMX[i] = TI; 43 | SKIP: 44 | k = ND2; 45 | while (k <= j) { 46 | j = j-k; 47 | k /= 2; 48 | } 49 | j = j + k; 50 | } 51 | 52 | for (l = 1; l <= M; ++l) { 53 | LE = pow(2.0, l); 54 | LE2 = LE / 2; 55 | UR = 1; 56 | UI = 0; 57 | SR = cos(M_PI / LE2); 58 | SI = sin(M_PI / LE2); 59 | for (j = 1; j <= LE2; ++j) { 60 | jm1 = j - 1; 61 | for (i = jm1; i <= NM1; i += LE) { 62 | IP = i + LE2; 63 | TR = REX[IP] * UR - IMX[IP] * UI; 64 | TI = REX[IP] * UI + IMX[IP] * UR; 65 | REX[IP] = REX[i] - TR; 66 | IMX[IP] = IMX[i] - TI; 67 | REX[i] = REX[i] + TR; 68 | IMX[i] = IMX[i] + TI; 69 | } 70 | TR = UR; 71 | UR = TR * SR - UI * SI; 72 | UI = TR * SI + UI * SR; 73 | } 74 | } 75 | } 76 | 77 | 78 | /* Inverse Fast Fourier Transform */ 79 | template 80 | void ifft(T *REX, T *IMX, unsigned int N) 81 | { 82 | int i, k; 83 | 84 | /* Change the sign of IMX[] */ 85 | for (k = 0; k < N; ++k) 86 | IMX[k] *= -1; 87 | 88 | fft(REX, IMX, N); 89 | 90 | for (i=0; i < N; ++i) { 91 | REX[i] = REX[i] / N; 92 | IMX[i] = -1 * IMX[i] / N; 93 | } 94 | } 95 | #endif -------------------------------------------------------------------------------- /Source/util/util.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // util.cpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/5/17. 6 | // 7 | // 8 | 9 | #include "util.h" 10 | 11 | int isPowerOfTwo (unsigned int x) 12 | { 13 | return ((x != 0) && !(x & (x - 1))); 14 | } 15 | 16 | 17 | void* checkNull(void *x) 18 | { 19 | if (x == nullptr) 20 | { 21 | std::cerr << "Error: could not allocate memory" << std::endl; 22 | throw std::exception(); 23 | } 24 | return x; 25 | } -------------------------------------------------------------------------------- /Source/util/util.h: -------------------------------------------------------------------------------- 1 | // 2 | // util.hpp 3 | // RTConvolve 4 | // 5 | // Created by Graham Barab on 2/5/17. 6 | // 7 | // 8 | 9 | #ifndef util_hpp 10 | #define util_hpp 11 | 12 | #include 13 | #include 14 | 15 | #define TRUEMOD(x, m) ( (x >= 0) ? (x % m) : (m + (x % m)) ) 16 | 17 | int isPowerOfTwo (unsigned int x); 18 | 19 | template 20 | INT_TYPE trueMod(INT_TYPE x, INT_TYPE m) 21 | { 22 | return ( (x >= 0) ? (x % m) : (m + (x % m)) ); 23 | } 24 | 25 | template 26 | T *throwIfNull(T *x) 27 | { 28 | if (x == nullptr) 29 | { 30 | std::cerr << "Error: could not allocate memory" << std::endl; 31 | throw std::exception(); 32 | } 33 | return x; 34 | } 35 | void *checkNull(void *x); 36 | 37 | template 38 | FLOAT_TYPE summation(FLOAT_TYPE *x, int N) 39 | { 40 | FLOAT_TYPE sum = 0.0; 41 | 42 | for (int i = 0; i < N; ++i) 43 | { 44 | sum += fabs(x[i]); 45 | } 46 | return sum; 47 | } 48 | 49 | template 50 | void scaleArray(FLOAT_TYPE *x, int N, FLOAT_TYPE amount) 51 | { 52 | for (int i = 0; i < N; ++i) 53 | { 54 | x[i] *= amount; 55 | } 56 | } 57 | 58 | template 59 | void normalizeStereoImpulseResponse(FLOAT_TYPE *left, FLOAT_TYPE* right, int numSamples) 60 | { 61 | FLOAT_TYPE sumL = summation(left, numSamples); 62 | FLOAT_TYPE sumR = summation(right, numSamples); 63 | 64 | FLOAT_TYPE scale = fabs(20.0f/std::max(sumL, sumR)); 65 | 66 | scaleArray(left, numSamples, scale); 67 | scaleArray(right, numSamples, scale); 68 | } 69 | 70 | template 71 | void normalizeMonoImpulseResponse(FLOAT_TYPE *x, int numSamples) 72 | { 73 | FLOAT_TYPE sum = fabs(summation(x, numSamples)); 74 | scaleArray(x, numSamples, (20.0f/sum)); 75 | } 76 | #endif /* util_hpp */ 77 | --------------------------------------------------------------------------------