//**************************************************************************
//*                     This file is part of the                           *
//*                      Mpxplay - audio player.                           *
//*                  The source code of Mpxplay is                         *
//*        (C) copyright 1998-2011 by PDSoft (Attila Padar)                *
//*                http://mpxplay.sourceforge.net                          *
//*                  email: mpxplay@freemail.hu                            *
//**************************************************************************
//*  This program is distributed in the hope that it will be useful,       *
//*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
//*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                  *
//*  Please contact with the author (with me) if you want to use           *
//*  or modify this source.                                                *
//**************************************************************************
//function: ID3v1,ID3v2,APETag,VorbisComment read/write

#include "mpxplay.h"
#include "tagging.h"
#include <string.h>
#include <malloc.h>

//--------------------------------------------------------------------------
//ID3v1

static char *id3v1genres[]={
  "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
  "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
  "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
  "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
  "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
  "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise",
  "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
  "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave",
  "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream",
  "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap",
  "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave",
  "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
  "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll",
  "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing", "Fast-Fusion",
  "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
  "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock",
  "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
  "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony",
  "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club",
  "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul",
  "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House",
  "Dance Hall", "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
  "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat", "Christian Gangsta",
  "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
  "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
  "SynthPop"
};

#define MAX_ID3GENRENUM (sizeof(id3v1genres)/sizeof(char *))

static unsigned int id3v10_partlens[5]={30,30,30,4,30};

unsigned int mpxplay_tagging_id3v1_check(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 char tag[4];
 if(fbfs->fseek(fbds,-128,SEEK_END)<0)
  return 0;
 if(fbfs->fread(fbds,tag,3)!=3)
  return 0;
 if(tag[0]=='T' && tag[1]=='A' && tag[2]=='G')
  return 1;
 return 0;
}

//always call check_id3tag_v1 before this
int mpxplay_tagging_id3v1_get(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis)
{
 unsigned int i,partlen;
 int datalen;
 char *readp,readtmp[128],stmp[128];

 readp=&readtmp[0];
 if(fbfs->fread(fbds,readp,128-3)!=(128-3))
  return MPXPLAY_ERROR_FILEHAND_CANTSEEK;

 for(i=0;i<5;i++){
  partlen=id3v10_partlens[i];
  pds_strncpy(stmp,readp,partlen);
  if(i==I3I_COMMENT){
   if(stmp[partlen-1]<32) // tracknumber check
    stmp[partlen-1]=0;
  }
  stmp[partlen]=0;
  datalen=partlen;
  miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,MPXPLAY_TEXTCONV_TYPE_CHAR,i),stmp,&datalen);
  readp+=partlen;
 }
 //tracknumber from id3v1.1
 i=(unsigned int)readp[-1];
 if(i && !readp[-2]){
  datalen=sprintf(stmp,"%d",i);
  miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,MPXPLAY_TEXTCONV_TYPE_CHAR,I3I_TRACKNUM),stmp,&datalen);
 }
 //genre
 i=(unsigned int)readp[0];
 if(i<MAX_ID3GENRENUM){
  datalen=-1;
  miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,MPXPLAY_TEXTCONV_TYPE_CHAR,I3I_GENRE),id3v1genres[i],&datalen);
 }
 return MPXPLAY_ERROR_OK;
}

char *mpxplay_tagging_id3v1_index_to_genre(unsigned int i)
{
 if(i<MAX_ID3GENRENUM)
  return id3v1genres[i];
 return NULL;
}

unsigned int mpxplay_tagging_id3v1_genre_to_index(char *genrename)
{
 unsigned int i;

 if(genrename)
  for(i=0;i<MAX_ID3GENRENUM;i++)
   if(pds_stricmp(id3v1genres[i],genrename)==0)
    return i;

 return 255;
}

int mpxplay_tagging_id3v1_put(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis)
{
 int len,error=MPXPLAY_ERROR_INFILE_WRITETAG_UNKNOWN;
 long i,fileoffset;
 char *cvdata[I3I_MAX+1],strtemp[192];

 if(mpxplay_tagging_id3v1_check(fbfs,fbds)) // is file tagged already?
  fileoffset=fbfs->fseek(fbds,-128,SEEK_END);
 else
  fileoffset=fbfs->fseek(fbds,   0,SEEK_END);
 if(fileoffset>0){                // successfull file positioning
  pds_memset(cvdata,0,sizeof(cvdata));
  for(i=0;i<=I3I_MAX;i++){
   len=0;
   miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,MPXPLAY_TEXTCONV_TYPE_CHAR,i),&cvdata[i],&len);
  }
  if(cvdata[I3I_TRACKNUM]){       // ID3v1.1
   unsigned int tracknum=pds_atol(cvdata[I3I_TRACKNUM]);
   if(tracknum>255)
    tracknum=255;
   sprintf(strtemp,"TAG%-30.30s%-30.30s%-30.30s%-4.4s%-28.28s%c%c%c",
                   (cvdata[I3I_TITLE])?  cvdata[I3I_TITLE]:"",
                   (cvdata[I3I_ARTIST])? cvdata[I3I_ARTIST]:"",
                   (cvdata[I3I_ALBUM])?  cvdata[I3I_ALBUM]:"",
                   (cvdata[I3I_YEAR])?   cvdata[I3I_YEAR]:"",
                   (cvdata[I3I_COMMENT])?cvdata[I3I_COMMENT]:"",0,
                   tracknum,
                   mpxplay_tagging_id3v1_genre_to_index(cvdata[I3I_GENRE]));
  }else{                                                   // ID3v1.0
   sprintf(strtemp,"TAG%-30.30s%-30.30s%-30.30s%-4.4s%-30.30s%c",
                   (cvdata[I3I_TITLE])?  cvdata[I3I_TITLE]:"",
                   (cvdata[I3I_ARTIST])? cvdata[I3I_ARTIST]:"",
                   (cvdata[I3I_ALBUM])?  cvdata[I3I_ALBUM]:"",
                   (cvdata[I3I_YEAR])?   cvdata[I3I_YEAR]:"",
                   (cvdata[I3I_COMMENT])?cvdata[I3I_COMMENT]:"",
                   mpxplay_tagging_id3v1_genre_to_index(cvdata[I3I_GENRE]));
  }
  if(fbfs->fwrite(fbds,strtemp,128)==128) // successfull TAG writing
   error=MPXPLAY_ERROR_INFILE_OK;
  else
   error=MPXPLAY_ERROR_FILEHAND_CANTWRITE;
  for(i=0;i<=I3I_MAX;i++)
   if(cvdata[i])
    pds_free(cvdata[i]);
 }else
  error=MPXPLAY_ERROR_FILEHAND_CANTSEEK;
 return error;
}

//---------------------------------------------------------------------
//ID3v2

#define ID3V2_HEADSIZE 10
#define ID3V2_FOOTERSIZE 10
#define ID3V2_FRAMEHEADSIZE 10
#define ID3V2_MAX_DATALEN 1048576
#define ID3V2_MAX_ID3LEN  256

#define ID3V2_TEXTENCTYPE_NONE    0
#define ID3V2_TEXTENCTYPE_UTF16LE 1
#define ID3V2_TEXTENCTYPE_UTF16BE 2
#define ID3V2_TEXTENCTYPE_UTF8    3

#define ID3V2_FLAG_UNSYNCHRONISATION 0x80
#define ID3V2_FLAG_EXTENDED_HEADER   0x40
#define ID3V2_FLAG_EXPERIMENTAL      0x20
#define ID3V2_FLAG_FOOTER_PRESENT    0x10

#define ID3V2_FRAMENUM_ALLOC 32

#define ID3V2_CONTROL_READCHK  0
#define ID3V2_CONTROL_READ     1
#define ID3V2_CONTROL_WRITECHK 2
#define ID3V2_CONTROL_WRITE    3

struct id3v2x_one_frame_data_s{
 char frameid[8]; // same like in one_frame_handler
 char text_enc_type;
 char language_type[3];
 unsigned int flags;
 void *framebuf;  // if we want to write it back without modification
 unsigned long total_framelen; // incl. header
};

struct id3v2x_main_data_s{
 struct mpxplay_infile_info_s *miis;
 unsigned int control;
 char *databuftmp;
 int  datalen;
 char global_textenc_type;

 unsigned int version;
 unsigned int flags;
 unsigned long totalsize;
 unsigned long oldtotalsize;
 unsigned long filepos;

