| @@ -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 | } |