//////////////////////////////////////////////////////////////////////////// // **** WAVPACK **** // // Hybrid Lossless Wavefile Compressor // // Copyright (c) 1998 - 2005 Conifer Software. // // All Rights Reserved. // // Distributed under the BSD Software License (see license.txt) // //////////////////////////////////////////////////////////////////////////// // wvunpack.c // This is the main module for the WavPack command-line decompressor. #if defined(WIN32) #include #include #else #include #include #if defined (__GNUC__) #include #include #endif #endif #ifdef __BORLANDC__ #include #elif defined(__GNUC__) && !defined(WIN32) #include #else #include #endif #include #include #include #include #include "wavpack.h" #include "md5.h" #ifdef DEBUG_ALLOC #define malloc malloc_db #define realloc realloc_db #define free free_db void *malloc_db (uint32_t size); void *realloc_db (void *ptr, uint32_t size); void free_db (void *ptr); int32_t dump_alloc (void); static char *strdup (const char *s) { char *d = malloc (strlen (s) + 1); return strcpy (d, s); } #endif ///////////////////////////// local variable storage ////////////////////////// static const char *sign_on = "\n" " WVUNPACK Hybrid Lossless Wavefile Decompressor %s Version %s %s\n" " Copyright (c) 1998 - 2005 Conifer Software. All Rights Reserved.\n\n"; static const char *usage = " Usage: WVUNPACK [-options] [@]infile[.wv]|- [[@]outfile[.wav]|outpath|-]\n" " (infile may contain wildcards: ?,*)\n\n" " Options: -d = delete source file if successful (use with caution!)\n" " -i = ignore .wvc file (forces hybrid lossy decompression)\n" " -m = calculate and display MD5 signature; verify if lossless\n" " -q = quiet (keep console output to a minimum)\n" " -r = force raw audio decode (skip RIFF headers & trailers)\n" " -s = display summary information only to stdout (no decode)\n" #if defined (WIN32) " -t = copy input file's time stamp to output file(s)\n" #endif " -v = verify source data only (no output file created)\n" " -y = yes to overwrite warning (use with caution!)\n\n" " Web: Visit www.wavpack.com for latest version and info\n"; static char overwrite_all = 0, delete_source = 0, raw_decode = 0, summary = 0, ignore_wvc = 0, quiet_mode = 0, calc_md5 = 0, copy_time = 0; static int num_files, file_index, outbuf_k; /////////////////////////// local function declarations /////////////////////// static int unpack_file (char *infilename, char *outfilename); static void display_progress (double file_progress); #define NO_ERROR 0L #define SOFT_ERROR 1 #define HARD_ERROR 2 ////////////////////////////////////////////////////////////////////////////// // The "main" function for the command-line WavPack decompressor. // ////////////////////////////////////////////////////////////////////////////// int main (argc, argv) int argc; char **argv; { int verify_only = 0, usage_error = 0, filelist = 0, add_extension = 0; char *infilename = NULL, *outfilename = NULL; char outpath, **matches = NULL; int result, i; #ifdef __BORLANDC__ struct ffblk ffblk; #elif defined(WIN32) struct _finddata_t _finddata_t; #else glob_t globs; struct stat fstats; #endif // loop through command-line arguments while (--argc) { #if defined (WIN32) if ((**++argv == '-' || **argv == '/') && (*argv)[1]) #else if ((**++argv == '-') && (*argv)[1]) #endif while (*++*argv) switch (**argv) { case 'Y': case 'y': overwrite_all = 1; break; case 'D': case 'd': delete_source = 1; break; #if defined (WIN32) case 'T': case 't': copy_time = 1; break; #endif case 'V': case 'v': verify_only = 1; break; case 'S': case 's': summary = 1; break; case 'K': case 'k': outbuf_k = strtol (++*argv, argv, 10); --*argv; break; case 'M': case 'm': calc_md5 = 1; break; case 'R': case 'r': raw_decode = 1; break; case 'Q': case 'q': quiet_mode = 1; break; case 'I': case 'i': ignore_wvc = 1; break; default: error_line ("illegal option: %c !", **argv); usage_error = 1; } else { if (!infilename) { infilename = malloc (strlen (*argv) + PATH_MAX); strcpy (infilename, *argv); } else if (!outfilename) { outfilename = malloc (strlen (*argv) + PATH_MAX); strcpy (outfilename, *argv); } else { error_line ("extra unknown argument: %s !", *argv); usage_error = 1; } } } // check for various command-line argument problems if (verify_only && delete_source) { error_line ("can't delete in verify mode!"); delete_source = 0; } if (verify_only && outfilename) { error_line ("outfile specification and verify mode are incompatible!"); usage_error = 1; } if (!quiet_mode && !usage_error) fprintf (stderr, sign_on, VERSION_OS, VERSION_STR, DATE_STR); if (!infilename) { printf ("%s", usage); return 0; } if (usage_error) { free (infilename); return 0; } setup_break (); // If the infile specification begins with a '@', then it actually points // to a file that contains the names of the files to be converted. This // was included for use by Wim Speekenbrink's frontends, but could be used // for other purposes. if (infilename [0] == '@') { FILE *list = fopen (infilename+1, "rt"); int c; if (list == NULL) { error_line ("file %s not found!", infilename+1); free(infilename); return 1; } while ((c = getc (list)) != EOF) { while (c == '\n') c = getc (list); if (c != EOF) { char *fname = malloc (PATH_MAX); int ci = 0; do fname [ci++] = c; while ((c = getc (list)) != '\n' && c != EOF && ci < PATH_MAX); fname [ci++] = '\0'; matches = realloc (matches, ++num_files * sizeof (*matches)); matches [num_files - 1] = realloc (fname, ci); } } fclose (list); free (infilename); infilename = NULL; filelist = 1; } else if (*infilename != '-') { // skip this if infile is stdin (-) if (!filespec_ext (infilename)) strcat (infilename, ".wv"); #ifdef NO_WILDCARDS matches = malloc (sizeof (*matches)); matches [num_files++] = infilename; filelist = 1; #else // search for and store any filenames that match the user supplied spec #ifdef __BORLANDC__ if (findfirst (infilename, &ffblk, 0) == 0) { do { matches = realloc (matches, ++num_files * sizeof (*matches)); matches [num_files - 1] = strdup (ffblk.ff_name); } while (findnext (&ffblk) == 0); } #elif defined (WIN32) if ((i = _findfirst (infilename, &_finddata_t)) != -1L) { do { if (!(_finddata_t.attrib & _A_SUBDIR)) { matches = realloc (matches, ++num_files * sizeof (*matches)); matches [num_files - 1] = strdup (_finddata_t.name); } } while (_findnext (i, &_finddata_t) == 0); _findclose (i); } #else i = 0; if (glob(infilename, 0, NULL, &globs) == 0 && globs.gl_pathc > 0) { do { if (stat(globs.gl_pathv[i], &fstats) == 0 && !(fstats.st_mode & S_IFDIR)) { matches = realloc (matches, ++num_files * sizeof (*matches)); matches [num_files - 1] = strdup (globs.gl_pathv[i]); } } while (i++ < globs.gl_pathc); } globfree(&globs); #endif #endif } else { // handle case of stdin (-) matches = malloc (sizeof (*matches)); matches [num_files++] = infilename; } // If the outfile specification begins with a '@', then it actually points // to a file that contains the output specification. This was included for // use by Wim Speekenbrink's frontends because certain filenames could not // be passed on the command-line, but could be used for other purposes. if (outfilename && outfilename [0] == '@') { FILE *list = fopen (outfilename+1, "rt"); int c; if (list == NULL) { error_line ("file %s not found!", outfilename+1); free(outfilename); return 1; } while ((c = getc (list)) == '\n'); if (c != EOF) { int ci = 0; do outfilename [ci++] = c; while ((c = getc (list)) != '\n' && c != EOF && ci < PATH_MAX); outfilename [ci] = '\0'; } else { error_line ("output spec file is empty!"); free(outfilename); fclose (list); return 1; } fclose (list); } // if we found any files to process, this is where we start if (num_files) { int soft_errors = 0; if (outfilename && *outfilename != '-') { outpath = (filespec_path (outfilename) != NULL); if (num_files > 1 && !outpath) { error_line ("%s is not a valid output path", outfilename); free(outfilename); return 1; } } else outpath = 0; add_extension = !outfilename || outpath || !filespec_ext (outfilename); // loop through and process files in list for (file_index = 0; file_index < num_files; ++file_index) { if (check_break ()) break; // get input filename from list if (filelist) infilename = matches [file_index]; else if (*infilename != '-') { *filespec_name (infilename) = '\0'; strcat (infilename, matches [file_index]); } // generate output filename if (outpath) { strcat (outfilename, filespec_name (matches [file_index])); if (filespec_ext (outfilename)) *filespec_ext (outfilename) = '\0'; } else if (!outfilename) { outfilename = malloc (strlen (infilename) + 10); strcpy (outfilename, infilename); if (filespec_ext (outfilename)) *filespec_ext (outfilename) = '\0'; } if (outfilename && *outfilename != '-' && add_extension) strcat (outfilename, raw_decode ? ".raw" : ".wav"); if (num_files > 1) fprintf (stderr, "\n%s:\n", infilename); result = unpack_file (infilename, verify_only ? NULL : outfilename); if (result == HARD_ERROR) break; else if (result == SOFT_ERROR) ++soft_errors; // clean up in preparation for potentially another file if (outpath) *filespec_name (outfilename) = '\0'; else if (*outfilename != '-') { free (outfilename); outfilename = NULL; } free (matches [file_index]); } if (num_files > 1) { if (soft_errors) fprintf (stderr, "\n **** warning: errors occurred in %d of %d files! ****\n", soft_errors, num_files); else if (!quiet_mode) fprintf (stderr, "\n **** %d files successfully processed ****\n", num_files); } free (matches); } else error_line (filespec_wild (infilename) ? "nothing to do!" : "file %s not found!", infilename); if (outfilename) free(outfilename); #ifdef DEBUG_ALLOC error_line ("malloc_count = %d", dump_alloc ()); #endif return 0; } // Unpack the specified WavPack input file into the specified output file name. // This function uses the library routines provided in wputils.c to do all // unpacking. This function takes care of reformatting the data (which is // returned in native-endian longs) to the standard little-endian format. This // function also handles optionally calculating and displaying the MD5 sum of // the resulting audio data and verifying the sum if a sum was stored in the // source and lossless compression is used. static uchar *format_samples (int bps, uchar *dst, int32_t *src, uint32_t samcnt); static void dump_summary (WavpackContext *wpc, char *name, FILE *dst); extern int delta_blocks [8]; static int unpack_file (char *infilename, char *outfilename) { int result = NO_ERROR, md5_diff = FALSE, open_flags = 0, bytes_per_sample, num_channels, wvc_mode, bps; uint32_t outfile_length, output_buffer_size, bcount, total_unpacked_samples = 0; uchar *output_buffer = NULL, *output_pointer = NULL; double dtime, progress = -1.0; MD5_CTX md5_context; WavpackContext *wpc; int32_t *temp_buffer; char error [80]; FILE *outfile; #ifdef __BORLANDC__ struct time time1, time2; #elif defined(WIN32) struct _timeb time1, time2; #else struct timeval time1, time2; struct timezone timez; #endif // use library to open WavPack file if (outfilename && !raw_decode) open_flags |= OPEN_WRAPPER; if (raw_decode) open_flags |= OPEN_STREAMING; if (!ignore_wvc) open_flags |= OPEN_WVC; wpc = WavpackOpenFileInput (infilename, error, open_flags, 0); if (!wpc) { error_line (error); return SOFT_ERROR; } if (calc_md5) MD5Init (&md5_context); wvc_mode = WavpackGetMode (wpc) & MODE_WVC; num_channels = WavpackGetNumChannels (wpc); bps = WavpackGetBytesPerSample (wpc); bytes_per_sample = num_channels * bps; if (summary) { dump_summary (wpc, infilename, stdout); WavpackCloseFile (wpc); return NO_ERROR; } if (outfilename) { if (*outfilename != '-') { // check the output file for overwrite warning required if (!overwrite_all && (outfile = fopen (outfilename, "rb")) != NULL) { DoCloseHandle (outfile); fprintf (stderr, "overwrite %s (yes/no/all)? ", FN_FIT (outfilename)); SetConsoleTitle ("overwrite?"); switch (yna ()) { case 'n': result = SOFT_ERROR; break; case 'a': overwrite_all = 1; } if (result != NO_ERROR) { WavpackCloseFile (wpc); return result; } } // open output file for writing if ((outfile = fopen (outfilename, "wb")) == NULL) { error_line ("can't create file %s!", outfilename); WavpackCloseFile (wpc); return SOFT_ERROR; } else if (!quiet_mode) fprintf (stderr, "restoring %s,", FN_FIT (outfilename)); } else { // come here to open stdout as destination outfile = stdout; #if defined(WIN32) setmode (fileno (stdout), O_BINARY); #endif if (!quiet_mode) fprintf (stderr, "unpacking %s%s to stdout,", *infilename == '-' ? "stdin" : FN_FIT (infilename), wvc_mode ? " (+.wvc)" : ""); } if (outbuf_k) output_buffer_size = outbuf_k * 1024; else output_buffer_size = 1024 * 256; output_pointer = output_buffer = malloc (output_buffer_size); } else { // in verify only mode we don't worry about headers outfile = NULL; if (!quiet_mode) fprintf (stderr, "verifying %s%s,", *infilename == '-' ? "stdin" : FN_FIT (infilename), wvc_mode ? " (+.wvc)" : ""); } #ifdef __BORLANDC__ gettime (&time1); #elif defined(WIN32) _ftime (&time1); #else gettimeofday(&time1,&timez); #endif if (WavpackGetWrapperBytes (wpc)) { if (outfile && (!DoWriteFile (outfile, WavpackGetWrapperData (wpc), WavpackGetWrapperBytes (wpc), &bcount) || bcount != WavpackGetWrapperBytes (wpc))) { error_line ("can't write .WAV data, disk probably full!"); DoTruncateFile (outfile); result = HARD_ERROR; } WavpackFreeWrapper (wpc); } temp_buffer = malloc (4096L * num_channels * 4); while (result == NO_ERROR) { uint32_t samples_to_unpack, samples_unpacked; if (output_buffer) { samples_to_unpack = (output_buffer_size - (output_pointer - output_buffer)) / bytes_per_sample; if (samples_to_unpack > 4096) samples_to_unpack = 4096; } else samples_to_unpack = 4096; samples_unpacked = WavpackUnpackSamples (wpc, temp_buffer, samples_to_unpack); total_unpacked_samples += samples_unpacked; if (output_buffer) { if (samples_unpacked) output_pointer = format_samples (bps, output_pointer, temp_buffer, samples_unpacked * num_channels); if (!samples_unpacked || (output_buffer_size - (output_pointer - output_buffer)) < bytes_per_sample) { if (!DoWriteFile (outfile, output_buffer, output_pointer - output_buffer, &bcount) || bcount != output_pointer - output_buffer) { error_line ("can't write .WAV data, disk probably full!"); DoTruncateFile (outfile); result = HARD_ERROR; break; } output_pointer = output_buffer; } } if (calc_md5 && samples_unpacked) { format_samples (bps, (uchar *) temp_buffer, temp_buffer, samples_unpacked * num_channels); MD5Update (&md5_context, temp_buffer, bps * samples_unpacked * num_channels); } if (!samples_unpacked) break; if (check_break ()) { fprintf (stderr, "^C\n"); DoTruncateFile (outfile); result = SOFT_ERROR; break; } if (WavpackGetProgress (wpc) != -1.0 && progress != floor (WavpackGetProgress (wpc) * 100.0 + 0.5)) { int nobs = progress == -1.0; progress = WavpackGetProgress (wpc); display_progress (progress); progress = floor (progress * 100.0 + 0.5); if (!quiet_mode) fprintf (stderr, "%s%3d%% done...", nobs ? " " : "\b\b\b\b\b\b\b\b\b\b\b\b", (int) progress); } } free (temp_buffer); if (output_buffer) free (output_buffer); if (0) { int i; for (i = 0; i < 8; ++i) error_line ("delta = %d, count = %d", i, delta_blocks [i]); } if (!check_break () && calc_md5) { char md5_string1 [] = "00000000000000000000000000000000"; char md5_string2 [] = "00000000000000000000000000000000"; uchar md5_original [16], md5_unpacked [16]; int i; MD5Final (md5_unpacked, &md5_context); if (WavpackGetMD5Sum (wpc, md5_original)) { for (i = 0; i < 16; ++i) sprintf (md5_string1 + (i * 2), "%02x", md5_original [i]); error_line ("original md5: %s", md5_string1); if (memcmp (md5_unpacked, md5_original, 16)) md5_diff = TRUE; } for (i = 0; i < 16; ++i) sprintf (md5_string2 + (i * 2), "%02x", md5_unpacked [i]); error_line ("unpacked md5: %s", md5_string2); } if (WavpackGetWrapperBytes (wpc)) { if (outfile && result == NO_ERROR && (!DoWriteFile (outfile, WavpackGetWrapperData (wpc), WavpackGetWrapperBytes (wpc), &bcount) || bcount != WavpackGetWrapperBytes (wpc))) { error_line ("can't write .WAV data, disk probably full!"); DoTruncateFile (outfile); result = HARD_ERROR; } WavpackFreeWrapper (wpc); } // if we are not just in verify only mode, grab the size of the output // file and close the file if (outfile != NULL) { fflush (outfile); outfile_length = DoGetFileSize (outfile); if (!DoCloseHandle (outfile)) { error_line ("can't close file!"); result = SOFT_ERROR; } if (outfilename && *outfilename != '-' && !outfile_length) DoDeleteFile (outfilename); } #if defined (WIN32) if (result == NO_ERROR && copy_time && outfilename && !copy_timestamp (infilename, outfilename)) error_line ("failure copying time stamp!"); #endif if (result == NO_ERROR && WavpackGetNumSamples (wpc) != (uint32_t) -1 && total_unpacked_samples != WavpackGetNumSamples (wpc)) { error_line ("incorrect number of samples!"); result = SOFT_ERROR; } if (result == NO_ERROR && WavpackGetNumErrors (wpc)) { error_line ("crc errors detected in %d block(s)!", WavpackGetNumErrors (wpc)); result = SOFT_ERROR; } else if (result == NO_ERROR && md5_diff && (WavpackGetMode (wpc) & MODE_LOSSLESS)) { error_line ("MD5 signatures should match, but do not!"); result = SOFT_ERROR; } // Compute and display the time consumed along with some other details of // the unpacking operation (assuming there was no error). #ifdef __BORLANDC__ gettime (&time2); dtime = time2.ti_sec * 100.0 + time2.ti_hund + time2.ti_min * 6000.0 + time2.ti_hour * 360000.00; dtime -= time1.ti_sec * 100.0 + time1.ti_hund + time1.ti_min * 6000.0 + time1.ti_hour * 360000.00; if ((dtime /= 100.0) < 0.0) dtime += 86400.0; #elif defined(WIN32) _ftime (&time2); dtime = time2.time + time2.millitm / 1000.0; dtime -= time1.time + time1.millitm / 1000.0; #else gettimeofday(&time2,&timez); dtime = time2.tv_sec + time2.tv_usec / 1000000.0; dtime -= time1.tv_sec + time1.tv_usec / 1000000.0; #endif if (result == NO_ERROR && !quiet_mode) { char *file, *fext, *oper, *cmode, cratio [16] = ""; if (outfilename && *outfilename != '-') { file = FN_FIT (outfilename); fext = ""; oper = "restored"; } else { file = (*infilename == '-') ? "stdin" : FN_FIT (infilename); fext = wvc_mode ? " (+.wvc)" : ""; oper = outfilename ? "unpacked" : "verified"; } if (WavpackGetMode (wpc) & MODE_LOSSLESS) { cmode = "lossless"; if (WavpackGetRatio (wpc) != 0.0) sprintf (cratio, ", %.2f%%", 100.0 - WavpackGetRatio (wpc) * 100.0); } else { cmode = "lossy"; if (WavpackGetAverageBitrate (wpc, TRUE) != 0.0) sprintf (cratio, ", %d kbps", (int) (WavpackGetAverageBitrate (wpc, TRUE) / 1000.0)); } error_line ("%s %s%s in %.2f secs (%s%s)", oper, file, fext, dtime, cmode, cratio); } WavpackCloseFile (wpc); if (result == NO_ERROR && delete_source) { error_line ("%s source file %s", DoDeleteFile (infilename) ? "deleted" : "can't delete", infilename); if (wvc_mode) { char in2filename [PATH_MAX]; strcpy (in2filename, infilename); strcat (in2filename, "c"); error_line ("%s source file %s", DoDeleteFile (in2filename) ? "deleted" : "can't delete", in2filename); } } return result; } // Reformat samples from longs in processor's native endian mode to // little-endian data with (possibly) less than 4 bytes / sample. static uchar *format_samples (int bps, uchar *dst, int32_t *src, uint32_t samcnt) { int32_t temp; switch (bps) { case 1: while (samcnt--) *dst++ = *src++ + 128; break; case 2: while (samcnt--) { *dst++ = (uchar) (temp = *src++); *dst++ = (uchar) (temp >> 8); } break; case 3: while (samcnt--) { *dst++ = (uchar) (temp = *src++); *dst++ = (uchar) (temp >> 8); *dst++ = (uchar) (temp >> 16); } break; case 4: while (samcnt--) { *dst++ = (uchar) (temp = *src++); *dst++ = (uchar) (temp >> 8); *dst++ = (uchar) (temp >> 16); *dst++ = (uchar) (temp >> 24); } break; } return dst; } static void dump_summary (WavpackContext *wpc, char *name, FILE *dst) { int num_channels = WavpackGetNumChannels (wpc); uchar md5_sum [16], modes [80]; fprintf (dst, "\n"); if (name && *name != '-') { fprintf (dst, "file name: %s%s\n", name, (WavpackGetMode (wpc) & MODE_WVC) ? " (+wvc)" : ""); fprintf (dst, "file size: %lu bytes\n", WavpackGetFileSize (wpc)); } fprintf (dst, "source: %d-bit %s at %ld Hz\n", WavpackGetBitsPerSample (wpc), (WavpackGetMode (wpc) & MODE_FLOAT) ? "floats" : "ints", WavpackGetSampleRate (wpc)); fprintf (dst, "channels: %d (%s)\n", num_channels, num_channels > 2 ? "multichannel" : (num_channels == 1 ? "mono" : "stereo")); if (WavpackGetNumSamples (wpc) != (uint32_t) -1) { double seconds = (double) WavpackGetNumSamples (wpc) / WavpackGetSampleRate (wpc); int minutes = (int) floor (seconds / 60.0); int hours = (int) floor (seconds / 3600.0); seconds -= minutes * 60.0; minutes -= hours * 60.0; fprintf (dst, "duration: %d:%02d:%0.2f\n", hours, minutes, seconds); } modes [0] = 0; if (WavpackGetMode (wpc) & MODE_HYBRID) strcat (modes, "hybrid "); strcat (modes, (WavpackGetMode (wpc) & MODE_LOSSLESS) ? "lossless" : "lossy"); if (WavpackGetMode (wpc) & MODE_FAST) strcat (modes, ", fast"); else if (WavpackGetMode (wpc) & MODE_HIGH) strcat (modes, ", high"); if (WavpackGetMode (wpc) & MODE_EXTRA) strcat (modes, ", extra"); if (WavpackGetMode (wpc) & MODE_SFX) strcat (modes, ", sfx"); fprintf (dst, "modalities: %s\n", modes); if (WavpackGetRatio (wpc) != 0.0) { fprintf (dst, "compression: %.2f%%\n", 100.0 - (100 * WavpackGetRatio (wpc))); fprintf (dst, "ave bitrate: %d kbps\n", (int) ((WavpackGetAverageBitrate (wpc, TRUE) + 500.0) / 1000.0)); if (WavpackGetMode (wpc) & MODE_WVC) fprintf (dst, "ave lossy bitrate: %d kbps\n", (int) ((WavpackGetAverageBitrate (wpc, FALSE) + 500.0) / 1000.0)); } if (WavpackGetVersion (wpc)) fprintf (dst, "encoder version: %d\n", WavpackGetVersion (wpc)); if (WavpackGetMD5Sum (wpc, md5_sum)) { char md5_string [] = "00000000000000000000000000000000"; int i; for (i = 0; i < 16; ++i) sprintf (md5_string + (i * 2), "%02x", md5_sum [i]); fprintf (dst, "original md5: %s\n", md5_string); } } ////////////////////////////////////////////////////////////////////////////// // This function displays the progress status on the title bar of the DOS // // window that WavPack is running in. The "file_progress" argument is for // // the current file only and ranges from 0 - 1; this function takes into // // account the total number of files to generate a batch progress number. // ////////////////////////////////////////////////////////////////////////////// void display_progress (double file_progress) { char title [40]; file_progress = (file_index + file_progress) / num_files; sprintf (title, "%d%% (WvUnpack)", (int) ((file_progress * 100.0) + 0.5)); SetConsoleTitle (title); }