HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_impulse_response.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max(), swap()
4#include <cmath> // isnan(), isinf()
5#include <complex>
6#include <iostream>
7#include <vector>
8#include <sstream>
9
12#include "hart_utils.hpp" // floatsEqual(), nextPowerOfTwo()
13
14namespace hart
15{
16
17/// @brief Container for representing an impulse response (IR)
18/// @details It is guaranteed to have a specific sample rate associated with it
19/// @ingroup DataStructures
20template <typename SampleType>
22{
23public:
24 // TODO: Add a ctor that takes a pair of hart::Spectrum instances
25
26 /// @brief Create an IR instance from a pait of time-domain signals
27 /// @param input Signal at the input of the system
28 /// @param output Signal at the output of the system
29 /// @throws hart::SampleRateError if provided audio buffers either
30 /// don't have sample rate, or it has invalid value
31 /// @throws hart::SizeError if the lengths of provided buffers
32 /// (in frames) don't match
33 /// @throws hart::ChannelLayoutError if numbers of channels of the
34 /// provided buffers don't match
35 ImpulseResponse (AudioBuffer<SampleType> input, AudioBuffer<SampleType> output)
36 {
37 if (! input.hasSampleRate())
38 HART_THROW (hart::SampleRateError, "Input buffer should have sample rate associated with it");
39
40 if (! output.hasSampleRate())
41 HART_THROW (hart::SampleRateError, "Output buffer should have sample rate associated with it");
42
43 const SampleType inputSampleRateHz = input.getSampleRateHz();
44 const SampleType outputSampleRateHz = output.getSampleRateHz();
45
46 if (hart::floatsEqual (inputSampleRateHz, (SampleType) 0)
47 || inputSampleRateHz < (SampleType) 0
48 || std::isnan (inputSampleRateHz)
49 || std::isinf (inputSampleRateHz)
50 )
51 {
52 HART_THROW (hart::SampleRateError, "Input buffer should have valid sample rate");
53 }
54
55 if (hart::floatsEqual (outputSampleRateHz, (SampleType) 0)
56 || outputSampleRateHz < (SampleType) 0
57 || std::isnan (outputSampleRateHz)
58 || std::isinf (outputSampleRateHz)
59 )
60 {
61 HART_THROW (hart::SampleRateError, "Output buffer should have valid sample rate");
62 }
63
64 if (! hart::floatsEqual (inputSampleRateHz, outputSampleRateHz))
65 HART_THROW (hart::SampleRateError, "Input and output buffers should have matching sample rates");
66
67 if (input.getNumFrames() != output.getNumFrames())
68 HART_THROW (hart::SizeError, "Input and output buffers should have matching number of frames");
69
70 if (input.getNumChannels() != output.getNumChannels())
71 HART_THROW (hart::ChannelLayoutError, "Input and output buffers should have matching number of channels");
72
73 m_sampleRateHz = inputSampleRateHz;
74
75 std::ostringstream ctorArgumentsRepresentationStream;
76 ctorArgumentsRepresentationStream << input << ", " << output;
77 m_ctorArgumentsRepresentation = ctorArgumentsRepresentationStream.str();
78
79 m_numChannels = input.getNumChannels();
80 m_numFrames = input.getNumFrames();
81 m_frames.resize (m_numChannels * m_numFrames);
82 m_channelPointers.resize (m_numChannels);
83 updateChannelPointers();
84
85 calculateImpulseResponse (input, output);
86 }
87
88 /// @brief Get number of channels
89 /// @return Number of allocated channels
90 size_t getNumChannels() const { return m_numChannels; }
91
92 /// @brief Get number of frames (samples)
93 /// @return Number of allocated frames (samples) in every channel
94 size_t getNumFrames() const { return m_numFrames; }
95
96 /// @brief Get a sample rate metadata
97 /// @return IR's sample rate in Hz
98 double getSampleRateHz() const
99 {
100 // IR should have a valid sample rate associated with it
101 hassert (! std::isnan (m_sampleRateHz));
102 hassert (! std::isinf (m_sampleRateHz));
103 hassert (m_sampleRateHz > (SampleType) 0);
104
105 return m_sampleRateHz;
106 }
107
108 /// @brief Prints readable representation of the IR
109 /// @param stream String stream to append the representation to
110 void represent (std::ostream& stream) const
111 {
112 stream << "ImpulseResponse (" << m_ctorArgumentsRepresentation << ")";
113 }
114
115 /// @brief Get a raw pointer to a specific channel's read-only audio data
116 /// @note The data is guaranteed to have at least `getNumFrames()` items and to be a contiguous non-interleaved block of memory.
117 /// @return Pointer to the IR data of requested channel
118 const SampleType* operator[] (size_t channel) const
119 {
120 return m_channelPointers[channel];
121 }
122
123 /// @brief Prints readable text representation of the ImpulseResponse object into the I/O stream
124 /// @relates ImpulseResponse
125 friend std::ostream& operator<< (std::ostream& stream, const ImpulseResponse& ir)
126 {
127 ir.represent (stream);
128 return stream;
129 }
130
131private:
132 std::string m_ctorArgumentsRepresentation;
133 size_t m_numChannels = 0;
134 size_t m_numFrames = 0;
135 double m_sampleRateHz = nan<double>();
136 std::vector<SampleType> m_frames;
137 std::vector<SampleType*> m_channelPointers;
138
139 void updateChannelPointers()
140 {
141 for (size_t channel = 0; channel < m_numChannels; ++channel)
142 m_channelPointers[channel] = m_numFrames > 0 ? &m_frames[channel * m_numFrames] : nullptr;
143 }
144
145 void calculateImpulseResponse (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
146 {
147 const size_t fftSize = nextPowerOfTwo (std::max<size_t> (1, m_numFrames));
148
149 for (size_t channel = 0; channel < m_numChannels; ++channel)
150 {
151 std::vector<std::complex<double>> inputSpectrum (fftSize);
152 std::vector<std::complex<double>> outputSpectrum (fftSize);
153
154 for (size_t frame = 0; frame < m_numFrames; ++frame)
155 {
156 inputSpectrum[frame] = static_cast<double> (input[channel][frame]);
157 outputSpectrum[frame] = static_cast<double> (output[channel][frame]);
158 }
159
160 performFFT (inputSpectrum, false);
161 performFFT (outputSpectrum, false);
162
163 for (size_t bin = 0; bin < fftSize; ++bin)
164 {
165 if (hart::floatsEqual (std::norm (inputSpectrum[bin]), 0.0))
166 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Cannot calculate impulse response from an input buffer with empty frequency bins");
167
168 outputSpectrum[bin] /= inputSpectrum[bin];
169 }
170
171 performFFT (outputSpectrum, true);
172
173 SampleType* irChannel = m_channelPointers[channel];
174
175 for (size_t frame = 0; frame < m_numFrames; ++frame)
176 irChannel[frame] = static_cast<SampleType> (outputSpectrum[frame].real());
177 }
178 }
179
180 static void performFFT (std::vector<std::complex<double>>& data, bool isInverse)
181 {
182 const size_t n = data.size();
183
184 hassert (n != 0);
185 hassert ((n & (n - 1)) == 0); // Size should be a power of 2
186
187 size_t j = 0;
188
189 for (size_t i = 1; i < n; ++i)
190 {
191 size_t bit = n >> 1;
192
193 while (j & bit)
194 {
195 j ^= bit;
196 bit >>= 1;
197 }
198
199 j ^= bit;
200
201 if (i < j)
202 std::swap (data[i], data[j]);
203 }
204
205 for (size_t len = 2; len <= n; len <<= 1)
206 {
207 const double angle = (isInverse ? hart::twoPi : -hart::twoPi) / static_cast<double> (len);
208 const std::complex<double> wlen (std::cos (angle), std::sin (angle));
209
210 for (size_t i = 0; i < n; i += len)
211 {
212 std::complex<double> w (1.0, 0.0);
213
214 for (size_t jj = 0; jj < len / 2; ++jj)
215 {
216 const std::complex<double> u = data[i + jj];
217 const std::complex<double> v = data[i + jj + len / 2] * w;
218
219 data[i + jj] = u + v;
220 data[i + jj + len / 2] = u - v;
221
222 w *= wlen;
223 }
224 }
225 }
226
227 if (isInverse)
228 {
229 const double scale = 1.0 / static_cast<double> (n);
230
231 for (size_t i = 0; i < n; ++i)
232 data[i] *= scale;
233 }
234 }
235};
236
237} // namespace hart
Thrown when a numbers of channels is mismatched.
Container for representing an impulse response (IR)
size_t getNumFrames() const
Get number of frames (samples)
double getSampleRateHz() const
Get a sample rate metadata.
const SampleType * operator[](size_t channel) const
Get a raw pointer to a specific channel's read-only audio data.
ImpulseResponse(AudioBuffer< SampleType > input, AudioBuffer< SampleType > output)
Create an IR instance from a pait of time-domain signals.
void represent(std::ostream &stream) const
Prints readable representation of the IR.
size_t getNumChannels() const
Get number of channels.
Thrown when sample rate is mismatched or invalid.
Thrown when an unexpected container size is encountered.
Thrown when an inappropriate value is encountered.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns otherwise.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
#define HART_THROW(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message otherwise.
constexpr double twoPi
2 * pi
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static size_t nextPowerOfTwo(size_t x)
Finds next power of 2 after a non-negative number x.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.