Showing the changes to src/spectrogram.c (1 of 10 files). Show all
Unified · Split
| @@ -0,0 +1,536 @@ | ||
| 1 | +/* libSoX effect: Spectrogram (c) 2008 robs@users.sourceforge.net | |
| 2 | + * | |
| 3 | + * This library is free software; you can redistribute it and/or modify it | |
| 4 | + * under the terms of the GNU Lesser General Public License as published by | |
| 5 | + * the Free Software Foundation; either version 2.1 of the License, or (at | |
| 6 | + * your option) any later version. | |
| 7 | + * | |
| 8 | + * This library is distributed in the hope that it will be useful, but | |
| 9 | + * WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser | |
| 11 | + * General Public License for more details. | |
| 12 | + * | |
| 13 | + * You should have received a copy of the GNU Lesser General Public License | |
| 14 | + * along with this library; if not, write to the Free Software Foundation, | |
| 15 | + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
| 16 | + */ | |
| 17 | + | |
| 18 | +#ifdef NDEBUG /* Enable assert always. */ | |
| 19 | +#undef NDEBUG /* Must undef above assert.h or other that might include it. */ | |
| 20 | +#endif | |
| 21 | + | |
| 22 | +#include "sox_i.h" | |
| 23 | +#include "fft4g.h" | |
| 24 | +#include "getopt.h" | |
| 25 | +#include <assert.h> | |
| 26 | +#include <math.h> | |
| 27 | +#include <png.h> | |
| 28 | + | |
| 29 | +#define malloc lsx_malloc | |
| 30 | +#define calloc lsx_calloc | |
| 31 | +#define realloc lsx_realloc | |
| 32 | +#define FROM_SOX SOX_SAMPLE_TO_FLOAT_64BIT | |
| 33 | +#define DFT_BASE_SIZE 512 | |
| 34 | +#define MAX_DFT_SIZE_SHIFT 3 | |
| 35 | +#define MAX_DFT_SIZE (DFT_BASE_SIZE << MAX_DFT_SIZE_SHIFT) | |
| 36 | +#define MAX_COLS 999 /* Also max seconds */ | |
| 37 | + | |
| 38 | +static void apply_hann(double h[], const int num_points) | |
| 39 | +{ | |
| 40 | + int i, m = num_points - 1; | |
| 41 | + for (i = 0; i < num_points; ++i) { | |
| 42 | + double x = 2 * M_PI * i / m; | |
| 43 | + h[i] *= .5 - .5 * cos(x); | |
| 44 | + } | |
| 45 | +} | |
| 46 | + | |
| 47 | +static void apply_hamming(double h[], const int num_points) | |
| 48 | +{ | |
| 49 | + int i, m = num_points - 1; | |
| 50 | + for (i = 0; i < num_points; ++i) { | |
| 51 | + double x = 2 * M_PI * i / m; | |
| 52 | + h[i] *= .53836 - .46164 * cos(x); | |
| 53 | + } | |
| 54 | +} | |
| 55 | + | |
| 56 | +static void apply_bartlett(double h[], const int num_points) | |
| 57 | +{ | |
| 58 | + int i, m = num_points - 1; | |
| 59 | + for (i = 0; i < num_points; ++i) { | |
| 60 | + h[i] *= 2. / m * (m / 2. - fabs(i - m / 2.)); | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +static double kaiser_beta(double att) | |
| 65 | +{ | |
| 66 | + if (att > 100 ) return .1117 * att - 1.11; | |
| 67 | + if (att > 50 ) return .1102 * (att - 8.7); | |
| 68 | + if (att > 20.96) return .58417 * pow(att -20.96, .4) + .07886 * (att - 20.96); | |
| 69 | + return 0; | |
| 70 | +} | |
| 71 | + | |
| 72 | +static void apply_kaiser(double h[], const int num_points, double beta) | |
| 73 | +{ | |
| 74 | + int i, m = num_points - 1; | |
| 75 | + for (i = 0; i <= m; ++i) { | |
| 76 | + double x = 2. * i / m - 1; | |
| 77 | + h[i] *= bessel_I_0(beta * sqrt(1 - x * x)) / bessel_I_0(beta); | |
| 78 | + } | |
| 79 | +} | |
| 80 | + | |
| 81 | +typedef enum {Window_Hann, Window_Hamming, Window_Bartlett, Window_Rectangular, Window_Kaiser} win_type_t; | |
| 82 | +static enum_item const window_options[] = { | |
| 83 | + ENUM_ITEM(Window_,Hann) | |
| 84 | + ENUM_ITEM(Window_,Hamming) | |
| 85 | + ENUM_ITEM(Window_,Bartlett) | |
| 86 | + ENUM_ITEM(Window_,Rectangular) | |
| 87 | + ENUM_ITEM(Window_,Kaiser) | |
| 88 | + {0, 0}}; | |
| 89 | + | |
| 90 | +typedef struct { | |
| 91 | + int y_size, dft_size, rows, cols, step_size, block_steps, block_num; | |
| 92 | + double buf[MAX_DFT_SIZE]; | |
| 93 | + double dft_buf[MAX_DFT_SIZE]; | |
| 94 | + double window[MAX_DFT_SIZE]; | |
| 95 | + int bit_rev_table[100]; /* For Ooura fft */ | |
| 96 | + double sin_cos_table[dft_sc_len(MAX_DFT_SIZE)]; /* ditto */ | |
| 97 | + double magnitudes[(MAX_DFT_SIZE >> 1) + 1]; | |
| 98 | + float * dBfs; | |
| 99 | + double pixels_per_sec, block_norm, max; | |
| 100 | + int dB_range, gain, style, read, end, end_min, last_end; | |
| 101 | + char const * out_name, * title, * comment; | |
| 102 | + sox_bool no_axes; | |
| 103 | + win_type_t win_type; | |
| 104 | +} priv_t; | |
| 105 | + | |
| 106 | +#define GETOPT_NUMERIC(ch, name, min, max) case ch:{ \ | |
| 107 | + char * end_ptr; \ | |
| 108 | + double d = strtod(optarg, &end_ptr); \ | |
| 109 | + if (end_ptr == optarg || d < min || d > max || *end_ptr != '\0') {\ | |
| 110 | + sox_fail("parameter `%s' must be between %g and %g", #name, (double)min, (double)max); \ | |
| 111 | + return lsx_usage(effp); \ | |
| 112 | + } \ | |
| 113 | + p->name = d; \ | |
| 114 | + break; \ | |
| 115 | +} | |
| 116 | + | |
| 117 | +static int enum_option(int c, enum_item const * items) | |
| 118 | +{ | |
| 119 | + enum_item const * p = find_enum_text(optarg, items); | |
| 120 | + if (p == NULL) { | |
| 121 | + unsigned len = 1; | |
| 122 | + char * set = lsx_malloc(len); | |
| 123 | + *set = 0; | |
| 124 | + for (p = items; p->text; ++p) { | |
| 125 | + set = lsx_realloc(set, len += 2 + strlen(p->text)); | |
| 126 | + strcat(set, ", "); strcat(set, p->text); | |
| 127 | + } | |
| 128 | + sox_fail("-%c: '%s' is not one of: %s.", c, optarg, set + 2); | |
| 129 | + free(set); | |
| 130 | + return INT_MAX; | |
| 131 | + } | |
| 132 | + return p->value; | |
| 133 | +} | |
| 134 | + | |
| 135 | +static int getopts(sox_effect_t * effp, int argc, char **argv) | |
| 136 | +{ | |
| 137 | + priv_t * p = (priv_t *)effp->priv; | |
| 138 | + int c, callers_optind = optind, callers_opterr = opterr; | |
| 139 | + | |
| 140 | + assert(array_length(p->bit_rev_table) >= (size_t)dft_br_len(MAX_DFT_SIZE)); | |
| 141 | + | |
| 142 | + --argv, ++argc, optind = 1, opterr = 0; /* re-jig for getopt */ | |
| 143 | + p->pixels_per_sec = 100, p->y_size = 2, p->dB_range = 120;/* non-0 defaults */ | |
| 144 | + p->style = 1, p->out_name = "spectrogram.png", p->comment = "Created by SoX"; | |
| 145 | + | |
| 146 | + while ((c = getopt(argc, argv, "+x:y:z:g:s:w:t:c:o:a")) != -1) switch (c) { | |
| 147 | + GETOPT_NUMERIC('x', pixels_per_sec, 1 , 5000) | |
| 148 | + GETOPT_NUMERIC('y', y_size , 1 , 1 + MAX_DFT_SIZE_SHIFT) | |
| 149 | + GETOPT_NUMERIC('z', dB_range , 40 , 180) | |
| 150 | + GETOPT_NUMERIC('g', gain ,-100, 100) | |
| 151 | + GETOPT_NUMERIC('s', style , 1 , 4) | |
| 152 | + case 'w': p->win_type = enum_option(c, window_options); break; | |
| 153 | + case 't': p->title = optarg; break; | |
| 154 | + case 'c': p->comment = optarg; break; | |
| 155 | + case 'o': p->out_name = optarg; break; | |
| 156 | + case 'a': p->no_axes = sox_true; break; | |
| 157 | + default: sox_fail("unknown option `-%c'", optopt); return lsx_usage(effp); | |
| 158 | + } | |
| 159 | + p->y_size -= 1, p->style -= 1; | |
| 160 | + argc -= optind, optind = callers_optind, opterr = callers_opterr; | |
| 161 | + return argc || p->win_type == INT_MAX? lsx_usage(effp) : SOX_SUCCESS; | |
| 162 | +} | |
| 163 | + | |
| 164 | +static int make_window(priv_t * p, int end) | |
| 165 | +{ | |
| 166 | + double sum = 0, * w = end < 0? p->window : p->window + end; | |
| 167 | + int i, n = p->dft_size - abs(end); | |
| 168 | + | |
| 169 | + if (end) memset(p->window, 0, sizeof(p->window)); | |
| 170 | + for (i = 0; i < n; ++i) w[i] = 1; | |
| 171 | + switch (p->win_type) { | |
| 172 | + case Window_Hann: apply_hann(w, n); break; | |
| 173 | + case Window_Hamming: apply_hamming(w, n); break; | |
| 174 | + case Window_Bartlett: apply_bartlett(w, n); break; | |
| 175 | + case Window_Rectangular: break; | |
| 176 | + default: apply_kaiser(w, n, kaiser_beta(p->dB_range + 10.)); | |
| 177 | + } | |
| 178 | + for (i = 0; i < p->dft_size; ++i) sum += p->window[i]; | |
| 179 | + for (i = 0; i < p->dft_size; ++i) p->window[i] *= 2 / sum | |
| 180 | + * sqr((double)n / p->dft_size); /* empirical small window adjustment */ | |
| 181 | + return sum + .5; | |
| 182 | +} | |
| 183 | + | |
| 184 | +static int start(sox_effect_t * effp) | |
| 185 | +{ | |
| 186 | + priv_t * p = (priv_t *)effp->priv; | |
| 187 | + double actual; | |
| 188 | + | |
| 189 | + if (effp->in_signal.channels != 1) { | |
| 190 | + sox_fail("only 1 channel is supported"); | |
| 191 | + return SOX_EOF; | |
| 192 | + } | |
| 193 | + p->end = p->dft_size = DFT_BASE_SIZE << p->y_size; | |
| 194 | + p->rows = (p->dft_size >> 1) + 1; | |
| 195 | + p->step_size = make_window(p, p->last_end = 0); | |
| 196 | + p->block_steps = effp->in_signal.rate / p->pixels_per_sec; | |
| 197 | + p->step_size = p->block_steps / ceil((double)p->block_steps / p->step_size) +.5; | |
| 198 | + p->block_steps = floor((double)p->block_steps / p->step_size +.5); | |
| 199 | + p->block_norm = 1. / p->block_steps; | |
| 200 | + actual = effp->in_signal.rate / p->step_size / p->block_steps; | |
| 201 | + if (actual != p->pixels_per_sec) | |
| 202 | + sox_report("actual pixels/s = %g", actual); | |
| 203 | + sox_debug("step_size=%i block_steps=%i", p->step_size, p->block_steps); | |
| 204 | + p->max = -p->dB_range; | |
| 205 | + p->read = (p->step_size - p->dft_size) / 2; | |
| 206 | + return SOX_SUCCESS; | |
| 207 | +} | |
| 208 | + | |
| 209 | +static int do_column(sox_effect_t * effp) | |
| 210 | +{ | |
| 211 | + priv_t * p = (priv_t *)effp->priv; | |
| 212 | + int i; | |
| 213 | + | |
| 214 | + if (p->cols == MAX_COLS) { | |
| 215 | + sox_warn("PNG truncated at %g seconds", (double)MAX_COLS * p->step_size * p->block_steps / effp->in_signal.rate); | |
| 216 | + return SOX_EOF; | |
| 217 | + } | |
| 218 | + ++p->cols; | |
| 219 | + p->dBfs = realloc(p->dBfs, p->cols * p->rows * sizeof(*p->dBfs)); | |
| 220 | + for (i = 0; i < p->rows; ++i) { | |
| 221 | + double dBfs = 10 * log10(p->magnitudes[i] * p->block_norm); | |
| 222 | + p->dBfs[(p->cols - 1) * p->rows + i] = dBfs + p->gain; | |
| 223 | + if (dBfs > p->max) | |
| 224 | + p->max = dBfs; | |
| 225 | + } | |
| 226 | + memset(p->magnitudes, 0, p->rows * sizeof(*p->magnitudes)); | |
| 227 | + p->block_num = 0; | |
| 228 | + return SOX_SUCCESS; | |
| 229 | +} | |
| 230 | + | |
| 231 | +static int flow(sox_effect_t * effp, | |
| 232 | + const sox_sample_t * ibuf, sox_sample_t * obuf, | |
| 233 | + sox_size_t * isamp, sox_size_t * osamp) | |
| 234 | +{ | |
| 235 | + priv_t * p = (priv_t *)effp->priv; | |
| 236 | + sox_size_t len = min(*isamp, *osamp), dummy = 0; /* No need to clip count */ | |
| 237 | + int i; | |
| 238 | + | |
| 239 | + memcpy(obuf, ibuf, len * sizeof(*obuf)); /* Pass on audio unaffected */ | |
| 240 | + *isamp = *osamp = len; | |
| 241 | + | |
| 242 | + while (sox_true) { | |
| 243 | + if (p->read == p->step_size) { | |
| 244 | + memmove(p->buf, p->buf + p->step_size, | |
| 245 | + (p->dft_size - p->step_size) * sizeof(*p->buf)); | |
| 246 | + p->read = 0; | |
| 247 | + } | |
| 248 | + for (; len && p->read < p->step_size; --len, ++p->read, --p->end) | |
| 249 | + p->buf[p->dft_size - p->step_size + p->read] = FROM_SOX(*ibuf++, dummy); | |
| 250 | + if (p->read != p->step_size) | |
| 251 | + break; | |
| 252 | + | |
| 253 | + if ((p->end = max(p->end, p->end_min)) != p->last_end) | |
| 254 | + make_window(p, p->last_end = p->end); | |
| 255 | + for (i = 0; i < p->dft_size; ++i) p->dft_buf[i] = p->buf[i] * p->window[i]; | |
| 256 | + rdft(p->dft_size, 1, p->dft_buf, p->bit_rev_table, p->sin_cos_table); | |
| 257 | + p->magnitudes[0] += sqr(p->dft_buf[0]); | |
| 258 | + for (i = 1; i < p->dft_size >> 1; ++i) | |
| 259 | + p->magnitudes[i] += sqr(p->dft_buf[2*i]) + sqr(p->dft_buf[2*i+1]); | |
| 260 | + p->magnitudes[p->dft_size >> 1] += sqr(p->dft_buf[1]); | |
| 261 | + | |
| 262 | + if (++p->block_num == p->block_steps && do_column(effp) == SOX_EOF) | |
| 263 | + return SOX_EOF; | |
| 264 | + } | |
| 265 | + return SOX_SUCCESS; | |
| 266 | +} | |
| 267 | + | |
| 268 | +static int drain(sox_effect_t * effp, sox_sample_t * obuf_, sox_size_t * osamp) | |
| 269 | +{ | |
| 270 | + priv_t * p = (priv_t *)effp->priv; | |
| 271 | + | |
| 272 | + if (p->cols != MAX_COLS) { | |
| 273 | + sox_sample_t * ibuf = calloc(p->dft_size, sizeof(*ibuf)); | |
| 274 | + sox_sample_t * obuf = calloc(p->dft_size, sizeof(*obuf)); | |
| 275 | + sox_size_t isamp = (p->dft_size - p->step_size) / 2; | |
| 276 | + int left_over = (isamp + p->read) % p->step_size; | |
| 277 | + | |
| 278 | + if (left_over >= p->step_size >> 1) | |
| 279 | + isamp += p->step_size - left_over; | |
| 280 | + sox_debug("cols=%i left=%i end=%i", p->cols, p->read, p->end); | |
| 281 | + p->end = 0; | |
| 282 | + p->end_min = -p->dft_size; | |
| 283 | + if (flow(effp, ibuf, obuf, &isamp, &isamp) == SOX_SUCCESS && p->block_num) { | |
| 284 | + p->block_norm *= (double)p->block_steps / p->block_num; | |
| 285 | + do_column(effp); | |
| 286 | + } | |
| 287 | + sox_debug("flushed cols=%i left=%i end=%i", p->cols, p->read, p->end); | |
| 288 | + free(obuf); | |
| 289 | + free(ibuf); | |
| 290 | + } | |
| 291 | + (void)obuf_, *osamp = 0; | |
| 292 | + return SOX_SUCCESS; | |
| 293 | +} | |
| 294 | + | |
| 295 | +#define spectrum_points (1 + 250) | |
| 296 | +enum {Background = spectrum_points, Text, Labels, Grid, palette_size}; | |
| 297 | + | |
| 298 | +static unsigned colour(double x, int range) | |
| 299 | +{ | |
| 300 | + x = range_limit(x, -range, 0); | |
| 301 | + return (1 + x / range) * (spectrum_points - 1) + .5; | |
| 302 | +} | |
| 303 | + | |
| 304 | +static void make_palette(png_byte * palette, int style) | |
| 305 | +{ | |
| 306 | + int i, j, at; | |
| 307 | + for (i = j = 0; j < spectrum_points; i += 3, ++j) { | |
| 308 | + double r, g, b, x = (double)j / (spectrum_points - 1); | |
| 309 | + if (style > 1) | |
| 310 | + palette[i+2] = palette[i+1] = palette[i+0] = style == 2? j : 255 - j; | |
| 311 | + else { | |
| 312 | + at = style? (spectrum_points - 1) * 3 - i : i; | |
| 313 | + if (x < .16) r = 0; | |
| 314 | + else if (x < .72) r = .99 * sin((x - .16) / .56 * M_PI / 2); | |
| 315 | + else r = .01 * (x - .72) / .28 + .99; | |
| 316 | + if (x < .52) g = 0; | |
| 317 | + else if (x < .86) g = .99 * .5 * (1 - cos((x - .52) / .34 * M_PI)); | |
| 318 | + else g = .01 * (x - .86) / .14 + .99; | |
| 319 | + if (x < .34) b = .5 * sin((x - .00) / .34 * M_PI / 2); | |
| 320 | + else if (x < .63) b = .5 * .5 * (1 + cos((x - .34) / .29 * M_PI)); | |
| 321 | + else b = (1 - cos((x - .63) / .37 * M_PI / 2)); | |
| 322 | + palette[at + 0] = r * 255 + .5; | |
| 323 | + palette[at + 1] = g * 255 + .5; | |
| 324 | + palette[at + 2] = b * 255 + .5; | |
| 325 | + } | |
| 326 | + } | |
| 327 | + i = spectrum_points * 3; | |
| 328 | + switch (style) { | |
| 329 | + case 0: case 2: | |
| 330 | + memcpy(palette + i, "\0\0\0" , 3), i+= 3; | |
| 331 | + memcpy(palette + i, "\377\377\377", 3), i+= 3; | |
| 332 | + memcpy(palette + i, "\277\277\277", 3), i+= 3; | |
| 333 | + memcpy(palette + i, "\177\177\177", 3), i+= 3; | |
| 334 | + return; | |
| 335 | + case 1: memcpy(palette + i, "\335\330\320", 3), i+= 3; break; | |
| 336 | + default: memcpy(palette + i, "\337\337\337", 3), i+= 3; | |
| 337 | + } | |
| 338 | + memcpy(palette + i, "\0\0\0" , 3), i+= 3; | |
| 339 | + memcpy(palette + i, "\077\077\077", 3), i+= 3; | |
| 340 | + memcpy(palette + i, "\077\077\077", 3), i+= 3; | |
| 341 | +} | |
| 342 | + | |
| 343 | +static const Bytef fixed[] = | |
| 344 | + "x\332eT\241\266\2450\fDVV>Y\371$re%2\237\200|2\22YY\211D\"+\337'<y\345\312" | |
| 345 | + "\375\fd\345f\222\224\313\236\235{\270\344L\247a\232\4\246\351\201d\230\222" | |
| 346 | + "\304D\364^ \352\362S\"m\347\311\237\237\27\64K\243\2302\265\35\v\371<\363y" | |
| 347 | + "\354_\226g\354\214)e \2458\341\17\20J4\215[z<\271\277\367\0\63\64@\177\330c" | |
| 348 | + "\227\204 Ir.\5$U\200\260\224\326S\17\200=\\k\20QA\334%\342\20*\303P\234\211" | |
| 349 | + "\366\36#\370R\276_\316s-\345\222Dlz\363my*;\217\373\346z\267\343\236\364\246" | |
| 350 | + "\236\365\2419\305p\333\267\23(\207\265\333\233\325Y\342\243\265\357\262\215" | |
| 351 | + "\263t\271$\276\226ea\271.\367&\320\347\202_\234\27\377\345\222\253?\3422\364" | |
| 352 | + "\207y\256\236\229\331\33\f\376\227\266\"\356\253j\366\363\347\334US\34]\371?" | |
| 353 | + "\255\371\336\372z\265v\34\226\247\32\324\217\334\337\317U4\16\316{N\370\31" | |
| 354 | + "\365\357iL\231y\33y\264\211D7\337\4\244\261\220D\346\1\261\357\355>\3\342" | |
| 355 | + "\223\363\0\303\277\f[\214A,p\34`\255\355\364\37\372\224\342\277\f\207\255\36" | |
| 356 | + "_V\7\34\241^\316W\257\177\b\242\300\34\f\276\33?/9_\331f\346\36\25Y)\2301" | |
| 357 | + "\257\2414|\35\365\237\3424k\3\244\3\242\261\6\b\275>z$\370\215:\270\363w\36/" | |
| 358 | + "\265kF\v\20o6\242\301\364\336\27\325\257\321\364fs\231\215G\32=\257\305Di" | |
| 359 | + "\304^\177\304R\364Q=\225\373\33\320\375\375\372\200\337\37\374}\334\337\20" | |
| 360 | + "\364\310(]\304\267b\177\326Yrj\312\277\373\233\37\340"; | |
| 361 | +static unsigned char * font; | |
| 362 | +#define font_x 5 | |
| 363 | +#define font_y 12 | |
| 364 | +#define font_X (font_x + 1) | |
| 365 | + | |
| 366 | +#define pixel(x,y) pixels[(y) * cols + (x)] | |
| 367 | +#define print_at(x,y,c,t) print_at_(pixels,cols,x,y,c,t,0) | |
| 368 | +#define print_up(x,y,c,t) print_at_(pixels,cols,x,y,c,t,1) | |
| 369 | + | |
| 370 | +static void print_at_(png_byte * pixels, int cols, int x, int y, int c, char const * text, int orientation) | |
| 371 | +{ | |
| 372 | + for (;*text; ++text) { | |
| 373 | + int pos = (*text - ' ') * font_y; | |
| 374 | + int i, j; | |
| 375 | + for (i = 0; i < font_y; ++i) { | |
| 376 | + unsigned line = font[pos++]; | |
| 377 | + for (j = 0; j < font_x; ++j, line <<= 1) | |
| 378 | + if (line & 0x80) switch (orientation) { | |
| 379 | + case 0: pixel(x + j, y - i) = c; break; | |
| 380 | + case 1: pixel(x + i, y + j) = c; break; | |
| 381 | + } | |
| 382 | + } | |
| 383 | + switch (orientation) { | |
| 384 | + case 0: x += font_X; break; | |
| 385 | + case 1: y += font_X; break; | |
| 386 | + } | |
| 387 | + } | |
| 388 | +} | |
| 389 | + | |
| 390 | +static int axis(double to, int max_steps, int * limit, char * * prefix) | |
| 391 | +{ | |
| 392 | + double scale = 1, step = 1; | |
| 393 | + int i, prefix_num = 0; | |
| 394 | + if (max_steps) { | |
| 395 | + double try, log_10 = HUGE_VAL, min_step = (to *= 10) / max_steps; | |
| 396 | + for (i = 5; i; i >>= 1) if ((try = ceil(log10(min_step * i))) <= log_10) | |
| 397 | + step = pow(10., log_10 = try) / i, log_10 -= i > 1; | |
| 398 | + prefix_num = floor(log_10 / 3); | |
| 399 | + scale = pow(10., -3. * prefix_num); | |
| 400 | + } | |
| 401 | + *prefix = "pnum-kMGTPE" + prefix_num + (prefix_num? 4 : 11); | |
| 402 | + *limit = to * scale + .5; | |
| 403 | + return step * scale + .5; | |
| 404 | +} | |
| 405 | + | |
| 406 | +#define below 48 | |
| 407 | +#define left 58 | |
| 408 | +#define between 37 | |
| 409 | +#define spectrum_width 14 | |
| 410 | +#define right 35 | |
| 411 | + | |
| 412 | +static int stop(sox_effect_t * effp) | |
| 413 | +{ | |
| 414 | + priv_t * p = (priv_t *) effp->priv; | |
| 415 | + FILE * file = fopen(p->out_name, "w"); | |
| 416 | + uLong font_len = 96 * font_y; | |
| 417 | + int rows = below + p->rows + 30 + 20 * !!p->title; | |
| 418 | + int cols = left + p->cols + between + spectrum_width + right; | |
| 419 | + sox_bool do_frame = !p->no_axes; | |
| 420 | + png_byte * pixels = malloc(cols * rows * sizeof(*pixels)); | |
| 421 | + png_bytepp png_rows = malloc(rows * sizeof(*png_rows)); | |
| 422 | + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0,0); | |
| 423 | + png_infop png_info = png_create_info_struct(png); | |
| 424 | + png_byte palette[palette_size * 3]; | |
| 425 | + int i, j, step, limit, tick_len = 2 + do_frame; | |
| 426 | + char text[200], * prefix; | |
| 427 | + | |
| 428 | + if (!file) { | |
| 429 | + sox_fail("failed to create `%s' :(", p->out_name); | |
| 430 | + png_destroy_write_struct(&png, &png_info); | |
| 431 | + free(png_rows); | |
| 432 | + free(pixels); | |
| 433 | + free(p->dBfs); | |
| 434 | + return SOX_EOF; | |
| 435 | + } | |
| 436 | + sox_debug("signal-max=%g", p->max); | |
| 437 | + font = malloc(font_len); | |
| 438 | + assert(uncompress(font, &font_len, fixed, sizeof(fixed)-1) == Z_OK); | |
| 439 | + make_palette(palette, p->style); | |
| 440 | + memset(pixels, Background, cols * rows * sizeof(*pixels)); | |
| 441 | + png_init_io(png, file); | |
| 442 | + png_set_PLTE(png, png_info, (png_colorp)palette, palette_size); | |
| 443 | + png_set_IHDR(png, png_info, (size_t)cols, (size_t)rows, 8, | |
| 444 | + PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE, | |
| 445 | + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); | |
| 446 | + for (j = 0; j < rows; ++j) | |
| 447 | + png_rows[rows - 1 - j] = (png_bytep)(pixels + j * cols); | |
| 448 | + | |
| 449 | + if (p->title && (i = (int)strlen(p->title) * font_X) < cols + 1) /* Title */ | |
| 450 | + print_at((cols - i) / 2, rows - font_y, Text, p->title); | |
| 451 | + if ((int)strlen(p->comment) * font_X < cols + 1) /* Footer comment */ | |
| 452 | + print_at(1, font_y, Text, p->comment); | |
| 453 | + | |
| 454 | + /* Spectrogram */ | |
| 455 | + for (j = 0; j < p->rows; ++j) { | |
| 456 | + for (i = 0; i < p->cols; ++i) | |
| 457 | + pixel(left + i, below + j) = colour(p->dBfs[i*p->rows + j], p->dB_range); | |
| 458 | + if (do_frame) /* Frame verticals */ | |
| 459 | + pixel(left - 1, below + j) = pixel(left + p->cols,below + j) = Grid; | |
| 460 | + } | |
| 461 | + if (do_frame) for (i = -1; i <= p->cols; ++i) /* Frame horizontals */ | |
| 462 | + pixel(left + i, below - 1) = pixel(left + i, below + p->rows) = Grid; | |
| 463 | + | |
| 464 | + /* X-axis */ | |
| 465 | + step = axis((double)p->cols * p->step_size * p->block_steps / effp->in_signal.rate, p->cols / (font_X * 9 / 2), &limit, &prefix); | |
| 466 | + sprintf(text, "Time (%.1ss)", prefix); /* Axis label */ | |
| 467 | + print_at(left + (p->cols - font_X * (int)strlen(text)) / 2, 24, Text, text); | |
| 468 | + for (i = 0; i <= limit; i += step) { | |
| 469 | + int y, x = limit? (double)i / limit * p->cols + .5 : 0; | |
| 470 | + for (y = 0; y < tick_len; ++y) /* Ticks */ | |
| 471 | + pixel(left - 1 + x, below - 1 - y) = pixel(left - 1 + x, below + p->rows + y) = Grid; | |
| 472 | + if (step == 5 && (i%10)) | |
| 473 | + continue; | |
| 474 | + sprintf(text, "%g", .1 * i); /* Tick labels */ | |
| 475 | + x = left + x - 3 * strlen(text); | |
| 476 | + print_at(x, below - 6, Labels, text); | |
| 477 | + print_at(x, below + p->rows + 14, Labels, text); | |
| 478 | + } | |
| 479 | + | |
| 480 | + /* Y-axis */ | |
| 481 | + step = axis(effp->in_signal.rate / 2, (p->rows - 1) / ((font_y * 3 + 1) >> 1), &limit, &prefix); | |
| 482 | + sprintf(text, "Frequency (%.1sHz)", prefix); /* Axis label */ | |
| 483 | + print_up(10, below + (p->rows - font_X * (int)strlen(text)) / 2, Text, text); | |
| 484 | + for (i = 0; i <= limit; i += step) { | |
| 485 | + int x, y = limit? (double)i / limit * (p->rows - 1) + .5 : 0; | |
| 486 | + for (x = 0; x < tick_len; ++x) /* Ticks */ | |
| 487 | + pixel(left - 1 - x, below + y) = pixel(left + p->cols + x, below + y) = Grid; | |
| 488 | + if (step == 5 && (i%10)) | |
| 489 | + continue; | |
| 490 | + sprintf(text, i?"%5g":" DC", .1 * i); /* Tick labels */ | |
| 491 | + print_at(left - 4 - font_X * 5, below + y + 5, Labels, text); | |
| 492 | + sprintf(text, i?"%g":"DC", .1 * i); | |
| 493 | + print_at(left + p->cols + 6, below + y + 5, Labels, text); | |
| 494 | + } | |
| 495 | + | |
| 496 | + /* Z-axis */ | |
| 497 | + print_at(cols - right - 2 - font_X, below - 13, Text, "dBFS");/* Axis label */ | |
| 498 | + for (j = 0; j < p->rows; ++j) { /* Spectrum */ | |
| 499 | + png_byte b = colour(p->dB_range * (j / (p->rows - 1.) - 1), p->dB_range); | |
| 500 | + for (i = 0; i < spectrum_width; ++i) | |
| 501 | + pixel(cols - right - 1 - i, below + j) = b; | |
| 502 | + } | |
| 503 | + for (i = 0; i <= p->dB_range; i += 10) { /* (Tick) labels */ | |
| 504 | + int y = (double)i / p->dB_range * (p->rows - 1) + .5; | |
| 505 | + sprintf(text, "%+i", i - p->gain - p->dB_range); | |
| 506 | + print_at(cols - right + 1, below + y + 5, Labels, text); | |
| 507 | + } | |
| 508 | + | |
| 509 | + free(font); | |
| 510 | + png_set_rows(png, png_info, png_rows); | |
| 511 | + png_write_png(png, png_info, PNG_TRANSFORM_IDENTITY, NULL); | |
| 512 | + png_destroy_write_struct(&png, &png_info); | |
| 513 | + free(png_rows); | |
| 514 | + free(pixels); | |
| 515 | + fclose(file); | |
| 516 | + free(p->dBfs); | |
| 517 | + return SOX_SUCCESS; | |
| 518 | +} | |
| 519 | + | |
| 520 | +sox_effect_handler_t const * sox_spectrogram_effect_fn(void) | |
| 521 | +{ | |
| 522 | + static sox_effect_handler_t handler = { | |
| 523 | + "spectrogram", "[options]\n" | |
| 524 | + " -x num X-axis pixels/second, default 100\n" | |
| 525 | + " -y num Y-axis resolution (1 - 4), default 2\n" | |
| 526 | + " -z num Z-axis (colour) range in dB, default 120\n" | |
| 527 | + " -g num Apply num dB gain; to shift Z-axis\n" | |
| 528 | + " -w name Window: Hann (default), Hamming, Bartlett, Rectangular, Kaiser\n" | |
| 529 | + " -s num Display style (1 - 4), default 1\n" | |
| 530 | + " -a Toggle the presence of the axes' lines\n" | |
| 531 | + " -t text Title text\n" | |
| 532 | + " -c text Comment text\n" | |
| 533 | + " -o text Name of the spectrogram output file, default `spectrogram.png'\n", | |
| 534 | + 0, getopts, start, flow, drain, stop, NULL, sizeof(priv_t)}; | |
| 535 | + return &handler; | |
| 536 | +} | |