////////////////////////////////////////////////////////////////////////////
//                           **** WAVPACK ****                            //
//                  Hybrid Lossless Wavefile Compressor                   //
//                Copyright (c) 1998 - 2022 David Bryant.                 //
//                          All Rights Reserved.                          //
//      Distributed under the BSD Software License (see license.txt)      //
////////////////////////////////////////////////////////////////////////////

// wvgain.c

// This is the main module for the WavPack command-line ReplayGain Scanner/Tagger.

// This implementation is based on the ReplayGain proposal by David Robinson
// with table values copied from the Foobar2000 source code. Many thanks are
// due David Robinson and the others who contributed to ReplayGain.

// ReplayGain's [somewhat outdated] website: http://replaygain.org/

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <io.h>
#else
#if defined(__OS2__)
#define INCL_DOSPROCESS
#include <os2.h>
#include <io.h>
#endif
#include <sys/stat.h>
#include <sys/param.h>
#include <locale.h>
#if defined (__GNUC__)
#include <unistd.h>
#include <glob.h>
#endif
#endif

#if defined(__GNUC__) && !defined(_WIN32)
#include <sys/time.h>
#else
#include <sys/timeb.h>
#endif

#include <math.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

#include "wavpack.h"
#include "utils.h"

#ifdef _WIN32
#include "win32_unicode_support.h"
#define fputs fputs_utf8
#define fprintf fprintf_utf8
#define fopen(f,m) fopen_utf8(f,m)
#endif

///////////////////////////// local variable storage //////////////////////////

static const char *sign_on = "\n"
" WVGAIN  ReplayGain Scanner/Tagger for WavPack  %s Version %s\n"
" Copyright (c) 2005 - 2022 David Bryant.  All Rights Reserved.\n\n";

static const char *version_warning = "\n"
" WARNING: WVGAIN using libwavpack version %s, expected %s (see README)\n\n";

static const char *usage =
" Usage:   WVGAIN [-options] [@]infile[.wv] [...]\n"
#if defined (_WIN32) || defined (__OS2__)
"             (infiles may contain wildcards: ?,*)\n\n"
#else
"             (multiple input files are allowed)\n\n"
#endif
" Options: -a  = album mode (all files scanned are considered an album)\n"
"          -c  = clean ReplayGain values from all files (no analysis)\n"
"          -d  = display calculated values only (no files are modified)\n"
"          -i  = ignore .wvc file (forces hybrid lossy)\n"
#if defined (_WIN32) || defined (__OS2__)
"          -l  = run at low priority (for smoother multitasking)\n"
#endif
"          -n  = new files only (skip files with track info, or album\n"
"                 info if album mode specified)\n"
"          -q  = quiet (keep console output to a minimum)\n"
"          -s  = show stored values only (no analysis)\n"
"          -v  = write the version to stdout\n"
#if defined (_WIN32)
"          -z  = don't set console title to indicate progress\n\n"
#else
"          -z1 = set console title to indicate progress\n\n"
#endif
" Web:     Visit www.wavpack.com for latest version and info\n";

// this global is used to indicate the special "debug" mode where extra debug messages
// are displayed and all messages are logged to the file wavpack.log

int debug_logging_mode;

#define HISTOGRAM_SLOTS 12000
static uint32_t track_histogram [HISTOGRAM_SLOTS], album_histogram [HISTOGRAM_SLOTS];

static char album_mode, clean_mode, display_mode, ignore_wvc, quiet_mode, show_mode, new_mode, set_console_title;
static int num_files, file_index;

/////////////////////////// local function declarations ///////////////////////

static int update_file (char *infilename, float track_gain, float track_peak, float album_gain, float album_peak);
static int analyze_file (char *infilename, uint32_t *histogram, float *peak);
static int show_file_info (char *infilename, FILE *dst);
static float calc_replaygain (uint32_t *histogram);
static void *decimation_init (int num_channels, int ratio);
static int decimation_run (void *context, int32_t *samples, int num_samples);
static void *decimation_destroy (void *context);
static void display_progress (double file_progress);

#ifdef _WIN32
static void TextToUTF8 (void *string, int len);
#endif

#define WAVPACK_NO_ERROR    0
#define WAVPACK_SOFT_ERROR  1
#define WAVPACK_HARD_ERROR  2

// The "main" function for the command-line WavPack ReplayGain Scanner/Processor.
// Note that on Windows this is actually a static function that is called from the
// "real" main() defined immediately afterward that converts the wchar argument list
// into UTF-8 strings and sets the console to UTF-8 for better Unicode support.

