| /* Copyright (C)2012 Gregory Maxwell |
| File: info_opus.c |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions |
| are met: |
| |
| - Redistributions of source code must retain the above copyright |
| notice, this list of conditions and the following disclaimer. |
| |
| - Redistributions in binary form must reproduce the above copyright |
| notice, this list of conditions and the following disclaimer in the |
| documentation and/or other materials provided with the distribution. |
| |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
| CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <ogg/ogg.h> |
| |
| #ifndef OPUSTOOLS |
| # include "ogginfo2.h" |
| #else |
| # include "opusinfo.h" |
| #endif |
| #include "opus_header.h" |
| #include "info_opus.h" |
| |
| /* From libopus, src/opus_decode.c */ |
| static int packet_get_samples_per_frame(const unsigned char *data, ogg_int32_t Fs) |
| { |
| int audiosize; |
| if (data[0]&0x80) |
| { |
| audiosize = ((data[0]>>3)&0x3); |
| audiosize = (Fs<<audiosize)/400; |
| } else if ((data[0]&0x60) == 0x60) |
| { |
| audiosize = (data[0]&0x08) ? Fs/50 : Fs/100; |
| } else { |
| audiosize = ((data[0]>>3)&0x3); |
| if (audiosize == 3) |
| audiosize = Fs*60/1000; |
| else |
| audiosize = (Fs<<audiosize)/100; |
| } |
| return audiosize; |
| } |
| |
| /* From libopus, src/opus_decode.c */ |
| static int packet_get_nb_frames(const unsigned char packet[], ogg_int32_t len) |
| { |
| int count; |
| if (len<1) |
| return -1; |
| count = packet[0]&0x3; |
| if (count==0) |
| return 1; |
| else if (count!=3) |
| return 2; |
| else if (len<2) |
| return -4; |
| else |
| return packet[1]&0x3F; |
| } |
| |
| #define readle32(buf, base) (((buf[base+3]<<24)&0xff000000)| \ |
| ((buf[base+2]<<16)&0xff0000)| \ |
| ((buf[base+1]<<8)&0xff00)| \ |
| (buf[base]&0xff)) |
| |
| void info_opus_process(stream_processor *stream, ogg_page *page ) |
| { |
| ogg_packet packet; |
| ogg_int64_t page_samples=0; |
| misc_opus_info *inf = stream->data; |
| int header=0, packets=0; |
| int res; |
| |
| ogg_stream_pagein(&stream->os, page); |
| if(inf->doneheaders < 2) |
| header = 1; |
| inf->last_eos = ogg_page_eos(page); |
| |
| while(1) { |
| ogg_int32_t spp; |
| res = ogg_stream_packetout(&stream->os, &packet); |
| if(res < 0) { |
| oi_warn(_("WARNING: discontinuity in stream (%d)\n"), stream->num); |
| continue; |
| } |
| else if (res == 0) |
| break; |
| |
| packets++; |
| if(inf->doneheaders < 2) { |
| if(inf->doneheaders==0 && opus_header_parse(packet.packet,packet.bytes,&inf->oh)!=1) { |
| oi_warn(_("WARNING: Could not decode Opus header " |
| "packet %d - invalid Opus stream (%d)\n"), |
| inf->doneheaders, stream->num); |
| continue; |
| } else if (inf->doneheaders==0){ |
| if(inf->oh.preskip<120)oi_warn(_("WARNING: Implausibly low preskip in Opus stream (%d)\n"),stream->num); |
| } |
| if(inf->doneheaders==1 && (packet.bytes<8 || memcmp(packet.packet, "OpusTags",8)!=0)) { |
| oi_warn(_("WARNING: Could not decode OpusTags header " |
| "packet %d - invalid Opus stream (%d)\n"), |
| inf->doneheaders, stream->num); |
| continue; |
| } else if (inf->doneheaders==1) { |
| char *tmp; |
| char *c=(char *)packet.packet; |
| int length, len, i, nb_fields; |
| |
| length=packet.bytes; |
| if (length<(8+4+4)) { |
| oi_warn(_("Invalid/corrupted comments in stream %d\n"),stream->num); |
| continue; |
| } |
| c += 8; |
| len=readle32(c, 0); |
| c+=4; |
| if (len < 0 || len>(length-16)) { |
| oi_warn(_("Invalid/corrupted comments in stream %d\n"),stream->num); |
| continue; |
| } |
| tmp=calloc(len+1,1); |
| memcpy(tmp,c,len); |
| oi_info(_("Encoded with %s\n"),tmp); |
| free(tmp); |
| c+=len; |
| /*The -16 check above makes sure we can read this.*/ |
| nb_fields=readle32(c, 0); |
| c+=4; |
| length-=16+len; |
| if (nb_fields < 0 || nb_fields>(length>>2)) { |
| oi_warn(_("Invalid/corrupted comments in stream %d\n"),stream->num); |
| continue; |
| } |
| if(nb_fields)oi_info(_("User comments section follows...\n")); |
| for (i=0;i<nb_fields;i++) { |
| char *comment; |
| if (length<4) { |
| oi_warn(_("Invalid/corrupted comments in stream %d\n"),stream->num); |
| break; |
| } |
| len=readle32(c, 0); |
| c+=4; |
| length-=4; |
| if (len < 0 || len>length) { |
| oi_warn(_("Invalid/corrupted comments in stream %d\n"),stream->num); |
| break; |
| } |
| /*check_xiph_comment expects a null terminated comment*/ |
| comment=malloc((len+1)*sizeof(char)); |
| memcpy(comment,c,len); |
| comment[len]=0; |
| check_xiph_comment(stream, i, comment, len); |
| free(comment); |
| c+=len; |
| length-=len; |
| } |
| } |
| |
| inf->doneheaders++; |
| continue; |
| } |
| if(packet.bytes>=2 && memcmp(packet.packet, "Op",2)==0) { |
| oi_warn(_("WARNING: Invalid packet or misplaced header in stream %d\n"),stream->num); |
| continue; |
| } |
| if(packet.bytes<1) { |
| oi_warn(_("WARNING: Invalid zero byte packet in stream %d\n"),stream->num); |
| continue; |
| } |
| spp = packet_get_nb_frames(packet.packet,packet.bytes); |
| if(spp<1 || spp>48) { |
| oi_warn(_("WARNING: Invalid packet TOC in stream %d\n"),stream->num); |
| continue; |
| } |
| spp *= packet_get_samples_per_frame(packet.packet,48000); |
| if(spp<120 || spp>5760 || (spp%120)!=0) { |
| oi_warn(_("WARNING: Invalid packet TOC in stream %d\n"),stream->num); |
| continue; |
| } |
| inf->total_samples += spp; |
| page_samples += spp; |
| inf->total_packets++; |
| inf->last_packet_duration = spp; |
| if(inf->max_packet_duration<spp)inf->max_packet_duration=spp; |
| if(inf->min_packet_duration>spp)inf->min_packet_duration=spp; |
| if(inf->max_packet_bytes<packet.bytes)inf->max_packet_bytes=packet.bytes; |
| if(inf->min_packet_bytes>packet.bytes)inf->min_packet_bytes=packet.bytes; |
| } |
| |
| if(!header) { |
| ogg_int64_t gp = ogg_page_granulepos(page); |
| if(gp > 0) { |
| if(gp < inf->lastgranulepos) |
| oi_warn(_("WARNING: granulepos in stream %d decreases from %" |
| I64FORMAT " to %" I64FORMAT "\n" ), |
| stream->num, inf->lastgranulepos, gp); |
| if(inf->lastgranulepos==0 && inf->firstgranule==-1) { |
| /*First timed page, now we can recover the start time.*/ |
| inf->firstgranule = gp-inf->total_samples; |
| if(inf->firstgranule<0) { |
| /*There shouldn't be any negative samples after counting the samples in the page backwards |
| from the first GP, but if this is the last page of the stream there may need to be to trim.*/ |
| if(!ogg_page_eos(page))oi_warn(_("WARNING: Samples with negative granpos in stream %d\n"),stream->num); |
| else inf->firstgranule=0; |
| } |
| } |
| if(inf->total_samples<gp-inf->firstgranule)oi_warn(_("WARNING: Sample count behind granule (%" I64FORMAT "<%" I64FORMAT ") in stream %d\n"), |
| (long long)inf->total_samples,(long long)(gp-inf->firstgranule),stream->num); |
| if(!ogg_page_eos(page) && (inf->total_samples>gp-inf->firstgranule)) |
| oi_warn(_("WARNING: Sample count ahead of granule (%" I64FORMAT ">%" I64FORMAT ") in stream %d\n"), |
| (long long)inf->total_samples,(long long)(gp-inf->firstgranule),stream->num); |
| inf->lastlastgranulepos = inf->lastgranulepos; |
| inf->lastgranulepos = gp; |
| if(!packets) |
| oi_warn(_("WARNING: Page with positive granpos (%" I64FORMAT ") on a page with no completed packets in stream %d\n"),gp,stream->num); |
| } |
| else if(packets) { |
| /* Only do this if we saw at least one packet ending on this page. |
| * It's legal (though very unusual) to have no packets in a page at |
| * all - this is occasionally used to have an empty EOS page */ |
| oi_warn(_("Negative or zero granulepos (%" I64FORMAT ") on Opus stream outside of headers. This file was created by a buggy encoder\n"), gp); |
| } |
| inf->overhead_bytes += page->header_len; |
| if(page_samples)inf->last_page_duration = page_samples; |
| if(inf->max_page_duration<page_samples)inf->max_page_duration=page_samples; |
| if(inf->min_page_duration>page_samples)inf->min_page_duration=page_samples; |
| inf->total_pages++; |
| } else { |
| /* Headers and metadata are pure overhead. */ |
| inf->overhead_bytes += page->header_len + page->body_len; |
| } |
| inf->bytes += page->header_len + page->body_len; |
| } |
| |
| void info_opus_end(stream_processor *stream) |
| { |
| misc_opus_info *inf = stream->data; |
| |
| oi_info(_("Opus stream %d:\n"),stream->num); |
| |
| if(inf && inf->total_packets>0){ |
| int i; |
| long minutes, seconds, milliseconds; |
| double time; |
| time = (inf->lastgranulepos-inf->firstgranule-inf->oh.preskip) / 48000.; |
| if(time<=0)time=0; |
| minutes = (long)(time) / 60; |
| seconds = (long)(time - minutes*60); |
| milliseconds = (long)((time - minutes*60 - seconds)*1000); |
| if(inf->lastgranulepos-inf->firstgranule<inf->oh.preskip) |
| oi_error(_("\tERROR: stream %d has a negative duration: %" I64FORMAT "-%" I64FORMAT "-%d=%" I64FORMAT "\n"),stream->num, |
| inf->lastgranulepos,inf->firstgranule,inf->oh.preskip,inf->lastgranulepos-inf->firstgranule-inf->oh.preskip); |
| if((inf->total_samples-inf->last_page_duration)>(inf->lastgranulepos-inf->firstgranule)) |
| oi_error(_("\tERROR: stream %d has interior holes or more than one page of end trimming\n"),stream->num); |
| if(inf->last_eos &&( (inf->last_page_duration-inf->last_packet_duration)>(inf->lastgranulepos-inf->lastlastgranulepos))) |
| oi_warn(_("\tWARNING: stream %d has more than one packet of end trimming\n"),stream->num); |
| if(inf->max_page_duration>=240000) |
| oi_warn(_("\tWARNING: stream %d has high muxing delay\n"),stream->num); |
| oi_info(_("\tPre-skip: %d\n"),inf->oh.preskip); |
| oi_info(_("\tPlayback gain: %g dB\n"),inf->oh.gain/256.); |
| oi_info(_("\tChannels: %d\n"),inf->oh.channels); |
| if(inf->oh.input_sample_rate)oi_info(_("\tOriginal sample rate: %dHz\n"),inf->oh.input_sample_rate); |
| if(inf->oh.nb_streams>1)oi_info(_("\tStreams: %d, Coupled: %d\n"),inf->oh.nb_streams,inf->oh.nb_coupled); |
| if(inf->oh.channel_mapping>0) { |
| oi_info(_("\tChannel Mapping family: %d Map:"),inf->oh.channel_mapping); |
| for(i=0;i<inf->oh.channels;i++)oi_info("%s%d%s",i==0?" [":", ",inf->oh.stream_map[i],i==inf->oh.channels-1?"]\n":""); |
| } |
| if(inf->total_packets)oi_info(_("\tPacket duration: %6.1fms (max), %6.1fms (avg), %6.1fms (min)\n"), |
| inf->max_packet_duration/48.,inf->total_samples/(double)inf->total_packets/48.,inf->min_packet_duration/48.); |
| if(inf->total_pages)oi_info(_("\tPage duration: %8.1fms (max), %6.1fms (avg), %6.1fms (min)\n"), |
| inf->max_page_duration/48.,inf->total_samples/(double)inf->total_pages/48.,inf->min_page_duration/48.); |
| oi_info(_("\tTotal data length: %" I64FORMAT " bytes (overhead: %0.3g%%)\n"),inf->bytes,(double)inf->overhead_bytes/inf->bytes*100.); |
| oi_info(_("\tPlayback length: %ldm:%02ld.%03lds\n"), minutes, seconds, milliseconds); |
| oi_info(_("\tAverage bitrate: %0.4g kb/s, w/o overhead: %.04g kb/s%s\n"),time<=0?0:inf->bytes*8/time/1000.0, |
| time<=0?0:(inf->bytes-inf->overhead_bytes)*8/time/1000.0, |
| (inf->min_packet_duration==inf->max_packet_duration)&&(inf->min_packet_bytes==inf->max_packet_bytes)?" (hard-CBR)":""); |
| } else { |
| oi_warn(_("\tWARNING: stream %d is empty\n"),stream->num); |
| } |
| free(stream->data); |
| } |
| |
| void info_opus_start(stream_processor *stream) |
| { |
| misc_opus_info *oinfo; |
| |
| stream->type = "opus"; |
| stream->process_page = info_opus_process; |
| stream->process_end = info_opus_end; |
| |
| stream->data = calloc(1, sizeof(misc_opus_info)); |
| |
| oinfo = stream->data; |
| oinfo->firstgranule=-1; |
| oinfo->min_packet_duration=5760; |
| oinfo->min_page_duration=5760*255; |
| oinfo->min_packet_bytes=2147483647; |
| } |