 struct id3v2x_one_version_info_s *version_handler;

 unsigned int nb_frames;
 struct id3v2x_one_frame_data_s *frame_datas;
 unsigned int nb_allocated_frames;

 char footer[ID3V2_FOOTERSIZE];
};

struct id3v2x_one_frame_handler_s{
 char *frameid;
 unsigned int i3i_index;
 long (*frame_reader)(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen,unsigned int i3index);
 long (*frame_writer)(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,char *id3ip);
};

struct id3v2x_one_version_info_s{
 unsigned char version_number;
 unsigned long (*frame_getsize)(char *bufp);
 void (*frame_putsize)(char *bufp,unsigned long framesize);
 struct id3v2x_one_frame_handler_s *supported_frames;
};

static unsigned int tagging_id3v2_framespace_alloc(struct id3v2x_main_data_s *imds)
{
 if(imds->nb_frames>=imds->nb_allocated_frames){
  unsigned int alloc_framenum=imds->nb_allocated_frames+ID3V2_FRAMENUM_ALLOC;
  struct id3v2x_one_frame_data_s *fds=(struct id3v2x_one_frame_data_s *)pds_calloc(alloc_framenum,sizeof(struct id3v2x_one_frame_data_s));
  if(!fds)
   return 0;
  if(imds->frame_datas){
   if(imds->nb_frames)
    pds_memcpy((void *)fds,(void *)imds->frame_datas,(imds->nb_frames*sizeof(struct id3v2x_one_frame_data_s)));
   pds_free(imds->frame_datas);
  }
  imds->frame_datas=fds;
  imds->nb_allocated_frames=alloc_framenum;
 }
 return 1;
}

static void tagging_id3v2_framespace_dealloc(struct id3v2x_main_data_s *imds)
{
 if(imds->frame_datas){
  struct id3v2x_one_frame_data_s *fds=imds->frame_datas;
  while(imds->nb_allocated_frames){
   if(fds->framebuf)
    pds_free(fds->framebuf);
   fds++;
   imds->nb_allocated_frames--;
  }
  pds_free(imds->frame_datas);
  imds->frame_datas=NULL;
  imds->nb_frames=0;
 }
 if(imds->databuftmp){
  pds_free(imds->databuftmp);
  imds->databuftmp=NULL;
 }
}

static struct id3v2x_one_frame_handler_s *tagging_id3v2_search_frameid(struct id3v2x_one_frame_handler_s *f,char *framehead)
{
 while(f->frameid){
  if(pds_strncmp(framehead,f->frameid,4)==0)
   return f;
  f++;
 }
 return NULL;
}

static struct id3v2x_one_frame_handler_s *tagging_id3v2_search_i3i(struct id3v2x_one_frame_handler_s *f,unsigned int i3i)
{
 while(f->frameid){
  if(f->i3i_index==i3i)
   return f;
  f++;
 }
 return NULL;
}

static struct id3v2x_one_frame_data_s *tagging_id3v2_search_framedata(struct id3v2x_main_data_s *imds,char *frameid)
{
 struct id3v2x_one_frame_data_s *fds=imds->frame_datas;
 unsigned int i=imds->nb_frames;
 if(!fds || !i)
  return NULL;
 do{
  if(PDS_GETB_LE32(&fds->frameid[0])==PDS_GETB_LE32(frameid))
   return fds;
  fds++;
 }while(--i);
 return NULL;
}

static void tagging_id3v2_globaltextenc_get(struct id3v2x_main_data_s *imds)
{
 struct id3v2x_one_frame_data_s *fds=imds->frame_datas;
 unsigned int i=imds->nb_frames;

#ifdef MPXPLAY_UTF8
 imds->global_textenc_type=ID3V2_TEXTENCTYPE_UTF8;
#endif

 if(!fds || !i)
  return;

 do{
  if(fds->text_enc_type && (fds->text_enc_type<=3)){
   imds->global_textenc_type=fds->text_enc_type;
   break;
  }
  fds++;
 }while(--i);
}

//-------------------------------------------------------------------------
//read side

static unsigned long mpx_id3v2x_get_framesize_4x7(char *bufp)
{
 unsigned long framesize=0;

 if(!(bufp[1]&0x80))
  framesize+=(bufp[0]&0x7f) << 21;
 if(!(bufp[2]&0x80))
  framesize+=(bufp[1]&0x7f) << 14;
 if(!(bufp[3]&0x80))
  framesize+=(bufp[2]&0x7f) <<  7;
 framesize+=(bufp[3]&0x7f)  <<  0;

 return framesize;
}

static unsigned long mpx_id3v2x_get_framesize_4x8(char *bufp)
{
 return PDS_GETB_BE32(bufp);
}

static long mpx_id3v2x_alloc_and_read_tagdata(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen,unsigned int text_enc_type)
{
 unsigned int src_type;
 if(imds->databuftmp)
  pds_free(imds->databuftmp);
 imds->databuftmp=pds_malloc(datalen*3+2);
 if(!imds->databuftmp)
  return -1;
 if(fbfs->fread(fbds,imds->databuftmp,datalen)!=datalen)
  return -1;
 PDS_PUTB_LE16(&imds->databuftmp[datalen],0);
 switch(text_enc_type){
  case ID3V2_TEXTENCTYPE_UTF16LE:src_type=MPXPLAY_TEXTCONV_TYPE_UTF16LE;break;
  case ID3V2_TEXTENCTYPE_UTF16BE:src_type=MPXPLAY_TEXTCONV_TYPE_UTF16BE;break;
  case ID3V2_TEXTENCTYPE_UTF8   :src_type=MPXPLAY_TEXTCONV_TYPE_UTF8;break;
  default:src_type=MPXPLAY_TEXTCONV_TYPE_CHAR;
 }
 datalen=mpxplay_playlist_textconv_by_texttypes(
   MPXPLAY_TEXTCONV_TYPES_PUT(src_type,MPXPLAY_TEXTCONV_TYPE_MPXPLAY),
   imds->databuftmp,datalen,imds->databuftmp,datalen*3);
 datalen=pds_strcutspc(imds->databuftmp);
 return datalen;
}

static long mpx_id3v2x_tagread_text(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen)
{
 if(datalen<2)
  return 0;
 if(fbfs->fread(fbds,&framedata->text_enc_type,1)!=1)
  return -1;
 datalen--;
 if(imds->control==ID3V2_CONTROL_READ)
  datalen=mpx_id3v2x_alloc_and_read_tagdata(imds,fbfs,fbds,datalen,framedata->text_enc_type);
 else if(fbfs->fseek(fbds,datalen,SEEK_CUR)<0)
  return -1;

 return datalen;
}

static long mpx_id3v2x_tagend_text(struct id3v2x_main_data_s *imds,void *fbds,int datalen,unsigned int i3index)
{
 if((datalen>0) && imds->databuftmp)
  imds->miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,MPXPLAY_TEXTCONV_TYPE_MPXPLAY,i3index),imds->databuftmp,&datalen);
 return datalen;
}

static long mpx_id3v2x_tagread_txxx(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen,unsigned int i3index)
{
 datalen=mpx_id3v2x_tagread_text(imds,framedata,fbfs,fbds,datalen);
 datalen=mpx_id3v2x_tagend_text(imds,fbds,datalen,i3index);
 return datalen;
}

static long mpx_id3v2x_tagread_tcon(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen,unsigned int i3index)
{
 datalen=mpx_id3v2x_tagread_text(imds,framedata,fbfs,fbds,datalen);
 if((datalen>0) && imds->databuftmp){
  char *id3p=imds->databuftmp;
  int gennum;
  if(id3p[0]=='(')
   id3p++;
  gennum=pds_atol(id3p);
  if((gennum || id3p[0]=='0') && (gennum<MAX_ID3GENRENUM)){
   int len=-1;
   imds->miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,MPXPLAY_TEXTCONV_TYPE_MPXPLAY,i3index),id3v1genres[gennum],&len);
  }else
   datalen=mpx_id3v2x_tagend_text(imds,fbds,datalen,i3index);
 }
 return datalen;
}

