/* Copyright (C) 2002, 2003 Brian Victor Fernandes This file is part of XMMS mp3cue. XMMS mp3cue is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. XMMS mp3cue 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. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with XMMS mp3cue; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA XMMS mp3cue: http://brianvictor.tripod.com/mp3cue.htm Email: brianv@phreaker.net Brian Fernandes 503, Miaami Apts. II, St. Vincents Street, Pune 411001 INDIA */ #include #include using namespace std; #include #include #include #include "xmmsctrl.h" #include "defs.h" typedef unsigned char BYTE; typedef unsigned char BOOL; extern GeneralPlugin mp3cue; extern gchar *album_name_s; extern gchar *album_artist_s; int Read7Int(const BYTE *pBuff); void Write7Int(BYTE *pBuff, int nVal); int ReadInt(const BYTE *pBuff); void WriteInt(BYTE *pBuff, int nVal); BOOL LoadID3(const char *fname); BYTE *findGEOB(BYTE *tag, int size); char *parse_cuesheet_frame(char *framestart, int fsize); string Make_Cue_Sheet_Frame(short int version); BYTE * CreateHeader(FILE *fp, int &tSize); BYTE *CreateTextFrame(const char *ident, const char *text, int &fSize); void ReadID3v1(); //we parse and save the main parts of the cuesheet using the same routines ... extern void parse_cue_sheet(FILE *cue_sheet,char *cue_mem); extern string Make_Cue_String(); extern void quick_message(gchar *message); struct ID3v1 { char tag[3]; char title[30]; char artist[30]; char album[30]; char year[4]; char comment[30]; char genre; }; struct ID3v2_Header { BYTE identifier[3]; BYTE mjVersion, mnVersion; BYTE flags; BYTE size[4]; }; struct ID3v2_EHeader { BYTE size[4]; BYTE nflags; BYTE flags; }; ID3v2_Header Hdr; ID3v2_EHeader EHdr; BOOL LoadID3(const char *fname) { int tag_size, e_hdr_size=0; BYTE *tag; //this will hold the entire ID3tag FILE *fp = fopen(fname,"r"); if(!fp) return FALSE; //read the first 14 bytes for the header and extended header info fread(&Hdr, 1, sizeof(Hdr), fp); if(strncmp((const char *)Hdr.identifier,"ID3",3)) { #ifdef DEBUG printf("No ID3v2\n"); #endif fclose(fp); return FALSE; //no ID3v2 tag found, nothing to be gained from here } //here starts the first of v2.4 changes ... bastards, talk about downward compatibility .. //v2.4 has a footer, not included in the size of the tag //v2.3 has no footer.. in any case; we don't give a damn, we don't need the frickin footer tag_size = Read7Int(Hdr.size); if((Hdr.flags >> 6) % 2) { //read the extended header fread(&EHdr, 1, sizeof(EHdr), fp); //in v2.4, the extended header size is the size of the whole header //in v2.3, the extended header size does not include the 4 bytes for the size if(Hdr.mjVersion <= 3) e_hdr_size = ReadInt(EHdr.size) + 4; else e_hdr_size = Read7Int(EHdr.size); } #ifdef DEBUG printf("ID3v2.%c.%c Size is %d\nExtended Header %d\nFlag %d",Hdr.mjVersion+'0',Hdr.mnVersion+'0',tag_size, e_hdr_size, Hdr.flags); #endif int offset = 10 + e_hdr_size; //skip both the ID3 and extended header int frameslength = tag_size - e_hdr_size; //read size of the remaining frames //allocate frameslength amount of memory tag = (BYTE *)malloc(frameslength); if(!tag) { #ifdef DEBUG printf("Error: ID3v2 Tag too large\n"); #endif fclose(fp); return FALSE; } //now read all the frames into tag fseek(fp, offset, SEEK_SET); fread(tag, 1, frameslength, fp); BYTE *GEOB_loc; char *cue_start; GEOB_loc = findGEOB(tag, frameslength); if(GEOB_loc) { cue_start = parse_cuesheet_frame((char *)GEOB_loc, ((Hdr.mjVersion <= 3) ? ReadInt(GEOB_loc+4) : Read7Int(GEOB_loc+4)) ); if(cue_start) { parse_cue_sheet(NULL, cue_start); } } free(tag); fclose(fp); return TRUE; } BYTE *findGEOB(BYTE *tag, int size) { BYTE *t_tag = tag; int read = 0, fsize; while(read < size) { //read the four bytes if(!strncmp((const char *)t_tag,"GEOB",4) || !strncmp((const char *)t_tag,"XXQ1",4)) { #ifdef DEBUG printf("mp3cue tag located ... parsing.\n"); #endif return t_tag; } else if(!(*t_tag)) break; //we have hit padding else { //so now the actual nonsense data is fsize + 10 (frame header size) //just skip the shit if(Hdr.mjVersion <= 3) fsize = ReadInt(t_tag+4); //stored as 32 bit integer else fsize = Read7Int(t_tag+4); //stored as synch safe integer t_tag += 10 + fsize; read += 10 + fsize; } } return NULL; } BYTE *findPadding(BYTE *tag, int size) { int read = 0, fsize; while(read < size) { //read the four bytes if(!(*tag)) return tag; //we have hit padding else { //just skip the tag if(Hdr.mjVersion <= 3) fsize = ReadInt(tag+4); //stored as 32 bit integer else fsize = Read7Int(tag+4); //stored as synch safe integer tag += 10 + fsize; read += 10 + fsize; } } return NULL; } char *parse_cuesheet_frame(char *framestart, int fsize) { char *t_ptr = framestart + 10; //skip the ID, size and flags char *cue_start; char mime[10], filename[25]; //the following was almost copied from the original, we handle the 3rd condition properly.. if(!*t_ptr && ! *(t_ptr + 1)) { #ifdef DEBUG printf("option 1\n"); #endif cue_start = t_ptr+4; } else if(*t_ptr==0x01) { #ifdef DEBUG printf("Unicode mp3cue tag not supported\n"); #endif return NULL; } else if(*t_ptr==0x00) { #ifdef DEBUG printf("option 3\n"); #endif //scan the mime type and the "filename" t_ptr++; sscanf(t_ptr,"%s",mime); t_ptr+=strlen(mime)+1; sscanf(t_ptr,"%[^0]s",filename); t_ptr+=strlen(filename)+2; #ifdef DEBUG printf("%s\n%s\n",mime,filename); #endif cue_start = t_ptr; } else { cue_start = t_ptr; #ifdef DEBUG printf("option 4\n"); #endif } return cue_start; } string Make_Cue_Sheet_Frame(short int version) { string frame_string; frame_string = "GEOB"; frame_string += "0000"; //fill this in with the size later frame_string += '\0'; frame_string += '\0'; //flags //now start with the actual text frame_string +='\0'; //encoding as ascii frame_string +="text"; //mime frame_string += '\0'; frame_string +="mp3cue CueSheet";//filename frame_string +='\0'; frame_string +='Q'; //descriptor frame_string += "CUESHEET\r\n" + Make_Cue_String() + "ENDCUESHEET"; frame_string +="CUEVERSION\r\n" + string("XMMS mp3cue " VERSION) + "ENDCUEVERSION"; frame_string += "CUEINFO\r\nENDCUEINFO"; //currently no info //set the size int fsize = frame_string.length() - 10; //remove 10 bytes .. exclude the header size #ifdef DEBUG printf("len is %d\n ",fsize); #endif if(version <= 3) WriteInt((BYTE *)&frame_string[4], fsize); else Write7Int((BYTE *)&frame_string[4], fsize); return frame_string; } BYTE * CreateHeader(FILE *fp, int &tSize) { BYTE *ID3_Hdr; tSize = sizeof(Hdr); int e_hdr_size; //read the header fread(&Hdr, sizeof(Hdr), 1, fp); if(!strncmp((const char *)Hdr.identifier, "ID3", 3)) { //if there is a v2 header already ID3_Hdr = (BYTE *)malloc(tSize); memcpy(ID3_Hdr, &Hdr, sizeof(Hdr)); if( (Hdr.flags >> 6) % 2) { //if there is an extended header fread(&EHdr, 1, sizeof(EHdr), fp); //size of the extended header if(Hdr.mjVersion <= 3) e_hdr_size = ReadInt(EHdr.size) + 4; else e_hdr_size = Read7Int(EHdr.size); tSize += e_hdr_size; ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize); memcpy(ID3_Hdr+sizeof(Hdr), &EHdr, sizeof(EHdr)); //we still have to read e_hdr_size - sizeof(ehdr) bytes ... the actual extended header fread(ID3_Hdr+sizeof(Hdr)+sizeof(EHdr), e_hdr_size - sizeof(EHdr), 1, fp); } } else { //there was no ID3v2 tag.. create a dummy tag strncpy((char *)Hdr.identifier, "ID3", 3); Hdr.mjVersion = 3; Hdr.mnVersion = 0; Hdr.flags = 0x00; //the size will be updated later memset(&Hdr.size, 0 , 4); ID3_Hdr = (BYTE *)malloc(tSize); memcpy(ID3_Hdr, &Hdr, sizeof(Hdr)); int temp; //performer frame BYTE *tFrame = CreateTextFrame("TPE1",album_artist_s,temp); ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize+temp); memcpy(ID3_Hdr+tSize, tFrame, temp); tSize += temp; free(tFrame); //the title frame tFrame = CreateTextFrame("TIT2",album_name_s,temp); ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize+temp); memcpy(ID3_Hdr+tSize, tFrame, temp); tSize += temp; free(tFrame); } return ID3_Hdr; } BYTE *CreateTextFrame(const char *ident, const char *text, int &fSize) { int len = 1; if(text) len += strlen(text); fSize = 10 + len; BYTE *frame = (BYTE *)malloc(fSize); memset(frame, 0, fSize); memcpy(frame, ident, 4); if(Hdr.mjVersion <= 3) WriteInt(frame+4, fSize-10); else Write7Int(frame+4, fSize-10); if(text) memcpy(frame+11, text, len); return frame; } void SaveID3(GtkWidget *dummy, GdkEvent *dummy_event, gpointer remove) { //start collecting info about the file FILE *fp; gint pos = xmms_remote_get_playlist_pos(mp3cue.xmms_session); gchar *o_fname = xmms_remote_get_playlist_file(mp3cue.xmms_session,pos); if(!(fp=fopen(o_fname,"r+"))) { quick_message("Error opening file for modification\nCue Data cannot be updated\nPlease check file permissions."); return; } //tag will contain the entire tag, header + frames + footer BYTE *header, *other, *frames = NULL, *nLoc; int hSize, ehSize=0, tagSize, oSize, fSize=0, footSize = 0, nSize; header = CreateHeader(fp, hSize); //read all frames and padding into memory for easy manipulation tagSize = Read7Int(Hdr.size); if( (Hdr.flags >> 6) % 2) { if(Hdr.mjVersion <= 3) ehSize = ReadInt(EHdr.size) + 4; else ehSize = Read7Int(EHdr.size); } oSize = tagSize - ehSize; other = (BYTE *)malloc(oSize); fread(other, 1, oSize, fp); //seek past the footer, if present if(Hdr.mjVersion > 3 && (Hdr.flags & 0x10)) { footSize = 10; fseek(fp, 10, SEEK_CUR); } //check if we already have a tag in the system BYTE *GEOB_loc = findGEOB(other, oSize); int preSize = 0; if(remove && !GEOB_loc) { quick_message("Error: Tag does not already exist in the mp3 file.\nRemoval Failed."); free(header); if(other) free(other); fclose(fp); return; } if(GEOB_loc) { //if we do, copy all frames before the tag preSize = GEOB_loc-other; frames = (BYTE *)malloc(preSize); memcpy(frames, other, preSize); fSize = preSize; //skip this tag nLoc = GEOB_loc+10; if(Hdr.mjVersion <= 3) nLoc += ReadInt(GEOB_loc+4); else nLoc += Read7Int(GEOB_loc+4); nSize = oSize-(nLoc-other); } else { nLoc = other; nSize = oSize; } if(!remove) { string cueFrame = Make_Cue_Sheet_Frame(Hdr.mjVersion); #ifdef DEBUG cout << endl << cueFrame; printf("mp3cue Frame\n%s\n", cueFrame.data()); #endif //now copy our cue sheet tags.. woo hoo frames = (BYTE *)realloc(frames, fSize+cueFrame.length()); memcpy(frames+fSize, cueFrame.data(), cueFrame.length()); fSize += cueFrame.length(); } //now copy the stuff after the GEOB tag (or if the tag wasn't found), upto the padding //nLoc and nSize will contain the pointer and size of the remaining data to be copied //first locate the padding BYTE *padding; padding = findPadding(nLoc, nSize); //this is actually wrong, we should be going through each of the frames, //checking the preserve and discard flags, beacause their position in the file //is likely to have changed if we are updating the GEOB frame if(padding) nSize = padding-nLoc; frames = (BYTE *)realloc(frames, fSize+nSize); memcpy(frames+fSize, nLoc, nSize); fSize += nSize; //correct the sizes and perform other calculations //Is our framesize within the original size (osize ?) //If so, then we need not rewrite the file, there was sufficient padding //If not, we will add some padding and rewrite the entire file BOOL rewrite; int pSize = 0; if(fSize > oSize) { //if we exceeded the original tag size, then we create new padding pSize = fSize/2; rewrite = true; #ifdef DEBUG printf("New padding added: %d bytes\n",pSize); #endif } else { //we didn't exceed, there was padding left over (or GEOB is smaller than before) pSize = oSize - fSize; rewrite = false; #ifdef DEBUG printf("No padding added: %d bytes leftover\n",pSize); #endif } if(pSize) { padding = (BYTE *)malloc(pSize); memset(padding, 0, pSize); } //In v2.3, correct the size of the padding in the extended header, if present (what a royal pain...) if( (Hdr.mjVersion <= 3) && ((Hdr.flags >> 6) % 2) ) WriteInt(header + sizeof(Hdr) + 6, pSize); //now write to the mp3 file if(rewrite) { #ifdef DEBUG printf("Rewriting mp3 file\n"); #endif //we have to rewrite the entire file.. what fun.. //this will just shift the mp3 data down. int shift = fSize + pSize - oSize; //if we created a new header then add 10 shift if(!Read7Int(Hdr.size)) { shift += hSize; } #ifdef DEBUG printf("File shifted down by %d bytes\n",shift); #endif //the mp3 data must be shifted down by shift bytes int BSIZE = 1000000;//(1 MB) BYTE *buffer; buffer = (BYTE *)malloc(BSIZE); if(!buffer) { BSIZE = 10000; //(10KB) buffer = (BYTE *)malloc(BSIZE); } fseek(fp, 0, SEEK_END); int fileSize = ftell(fp); int rPos; for(rPos = fileSize - BSIZE; rPos >= 0; rPos-=BSIZE) { fseek(fp, rPos, SEEK_SET); fread(buffer, BSIZE, 1, fp); fseek(fp, rPos+shift, SEEK_SET); fwrite(buffer, BSIZE, 1, fp); } //one more copy, for the first BSIZE bytes, also accomodate files smaller than the buffer size rewind(fp); int read = fread(buffer, 1, rPos+BSIZE, fp); fseek(fp, shift, SEEK_SET); fwrite(buffer, read, 1, fp); free(buffer); } //correct the size of the tag in the the id3 header // tagSize = (hSize-10) + ehSize + fSize + pSize; //extended header + frames + padding sizes (we include the title in the header..) tagSize = (hSize-10) + fSize + pSize; //header (extended and TPE1 and TIT2.. if new) + frame size + padding Write7Int(&header[6], tagSize); //write the ID3v2 tag at the start of the file rewind(fp); fwrite(header, hSize, 1, fp); fwrite(frames, fSize, 1, fp); if(pSize) fwrite(padding, pSize, 1, fp); if(footSize) { //footer is the same as the header, just change the identifier memcpy(header, "3DI", 3); fwrite(header, footSize, 1, fp); } free(header); free(frames); if(other) free(other); if(padding) free(padding); fclose(fp); } //parse ID3v1 void ReadID3v1() { gint pos = xmms_remote_get_playlist_pos(mp3cue.xmms_session); gchar *o_fname = xmms_remote_get_playlist_file(mp3cue.xmms_session,pos); FILE *mp3 = fopen(o_fname,"rb"); if(!mp3) { #ifdef DEBUG printf("Error opening file %s\n",o_fname); #endif return; } fseek(mp3, 0, SEEK_END); int tloc = ftell(mp3) - 128; fseek(mp3, tloc, SEEK_SET); ID3v1 id3v1; fread(&id3v1, sizeof(id3v1), 1, mp3); if(strncmp(id3v1.tag,"TAG",3)) { #ifdef DEBUG printf("%s has no ID3v1 tag\n",o_fname); #endif fclose(mp3); return; } char artist[30]; strcpy(artist, id3v1.artist); #ifdef DEBUG printf("Title %s\nArtist %s\nAlbum %s\nYear %s\nComment %s\nGenre %d", id3v1.title, id3v1.artist, id3v1.album, id3v1.year, id3v1.comment, id3v1.genre); #endif fclose(mp3); } //Supplementary routines int Read7Int(const BYTE *pBuff) { //read the 7 bit / byte 4 byte integer BYTE size_a= *(pBuff + 0); BYTE size_b= *(pBuff + 1); BYTE size_c= *(pBuff + 2); BYTE size_d= *(pBuff + 3); int ret = size_d + (size_c << 7) + (size_b << 14) + (size_a << 21); return ret; } void Write7Int(BYTE *pBuff, int nVal) { *(pBuff + 3) = nVal % 128; *(pBuff + 2) = (nVal >> 7) % 128; *(pBuff + 1) = (nVal >> 14) % 128; *(pBuff + 0) = (nVal >> 21) % 128; } int ReadInt(const BYTE *pBuff) { BYTE size_a= *(pBuff + 0); BYTE size_b= *(pBuff + 1); BYTE size_c= *(pBuff + 2); BYTE size_d= *(pBuff + 3); int ret = size_d + (size_c << 8) + (size_b << 16) + (size_a << 24); return ret; } void WriteInt(BYTE *pBuff, int nVal) { //Intel is Little Endian.. the LSB is first MSB is last (higher order bits) //We need MSB first, so we shift.. woo hoo *(pBuff) = nVal >> 8*3; *(pBuff+1) = (nVal >> 8*2) & 0xFF; *(pBuff+2) = (nVal >> 8*1) & 0xFF; *(pBuff+3) = nVal & 0xFF; }