#ifdef _WIN32
static int wvgain_main(int argc, char **argv)
#else
int main(int argc, char **argv)
#endif
{
#ifdef __EMX__ /* OS/2 */
    _wildcard (&argc, &argv);
#endif
    int  error_count = 0;
    char **matches = NULL;
    int result = WAVPACK_NO_ERROR;

#if defined(_WIN32)
    char selfname [MAX_PATH];

    if (GetModuleFileName (NULL, selfname, sizeof (selfname)) && filespec_name (selfname) &&
        _strupr (filespec_name (selfname)) && strstr (filespec_name (selfname), "DEBUG"))
            debug_logging_mode = TRUE;

    strcpy (selfname, *argv);
#else
    if (filespec_name (*argv) &&
        (strstr (filespec_name (*argv), "ebug") || strstr (filespec_name (*argv), "DEBUG")))
            debug_logging_mode = TRUE;
#endif

    if (debug_logging_mode) {
        char **argv_t = argv;
        int argc_t = argc;

        while (--argc_t)
            error_line ("arg %d: %s", argc - argc_t, *++argv_t);
    }

#if defined (_WIN32)
   set_console_title = 1;      // on Windows, we default to messing with the console title
#endif                          // on Linux, this is considered uncool to do by default

    // 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 'V': case 'v':
                        printf ("wvgain %s\n", PACKAGE_VERSION);
                        printf ("libwavpack %s\n", WavpackGetLibraryVersionString ());
                        return 0;

                    case 'A': case 'a':
                        album_mode = 1;
                        break;

                    case 'C': case 'c':
                        clean_mode = 1;
                        break;

                    case 'D': case 'd':
                        display_mode = 1;
                        break;

#if defined (_WIN32)
                    case 'L': case 'l':
                        SetPriorityClass (GetCurrentProcess(), IDLE_PRIORITY_CLASS);
                        break;
#elif defined (__OS2__)
                    case 'L': case 'l':
                        DosSetPriority (0, PRTYC_IDLETIME, 0, 0);
                        break;
#endif
                    case 'N': case 'n':
                        new_mode = 1;
                        break;

                    case 'Q': case 'q':
                        quiet_mode = 1;
                        break;

                    case 'Z': case 'z':
                        set_console_title = (char) strtol (++*argv, argv, 10);
                        --*argv;
                        break;

                    case 'I': case 'i':
                        ignore_wvc = 1;
                        break;

                    case 'S': case 's':
                        show_mode = 1;
                        break;

                    default:
                        error_line ("illegal option: %c !", **argv);
                        ++error_count;
                }
        else {
            matches = realloc (matches, (num_files + 1) * sizeof (*matches));
            matches [num_files] = malloc (strlen (*argv) + 10);
            strcpy (matches [num_files], *argv);

            if (*(matches [num_files]) != '-' && *(matches [num_files]) != '@' &&
                !filespec_ext (matches [num_files]))
                    strcat (matches [num_files], ".wv");

            num_files++;
        }
    }

    // check for various command-line argument problems

    if (clean_mode && (album_mode || display_mode || show_mode)) {
        error_line ("clean mode can't be used with album, show, or display mode!");
        ++error_count;
    }
    else if (show_mode && (album_mode || display_mode)) {
        error_line ("show mode can't be used with album or display mode!");
        ++error_count;
    }

    if (strcmp (WavpackGetLibraryVersionString (), PACKAGE_VERSION)) {
        fprintf (stderr, version_warning, WavpackGetLibraryVersionString (), PACKAGE_VERSION);
        fflush (stderr);
    }
    else if (!quiet_mode && !error_count) {
        fprintf (stderr, sign_on, VERSION_OS, WavpackGetLibraryVersionString ());
        fflush (stderr);
    }

    if (!num_files) {
        printf ("%s", usage);
        return 1;
    }

    if (error_count)
        return 1;

    setup_break ();

    for (file_index = 0; file_index < num_files; ++file_index) {
        char *infilename = matches [file_index];

        // If the single 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 == '@') {
            FILE *list = fopen (infilename+1, "rb");
            char *listbuff = NULL, *cp;
            int listbytes = 0, di, c;

            for (di = file_index; di < num_files - 1; di++)
                matches [di] = matches [di + 1];

            file_index--;
            num_files--;

            if (list == NULL) {
                error_line ("file %s not found!", infilename+1);
                free (infilename);
                return 1;
            }

            while (1) {
                int bytes_read;

                listbuff = realloc (listbuff, listbytes + 1024);
                memset (listbuff + listbytes, 0, 1024);
                listbytes += bytes_read = (int) fread (listbuff + listbytes, 1, 1024, list);

                if (bytes_read < 1024)
                    break;
            }

#if defined (_WIN32)
            listbuff = realloc (listbuff, listbytes *= 2);
            TextToUTF8 (listbuff, listbytes);
#endif
            cp = listbuff;

            while ((c = *cp++)) {

                while (c == '\n' || c == '\r')
                    c = *cp++;

                if (c) {
                    char *fname = malloc (PATH_MAX);
                    int ci = 0;

                    do
                        fname [ci++] = c;
                    while ((c = *cp++) != '\n' && c != '\r' && c && ci < PATH_MAX);

                    fname [ci++] = '\0';
                    matches = realloc (matches, ++num_files * sizeof (*matches));

                    for (di = num_files - 1; di > file_index + 1; di--)
                        matches [di] = matches [di - 1];

                    matches [++file_index] = fname;
                }

                if (!c)
                    break;
            }

            fclose (list);
            free (listbuff);
            free (infilename);
        }
#if defined (_WIN32)
        else if (filespec_wild (infilename)) {
            wchar_t *winfilename = utf8_to_utf16(infilename);
            struct _wfinddata_t _wfinddata_t;
            intptr_t file;
            int di;

            for (di = file_index; di < num_files - 1; di++)
                matches [di] = matches [di + 1];

            file_index--;
            num_files--;

            if ((file = _wfindfirst (winfilename, &_wfinddata_t)) != (intptr_t) -1) {
                do {
                    char *name_utf8;

                    if (!(_wfinddata_t.attrib & _A_SUBDIR) && (name_utf8 = utf16_to_utf8(_wfinddata_t.name))) {
                        matches = realloc (matches, ++num_files * sizeof (*matches));

                        for (di = num_files - 1; di > file_index + 1; di--)
                            matches [di] = matches [di - 1];

                        matches [++file_index] = malloc (strlen (infilename) + strlen (name_utf8) + 10);
                        strcpy (matches [file_index], infilename);
                        *filespec_name (matches [file_index]) = '\0';
                        strcat (matches [file_index], name_utf8);
                        free (name_utf8);
                    }
                } while (_wfindnext (file, &_wfinddata_t) == 0);

                _findclose (file);
            }

            free (winfilename);
            free (infilename);
        }
#endif
    }

    // if we found any files to process, this is where we start

    if (num_files) {
        float *track_gains, *track_peaks, album_gain;
        float track_peak, album_peak = 0.0;
        int i;

        track_gains = malloc (sizeof (*track_gains) * num_files);
        track_peaks = malloc (sizeof (*track_peaks) * num_files);

        // Loop through and analyze files in list. If we are in album mode we just keep
        // track of everything and modify the tags in another pass. If we're not in
        // album mode then we can update the files here.

        for (file_index = 0; !clean_mode && !show_mode && file_index < num_files; ++file_index) {
            if (check_break ())
                break;

            if (num_files > 1 && !quiet_mode) {
                fprintf (stderr, "\n%s:\n", matches [file_index]);
                fflush (stderr);
            }

            if (new_mode) {
                WavpackContext *wpc;
                char error [80];
#ifdef _WIN32
                wpc = WavpackOpenFileInput (matches [file_index], error, OPEN_TAGS | OPEN_FILE_UTF8 | OPEN_DSD_AS_PCM, 0);
#else
                wpc = WavpackOpenFileInput (matches [file_index], error, OPEN_TAGS | OPEN_DSD_AS_PCM, 0);
#endif
                if (wpc) {
                    int alreadyHasTag = WavpackGetTagItem (wpc, album_mode ? "replaygain_album_gain" : "replaygain_track_gain", NULL, 0);
                    WavpackCloseFile (wpc);

                    if (alreadyHasTag) {
                        if (album_mode) {
                            error_line ("ReplayGain album information already present...aborting");
                            result = WAVPACK_HARD_ERROR;
                            break;
                        }
                        else {
                            error_line ("ReplayGain track information already present...skipping");
                            continue;
                        }
                    }
                }
            }

            result = analyze_file (matches [file_index], track_histogram, &track_peak);

            if (result != WAVPACK_NO_ERROR) {
                ++error_count;

                if (album_mode || result == WAVPACK_HARD_ERROR) {
                    result = WAVPACK_HARD_ERROR;
                    break;
                }
                else
                    continue;
            }

            track_gains [file_index] = calc_replaygain (track_histogram);
            track_peaks [file_index] = track_peak;

            if (!quiet_mode) {
                error_line ("replaygain_track_gain = %+.2f dB", track_gains [file_index]);
                error_line ("replaygain_track_peak = %.6f", track_peaks [file_index]);
            }

            if (album_mode) {
                for (i = 0; i < HISTOGRAM_SLOTS; ++i)
                    album_histogram [i] += track_histogram [i];

                if (track_peak > album_peak)
                    album_peak = track_peak;
            }
            else if (!display_mode) {
                result = update_file (matches [file_index], track_gains [file_index], track_peaks [file_index], 0, 0);

                if (result != WAVPACK_NO_ERROR) {
                    ++error_count;

                    if (result == WAVPACK_HARD_ERROR)
                        break;
                }
            }
        }

        if (result != WAVPACK_HARD_ERROR) {
            album_gain = calc_replaygain (album_histogram);

            if (album_mode && !quiet_mode && num_files > 1) {
                error_line ("\nalbum results:");
                error_line ("replaygain_album_gain = %+.2f dB", album_gain);
                error_line ("replaygain_album_peak = %.6f", album_peak);
            }
        }

        // If we are in album mode or clear mode, this is where we loop through and modify
        // the tags (or just show existing stored values).

        if (result != WAVPACK_HARD_ERROR)
            for (file_index = 0; (clean_mode || album_mode || show_mode) && !display_mode && file_index < num_files; ++file_index) {
                if (check_break ())
                    break;

                if (num_files > 1 && !quiet_mode) {
                    fprintf (stderr, "\n%s:\n", matches [file_index]);
                    fflush (stderr);
                }

                if (show_mode)
                    result = show_file_info (matches [file_index], stdout);
                else
                    result = update_file (matches [file_index], track_gains [file_index], track_peaks [file_index], album_gain, album_peak);

                free (matches [file_index]);

                if (result != WAVPACK_NO_ERROR) {
                    ++error_count;

                    if (result == WAVPACK_HARD_ERROR)
                        break;
                }
            }

        if (num_files > 1) {
            if (error_count) {
                fprintf (stderr, "\n **** warning: errors occurred in %d of %d files! ****\n", error_count, num_files);
                fflush (stderr);
            }
            else if (!quiet_mode) {
                fprintf (stderr, "\n **** %d files successfully processed ****\n", num_files);
                fflush (stderr);
            }
        }

        free (track_peaks);
        free (track_gains);
        free (matches);
    }
    else {
        ++error_count;
        error_line ("nothing to do!");
    }

    if (set_console_title)
        DoSetConsoleTitle ("WvGain Completed");

    return error_count ? 1 : 0;
}