static long mpx_id3v2x_tagread_comm(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,long datalen,unsigned int i3index)
{
 unsigned int content_desc_len;

 if(datalen<=5){    // commentlen == 0
  if(fbfs->fseek(fbds,datalen,SEEK_CUR)<0) // skip frame
   return -1;
  return 0;
 }
 if(fbfs->fread(fbds,&framedata->text_enc_type,1)!=1) // text encoding type (1 byte)
  return -1;
 if(fbfs->fread(fbds,&framedata->language_type,3)!=3) // language           (3 bytes)
  return -1;
 datalen-=4;

 if(imds->control==ID3V2_CONTROL_READ){
  datalen=mpx_id3v2x_alloc_and_read_tagdata(imds,fbfs,fbds,datalen,framedata->text_enc_type);
  if((datalen>0) && imds->databuftmp){
   content_desc_len=pds_strlen(imds->databuftmp)+1;
   if(datalen<=content_desc_len)  // there's no comment data
    return 0;
   datalen-=content_desc_len;
   pds_memcpy(imds->databuftmp,(imds->databuftmp+content_desc_len),datalen); // remove content desc data from our datafield (we don't use it)
   datalen=mpx_id3v2x_tagend_text(imds,fbds,datalen,i3index);
  }
 }else{
  if(fbfs->fseek(fbds,datalen,SEEK_CUR)<0)
   return -1;
 }

 return datalen;
}

static int mpx_id3v2_read_frame(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 long datalen;
 struct id3v2x_one_frame_handler_s *frameselect;
 struct id3v2x_one_frame_data_s *framedata=NULL;
 char readtmp[16];

 if(fbfs->fread(fbds,&readtmp[0],ID3V2_FRAMEHEADSIZE)!=ID3V2_FRAMEHEADSIZE)
  return -1;

 imds->filepos+=ID3V2_FRAMEHEADSIZE;

 datalen=imds->version_handler->frame_getsize(&readtmp[4]);
 if(!datalen)
  return 0;

 imds->filepos+=datalen;
 if(imds->filepos>imds->totalsize)
  return -1;
 if(!tagging_id3v2_framespace_alloc(imds))
  goto err_out_seek;

 framedata=imds->frame_datas+(imds->nb_frames++);
 pds_memcpy((void *)&framedata->frameid[0],(void *)&readtmp[0],4);
 framedata->total_framelen=datalen+ID3V2_FRAMEHEADSIZE;
 framedata->flags=PDS_GETB_LE16(&readtmp[8]); // ???

 frameselect=tagging_id3v2_search_frameid(imds->version_handler->supported_frames,&readtmp[0]);

 if((frameselect==NULL) || (datalen>ID3V2_MAX_ID3LEN)){ // save frame data if we can't handle it
  if((imds->control==ID3V2_CONTROL_WRITECHK) || (imds->control==ID3V2_CONTROL_WRITE)){
   framedata->framebuf=pds_malloc(framedata->total_framelen);
   if(framedata->framebuf){
    pds_memcpy(framedata->framebuf,(void *)&readtmp[0],ID3V2_FRAMEHEADSIZE);
    if(fbfs->fread(fbds,((char *)framedata->framebuf)+ID3V2_FRAMEHEADSIZE,datalen)!=datalen){
     pds_free(framedata->framebuf);
     framedata->framebuf=NULL;
    }
   }
  }
  goto err_out_seek;
 }

 datalen=frameselect->frame_reader(imds,framedata,fbfs,fbds,datalen,frameselect->i3i_index);
 if(datalen!=0)
  return datalen;

err_out_seek:
 if(fbfs->fseek(fbds,imds->filepos,SEEK_SET)<0)
  return -1;
 return 0;
}

static unsigned long mpx_id3v2x_read_header(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 unsigned long tsize;
 char header[ID3V2_HEADSIZE];

 if(fbfs->fseek(fbds,0,SEEK_SET)<0)
  return 0;
 if(fbfs->fread(fbds,header,ID3V2_HEADSIZE)!=ID3V2_HEADSIZE)
  return 0;

 if(PDS_GETB_LE24(&header[0])!=PDS_GET4C_LE32('I','D','3',0))
  return 0;
 imds->version=(unsigned char)header[3];
 imds->flags  =(unsigned char)header[5];

 tsize=mpx_id3v2x_get_framesize_4x7(&header[6]);
 if(!tsize)
  return 0;
 tsize+=ID3V2_HEADSIZE;
 if(imds->flags&ID3V2_FLAG_FOOTER_PRESENT)
  tsize+=ID3V2_FOOTERSIZE;
 if(tsize>=fbfs->filelength(fbds))
  return 0;
 imds->totalsize=tsize;

 return ID3V2_HEADSIZE;
}

static unsigned long mpx_id3v2_read_extended_header(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 unsigned long ehsize;
 char readtmp[4];

 if(fbfs->fread(fbds,&readtmp[0],4)!=4)
  return 0;
 ehsize=imds->version_handler->frame_getsize(&readtmp[0]);
 if(ehsize<4 || (imds->totalsize+ehsize)>=fbfs->filelength(fbds))
  return 0;
 imds->filepos+=ehsize;
 if(fbfs->fseek(fbds,ehsize-4,SEEK_CUR)<0) // skip only, not read
  return 0;
 return 1;
}

//--------------------------------------------------------------------------
//write side

static void mpx_id3v2x_put_framesize_4x7(char *bufp,unsigned long framesize)
{
 bufp[3]=(framesize>> 0)&0x7f;
 bufp[2]=(framesize>> 7)&0x7f;
 bufp[1]=(framesize>>14)&0x7f;
 bufp[0]=(framesize>>21)&0x7f;
}

static void mpx_id3v2x_put_framesize_4x8(char *bufp,unsigned long framesize)
{
 PDS_PUTB_BEU32(bufp,framesize);
}

static int mpx_id3v2x_write_header(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 unsigned long totalsize;
 char header[ID3V2_HEADSIZE]="ID3";

 header[3]=imds->version;
 header[4]=0;
 header[5]=imds->flags;
 totalsize=imds->totalsize-ID3V2_HEADSIZE; // !!! because we add them
 if(imds->flags&ID3V2_FLAG_FOOTER_PRESENT) // to imds->totalsize in read_header
  totalsize-=ID3V2_FOOTERSIZE;             //

 mpx_id3v2x_put_framesize_4x7(&header[6],totalsize);

 if(fbfs->fseek(fbds,0,SEEK_SET)<0)
  return MPXPLAY_ERROR_FILEHAND_CANTSEEK;
 if(fbfs->fwrite(fbds,header,ID3V2_HEADSIZE)!=ID3V2_HEADSIZE)
  return MPXPLAY_ERROR_FILEHAND_CANTWRITE;

 imds->filepos=ID3V2_HEADSIZE; // !!!

 return MPXPLAY_ERROR_INFILE_OK;
}

static unsigned int mpx_id3v2x_textconv_char_to_utf(char *destbuf,unsigned int buflen,char *srcbuf,unsigned int text_enc_type)
{
 unsigned int datalen=0,dest_type;
 switch(text_enc_type){
  case ID3V2_TEXTENCTYPE_UTF16LE:
   destbuf[0]=0xff; // !!! we put utf header before every string/tag
   destbuf[1]=0xfe; //
   destbuf+=2;buflen-=2;datalen=2;
   dest_type=MPXPLAY_TEXTCONV_TYPE_UTF16LE;
   // !!! trailing zeroes are not written
   break;
  case ID3V2_TEXTENCTYPE_UTF16BE:
   destbuf[0]=0xfe; // !!!
   destbuf[1]=0xff; //
   destbuf+=2;buflen-=2;datalen=2;
   dest_type=MPXPLAY_TEXTCONV_TYPE_UTF16BE;
   break;
  case ID3V2_TEXTENCTYPE_UTF8:
   //destbuf[0]=0xef; // ???
   //destbuf[1]=0xbb; //
   //destbuf[2]=0xbf; //
   //destbuf+=3;buflen-=3;datalen=3;
   dest_type=MPXPLAY_TEXTCONV_TYPE_UTF8;
   break;
  default:
   dest_type=MPXPLAY_TEXTCONV_TYPE_CHAR;
 }
 datalen+=mpxplay_playlist_textconv_by_texttypes(
   MPXPLAY_TEXTCONV_TYPES_PUT(MPXPLAY_TEXTCONV_TYPE_MPXPLAY,dest_type),
   srcbuf,-1,destbuf,buflen);
 // !!! trailing zeroes are not written
 return datalen;
}

