| /* Opusinfo |
| * |
| * A tool to describe opus file contents and metadata. |
| * |
| * This is a fork of ogginfo from the vorbis-tools package |
| * which has been cut down to only have opus support. |
| * |
| * Ogginfo is |
| * Copyright 2002-2005 Michael Smith <msmith@xiph.org> |
| * Licensed under the GNU GPL, distributed with this program. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <string.h> |
| #include <stdarg.h> |
| #include <getopt.h> |
| #include <math.h> |
| |
| #include <ogg/ogg.h> |
| |
| /*No NLS support for now*/ |
| #define _(X) (X) |
| |
| #include "opusinfo.h" |
| #include "opus_header.h" |
| #include "info_opus.h" |
| #include "picture.h" |
| |
| #if defined WIN32 || defined _WIN32 || defined WIN64 || defined _WIN64 |
| # include "unicode_support.h" |
| #else |
| # define fopen_utf8(_x,_y) fopen((_x),(_y)) |
| # define argc_utf8 argc |
| # define argv_utf8 argv |
| #endif |
| |
| #define CHUNK 4500 |
| |
| static int printlots = 0; |
| static int printinfo = 1; |
| static int printwarn = 1; |
| static int verbose = 1; |
| |
| static int flawed; |
| |
| #define CONSTRAINT_PAGE_AFTER_EOS 1 |
| #define CONSTRAINT_MUXING_VIOLATED 2 |
| |
| static stream_set *create_stream_set(void) { |
| stream_set *set = calloc(1, sizeof(stream_set)); |
| |
| set->streams = calloc(5, sizeof(stream_processor)); |
| set->allocated = 5; |
| set->used = 0; |
| |
| return set; |
| } |
| |
| void oi_info(char *format, ...) |
| { |
| va_list ap; |
| |
| if(!printinfo) |
| return; |
| |
| va_start(ap, format); |
| vfprintf(stdout, format, ap); |
| va_end(ap); |
| } |
| |
| void oi_warn(char *format, ...) |
| { |
| va_list ap; |
| |
| flawed = 1; |
| if(!printwarn) |
| return; |
| |
| va_start(ap, format); |
| vfprintf(stdout, format, ap); |
| va_end(ap); |
| } |
| |
| void oi_error(char *format, ...) |
| { |
| va_list ap; |
| |
| flawed = 1; |
| |
| va_start(ap, format); |
| vfprintf(stdout, format, ap); |
| va_end(ap); |
| } |
| |
| #define READ_U32_BE(buf) \ |
| (((buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|((buf)[3]&0xff)) |
| |
| void check_xiph_comment(stream_processor *stream, int i, const char *comment, |
| int comment_length) |
| { |
| char *sep = strchr(comment, '='); |
| int j; |
| int broken = 0; |
| unsigned char *val; |
| int bytes; |
| int remaining; |
| |
| if(sep == NULL) { |
| oi_warn(_("WARNING: Comment %d in stream %d has invalid " |
| "format, does not contain '=': \"%s\"\n"), |
| i, stream->num, comment); |
| return; |
| } |
| |
| for(j=0; j < sep-comment; j++) { |
| if(comment[j] < 0x20 || comment[j] > 0x7D) { |
| oi_warn(_("WARNING: Invalid comment fieldname in " |
| "comment %d (stream %d): \"%s\"\n"), |
| i, stream->num, comment); |
| broken = 1; |
| break; |
| } |
| } |
| |
| if(broken) |
| return; |
| |
| val = (unsigned char *)comment; |
| |
| j = sep-comment+1; |
| while(j < comment_length) |
| { |
| remaining = comment_length - j; |
| if((val[j] & 0x80) == 0) |
| bytes = 1; |
| else if((val[j] & 0x40) == 0x40) { |
| if((val[j] & 0x20) == 0) |
| bytes = 2; |
| else if((val[j] & 0x10) == 0) |
| bytes = 3; |
| else if((val[j] & 0x08) == 0) |
| bytes = 4; |
| else if((val[j] & 0x04) == 0) |
| bytes = 5; |
| else if((val[j] & 0x02) == 0) |
| bytes = 6; |
| else { |
| oi_warn(_("WARNING: Illegal UTF-8 sequence in " |
| "comment %d (stream %d): length marker wrong\n"), |
| i, stream->num); |
| broken = 1; |
| break; |
| } |
| } |
| else { |
| oi_warn(_("WARNING: Illegal UTF-8 sequence in comment " |
| "%d (stream %d): length marker wrong\n"), i, stream->num); |
| broken = 1; |
| break; |
| } |
| |
| if(bytes > remaining) { |
| oi_warn(_("WARNING: Illegal UTF-8 sequence in comment " |
| "%d (stream %d): too few bytes\n"), i, stream->num); |
| broken = 1; |
| break; |
| } |
| |
| switch(bytes) { |
| case 1: |
| /* No more checks needed */ |
| break; |
| case 2: |
| if((val[j+1] & 0xC0) != 0x80) |
| broken = 1; |
| if((val[j] & 0xFE) == 0xC0) |
| broken = 1; |
| break; |
| case 3: |
| if(!((val[j] == 0xE0 && val[j+1] >= 0xA0 && val[j+1] <= 0xBF && |
| (val[j+2] & 0xC0) == 0x80) || |
| (val[j] >= 0xE1 && val[j] <= 0xEC && |
| (val[j+1] & 0xC0) == 0x80 && |
| (val[j+2] & 0xC0) == 0x80) || |
| (val[j] == 0xED && val[j+1] >= 0x80 && |
| val[j+1] <= 0x9F && |
| (val[j+2] & 0xC0) == 0x80) || |
| (val[j] >= 0xEE && val[j] <= 0xEF && |
| (val[j+1] & 0xC0) == 0x80 && |
| (val[j+2] & 0xC0) == 0x80))) |
| broken = 1; |
| if(val[j] == 0xE0 && (val[j+1] & 0xE0) == 0x80) |
| broken = 1; |
| break; |
| case 4: |
| if(!((val[j] == 0xF0 && val[j+1] >= 0x90 && |
| val[j+1] <= 0xBF && |
| (val[j+2] & 0xC0) == 0x80 && |
| (val[j+3] & 0xC0) == 0x80) || |
| (val[j] >= 0xF1 && val[j] <= 0xF3 && |
| (val[j+1] & 0xC0) == 0x80 && |
| (val[j+2] & 0xC0) == 0x80 && |
| (val[j+3] & 0xC0) == 0x80) || |
| (val[j] == 0xF4 && val[j+1] >= 0x80 && |
| val[j+1] <= 0x8F && |
| (val[j+2] & 0xC0) == 0x80 && |
| (val[j+3] & 0xC0) == 0x80))) |
| broken = 1; |
| if(val[j] == 0xF0 && (val[j+1] & 0xF0) == 0x80) |
| broken = 1; |
| break; |
| /* 5 and 6 aren't actually allowed at this point */ |
| case 5: |
| broken = 1; |
| break; |
| case 6: |
| broken = 1; |
| break; |
| } |
| |
| if(broken) { |
| char *simple = malloc (comment_length + 1); |
| char *seq = malloc (comment_length * 3 + 1); |
| static char hex[] = {'0', '1', '2', '3', '4', '5', '6', '7', |
| '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; |
| int k, c1 = 0, c2 = 0; |
| for (k = 0; k < comment_length; k++) { |
| seq[c1++] = hex[((unsigned char)comment[k]) >> 4]; |
| seq[c1++] = hex[((unsigned char)comment[k]) & 0xf]; |
| seq[c1++] = ' '; |
| |
| if(comment[k] < 0x20 || comment[k] > 0x7D) |
| simple[c2++] = '?'; |
| else |
| simple[c2++] = comment[k]; |
| } |
| seq[c1] = 0; |
| simple[c2] = 0; |
| oi_warn(_("WARNING: Illegal UTF-8 sequence in comment " |
| "%d (stream %d): invalid sequence \"%s\": %s\n"), i, |
| stream->num, simple, seq); |
| broken = 1; |
| free (simple); |
| free (seq); |
| break; |
| } |
| |
| j += bytes; |
| } |
| |
| if(sep - comment == 22 |
| && oi_strncasecmp(comment, "METADATA_BLOCK_PICTURE", 22) == 0) { |
| ogg_uint32_t picture_type; |
| ogg_uint32_t mime_type_length; |
| ogg_uint32_t description_length; |
| ogg_uint32_t width; |
| ogg_uint32_t height; |
| ogg_uint32_t depth; |
| ogg_uint32_t colors; |
| ogg_uint32_t image_length; |
| ogg_uint32_t file_width; |
| ogg_uint32_t file_height; |
| ogg_uint32_t file_depth; |
| ogg_uint32_t file_colors; |
| unsigned char *data; |
| int data_sz; |
| int len; |
| int is_url; |
| int format; |
| int has_palette; |
| int colors_set; |
| len=comment_length - (sep+1-comment); |
| /*Decode the Base64 encoded data.*/ |
| if(len&3) { |
| oi_warn(_("WARNING: Illegal Base64 length in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): %i is not " |
| "divisible by 4\n"), i, stream->num, len); |
| } |
| len>>=2; |
| data_sz=3*len; |
| if(data_sz > 0) { |
| if(comment[comment_length - 1] == '=') { |
| data_sz--; |
| } |
| if(comment[comment_length - 2] == '=') { |
| data_sz--; |
| } |
| } |
| data=(unsigned char *)malloc(data_sz*sizeof(*data)); |
| for (j = 0; j < len; j++) { |
| ogg_uint32_t value; |
| int k; |
| value = 0; |
| for (k = 1; k <= 4; k++) { |
| unsigned c; |
| unsigned d; |
| c = (unsigned char)sep[4*j+k]; |
| if(c == '+') { |
| d = 62; |
| } |
| else if(c == '/') { |
| d = 63; |
| } |
| else if(c >= '0' && c <= '9') { |
| d = 52+c-'0'; |
| } |
| else if(c >= 'a' && c <= 'z') { |
| d = 26+c-'a'; |
| } |
| else if(c >= 'A' && c <= 'Z') { |
| d = c-'A'; |
| } |
| else if(c == '=') { |
| if(3*j+k-1 < data_sz) { |
| oi_warn(_("WARNING: Terminating '=' in illegal " |
| "position in Base64 encoded " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "%i characters before the end.\n"), i, |
| stream->num, data_sz - (3*j+k-1)); |
| free(data); |
| return; |
| } |
| d = 0; |
| } |
| else { |
| oi_warn(_("WARNING: Illegal Base64 character in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "'%c' (0x%02X)\n"), i, stream->num, |
| (char)(c<0x20||c>0x7E?'?':c), c); |
| free(data); |
| return; |
| } |
| value = value << 6 | d; |
| } |
| data[3*j] = (unsigned char)(value>>16); |
| if(3*j+1 < data_sz) { |
| data[3*j+1] = (unsigned char)(value>>8); |
| if(3*j+2 < data_sz) { |
| data[3*j+2] = (unsigned char)value; |
| } |
| } |
| } |
| /*Now validate the METADATA_BLOCK_PICTURE structure.*/ |
| if(data_sz < 32) { |
| oi_warn(_("WARNING: Not enough data for " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "expected at least 32 bytes, got %i\n"), i, stream->num, |
| data_sz); |
| free(data); |
| return; |
| } |
| j = 0; |
| picture_type = READ_U32_BE(data+j); |
| if(picture_type > 20) { |
| oi_warn(_("WARNING: Unknown picture type in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "%li\n"), i, stream->num, (long)picture_type); |
| broken = 1; |
| } |
| if(picture_type >= 1 && picture_type <= 2) { |
| if(stream->seen_file_icons & picture_type) { |
| oi_warn(_("WARNING: Duplicate picture type in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| " %s\n"), i, stream->num, picture_type == 1 ? |
| _("only one picture of type 1 (32x32 icon) allowed") : |
| _("only one picture of type 2 (icon) allowed")); |
| broken = 1; |
| } |
| stream->seen_file_icons |= picture_type; |
| } |
| j += 4; |
| mime_type_length = READ_U32_BE(data+j); |
| if(mime_type_length > (size_t)data_sz-32) { |
| oi_warn(_("WARNING: Invalid mime type length in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "%lu bytes when %i are available\n"), i, stream->num, |
| (long)mime_type_length, data_sz-32); |
| free(data); |
| return; |
| } |
| for (j += 4; j < 8+(int)mime_type_length; j++) { |
| if(data[j] < 0x20 || data[j] > 0x7E) { |
| oi_warn(_("WARNING: Invalid character in mime type of " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "0x%02X\n"), i, stream->num, data[j]); |
| broken = 1; |
| } |
| } |
| description_length = READ_U32_BE(data+j); |
| if(description_length > (size_t)data_sz-mime_type_length-32) { |
| oi_warn(_("WARNING: Invalid description length in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "%lu bytes when %i are available\n"), i, stream->num, |
| (long)description_length, data_sz-mime_type_length-32); |
| free(data); |
| return; |
| } |
| /*TODO: Validate that description is UTF-8.*/ |
| j += 4+description_length; |
| width = READ_U32_BE(data+j); |
| j += 4; |
| height = READ_U32_BE(data+j); |
| j += 4; |
| depth = READ_U32_BE(data+j); |
| j += 4; |
| colors = READ_U32_BE(data+j); |
| j += 4; |
| /*If any value is non-zero, then they all MUST be valid values, and |
| so colors should be treated as set (even if zero).*/ |
| colors_set = width != 0 || height != 0 || depth != 0 || colors != 0; |
| /*This isn't triggered if colors == 0, since that can be a valid |
| value.*/ |
| if((width == 0 || height == 0 || depth == 0) && colors_set) { |
| oi_warn(_("WARNING: Invalid picture parameters in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "width (%i), height (%i), depth (%i), and colors (%i) MUST " |
| "either be set to valid values or all set to 0\n"), i, |
| stream->num, (int)width, (int)height, (int)depth, |
| (int)colors); |
| broken = 1; |
| } |
| image_length = READ_U32_BE(data+j); |
| j += 4; |
| /*This one should match exactly.*/ |
| if(image_length != (size_t)data_sz-j) { |
| oi_warn(_("WARNING: Invalid image data size in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "%lu bytes when %i are available\n"), i, stream->num, |
| (long)image_length, data_sz-j); |
| free(data); |
| return; |
| } |
| is_url = 0; |
| format = -1; |
| if(mime_type_length == 10 |
| && oi_strncasecmp((const char*)data+8, "image/jpeg", |
| mime_type_length) == 0) { |
| if(!is_jpeg(data+j, image_length)) { |
| oi_warn(_("WARNING: Invalid image data in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "mime type is %.*s but image does not appear to be " |
| "JPEG\n"), i, stream->num, mime_type_length, data+8); |
| free(data); |
| return; |
| } |
| format = PIC_FORMAT_JPEG; |
| } |
| else if(mime_type_length == 9 |
| && oi_strncasecmp((const char *)data+8, "image/png", |
| mime_type_length) == 0) { |
| if(!is_png(data+j, image_length)) { |
| oi_warn(_("WARNING: Invalid image data in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "mime type is %.*s but image does not appear to be " |
| "PNG\n"), i, stream->num, mime_type_length, data+8); |
| free(data); |
| return; |
| } |
| format = PIC_FORMAT_PNG; |
| } |
| else if(mime_type_length == 9 |
| && oi_strncasecmp((const char *)data+8, "image/gif", |
| mime_type_length) == 0) { |
| if(!is_gif(data+j, image_length)) { |
| oi_warn(_("WARNING: Invalid image data in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "mime type is %.*s but image does not appear to be " |
| "PNG\n"), i, stream->num, mime_type_length, data+8); |
| free(data); |
| return; |
| } |
| format = PIC_FORMAT_GIF; |
| } |
| else if(mime_type_length == 3 |
| && strncmp((const char *)data+8, "-->", mime_type_length) == 0) { |
| is_url = 1; |
| /*TODO: validate URL.*/ |
| } |
| else if(mime_type_length == 0 || (mime_type_length == 6 && |
| oi_strncasecmp((const char *)data+8, "image/", |
| mime_type_length) == 0)) { |
| if(is_jpeg(data+j, image_length)) { |
| format = PIC_FORMAT_JPEG; |
| } |
| else if(is_png(data+j, image_length)) { |
| format = PIC_FORMAT_PNG; |
| } |
| else if(is_gif(data+j, image_length)) { |
| format = PIC_FORMAT_GIF; |
| } |
| else { |
| oi_warn(_("WARNING: Unknown image format in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "\"%.*s\" may not be well-supported\n"), i, stream->num, |
| mime_type_length, data+8); |
| } |
| } |
| else { |
| oi_warn(_("WARNING: Unknown mime type in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "\"%.*s\" may not be well-supported\n"), i, stream->num, |
| mime_type_length, data+8); |
| } |
| file_width = file_height = file_depth = file_colors = 0; |
| has_palette = -1; |
| switch(format) { |
| case PIC_FORMAT_JPEG: |
| extract_jpeg_params(data+j, image_length, |
| &file_width, &file_height, &file_depth, &file_colors, |
| &has_palette); |
| break; |
| case PIC_FORMAT_PNG: |
| extract_png_params(data+j, image_length, |
| &file_width, &file_height, &file_depth, &file_colors, |
| &has_palette); |
| break; |
| case PIC_FORMAT_GIF: |
| extract_gif_params(data+j, image_length, |
| &file_width, &file_height, &file_depth, &file_colors, |
| &has_palette); |
| break; |
| } |
| if(format >= 0 && has_palette < 0) { |
| /*We should have been able to affirmatively determine whether or |
| not there was a palette if we parsed the image successfully.*/ |
| oi_warn(_("WARNING: Could not parse image parameters in" |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "possibly corrupt image?\n"), i, stream->num); |
| broken = 1; |
| } |
| if(width && width != file_width) { |
| oi_warn(_("WARNING: Mismatched picture parameters in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "width declared as %u but appears to be %u\n"), i, |
| stream->num, (unsigned)width, (unsigned)file_width); |
| broken = 1; |
| } |
| if(height && height != file_height) { |
| oi_warn(_("WARNING: Mismatched picture parameters in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "height declared as %u but appears to be %u\n"), i, |
| stream->num, (unsigned)height, (unsigned)file_height); |
| broken = 1; |
| } |
| if(depth && depth != file_depth) { |
| oi_warn(_("WARNING: Mismatched picture parameters in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "depth declared as %u but appears to be %u\n"), i, |
| stream->num, (unsigned)depth, (unsigned)file_depth); |
| broken = 1; |
| } |
| if(has_palette >= 0 && colors_set && colors != file_colors) { |
| oi_warn(_("WARNING: Mismatched picture parameters in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "palette size declared as %u but appears to be %u\n"), i, |
| stream->num, (unsigned)colors, (unsigned)file_colors); |
| broken = 1; |
| } |
| if(picture_type == 1 |
| && ((is_url && (width != 0 || height != 0) |
| && (width != 32 || height != 32)) |
| || (!is_url && (file_width != 32 || file_height != 32)))) { |
| oi_warn(_("WARNING: Invalid picture in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "picture of type 1 (32x32 icon) MUST be a 32x32 PNG, but " |
| "the image has dimensions %ux%u\n"), i, stream->num, |
| (unsigned)is_url?width:file_width, |
| (unsigned)is_url?height:file_height); |
| broken = 1; |
| } |
| if(picture_type == 1 && !is_url && format != PIC_FORMAT_PNG) { |
| oi_warn(_("WARNING: Invalid picture in " |
| "METADATA_BLOCK_PICTURE comment %d (stream %d): " |
| "picture of type 1 (32x32 icon) MUST be a 32x32 PNG, but " |
| "the image does not appear to be a PNG\n"), i, stream->num); |
| broken = 1; |
| } |
| /*Print the contents of the block using the same format as the |
| SPECIFICATION argument to opusenc/flac/etc. (except without an image |
| filename, since we don't know the original).*/ |
| oi_info("\t%.*s%u|%.*s|%.*s|%ux%ux%u", |
| sep+1-comment, comment, (unsigned)picture_type, |
| mime_type_length, data+8, |
| description_length, data+12+mime_type_length, |
| (unsigned)width, (unsigned)height, (unsigned)depth); |
| if(colors) { |
| oi_info("/%u", (unsigned)colors); |
| } |
| if(is_url) { |
| oi_info("|%.*s\n", image_length, data+j); |
| } |
| else { |
| oi_info("|<%u bytes of image data>\n",(unsigned)image_length); |
| } |
| free(data); |
| return; |
| } |
| |
| if(!broken) { |
| oi_info("\t%s\n", comment); |
| } |
| } |
| |
| static void process_null(stream_processor *stream, ogg_page *page) |
| { |
| /* This is for invalid streams. */ |
| (void)stream; |
| (void)page; |
| } |
| |
| static void process_other(stream_processor *stream, ogg_page *page ) |
| { |
| ogg_packet packet; |
| |
| ogg_stream_pagein(&stream->os, page); |
| |
| while(ogg_stream_packetout(&stream->os, &packet) > 0) { |
| /* Should we do anything here? Currently, we don't */ |
| } |
| } |
| |
| |
| static void free_stream_set(stream_set *set) |
| { |
| int i; |
| for(i=0; i < set->used; i++) { |
| if(!set->streams[i].end) { |
| oi_warn(_("WARNING: EOS not set on stream %d (normal for live streams)\n"), |
| set->streams[i].num); |
| if(set->streams[i].process_end) |
| set->streams[i].process_end(&set->streams[i]); |
| } |
| ogg_stream_clear(&set->streams[i].os); |
| } |
| |
| free(set->streams); |
| free(set); |
| } |
| |
| static int streams_open(stream_set *set) |
| { |
| int i; |
| int res=0; |
| for(i=0; i < set->used; i++) { |
| if(!set->streams[i].end) |
| res++; |
| } |
| |
| return res; |
| } |
| |
| static void null_start(stream_processor *stream) |
| { |
| stream->process_end = NULL; |
| stream->type = "invalid"; |
| stream->process_page = process_null; |
| } |
| |
| static void other_start(stream_processor *stream, char *type) |
| { |
| if(type) |
| stream->type = type; |
| else |
| stream->type = "unknown"; |
| stream->process_page = process_other; |
| stream->process_end = NULL; |
| } |
| |
| static stream_processor *find_stream_processor(stream_set *set, ogg_page *page) |
| { |
| ogg_uint32_t serial = ogg_page_serialno(page); |
| int i; |
| int invalid = 0; |
| int constraint = 0; |
| stream_processor *stream; |
| |
| for(i=0; i < set->used; i++) { |
| if(serial == set->streams[i].serial) { |
| /* We have a match! */ |
| stream = &(set->streams[i]); |
| |
| set->in_headers = 0; |
| /* if we have detected EOS, then this can't occur here. */ |
| if(stream->end) { |
| stream->isillegal = 1; |
| stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; |
| return stream; |
| } |
| |
| stream->isnew = 0; |
| stream->start = ogg_page_bos(page); |
| stream->end = ogg_page_eos(page); |
| stream->serial = serial; |
| return stream; |
| } |
| } |
| |
| /* If there are streams open, and we've reached the end of the |
| * headers, then we can't be starting a new stream. |
| * XXX: might this sometimes catch ok streams if EOS flag is missing, |
| * but the stream is otherwise ok? |
| */ |
| if(streams_open(set) && !set->in_headers) { |
| constraint = CONSTRAINT_MUXING_VIOLATED; |
| invalid = 1; |
| } |
| |
| set->in_headers = 1; |
| |
| if(set->allocated < set->used) |
| stream = &set->streams[set->used]; |
| else { |
| set->allocated += 5; |
| set->streams = realloc(set->streams, sizeof(stream_processor)* |
| set->allocated); |
| stream = &set->streams[set->used]; |
| } |
| set->used++; |
| stream->num = set->used; /* We count from 1 */ |
| |
| stream->isnew = 1; |
| stream->isillegal = invalid; |
| stream->constraint_violated = constraint; |
| stream->seen_file_icons = 0; |
| |
| { |
| int res; |
| int has_oi_supported=0; |
| ogg_packet packet; |
| |
| /* We end up processing the header page twice, but that's ok. */ |
| ogg_stream_init(&stream->os, serial); |
| ogg_stream_pagein(&stream->os, page); |
| res = ogg_stream_packetout(&stream->os, &packet); |
| if(res <= 0) { |
| oi_warn(_("WARNING: Invalid header page, no packet found\n")); |
| null_start(stream); |
| } |
| else if(packet.bytes >= 19 && memcmp(packet.packet, "OpusHead", 8)==0) |
| info_opus_start(stream); |
| else if(packet.bytes >= 7 && memcmp(packet.packet, "\x01vorbis", 7)==0) { |
| other_start(stream, "Vorbis"); |
| has_oi_supported=1; |
| } else if(packet.bytes >= 7 && memcmp(packet.packet, "\x80theora", 7)==0) { |
| other_start(stream, "Theora"); |
| has_oi_supported=1; |
| } else if(packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8)==0) |
| other_start(stream, "MIDI"); |
| else if(packet.bytes >= 5 && memcmp(packet.packet, "\177FLAC", 5)==0) |
| other_start(stream, "FLAC"); |
| else if(packet.bytes == 4 && memcmp(packet.packet, "fLaC", 4)==0) |
| other_start(stream, "FLAC (legacy)"); |
| else if(packet.bytes >= 8 && memcmp(packet.packet, "Speex ", 8)==0) |
| other_start(stream, "speex"); |
| else if(packet.bytes >= 8 && memcmp(packet.packet, "fishead\0", 8)==0) |
| other_start(stream, "skeleton"); |
| else if(packet.bytes >= 5 && memcmp(packet.packet, "BBCD\0", 5)==0) |
| other_start(stream, "dirac"); |
| else if(packet.bytes >= 8 && memcmp(packet.packet, "KW-DIRAC", 8)==0) |
| other_start(stream, "dirac (legacy)"); |
| else if(packet.bytes >= 8 && memcmp(packet.packet, "\x80kate\0\0\0", 8)==0) { |
| other_start(stream, "Kate"); |
| has_oi_supported=1; |
| } else |
| other_start(stream, NULL); |
| |
| res = ogg_stream_packetout(&stream->os, &packet); |
| if(res > 0) { |
| oi_warn(_("WARNING: Invalid header page in stream %d, " |
| "contains multiple packets\n"), stream->num); |
| } |
| if(has_oi_supported)oi_info(_("Use ogginfo for more information on this file.\n")); |
| |
| /* re-init, ready for processing */ |
| ogg_stream_clear(&stream->os); |
| ogg_stream_init(&stream->os, serial); |
| } |
| |
| stream->start = ogg_page_bos(page); |
| stream->end = ogg_page_eos(page); |
| stream->serial = serial; |
| stream->shownillegal = 0; |
| stream->seqno = ogg_page_pageno(page); |
| |
| if(stream->serial == 0 || stream->serial == 0xFFFFFFFFUL) { |
| oi_info(_("Note: Stream %d has serial number %d, which is legal but may " |
| "cause problems with some tools.\n"), stream->num, |
| stream->serial); |
| } |
| |
| return stream; |
| } |
| |
| static int get_next_page(FILE *f, ogg_sync_state *ogsync, ogg_page *page, |
| ogg_int64_t *written) |
| { |
| int ret; |
| char *buffer; |
| int bytes; |
| |
| while((ret = ogg_sync_pageseek(ogsync, page)) <= 0) { |
| if(ret < 0) { |
| /* unsynced, we jump over bytes to a possible capture - we don't need to read more just yet */ |
| oi_warn(_("WARNING: Hole in data (%d bytes) found at approximate offset %" I64FORMAT " bytes. Corrupted Ogg.\n"), -ret, *written); |
| continue; |
| } |
| |
| /* zero return, we didn't have enough data to find a whole page, read */ |
| buffer = ogg_sync_buffer(ogsync, CHUNK); |
| bytes = fread(buffer, 1, CHUNK, f); |
| if(bytes <= 0) { |
| ogg_sync_wrote(ogsync, 0); |
| return 0; |
| } |
| ogg_sync_wrote(ogsync, bytes); |
| *written += bytes; |
| } |
| |
| return 1; |
| } |
| |
| static void process_file(char *filename) { |
| FILE *file = fopen_utf8(filename, "rb"); |
| ogg_sync_state ogsync; |
| ogg_page page; |
| stream_set *processors = create_stream_set(); |
| int gotpage = 0; |
| ogg_int64_t written = 0; |
| |
| if(!file) { |
| oi_error(_("Error opening input file \"%s\": %s\n"), filename, |
| strerror(errno)); |
| return; |
| } |
| |
| printf(_("Processing file \"%s\"...\n\n"), filename); |
| |
| ogg_sync_init(&ogsync); |
| |
| while(get_next_page(file, &ogsync, &page, &written)) { |
| stream_processor *p = find_stream_processor(processors, &page); |
| gotpage = 1; |
| |
| if(!p) { |
| oi_error(_("Could not find a processor for stream, bailing\n")); |
| return; |
| } |
| |
| if(p->isillegal && !p->shownillegal) { |
| char *constraint; |
| switch(p->constraint_violated) { |
| case CONSTRAINT_PAGE_AFTER_EOS: |
| constraint = _("Page found for stream after EOS flag"); |
| break; |
| case CONSTRAINT_MUXING_VIOLATED: |
| constraint = _("Ogg muxing constraints violated, new " |
| "stream before EOS of all previous streams"); |
| break; |
| default: |
| constraint = _("Error unknown."); |
| } |
| |
| oi_warn(_("WARNING: illegally placed page(s) for logical stream %d\n" |
| "This indicates a corrupt Ogg file: %s.\n"), |
| p->num, constraint); |
| p->shownillegal = 1; |
| /* If it's a new stream, we want to continue processing this page |
| * anyway to suppress additional spurious errors |
| */ |
| if(!p->isnew) |
| continue; |
| } |
| |
| if(p->isnew) { |
| oi_info(_("New logical stream (#%d, serial: %08x): type %s\n"), |
| p->num, p->serial, p->type); |
| if(!p->start) |
| oi_warn(_("WARNING: stream start flag not set on stream %d\n"), |
| p->num); |
| } |
| else if(p->start) |
| oi_warn(_("WARNING: stream start flag found in mid-stream " |
| "on stream %d\n"), p->num); |
| |
| if(p->seqno++ != ogg_page_pageno(&page)) { |
| if(!p->lostseq) |
| oi_warn(_("WARNING: sequence number gap in stream %d. Got page " |
| "%ld when expecting page %ld. Indicates missing data.%s\n" |
| ), p->num, ogg_page_pageno(&page), p->seqno - 1, p->seqno-1==2?_(" (normal for live streams)"):""); |
| p->seqno = ogg_page_pageno(&page); |
| p->lostseq = 1; |
| } |
| else |
| p->lostseq = 0; |
| |
| if(!p->isillegal) { |
| p->process_page(p, &page); |
| |
| if(p->end) { |
| if(p->process_end) |
| p->process_end(p); |
| oi_info(_("Logical stream %d ended\n"), p->num); |
| p->isillegal = 1; |
| p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS; |
| } |
| } |
| } |
| |
| if(!gotpage) |
| oi_error(_("ERROR: No Ogg data found in file \"%s\".\n" |
| "Input probably not Ogg.\n"), filename); |
| |
| free_stream_set(processors); |
| |
| ogg_sync_clear(&ogsync); |
| |
| fclose(file); |
| } |
| |
| static void version (void) { |
| printf (_("opusinfo from %s %s\n"), PACKAGE_NAME, PACKAGE_VERSION); |
| } |
| |
| static void usage(void) { |
| version (); |
| printf (_(" by the Xiph.Org Foundation (http://www.xiph.org/)\n\n")); |
| printf(_("(c) 2003-2005 Michael Smith <msmith@xiph.org>\n" |
| "(c) 2012 Gregory Maxwell <greg@xiph.org>\n\n" |
| "Opusinfo is a fork of ogginfo from the vorbis-tools package\n" |
| "which has been cut down to only support opus files.\n\n" |
| "Usage: opusinfo [flags] file1.opus [file2.opus ... fileN.opus]\n" |
| "Flags supported:\n" |
| "\t-h Show this help message.\n" |
| "\t-q Make less verbose. Once will remove detailed informative\n" |
| "\t messages, twice will remove warnings.\n" |
| "\t-v Make more verbose. This may enable more detailed checks\n" |
| "\t for some stream types.\n")); |
| printf (_("\t-V Output version information and exit.\n")); |
| } |
| |
| int main(int argc, char **argv) |
| { |
| int f, ret; |
| |
| #ifdef WIN_UNICODE |
| int argc_utf8; |
| char **argv_utf8; |
| |
| (void)argc; |
| (void)argv; |
| |
| init_console_utf8(); |
| init_commandline_arguments_utf8(&argc_utf8, &argv_utf8); |
| #endif |
| |
| if(argc_utf8 < 2) { |
| fprintf(stdout, |
| _("Usage: opusinfo [flags] file1.opus [file2.opus ... fileN.opus]\n" |
| "\n" |
| "opusinfo is a tool for printing information about Opus files\n" |
| "and for diagnosing problems with them.\n" |
| "Full help shown with \"opusinfo -h\".\n")); |
| #ifdef WIN_UNICODE |
| uninit_console_utf8(); |
| #endif |
| exit(1); |
| } |
| |
| while((ret = getopt(argc_utf8, argv_utf8, "hqvV")) >= 0) { |
| switch(ret) { |
| case 'h': |
| usage(); |
| return 0; |
| case 'V': |
| version(); |
| return 0; |
| case 'v': |
| verbose++; |
| break; |
| case 'q': |
| verbose--; |
| break; |
| } |
| } |
| |
| if(verbose > 1) |
| printlots = 1; |
| if(verbose < 1) |
| printinfo = 0; |
| if(verbose < 0) |
| printwarn = 0; |
| |
| if(optind >= argc_utf8) { |
| fprintf(stderr, |
| _("No input files specified. \"opusinfo -h\" for help\n")); |
| return 1; |
| } |
| |
| ret = 0; |
| |
| for(f=optind; f < argc_utf8; f++) { |
| flawed = 0; |
| process_file(argv_utf8[f]); |
| if(flawed != 0) |
| ret = flawed; |
| } |
| |
| #ifdef WIN_UNICODE |
| free_commandline_arguments_utf8(&argc_utf8, &argv_utf8); |
| uninit_console_utf8(); |
| #endif |
| |
| return ret; |
| } |