├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md └── waveform.c /.gitignore: -------------------------------------------------------------------------------- 1 | /waveform 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/waveform/9657025882dd534eda2f082a5077cf0420fad14a/.npmignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) 2014 Andrew Kelley 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation files 7 | (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -Wall -O3 2 | LDLIBS = -lgroove -lz -lpng -pthread 3 | 4 | waveform: 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # waveform 2 | 3 | ![](http://i.imgur.com/oNy41Cr.png) 4 | 5 | Input: any format audio or video file 6 | 7 | Output: any or all of these: 8 | 9 | * transcoded audio file 10 | * waveform.js-compatible JSON representation of the audio file 11 | * PNG rendering of the waveform 12 | 13 | ## Usage 14 | 15 | waveform [options] in [--transcode out] [--waveformjs out] [--png out] 16 | (where `in` is a file path and `out` is a file path or `-` for STDOUT) 17 | 18 | Options: 19 | --scan duration scan (default off) 20 | 21 | Transcoding Options: 22 | --bitrate 320 audio bitrate in kbps 23 | --format name e.g. mp3, ogg, mp4 24 | --codec name e.g. mp3, vorbis, flac, aac 25 | --mime mimetype e.g. audio/vorbis 26 | --tag-artist artistname artist tag 27 | --tag-title title title tag 28 | --tag-year 2000 year tag 29 | --tag-comment comment comment tag 30 | 31 | WaveformJs Options: 32 | --wjs-width 800 width in samples 33 | --wjs-precision 4 how many digits of precision 34 | --wjs-plain exclude metadata in output JSON (default off) 35 | 36 | PNG Options: 37 | --png-width 256 width of the image 38 | --png-height 64 height of the image 39 | --png-color-bg 00000000 bg color, rrggbbaa 40 | --png-color-center 000000ff gradient center color, rrggbbaa 41 | --png-color-outer 000000ff gradient outer color, rrggbbaa 42 | 43 | ## Installation 44 | 45 | 1. Install [libgroove](https://github.com/andrewrk/libgroove) dev package. 46 | Only the main library is needed. 47 | 48 | 2. Install libpng and zlib dev packages. 49 | 50 | 3. `make` 51 | 52 | ## Related Projects 53 | 54 | * [Node.js module](https://github.com/andrewrk/node-waveform) 55 | * [PHP Wrapper Script](https://github.com/polem/WaveformGenerator) 56 | * [Native Interface for Go](https://github.com/dz0ny/podcaster/blob/master/utils/waveform.go) 57 | -------------------------------------------------------------------------------- /waveform.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // PNG stuff 11 | static int16_t png_min_sample = INT16_MAX; 12 | static int16_t png_max_sample = INT16_MIN; 13 | static float y_range = (float)INT16_MAX - (float)INT16_MIN; 14 | static float y_half_range = (float)INT16_MAX; 15 | static int image_bound_y; 16 | static int png_x = 0; 17 | static png_bytep *row_pointers; 18 | static png_byte color_bg[4] = {0, 0, 0, 0}; 19 | static png_byte color_center[4] = {0, 0, 0, 255}; 20 | static png_byte color_outer[4] = {0, 0, 0, 255}; 21 | static png_bytep color_at_pix; 22 | static int png_width = 256; 23 | static int png_height = 64; 24 | static int png_frames_per_pixel; 25 | static int png_frames_until_emit; 26 | 27 | // transcoding stuff 28 | static FILE *transcode_out_f = NULL; 29 | static struct GrooveEncoder *encoder = NULL; 30 | 31 | static int version() { 32 | printf("2.0.0\n"); 33 | return 0; 34 | } 35 | 36 | static int usage(const char *exe) { 37 | fprintf(stderr, "\ 38 | \n\ 39 | Usage:\n\ 40 | \n\ 41 | waveform [options] in [--transcode out] [--waveformjs out] [--png out]\n\ 42 | (where `in` is a file path and `out` is a file path or `-` for STDOUT)\n\ 43 | \n\ 44 | Options:\n\ 45 | --scan duration scan (default off)\n\ 46 | \n\ 47 | Transcoding Options:\n\ 48 | --bitrate 320 audio bitrate in kbps\n\ 49 | --format name e.g. mp3, ogg, mp4\n\ 50 | --codec name e.g. mp3, vorbis, flac, aac\n\ 51 | --mime mimetype e.g. audio/vorbis\n\ 52 | --tag-artist artistname artist tag\n\ 53 | --tag-title title title tag\n\ 54 | --tag-year 2000 year tag\n\ 55 | --tag-comment comment comment tag\n\ 56 | \n\ 57 | WaveformJs Options:\n\ 58 | --wjs-width 800 width in samples\n\ 59 | --wjs-precision 4 how many digits of precision\n\ 60 | --wjs-plain exclude metadata in output JSON (default off)\n\ 61 | \n\ 62 | PNG Options:\n\ 63 | --png-width 256 width of the image\n\ 64 | --png-height 64 height of the image\n\ 65 | --png-color-bg 00000000 bg color, rrggbbaa\n\ 66 | --png-color-center 000000ff gradient center color, rrggbbaa\n\ 67 | --png-color-outer 000000ff gradient outer color, rrggbbaa\n\ 68 | \n"); 69 | 70 | return 1; 71 | } 72 | 73 | static void *encode_write_thread(void *arg) { 74 | struct GrooveBuffer *buffer; 75 | 76 | while (groove_encoder_buffer_get(encoder, &buffer, 1) == GROOVE_BUFFER_YES) { 77 | fwrite(buffer->data[0], 1, buffer->size, transcode_out_f); 78 | groove_buffer_unref(buffer); 79 | } 80 | return NULL; 81 | } 82 | 83 | static int16_t int16_abs(int16_t x) { 84 | return x < 0 ? -x : x; 85 | } 86 | 87 | static int double_ceil(double x) { 88 | int n = x; 89 | return (x == (double)n) ? n : n + 1; 90 | } 91 | 92 | static void parseColor(const char * hex_str, png_bytep color) { 93 | unsigned long value = strtoul(hex_str, NULL, 16); 94 | color[3] = value & 0xff; value >>= 8; 95 | color[2] = value & 0xff; value >>= 8; 96 | color[1] = value & 0xff; value >>= 8; 97 | color[0] = value & 0xff; 98 | } 99 | 100 | static void emit_png_column() { 101 | // translate into y pixel coord. 102 | float float_min_sample = (float)png_min_sample; 103 | float float_max_sample = (float)png_max_sample; 104 | int y_min = ((float_min_sample + y_half_range) / y_range) * image_bound_y; 105 | int y_max = ((float_max_sample + y_half_range) / y_range) * image_bound_y; 106 | 107 | int y = 0; 108 | int four_x = 4 * png_x; 109 | 110 | // top bg 111 | for (; y < y_min; ++y) { 112 | memcpy(row_pointers[y] + four_x, color_bg, 4); 113 | } 114 | // top and bottom wave 115 | for (; y <= y_max; ++y) { 116 | memcpy(row_pointers[y] + four_x, color_at_pix + 4*y, 4); 117 | } 118 | // bottom bg 119 | for (; y < png_height; ++y) { 120 | memcpy(row_pointers[y] + four_x, color_bg, 4); 121 | } 122 | 123 | png_x += 1; 124 | png_frames_until_emit = png_frames_per_pixel; 125 | png_max_sample = INT16_MIN; 126 | png_min_sample = INT16_MAX; 127 | } 128 | 129 | int main(int argc, char * argv[]) { 130 | // arg parsing 131 | char *exe = argv[0]; 132 | 133 | char *input_filename = NULL; 134 | char *transcode_output = NULL; 135 | char *waveformjs_output = NULL; 136 | char *png_output = NULL; 137 | 138 | int bit_rate_k = 320; 139 | char *format = NULL; 140 | char *codec = NULL; 141 | char *mime = NULL; 142 | char *tag_artist = NULL; 143 | char *tag_title = NULL; 144 | char *tag_year = NULL; 145 | char *tag_comment = NULL; 146 | 147 | int wjs_width = 800; 148 | int wjs_precision = 4; 149 | int wjs_plain = 0; 150 | 151 | int scan = 0; 152 | 153 | int i; 154 | for (i = 1; i < argc; ++i) { 155 | char *arg = argv[i]; 156 | if (arg[0] == '-' && arg[1] == '-') { 157 | arg += 2; 158 | if (strcmp(arg, "scan") == 0) { 159 | scan = 1; 160 | } else if (strcmp(arg, "wjs-plain") == 0) { 161 | wjs_plain = 1; 162 | } else if (strcmp(arg, "version") == 0) { 163 | return version(); 164 | } else if (i + 1 >= argc) { 165 | // args that take 1 parameter 166 | return usage(exe); 167 | } else if (strcmp(arg, "png") == 0) { 168 | png_output = argv[++i]; 169 | } else if (strcmp(arg, "png-width") == 0) { 170 | png_width = atoi(argv[++i]); 171 | } else if (strcmp(arg, "png-height") == 0) { 172 | png_height = atoi(argv[++i]); 173 | } else if (strcmp(arg, "png-color-bg") == 0) { 174 | parseColor(argv[++i], color_bg); 175 | } else if (strcmp(arg, "png-color-center") == 0) { 176 | parseColor(argv[++i], color_center); 177 | } else if (strcmp(arg, "png-color-outer") == 0) { 178 | parseColor(argv[++i], color_outer); 179 | } else if (strcmp(arg, "bitrate") == 0) { 180 | bit_rate_k = atoi(argv[++i]); 181 | } else if (strcmp(arg, "format") == 0) { 182 | format = argv[++i]; 183 | } else if (strcmp(arg, "codec") == 0) { 184 | codec = argv[++i]; 185 | } else if (strcmp(arg, "mime") == 0) { 186 | mime = argv[++i]; 187 | } else if (strcmp(arg, "transcode") == 0) { 188 | transcode_output = argv[++i]; 189 | } else if (strcmp(arg, "waveformjs") == 0) { 190 | waveformjs_output = argv[++i]; 191 | } else if (strcmp(arg, "tag-artist") == 0) { 192 | tag_artist = argv[++i]; 193 | } else if (strcmp(arg, "tag-title") == 0) { 194 | tag_title = argv[++i]; 195 | } else if (strcmp(arg, "tag-year") == 0) { 196 | tag_year = argv[++i]; 197 | } else if (strcmp(arg, "tag-comment") == 0) { 198 | tag_comment = argv[++i]; 199 | } else if (strcmp(arg, "wjs-width") == 0) { 200 | wjs_width = atoi(argv[++i]); 201 | } else if (strcmp(arg, "wjs-precision") == 0) { 202 | wjs_precision = atoi(argv[++i]); 203 | } else { 204 | fprintf(stderr, "Unrecognized argument: %s\n", arg); 205 | return usage(exe); 206 | } 207 | } else if (!input_filename) { 208 | input_filename = arg; 209 | } else { 210 | fprintf(stderr, "Unexpected parameter: %s\n", arg); 211 | return usage(exe); 212 | } 213 | } 214 | 215 | if (!input_filename) { 216 | fprintf(stderr, "input file parameter required\n"); 217 | return usage(exe); 218 | } 219 | 220 | if (!transcode_output && !waveformjs_output && !png_output) { 221 | fprintf(stderr, "at least one output required\n"); 222 | return usage(exe); 223 | } 224 | 225 | // arg parsing done. let's begin. 226 | 227 | groove_init(); 228 | atexit(groove_finish); 229 | 230 | struct GrooveFile *file = groove_file_open(input_filename); 231 | if (!file) { 232 | fprintf(stderr, "Error opening input file: %s\n", input_filename); 233 | return 1; 234 | } 235 | struct GroovePlaylist *playlist = groove_playlist_create(); 236 | groove_playlist_set_fill_mode(playlist, GROOVE_ANY_SINK_FULL); 237 | 238 | struct GrooveSink *sink = groove_sink_create(); 239 | sink->audio_format.sample_rate = 44100; 240 | sink->audio_format.channel_layout = GROOVE_CH_LAYOUT_STEREO; 241 | sink->audio_format.sample_fmt = GROOVE_SAMPLE_FMT_S16; 242 | 243 | if (groove_sink_attach(sink, playlist) < 0) { 244 | fprintf(stderr, "error attaching sink\n"); 245 | return 1; 246 | } 247 | 248 | struct GrooveBuffer *buffer; 249 | int frame_count; 250 | 251 | // scan the song for the exact correct frame count and duration 252 | float duration; 253 | if (scan) { 254 | struct GroovePlaylistItem *item = groove_playlist_insert(playlist, file, 1.0, 1.0, NULL); 255 | frame_count = 0; 256 | while (groove_sink_buffer_get(sink, &buffer, 1) == GROOVE_BUFFER_YES) { 257 | frame_count += buffer->frame_count; 258 | groove_buffer_unref(buffer); 259 | } 260 | groove_playlist_remove(playlist, item); 261 | duration = frame_count / 44100.0f; 262 | } else { 263 | duration = groove_file_duration(file); 264 | frame_count = double_ceil(duration * 44100.0); 265 | } 266 | 267 | pthread_t thread_id; 268 | if (transcode_output) { 269 | if (strcmp(transcode_output, "-") == 0) { 270 | transcode_out_f = stdout; 271 | } else { 272 | transcode_out_f = fopen(transcode_output, "wb"); 273 | if (!transcode_out_f) { 274 | fprintf(stderr, "Error opening output file %s\n", transcode_output); 275 | return 1; 276 | } 277 | } 278 | 279 | encoder = groove_encoder_create(); 280 | encoder->bit_rate = bit_rate_k * 1000; 281 | encoder->format_short_name = format; 282 | encoder->codec_short_name = codec; 283 | encoder->filename = transcode_output; 284 | encoder->mime_type = mime; 285 | if (tag_artist) 286 | groove_encoder_metadata_set(encoder, "artist", tag_artist, 0); 287 | if (tag_title) 288 | groove_encoder_metadata_set(encoder, "title", tag_title, 0); 289 | if (tag_year) 290 | groove_encoder_metadata_set(encoder, "year", tag_year, 0); 291 | if (tag_comment) 292 | groove_encoder_metadata_set(encoder, "comment", tag_comment, 0); 293 | 294 | encoder->target_audio_format.sample_rate = 44100; 295 | encoder->target_audio_format.channel_layout = GROOVE_CH_LAYOUT_STEREO; 296 | encoder->target_audio_format.sample_fmt = GROOVE_SAMPLE_FMT_S16; 297 | 298 | if (groove_encoder_attach(encoder, playlist) < 0) { 299 | fprintf(stderr, "error attaching encoder\n"); 300 | return 1; 301 | } 302 | 303 | // this thread pops encoded audio buffers from the sink and writes them 304 | // to the output 305 | // we'll use the main thread for waveformjs/png output 306 | pthread_create(&thread_id, NULL, encode_write_thread, NULL); 307 | } 308 | 309 | // insert the item *after* creating the sinks to avoid race conditions 310 | groove_playlist_insert(playlist, file, 1.0, 1.0, NULL); 311 | 312 | int png_center_y; 313 | double png_center_y_float; 314 | FILE *png_file = NULL; 315 | png_structp png = NULL; 316 | png_infop png_info = NULL; 317 | if (png_output) { 318 | png_frames_per_pixel = frame_count / png_width; 319 | if (png_frames_per_pixel < 1) png_frames_per_pixel = 1; 320 | 321 | png_center_y = png_height / 2; 322 | png_center_y_float = (double) png_center_y; 323 | 324 | png_frames_until_emit = png_frames_per_pixel; 325 | 326 | if (strcmp(png_output, "-") == 0) { 327 | png_file = stdout; 328 | } else { 329 | png_file = fopen(png_output, "wb"); 330 | if (!png_file) { 331 | fprintf(stderr, "Unable to open %s for writing\n", png_output); 332 | return 1; 333 | } 334 | } 335 | 336 | png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 337 | if (!png) { 338 | fprintf(stderr, "Unable to allocate png write structure\n"); 339 | return 1; 340 | } 341 | 342 | png_info = png_create_info_struct(png); 343 | 344 | if (! png_info) { 345 | fprintf(stderr, "Unable to allocate png info structure\n"); 346 | return 1; 347 | } 348 | 349 | png_init_io(png, png_file); 350 | 351 | png_set_IHDR(png, png_info, png_width, png_height, 8, 352 | PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, 353 | PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); 354 | 355 | png_write_info(png, png_info); 356 | 357 | // allocate memory to write to png file 358 | row_pointers = (png_bytep *) malloc(sizeof(png_bytep) * png_height); 359 | 360 | if (! row_pointers) { 361 | fprintf(stderr, "Out of memory.\n"); 362 | return 1; 363 | } 364 | 365 | color_at_pix = (png_bytep) malloc(sizeof(png_byte) * png_height * 4); 366 | 367 | if (! color_at_pix) { 368 | fprintf(stderr, "Out of memory.\n"); 369 | return 1; 370 | } 371 | 372 | int y; 373 | for (y = 0; y < png_height; ++y) { 374 | png_bytep row = (png_bytep) malloc(png_width * 4); 375 | if (! row) { 376 | fprintf(stderr, "Out of memory.\n"); 377 | return 1; 378 | } 379 | row_pointers[y] = row; 380 | 381 | // compute the foreground color at each y pixel 382 | int i; 383 | for (i = 0; i < 4; ++i) { 384 | double amt = abs(y - png_center_y) / png_center_y_float; 385 | color_at_pix[4*y + i] = (1-amt) * color_center[i] + amt * color_outer[i]; 386 | } 387 | } 388 | 389 | image_bound_y = png_height - 1; 390 | } 391 | 392 | FILE *waveformjs_f = NULL; 393 | int wjs_frames_per_pixel = 0; 394 | int16_t wjs_max_sample; 395 | int wjs_frames_until_emit = 0; 396 | int wjs_emit_count; 397 | if (waveformjs_output) { 398 | if (strcmp(waveformjs_output, "-") == 0) { 399 | waveformjs_f = stdout; 400 | } else { 401 | waveformjs_f = fopen(waveformjs_output, "wb"); 402 | if (!waveformjs_f) { 403 | fprintf(stderr, "Error opening output file: %s\n", waveformjs_output); 404 | return 1; 405 | } 406 | } 407 | 408 | wjs_frames_per_pixel = frame_count / wjs_width; 409 | if (wjs_frames_per_pixel < 1) 410 | wjs_frames_per_pixel = 1; 411 | 412 | if (!wjs_plain) { 413 | fprintf(waveformjs_f, "{\"frameCount\":%d,\"frameRate\":%d, \"waveformjs\":", 414 | frame_count, 44100); 415 | } 416 | 417 | fprintf(waveformjs_f, "["); 418 | 419 | wjs_max_sample = INT16_MIN; 420 | wjs_frames_until_emit = wjs_frames_per_pixel; 421 | wjs_emit_count = 0; 422 | } 423 | 424 | while (groove_sink_buffer_get(sink, &buffer, 1) == GROOVE_BUFFER_YES) { 425 | if (png_output) { 426 | int i; 427 | for (i = 0; i < buffer->frame_count && png_x < png_width; 428 | i += 1, png_frames_until_emit -= 1) 429 | { 430 | if (png_frames_until_emit == 0) { 431 | emit_png_column(); 432 | } 433 | int16_t *samples = (int16_t *) buffer->data[0]; 434 | int16_t left = samples[i]; 435 | int16_t right = samples[i + 1]; 436 | int16_t avg = (left + right) / 2; 437 | 438 | if (avg > png_max_sample) png_max_sample = avg; 439 | if (avg < png_min_sample) png_min_sample = avg; 440 | } 441 | } 442 | if (waveformjs_output) { 443 | int i; 444 | for (i = 0; i < buffer->frame_count && wjs_emit_count < wjs_width; 445 | i += 1, wjs_frames_until_emit -= 1) 446 | { 447 | if (wjs_frames_until_emit == 0) { 448 | wjs_emit_count += 1; 449 | char *comma = (wjs_emit_count == wjs_width) ? "" : ","; 450 | double float_sample = wjs_max_sample / (double) INT16_MAX; 451 | fprintf(waveformjs_f, "%.*f%s", wjs_precision, float_sample, comma); 452 | wjs_max_sample = INT16_MIN; 453 | wjs_frames_until_emit = wjs_frames_per_pixel; 454 | } 455 | int16_t *data = (int16_t *) buffer->data[0]; 456 | int16_t *left = &data[i]; 457 | int16_t *right = &data[i + 1]; 458 | int16_t abs_left = int16_abs(*left); 459 | int16_t abs_right = int16_abs(*right); 460 | if (abs_left > wjs_max_sample) wjs_max_sample = abs_left; 461 | if (abs_right > wjs_max_sample) wjs_max_sample = abs_right; 462 | } 463 | } 464 | 465 | groove_buffer_unref(buffer); 466 | } 467 | 468 | if (png_output) { 469 | // emit the last column if necessary. This will have to run multiple times 470 | // if the duration specified in the metadata is incorrect. 471 | while (png_x < png_width) { 472 | emit_png_column(); 473 | } 474 | 475 | png_write_image(png, row_pointers); 476 | png_write_end(png, png_info); 477 | fclose(png_file); 478 | } 479 | 480 | if (waveformjs_output) { 481 | if (wjs_emit_count < wjs_width) { 482 | // emit the last sample 483 | double float_sample = wjs_max_sample / (double) INT16_MAX; 484 | fprintf(waveformjs_f, "%.*f", wjs_precision, float_sample); 485 | } 486 | 487 | fprintf(waveformjs_f, "]"); 488 | 489 | if (!wjs_plain) 490 | fprintf(waveformjs_f, "}"); 491 | 492 | fclose(waveformjs_f); 493 | } 494 | 495 | if (transcode_output) { 496 | pthread_join(thread_id, NULL); 497 | fclose(transcode_out_f); 498 | 499 | groove_encoder_detach(encoder); 500 | groove_encoder_destroy(encoder); 501 | } 502 | 503 | 504 | groove_sink_detach(sink); 505 | groove_sink_destroy(sink); 506 | 507 | groove_playlist_clear(playlist); 508 | groove_file_close(file); 509 | groove_playlist_destroy(playlist); 510 | 511 | return 0; 512 | } 513 | --------------------------------------------------------------------------------