#ifdef _WIN32

// On Windows, this "real" main() acts as a shell to our static wvgain_main().
// Its purpose is to convert the wchar command-line arguments into UTF-8 encoded
// strings and set the console output to UTF-8.

int main(int argc, char **argv)
{
    int ret = -1, argc_utf8 = -1;
    char **argv_utf8 = NULL;
    char **argv_copy = NULL;

    init_commandline_arguments_utf8(&argc_utf8, &argv_utf8);

    // we have to make a copy of the argv pointer array because the command parser
    // sometimes modifies them, which is problematic when it comes time to free them

    if (argc_utf8 && argv_utf8) {
        argv_copy = malloc (sizeof (char*) * argc_utf8);
        memcpy (argv_copy, argv_utf8, sizeof (char*) * argc_utf8);
    }

    ret = wvgain_main(argc_utf8, argv_copy);

    if (argv_copy)
        free (argv_copy);

    free_commandline_arguments_utf8(&argc_utf8, &argv_utf8);

    return ret;
}

#endif

// Unpack the specified WavPack input file and analyze it for ReplayGain
// information.

static void calc_stereo_peak (float *samples, uint32_t samcnt, float *peak_p);
static double calc_stereo_rms (float *samples, uint32_t samcnt);
static int filter_init (uint32_t sample_rate);
static void filter_stereo_samples (float *samples, uint32_t samcnt);
static void float_samples (float *dst, int32_t *src, uint32_t samcnt, float scale);

static int analyze_file (char *infilename, uint32_t *histogram, float *peak)
{
    int result = WAVPACK_NO_ERROR, open_flags = 0, num_channels, wvc_mode;
    uint32_t sample_rate, window_samples;
    int64_t total_unpacked_samples = 0;
    void *decimation_context = NULL;
    double progress = -1.0;
    int32_t *temp_buffer;
    WavpackContext *wpc;
    char error [80];

    memset (histogram, 0, sizeof (*histogram) * HISTOGRAM_SLOTS);
    *peak = 0.0;

    // use library to open WavPack file

#ifdef _WIN32
    open_flags |= OPEN_FILE_UTF8;
#endif

    if (!ignore_wvc)
        open_flags |= OPEN_WVC;

    open_flags |= OPEN_TAGS | OPEN_NORMALIZE | OPEN_DSD_AS_PCM;

    wpc = WavpackOpenFileInput (infilename, error, open_flags, 0);

    if (!wpc) {
        error_line (error);
        return WAVPACK_SOFT_ERROR;
    }

    wvc_mode = WavpackGetMode (wpc) & MODE_WVC;
    num_channels = WavpackGetNumChannels (wpc);

    if (num_channels > 2) {
        error_line ("can't handle multichannel files yet!");
        return WAVPACK_SOFT_ERROR;
    }

    if (!quiet_mode) {
        fprintf (stderr, "analyzing %s%s,", *infilename == '-' ? "stdin" :
            FN_FIT (infilename), wvc_mode ? " (+.wvc)" : "");
        fflush (stderr);
    }

    sample_rate = WavpackGetSampleRate (wpc);

    if (sample_rate >= 256000) {
        decimation_context = decimation_init (num_channels, 4);
        sample_rate /= 4;
    }

    window_samples = sample_rate / 20;

    if (decimation_context)
        temp_buffer = malloc (window_samples * 8 * 4);
    else
        temp_buffer = malloc (window_samples * 8);

    if (!filter_init (sample_rate))
        result = WAVPACK_SOFT_ERROR;

    while (result == WAVPACK_NO_ERROR) {
        uint32_t samples_to_unpack, samples_unpacked;
        int32_t level;

        samples_to_unpack = window_samples;

        if (decimation_context) {
            samples_unpacked = WavpackUnpackSamples (wpc, temp_buffer, samples_to_unpack * 4);
            total_unpacked_samples += samples_unpacked;
            samples_unpacked = decimation_run (decimation_context, temp_buffer, samples_unpacked);
        }
        else {
            samples_unpacked = WavpackUnpackSamples (wpc, temp_buffer, samples_to_unpack);
            total_unpacked_samples += samples_unpacked;
        }

        if (samples_unpacked) {
            if (!(WavpackGetMode (wpc) & MODE_FLOAT))
                switch (WavpackGetBytesPerSample (wpc)) {
                    case 1:
                        float_samples ((float *) temp_buffer, temp_buffer, samples_unpacked * num_channels, 1.0 / 128.0);
                        break;

                    case 2:
                        float_samples ((float *) temp_buffer, temp_buffer, samples_unpacked * num_channels, 1.0 / 32768.0);
                        break;

                    case 3:
                        float_samples ((float *) temp_buffer, temp_buffer, samples_unpacked * num_channels, 1.0 / 8388608.0);
                        break;

                    case 4:
                        float_samples ((float *) temp_buffer, temp_buffer, samples_unpacked * num_channels, 1.0 / 2147483648.0);
                        break;
                }

            if (num_channels == 1) {
                int32_t *dst = temp_buffer + samples_unpacked * 2;
                int32_t *src = temp_buffer + samples_unpacked;
                uint32_t cnt = samples_unpacked;

                while (cnt--) {
                    *--dst = *--src;
                    *--dst = *src;
                }
            }

            calc_stereo_peak ((float *) temp_buffer, samples_unpacked, peak);
            filter_stereo_samples ((float *) temp_buffer, samples_unpacked);
            level = (int32_t) floor (100 * calc_stereo_rms ((float *) temp_buffer, samples_unpacked));

            if (level < 0)
                histogram [0]++;
            else if (level >= HISTOGRAM_SLOTS)
                histogram [HISTOGRAM_SLOTS - 1]++;
            else
                histogram [level]++;
        }
        else
            break;

        if (check_break ()) {
#if defined(_WIN32)
            fprintf (stderr, "^C\n");
#else
            fprintf (stderr, "\n");
#endif
            fflush (stderr);
            result = WAVPACK_HARD_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);
                    fflush (stderr);
                }
        }
    }

    free (temp_buffer);

    if (decimation_context)
        decimation_destroy (decimation_context);

    if (result == WAVPACK_NO_ERROR && WavpackGetNumSamples64 (wpc) != -1 &&
        total_unpacked_samples != WavpackGetNumSamples64 (wpc)) {
            error_line ("incorrect number of samples!");
            result = WAVPACK_SOFT_ERROR;
    }

    if (result == WAVPACK_NO_ERROR && WavpackGetNumErrors (wpc)) {
        error_line ("crc errors detected in %d block(s)!", WavpackGetNumErrors (wpc));
        result = WAVPACK_SOFT_ERROR;
    }

    WavpackCloseFile (wpc);
    return result;
}