static long mpx_id3v2x_tagwrite_text(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,char *id3ip,unsigned int comment)
{
 unsigned int datalen,buflen,framelen,ccd_len;
 char *utfbuf,text_enc_type;
 char header[ID3V2_FRAMEHEADSIZE +4];

 datalen=pds_strlen(id3ip);
 if(!datalen)
  return 0;
#ifdef MPXPLAY_UTF8
 buflen=datalen*2+8;
#else
 buflen=datalen*3+8;
#endif
 utfbuf=alloca(buflen);
 if(!utfbuf)
  return MPXPLAY_ERROR_INFILE_MEMORY;

 text_enc_type=framedata->text_enc_type;

 datalen=mpx_id3v2x_textconv_char_to_utf(utfbuf,buflen,id3ip,text_enc_type);
 if(!datalen)
  return MPXPLAY_ERROR_INFILE_MEMORY;
 framelen=1+datalen; // sizeof text_enc + datalen
 if(comment){
  ccd_len=((text_enc_type==ID3V2_TEXTENCTYPE_UTF16LE) || (text_enc_type==ID3V2_TEXTENCTYPE_UTF16BE))? 2:1;
  framelen+=3+ccd_len; // sizeof language + comment_content_descriptor
 }

 if(imds->control==ID3V2_CONTROL_WRITE){
  pds_memcpy(&header[0],&framedata->frameid[0],4);
  imds->version_handler->frame_putsize(&header[4],framelen);
  PDS_PUTB_LE16(&header[8],framedata->flags);

  if(fbfs->fwrite(fbds,header,ID3V2_FRAMEHEADSIZE)!=ID3V2_FRAMEHEADSIZE)
   return MPXPLAY_ERROR_FILEHAND_CANTWRITE;
  if(fbfs->fwrite(fbds,&text_enc_type,1)!=1)                // text_enc
   return MPXPLAY_ERROR_FILEHAND_CANTWRITE;
  if(comment){
   if(fbfs->fwrite(fbds,&framedata->language_type[0],3)!=3) // language
    return MPXPLAY_ERROR_FILEHAND_CANTWRITE;
   header[0]=header[1]=0;
   if(fbfs->fwrite(fbds,&header[0],ccd_len)!=ccd_len) // comment content descriptor (closed with a 00h)
    return MPXPLAY_ERROR_FILEHAND_CANTWRITE;
  }
  if(fbfs->fwrite(fbds,utfbuf,datalen)!=datalen)
   return MPXPLAY_ERROR_FILEHAND_CANTWRITE;
 }
 return (framelen+ID3V2_FRAMEHEADSIZE); // total bytes written
}

static long mpx_id3v2x_tagwrite_txxx(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,char *id3ip)
{
 return mpx_id3v2x_tagwrite_text(imds,framedata,fbfs,fbds,id3ip,0);
}

static long mpx_id3v2x_tagwrite_tcon(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,char *id3ip)
{
 unsigned int genrenum=mpxplay_tagging_id3v1_genre_to_index(id3ip);
 if(genrenum<MAX_ID3GENRENUM){ // standard id3v1 genre-string
  char gennumstr[8];
  sprintf(gennumstr,"(%d)",genrenum); // we write the index only
  return mpx_id3v2x_tagwrite_text(imds,framedata,fbfs,fbds,&gennumstr[0],0);
 }
 return mpx_id3v2x_tagwrite_text(imds,framedata,fbfs,fbds,id3ip,0);
}

static long mpx_id3v2x_tagwrite_comm(struct id3v2x_main_data_s *imds,struct id3v2x_one_frame_data_s *framedata,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,char *id3ip)
{
 return mpx_id3v2x_tagwrite_text(imds,framedata,fbfs,fbds,id3ip,1);
}

//--------------------------------------------------------------------------

static struct id3v2x_one_frame_handler_s id3v23_supported_frames[]=
{{"TPE1",I3I_ARTIST  ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TIT2",I3I_TITLE   ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TALB",I3I_ALBUM   ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TYER",I3I_YEAR    ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TCON",I3I_GENRE   ,&mpx_id3v2x_tagread_tcon,&mpx_id3v2x_tagwrite_tcon},
 {"COMM",I3I_COMMENT ,&mpx_id3v2x_tagread_comm,&mpx_id3v2x_tagwrite_comm},
 {"TRCK",I3I_TRACKNUM,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {NULL,0,NULL,NULL}
};

static struct id3v2x_one_frame_handler_s id3v24_supported_frames[]=
{{"TPE1",I3I_ARTIST  ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TIT2",I3I_TITLE   ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TALB",I3I_ALBUM   ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TYER",I3I_YEAR    ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TDRC",I3I_YEAR    ,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {"TCON",I3I_GENRE   ,&mpx_id3v2x_tagread_tcon,&mpx_id3v2x_tagwrite_tcon},
 {"COMM",I3I_COMMENT ,&mpx_id3v2x_tagread_comm,&mpx_id3v2x_tagwrite_comm},
 {"TRCK",I3I_TRACKNUM,&mpx_id3v2x_tagread_txxx,&mpx_id3v2x_tagwrite_txxx},
 {NULL,0,NULL,NULL}
};

//2.3 and 2.4 are supported
static struct id3v2x_one_version_info_s id3v2_all_version_infos[]=
{{0x03,mpx_id3v2x_get_framesize_4x8,mpx_id3v2x_put_framesize_4x8,&id3v23_supported_frames[0]},
 {0x04,mpx_id3v2x_get_framesize_4x7,mpx_id3v2x_put_framesize_4x7,&id3v24_supported_frames[0]},
 {0x00,NULL,NULL,NULL}
};

static struct id3v2x_one_version_info_s *mpx_id3v2_version_selector(unsigned int version)
{
 struct id3v2x_one_version_info_s *v=&id3v2_all_version_infos[0];
 while(v->frame_getsize){
  if(v->version_number==version)
   return v;
  v++;
 }
 return NULL;
}

static unsigned int mpx_id3v2_check_header(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 imds->filepos=mpx_id3v2x_read_header(imds,fbfs,fbds);
 if(!imds->filepos)
  return 0;

 imds->version_handler=mpx_id3v2_version_selector(imds->version);
 if(!imds->version_handler)
  return 0;

 if(imds->flags&ID3V2_FLAG_EXTENDED_HEADER)
  if(!mpx_id3v2_read_extended_header(imds,fbfs,fbds))
   return 0;

 return 1;
}

static unsigned int mpx_id3v2_read_main(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 if(!mpx_id3v2_check_header(imds,fbfs,fbds))
  return 0;

 if((imds->filepos+ID3V2_FRAMEHEADSIZE)>=imds->totalsize){ // no frames (tags) in the ID3V2
  fbfs->fseek(fbds,imds->totalsize,SEEK_SET);
  return 1;
 }

 do{
  if(mpx_id3v2_read_frame(imds,fbfs,fbds)<0)
   break;
 }while(imds->filepos<(imds->totalsize-ID3V2_FRAMEHEADSIZE));

 if(imds->filepos!=imds->totalsize)
  fbfs->fseek(fbds,imds->totalsize,SEEK_SET);

 return 1;
}

static int mpx_id3v2_write_main(struct id3v2x_main_data_s *imds,struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis,char **id3ip,unsigned int control)
{
 unsigned long totalsize=0;
 void *original_filehand=fbds,*tmpfile_filehand=NULL;
 int i,retcode=MPXPLAY_ERROR_INFILE_OK,framelen,passes,passcount,pass_write=0;
 struct id3v2x_one_frame_data_s *fds;
 struct id3v2x_one_frame_data_s fd_tmp;
 char writetmp[128],original_filename[MAX_PATHNAMELEN];

 if(!(control&MPXPLAY_WRITETAG_CNTRL_TRIMTAGS))
  passes=2;
 else
  passes=1;

 passcount=1;
 do{
  if((passcount==2) || (control&MPXPLAY_WRITETAG_CNTRL_TRIMTAGS)){
   pass_write=1;
   if((control&MPXPLAY_WRITETAG_CNTRL_DUPFILE) && (totalsize>imds->totalsize)){
    imds->oldtotalsize=imds->totalsize;
    imds->totalsize=(totalsize+2048)&(~1023);
    i=sizeof(original_filename);
    original_filename[0]=0;
    retcode=miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_FINAME_GET,&original_filename[0],&i);
    if(retcode!=MPXPLAY_ERROR_OK)
     goto err_out_v2wm;
    if(!original_filename[0])
     goto err_out_write;
    retcode=miis->control_cb(original_filehand,MPXPLAY_CFGFUNCNUM_INFILE_FILE_TMPOPEN,&tmpfile_filehand,NULL);
    if(retcode!=MPXPLAY_ERROR_OK)
     goto err_out_v2wm;
    fbds=tmpfile_filehand;
   }
   retcode=mpx_id3v2x_write_header(imds,fbfs,fbds);
   if(retcode!=MPXPLAY_ERROR_INFILE_OK)
    goto err_out_v2wm;
  }
  totalsize=ID3V2_HEADSIZE;

  for(i=0;i<=I3I_MAX;i++){ // first write the supported frames
   struct id3v2x_one_frame_handler_s *frameselect=tagging_id3v2_search_i3i(imds->version_handler->supported_frames,i);
   if(frameselect){
    fds=tagging_id3v2_search_framedata(imds,frameselect->frameid);
    if(!fds){ // frame not exists in the file yet, create new framedata
     fds=&fd_tmp;
     pds_memset((void *)fds,0,sizeof(struct id3v2x_one_frame_data_s));
     pds_memcpy((void *)&fds->frameid[0],(void *)&frameselect->frameid[0],4);
     fds->text_enc_type=imds->global_textenc_type;
    }
    if(passcount==1){
     imds->control=ID3V2_CONTROL_WRITECHK;
     framelen=frameselect->frame_writer(imds,fds,fbfs,fbds,id3ip[i]);
     if(framelen<0){    // error
      retcode=framelen;
      goto err_out_v2wm;
     }
     if(framelen==0)   // datalen=0
      continue;
     if(!pass_write)
      totalsize+=framelen;
     else if((totalsize+framelen)>imds->totalsize) // not enough space, skip frame
      continue;
    }
    if(pass_write){
     imds->control=ID3V2_CONTROL_WRITE;
     framelen=frameselect->frame_writer(imds,fds,fbfs,fbds,id3ip[i]);
     if(framelen<0){    // error
      retcode=framelen;
      goto err_out_v2wm;
     }
     totalsize+=framelen;
     if(totalsize>=imds->totalsize)
      break;
    }
   }
  }

  i=imds->nb_frames;      // write the other frames
  fds=imds->frame_datas;
  if(i && fds){
   do{
    long bytes=fds->total_framelen;
    if(fds->framebuf && bytes){
     if(pass_write && ((totalsize+bytes)<=imds->totalsize)){
      bytes=fbfs->fwrite(fbds,fds->framebuf,bytes);
      if(bytes!=fds->total_framelen)
       goto err_out_write;
     }
     totalsize+=bytes;
    }
    fds++;
   }while(--i);
  }

  if(pass_write){
   pds_memset(writetmp,0,sizeof(writetmp));
   while(totalsize<imds->totalsize){ // fills up the id3v2 with zeroes
    i=min((imds->totalsize-totalsize),sizeof(writetmp));
    if(fbfs->fwrite(fbds,writetmp,i)!=i)
     goto err_out_write;
    totalsize+=i;
   }
   if((control&MPXPLAY_WRITETAG_CNTRL_DUPFILE) && tmpfile_filehand){
    mpxp_int64_t pos=imds->oldtotalsize,len=-1;
    int whence=SEEK_SET;
    retcode=miis->control_cb(original_filehand,MPXPLAY_CFGFUNCNUM_INFILE_FILE_SEEK,&pos,&whence);
    if(retcode<0)
     break;
    retcode=miis->control_cb(original_filehand,MPXPLAY_CFGFUNCNUM_INFILE_FILE_COPY,tmpfile_filehand,&len);
    if(retcode!=MPXPLAY_ERROR_OK)
     break;
    retcode=miis->control_cb(original_filehand,MPXPLAY_CFGFUNCNUM_INFILE_FILE_TMPXCHCLOSE,&tmpfile_filehand,NULL);
    if(retcode!=MPXPLAY_ERROR_OK)
     break;
    if(!fbfs->fopen(original_filehand,original_filename,O_RDWR|O_BINARY,0))
     retcode=MPXPLAY_ERROR_INFILE_CANTOPEN;
   }
  }else if(totalsize>imds->totalsize){
   if(!(control&(MPXPLAY_WRITETAG_CNTRL_TRIMTAGS|MPXPLAY_WRITETAG_CNTRL_DUPFILE))){
    retcode=MPXPLAY_ERROR_INFILE_WRITETAG_NOSPACE;
    break;
   }
  }

 }while(++passcount<=passes);

