#!/usr/bin/perl

##################################################
#                                                #
#    MP3 Stream Analyzer / Recorder              #
#                                                #
#    (C) 2019 Chaosje, Domero                    #
#    ALL RIGHTS RESERVED                         #
#                                                #
##################################################

package gmp3;

use strict;
no strict 'refs';
use warnings;
use Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

$VERSION     = '1.1.1';
@ISA         = qw(Exporter);
@EXPORT      = qw(searchsync isid3 id3 checkmp3frame);
@EXPORT_OK   = qw(crc16);

use Gzip::Faster;

# MP3 is stupid ! Such guessing for geeksplay.

1;

sub framebegin {
  my ($cc) = @_;
  return (($cc == 73) || ($cc == 84) || ($cc == 255))
}

sub searchsync {
  my ($data) = @_;
  my $len=length($data);
  my $i=0; my $fsz={};
  while ($i<$len) {
    my $c=ord(substr($data,$i,1));
    if ($c == 255) {
      my $frame=checkmp3frame(substr($data,$i));
      if ($frame) {
        my $size=$frame->{framelen};
        if ($fsz->{$size}) {
          return { frame => $frame, start => $fsz->{$size}, size => $size }
        }
        $fsz->{$size}=$i
      }
    }
    $i++
  }
  return 0
}

sub isid3 {
  # output: 0 if not id3, size of block if id3
  my ($data) = @_;
  if ((substr($data,0,3) eq 'TAG') && ((length($data) <= 128) || framebegin(ord(substr($data,128,1))))) { return 128 } # Assume ID3v1
  if (length($data) < 10) { return 0 }
  if (substr($data,0,3) ne 'ID3') { return 0 }  
  # version = byte 3 & 4, not worthy checking because of too much ambiguity.
  my $flags = substr($data,5,1);
  if (ord($flags) & 31 != 0) { return 0 } # if one of these = 1, should be a special mp3 for specific personal protocols, won't happen.
  my $size = 0;
  for my $i (6..9) {
    my $d = ord(substr($data,$i,1));
    if ($d & 128) { return 0 } # build in anti false-sync, bit 7 must be 0
    $size = ($size<<7) + $d
  }
  return $size + 10
}

sub checkmp3frame {
  # output: 0 if not a valid frame, information about block if SUPPOSED to be a valid block, this is NOT a guarantee, check next sync-frame!
  # IF any suggestion that is is NOT a synced frame, return NO! .. Normal playing MP3's are very secure ;)
  my ($data) = @_;
  # check sync marker
  if (ord(substr($data,0,1)) != 255) { return 0 }
  my $flag=ord(substr($data,1,1));
  if ($flag >> 5 != 7) { return 0 }
  # calculate supposed size
  my $self={}; bless $self;
  $self->{version} = $flag >> 3 & 3;
  if ($self->{version} == 1) { return 0 } # reserved = ?? probably data
  $self->{mpeg}=('MPEG Version 2.5','Reserved','MPEG Version 2 (ISO/IEC 13818-3)','MPEG Version 1 (ISO/IEC 11172-3)')[$self->{version}];
  $self->{layer} = $flag >> 1 & 3;
  if ($self->{layer} == 0) { return 0 } # not defined, probably not a synced frame, or special purpose frame, probably just data.
  $self->{layertype}=('III','II','I')[$self->{layer}-1];
  $self->{crcpresent} = $flag & 1;
  $flag=ord(substr($data,2,1));
  $self->{bitrateindex} = $flag >> 4;
  $self->{samplerateindex} = $flag >> 2 & 3;
  if ($self->{samplerateindex} == 3) { return 0 } # reserved = ?? probably data
  $self->{padding} = $flag >> 1 & 1;
  $self->{private} = $flag & 1;
  $flag=ord(substr($data,3,1));
  $self->{chanmode} = $flag >> 6; # 0=stereo, 1=joint stereo, 2=dual mono, 3=mono
  $self->{modeext} = $flag >> 4 & 3;
  $self->{copyright} = $flag >> 3 & 1;
  $self->{original} = $flag >> 2 & 1;
  $self->{emphasis} = $flag & 3;
  my $start=4;
  if ($self->{crcpresent}) { $self->{crc} = (ord(substr($data,4,1)) << 8) + ord(substr($data,5,1)); $start=6 }
  my @bri=(0);
  if ($self->{version} == 3) {
    # layer 1 - ISO/IEC 11172-3
    if ($self->{layer} == 3) {
      # layer I
      push @bri,(32,64,96,128,160,192,224,256,288,320,352,384,416,448,0)
    } elsif ($self->{layer} == 2) {
      # layer II
      push @bri,(32,48,56,64,80,96,112,128,160,192,224,256,320,384,0)
    } elsif ($self->{layer} == 1) {
      # layer III
      push @bri,(32,40,48,56,64,80,96,112,128,160,192,224,256,320,0)
    }
  } else {
    if ($self->{layer} == 3) {
      # layer I
      push @bri,(32,48,56,64,80,96,112,128,144,160,176,192,224,256,0)
    } else {
      # layer II & III
      push @bri,(8,16,24,32,40,48,56,64,80,96,112,128,144,160,0)
    }
  }
  $self->{bitrate} = $bri[$self->{bitrateindex}]*1000;
  if (!$self->{bitrate}) { return 0 } # no bitrate? must be data, not a synced frame, or a internally used mp3
  # we now ASSUME this is a valid frame! chance is around 1:48000
  my $vnr = 0;
  if ($self->{version} == 2) { $vnr=1 }
  if ($self->{version} == 0) { $vnr=2 }
  $self->{samplerate}=(44100,48000,32000)[$self->{samplerateindex}];
  $self->{samplerate}>>=$vnr;
  if ($self->{layer} == 3) {
    $self->{framelen} = (12 * $self->{bitrate} / $self->{samplerate} + $self->{padding}) << 2
  } else {
    $self->{framelen} = int (144 * $self->{bitrate} / $self->{samplerate}) + $self->{padding}
  }
  if (length($data) < $self->{framelen} + $start) { return 0 } # probably way too large framelen -> probably datablock
  # some calculate CRC wrong, skip this check!
  if (0 && $self->{crcpresent}) {
    # to be fixed
    my $crc=crc16(substr($data,$start,$self->{framelen}));
    if ($self->{crc} != $crc) {
    }
  }
  $self->{datastart}=$start;
  $self->{framelen}+=$start;
  return $self
}