// Update the tag of the specified file to reflect the results of the ReplayGain analysis
// (or just to remove existing ReplayGain information).

static int update_file (char *infilename, float track_gain, float track_peak, float album_gain, float album_peak)
{
    int write_tag = FALSE;
    char error [80], value [20];
    WavpackContext *wpc;

    // use library to open WavPack file

#ifdef _WIN32
    wpc = WavpackOpenFileInput (infilename, error, OPEN_EDIT_TAGS | OPEN_FILE_UTF8 | OPEN_DSD_AS_PCM, 0);
#else
    wpc = WavpackOpenFileInput (infilename, error, OPEN_EDIT_TAGS | OPEN_DSD_AS_PCM, 0);
#endif

    if (!wpc) {
        error_line (error);
        return WAVPACK_SOFT_ERROR;
    }

    if (clean_mode) {
        int items_removed = 0;

        if (WavpackDeleteTagItem (wpc, "replaygain_track_gain"))
            ++items_removed;

        if (WavpackDeleteTagItem (wpc, "replaygain_track_peak"))
            ++items_removed;

        if (WavpackDeleteTagItem (wpc, "replaygain_album_gain"))
            ++items_removed;

        if (WavpackDeleteTagItem (wpc, "replaygain_album_peak"))
            ++items_removed;

        if (items_removed) {
            if (!quiet_mode)
                error_line ("%d ReplayGain values cleaned", items_removed);

            write_tag = TRUE;
        }
        else
            error_line ("no ReplayGain values found");
    }
    else {
        if ((WavpackGetMode (wpc) & (MODE_VALID_TAG | MODE_APETAG)) == MODE_VALID_TAG) {
            char title [40], artist [40], album [40], year [10], comment [40], track [10];

            WavpackGetTagItem (wpc, "title", title, sizeof (title));
            WavpackGetTagItem (wpc, "artist", artist, sizeof (artist));
            WavpackGetTagItem (wpc, "album", album, sizeof (album));
            WavpackGetTagItem (wpc, "year", year, sizeof (year));
            WavpackGetTagItem (wpc, "comment", comment, sizeof (comment));
            WavpackGetTagItem (wpc, "track", track, sizeof (track));

            if (title [0])
                WavpackAppendTagItem (wpc, "Title", title, (int) strlen (title));

            if (artist [0])
                WavpackAppendTagItem (wpc, "Artist", artist, (int) strlen (artist));

            if (album [0])
                WavpackAppendTagItem (wpc, "Album", album, (int) strlen (album));

            if (year [0])
                WavpackAppendTagItem (wpc, "Year", year, (int) strlen (year));

            if (comment [0])
                WavpackAppendTagItem (wpc, "Comment", comment, (int) strlen (comment));

            if (track [0])
                WavpackAppendTagItem (wpc, "Track", track, (int) strlen (track));

            error_line ("warning: ID3v1 tag converted to APEv2");
        }

        sprintf (value, "%+.2f dB", track_gain);
        WavpackAppendTagItem (wpc, "replaygain_track_gain", value, (int) strlen (value));

        sprintf (value, "%.6f", track_peak);
        WavpackAppendTagItem (wpc, "replaygain_track_peak", value, (int) strlen (value));

        if (album_mode) {
            sprintf (value, "%+.2f dB", album_gain);
            WavpackAppendTagItem (wpc, "replaygain_album_gain", value, (int) strlen (value));
            sprintf (value, "%.6f", album_peak);
            WavpackAppendTagItem (wpc, "replaygain_album_peak", value, (int) strlen (value));
        }

        if (!quiet_mode)
            error_line ("%d ReplayGain values appended", album_mode ? 4 : 2);

        write_tag = TRUE;
    }

    if (write_tag && !WavpackWriteTag (wpc)) {
        error_line ("%s", WavpackGetErrorMessage (wpc));
        return WAVPACK_SOFT_ERROR;
    }

    WavpackCloseFile (wpc);
    return WAVPACK_NO_ERROR;
}

// Just show any ReplayGain tags for the specified file

static int show_file_info (char *infilename, FILE *dst)
{
    char error [80], value [20];
    WavpackContext *wpc;
    int items = 0;

    // use library to open WavPack file

#ifdef _WIN32
    wpc = WavpackOpenFileInput (infilename, error, OPEN_TAGS | OPEN_FILE_UTF8 | OPEN_DSD_AS_PCM, 0);
#else
    wpc = WavpackOpenFileInput (infilename, error, OPEN_TAGS | OPEN_DSD_AS_PCM, 0);
#endif

    if (!wpc) {
        error_line (error);
        return WAVPACK_SOFT_ERROR;
    }

    fprintf (dst, "\nfile: %s\n", infilename);

    if (WavpackGetTagItem (wpc, "replaygain_track_gain", value, sizeof (value))) {
        fprintf (dst, "replaygain_track_gain = %s\n", value);
        ++items;
    }

    if (WavpackGetTagItem (wpc, "replaygain_track_peak", value, sizeof (value))) {
        fprintf (dst, "replaygain_track_peak = %s\n", value);
        ++items;
    }

    if (WavpackGetTagItem (wpc, "replaygain_album_gain", value, sizeof (value))) {
        fprintf (dst, "replaygain_album_gain = %s\n", value);
        ++items;
    }

    if (WavpackGetTagItem (wpc, "replaygain_album_peak", value, sizeof (value))) {
        fprintf (dst, "replaygain_album_peak = %s\n", value);
        ++items;
    }

    if (!items)
        fprintf (dst, "no ReplayGain values found\n");

    WavpackCloseFile (wpc);
    return WAVPACK_NO_ERROR;
}

// Calculate the ReplayGain value from the specified loudness histogram; clip to -24 / +64 dB

static float calc_replaygain (uint32_t *histogram)
{
    uint32_t loud_count = 0, total_windows = 0;
    float unclipped_gain;
    int i;

    for (i = 0; i < HISTOGRAM_SLOTS; i++)
        total_windows += histogram [i];

    while (i--)
        if ((loud_count += histogram [i]) * 20 >= total_windows)
            break;

    unclipped_gain = (float)(64.54 - i / 100.0);

    if (unclipped_gain > 64.0)
        return 64.0;
    else if (unclipped_gain < -24.0)
        return -24.0;
    else
        return unclipped_gain;
}