 goto err_out_v2wm;

err_out_write:
 retcode=MPXPLAY_ERROR_FILEHAND_CANTWRITE;
err_out_v2wm:
 if(tmpfile_filehand)
  miis->control_cb(original_filehand,MPXPLAY_CFGFUNCNUM_INFILE_FILE_CLOSE,&tmpfile_filehand,NULL);
 return retcode;
}

unsigned int mpxplay_tagging_id3v2_check(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 struct id3v2x_main_data_s imds;
 pds_memset((void *)&imds,0,sizeof(imds));
 imds.control=ID3V2_CONTROL_READCHK;
 return mpx_id3v2_check_header(&imds,fbfs,fbds);
}

int mpxplay_tagging_id3v2_get(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis)
{
 int retcode;
 struct id3v2x_main_data_s imds;
 pds_memset((void *)&imds,0,sizeof(imds));
 imds.control=ID3V2_CONTROL_READ;
 imds.miis=miis;
 retcode=mpx_id3v2_read_main(&imds,fbfs,fbds);
 tagging_id3v2_framespace_dealloc(&imds);
 return retcode;
}

int mpxplay_tagging_id3v2_put(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis,char **id3ip,unsigned long control)
{
 int retcode;
 struct id3v2x_main_data_s imds;
 pds_memset((void *)&imds,0,sizeof(imds));
 imds.control=ID3V2_CONTROL_WRITE;
 imds.miis=miis;
 if(!mpx_id3v2_read_main(&imds,fbfs,fbds) && !(control&MPXPLAY_WRITETAG_CNTRL_DUPFILE))
  retcode=MPXPLAY_ERROR_INFILE_WRITETAG_NOSPACE;
 else{
  tagging_id3v2_globaltextenc_get(&imds); // use the same (utf) text encoding at the new tags
  retcode=mpx_id3v2_write_main(&imds,fbfs,fbds,miis,id3ip,control);
 }
 tagging_id3v2_framespace_dealloc(&imds);
 if((retcode==MPXPLAY_ERROR_INFILE_WRITETAG_NOSPACE) && (control&MPXPLAY_WRITETAG_CNTRL_TRIMTAGS))
  retcode=MPXPLAY_ERROR_INFILE_OK;
 return retcode;
}

//-------------------------------------------------------------------------
// requires 10 bytes in bufp !
unsigned long mpxplay_tagging_id3v2_totalsize(char *bufp)
{
 unsigned long id3v2totalsize;
 unsigned int flags;

 if(PDS_GETB_LE24(bufp)!=PDS_GET4C_LE32('I','D','3',0))
  return 0;

 bufp+=3;   // ID3
 bufp+=2;   // version

 flags=bufp[0];
 bufp+=1;   // flags

 id3v2totalsize=ID3V2_HEADSIZE+mpx_id3v2x_get_framesize_4x7(bufp);
 if(flags&ID3V2_FLAG_FOOTER_PRESENT)
  id3v2totalsize+=ID3V2_FOOTERSIZE;

 return id3v2totalsize;
}

//--------------------------------------------------------------------
//APEtag

typedef struct APETag_s {
 unsigned char   ID       [8];    // should equal 'APETAGEX'
 unsigned char   Version  [4];    // 1000 or 2000
 unsigned char   Length   [4];    // the complete size of the tag, including this footer
 unsigned char   TagCount [4];    // the number of fields in the tag
 unsigned char   Flags    [4];    // the tag flags (none currently defined)
 unsigned char   Reserved [8];    // reserved for later use
}APETag_s;

typedef struct tagtype_s{
 char *name;
 unsigned int i3i_index;
}tagtype_s;

static tagtype_s tagtypes[]={
 {"Artist",I3I_ARTIST},{"Title"  ,I3I_TITLE}  ,{"Album",I3I_ALBUM},
 {"Year"  ,I3I_YEAR}  ,{"Comment",I3I_COMMENT},{"Genre",I3I_GENRE},
 {"Track" ,I3I_TRACKNUM}
};

#define TAGTYPENUM (sizeof(tagtypes)/sizeof(struct tagtype_s))

static int ape_tag_check_apetag(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct APETag_s *apetag)
{
 int version,taglen;
 long filesize=fbfs->filelength(fbds);
 if(filesize<=((long)sizeof(struct APETag_s)))
  return 0;
 if(fbfs->fseek(fbds,-((long)sizeof(struct APETag_s)),SEEK_END)<0)
  return 0;
 if(fbfs->fread(fbds,(char *)apetag,sizeof(struct APETag_s))!=sizeof(struct APETag_s))
  return 0;
 if(pds_strncmp(apetag->ID,"APETAGEX",sizeof(apetag->ID))!=0)
  return 0;
 version=PDS_GETB_LE32(apetag->Version);
 if(version!=1000 && version!=2000)
  return 0;
 taglen=PDS_GETB_LE32(apetag->Length);
 if(taglen<=sizeof(struct APETag_s) || (taglen>=filesize))
  return 0;
 if(!PDS_GETB_LE32(apetag->TagCount))
  return 0;
 if(fbfs->fseek(fbds,-taglen,SEEK_END)<0)
  return 0;
 return 1;
}

int mpxplay_tagging_apetag_check(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds)
{
 struct APETag_s apetag;
 return ape_tag_check_apetag(fbfs,fbds,&apetag);
}

static int ape_tag_read_apetag(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct APETag_s *apetag,struct mpxplay_infile_info_s *miis)
{
 long tt,version,taglen,tagcount,namelen,datalen,dataflags,retcode=MPXPLAY_ERROR_INFILE_NODATA;
 char *tagbuff,*tbp,*tname,*tdata;

 taglen=PDS_GETB_LE32(apetag->Length);
 tagbuff=pds_malloc(taglen);
 if(!tagbuff)
  return MPXPLAY_ERROR_INFILE_MEMORY;
 if(fbfs->fread(fbds,tagbuff,taglen-sizeof(struct APETag_s))!=(taglen-sizeof(struct APETag_s))){
  retcode=MPXPLAY_ERROR_FILEHAND_CANTREAD;
  goto err_out_tagread;
 }
 tbp=tagbuff;
 tagbuff[taglen-sizeof(struct APETag_s)]=0;
 version=PDS_GETB_LE32(apetag->Version);
 tagcount=PDS_GETB_LE32(apetag->TagCount);
 for(;*tbp && tagcount--;){
  datalen  =PDS_GETB_LE32(tbp); tbp+=4;
  dataflags=PDS_GETB_LE32(tbp); tbp+=4;
  namelen=pds_strlen(tbp)+1;
  if(namelen>128)        // impossible data
   goto err_out_tagread;
  tname=tbp; tbp+=namelen;
  tdata=tbp; tbp+=datalen;
  if(tbp>(&tagbuff[taglen-sizeof(struct APETag_s)])) // bad data -> overflow
   goto err_out_tagread;
  if((namelen>1) && ((version==1000 && datalen>1) || (version==2000 && datalen>0 && !(dataflags&2)))){
   /*if(pds_stricmp(tname,"replaygain_track_gain")==0){// || pds_stricmp(tname,"replaygain_album_gain")==0)
    miis->audio_decoder_infos->replaygain=atof(tdata);
    //fprintf(stdout,"%s %s %2.2f df:%d\n",tname,tdata,miis->audio_decoder_infos->replaygain,dataflags);
   }else{*/
   for(tt=0;tt<TAGTYPENUM;tt++){
    if(pds_stricmp(tname,tagtypes[tt].name)==0){
     unsigned int srctexttype;
     if(version==1000){
      srctexttype=MPXPLAY_TEXTCONV_TYPE_CHAR;
      datalen--;
     }else
      srctexttype=MPXPLAY_TEXTCONV_TYPE_UTF8;
     miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PUT,srctexttype,tagtypes[tt].i3i_index),tdata,&datalen);
    }
   }
  }
 }
 retcode=MPXPLAY_ERROR_OK;
