Real-valued discrete Fourier transform¶
We consider the discrete Fourier transform of a sequence composed of \(N\) real numbers \(x_n\):
and its inverse transform:
Note that these relations satisfy the Hermitian symmetry \(X_{N - k} = X_k^*\).
Derivation
This symmetry can facilitate the reduction of the computational load, along with the fact that \(x_n \in \mathbb{R}\).
Order of signals¶
We store \(X_k\): the output of the forward transform and the input of the backward transform in the following manner:
Namely the real part \(\real{X_k}\) is stored first up to \(k = N / 2\), which is followed by the imaginary part. Recall the Hermitian symmetry: \(\imag{X_k} = - \imag{X_{N - k}}\), and accordingly \(\imag{X_0} = \imag{X_{N / 2}} = 0\).
Forward transform¶
We consider the forward transform:
for \(\seq{k}{0}{1}{N - 1}\). Although this can be computed as the discrete Fourier transform of a complex sequence having zero imaginary numbers, we aim at computing it with less computational effort here.
By utilizing the decimation in time (assuming \(N\) is a multiple of \(2\)), we have
for \(\seq{k}{0}{1}{N / 2 - 1}\). Instead of taking this recurrence relation as it is, we introduce \(z_n\) defined as
for \(\seq{n}{0}{1}{N / 2 - 1}\), giving
Their discrete Fourier transforms, due to the linearity, lead to
for \(\seq{k}{0}{1}{N / 2 - 1}\).
Now two discrete Fourier transforms are involved, and we focus on the latter:
Since this is equal to
Derivation
we notice that the latter relation can be obtained from the former; namely we need to evaluate only one discrete Fourier transform
to find
for \(\seq{k}{0}{1}{N / 2 - 1}\).
For the corner case \(k = 0\), we see
In summary, the discrete Fourier transform of a real sequence is computed as follows.
First, we create a signal \(z_n\) composed of \(N / 2\) complex numbers:
where \(\seq{n}{0}{1}{N / 2 - 1}\). Practically, this can be achieved merely by casting the user input as a series of complex numbers; namely no data manipulations are needed.
Next, the discrete Fourier transform of \(z_n\) is evaluated to find
for \(\seq{k}{0}{1}{N / 2 - 1}\):
171dft(nh, 1, trigs, xs, zs);
193const double complex z0 =
194 + 0.5 * zs[2 * k + 0]
195 + 0.5 * zs[2 * k + 1] * I;
197const double complex z1 =
198 + 0.5 * zs[2 * (nh - k) + 0]
199 + 0.5 * zs[2 * (nh - k) + 1] * I;
The result is used to compute
where \(\seq{k}{0}{1}{N / 2 - 1}\):
201const double complex xe = + z0 + conj(z1);
203const double complex xo = - I * z0 + I * conj(z1);
Finally they are adopted to calculate
for \(\seq{k}{0}{1}{N / 2 - 1}\).
Due to the order of signals, it is worthwhile to decompose the real and the imaginary parts:
205// X_k^e + exp(- 2 pi k / nitems) X_k^o
206// X_k^e - exp(- 2 pi k / nitems) X_k^o
207const double complex za = xe + xo * twiddle;
208const double complex zb = xe - xo * twiddle;
209xs[k ] = creal(za);
210xs[k + nh] = cimag(zb);
Note that
is used to get
and as a consequence:
174const double xe = zs[0];
175const double xo = zs[1];
176xs[ 0] = xe + xo;
177xs[nh] = xe - xo;
The whole process is given below for completeness:
158int rdft_exec_f (
159 rdft_plan_t * const plan,
160 double * const xs
161) {
162 if (NULL == plan) {
163 puts("uninitialized plan is passed");
164 return 1;
165 }
166 const size_t nitems = plan->nitems;
167 const size_t nh = nitems / 2;
168 const trig_t * const trigs = plan->trigs;
169 double * const zs = plan->buf;
170 // compute complex dft to find Z_k | 1
171 dft(nh, 1, trigs, xs, zs);
172 // compute FFT of the original real signal, corner cases | 6
173 {
174 const double xe = zs[0];
175 const double xo = zs[1];
176 xs[ 0] = xe + xo;
177 xs[nh] = xe - xo;
178 }
179 // compute FFT of the original real signal, common cases
180 for (size_t k = 1; k < nh; k++) {
181 // trigonometric table stores
182 // cos(+ 2 pi k / nitems)
183 // +
184 // sin(+ 2 pi k / nitems) I
185 // here we need
186 // cos(- 2 pi k / nitems)
187 // +
188 // sin(- 2 pi k / nitems) I
189 const double complex twiddle =
190 + 1. * trigs[k].cos
191 - 1. * trigs[k].sin * I;
192 // compute 1/2 Z_k | 3
193 const double complex z0 =
194 + 0.5 * zs[2 * k + 0]
195 + 0.5 * zs[2 * k + 1] * I;
196 // compute 1/2 Z_{N / 2 - k} | 3
197 const double complex z1 =
198 + 0.5 * zs[2 * (nh - k) + 0]
199 + 0.5 * zs[2 * (nh - k) + 1] * I;
200 // compute X_k^e = + 1/2 Z_k + 1/2 Z_{N / 2 - k}^* | 1
201 const double complex xe = + z0 + conj(z1);
202 // compute X_k^o = - I/2 Z_k + I/2 Z_{N / 2 - k}^* | 1
203 const double complex xo = - I * z0 + I * conj(z1);
204 // real and imaginary parts are stored separately | 6
205 // X_k^e + exp(- 2 pi k / nitems) X_k^o
206 // X_k^e - exp(- 2 pi k / nitems) X_k^o
207 const double complex za = xe + xo * twiddle;
208 const double complex zb = xe - xo * twiddle;
209 xs[k ] = creal(za);
210 xs[k + nh] = cimag(zb);
211 }
212 return 0;
213}
Backward transform¶
We consider the inverse transform:
for \(\seq{n}{0}{1}{N - 1}\), where \(X_k\) should satisfy the Hermitian symmetry:
Also it should be ordered in a specific manner.
The inverse transform is obtained essentially by backtracing the forward procedures. To start, we calculate \(X_k^e\) and \(X_k^o\) following
for \(\seq{k}{0}{1}{N / 2 - 1}\).
Note that, since \(X_k\) does not store the entire signal, we need to recover the series of complex numbers:
247const double complex x0 =
248 + 0.5 * xs[ k]
249 - 0.5 * xs[nitems - k] * I;
251const double complex x1 =
252 + 0.5 * xs[nh - k]
253 + 0.5 * xs[nh + k] * I;
They are combined to yield \(X_k^e\) and \(X_k^o\):
255const double complex xe = (x0 + x1);
257const double complex xo = (x0 - x1) * twiddle;
Using these values, \(Z_k\) is recovered following
259// real and imaginary parts are stored alternately
260const double complex z = xe + xo * I;
261zs[2 * k + 0] = creal(z);
262zs[2 * k + 1] = cimag(z);
For the corner case \(k = 0\), we have
and due to \(\imag{X_0} = \imag{X_{N / 2}} = 0\), we obtain
230const double x0 = 0.5 * xs[ 0];
231const double x1 = 0.5 * xs[nh];
232const double complex xe = x0 + x1;
233const double complex xo = x0 - x1;
234const double complex z = xe + xo * I;
235zs[0] = creal(z);
236zs[1] = cimag(z);
An inverse transform with respect to \(Z_k\) is performed to yield \(z_n\):
265idft(nh, 1, trigs, zs, xs);
From the result, we obtain
for \(\seq{n}{0}{1}{N / 2 - 1}\). Since the output signal is already ordered in a desired manner, no additional manipulation is necessary here.
The whole process is given below for completeness:
216int rdft_exec_b (
217 rdft_plan_t * const plan,
218 double * const xs
219) {
220 if (NULL == plan) {
221 puts("uninitialized plan is passed");
222 return 1;
223 }
224 const size_t nitems = plan->nitems;
225 const size_t nh = nitems / 2;
226 const trig_t * const trigs = plan->trigs;
227 double * const zs = plan->buf;
228 // create a complex signal, edge cases | 9
229 {
230 const double x0 = 0.5 * xs[ 0];
231 const double x1 = 0.5 * xs[nh];
232 const double complex xe = x0 + x1;
233 const double complex xo = x0 - x1;
234 const double complex z = xe + xo * I;
235 zs[0] = creal(z);
236 zs[1] = cimag(z);
237 }
238 // create a complex signal, common cases
239 for (size_t k = 1; k < nh; k++) {
240 // trigonometric table
241 // cos(+ 2 pi k / nitems)
242 // sin(+ 2 pi k / nitems)
243 const double complex twiddle =
244 + 1. * trigs[k].cos
245 + 1. * trigs[k].sin * I;
246 // compute 1/2 X_k | 3
247 const double complex x0 =
248 + 0.5 * xs[ k]
249 - 0.5 * xs[nitems - k] * I;
250 // compute 1/2 X_{k + N / 2} | 3
251 const double complex x1 =
252 + 0.5 * xs[nh - k]
253 + 0.5 * xs[nh + k] * I;
254 // X_k^e = (+ 1/2 X_k + 1/2 X_{k + N / 2}) | 1
255 const double complex xe = (x0 + x1);
256 // X_k^o = (+ 1/2 X_k - 1/2 X_{k + N / 2}) * exp(arg) | 1
257 const double complex xo = (x0 - x1) * twiddle;
258 // Z_k = X_k^e + X_k^o * I | 4
259 // real and imaginary parts are stored alternately
260 const double complex z = xe + xo * I;
261 zs[2 * k + 0] = creal(z);
262 zs[2 * k + 1] = cimag(z);
263 }
264 // compute complex idft to find z_n | 1
265 idft(nh, 1, trigs, zs, xs);
266 return 0;
267}