// Convert the specified samples into floating-point using the specified scale factor.

static void float_samples (float *dst, int32_t *src, uint32_t samcnt, float scale)
{
    while (samcnt--)
        *dst++ = *src++ * scale;
}

// These are the filters used to calculate perceived loudness. The table data was copied
// from the Foobar2000 source code.

#define YULE_ORDER         10
#define BUTTER_ORDER        2

struct rg_freqinfo
{
    uint32_t rate;
    double BYule[YULE_ORDER+1],AYule[YULE_ORDER+1],BButter[BUTTER_ORDER+1],AButter[BUTTER_ORDER+1];
};

static struct rg_freqinfo freqinfos[] =
{
    {
        48000,
        { 0.03857599435200, -0.02160367184185, -0.00123395316851, -0.00009291677959, -0.01655260341619,  0.02161526843274, -0.02074045215285,  0.00594298065125,  0.00306428023191,  0.00012025322027,  0.00288463683916 },
        { 1., -3.84664617118067,  7.81501653005538,-11.34170355132042, 13.05504219327545,-12.28759895145294,  9.48293806319790, -5.87257861775999,  2.75465861874613, -0.86984376593551, 0.13919314567432 },
        { 0.98621192462708, -1.97242384925416, 0.98621192462708 },
        { 1., -1.97223372919527, 0.97261396931306 },
    },

    {
        44100,
        { 0.05418656406430, -0.02911007808948, -0.00848709379851, -0.00851165645469, -0.00834990904936,  0.02245293253339, -0.02596338512915,  0.01624864962975, -0.00240879051584,  0.00674613682247, -0.00187763777362 },
        { 1., -3.47845948550071,  6.36317777566148, -8.54751527471874,  9.47693607801280, -8.81498681370155,  6.85401540936998, -4.39470996079559,  2.19611684890774, -0.75104302451432, 0.13149317958808 },
        { 0.98500175787242, -1.97000351574484, 0.98500175787242 },
        { 1., -1.96977855582618, 0.97022847566350 },
    },

    {
        32000,
        { 0.15457299681924, -0.09331049056315, -0.06247880153653,  0.02163541888798, -0.05588393329856,  0.04781476674921,  0.00222312597743,  0.03174092540049, -0.01390589421898,  0.00651420667831, -0.00881362733839 },
        { 1., -2.37898834973084,  2.84868151156327, -2.64577170229825,  2.23697657451713, -1.67148153367602,  1.00595954808547, -0.45953458054983,  0.16378164858596, -0.05032077717131, 0.02347897407020 },
        { 0.97938932735214, -1.95877865470428, 0.97938932735214 },
        { 1., -1.95835380975398, 0.95920349965459 },
    },

    {
        24000,
        { 0.30296907319327, -0.22613988682123, -0.08587323730772,  0.03282930172664, -0.00915702933434, -0.02364141202522, -0.00584456039913,  0.06276101321749, -0.00000828086748,  0.00205861885564, -0.02950134983287 },
        { 1., -1.61273165137247,  1.07977492259970, -0.25656257754070, -0.16276719120440, -0.22638893773906,  0.39120800788284, -0.22138138954925,  0.04500235387352,  0.02005851806501, 0.00302439095741 },
        { 0.97531843204928, -1.95063686409857, 0.97531843204928 },
        { 1., -1.95002759149878, 0.95124613669835 },
    },

    {
        22050,
        { 0.33642304856132, -0.25572241425570, -0.11828570177555,  0.11921148675203, -0.07834489609479, -0.00469977914380, -0.00589500224440,  0.05724228140351,  0.00832043980773, -0.01635381384540, -0.01760176568150 },
        { 1., -1.49858979367799,  0.87350271418188,  0.12205022308084, -0.80774944671438,  0.47854794562326, -0.12453458140019, -0.04067510197014,  0.08333755284107, -0.04237348025746, 0.02977207319925 },
        { 0.97316523498161, -1.94633046996323, 0.97316523498161 },
        { 1., -1.94561023566527, 0.94705070426118 },
    },

    {
        16000,
        { 0.44915256608450, -0.14351757464547, -0.22784394429749, -0.01419140100551,  0.04078262797139, -0.12398163381748,  0.04097565135648,  0.10478503600251, -0.01863887810927, -0.03193428438915,  0.00541907748707 },
        { 1., -0.62820619233671,  0.29661783706366, -0.37256372942400,  0.00213767857124, -0.42029820170918,  0.22199650564824,  0.00613424350682,  0.06747620744683,  0.05784820375801, 0.03222754072173 },
        { 0.96454515552826, -1.92909031105652, 0.96454515552826 },
        { 1., -1.92783286977036, 0.93034775234268 },
    },

    {
        12000,
        { 0.56619470757641, -0.75464456939302,  0.16242137742230,  0.16744243493672, -0.18901604199609,  0.30931782841830, -0.27562961986224,  0.00647310677246,  0.08647503780351, -0.03788984554840, -0.00588215443421 },
        { 1., -1.04800335126349,  0.29156311971249, -0.26806001042947,  0.00819999645858,  0.45054734505008, -0.33032403314006,  0.06739368333110, -0.04784254229033,  0.01639907836189, 0.01807364323573 },
        { 0.96009142950541, -1.92018285901082, 0.96009142950541 },
        { 1., -1.91858953033784, 0.92177618768381 },
    },

    {
        11025,
        { 0.58100494960553, -0.53174909058578, -0.14289799034253,  0.17520704835522,  0.02377945217615,  0.15558449135573, -0.25344790059353,  0.01628462406333,  0.06920467763959, -0.03721611395801, -0.00749618797172 },
        { 1., -0.51035327095184, -0.31863563325245, -0.20256413484477,  0.14728154134330,  0.38952639978999, -0.23313271880868, -0.05246019024463, -0.02505961724053,  0.02442357316099, 0.01818801111503 },
        { 0.95856916599601, -1.91713833199203, 0.95856916599601 },
        { 1., -1.91542108074780, 0.91885558323625 },
    },

    {
        8000,
        { 0.53648789255105, -0.42163034350696, -0.00275953611929,  0.04267842219415, -0.10214864179676,  0.14590772289388, -0.02459864859345, -0.11202315195388, -0.04060034127000,  0.04788665548180, -0.02217936801134 },
        { 1., -0.25049871956020, -0.43193942311114, -0.03424681017675, -0.04678328784242,  0.26408300200955,  0.15113130533216, -0.17556493366449, -0.18823009262115,  0.05477720428674, 0.04704409688120 },
        { 0.94597685600279, -1.89195371200558, 0.94597685600279 },
        { 1., -1.88903307939452, 0.89487434461664 },
    },

    {
        18900,
        {0.38524531015142,  -0.27682212062067,  -0.09980181488805,   0.09951486755646,  -0.08934020156622,  -0.00322369330199,  -0.00110329090689,   0.03784509844682,   0.01683906213303,  -0.01147039862572,  -0.01941767987192 },
        {1.00000000000000,  -1.29708918404534,   0.90399339674203,  -0.29613799017877,  -0.42326645916207,   0.37934887402200,  -0.37919795944938,   0.23410283284785,  -0.03892971758879,   0.00403009552351,   0.03640166626278 },
        {0.96535326815829,  -1.93070653631658,   0.96535326815829 },
        {1.00000000000000,  -1.92950577983524,   0.93190729279793 },
    },