err_out_tagread:
 pds_free(tagbuff);
 return retcode;
}

int mpxplay_tagging_apetag_get(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis)
{
 struct APETag_s apetag;

 if(ape_tag_check_apetag(fbfs,fbds,&apetag))
  return ape_tag_read_apetag(fbfs,fbds,&apetag,miis);

 return MPXPLAY_ERROR_INFILE_NODATA;
}

int mpxplay_tagging_apetag_put(struct mpxplay_filehand_buffered_func_s *fbfs,void *fbds,struct mpxplay_infile_info_s *miis)
{
 int tt,i,len,extratags=0,cutfile=0,retcode=MPXPLAY_ERROR_INFILE_OK;
 long alltagsize=0,tagcount=0,newalltagsize,*tagdlens=NULL,*tagflags=NULL;
 mpxp_int32_t tagnamelen,tagdatalen,flags=0,version,texttype;
 char *tagbuff=NULL,*tbp,**tagnames=NULL,**tagdatas=NULL,*extradatas[TAGTYPENUM];
 struct APETag_s apetag;

 if(mpxplay_tagging_id3v1_check(fbfs,fbds))
  return mpxplay_tagging_id3v1_put(fbfs,fbds,miis);

 pds_memset(extradatas,0,sizeof(extradatas));

 if(ape_tag_check_apetag(fbfs,fbds,&apetag)){ // APE file already has APETAGEX
  // load all tagdatas, update and write back
  version=PDS_GETB_LE32(apetag.Version);
  if((version!=1000) && (version!=2000))
   return MPXPLAY_ERROR_INFILE_WRITETAG_TAGTYPE;
  texttype=(version==1000)? MPXPLAY_TEXTCONV_TYPE_CHAR:MPXPLAY_TEXTCONV_TYPE_UTF8;
  alltagsize=PDS_GETB_LE32(apetag.Length);
  tbp=tagbuff=pds_malloc(alltagsize);
  tagcount=PDS_GETB_LE32(apetag.TagCount);
  tagnames=(char **)pds_malloc((tagcount+TAGTYPENUM)*sizeof(char *));
  tagdatas=(char **)pds_malloc((tagcount+TAGTYPENUM)*sizeof(char *));
  tagdlens=(long *)pds_malloc((tagcount+TAGTYPENUM)*sizeof(long));
  tagflags=(long *)pds_malloc((tagcount+TAGTYPENUM)*sizeof(long));
  if(!tagbuff || !tagnames || !tagdatas || !tagdlens || !tagflags){
   retcode=MPXPLAY_ERROR_INFILE_MEMORY;
   goto close_tagput;
  }
  //load
  if(fbfs->fread(fbds,tagbuff,alltagsize-sizeof(struct APETag_s))!=(alltagsize-sizeof(struct APETag_s))){
   retcode=MPXPLAY_ERROR_INFILE_EOF;
   goto close_tagput;
  }

  for(i=0;i<tagcount && (tbp<(tagbuff+alltagsize-sizeof(struct APETag_s)));i++){
   tagdlens[i]=PDS_GETB_LE32(tbp);tbp+=4;
   tagflags[i]=PDS_GETB_LE32(tbp);tbp+=4;
   tagnames[i]=tbp;               tbp+=pds_strlen(tbp)+1;
   tagdatas[i]=tbp;               tbp+=tagdlens[i];
  }
  tagcount=i; // correction in the case of a wrong ape-tag
  //update
  for(tt=0;tt<TAGTYPENUM;tt++){
   int i3iindex=tagtypes[tt].i3i_index;
   for(i=0;i<tagcount;i++){
    if(!(tagflags[i]&2)){
     if(pds_stricmp(tagnames[i],tagtypes[tt].name)==0){ // found, update
      len=0;
      miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,texttype,i3iindex),&extradatas[tt],&len);
      if((len>0) && extradatas[tt]){
       tagdatas[i]=extradatas[tt];
       tagdlens[i]=len+((version==2000)? 0:1); // v2 does not count terminating null
      }else{
       tagdatas[i]="";
       tagdlens[i]=(version==2000)? 0:1;
      }
      break;
     }
    }
   }
   if(i==tagcount){  // not found, new
    len=0;
    miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,texttype,i3iindex),&extradatas[tt],&len);
    if((len>0) && extradatas[tt]){
     tagnames[tagcount+extratags]=tagtypes[tt].name;
     tagdatas[tagcount+extratags]=extradatas[tt];
     tagdlens[tagcount+extratags]=len+((version==2000)? 0:1);
     tagflags[tagcount+extratags]=0;
     extratags++;
    }
   }
  }
  //write back
  if(fbfs->fseek(fbds,-alltagsize,SEEK_END)<0){
   retcode=MPXPLAY_ERROR_INFILE_EOF;
   goto close_tagput;
  }
  newalltagsize=0;
  tagcount+=extratags;
  for(i=0;i<tagcount;i++){
   if((version==1000 && tagdlens[i]>1) || (version==2000 && tagdlens[i])){
    tagnamelen=pds_strlen(tagnames[i])+1;
    fbfs->fwrite(fbds,&tagdlens[i],sizeof(tagdlens[i]));
    fbfs->fwrite(fbds,&tagflags[i],sizeof(tagflags[i]));
    fbfs->fwrite(fbds,tagnames[i],tagnamelen);
    fbfs->fwrite(fbds,tagdatas[i],tagdlens[i]);
    newalltagsize+=sizeof(tagdlens[i])+sizeof(tagflags[i])+tagnamelen+tagdlens[i];
   }
  }
  if(newalltagsize<alltagsize)
   cutfile=1;
  alltagsize=newalltagsize;
 }else{
  // write an absolute new APEv2 tag
  version=2000;
  if(fbfs->fseek(fbds,0,SEEK_END)<0)
   return MPXPLAY_ERROR_INFILE_EOF;
  for(tt=0;tt<TAGTYPENUM;tt++){
   len=0;
   miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,MPXPLAY_TEXTCONV_TYPE_UTF8,tagtypes[tt].i3i_index),&extradatas[0],&len);
   if(extradatas[0]){
    if(len>0){
     tagnamelen=pds_strlen(tagtypes[tt].name)+1;
     tagdatalen=len;
     fbfs->fwrite(fbds,&tagdatalen,sizeof(tagdatalen));
     fbfs->fwrite(fbds,&flags,sizeof(flags));
     fbfs->fwrite(fbds,tagtypes[tt].name,tagnamelen);
     fbfs->fwrite(fbds,extradatas[0],tagdatalen);
     alltagsize+=sizeof(tagdatalen)+sizeof(flags)+tagnamelen+tagdatalen;
     tagcount++;
    }
    pds_free(extradatas[0]);
    extradatas[0]=NULL;
   }
  }
 }
 //finish/close the tag
 memcpy(apetag.ID,"APETAGEX",sizeof("APETAGEX"));
 PDS_PUTB_LE32(apetag.Version,version);
 PDS_PUTB_LE32(apetag.Length,alltagsize+sizeof(struct APETag_s));
 PDS_PUTB_LE32(apetag.TagCount,tagcount);
 PDS_PUTB_LE32(apetag.Flags,0);
 memset(apetag.Reserved,0,sizeof(apetag.Reserved));
 fbfs->fwrite(fbds,(void *)&apetag,sizeof(struct APETag_s));

 if(cutfile)
  if(!fbfs->chsize(fbds,fbfs->ftell(fbds)))
   retcode=MPXPLAY_ERROR_INFILE_EOF;