sub crc16 {
  # Mp3 uses poly 0x8005
  my ($string) = @_;
  my $crc = 0;
  for my $c ( unpack 'C*', $string ) {
    $crc ^= $c;
    for ( 0 .. 7 ) {
      my $carry = $crc & 1;
      $crc >>= 1;
      $crc ^= 0x8005 if $carry
    }
  }
  return $crc
}

sub id3 {
  my ($data) = @_;
  my $self={}; bless $self;
  $self->{framelen}=isid3($data);
  $self->{version}=chr(ord(substr($data,3,1)) + 48) .'.'. chr(ord(substr($data,4,1)) + 48);
  my $flags=ord(substr($data,5,1));
  $self->{unsync}=($flags>>7) & 1;
  $self->{extheader}=($flags>>6) & 1;
  $self->{expirimental}=($flags>>5) & 1;
  my $pos=10;
  if ($self->{extheader}) {
    my $ehlen=unpack('N',substr($data,10,4));
    $pos+=$ehlen+4;
    $self->{extflags}=unpack('n',substr($data,14,2));
    if ($self->{extflags}>>15) {
      $self->{crc}=unpack('N',substr($data,16,4))
    }
  }
  $self->{frames}=[]; $self->{info}={};
  # we do NOT ignore discarded frames, information is king, also we are not supposed to do stupid things, like allowing unallowable stuff.
  while ($pos<$self->{framelen}) {
    my $frame={ tag => substr($data,$pos,4), size => unpack('N',substr($data,$pos+4,4)), flags => unpack('n',substr($data,$pos+8,2)) };
    if (!$frame->{size}) { last }
    $pos+=10;
    $frame->{discard}=(($frame->{flags}>>14) > 0);
    $frame->{readonly}=($frame->{flags}>>13) & 1;
    my $dat=substr($data,$pos,$frame->{size});
    if ($frame->{flags} & 128) {
      # compressed
      $dat=gunzip($dat)
    }
    if ($frame->{flags} & 64) {
      # encrypted
      $frame->{encmethod}=ord(substr($data,$pos,1)); $pos++
    }
    if ($frame->{flags} & 32) {
      # grouped
      $frame->{group}=ord(substr($data,$pos,1)); $pos++
    }
    #$dat =~ s/\x00\x00\x00/ /g;
    #$dat =~ s/[\x00-\x19]//g;
    #$dat =~ s/[\x80-\xFF]//g;
    $frame->{data}=$dat;
    $pos+=$frame->{size};
#    print $frame->{tag}.' '.$frame->{data}."\n";
    push @{$self->{frames}},$frame
  }
  $self->id3convert();
  return $self
}

sub id3convert {
  my ($self) = @_;
  $self->{info}={};
  my $cnv = {
    'TIT2' => 'title',
    'TPE1' => 'artist',
    'TYER' => 'year',
    'TALB' => 'album',
    'TCON' => 'style',
    'TRCK' => 'track',
    'TBPM' => 'bpm',
    'TKEY' => 'key',
    'TSSE' => 'encoding',
    'APIC' => 'picture',
    'COMM' => 'comments'
  };
  for my $frame (@{$self->{frames}}) {
    if ($cnv->{$frame->{tag}}) {
      if ($frame->{tag} eq 'APIC') {
        my $i={ encoding => ord(substr($frame->{data},0,1)) };
        my $g=1; my $mime=""; my $fnd=0;
        while (!$fnd && ($g < length($frame->{data}))) {
          my $t=substr($frame->{data},$g,1);
          if (!ord($t)) { $fnd=1 }
          else { $mime.=$t }
          $g++
        }
        $i->{mime}=$mime;
        $i->{type}=ord(substr($frame->{data},$g,1));
        my $desc=""; $fnd=0;
        while (!$fnd && ($g < length($frame->{data}))) {
          my $t=substr($frame->{data},$g,1);
          if (!ord($t)) { $fnd=1 }
          else { $desc.=$t }
          $g++
        }
        $i->{desc}=$desc;
        $i->{image}=substr($frame->{data},$g+1);
        print "[IMG]\n$i->{image}\n";
        $self->{info}{$cnv->{$frame->{tag}}}=$i
      } else {
        $self->{info}{$cnv->{$frame->{tag}}}=$frame->{data}
      }
    }
  }
}

# EOF gmp3.pm (C) 2019 Chaosje, Domero