1 //
2 // Copyright 2010 Ettus Research LLC
3 // Copyright 2018 Ettus Research, a National Instruments Company
4 //
5 // SPDX-License-Identifier: GPL-3.0-or-later
6 //
7
8 #ifndef ASCII_ART_DFT_HPP
9 #define ASCII_ART_DFT_HPP
10
11 #include <complex>
12 #include <cstddef>
13 #include <stdexcept>
14 #include <string>
15 #include <vector>
16
17 namespace ascii_art_dft {
18
19 //! Type produced by the log power DFT function
20 typedef std::vector<float> log_pwr_dft_type;
21
22 /*!
23 * Get a logarithmic power DFT of the input samples.
24 * Samples are expected to be in the range [-1.0, 1.0].
25 * \param samps a pointer to an array of complex samples
26 * \param nsamps the number of samples in the array
27 * \return a real range of DFT bins in units of dB
28 */
29 template <typename T>
30 log_pwr_dft_type log_pwr_dft(const std::complex<T>* samps, size_t nsamps);
31
32 /*!
33 * Convert a DFT to a piroundable ascii plot.
34 * \param dft the log power dft bins
35 * \param width the frame width in characters
36 * \param height the frame height in characters
37 * \param samp_rate the sample rate in Sps
38 * \param dc_freq the DC frequency in Hz
39 * \param dyn_rng the dynamic range in dB
40 * \param ref_lvl the reference level in dB
41 * \return the plot as an ascii string
42 */
43 std::string dft_to_plot(const log_pwr_dft_type& dft,
44 size_t width,
45 size_t height,
46 double samp_rate,
47 double dc_freq,
48 float dyn_rng,
49 float ref_lvl);
50
51 } // namespace ascii_art_dft
52
53 /***********************************************************************
54 * Implementation includes
55 **********************************************************************/
56 #include <algorithm>
57 #include <cmath>
58 #include <sstream>
59
60 /***********************************************************************
61 * Helper functions
62 **********************************************************************/
63 namespace { /*anon*/
64
65 static const double pi = double(std::acos(-1.0));
66
67 //! Round a floating-point value to the nearest integer
68 template <typename T>
iround(T val)69 int iround(T val)
70 {
71 return (val > 0) ? int(val + 0.5) : int(val - 0.5);
72 }
73
74 //! Pick the closest number that is nice to display
75 template <typename T>
to_clean_num(const T num)76 T to_clean_num(const T num)
77 {
78 if (num == 0)
79 return 0;
80 const T pow10 = std::pow(T(10), int(std::floor(std::log10(std::abs(num)))));
81 const T norm = std::abs(num) / pow10;
82 static const int cleans[] = {1, 2, 5, 10};
83 int clean = cleans[0];
84 for (size_t i = 1; i < sizeof(cleans) / sizeof(cleans[0]); i++) {
85 if (std::abs(norm - cleans[i]) < std::abs(norm - clean))
86 clean = cleans[i];
87 }
88 return ((num < 0) ? -1 : 1) * clean * pow10;
89 }
90
91 //! Compute an FFT with pre-computed factors using Cooley-Tukey
92 template <typename T>
ct_fft_f(const std::complex<T> * samps,size_t nsamps,const std::complex<T> * factors,size_t start=0,size_t step=1)93 std::complex<T> ct_fft_f(const std::complex<T>* samps,
94 size_t nsamps,
95 const std::complex<T>* factors,
96 size_t start = 0,
97 size_t step = 1)
98 {
99 if (nsamps == 1)
100 return samps[start];
101 std::complex<T> E_k = ct_fft_f(samps, nsamps / 2, factors + 1, start, step * 2);
102 std::complex<T> O_k =
103 ct_fft_f(samps, nsamps / 2, factors + 1, start + step, step * 2);
104 return E_k + factors[0] * O_k;
105 }
106
107 //! Compute an FFT for a particular bin k using Cooley-Tukey
108 template <typename T>
ct_fft_k(const std::complex<T> * samps,size_t nsamps,size_t k)109 std::complex<T> ct_fft_k(const std::complex<T>* samps, size_t nsamps, size_t k)
110 {
111 // pre-compute the factors to use in Cooley-Tukey
112 std::vector<std::complex<T>> factors;
113 for (size_t N = nsamps; N != 0; N /= 2) {
114 factors.push_back(std::exp(std::complex<T>(0, T(-2 * pi * k / N))));
115 }
116 return ct_fft_f(samps, nsamps, &factors.front());
117 }
118
119 //! Helper class to build a DFT plot frame
120 class frame_type
121 {
122 public:
frame_type(size_t width,size_t height)123 frame_type(size_t width, size_t height)
124 : _frame(width - 1, std::vector<char>(height, ' '))
125 {
126 /* NOP */
127 }
128
129 // accessors to parts of the frame
get_plot(size_t b,size_t z)130 char& get_plot(size_t b, size_t z)
131 {
132 return _frame.at(b + albl_w).at(z + flbl_h);
133 }
get_albl(size_t b,size_t z)134 char& get_albl(size_t b, size_t z)
135 {
136 return _frame.at(b).at(z + flbl_h);
137 }
get_ulbl(size_t b)138 char& get_ulbl(size_t b)
139 {
140 return _frame.at(b).at(flbl_h - 1);
141 }
get_flbl(size_t b)142 char& get_flbl(size_t b)
143 {
144 return _frame.at(b + albl_w).at(flbl_h - 1);
145 }
146
147 // dimension accessors
get_plot_h(void) const148 size_t get_plot_h(void) const
149 {
150 return _frame.front().size() - flbl_h;
151 }
get_plot_w(void) const152 size_t get_plot_w(void) const
153 {
154 return _frame.size() - albl_w;
155 }
get_albl_w(void) const156 size_t get_albl_w(void) const
157 {
158 return albl_w;
159 }
160
to_string(void)161 std::string to_string(void)
162 {
163 std::stringstream frame_ss;
164 for (size_t z = 0; z < _frame.front().size(); z++) {
165 for (size_t b = 0; b < _frame.size(); b++) {
166 frame_ss << _frame[b][_frame[b].size() - z - 1];
167 }
168 frame_ss << std::endl;
169 }
170 return frame_ss.str();
171 }
172
173 private:
174 static const size_t albl_w = 6, flbl_h = 1;
175 std::vector<std::vector<char>> _frame;
176 };
177
178 } // namespace
179
180 /***********************************************************************
181 * Implementation code
182 **********************************************************************/
183 namespace ascii_art_dft {
184
185 //! skip constants for amplitude and frequency labels
186 static const size_t albl_skip = 5, flbl_skip = 20;
187
188 template <typename T>
log_pwr_dft(const std::complex<T> * samps,size_t nsamps)189 log_pwr_dft_type log_pwr_dft(const std::complex<T>* samps, size_t nsamps)
190 {
191 if (nsamps & (nsamps - 1))
192 throw std::runtime_error("num samps is not a power of 2");
193
194 // compute the window
195 double win_pwr = 0;
196 std::vector<std::complex<T>> win_samps;
197 for (size_t n = 0; n < nsamps; n++) {
198 // double w_n = 1;
199 // double w_n = 0.54 //hamming window
200 // -0.46*std::cos(2*pi*n/(nsamps-1))
201 //;
202 double w_n = 0.35875 // blackman-harris window
203 - 0.48829 * std::cos(2 * pi * n / (nsamps - 1))
204 + 0.14128 * std::cos(4 * pi * n / (nsamps - 1))
205 - 0.01168 * std::cos(6 * pi * n / (nsamps - 1));
206 // double w_n = 1 // flat top window
207 // -1.930*std::cos(2*pi*n/(nsamps-1))
208 // +1.290*std::cos(4*pi*n/(nsamps-1))
209 // -0.388*std::cos(6*pi*n/(nsamps-1))
210 // +0.032*std::cos(8*pi*n/(nsamps-1))
211 //;
212 win_samps.push_back(T(w_n) * samps[n]);
213 win_pwr += w_n * w_n;
214 }
215
216 // compute the log-power dft
217 log_pwr_dft_type log_pwr_dft;
218 for (size_t k = 0; k < nsamps; k++) {
219 std::complex<T> dft_k = ct_fft_k(&win_samps.front(), nsamps, k);
220 log_pwr_dft.push_back(
221 float(+20 * std::log10(std::abs(dft_k)) - 20 * std::log10(T(nsamps))
222 - 10 * std::log10(win_pwr / nsamps) + 3));
223 }
224
225 return log_pwr_dft;
226 }
227
dft_to_plot(const log_pwr_dft_type & dft_,size_t width,size_t height,double samp_rate,double dc_freq,float dyn_rng,float ref_lvl)228 std::string dft_to_plot(const log_pwr_dft_type& dft_,
229 size_t width,
230 size_t height,
231 double samp_rate,
232 double dc_freq,
233 float dyn_rng,
234 float ref_lvl)
235 {
236 frame_type frame(width, height); // fill this frame
237
238 // re-order the dft so dc in in the center
239 const size_t num_bins = dft_.size() - 1 + dft_.size() % 2; // make it odd
240 log_pwr_dft_type dft(num_bins);
241 for (size_t n = 0; n < num_bins; n++) {
242 dft[n] = dft_[(n + num_bins / 2) % num_bins];
243 }
244
245 // fill the plot with dft bins
246 for (size_t b = 0; b < frame.get_plot_w(); b++) {
247 // indexes from the dft to grab for the plot
248 const size_t n_start = std::max(
249 iround(double(b - 0.5) * (num_bins - 1) / (frame.get_plot_w() - 1)), 0);
250 const size_t n_stop =
251 std::min(iround(double(b + 0.5) * (num_bins - 1) / (frame.get_plot_w() - 1)),
252 int(num_bins));
253
254 // calculate val as the max across points
255 float val = dft.at(n_start);
256 for (size_t n = n_start; n < n_stop; n++)
257 val = std::max(val, dft.at(n));
258
259 const float scaled =
260 (val - (ref_lvl - dyn_rng)) * (frame.get_plot_h() - 1) / dyn_rng;
261 for (size_t z = 0; z < frame.get_plot_h(); z++) {
262 static const std::string syms(".:!|");
263 if (scaled - z >= 1)
264 frame.get_plot(b, z) = syms.at(syms.size() - 1);
265 else if (scaled - z > 0)
266 frame.get_plot(b, z) = syms.at(size_t((scaled - z) * syms.size()));
267 }
268 }
269
270 // create vertical amplitude labels
271 const float db_step = to_clean_num(dyn_rng / (frame.get_plot_h() - 1) * albl_skip);
272 for (float db = db_step * (int((ref_lvl - dyn_rng) / db_step));
273 db <= db_step * (int(ref_lvl / db_step));
274 db += db_step) {
275 const int z =
276 iround((db - (ref_lvl - dyn_rng)) * (frame.get_plot_h() - 1) / dyn_rng);
277 if (z < 0 or size_t(z) >= frame.get_plot_h())
278 continue;
279 std::stringstream ss;
280 ss << db;
281 std::string lbl = ss.str();
282 for (size_t i = 0; i < lbl.size() and i < frame.get_albl_w(); i++) {
283 frame.get_albl(i, z) = lbl[i];
284 }
285 }
286
287 // create vertical units label
288 std::string ulbl = "dBfs";
289 for (size_t i = 0; i < ulbl.size(); i++) {
290 frame.get_ulbl(i + 1) = ulbl[i];
291 }
292
293 // create horizontal frequency labels
294 const double f_step = to_clean_num(samp_rate / frame.get_plot_w() * flbl_skip);
295 for (double freq = f_step * int((-samp_rate / 2 / f_step));
296 freq <= f_step * int((+samp_rate / 2 / f_step));
297 freq += f_step) {
298 const int b =
299 iround((freq + samp_rate / 2) * (frame.get_plot_w() - 1) / samp_rate);
300 std::stringstream ss;
301 ss << (freq + dc_freq) / 1e6 << "MHz";
302 std::string lbl = ss.str();
303 if (b < int(lbl.size() / 2)
304 or b + lbl.size() - lbl.size() / 2 >= frame.get_plot_w())
305 continue;
306 for (size_t i = 0; i < lbl.size(); i++) {
307 frame.get_flbl(b + i - lbl.size() / 2) = lbl[i];
308 }
309 }
310
311 return frame.to_string();
312 }
313 } // namespace ascii_art_dft
314
315 /*
316
317 //example main function to test the dft
318
319 #include <curses.h>
320 #include <cstdlib>
321 #include <iostream>
322 #include <thread>
323 #include <chrono>
324
325 int main(void){
326 using namespace std::chrono_literals;
327
328 initscr();
329
330 while (true){
331 clear();
332
333 std::vector<std::complex<float> > samples;
334 for(size_t i = 0; i < 512; i++){
335 samples.push_back(std::complex<float>(
336 float(std::rand() - RAND_MAX/2)/(RAND_MAX)/4,
337 float(std::rand() - RAND_MAX/2)/(RAND_MAX)/4
338 ));
339 samples[i] += 0.5*std::sin(i*3.14/2) + 0.7;
340 }
341
342 ascii_art_dft::log_pwr_dft_type dft;
343 dft = ascii_art_dft::log_pwr_dft(&samples.front(), samples.size());
344
345 printw("%s", ascii_art_dft::dft_to_plot(
346 dft, COLS, LINES,
347 12.5e4, 2.45e9,
348 60, 0
349 ).c_str());
350 refresh();
351
352 std::this_thread::sleep_for(1s);
353 }
354
355
356 endwin();
357 std::cout << "here\n";
358 return 0;
359 }
360
361 */
362
363 #endif /*ASCII_ART_DFT_HPP*/
364