close_tagput:
 if(tagflags) pds_free(tagflags);
 if(tagdlens) pds_free(tagdlens);
 if(tagdatas) pds_free(tagdatas);
 if(tagnames) pds_free(tagnames);
 if(tagbuff)  pds_free(tagbuff);
 for(tt=0;tt<TAGTYPENUM;tt++)
  if(extradatas[tt]) pds_free(extradatas[tt]);
 return retcode;
}

//--------------------------------------------------------------------------
// Vorbis comment read/edit/write (common routines for FLAC,OGG)

/*#define VORBIS_COMMENT_TYPES 9
static char *vorbiscommenttypes[VORBIS_COMMENT_TYPES]={"title","artist","author","album","date","comment","description","genre","tracknumber"};
static unsigned int id3index[VORBIS_COMMENT_TYPES]={I3I_TITLE,I3I_ARTIST,I3I_ARTIST,I3I_ALBUM,I3I_YEAR,I3I_COMMENT,I3I_COMMENT,I3I_GENRE,I3I_TRACKNUM};*/

#define VORBIS_COMMENT_TYPES 7
static char *vorbiscommenttypes[VORBIS_COMMENT_TYPES]={"artist","title","album","date","comment","genre","tracknumber"};
static unsigned int id3index[VORBIS_COMMENT_TYPES]={I3I_ARTIST,I3I_TITLE,I3I_ALBUM,I3I_YEAR,I3I_COMMENT,I3I_GENRE,I3I_TRACKNUM};

int mpxplay_tagging_vorbiscomment_read_to_mpxplay(struct mpxplay_infile_info_s *miis,void *fbds,char *metadata_field,unsigned long metadata_size)
{
 char *md=metadata_field;
 int retcode=MPXPLAY_ERROR_OK,counted_size=0,comment_size,comments,i;
 unsigned int len;

 comment_size=PDS_GETB_LE32(md);md+=4;counted_size+=4;
 md+=comment_size;counted_size+=comment_size; // reference lib
 comments=PDS_GETB_LE32(md);md+=4;counted_size+=4;
 while(comments-- && (counted_size<metadata_size)){
  comment_size=PDS_GETB_LE32(md);md+=4;counted_size+=4;
  if(comment_size){
   char *p=pds_strchr(md,'=');
   if(p){
    *p++=0;
    for(i=0;i<VORBIS_COMMENT_TYPES;i++){
     if(pds_stricmp(md,vorbiscommenttypes[i])==0){
      len=comment_size-(p-md);
      miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_UPDATE,MPXPLAY_TEXTCONV_TYPE_UTF8,id3index[i]),
                       p,&len);
     }
    }
   }
   md+=comment_size;
   counted_size+=comment_size;
  }
 }
 return retcode;
}

int mpxplay_tagging_vorbiscomment_load_from_field(struct vorbiscomment_info_s *vci,char *metadata_field,unsigned long metadata_size)
{
 char *md=metadata_field;
 struct vorbiscomment_line_s *vcl;
 int retcode=MPXPLAY_ERROR_OK;
 long comment_size,counted_size,comments;

 if(!vci || !md || !metadata_size)
  return MPXPLAY_ERROR_CFGFUNC_INVALIDDATA;

 counted_size=0;
 comment_size=PDS_GETB_LE32(md);md+=4;counted_size+=4;
 if(comment_size){
  vci->referencelib_str=pds_malloc(comment_size+1);
  if(!vci->referencelib_str)
   return MPXPLAY_ERROR_INFILE_MEMORY;
  vci->referencelib_len=pds_strncpy(vci->referencelib_str,md,comment_size);
  vci->referencelib_str[vci->referencelib_len]=0;
  md+=comment_size;counted_size+=comment_size;
 }
 vci->metadata_size_cur=metadata_size;
 comments=PDS_GETB_LE32(md);md+=4;counted_size+=4;
 if(!comments)
  goto err_out_tvl;
 vci->comment_datas=vcl=pds_calloc(comments,sizeof(struct vorbiscomment_line_s));
 if(!vcl){
  retcode=MPXPLAY_ERROR_INFILE_MEMORY;
  goto err_out_tvl;
 }
 vci->comments_allocated=comments;
 vci->comments_loaded=0;
 while(counted_size<metadata_size){
  comment_size=PDS_GETB_LE32(md);md+=4;counted_size+=4;
  if(comment_size){
   char *p=pds_strchr(md,'=');
   if(p){
    vcl->typelen=(unsigned long)(p-md);
    vcl->typestr=pds_malloc(vcl->typelen+1);
    if(!vcl->typestr){
     retcode=MPXPLAY_ERROR_INFILE_MEMORY;
     goto err_out_tvl;
    }
    *p++=0;
    vcl->typelen=pds_strcpy(vcl->typestr,md);
    if(comment_size>(vcl->typelen+1)){
     vcl->datalen=comment_size-(vcl->typelen+1);
     vcl->datastr=pds_malloc(vcl->datalen+1);
     if(!vcl->datastr){
      retcode=MPXPLAY_ERROR_INFILE_MEMORY;
      goto err_out_tvl;
     }
     vcl->datalen=pds_strncpy(vcl->datastr,p,vcl->datalen);
     vcl->datastr[vcl->datalen]=0;
    }
    vci->comments_loaded++;
    vcl++;
   }else if(*md){
    vcl->typelen=comment_size;
    vcl->typestr=pds_malloc(vcl->typelen+1);
    if(!vcl->typestr){
     retcode=MPXPLAY_ERROR_INFILE_MEMORY;
     goto err_out_tvl;
    }
    vcl->typelen=pds_strncpy(vcl->typestr,md,vcl->typelen);
    vcl->typestr[vcl->typelen]=0;
    vci->comments_loaded++;
    vcl++;
   }
   md+=comment_size;
   counted_size+=comment_size;
  }
  if(!(--comments))
   break;
 }

err_out_tvl:
 if(retcode!=MPXPLAY_ERROR_OK)
  mpxplay_tagging_vorbiscomment_free(vci);
 return retcode;
}