    {
        37800,
        {0.08717879977844,  -0.01000374016172,  -0.06265852122368,  -0.01119328800950,  -0.00114279372960,   0.02081333954769,  -0.01603261863207,   0.01936763028546,   0.00760044736442,  -0.00303979112271,  -0.00075088605788 },
        {1.00000000000000,  -2.62816311472146,   3.53734535817992,  -3.81003448678921,   3.91291636730132,  -3.53518605896288,   2.71356866157873,  -1.86723311846592,   1.12075382367659,  -0.48574086886890,   0.11330544663849 },
        {0.98252400815195,  -1.96504801630391,   0.98252400815195 },
        {1.00000000000000,  -1.96474258269041,   0.96535344991740 },
    },

    {
        56000,
        {0.03144914734085,  -0.06151729206963,   0.08066788708145,  -0.09737939921516,   0.08943210803999,  -0.06989984672010,   0.04926972841044,  -0.03161257848451,   0.01456837493506,  -0.00316015108496,   0.00132807215875 },
        {1.00000000000000,  -4.87377313090032,  12.03922160140209, -20.10151118381395,  25.10388534415171, -24.29065560815903,  18.27158469090663, -10.45249552560593,   4.30319491872003,  -1.13716992070185,   0.14510733527035 },
        {0.98816995007392,  -1.97633990014784,   0.98816995007392 },
        {1.00000000000000,  -1.97619994516973,   0.97647985512594 },
    },

    {
        64000,
        {0.02613056568174,  -0.08128786488109,   0.14937282347325,  -0.21695711675126,   0.25010286673402,  -0.23162283619278,   0.17424041833052,  -0.10299599216680,   0.04258696481981,  -0.00977952936493,   0.00105325558889 },
        {1.00000000000000,  -5.73625477092119,  16.15249794355035, -29.68654912464508,  39.55706155674083, -39.82524556246253,  30.50605345013009, -17.43051772821245,   7.05154573908017,  -1.80783839720514,   0.22127840210813 },
        {0.98964101933472,  -1.97928203866944,   0.98964101933472 },
        {1.00000000000000,  -1.97917472731009,   0.97938935002880 },
    },

    {
        88200,
        {0.02667482047416,  -0.11377479336097,   0.23063167910965,  -0.30726477945593,   0.33188520686529,  -0.33862680249063,   0.31807161531340,  -0.23730796929880,   0.12273894790371,  -0.03840017967282,   0.00549673387936 },
        {1.00000000000000,  -6.31836451657302,  18.31351310801799, -31.88210014815921,  36.53792146976740, -28.23393036467559,  14.24725258227189,  -4.04670980012854,   0.18865757280515,   0.25420333563908,  -0.06012333531065 },
        {0.99247255046129,  -1.98494510092259,   0.99247255046129 },
        {1.00000000000000,  -1.98488843762335,   0.98500176422183 },
    },

    {
        96000,
        {0.00588138296683,  -0.01613559730421,   0.02184798954216,  -0.01742490405317,   0.00464635643780,   0.01117772513205,  -0.02123865824368,   0.01959354413350,  -0.01079720643523,   0.00352183686289,  -0.00063124341421 },
        {1.00000000000000,  -5.97808823642008,  16.21362507964068, -25.72923730652599,  25.40470663139513, -14.66166287771134,   2.81597484359752,   2.51447125969733,  -2.23575306985286,   0.75788151036791,  -0.10078025199029 },
        {0.99308203517541,  -1.98616407035082,   0.99308203517541 },
        {1.00000000000000,  -1.98611621154089,   0.98621192916075 },
    },

    {
        112000,
        {0.00528778718259,  -0.01893240907245,   0.03185982561867,  -0.02926260297838,   0.00715743034072,   0.01985743355827,  -0.03222614850941,   0.02565681978192,  -0.01210662313473,   0.00325436284541,  -0.00044173593001 },
        {1.00000000000000,  -6.24932108456288,  17.42344320538476, -27.86819709054896,  26.79087344681326, -13.43711081485123,  -0.66023612948173,   6.03658091814935,  -4.24926577030310,   1.40829268709186,  -0.19480852628112 },
        {0.99406737810867,  -1.98813475621734,   0.99406737810867 },
        {1.00000000000000,  -1.98809955990514,   0.98816995252954 },
    },

    {
        128000,
        {0.00553120584305,  -0.02112620545016,   0.03549076243117,  -0.03362498312306,   0.01425867248183,   0.01344686928787,  -0.03392770787836,   0.03464136459530,  -0.02039116051549,   0.00667420794705,  -0.00093763762995 },
        {1.00000000000000,  -6.14581710839925,  16.04785903675838, -22.19089131407749,  15.24756471580286,  -0.52001440400238,  -8.00488641699940,   6.60916094768855,  -2.37856022810923,   0.33106947986101,   0.00459820832036 },
        {0.99480702681278,  -1.98961405362557,   0.99480702681278 },
        {1.00000000000000,  -1.98958708647324,   0.98964102077790 },
    },

    {
        144000,
        {0.00639682359450,  -0.02556437970955,   0.04230854400938,  -0.03722462201267,   0.01718514827295,   0.00610592243009,  -0.03065965747365,   0.04345745003539,  -0.03298592681309,   0.01320937236809,  -0.00220304127757 },
        {1.00000000000000,  -6.14814623523425,  15.80002457141566, -20.78487587686937,  11.98848552310315,   3.36462015062606, -10.22419868359470,   6.65599702146473,  -1.67141861110485,  -0.05417956536718,   0.07374767867406 },
        {0.99538268958706,  -1.99076537917413,   0.99538268958706 },
        {1.00000000000000,  -1.99074405950505,   0.99078669884321 },
    },

    {
        176400,
        {0.00268568524529,  -0.00852379426080,   0.00852704191347,   0.00146116310295,  -0.00950855828762,   0.00625449515499,   0.00116183868722,  -0.00362461417136,   0.00203961000134,  -0.00050664587933,   0.00004327455427 },
        {1.00000000000000,  -5.57512782763045,  12.44291056065794, -12.87462799681221,   3.08554846961576,   6.62493459880692,  -7.07662766313248,   2.51175542736441,   0.06731510802735,  -0.24567753819213,   0.03961404162376 },
        {0.99622916581118,  -1.99245833162236,   0.99622916581118 },
        {1.00000000000000,  -1.99244411238133,   0.99247255086339 },
    },

    {
        192000,
        {0.01184742123123,  -0.04631092400086,   0.06584226961238,  -0.02165588522478,  -0.05656260778952,   0.08607493592760,  -0.03375544339786,  -0.04216579932754,   0.06416711490648,  -0.03444708260844,   0.00697275872241 },
        {1.00000000000000,  -5.24727318348167,  10.60821585192244,  -8.74127665810413,  -1.33906071371683,   8.07972882096606,  -5.46179918950847,   0.54318070652536,   0.87450969224280,  -0.34656083539754,   0.03034796843589 },
        {0.99653501465135,  -1.99307002930271,   0.99653501465135 },
        {1.00000000000000,  -1.99305802314321,   0.99308203546221 },
    }
};

