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