//update vorbiscomment from Mpxplay's database (pei)
int mpxplay_tagging_vorbiscomment_update_from_mpxplay(struct mpxplay_infile_info_s *miis,void *fbds,struct vorbiscomment_info_s *vci)
{
 unsigned int i,l;
 long len;
 struct vorbiscomment_line_s *vcl;

 if(!miis || !vci)
  return MPXPLAY_ERROR_CFGFUNC_INVALIDDATA;

 vcl=vci->comment_datas;

 for(i=0;i<VORBIS_COMMENT_TYPES;i++){
  vcl=vci->comment_datas;
  for(l=0;l<vci->comments_loaded;l++,vcl++){
   if(pds_stricmp(vcl->typestr,vorbiscommenttypes[i])==0){
    if(vcl->datastr){
     pds_free(vcl->datastr);
     vcl->datastr=NULL;
     vcl->datalen=0;
    }
    len=0;
    miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,MPXPLAY_TEXTCONV_TYPE_UTF8,id3index[i]),
                     &vcl->datastr,&len);
    if(len>0)
     vcl->datalen=len;
    else{
     pds_free(vcl->typestr);
     vcl->typestr=NULL;
     vcl->typelen=0;
     if(vcl->datastr){
      pds_free(vcl->datastr);
      vcl->datastr=NULL;
     }
    }
    break;
   }
  }
  if(l==vci->comments_loaded){ // entry not found, add new one
   if(vci->comments_loaded==vci->comments_allocated){
    vcl=pds_calloc((vci->comments_allocated+8),sizeof(struct vorbiscomment_line_s));
    if(!vcl)
     return MPXPLAY_ERROR_INFILE_MEMORY;
    if(vci->comment_datas){
     if(vci->comments_allocated)
      pds_memcpy(vcl,vci->comment_datas,(vci->comments_allocated*sizeof(struct vorbiscomment_line_s)));
     pds_free(vci->comment_datas);
    }
    vci->comments_allocated+=8;
    vci->comment_datas=vcl;
   }
   vcl=&vci->comment_datas[vci->comments_loaded];
   vcl->typelen=pds_strlen(vorbiscommenttypes[i]);
   vcl->typestr=pds_malloc(vcl->typelen+1);
   if(!vcl->typestr)
    return MPXPLAY_ERROR_INFILE_MEMORY;
   pds_strcpy(vcl->typestr,vorbiscommenttypes[i]);
   len=0;
   miis->control_cb(fbds,MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_PACKFUNCTTI3I(MPXPLAY_CFGFUNCNUM_INFILE_ENTRY_TAG_GET,MPXPLAY_TEXTCONV_TYPE_UTF8,id3index[i]),
                    &vcl->datastr,&len);
   if(len>0){
    vcl->datalen=len;
    vci->comments_loaded++;
   }else{
    pds_free(vcl->typestr);
    vcl->typestr=NULL;
    vcl->typelen=0;
    if(vcl->datastr){
     pds_free(vcl->datastr);
     vcl->datastr=NULL;
    }
   }
  }
 }
 return MPXPLAY_ERROR_OK;
}

int mpxplay_tagging_vorbiscomment_write_to_field(struct vorbiscomment_info_s *vci,char **metadata_field,unsigned long size_limit,unsigned long control)
{
 char *md;
 struct vorbiscomment_line_s *vcl;
 int commentc,validc,counted_size,valid_comments,all_comments;

 if(!vci || !vci->comment_datas)
  return MPXPLAY_ERROR_CFGFUNC_INVALIDDATA;

 if(control&MPXPLAY_WRITETAG_CNTRL_TRIMTAGS)
  size_limit=vci->metadata_size_cur;
 if(!size_limit)
  size_limit=0x0fffffff;

 vcl=vci->comment_datas;
 counted_size=4+vci->referencelib_len+4;
 valid_comments=0;
 for(commentc=0;commentc<vci->comments_loaded;commentc++,vcl++){
  if(vcl->typelen){
   unsigned long csize=counted_size+4+vcl->typelen+((vcl->datalen)? 1:0)+vcl->datalen;
   if((csize==size_limit) || ((csize+4)<=size_limit)){ // fill up the comment field to maximum
    counted_size=csize;
    valid_comments++;
    if(csize==size_limit)
     break;
   }
  }
 }
 all_comments=valid_comments;
 vci->metadata_size_new=counted_size;
 if(!(control&MPXPLAY_WRITETAG_CNTRL_NOPADD)){ // Flac padding is added in flac header (not in vorbiscomment)
  if((control&MPXPLAY_WRITETAG_CNTRL_TRIMTAGS) || (!(control&MPXPLAY_WRITETAG_CNTRL_DUPFILE) && ((counted_size+4)<=vci->metadata_size_cur))){
   if(counted_size<vci->metadata_size_cur)
    all_comments++; // adds an empty line (padding) to fill up the field (also if not TRIMTAGS !!!)
   vci->metadata_size_new=vci->metadata_size_cur;
  }else{
   if((vci->metadata_size_new+4)<=size_limit){ //
    vci->metadata_size_new+=512;               // !!! padding
    if(vci->metadata_size_new>size_limit)
     vci->metadata_size_new=size_limit;
    all_comments++;
   }
  }
 }

 if(!vci->metadata_size_new || !all_comments)
  return 0;
 md=(char *)pds_malloc(vci->metadata_size_new+16);
 if(!md)
  return MPXPLAY_ERROR_INFILE_MEMORY;
 *metadata_field=md;
 PDS_PUTB_LE32(md,vci->referencelib_len);md+=4;
 if(vci->referencelib_len){
  pds_memcpy(md,vci->referencelib_str,vci->referencelib_len);
  md+=vci->referencelib_len;
 }
 PDS_PUTB_LE32(md,all_comments);md+=4;
 vcl=vci->comment_datas;

 counted_size=4+vci->referencelib_len+4;
 for(commentc=0,validc=0;(commentc<vci->comments_loaded) && (validc<valid_comments);commentc++,vcl++){
  if(vcl->typelen){
   unsigned long tdsize=vcl->typelen+((vcl->datalen)? 1:0)+vcl->datalen;
   unsigned long csize=counted_size+4+tdsize;
   if((csize==size_limit) || ((csize+4)<=size_limit)){ // fill up
    PDS_PUTB_LE32(md,tdsize);md+=4;
    pds_memcpy(md,vcl->typestr,vcl->typelen);
    md+=vcl->typelen;
    if(vcl->datalen){
     *md++='=';
     pds_memcpy(md,vcl->datastr,vcl->datalen);
     md+=vcl->datalen;
    }
    counted_size=csize;
    validc++;
   }
  }
 }

 if(valid_comments<all_comments){ // at trimtags or padding
  long leftbytes=vci->metadata_size_new-counted_size-4;
  if(leftbytes>=0){ // always have to be
   PDS_PUTB_LE32(md,leftbytes);
   if(leftbytes>0)
    pds_memset(md+4,0,leftbytes);
  }
 }

 return MPXPLAY_ERROR_OK;
}

void mpxplay_tagging_vorbiscomment_free(struct vorbiscomment_info_s *vci)
{
 unsigned int i;
 struct vorbiscomment_line_s *vcl;

 if(!vci)
  return;
 if(vci->referencelib_str)
  pds_free(vci->referencelib_str);
 i=vci->comments_allocated;
 if(!i)
  goto err_out_free;
 vcl=vci->comment_datas;
 do{
  if(vcl->typestr)
   pds_free(vcl->typestr);
  if(vcl->datastr)
   pds_free(vcl->datastr);
  vcl++;
 }while(--i);
 pds_free(vci->comment_datas);
err_out_free:
 pds_memset(vci,0,sizeof(*vci));
}