static double *yule_coeff_a, *yule_coeff_b, *butter_coeff_a, *butter_coeff_b;
static float yule_hist_a [256], yule_hist_b [256], butter_hist_a [256], butter_hist_b [256];
static int yule_hist_i, butter_hist_i;

// Initialize filters; return FALSE is specified sampling rate is not supported

static int filter_init (uint32_t sample_rate)
{
    int i, n = sizeof (freqinfos) / sizeof (freqinfos [0]);

    for (i = 0; i < n; ++i)
        if (freqinfos [i].rate == sample_rate)
            break;

    if (i == n) {
        error_line ("sample rate of %d is not supported!", sample_rate);
        return FALSE;
    }

    yule_coeff_a = freqinfos [i].AYule;
    yule_coeff_b = freqinfos [i].BYule;
    butter_coeff_a = freqinfos [i].AButter;
    butter_coeff_b = freqinfos [i].BButter;

    memset (yule_hist_a, 0, sizeof (yule_hist_a));
    memset (yule_hist_b, 0, sizeof (yule_hist_b));
    yule_hist_i = 20;

    memset (butter_hist_a, 0, sizeof (butter_hist_a));
    memset (butter_hist_b, 0, sizeof (butter_hist_b));
    butter_hist_i = 4;
    return TRUE;
}

// Optimized mplementation of 2nd-order IIR stereo filter

static void butter_filter_stereo_samples (float *samples, uint32_t samcnt)
{
    double left, right;
    int i, j;

    i = butter_hist_i;

    // If filter history is very small magnitude, clear it completely to prevent denormals
    // from rattling around in there forever (slowing us down).

    for (j = -4; j < 0; ++j)
        if (fabs (butter_hist_a [i + j]) > 1e-10 || fabs (butter_hist_b [i + j]) > 1e-10)
            break;

    if (!j) {
        memset (butter_hist_a, 0, sizeof (butter_hist_a));
        memset (butter_hist_b, 0, sizeof (butter_hist_b));
    }

    while (samcnt--) {
        left   = (butter_hist_b [i] = samples [0]) * butter_coeff_b [0];
        right  = (butter_hist_b [i + 1] = samples [1]) * butter_coeff_b [0];
        left  += butter_hist_b [i - 2] * butter_coeff_b [1] - butter_hist_a [i - 2] * butter_coeff_a [1];
        right += butter_hist_b [i - 1] * butter_coeff_b [1] - butter_hist_a [i - 1] * butter_coeff_a [1];
        left  += butter_hist_b [i - 4] * butter_coeff_b [2] - butter_hist_a [i - 4] * butter_coeff_a [2];
        right += butter_hist_b [i - 3] * butter_coeff_b [2] - butter_hist_a [i - 3] * butter_coeff_a [2];
        samples [0] = butter_hist_a [i] = (float) left;
        samples [1] = butter_hist_a [i + 1] = (float) right;
        samples += 2;

        if ((i += 2) == 256) {
            memcpy (butter_hist_a, butter_hist_a + 252, sizeof (butter_hist_a [0]) * 4);
            memcpy (butter_hist_b, butter_hist_b + 252, sizeof (butter_hist_b [0]) * 4);
            i = 4;
        }
    }

    butter_hist_i = i;
}

// Optimized mplementation of 10th-order IIR stereo filter

static void yule_filter_stereo_samples (float *samples, uint32_t samcnt)
{
    double left, right;
    int i, j;

    i = yule_hist_i;

    // If filter history is very small magnitude, clear it completely to prevent denormals
    // from rattling around in there forever (slowing us down).

    for (j = -20; j < 0; ++j)
        if (fabs (yule_hist_a [i + j]) > 1e-10 || fabs (yule_hist_b [i + j]) > 1e-10)
            break;

    if (!j) {
        memset (yule_hist_a, 0, sizeof (yule_hist_a));
        memset (yule_hist_b, 0, sizeof (yule_hist_b));
    }

    while (samcnt--) {
        left   = (yule_hist_b [i] = samples [0]) * yule_coeff_b [0];
        right  = (yule_hist_b [i + 1] = samples [1]) * yule_coeff_b [0];
        left  += yule_hist_b [i - 2] * yule_coeff_b [1] - yule_hist_a [i - 2] * yule_coeff_a [1];
        right += yule_hist_b [i - 1] * yule_coeff_b [1] - yule_hist_a [i - 1] * yule_coeff_a [1];
        left  += yule_hist_b [i - 4] * yule_coeff_b [2] - yule_hist_a [i - 4] * yule_coeff_a [2];
        right += yule_hist_b [i - 3] * yule_coeff_b [2] - yule_hist_a [i - 3] * yule_coeff_a [2];
        left  += yule_hist_b [i - 6] * yule_coeff_b [3] - yule_hist_a [i - 6] * yule_coeff_a [3];
        right += yule_hist_b [i - 5] * yule_coeff_b [3] - yule_hist_a [i - 5] * yule_coeff_a [3];
        left  += yule_hist_b [i - 8] * yule_coeff_b [4] - yule_hist_a [i - 8] * yule_coeff_a [4];
        right += yule_hist_b [i - 7] * yule_coeff_b [4] - yule_hist_a [i - 7] * yule_coeff_a [4];
        left  += yule_hist_b [i - 10] * yule_coeff_b [5] - yule_hist_a [i - 10] * yule_coeff_a [5];
        right += yule_hist_b [i - 9] * yule_coeff_b [5] - yule_hist_a [i - 9] * yule_coeff_a [5];
        left  += yule_hist_b [i - 12] * yule_coeff_b [6] - yule_hist_a [i - 12] * yule_coeff_a [6];
        right += yule_hist_b [i - 11] * yule_coeff_b [6] - yule_hist_a [i - 11] * yule_coeff_a [6];
        left  += yule_hist_b [i - 14] * yule_coeff_b [7] - yule_hist_a [i - 14] * yule_coeff_a [7];
        right += yule_hist_b [i - 13] * yule_coeff_b [7] - yule_hist_a [i - 13] * yule_coeff_a [7];
        left  += yule_hist_b [i - 16] * yule_coeff_b [8] - yule_hist_a [i - 16] * yule_coeff_a [8];
        right += yule_hist_b [i - 15] * yule_coeff_b [8] - yule_hist_a [i - 15] * yule_coeff_a [8];
        left  += yule_hist_b [i - 18] * yule_coeff_b [9] - yule_hist_a [i - 18] * yule_coeff_a [9];
        right += yule_hist_b [i - 17] * yule_coeff_b [9] - yule_hist_a [i - 17] * yule_coeff_a [9];
        left  += yule_hist_b [i - 20] * yule_coeff_b [10] - yule_hist_a [i - 20] * yule_coeff_a [10];
        right += yule_hist_b [i - 19] * yule_coeff_b [10] - yule_hist_a [i - 19] * yule_coeff_a [10];
        samples [0] = yule_hist_a [i] = (float) left;
        samples [1] = yule_hist_a [i + 1] = (float) right;
        samples += 2;

        if ((i += 2) == 256) {
            memcpy (yule_hist_a, yule_hist_a + 236, sizeof (yule_hist_a [0]) * 20);
            memcpy (yule_hist_b, yule_hist_b + 236, sizeof (yule_hist_b [0]) * 20);
            i = 20;
        }
    }

    yule_hist_i = i;
}

