void Export_ID3(FILE *inf, FILE *outf) { struct ID3Tag tag={NULL,0,0,0,0,0,0.0,NULL,0,NULL,NULL,NULL,NULL}; //Validate parameters assert_wrapper((inf != NULL) && (outf != NULL)); assert_wrapper(Lyrics.piececount != 0); //This function is not to be called with an empty Lyrics structure tag.fp=inf; //Store the input file pointer into the ID3 tag structure //Seek to first MPEG frame in input file (after ID3 tag, otherwise presume it's at the beginning of the file) if(Lyrics.verbose) (void) puts("Parsing input MPEG audio file"); if(FindID3Tag(&tag)) //Find start and end of ID3 tag fseek_err(tag.fp,tag.tagend,SEEK_SET); //Seek to the first MP3 frame (immediately after the ID3 tag) else rewind_err(tag.fp); //Rewind file if no ID3 tag is found //Load MPEG information if(GetMP3FrameDuration(&tag) == 0) //Find the sample rate defined in the MP3 frame at the current file position { printf("Error loading MPEG information from file \"%s\"\nAborting",Lyrics.srcfilename); exit_wrapper(1); } //Load any existing ID3 frames (void) ID3FrameProcessor(&tag); //Build output file (void) BuildID3Tag(&tag,outf); //Release memory DestroyID3(&tag); //Release the ID3 structure's memory DestroyOmitID3framelist(Lyrics.nosrctag); //Release ID3 frame omission list }
void DisplayID3Tag(char *filename) { char *buffer=NULL; //A buffer to store strings returned from ReadTextInfoFrame() struct ID3Tag tag={NULL,0,0,0,0,0,0.0,NULL,0,NULL,NULL,NULL,NULL}; struct ID3Frame *temp=NULL; //Conductor for the ID3 frames list //Validate parameters if(!filename) return; tag.fp=fopen(filename,"rb"); if(tag.fp == NULL) return; (void) ID3FrameProcessor(&tag); if(tag.id3v1present > 1) //If the ID3v1 tag exists and is populated { (void) puts("ID3v1 tag:"); if(tag.id3v1title != NULL) printf("\tTitle = \"%s\"\n",tag.id3v1title); if(tag.id3v1artist != NULL) printf("\tArtist = \"%s\"\n",tag.id3v1artist); if(tag.id3v1album != NULL) printf("\tAlbum = \"%s\"\n",tag.id3v1album); if(tag.id3v1year != NULL) printf("\tYear = \"%s\"\n",tag.id3v1year); } //Display ID3v2 frames if(tag.frames != NULL) { (void) puts("ID3v2 tag:"); for(temp=tag.frames;temp != NULL;temp=temp->next) { //For each frame that was parsed, check the tags[] array to see if it is a text info frame assert_wrapper(temp->frameid != NULL); if(IsTextInfoID3FrameID(temp->frameid)) { fseek_err(tag.fp,temp->pos,SEEK_SET); //Seek to the ID3 frame in question buffer=ReadTextInfoFrame(tag.fp); //Read and store the text info frame content if(buffer == NULL) printf("\tFrame: %s = Unreadable or Unicode format\n",temp->frameid); else printf("\tFrame: %s = \"%s\"\n",temp->frameid,buffer); free(buffer); //Release text info frame content buffer=NULL; } else printf("\t%s frame present\n",temp->frameid); }//For each frame that was parsed } else if(!tag.id3v1present) (void) puts("No ID3 Tag detected"); DestroyID3(&tag); //Release the ID3 structure's memory (void) fclose(tag.fp); }
int ValidateID3FrameHeader(struct ID3Tag *ptr) { unsigned long position=0; unsigned char buffer[10]={0}; //Used to read the frame header unsigned long ctr=0; unsigned long framesize=0; char success=1; //Will assume success unless failure is detected //Validate input parameters if((ptr == NULL) || (ptr->fp == NULL)) return 0; //Return failure //Record file position position=ftell_err(ptr->fp); //If file position is less than the start of the ID3 tag, return failure if(position < ptr->framestart) return 0; //Return failure //Read 10 bytes into buffer if(fread(buffer,10,1,ptr->fp) != 1) //If there was a file I/O error success=0; //Record failure //Validate header: Verify that each of the first four buffered characters are an uppercase letter or numerical character if(success) { for(ctr=0;ctr<4;ctr++) { if(!isupper(buffer[ctr]) && !isdigit(buffer[ctr])) //If the character is valid for an ID3 frame ID { success=0; //Record failure break; } } } //Validate frame position: Verify that the end of the frame occurs at or before the end of the ID3 tag if(success) { framesize=((unsigned long)buffer[4]<<24) | ((unsigned long)buffer[5]<<16) | ((unsigned long)buffer[6]<<8) | ((unsigned long)buffer[7]); //Convert to 4 byte integer if(position + framesize + 10 >= ptr->tagend) //If the defined frame size would cause the frame to extend beyond the end of the ID3 tag success=0; //Record failure } //Seek to original file position fseek_err(ptr->fp,position,SEEK_SET); //Return failure/success status return success; //Return success/failure status }
int EOF_EXPORT_TO_LC(EOF_VOCAL_TRACK * tp, char *outputfilename, char *string2, int format) { unsigned long linectr = 0, lyrctr = 0, lastlyrtime = 0, linestart = 0, lineend = 0; unsigned char pitch = 0; FILE *outf = NULL; //Used to open output file FILE *pitchedlyrics = NULL; //Used to open output pitched lyric fle char *vrhythmid = NULL; EOF_PHRASE_SECTION temp; //Used to store the first lyric line in the project, which gets overridden with one covering all lyrics during RS1 export unsigned long original_lines; char *tempoutputfilename = "lyrics.temp"; eof_log("EOF_EXPORT_TO_LC() entered", 1); if((tp == NULL) || (outputfilename == NULL) || (tp->lyrics == 0)) return -1; //Return failure //Initialize variables InitLyrics(); //Initialize all variables in the Lyrics structure InitMIDI(); //Initialize all variables in the MIDI structure qsort(tp->line, (size_t)tp->lines, sizeof(EOF_PHRASE_SECTION), eof_song_qsort_phrase_sections); //Sort the lyric lines temp = tp->line[0]; //Preserve the original lyric line information original_lines = tp->lines; //Set export-specific settings if(format == SCRIPT_FORMAT) { Lyrics.grouping = 2; //Enable line grouping for script.txt export Lyrics.nohyphens = 3; //Disable hyphen output Lyrics.noplus = 1; //Disable plus output Lyrics.filter = DuplicateString("^=%#/"); //Use default filter list Lyrics.defaultfilter = 1; //Track that the above string will need to be freed } else if((format == RS_FORMAT) || (format == RS2_FORMAT)) { Lyrics.noplus = 1; //Disable plus output Lyrics.filter = DuplicateString("^=%#/"); //Use default filter list Lyrics.defaultfilter = 1; //Track that the above string will need to be freed if((format == RS_FORMAT) || (!tp->lines)) { //If exporting to Rocksmith 1 format or if the lyrics don't have any lines defined tp->lines = 0; //Temporarily disregard any existing lyric lines (void) eof_vocal_track_add_line(tp, 0, tp->lyric[tp->lyrics - 1]->pos + 1, 0xFF); //Create a single line encompassing all lyrics } } else if(format == PLAIN_FORMAT) { //This format option is meant to invoke script export with the plain flag set and filtering enabled Lyrics.nohyphens = 3; //Disable hyphen output Lyrics.noplus = 1; //Disable plus output Lyrics.filter = DuplicateString("^=%#/"); //Use default filter list Lyrics.defaultfilter = 1; //Track that the above string will need to be freed format = SCRIPT_FORMAT; Lyrics.plain = 1; Lyrics.grouping = 2; //Enable line grouping for script.txt export } //Import lyrics from EOF structure lyrctr = 0; //Begin indexing into lyrics from the very first lastlyrtime = 0; //First lyric is expected to be greater than or equal to this timestamp for(linectr = 0; linectr < (unsigned long)tp->lines; linectr++) { //For each line of lyrics in the EOF structure linestart = (tp->line[linectr]).start_pos; lineend = (tp->line[linectr]).end_pos; if(linestart > lineend) //If the line starts after it ends { ReleaseMemory(1); return -1; //Return failure } if(lyrctr < tp->lyrics) //If there are lyrics remaining CreateLyricLine(); //Initialize new line of lyrics if((tp->line[linectr]).flags & EOF_LYRIC_LINE_FLAG_OVERDRIVE) //If this line is overdrive Lyrics.overdrive_on = 1; else Lyrics.overdrive_on = 0; while(lyrctr < tp->lyrics) { //For each lyric if((tp->lyric[lyrctr])->text[0] != '\0') { //If this lyric's text isn't an empty string if((tp->lyric[lyrctr])->pos < lastlyrtime) //If this lyric precedes the previous lyric { (void) snprintf(eof_log_string, sizeof(eof_log_string) - 1, "\tLogic error while preparing lyrics for export to file \"%s\"", tempoutputfilename); eof_log(eof_log_string, 1); ReleaseMemory(1); return -1; //Return failure } if((tp->lyric[lyrctr])->pos < linestart) //If this lyric precedes the beginning of the line { (void) snprintf(eof_log_string, sizeof(eof_log_string) - 1, "\tWarning: Lyric \"%s\" at %lums is outside of defined lyric lines", tp->lyric[lyrctr]->text, tp->lyric[lyrctr]->pos); eof_log(eof_log_string, 1); CreateLyricLine(); //Initialize new line of lyrics } if((tp->lyric[lyrctr])->pos > lineend) //If this lyric is placed beyond the end of this line { break; //Break from this while loop to have another line created } pitch = (tp->lyric[lyrctr])->note; //Store the lyric's pitch if((tp->lyric[lyrctr])->note == 0) //Remap EOF's pitchless value to FLC's pitchless value pitch = PITCHLESS; if(!Lyrics.line_on) //If a lyric line is not in progress CreateLyricLine(); //Force one to be before adding the next lyric AddLyricPiece((tp->lyric[lyrctr])->text, (tp->lyric[lyrctr])->pos, (tp->lyric[lyrctr])->pos+(tp->lyric[lyrctr])->length, pitch, 0); //Add the lyric to the Lyrics structure if((Lyrics.lastpiece != NULL) && (Lyrics.lastpiece->lyric[strlen(Lyrics.lastpiece->lyric)-1] == '-')) //If the piece that was just added ended in a hyphen Lyrics.lastpiece->groupswithnext = 1; //Set its grouping status }//If this lyric's text isn't an empty string lyrctr++; //Advance to next lyric } ForceEndLyricLine(); //End the current line of lyrics } if(Lyrics.piececount == 0) //No lyrics imported { ReleaseMemory(1); return 0; //Return no lyrics found } //Load chart tags if(eof_song->tags->artist[0] != '\0') Lyrics.Artist = DuplicateString(eof_song->tags->artist); if(eof_song->tags->title[0] != '\0') Lyrics.Title = DuplicateString(eof_song->tags->title); if(eof_song->tags->frettist[0] != '\0') Lyrics.Editor = DuplicateString(eof_song->tags->frettist); if(eof_song->tags->album[0] != '\0') Lyrics.Album = DuplicateString(eof_song->tags->album); PostProcessLyrics(); //Perform hyphen and grouping validation/handling Lyrics.outfilename = tempoutputfilename; Lyrics.out_format = format; //If the export format is MIDI-based, write a MIDI file header and a MIDI track (track 0) specifying a tempo of 120BPM if((Lyrics.out_format == MIDI_FORMAT) || (Lyrics.out_format == VRHYTHM_FORMAT) || (Lyrics.out_format == SKAR_FORMAT) || (Lyrics.out_format == KAR_FORMAT)) { outf = fopen_err(Lyrics.outfilename,"wb"); //These are binary formats Write_Default_Track_Zero(outf); } //Export lyrics switch(Lyrics.out_format) { case SCRIPT_FORMAT: //Export as script.txt format file outf = fopen_err(Lyrics.outfilename,"wt"); //Script.txt is a text format Export_Script(outf); break; case VL_FORMAT: //Export as VL format file outf = fopen_err(Lyrics.outfilename,"wb"); //VL is a binary format Export_VL(outf); break; case MIDI_FORMAT: //Export as MIDI format file. Default export track is "PART VOCALS" if(string2 == NULL) //If a destination track name wasn't given Lyrics.outputtrack = DuplicateString("PART VOCALS"); //Write track name as PART VOCALS by default else Lyrics.outputtrack = DuplicateString(string2); Export_MIDI(outf); break; case USTAR_FORMAT: //Export as UltraStar format file outf = fopen_err(Lyrics.outfilename,"wt"); //UltraStar is a text format Export_UStar(outf); break; case LRC_FORMAT: //Export as simple LRC case ELRC_FORMAT: //Export as extended LRC outf = fopen_err(Lyrics.outfilename,"wt"); //LRC is a text format Export_LRC(outf); break; case VRHYTHM_FORMAT: //Export as Vocal Rhythm (MIDI and text file) if(string2 == NULL) //If a pitched lyric file wasn't given { fclose_err(outf); return -1; //Return failure } pitchedlyrics = fopen_err(string2,"wt"); //Pitched lyrics is a text format vrhythmid = DuplicateString("G4"); Export_Vrhythm(outf, pitchedlyrics, vrhythmid); fflush_err(pitchedlyrics); //Commit any pending pitched lyric writes to file fclose_err(pitchedlyrics); //Close pitched lyric file free(vrhythmid); break; case SKAR_FORMAT: //Export as Soft Karaoke. Default export track is "Words" if(string2 == NULL) //If a destination track name wasn't given Lyrics.outputtrack = DuplicateString("Words"); //Write track name as "Words" by default else Lyrics.outputtrack = DuplicateString(string2); Export_SKAR(outf); break; case KAR_FORMAT: //Export as unofficial KAR. Default export track is "Melody" if(Lyrics.outputtrack == NULL) { (void) puts("\aNo ouput track name for KAR file was given. A track named \"Melody\" will be used by default"); Lyrics.outputtrack = DuplicateString("Melody"); } Export_MIDI(outf); break; case RS_FORMAT: //Export as Rocksmith XML outf = fopen_err(Lyrics.outfilename,"wt"); //XML is a text format Lyrics.rocksmithver = 1; Export_RS(outf); break; case RS2_FORMAT: //Export as Rocksmith 2 XML outf = fopen_err(Lyrics.outfilename,"wt"); //XML is a text format Lyrics.rocksmithver = 2; Export_RS(outf); break; default: (void) puts("Unexpected error in export switch\nAborting"); exit_wrapper(4); break; } if((Lyrics.out_format == MIDI_FORMAT) || (Lyrics.out_format == VRHYTHM_FORMAT) || (Lyrics.out_format == SKAR_FORMAT) || (Lyrics.out_format == KAR_FORMAT)) { //Update the MIDI header to reflect the number of MIDI tracks written to file for all applicable export formats fseek_err(outf, 10, SEEK_SET); //The number of tracks is 10 bytes in from the start of the file header fputc_err(MIDIstruct.trackswritten>>8, outf); fputc_err(MIDIstruct.trackswritten&0xFF, outf); }
void Export_VL(FILE *outf) { struct _VLSTRUCT_ *OutVL=NULL; //The prepared VL structure to write to file struct VL_Text_entry *curtext=NULL; //Conductor for the text chunk list struct VL_Text_entry *textnext=NULL; //Used for OutVL deallocation struct VL_Sync_entry *cursync=NULL; //Conductor for the sync chunk list struct VL_Sync_entry *syncnext=NULL; //Used for OutVL deallocation unsigned long ctr=0; long filepos=0; assert_wrapper(outf != NULL); //This must not be NULL assert_wrapper(Lyrics.piececount != 0); //This function is not to be called with an empty Lyrics structure if(Lyrics.verbose) printf("\nExporting VL lyrics to file \"%s\"\n\n",Lyrics.outfilename); //Write VL header: 'V','L','2',0 fputc_err('V',outf); fputc_err('L',outf); fputc_err('2',outf); fputc_err(0,outf); //Pad the next four bytes, which are supposed to hold the size of the file, minus the size of the VL header (8 bytes) WriteDWORDLE(outf,0); if(Lyrics.verbose>=2) (void) puts("Writing text chunk"); //Write Text chunk header: 'T','E','X','T' fputc_err('T',outf); fputc_err('E',outf); fputc_err('X',outf); fputc_err('T',outf); //Pad the next four bytes, which are supposed to hold the size of the header chunk, minus the size of the chunk header (8 bytes) WriteDWORDLE(outf,0); //Convert the Offset string to centiseconds from milliseconds (as this is how the VL format interprets it) if((Lyrics.Offset != NULL)) //If there is an offset { if(strlen(Lyrics.Offset) < 2) //If the offset is 0 or 1 characters long, it will be lost, free the string { free(Lyrics.Offset); Lyrics.Offset=NULL; } else Lyrics.Offset[strlen(Lyrics.Offset)-1]='\0'; //Truncate last digit to effectively divide by ten } //Write 8 Unicode strings, for Title, Artist, Album, Editor, Offset and 3 empty strings WriteUnicodeString(outf,Lyrics.Title); WriteUnicodeString(outf,Lyrics.Artist); WriteUnicodeString(outf,Lyrics.Album); WriteUnicodeString(outf,Lyrics.Editor); WriteUnicodeString(outf,NULL); //The Offset tag should be 0 because all timestamps are written as being absolute WriteUnicodeString(outf,NULL); WriteUnicodeString(outf,NULL); WriteUnicodeString(outf,NULL); //Create sync and lyric structures OutVL=VL_PreWrite(); //Write lyric strings curtext=OutVL->Lyrics; if(Lyrics.verbose) (void) puts("Writing VL file"); while(curtext != NULL) //For each text chunk { assert_wrapper(curtext->text != NULL); if(Lyrics.verbose) printf("\tText chunk entry written: \"%s\"\n",curtext->text); WriteUnicodeString(outf,curtext->text); curtext=curtext->next; //Point to next text chunk entry } //Write padding if necessary ctr=ftell_err(outf)%4; //ctr=# of padding bytes needed to align output file position to a DWORD boundary while(ctr--) //For each byte of padding necessary fputc_err(0,outf); //Go back and write the text chunk size after the text chunk header filepos=ftell_err(outf); //Store file position OutVL->textsize=filepos-16; //File position - VL and Text header sizes = text chunk size fseek_err(outf,12,SEEK_SET); //File position 12 is where the text chunk size is to be stored WriteDWORDLE(outf,OutVL->textsize); //Write text chunk size to file fseek_err(outf,filepos,SEEK_SET); //Seek back to the end of the text chunk (where the Sync chunk will begin) if(Lyrics.verbose>=2) (void) puts("Writing Sync chunk"); //Write Sync chunk header: 'S','Y','N','C' fputc_err('S',outf); fputc_err('Y',outf); fputc_err('N',outf); fputc_err('C',outf); //Pad the next four bytes, which are supposed to hold the size of the sync chunk, minus the size of the chunk header (8 bytes) WriteDWORDLE(outf,0); //Write sync entries cursync=OutVL->Syncs; while(cursync != NULL) { WriteWORDLE(outf,cursync->lyric_number);//Write lyric line number this sync entry pertains to WriteWORDLE(outf,0); //Write reserved Word value WriteWORDLE(outf,cursync->start_char); //Write start offset WriteWORDLE(outf,cursync->end_char); //Write end offset WriteDWORDLE(outf,cursync->start_time); //Write start time WriteDWORDLE(outf,cursync->end_time); //Write end time cursync=cursync->next; } //Go back and write the sync chunk size after the sync chunk header filepos=ftell_err(outf); //Store file position OutVL->syncsize=filepos - OutVL->textsize - 24; //File position - Text chunk size - 3 header sizes = sync chunk size fseek_err(outf,OutVL->textsize+20,SEEK_SET); //File position for sync chunk size is text chunk size +2.5 headers WriteDWORDLE(outf,OutVL->syncsize); //Write sync chunk size to file //Go back and write the VL file size after the VL header, which completes the VL export filepos-=8; //Subtract the VL header size from the total file size fseek_err(outf,4,SEEK_SET); //File posistion for VL file size is after the "VL2\0" string WriteDWORDLE(outf,filepos); //Write the VL file size - header value //Destroy the OutVL structure if(Lyrics.verbose>=2) (void) puts("\tReleasing memory from export VL structure"); curtext=OutVL->Lyrics; //Point conductor at first text chunk entry while(curtext != NULL) //For each text chunk entry { textnext=curtext->next; if(curtext->text != NULL) free(curtext->text); //Free string free(curtext); //Free text chunk structure curtext=textnext; } cursync=OutVL->Syncs; //Point conductor at first sync chunk entry while(cursync != NULL) //For each sync chunk entry { syncnext=cursync->next; free(cursync); //Free sync chunk structure cursync=syncnext; } free(OutVL); if(Lyrics.verbose) printf("\nVL export complete. %lu lyrics written\n",Lyrics.piececount); }
int VL_PreLoad(FILE *inf,char validate) { long ftell_result=0; //Used to store return value from ftell() char buffer[5]= {0}; //Used to read word/doubleword integers from file unsigned long ctr=0; //Generic counter char *temp=NULL; //Temporary pointer for allocated strings struct VL_Sync_entry se= {0,0,0,0,0,NULL}; //Used to store sync entries from file during parsing struct VL_Text_entry *ptr1=NULL; //Used for allocating links for the text chunk list struct VL_Sync_entry *ptr2=NULL; //Used for allocation links for the sync chunk list struct VL_Text_entry *curtext; //Conductor for text chunk linked list struct VL_Sync_entry *cursync; //Conductor for sync chunk linked list assert_wrapper(inf != NULL); //Initialize variables VL.numlines=0; VL.numsyncs=0; VL.filesize=0; VL.textsize=0; VL.syncsize=0; VL.Lyrics=NULL; VL.Syncs=NULL; curtext=NULL; //list starts out empty cursync=NULL; //list starts out empty se.next=NULL; //In terms of linked lists, this always represents the last link if(Lyrics.verbose) (void) puts("Reading VL file header"); //Read file header fread_err(buffer,4,1,inf); //Read 4 bytes, which should be 'V','L','2',0 ReadDWORDLE(inf,&(VL.filesize)); //Read doubleword from file in little endian format if(strncmp(buffer,"VL2",4) != 0) { if(validate) return 1; else { (void) puts("Error: Invalid file header string\nAborting"); exit_wrapper(1); } } //Read text header fread_err(buffer,4,1,inf); //Read 4 bytes, which should be 'T','E','X','T' buffer[4]='\0'; //Add a NULL character to make buffer into a proper string ReadDWORDLE(inf,&(VL.textsize)); //Read doubleword from file in little endian format if(VL.textsize % 4 != 0) { (void) puts("Error: VL spec. violation: Sync chunk is not padded to a double-word alignment"); if(validate) return 2; else { (void) puts("Aborting"); exit_wrapper(2); } } if(strncmp(buffer,"TEXT",5) != 0) { (void) puts("Error: Invalid text header string"); if(validate) return 3; else { (void) puts("Aborting"); exit_wrapper(3); } } //Read sync header, which should be th.textsize+16 bytes into the file fseek_err(inf,VL.textsize+16,SEEK_SET); fread_err(buffer,4,1,inf); //Read 4 bytes, which should be 'S','Y','N','C' buffer[4]='\0'; //Add a NULL character to make buffer into a proper string ReadDWORDLE(inf,&(VL.syncsize)); //Read doubleword from file in little endian format if(strncmp(buffer,"SYNC",5) != 0) { (void) puts("Error: Invalid sync header string"); if(validate) return 4; else { (void) puts("Aborting"); exit_wrapper(4); } } //Validate chunk sizes given in headers (textsize was already validated if the Sync header was correctly read) fseek_err(inf,0,SEEK_END); //Seek to end of file ftell_result = ftell(inf); //Get position of end of file if(ftell_result < 0) { printf("Error determining file length during file size validation: %s\n",strerror(errno)); if(validate) return 5; else { (void) puts("Aborting"); exit_wrapper(5); } } if((unsigned long)ftell_result != VL.filesize+8) //Validate filesize { (void) puts("Error: Filesize does not match size given in file header"); if(validate) return 6; else { (void) puts("Aborting"); exit_wrapper(6); } } if(VL.filesize != VL.textsize + VL.syncsize + 16) //Validate syncsize { (void) puts("Error: Incorrect size given in sync chunk"); if(validate) return 7; else { (void) puts("Aborting"); exit_wrapper(7); } } //Load tags if(Lyrics.verbose) (void) puts("Loading VL tag strings"); fseek_err(inf,16,SEEK_SET); //Seek to the Title tag for(ctr=0; ctr<5; ctr++) //Expecting 5 null terminated unicode strings { temp=ReadUnicodeString(inf); //Allocate array and read Unicode string if(temp[0] != '\0') //If the string wasn't empty, save the tag switch(ctr) { //Look at ctr to determine which tag we have read, and assign to the appropriate tag string: case 0: SetTag(temp,'n',0); break; case 1: SetTag(temp,'s',0); break; case 2: SetTag(temp,'a',0); break; case 3: SetTag(temp,'e',0); break; case 4: //Offset SetTag(temp,'o',0); //Do not multiply the offset by -1 break; default: (void) puts("Unexpected error"); if(validate) return 8; else { (void) puts("Aborting"); exit_wrapper(8); } break; }//end switch(ctr) free(temp); //Free string, a copy of which would have been stored in the Lyrics structure as necessary }//end for(ctr=0;ctr<5;) //Validate the existence of three empty unicode strings for(ctr=0; ctr<3; ctr++) //Expecting 3 empty, null terminated unicode strings if(ParseUnicodeString(inf) != 0) { (void) puts("Error: Reserved string is not empty during load"); if(validate) return 9; else { (void) puts("Aborting"); exit_wrapper(9); } } //Load the lyric strings if(Lyrics.verbose) (void) puts("Loading text chunk entries"); while(1) //Read lyric lines { //Check to see if the Sync Chunk has been reached if((unsigned long)ftell_err(inf) >= VL.textsize + 16) //The Text chunk size + the file and text header sizes is the position of the Sync chunk break; //Read lyric string temp=ReadUnicodeString(inf); ftell_result=ftell_err(inf); //If this string is empty if(temp[0] == '\0') { if(VL.textsize+16 - ftell_result <= 3) //This 0 word value ends within 3 bytes of the sync header (is padding) { free(temp); //Release string as it won't be used break; //text chunk has been read } else { printf("Error: Empty lyric string detected before file position 0x%lX\n",ftell_result); free(temp); if(validate) return 10; else { (void) puts("Aborting"); exit_wrapper(10); } } } if((unsigned long)ftell_result > VL.textsize + 16) //If reading this string caused the file position to cross into Sync chunk { (void) puts("Error: Lyric string overlapped into Sync chunk"); free(temp); if(validate) return 11; else { (void) puts("Aborting"); exit_wrapper(11); } } if(Lyrics.verbose) printf("\tLoading text piece #%lu: \"%s\"\n",VL.numlines,temp); //Allocate link ptr1=malloc_err(sizeof(struct VL_Text_entry)); //Allocate memory for new link //Build link and insert into list ptr1->text=temp; ptr1->next=NULL; if(VL.Lyrics == NULL) //This is the first lyric piece read from the input file VL.Lyrics=ptr1; else { assert_wrapper(curtext != NULL); curtext->next=ptr1; //The end of the list points forward to this new link } curtext=ptr1; //This link is now at the end of the list VL.numlines++; //one more lyric string has been parsed }//end while(1) if(Lyrics.verbose) printf("%lu text chunk entries loaded\n",VL.numlines); //Load the Sync points if(Lyrics.verbose) (void) puts("Loading sync chunk entries"); fseek_err(inf,VL.textsize+16+8,SEEK_SET); //Seek to first sync entry (8 bytes past start of sync header) while(ReadSyncEntry(&se,inf) == 0) //Read all entries { if(se.start_char > se.end_char) { (void) puts("Error: Invalid sync point offset is specified to start after end offset"); if(validate) return 12; else { (void) puts("Aborting"); exit_wrapper(12); } } if(se.lyric_number!=0xFFFF && se.start_char!=0xFFFF && se.start_time!=0xFFFFFFFF) { //This is a valid sync entry //Allocate link ptr2=malloc_err(sizeof(struct VL_Sync_entry)); //Build link and insert into list memcpy(ptr2,&se,sizeof(struct VL_Sync_entry)); //copy structure into newly allocated link if(VL.Syncs == NULL) //This is the first sync piece read from the input file VL.Syncs=ptr2; else //The end of the list points forward to this new link { assert_wrapper(cursync != NULL); cursync->next=ptr2; } cursync=ptr2; //This link is now at the end of the list VL.numsyncs++; //one more sync entry has been parsed } } if(Lyrics.verbose) (void) puts("VL_PreLoad completed"); return 0; }
unsigned long BuildID3Tag(struct ID3Tag *ptr,FILE *outf) { unsigned long tagpos=0; //Records the position of the ID3 tag in the output file unsigned long framepos=0; //Records the position of the SYLT frame in the output file unsigned long ctr=0; struct ID3Frame *temp=NULL; struct Lyric_Line *curline=NULL; //Conductor of the lyric line linked list struct Lyric_Piece *curpiece=NULL; //A conductor for the lyric pieces list unsigned long framesize=0; //The calculated size of the written SYLT frame unsigned long tagsize=0; //The calculated size of the modified ID3 tag unsigned char array[4]={0}; //Used to build the 28 bit ID3 tag size unsigned char defaultID3tag[10]={'I','D','3',3,0,0,0,0,0,0}; //If the input file had no ID3 tag, this tag will be written unsigned long fileendpos=0; //Used to store the size of the input file char newline=0; //ID3 spec indicates that newline characters should be used at the beginning //of the new line instead of at the end of a line, use this to track for this //Validate input parameters if((ptr == NULL) || (ptr->fp == NULL) || (outf == NULL)) return 0; //Return failure if(Lyrics.verbose) printf("\nExporting ID3 lyrics to file \"%s\"\n",Lyrics.outfilename); //Conditionally copy the existing ID3v2 tag from the source file, or create one from scratch if(ptr->frames == NULL) { //If there was no ID3 tag in the input file if(Lyrics.verbose) (void) puts("Writing new ID3v2 tag"); tagpos=ftell_err(outf); //Record this file position so the tag size can be rewritten later fwrite_err(defaultID3tag,10,1,outf); //Write a pre-made ID3 tag //Write tag information obtained from input file if(Lyrics.Title != NULL) WriteTextInfoFrame(outf,"TIT2",Lyrics.Title); //Write song title frame if(Lyrics.Artist != NULL) WriteTextInfoFrame(outf,"TPE1",Lyrics.Artist); //Write song artist frame if(Lyrics.Album != NULL) WriteTextInfoFrame(outf,"TALB",Lyrics.Album); //Write album frame } else { //If there was an ID3v2 tag in the source file rewind_err(ptr->fp); //If the ID3v2 header isn't at the start of the source file, copy all data that precedes it to the output file BlockCopy(ptr->fp,outf,(size_t)(ptr->tagstart - ftell_err(ptr->fp))); //Copy the original ID3v2 header from source file to output file (record the file position) if(Lyrics.verbose) (void) puts("Copying ID3v2 tag header"); tagpos=ftell_err(outf); //Record this file position so the tag size can be rewritten later BlockCopy(ptr->fp,outf,(size_t)(ptr->framestart - ftell_err(ptr->fp))); //Write tag information from input file if applicable, and ensure that equivalent ID3v2 frames from the source file are omitted if(Lyrics.Title != NULL) { WriteTextInfoFrame(outf,"TIT2",Lyrics.Title); //Write song title frame Lyrics.nosrctag=AddOmitID3framelist(Lyrics.nosrctag,"TIT2"); //Linked list create/append to omit source song title frame } if(Lyrics.Artist != NULL) { WriteTextInfoFrame(outf,"TPE1",Lyrics.Artist); //Write song artist frame Lyrics.nosrctag=AddOmitID3framelist(Lyrics.nosrctag,"TPE1"); //Linked list create/append to omit source song artist frame } if(Lyrics.Album != NULL) { WriteTextInfoFrame(outf,"TALB",Lyrics.Album); //Write album frame Lyrics.nosrctag=AddOmitID3framelist(Lyrics.nosrctag,"TALB"); //Linked list create/append to omit source album frame } if(Lyrics.Year != NULL) { WriteTextInfoFrame(outf,"TYER",Lyrics.Year); //Write year frame Lyrics.nosrctag=AddOmitID3framelist(Lyrics.nosrctag,"TYER"); //Linked list create/append to omit source year frame } //Omit any existing SYLT frame from the source file Lyrics.nosrctag=AddOmitID3framelist(Lyrics.nosrctag,"SYLT"); //Linked list create/append with SYLT frame ID to ensure SYLT source frame is omitted //Write all frames from source MP3 to export MP3 except for those in the omit list for(temp=ptr->frames;temp!=NULL;temp=temp->next) { //For each ID3Frame in the list if(SearchOmitID3framelist(Lyrics.nosrctag,temp->frameid) == 0) //If the source frame isn't to be omitted { if(Lyrics.verbose >= 2) printf("\tCopying frame \"%s\"\n",temp->frameid); if((unsigned long)ftell_err(ptr->fp) != temp->pos) //If the input file isn't already positioned at the frame fseek_err(ptr->fp,temp->pos,SEEK_SET); //Seek to it now BlockCopy(ptr->fp,outf,(size_t)temp->length + 10); //Copy frame body size + header size number of bytes ctr++; //Increment counter } else if(Lyrics.verbose >= 2) printf("\tOmitting \"%s\" frame from source file\n",temp->frameid); } }//If there was an ID3 tag in the input file if(Lyrics.verbose) (void) puts("Writing SYLT frame header"); //Write SYLT frame header framepos=ftell_err(outf); //Record this file position so the frame size can be rewritten later fputs_err("SYLTxxxx\xC0",outf); //Write the Frame ID, 4 dummy bytes for the unknown frame size and the first flag byte (preserve frame for both tag and file alteration) fputc_err(0,outf); //Write second flag byte (no compression, encryption or grouping identity) fputc_err(0,outf); //ASCII encoding fputs_err("eng\x02\x01",outf); //Write the language as "English", the timestamp format as milliseconds and the content type as "lyrics" fputs_err(PROGVERSION,outf); //Embed the program version as the content descriptor fputc_err(0,outf); //Write NULL terminator for content descriptor //Write SYLT frame using the Lyrics structure curline=Lyrics.lines; //Point lyric line conductor to first line of lyrics if(Lyrics.verbose) (void) puts("Writing SYLT lyrics"); while(curline != NULL) //For each line of lyrics { curpiece=curline->pieces; //Starting with the first piece of lyric in this line while(curpiece != NULL) //For each piece of lyric in this line { if(newline) //If the previous lyric was the last in its line { fputc_err('\n',outf); //Append a newline character in front of this lyric entry newline=0; //Reset this status } fputs_err(curpiece->lyric,outf); //Write the lyric if(Lyrics.verbose >= 2) printf("\t\"%s\"\tstart=%lu\t",curpiece->lyric,curpiece->start); //Line/word grouping logic if(curpiece->next == NULL) //If this is the last lyric in the line { newline=1; if(Lyrics.verbose >= 2) printf("(newline)"); } else if(!curpiece->groupswithnext) //Otherwise, if this lyric does not group with the next { fputc_err(' ',outf); //Append a space if(Lyrics.verbose >= 2) printf("(whitespace)"); } if(Lyrics.verbose >= 2) (void) putchar('\n'); fputc_err(0,outf); //Write a NULL terminator WriteDWORDBE(outf,curpiece->start); //Write the lyric's timestamp as a big endian value curpiece=curpiece->next; //Point to next lyric in the line } curline=curline->next; //Point to next line of lyrics } framesize=ftell_err(outf)-framepos-10; //Find the length of the SYLT frame that was written (minus frame header size) ctr++; //Increment counter if(ptr->frames != NULL) { //If the input MP3 had an ID3 tag //Copy any unidentified data (ie. padding) that occurs between the end of the last ID3 frame in the list and the defined end of the ID3 tag for(temp=ptr->frames;temp->next!=NULL;temp=temp->next); //Seek to last frame link fseek_err(ptr->fp,temp->pos + temp->length + 10,SEEK_SET); //Seek to one byte past the end of the frame BlockCopy(ptr->fp,outf,(size_t)(ptr->tagend - ftell_err(ptr->fp))); } tagsize=ftell_err(outf)-tagpos-10; //Find the length of the ID3 tag that has been written (minus tag header size) if(Lyrics.verbose) (void) puts("Copying audio data"); fileendpos=GetFileEndPos(ptr->fp); //Find the position of the last byte in the input MP3 file (the filesize) if(ptr->id3v1present && SearchOmitID3framelist(Lyrics.nosrctag,"*")) //If the user specified to leave off the ID3v1 tag, and the source MP3 has an ID3 tag { BlockCopy(ptr->fp,outf,(size_t)(fileendpos - ftell_err(ptr->fp) - 128)); //Copy rest of source file to output file (minus 128 bytes, the size of the ID3v1 tag) ptr->id3v1present=0; //Consider the tag as being removed, so a new ID3v1 tag is written below } else BlockCopy(ptr->fp,outf,(size_t)(fileendpos - ftell_err(ptr->fp))); //Copy rest of source file to output file //Write/Overwrite ID3v1 tag if(ptr->id3v1present) //If an ID3v1 tag existed in the source MP3 { //Overwrite it with tags from the input file if(Lyrics.verbose) (void) puts("Editing ID3v1 tag"); fseek_err(outf,-125,SEEK_CUR); //Seek 125 bytes back, where the first field of this tag should exist if(Lyrics.Title != NULL) //If the input file defined a Title WritePaddedString(outf,Lyrics.Title,30,0); //Overwrite the Title field (30 bytes) else fseek_err(outf,30,SEEK_CUR); //Otherwise seek 30 bytes ahead to the next field if(Lyrics.Artist != NULL) //If the input file defined an Artist WritePaddedString(outf,Lyrics.Artist,30,0); //Overwrite the Artist field (30 bytes) else fseek_err(outf,30,SEEK_CUR); //Otherwise seek 30 bytes ahead to the next field if(Lyrics.Album != NULL) //If the input file defined an Album WritePaddedString(outf,Lyrics.Album,30,0); //Overwrite the Album field (30 bytes) else fseek_err(outf,30,SEEK_CUR); //Otherwise seek 30 bytes ahead to the next field if(Lyrics.Year != NULL) //If the input file defined a Year WritePaddedString(outf,Lyrics.Year,4,0); //Overwrite the Year field (4 bytes) } else { //Write a new ID3v1 tag if(Lyrics.verbose) (void) puts("Writing new ID3v1 tag"); fseek_err(outf,0,SEEK_END); //Seek to end of file fputs_err("TAG",outf); //Write ID3v1 header WritePaddedString(outf,Lyrics.Title,30,0); //Write the Title field (30 bytes) WritePaddedString(outf,Lyrics.Artist,30,0); //Write the Artist field (30 bytes) WritePaddedString(outf,Lyrics.Album,30,0); //Write the Album field (30 bytes) WritePaddedString(outf,ptr->id3v1year,4,0); //Write the Year field (4 bytes) WritePaddedString(outf,NULL,30,0); //Write a blank Comment field (30 bytes) fputc_err(255,outf); //Write unknown genre (1 byte) } if(Lyrics.verbose) (void) puts("Correcting ID3 headers"); //Rewind to the SYLT header in the output file and write the correct frame length fseek_err(outf,framepos+4,SEEK_SET); //Seek to where the SYLT frame size is to be written WriteDWORDBE(outf,framesize); //Write the SYLT frame size //Rewind to the ID3 header in the output file and write the correct ID3 tag length fseek_err(outf,ptr->tagstart+6,SEEK_SET); //Seek to where the ID3 tag size is to be written if(tagsize > 0x0FFFFFFF) { (void) puts("\aError: Tag size is larger than the ID3 specification allows"); exit_wrapper(2); } array[3]=tagsize & 127; //Mask out everything except the lowest 7 bits array[2]=(tagsize>>7) & 127; //Mask out everything except the 2nd set of 7 bits array[1]=(tagsize>>14) & 127; //Mask out everything except the 3rd set of 7 bits array[0]=(tagsize>>21) & 127; //Mask out everything except the 4th set of 7 bits fwrite_err(array,4,1,outf); //Write the ID3 tag size if(Lyrics.verbose) printf("\nID3 export complete. %lu lyrics written\n",Lyrics.piececount); return ctr; //Return counter }
unsigned long ID3FrameProcessor(struct ID3Tag *ptr) { unsigned char header[10]={0}; //Used to store the ID3 frame header unsigned long filepos=0; int ctr2=0; //Used to validate frame ID struct ID3Frame *temp=NULL; //Used to allocate an ID3Frame link struct ID3Frame *head=NULL; //Used as the head pointer struct ID3Frame *cond=NULL; //Used as the conductor unsigned long ctr=0; //Count the number of frames processed unsigned long framesize=0; //Used to validate frame size char buffer[31]={0}; //Used to read an ID3v1 tag in the absence of an ID3v2 tag static struct ID3Frame emptyID3Frame; //Auto-initialize all members to 0/NULL //Validate input parameter if((ptr == NULL) || (ptr->fp == NULL)) return 0; //Return failure //Find and parse the ID3 header //ID3v1 fseek_err(ptr->fp,-128,SEEK_END); //Seek 128 back from the end of the file, where this tag would exist fread_err(buffer,3,1,ptr->fp); //Read 3 bytes for the "TAG" header buffer[3] = '\0'; //Ensure NULL termination if(strcasecmp(buffer,"TAG") == 0) //If this is an ID3v1 header { if(Lyrics.verbose) (void) puts("Loading ID3v1 tag"); ptr->id3v1present=1; //Track that this tag exists fread_err(buffer,30,1,ptr->fp); //Read the first field in the tag (song title) buffer[30] = '\0'; //Ensure NULL termination if(buffer[0] != '\0') //If the string isn't empty { ptr->id3v1present=2; //Track that this tag is populated ptr->id3v1title=DuplicateString(buffer); if(Lyrics.verbose) (void) puts("\tTitle loaded"); } fread_err(buffer,30,1,ptr->fp); //Read the second field in the tag (artist) buffer[30] = '\0'; //Ensure NULL termination if(buffer[0] != '\0') //If the string isn't empty { ptr->id3v1present=2; //Track that this tag is populated ptr->id3v1artist=DuplicateString(buffer); if(Lyrics.verbose) (void) puts("\tArtist loaded"); } fread_err(buffer,30,1,ptr->fp); //Read the third field in the tag (album) buffer[30] = '\0'; //Ensure NULL termination if(buffer[0] != '\0') //If the string isn't empty { ptr->id3v1present=2; //Track that this tag is populated ptr->id3v1album=DuplicateString(buffer); if(Lyrics.verbose) (void) puts("\tAlbum loaded"); } fread_err(buffer,4,1,ptr->fp); //Read the fourth field in the tag (year) buffer[4]='\0'; //Terminate the buffer to make it a string if(buffer[0] != '\0') //If the string isn't empty { ptr->id3v1present=2; //Track that this tag is populated ptr->id3v1year=DuplicateString(buffer); if(Lyrics.verbose) (void) puts("\tYear loaded"); } } //ID3v2 //Load ID3 frames into linked list rewind_err(ptr->fp); //Rewind file so that the ID3v2 header can be searched for if(FindID3Tag(ptr) == 0) //Locate the ID3 tag return 0; //Return if there was none if(Lyrics.verbose) (void) puts("Loading ID3v2 tag"); fseek_err(ptr->fp,ptr->framestart,SEEK_SET); //Seek to first ID3 frame filepos=ftell_err(ptr->fp); //Record file position of first expected ID3 frame while((filepos >= ptr->framestart) && (filepos < ptr->tagend)) { //While file position is at or after the end of the ID3 header, before or at end of the ID3 tag //Read frame header into buffer if(fread(header,10,1,ptr->fp) != 1) { ptr->frames=head; //Store the linked list into the ID3 tag structure return 0; //Return failure on I/O error } //Validate frame ID for(ctr2=0;ctr2<4;ctr2++) { if(!isupper(header[ctr2]) && !isdigit(header[ctr2])) //If the character is NOT valid for an ID3 frame ID { ptr->frames=head; //Store the linked list into the ID3 tag structure return ctr; //Return the number of frames that were already processed } } //Validate frame end framesize=((unsigned long)header[4]<<24) | ((unsigned long)header[5]<<16) | ((unsigned long)header[6]<<8) | ((unsigned long)header[7]); //Convert to 4 byte integer if(filepos + framesize + 10 > ptr->tagend) //If the defined frame size would cause the frame to extend beyond the end of the ID3 tag { ptr->frames=head; //Store the linked list into the ID3 tag structure return 0; //Return failure } //Initialize an ID3Frame structure temp=malloc_err(sizeof(struct ID3Frame)); //Allocate memory *temp=emptyID3Frame; //Reliably initialize all values to 0/NULL temp->frameid=malloc_err(5); //Allocate 5 bytes for the ID3 frame ID memcpy(temp->frameid,header,4); //Copy the 4 byte ID3 frame ID into the pre-terminated string temp->frameid[4]='\0'; //Terminate the string temp->pos=filepos; //Record the file position of the ID3 frame temp->length=((unsigned long)header[4]<<24) | ((unsigned long)header[5]<<16) | ((unsigned long)header[6]<<8) | ((unsigned long)header[7]); //Record the frame length (defined in header as 4 byte Big Endian integer) //Append ID3Frame link to the list if(head == NULL) //Empty list { head=temp; //Point head to new link cond=temp; //Point conductor to new link } else { assert_wrapper(cond != NULL); //The linked list is expected to not be corrupted cond->next=temp; //Conductor points forward to new link temp->prev=cond; //New link points back to conductor cond=temp; //Conductor points to new link } if(Lyrics.verbose >= 2) printf("\tFrame ID %s loaded\n",temp->frameid); ctr++; //Iterate counter (void) fseek(ptr->fp,framesize,SEEK_CUR); //Seek ahead to the beginning of the next ID3 frame filepos+=framesize + 10; //Update file position }//While file position is at or after the end of the ID3 header, before or at end of the ID3 tag if(Lyrics.verbose) printf("%lu ID3 frames loaded\n\n",ctr); ptr->frames=head; //Store the linked list into the ID3 tag structure return ctr; //Return the number of frames loaded }
void ID3_Load(FILE *inf) { //Parses the file looking for an ID3 tag. If found, the first MP3 frame is examined to obtain the sample rate //Then an SYLT frame is searched for within the ID3 tag. If found, synchronized lyrics are imported struct ID3Tag tag={NULL,0,0,0,0,0,0.0,NULL,0,NULL,NULL,NULL,NULL}; struct ID3Frame *frameptr=NULL; struct ID3Frame *frameptr2=NULL; assert_wrapper(inf != NULL); //This must not be NULL tag.fp=inf; if(Lyrics.verbose) printf("Importing ID3 lyrics from file \"%s\"\n\nParsing input MPEG audio file\n",Lyrics.infilename); if(ID3FrameProcessor(&tag) == 0) //Build a list of the ID3 frames { DestroyID3(&tag); //Release the ID3 structure's memory return; //Return if no frames were successfully parsed } //Parse MPEG frame header fseek_err(tag.fp,tag.tagend,SEEK_SET); //Seek to the first MP3 frame (immediately after the ID3 tag) if(GetMP3FrameDuration(&tag) == 0) //Find the sample rate defined in the MP3 frame { DestroyID3(&tag); //Release the ID3 structure's memory return; //Return if the sample rate was not found or was invalid } frameptr=FindID3Frame(&tag,"SYLT"); //Search ID3 frame list for SYLT ID3 frame frameptr2=FindID3Frame(&tag,"USLT"); //Search ID3 frame list for USLT ID3 frame if(frameptr != NULL) { //Perform Synchronized ID3 Lyric import fseek_err(tag.fp,frameptr->pos,SEEK_SET); //Seek to SYLT ID3 frame SYLT_Parse(&tag); } else if(frameptr2 != NULL) { //Perform Unsynchronized ID3 Lyric import (void) puts("\aUnsynchronized ID3 lyric import currently not supported"); exit_wrapper(1); } else { DestroyID3(&tag); //Release the ID3 structure's memory return; //Return if neither lyric frame is present } //Load song tags Lyrics.Title=GrabID3TextFrame(&tag,"TIT2",NULL,0); //Return the defined song title, if it exists Lyrics.Artist=GrabID3TextFrame(&tag,"TPE1",NULL,0); //Return the defined artist, if it exists Lyrics.Album=GrabID3TextFrame(&tag,"TALB",NULL,0); //Return the defined album, if it exists Lyrics.Year=GrabID3TextFrame(&tag,"TYER",NULL,0); //Return the defined year, if it exists if(Lyrics.Title == NULL) //If there was no Title defined in the ID3v2 tag Lyrics.Title=DuplicateString(tag.id3v1title); //Use one defined in the ID3v1 tag if it exists if(Lyrics.Artist == NULL) //If there was no Artist defined in the ID3v2 tag Lyrics.Artist=DuplicateString(tag.id3v1artist); //Use one defined in the ID3v1 tag if it exists if(Lyrics.Album == NULL) //If there was no Album defined in the ID3v2 tag Lyrics.Album=DuplicateString(tag.id3v1album); //Use one defined in the ID3v1 tag if it exists if(Lyrics.Year == NULL) //If there was no Year defined in the ID3v2 tag Lyrics.Year=DuplicateString(tag.id3v1year); //Use one defined in the ID3v1 tag if it exists ForceEndLyricLine(); DestroyID3(&tag); //Release the ID3 structure's memory if(Lyrics.verbose) printf("ID3 import complete. %lu lyrics loaded\n\n",Lyrics.piececount); }