shithub: opus-tools

ref: 7b92abb9f17b4ec813344cad2aa931d17926c7b2
dir: /src/opusinfo.c/

View raw version
/* 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"
#include "tagcompare.h"

#if defined WIN32 || defined _WIN32
# 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) \
    (((ogg_uint32_t)(buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|(buf)[3])

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
           && tagcompare(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 media 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 media 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
               && tagcompare((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): "
                       "media 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
               && tagcompare((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): "
                       "media 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
               && tagcompare((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): "
                       "media 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 &&
               tagcompare((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 media 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",
               (int)(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;
    size_t 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 %" PRId64 " 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, (long)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 (https://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;
}