// Apply both filters sequentially

static void filter_stereo_samples (float *samples, uint32_t samcnt)
{
    yule_filter_stereo_samples (samples, samcnt);
    butter_filter_stereo_samples (samples, samcnt);
}

// Update largest absolute sample value

static void calc_stereo_peak (float *samples, uint32_t samcnt, float *peak_p)
{
    float peak = 0.0;

    while (samcnt--) {
        if (samples [0] > peak)
            peak = samples [0];
        else if (-samples [0] > peak)
            peak = -samples [0];

        if (samples [1] > peak)
            peak = samples [1];
        else if (-samples [1] > peak)
            peak = -samples [1];

        samples += 2;
    }

    if (peak > *peak_p)
        *peak_p = peak;
}

// Calculate stereo rms level. Minimum value is about -100 dB for digital silence. The 90 dB
// offset is to compensate for the normalized float range and 3 dB is for stereo samples.

static double calc_stereo_rms (float *samples, uint32_t samcnt)
{
    uint32_t cnt = samcnt;
    double sum = 1e-16;

    while (cnt--) {
        sum += samples [0] * samples [0] + samples [1] * samples [1];
        samples += 2;
    }

    return 10 * log10 (sum / samcnt) + 90.0 - 3.0;
}

/////////////////////////////////////////////////////////////////////////////////
// Decimation code for properly handling DSD or PCM sample rates >= 256,000 Hz //
/////////////////////////////////////////////////////////////////////////////////

// sinc low-pass filter, cutoff = fs/12, 80 terms

static int32_t filter [] = {
    50, 464, 968, 711, -1203, -5028, -9818, -13376,
    -12870, -6021, 7526, 25238, 41688, 49778, 43050, 18447,
    -21428, -67553, -105876, -120890, -100640, -41752, 47201, 145510,
    224022, 252377, 208224, 86014, -97312, -301919, -470919, -541796,
    -461126, -199113, 239795, 813326, 1446343, 2043793, 2509064, 2763659,
    2763659, 2509064, 2043793, 1446343, 813326, 239795, -199113, -461126,
    -541796, -470919, -301919, -97312, 86014, 208224, 252377, 224022,
    145510, 47201, -41752, -100640, -120890, -105876, -67553, -21428,
    18447, 43050, 49778, 41688, 25238, 7526, -6021, -12870,
    -13376, -9818, -5028, -1203, 711, 968, 464, 50
};

#define NUM_TERMS ((int)(sizeof (filter) / sizeof (filter [0])))

typedef struct chan_state {
    int delay [NUM_TERMS], index, num_channels, ratio;
} ChanState;

static void *decimation_init (int num_channels, int ratio)
{
    ChanState *sp = malloc (sizeof (ChanState) * num_channels);
    int i;

    if (sp) {
        memset (sp, 0, sizeof (ChanState) * num_channels);

        for (i = 0; i < num_channels; ++i) {
            sp [i].num_channels = num_channels;
            sp [i].index = NUM_TERMS - ratio;
            sp [i].ratio = ratio;
        }
    }

    return sp;
}

static int decimation_run (void *context, int32_t *samples, int num_samples)
{
    int32_t *in_samples = samples, *out_samples = samples;
    ChanState *sp = (ChanState *) context;
    int num_channels, ratio, chan;

    if (!sp)
        return 0;

    num_channels = sp->num_channels;
    ratio = sp->ratio;
    chan = 0;

    while (num_samples) {
        sp = ((ChanState *) context) + chan;

        sp->delay [sp->index++] = *in_samples++;

        if (sp->index == NUM_TERMS) {
            int64_t sum = 0;
            int i;

            for (i = 0; i < NUM_TERMS; ++i)
                sum += (int64_t) filter [i] * sp->delay [i];

            *out_samples++ = (int32_t)(sum >> 24);
            memmove (sp->delay, sp->delay + ratio, sizeof (sp->delay [0]) * (NUM_TERMS - ratio));
            sp->index = NUM_TERMS - ratio;
        }

        if (++chan == num_channels) {
            num_samples--;
            chan = 0;
        }
    }

    return (int)(out_samples - samples) / num_channels;
}

static void *decimation_destroy (void *context)
{
    if (context)
        free (context);

    return NULL;
}

#ifdef _WIN32

// Convert the Unicode wide-format string into a UTF-8 string using no more
// than the specified buffer length. The wide-format string must be NULL
// terminated and the resulting string will be NULL terminated. The actual
// number of characters converted (not counting terminator) is returned, which
// may be less than the number of characters in the wide string if the buffer
// length is exceeded.

static int WideCharToUTF8 (const wchar_t *Wide, unsigned char *pUTF8, int len)
{
    const wchar_t *pWide = Wide;
    int outndx = 0;

    while (*pWide) {
        if (*pWide < 0x80 && outndx + 1 < len)
            pUTF8 [outndx++] = (unsigned char) *pWide++;
        else if (*pWide < 0x800 && outndx + 2 < len) {
            pUTF8 [outndx++] = (unsigned char) (0xc0 | ((*pWide >> 6) & 0x1f));
            pUTF8 [outndx++] = (unsigned char) (0x80 | (*pWide++ & 0x3f));
        }
        else if (outndx + 3 < len) {
            pUTF8 [outndx++] = (unsigned char) (0xe0 | ((*pWide >> 12) & 0xf));
            pUTF8 [outndx++] = (unsigned char) (0x80 | ((*pWide >> 6) & 0x3f));
            pUTF8 [outndx++] = (unsigned char) (0x80 | (*pWide++ & 0x3f));
        }
        else
            break;
    }

    pUTF8 [outndx] = 0;
    return (int)(pWide - Wide);
}

// Convert a text string into its Unicode UTF-8 format equivalent. The
// conversion is done in-place so the maximum length of the string buffer must
// be specified because the string may become longer or shorter. If the
// resulting string will not fit in the specified buffer size then it is
// truncated.

static void TextToUTF8 (void *string, int len)
{
    unsigned char *inp = string;

    // simple case: test for UTF8 BOM and if so, simply delete the BOM

    if (len > 3 && inp [0] == 0xEF && inp [1] == 0xBB && inp [2] == 0xBF) {
        memmove (inp, inp + 3, len - 3);
        inp [len - 3] = 0;
    }
    else if (* (wchar_t *) string == 0xFEFF) {
        wchar_t *temp = _wcsdup (string);

        WideCharToUTF8 (temp + 1, (unsigned char *) string, len);
        free (temp);
    }
    else {
        int max_chars = (int) strlen (string);
        wchar_t *temp = (wchar_t *) malloc ((max_chars + 1) * 2);

        MultiByteToWideChar (CP_ACP, 0, string, -1, temp, max_chars + 1);
        WideCharToUTF8 (temp, (unsigned char *) string, len);
        free (temp);
    }
}

#endif

//////////////////////////////////////////////////////////////////////////////
// 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];

    if (set_console_title) {
        file_progress = (file_index + file_progress) / num_files;
        sprintf (title, "%d%% (WvGain)", (int) ((file_progress * 100.0) + 0.5));
        DoSetConsoleTitle (title);
    }
}
