├── 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 |
--------------------------------------------------------------------------------