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

// wvtest.c

// This is the main module for the WavPack command-line library tester.

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

// Threading support is required for wvtest (although it can still test
// a libwavpack that's NOT built with threading support).

#ifndef ENABLE_THREADS
#define ENABLE_THREADS
#endif

#include "../src/wavpack_local.h"   // for threading typedefs and macros
#include "utils.h"                  // for PACKAGE_VERSION, etc.
#include "md5.h"

#ifndef M_PI
#define M_PI 3.14159265358979323
#endif

static const char *sign_on = "\n"
" WVTEST  libwavpack Tester/Exerciser for WavPack  %s Version %s\n"
" Copyright (c) 2025 David Bryant.  All Rights Reserved.\n\n";

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

static const char *usage =
" Usage:   WVTEST --default|--exhaustive [-options]\n"
"          WVTEST --seektest[=n] file.wv [...] (n=runs per file, def=1)\n\n"
" Options: --default           = perform the default test suite\n"
"          --exhaustive        = perform the exhaustive test suite\n"
"          --short             = perform shorter runs of each test\n"
"          --long              = perform longer runs of each test\n"
"          --no-decode         = skip the decoding process\n"
"          --no-extras         = skip the \"extra\" modes\n"
"          --no-hybrid         = skip the hybrid modes\n"
"          --no-floats         = skip the float modes\n"
"          --no-lossy          = skip the lossy modes\n"
"          --no-speeds         = skip the speed modes (fast, high, etc.)\n"
"          --help              = display this message\n"
"          --version           = write the version to stdout\n"
"          --threads[=n]       = use multiple threads, optional 'n' must\n"
"                                 be 1 - 12, 1 = single thread only\n"
"          --write=n[-n][,...] = write specific test(s) (or range(s)) to disk\n\n"
" Web:     Visit www.wavpack.com for latest version and info\n";

#define TEST_FLAG_EXTRA_MODE(x) ((x) & TEST_FLAG_EXTRA_MASK)
#define TEST_FLAG_EXTRA_MASK            0x7
#define TEST_FLAG_FLOAT_DATA            0x8
#define TEST_FLAG_WRITE_FILE            0x10
#define TEST_FLAG_DEFAULT               0x20
#define TEST_FLAG_EXHAUSTIVE            0x40
#define TEST_FLAG_NO_FLOATS             0x80
#define TEST_FLAG_NO_HYBRID             0x100
#define TEST_FLAG_NO_EXTRAS             0x200
#define TEST_FLAG_NO_LOSSY              0x400
#define TEST_FLAG_NO_SPEEDS             0x800
#define TEST_FLAG_STORE_FLOAT_AS_INT32  0x1000
#define TEST_FLAG_STORE_INT32_AS_FLOAT  0x2000
#define TEST_FLAG_IGNORE_WVC            0x4000
#define TEST_FLAG_NO_DECODE             0x8000
#define TEST_FLAG_INT32_FILL_LOW_BITS   0x10000

static int run_test_size_modes (int wpconfig_flags, int test_flags, int base_minutes);
static int run_test_speed_modes (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds);
static int run_test_extra_modes (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds);
static int run_test (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds);

#define NUM_WRITE_RANGES 10
static struct { int start, stop; } write_ranges [NUM_WRITE_RANGES];
int number_of_ranges;

static int worker_threads;

enum generator_type { noise, tone };

struct audio_generator {
    enum generator_type type;
    union {
        struct noise_generator {
            double sum1, sum2, sum2p;               // these are changing
            double factor, scalar;                  // these are constant
        } noise_cxt;

        struct tone_generator {
            int sample_rate, samples_per_update;    // these are constant
            int high_frequency, low_frequency;      // these are constant
            double angle, velocity, acceleration;   // these are changing
            int samples_left;
        } tone_cxt;    
    } u;
};

static int seeking_test (char *filename, int32_t test_count);
static void tone_generator_init (struct audio_generator *cxt, int sample_rate, int low_freq, int high_freq);
static void noise_generator_init (struct audio_generator *cxt, double factor);
static void audio_generator_run (struct audio_generator *cxt, float *samples, int num_samples);
static void mix_samples_with_gain (float *destin, float *source, int num_samples, int num_chans, double initial_gain, double final_gain);
static void truncate_float_samples (float *samples, int num_samples, int bits);
static void float_to_integer_samples (float *samples, int num_samples, int bits);
static void float_to_32bit_integer_samples (float *samples, int num_samples, int test_flags);
static void *store_samples (void *dst, int32_t *src, int qmode, int bps, int count);
static double frandom (void);

typedef struct {
    uint32_t buffer_size, bytes_written, bytes_read, first_block_size;
    volatile unsigned char *buffer_base, *buffer_head, *buffer_tail;
    int push_back, done, error, empty_waits, full_waits;
    wp_condvar_t cond_read, cond_write;
    wp_mutex_t mutex;
    FILE *file;
} StreamingFile;

typedef struct {
    StreamingFile *wv_stream, *wvc_stream;
    unsigned char md5_decoded [16];
    uint32_t sample_count;
    int num_errors;
} WavpackDecoder;

static void initialize_stream (StreamingFile *ws, int buffer_size);
static int write_block (void *id, void *data, int32_t length);
static void flush_stream (StreamingFile *ws);
static void free_stream (StreamingFile *ws);
static WavpackStreamReader freader;

//////////////////////////////////////// main () function for CLI //////////////////////////////////////

int main (argc, argv) int argc; char **argv;
{
    int wpconfig_flags = CONFIG_MD5_CHECKSUM | CONFIG_OPTIMIZE_MONO, test_flags = 0, base_minutes = 2, res = 0;
    int seektest = 0;

    // loop through command-line arguments

    while (--argc) {
        if (**++argv == '-' && (*argv)[1] == '-' && (*argv)[2]) {
            char *long_option = *argv + 2, *long_param = long_option;

            while (*long_param)
                if (*long_param++ == '=')
                    break;

            if (!strcmp (long_option, "help")) {                        // --help
                printf ("%s", usage);
                return 0;
            }
            else if (!strcmp (long_option, "version")) {                // --version
                printf ("wvtest %s\n", PACKAGE_VERSION);
                printf ("libwavpack %s\n", WavpackGetLibraryVersionString ());
                return 0;
            }
            else if (!strcmp (long_option, "short")) {                  // --short
                base_minutes = 1;
            }
            else if (!strcmp (long_option, "long")) {                   // --long
                base_minutes = 5;
            }
            else if (!strcmp (long_option, "default")) {                // --default
                test_flags |= TEST_FLAG_DEFAULT;
            }
            else if (!strcmp (long_option, "exhaustive")) {             // --exhaustive
                test_flags |= TEST_FLAG_EXHAUSTIVE;
            }
            else if (!strcmp (long_option, "no-extras")) {              // --no-extras
                test_flags |= TEST_FLAG_NO_EXTRAS;
            }
            else if (!strcmp (long_option, "no-hybrid")) {              // --no-hybrid
                test_flags |= TEST_FLAG_NO_HYBRID;
            }
            else if (!strcmp (long_option, "no-lossy")) {               // --no-lossy
                test_flags |= TEST_FLAG_NO_LOSSY;
            }
            else if (!strcmp (long_option, "no-speeds")) {              // --no-speeds
                test_flags |= TEST_FLAG_NO_SPEEDS;
            }
            else if (!strcmp (long_option, "no-floats")) {              // --no-floats
                test_flags |= TEST_FLAG_NO_FLOATS;
            }
            else if (!strcmp (long_option, "no-decode")) {              // --no-decode
                test_flags |= TEST_FLAG_NO_DECODE;
            }
            else if (!strncmp (long_option, "write", 5)) {              // --write
                for (number_of_ranges = 0; *long_param && isdigit (*long_param) && number_of_ranges < NUM_WRITE_RANGES;) {
                    write_ranges [number_of_ranges].start = strtol (long_param, &long_param, 10);

                    if (*long_param == '-') {
                        long_param++;
                        if (isdigit (*long_param))
                            write_ranges [number_of_ranges].stop = strtol (long_param, &long_param, 10);
                        else
                            break;
                    }
                    else
                        write_ranges [number_of_ranges].stop = write_ranges [number_of_ranges].start;

                    number_of_ranges++;

                    if (*long_param == ',')
                        long_param++;
                    else
                        break;
                }

                if (*long_param || !number_of_ranges) {
                    printf ("syntax error in write specification!\n");
                    return 1;
                }
                else
                    test_flags |= TEST_FLAG_WRITE_FILE;
            }
            else if (!strncmp (long_option, "seektest", 8)) {           // --seektest[=n]
                if (*long_param)
                    seektest = strtol (long_param, NULL, 10);
                else
                    seektest = 1;

                if (seektest)
                    break;
            }
            else if (!strncmp (long_option, "threads", 7)) {            // --threads
                if (isdigit (*long_param)) {
                    // "worker_threads" doesn't include main thread, so subtract 1 from user value
                    worker_threads = strtol (long_param, &long_param, 10) - 1;

                    if (worker_threads < 0 || worker_threads > 11) {
                        printf ("specified thread count must be 1 - 12!\n");
                        return 1;
                    }
                }
                else
                    worker_threads = 4;             // 4 is a good default for now
            }
            else {
                printf ("unknown option: %s !\n", long_option);
                return 1;
            }
        }
        else {
            printf ("unknown option: %s !\n", *argv);
            return 1;
        }
    }

    if (strcmp (WavpackGetLibraryVersionString (), PACKAGE_VERSION))
        printf (version_warning, WavpackGetLibraryVersionString (), PACKAGE_VERSION);
    else
        printf (sign_on, VERSION_OS, WavpackGetLibraryVersionString ());

    if (!seektest && !(test_flags & (TEST_FLAG_DEFAULT | TEST_FLAG_EXHAUSTIVE))) {
        puts (usage);
        return 1;
    }

    if (seektest) {
        while (--argc)
            if ((res = seeking_test (*++argv, seektest)))
                break;
    }
    else {
        printf ("\n\n                          ****** pure lossless ******\n");
        res = run_test_size_modes (wpconfig_flags, test_flags, base_minutes);
        if (res) goto done;

        if (!(test_flags & TEST_FLAG_NO_HYBRID)) {
            printf ("\n\n                         ****** hybrid lossless ******\n");
            res = run_test_size_modes (wpconfig_flags | CONFIG_HYBRID_FLAG | CONFIG_CREATE_WVC, test_flags, base_minutes);
            if (res) goto done;

            if (!(test_flags & TEST_FLAG_NO_LOSSY)) {
                printf ("\n\n                          ****** hybrid lossy ******\n");
                res = run_test_size_modes (wpconfig_flags | CONFIG_HYBRID_FLAG, test_flags, base_minutes);
                if (res) goto done;

                printf ("\n\n            ****** hybrid lossless (but ignore wvc on decode) ******\n");
                res = run_test_size_modes (wpconfig_flags | CONFIG_HYBRID_FLAG | CONFIG_CREATE_WVC,
                    test_flags | TEST_FLAG_IGNORE_WVC, base_minutes);
                if (res) goto done;
            }
        }
    }

done:
    if (res)
        printf ("\ntest failed!\n\n");
    else
        printf ("\nall tests pass\n\n");

    return res;
}

// Function to stress-test the WavpackSeekSample() API. Given the specified WavPack file, perform
// the specified number of seektest runs on that file. For each test run, a different, random
// seek interval is chosen. Note that MD5 sums are calculated for each chunk interval so we
// actually verify that every sample decoded is correct. For each test run, we decode the entire
// file 4 times over, on average.

static int seeking_test (char *filename, int32_t test_count)
{
    char error [80];
    int open_flags = OPEN_WVC | OPEN_DSD_NATIVE | OPEN_ALT_TYPES;
    int64_t min_chunk_size = 256, total_samples, sample_count = 0;
    char md5_string1 [] = "????????????????????????????????";
    char md5_string2 [] = "????????????????????????????????";
    int32_t *decoded_samples, num_chans, bps, test_index, qmode;
    unsigned char md5_initial [16], md5_stored [16];
    MD5_CTX md5_global, md5_local;
    unsigned char *chunked_md5;
    WavpackContext *wpc;

    if (worker_threads)
        open_flags |= worker_threads << OPEN_THREADS_SHFT;

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

    printf ("\n-------------------- file: %s %s--------------------\n",
        filename, (WavpackGetMode (wpc) & MODE_WVC) ? "(+wvc) " : "");

    if (!wpc) {
        printf ("seeking_test(): error \"%s\" opening input file \"%s\"\n", error, filename);
        return -1;
    }

    num_chans = WavpackGetNumChannels (wpc);
    total_samples = WavpackGetNumSamples64 (wpc);
    bps = WavpackGetBytesPerSample (wpc);
    qmode = WavpackGetQualifyMode (wpc);

    if (total_samples < 2 || total_samples == -1) {
        printf ("seeking_test(): can't determine file size!\n");
        return -1;
    }

    if (qmode & QMODE_DSD_IN_BLOCKS) {
        printf ("seeking_test(): can't handle blocked DSD audio (i.e., from .dsf files)!\n");
        return -1;
    }

    // For very short files, reduce the minimum chunk size

    while (min_chunk_size > 1 && total_samples / min_chunk_size < 256)
        min_chunk_size /= 2;

    for (test_index = 0; test_index < test_count; test_index++) {
        uint32_t chunk_samples, total_chunks, chunk_count = 0, seek_count = 0;

        chunk_samples = (uint32_t) (min_chunk_size + frandom () * min_chunk_size);   // 256 - 511 (unless reduced)
        total_chunks = (uint32_t) ((total_samples + chunk_samples - 1) / chunk_samples);
        decoded_samples = malloc (sizeof (int32_t) * chunk_samples * num_chans);
        chunked_md5 = malloc (total_chunks * 16);

        if (!chunked_md5 || !decoded_samples) {
            printf ("seeking_test(): can't allocate memory!\n");
            return -1;
        }

        sample_count = chunk_count = 0;
        MD5_Init (&md5_global);

        // read the entire file, calculating the MD5 sums for the whole file and for each "chunk"

        while (1) {
            int samples = WavpackUnpackSamples (wpc, decoded_samples, chunk_samples);

            if (!samples)
                break;

            if ((sample_count += samples) > total_samples || chunk_count >= total_chunks) {
                printf ("seeking_test(): WavPack file is invalid or corrupt!\n");
                return -1;
            }

            store_samples (decoded_samples, decoded_samples, qmode, bps, samples * num_chans);
            MD5_Update (&md5_global, (unsigned char *) decoded_samples, bps * samples * num_chans);

            MD5_Init (&md5_local);
            MD5_Update (&md5_local, (unsigned char *) decoded_samples, bps * samples * num_chans);
            MD5_Final (chunked_md5 + chunk_count * 16, &md5_local);
            chunk_count++;
        }

        if (WavpackGetNumErrors (wpc)) {
            printf ("seeking_test(): decoder reported %d errors!\n", WavpackGetNumErrors (wpc));
            return -1;
        }

        if (total_samples != sample_count) {
            printf ("seeking_test(): sample count is not correct!\n");
            return -1;
        }

        if (total_chunks != chunk_count) {
            printf ("seeking_test(): chunk count is not correct (not sure if this can happen)!\n");
            return -1;
        }

        // The first time through the file we verify that the MD5 sum matches what's stored in the file
        // (if one is stored there). On subsequent tests, we verify that the whole-file MD5 sum matches
        // what we got the first time.

        if (!test_index) {
            int file_has_md5 = WavpackGetMD5Sum (wpc, md5_stored), i;

            MD5_Final (md5_initial, &md5_global);

            for (i = 0; i < 16; ++i) {
                sprintf (md5_string1 + (i * 2), "%02x", md5_stored [i]);
                sprintf (md5_string2 + (i * 2), "%02x", md5_initial [i]);
            }

            printf ("stored/actual sample count: %lld / %lld\n", (long long int) total_samples, (long long int) sample_count);
            if (file_has_md5) printf ("stored md5: %s\n", md5_string1);
            printf ("actual md5: %s\n", md5_string2);

            if (WavpackGetMode (wpc) & MODE_LOSSLESS)
                if (file_has_md5 && memcmp (md5_stored, md5_initial, sizeof (md5_stored))) {
                    printf ("seeking_test(): MD5 does not match MD5 stored in file!\n");
                    return -1;
                }
        }
        else {
            unsigned char md5_subsequent [16];

            MD5_Final (md5_subsequent, &md5_global);

            if (memcmp (md5_subsequent, md5_initial, sizeof (md5_stored))) {
                printf ("seeking_test(): MD5 does not match MD5 read initially!\n");
                return -1;
            }
        }

        // Half the time, reopen the file. This lets us catch errors caused by seeking to locations
        // that have never been decoded (at least not for this open call).

        if (frandom() < 0.5) {
            WavpackCloseFile (wpc);
            wpc = WavpackOpenFileInput (filename, error, open_flags, 0);

            if (!wpc) {
                printf ("seeking_test(): error \"%s\" reopening input file \"%s\"\n", error, filename);
                return -1;
            }
        }

        chunk_count *= 4;       // decode each chunk 4 times, on average

        while (chunk_count) {
            int start_chunk = 0, stop_chunk, current_chunk, num_chunks = 1;

            start_chunk = (int) floor (frandom () * total_chunks);
            if (start_chunk == total_chunks) start_chunk--;

            // At a minimum, we very one chunk after the seek. However, we also random chose additional
            // chunks to verify so that sometimes we verify lots of data after the seek.

            while (start_chunk + num_chunks < (int) total_chunks && frandom () < 0.667)
                num_chunks *= 2;

            if (start_chunk + num_chunks > (int) total_chunks)
                num_chunks = total_chunks - start_chunk;

            stop_chunk = start_chunk + num_chunks - 1;

            if (!WavpackSeekSample64 (wpc, (int64_t) start_chunk * chunk_samples)) {
                printf ("seeking_test(): seek error!\n");
                return -1;
            }

            for (current_chunk = start_chunk; current_chunk <= stop_chunk; ++current_chunk) {
                int samples = WavpackUnpackSamples (wpc, decoded_samples, chunk_samples);
                unsigned char md5_chunk [16];

                if (!samples) {
                    printf ("seeking_test(): seek error!\n");
                    return -1;
                }

                store_samples (decoded_samples, decoded_samples, qmode, bps, samples * num_chans);

                // if (frandom() < 0.0001)
                //     decoded_samples [(int) floor (samples * frandom())] ^= 1;

                MD5_Init (&md5_local);
                MD5_Update (&md5_local, (unsigned char *) decoded_samples, bps * samples * num_chans);
                MD5_Final (md5_chunk, &md5_local);

                if (memcmp (chunked_md5 + current_chunk * 16, md5_chunk, sizeof (md5_chunk))) {
                    printf ("seeking_test(): seek+decode error at %lld!\n", (long long int) current_chunk * chunk_samples);
                    return -1;
                }

                if (chunk_count)
                    chunk_count--;
                else
                    break;
            }

            // display the "dot" every 10 seeks

            if (++seek_count % 10 == 0) {
                if (seek_count % 640) {
                    putchar ('.'); fflush (stdout);
                }
                else
                    puts (".");
            }
        }

        printf ("\nresult: %u successful seeks on %u-sample boundaries\n", seek_count, chunk_samples);

        if (!WavpackSeekSample (wpc, 0)) {
            printf ("seeking_test(): rewind error!\n");
            return -1;
        }

        free (chunked_md5);
        free (decoded_samples);
    }

    WavpackCloseFile (wpc);
    return 0;
}

// Given a WavPack configuration and test flags, run the various combinations of
// bit-depth and channel configurations. A return value of FALSE indicates an error.

static int run_test_size_modes (int wpconfig_flags, int test_flags, int base_minutes)
{
    int res;

    printf ("\n   *** 8-bit, mono ***\n");
    res = run_test_speed_modes (wpconfig_flags, test_flags, 8, 1, base_minutes*5*60);
    if (res) return res;

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        printf ("\n   *** 16-bit, mono ***\n");
        res = run_test_speed_modes (wpconfig_flags, test_flags, 16, 1, base_minutes*5*60);
        if (res) return res;
    }

    printf ("\n   *** 16-bit, stereo ***\n");
    res = run_test_speed_modes (wpconfig_flags, test_flags, 16, 2, base_minutes*3*60);
    if (res) return res;

    if ((test_flags & TEST_FLAG_EXHAUSTIVE) && !(test_flags & TEST_FLAG_NO_FLOATS)) {
        printf ("\n   *** 16-bit (converted to float), stereo ***\n");
        res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_FLOAT_DATA, 16, 2, base_minutes*3*60);
        if (res) return res;
    }

    printf ("\n   *** 24-bit, 5.1 channels ***\n");
    res = run_test_speed_modes (wpconfig_flags, test_flags, 24, 6, base_minutes*60);
    if (res) return res;

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        if (!(test_flags & TEST_FLAG_NO_FLOATS)) {
            printf ("\n   *** 24-bit (converted to float), 5.1 channels ***\n");
            res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_FLOAT_DATA, 24, 6, base_minutes*60);
            if (res) return res;
        }
 
        printf ("\n   *** 32-bit integer (converted from float), 5.1 channels ***\n");
        res = run_test_speed_modes (wpconfig_flags, test_flags, 32, 6, base_minutes*60);
        if (res) return res;

        printf ("\n   *** 32-bit integer, 5.1 channels ***\n");
        res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_INT32_FILL_LOW_BITS, 32, 6, base_minutes*60);
        if (res) return res;

        if (!(test_flags & TEST_FLAG_NO_FLOATS)) {
            printf ("\n   *** 32-bit float stored as integer (pathological), 5.1 channels ***\n");
            res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_STORE_FLOAT_AS_INT32, 32, 6, base_minutes*60);
            if (res) return res;
        
            if (!(wpconfig_flags & CONFIG_HYBRID_FLAG)) {
                printf ("\n   *** 32-bit integer stored as float (pathological), 5.1 channels ***\n");
                res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_STORE_INT32_AS_FLOAT, 32, 6, base_minutes*60);
                if (res) return res;
            }
        }
    }

    if (!(test_flags & TEST_FLAG_NO_FLOATS)) {
        printf ("\n   *** 32-bit float, 5.1 channels ***\n");
        res = run_test_speed_modes (wpconfig_flags, test_flags | TEST_FLAG_FLOAT_DATA, 32, 6, base_minutes*60);
        if (res) return res;
    }

    return 0;
}

// Given a WavPack configuration and test flags, run the various combinations of
// speed modes (i.e, fast, high, etc). A return value of FALSE indicates an error.

static int run_test_speed_modes (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds)
{
    int res;

    if (!(test_flags & TEST_FLAG_NO_SPEEDS)) {
        res = run_test_extra_modes (wpconfig_flags | CONFIG_FAST_FLAG, test_flags, bits, num_chans, num_seconds);
        if (res) return res;
    }

    res = run_test_extra_modes (wpconfig_flags, test_flags, bits, num_chans, num_seconds);
    if (res) return res;

    if (!(test_flags & TEST_FLAG_NO_SPEEDS)) {
        res = run_test_extra_modes (wpconfig_flags | CONFIG_HIGH_FLAG, test_flags, bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (!(test_flags & TEST_FLAG_NO_SPEEDS)) {
        res = run_test_extra_modes (wpconfig_flags | CONFIG_VERY_HIGH_FLAG, test_flags, bits, num_chans, num_seconds);
        if (res) return res;
    }

    return 0;
}

// Given a WavPack configuration and test flags, run the various combinations of "extra" modes (0-6).
// Note that except for the base mode (no extra), the "default" and "exhaustive" configurations do
// different extra modes. Combining the "default" and "exhaustive" configurations does all the extra
// modes. A return value of FALSE indicates an error.

static int run_test_extra_modes (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds)
{
    int res;

    res = run_test (wpconfig_flags, test_flags, bits, num_chans, num_seconds);
    if (res) return res;

    if (test_flags & TEST_FLAG_NO_EXTRAS)
        return 0;

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (1), bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (test_flags & TEST_FLAG_DEFAULT) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (2), bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (3), bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (4), bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (test_flags & TEST_FLAG_DEFAULT) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (5), bits, num_chans, num_seconds);
        if (res) return res;
    }

    if (test_flags & TEST_FLAG_EXHAUSTIVE) {
        res = run_test (wpconfig_flags, test_flags | TEST_FLAG_EXTRA_MODE (6), bits, num_chans, num_seconds);
        if (res) return res;
    }

    return 0;
}

// Given a WavPack configuration and test flags, actually run the specified test. This entails
// generating the actual audio test data, creating the "virtual" WavPack file and writing to it,
// and spawning the thread that will read the "virtual" file and do the decoding (which is obviously
// required to verify the entire encode/decode chain). For lossless modes, and MD5 hash is used to
// verify the result, otherwise the decoder is trusted to detect and report errors (although the
// total number of samples is verified).

#define BUFFER_SIZE 1000000
#define NUM_GENERATORS 6

struct audio_channel {
    double audio_gain_hist [NUM_GENERATORS], audio_gain [NUM_GENERATORS], angle_offset;
    int lfe_flag;
};

#define SAMPLE_RATE 44100
#define ENCODE_SAMPLES 128
#define DESTIN_SAMPLES (220672)     // multiple of ENCODE_SAMPLES, long enough for temporal multithreading
#define NOISE_GAIN 0.6667
#define TONE_GAIN 0.3333

#ifdef _WIN32
static unsigned WINAPI decode_thread (LPVOID threadid);
#else
static void *decode_thread (void *threadid);
#endif

static int run_test (int wpconfig_flags, int test_flags, int bits, int num_chans, int num_seconds)
{
    static int test_number;

    double sequencing_angle = 0.0, speed = 60.0, width = 200.0, ratio, bps;
    int lossless = !(wpconfig_flags & CONFIG_HYBRID_FLAG) || ((wpconfig_flags & CONFIG_CREATE_WVC) && !(test_flags & TEST_FLAG_IGNORE_WVC));
    char md5_string1 [] = "????????????????????????????????";
    char md5_string2 [] = "????????????????????????????????";
    uint32_t total_encoded_bytes, total_encoded_samples;
    struct audio_generator generators [NUM_GENERATORS];
    int seconds = 0, samples = 0, wc = 0, chan_mask;
    char *filename = NULL, mode_string [32] = "-";
    struct audio_channel *channels;
    float *source, *destin;
    wp_thread_t thread;
    WavpackContext *out_wpc;
    WavpackConfig wpconfig;
    StreamingFile wv_stream, wvc_stream;
    WavpackDecoder wv_decoder;
    unsigned char md5_encoded [16];
    MD5_CTX md5_context;
    int i, j, k;

    if (wpconfig_flags & CONFIG_FAST_FLAG)
        strcat (mode_string, "f");
    else if (wpconfig_flags & CONFIG_HIGH_FLAG)
        strcat (mode_string, "h");
    else if (wpconfig_flags & CONFIG_VERY_HIGH_FLAG)
        strcat (mode_string, "hh");

    printf ("test %04d...", ++test_number); fflush (stdout);
    MD5_Init (&md5_context);

    noise_generator_init (&generators [0], 128.0);
    tone_generator_init (&generators [1], SAMPLE_RATE, 20, 200);
    noise_generator_init (&generators [2], 12.0);
    tone_generator_init (&generators [3], SAMPLE_RATE, 200, 2000);
    noise_generator_init (&generators [4], 1.75);
    tone_generator_init (&generators [5], SAMPLE_RATE, 2000, 20000);

    CLEAR (wpconfig);
    CLEAR (wv_decoder);
    CLEAR (wv_stream);
    CLEAR (wvc_stream);

    channels = malloc (num_chans * sizeof (*channels));
    source = malloc (ENCODE_SAMPLES * sizeof (*source));
    destin = malloc (DESTIN_SAMPLES * num_chans * sizeof (*destin));

    if (!channels || !source || !destin) {
        printf ("run_test(): can't allocate memory!\n");
        exit (-1);
    }

    memset (channels, 0, num_chans * sizeof (*channels));

    switch (num_chans) {
        case 1:
            channels [0].angle_offset = 0.0;
            chan_mask = 0x4;
            break;

        case 2:
            channels [0].angle_offset -= M_PI / 24.0;
            channels [1].angle_offset += M_PI / 24.0;
            chan_mask = 0x3;
            break;

        case 4:
            channels [0].angle_offset -= M_PI / 24.0;
            channels [1].angle_offset += M_PI / 24.0;
            channels [2].angle_offset -= 23.0 * M_PI / 24.0;
            channels [3].angle_offset += 23.0 * M_PI / 24.0;
            chan_mask = 0x33;
            break;

        case 6:
            channels [0].angle_offset -= M_PI / 24.0;
            channels [1].angle_offset += M_PI / 24.0;
            channels [3].lfe_flag = 1;
            channels [4].angle_offset -= 23.0 * M_PI / 24.0;
            channels [5].angle_offset += 23.0 * M_PI / 24.0;
            chan_mask = 0x3F;
            break;

        default:
            printf ("invalid channel count = %d\n", num_chans);
            exit (-1);
    }

    if (!(test_flags & TEST_FLAG_NO_DECODE)) {
        initialize_stream (&wv_stream, BUFFER_SIZE);
        wv_decoder.wv_stream = &wv_stream;
    }
    else
        initialize_stream (&wv_stream, 0);

    if (test_flags & TEST_FLAG_WRITE_FILE) {
        int i;

        for (i = 0; i < number_of_ranges; ++i)
            if (test_number >= write_ranges [i].start && test_number <= write_ranges [i].stop) {
                filename = malloc (32);

                if (!filename) {
                    printf ("run_test(): can't allocate memory!\n");
                    exit (-1);
                }

                sprintf (filename, "testfile-%04d.wv", test_number);

                if (((wv_stream.file = fopen (filename, "w+b")) == NULL)) {
                    printf ("can't create file %s!\n", filename);
                    free_stream (&wv_stream);
                    return 1;
                }

                break;
            }
    }

    if (wpconfig_flags & CONFIG_CREATE_WVC) {
        if (!(test_flags & (TEST_FLAG_IGNORE_WVC | TEST_FLAG_NO_DECODE))) {
            initialize_stream (&wvc_stream, BUFFER_SIZE);
            wv_decoder.wvc_stream = &wvc_stream;
        }
        else
            initialize_stream (&wvc_stream, 0);

        if (filename) {
            char *filename_c = malloc (strlen (filename) + 10);

            strcpy (filename_c, filename);
            strcat (filename_c, "c");

            if ((wvc_stream.file = fopen (filename_c, "w+b")) == NULL) {
                printf ("can't create file %s!\n", filename_c);
                free_stream (&wv_stream);
                free_stream (&wvc_stream);
                return 1;
            }

            free (filename_c);
        }
    }

    out_wpc = WavpackOpenFileOutput (write_block, &wv_stream, (wpconfig_flags & CONFIG_CREATE_WVC) ? &wvc_stream : NULL);

    if (!(test_flags & TEST_FLAG_NO_DECODE))
        wp_thread_create (thread, decode_thread, (void *) &wv_decoder);

    if (test_flags & (TEST_FLAG_FLOAT_DATA | TEST_FLAG_STORE_INT32_AS_FLOAT)) {
        wpconfig.float_norm_exp = 127;
        wpconfig.bytes_per_sample = 4;
        wpconfig.bits_per_sample = 32;
    }
    else {
        wpconfig.bytes_per_sample = (bits + 7) >> 3;
        wpconfig.bits_per_sample = bits;

        // for 32-bit integers, set the new optimize flag in the "high" modes only
        if (bits == 32 && (wpconfig_flags & (CONFIG_HIGH_FLAG | CONFIG_VERY_HIGH_FLAG)))
            wpconfig_flags |= CONFIG_OPTIMIZE_32BIT;
    }

    if (test_flags & TEST_FLAG_EXTRA_MASK) {
        sprintf (mode_string + strlen (mode_string), "x%c", '0' + (test_flags & TEST_FLAG_EXTRA_MASK));
        wpconfig.xmode = test_flags & TEST_FLAG_EXTRA_MASK;
        wpconfig_flags |= CONFIG_EXTRA_MODE;
    }

    wpconfig.sample_rate = SAMPLE_RATE;
    wpconfig.num_channels = num_chans;
    wpconfig.channel_mask = chan_mask;
    wpconfig.flags = wpconfig_flags;

    if (wpconfig_flags & CONFIG_HYBRID_FLAG) {
        if (wpconfig_flags & CONFIG_CREATE_WVC) {
            if (test_flags & TEST_FLAG_IGNORE_WVC) {
                strcat (mode_string, "b4c");
                wpconfig.bitrate = 4.0;
            }
            else {
                strcat (mode_string, "b3c");
                wpconfig.bitrate = 3.0;
            }
        }
        else {
            strcat (mode_string, "b5");
            wpconfig.bitrate = 5.0;
        }
    }

    if (worker_threads)
        wpconfig.worker_threads = worker_threads;

    WavpackSetConfiguration64 (out_wpc, &wpconfig, -1, NULL);
    WavpackPackInit (out_wpc);

    while (seconds < num_seconds) {
        int destin_samples = 0;

        while (destin_samples < DESTIN_SAMPLES && seconds < num_seconds) {
            float *destin_ptr = destin + destin_samples * num_chans;
            double translated_angle = cos (sequencing_angle) * 100.0;
            double width_scalar = pow (2.0, -width);

            for (k = 0; k < num_chans; ++k) {
                channels [k].audio_gain [0] = pow (sin (translated_angle + channels [k].angle_offset - M_PI * 1.6667) + 1.0, width) * width_scalar * NOISE_GAIN;
                channels [k].audio_gain [1] = pow (sin (translated_angle + channels [k].angle_offset - M_PI * 0.6667) + 1.0, width) * width_scalar * TONE_GAIN;
                channels [k].audio_gain [2] = pow (sin (translated_angle + channels [k].angle_offset - M_PI * 0.3333) + 1.0, width) * width_scalar * NOISE_GAIN;
                channels [k].audio_gain [3] = pow (sin (translated_angle + channels [k].angle_offset - M_PI * 1.3333) + 1.0, width) * width_scalar * TONE_GAIN;
                channels [k].audio_gain [4] = pow (sin (translated_angle + channels [k].angle_offset - M_PI) + 1.0, width) * width_scalar * NOISE_GAIN;
                channels [k].audio_gain [5] = pow (sin (translated_angle + channels [k].angle_offset) + 1.0, width) * width_scalar * TONE_GAIN;
            }

            memset (destin_ptr, 0, ENCODE_SAMPLES * num_chans * sizeof (*destin));

            for (j = 0; j < NUM_GENERATORS; ++j) {
                audio_generator_run (&generators [j], source, ENCODE_SAMPLES);

                for (k = 0; k < num_chans; ++k) {
                    if (!channels [k].lfe_flag || j < 2)
                        mix_samples_with_gain (destin_ptr + k, source, ENCODE_SAMPLES, num_chans, channels [k].audio_gain_hist [j], channels [k].audio_gain [j]);

                    channels [k].audio_gain_hist [j] = channels [k].audio_gain [j];
                }
            }

            sequencing_angle += 2.0 * M_PI / SAMPLE_RATE / speed * ENCODE_SAMPLES;
            if (sequencing_angle > M_PI) sequencing_angle -= M_PI * 2.0;

            if ((samples += ENCODE_SAMPLES) >= SAMPLE_RATE) {
                samples -= SAMPLE_RATE;
                ++seconds;

                if (!(wc & 1)) {
                    if (width > 1.0) width *= 0.875;
                    else if (width > 0.125) width -= 0.125;
                    else {
                        width = 0.0;
                        wc++;
                    }
                }
                else {
                    if (width < 1.0) width += 0.125;
                    else if (width < 200.0) width *= 1.125;
                    else wc++;
                }
            }

            destin_samples += ENCODE_SAMPLES;
        }

        if (test_flags & TEST_FLAG_FLOAT_DATA) {
            if (bits <= 25)
                truncate_float_samples (destin, destin_samples * num_chans, bits);
            else if (bits != 32) {
                printf ("invalid bits configuration\n");
                exit (-1);
            }
        }
        else if (!(test_flags & TEST_FLAG_STORE_FLOAT_AS_INT32)) {
            if (bits < 32)
                float_to_integer_samples (destin, destin_samples * num_chans, bits);
            else if (bits == 32)
                float_to_32bit_integer_samples (destin, destin_samples * num_chans, test_flags);
            else {
                printf ("invalid bits configuration\n");
                exit (-1);
            }
        }

        WavpackPackSamples (out_wpc, (int32_t *) destin, destin_samples);
        store_samples (destin, (int32_t *) destin, 0, wpconfig.bytes_per_sample, destin_samples * num_chans);
        MD5_Update (&md5_context, (unsigned char *) destin, wpconfig.bytes_per_sample * destin_samples * num_chans);
    }

    WavpackFlushSamples (out_wpc);
    MD5_Final (md5_encoded, &md5_context);

    if (wpconfig.flags & CONFIG_MD5_CHECKSUM) {
        WavpackStoreMD5Sum (out_wpc, md5_encoded);
        WavpackFlushSamples (out_wpc);
    }

    WavpackCloseFile (out_wpc);

    free (channels);
    free (source);
    free (destin);

    if ((wpconfig_flags & CONFIG_CREATE_WVC) && !(test_flags & TEST_FLAG_IGNORE_WVC))
        total_encoded_bytes = wv_stream.bytes_written + wvc_stream.bytes_written;
    else
        total_encoded_bytes = wv_stream.bytes_written;

    total_encoded_samples = seconds * SAMPLE_RATE + samples;
    ratio = total_encoded_bytes / ((float) total_encoded_samples * wpconfig.bytes_per_sample * num_chans);
    bps = total_encoded_bytes * 8 / ((float) total_encoded_samples * num_chans);

    flush_stream (&wv_stream);
    flush_stream (&wvc_stream);

    if (!(test_flags & TEST_FLAG_NO_DECODE))
        wp_thread_join (thread);

    if (!(test_flags & TEST_FLAG_NO_DECODE)) {
        for (i = 0; i < 16; ++i) {
            sprintf (md5_string1 + (i * 2), "%02x", md5_encoded [i]);
            sprintf (md5_string2 + (i * 2), "%02x", wv_decoder.md5_decoded [i]);
        }

        if (wv_decoder.num_errors || wv_decoder.sample_count != total_encoded_samples ||
            (lossless && memcmp (md5_encoded, wv_decoder.md5_decoded, sizeof (md5_encoded)))) {
                printf ("\n---------------------------------------------\n");
                printf ("enc/dec sample count: %u / %u\n", total_encoded_samples, wv_decoder.sample_count);
                printf ("encoded md5: %s\n", md5_string1);
                printf ("decoded md5: %s\n", md5_string2);
                printf ("reported decode errors: %d\n", wv_decoder.num_errors);
                printf ("---------------------------------------------\n");
                return wv_decoder.num_errors + 1;
        }
    }

    free_stream (&wv_stream);
    free_stream (&wvc_stream);

    printf ("pass (%8s, %.2f%%, %.2f bps, %s)\n", mode_string, 100.0 - ratio * 100.0, bps, md5_string2);

    return 0;
}

// Thread / function that opens a virtual WavPack file, decodes it and calculates the MD5 hash of the
// decoded audio data.

#define DECODE_SAMPLES 200000       // long enough for temporal multithreading

#ifdef _WIN32
static unsigned WINAPI decode_thread (LPVOID threadid)
#else
static void *decode_thread (void *threadid)
#endif
{
    WavpackDecoder *wd = (WavpackDecoder *) threadid;
    char error [80];
    WavpackContext *wpc;
    int32_t *decoded_samples, num_chans, bps;
    MD5_CTX md5_context;
    int open_flags = 0;

    if (worker_threads)
        open_flags |= worker_threads << OPEN_THREADS_SHFT;

    wpc = WavpackOpenFileInputEx (&freader, wd->wv_stream, wd->wvc_stream, error, open_flags, 0);

    if (!wpc) {
        printf ("decode_thread(): error \"%s\" opening input file\n", error);
        wd->num_errors = 1;
        wp_thread_exit (0);
    }

    MD5_Init (&md5_context);
    num_chans = WavpackGetNumChannels (wpc);
    bps = WavpackGetBytesPerSample (wpc);

    decoded_samples = malloc (sizeof (int32_t) * DECODE_SAMPLES * num_chans);

    if (!decoded_samples) {
        printf ("decode_thread(): can't allocate memory!\n");
        exit (-1);
    }

    while (1) {
        int samples = WavpackUnpackSamples (wpc, decoded_samples, DECODE_SAMPLES);

        if (!samples)
            break;

        store_samples (decoded_samples, decoded_samples, 0, bps, samples * num_chans);
        MD5_Update (&md5_context, (unsigned char *) decoded_samples, bps * samples * num_chans);
        wd->sample_count += samples;
    }

    MD5_Final (wd->md5_decoded, &md5_context);
    wd->num_errors = WavpackGetNumErrors (wpc);
    free (decoded_samples);
    WavpackCloseFile (wpc);
    wp_thread_exit (0);
    return 0;
}

// This code implements a simple virtual "file" so that we can have a WavPack encoding process and
// a WavPack decoding process running at the same time (using Pthreads).

static int write_block (void *id, void *data, int32_t length)
{
    StreamingFile *ws = (StreamingFile *) id;
    unsigned char *data_ptr = data;

    if (!ws || !data || !length)
        return 0;

//    if (frandom() < .0001)
//        ((char *) data) [(int) floor (length * frandom())] ^= 1;

    if (!ws->first_block_size)
        ws->first_block_size = length;

    ws->bytes_written += length;

    if (ws->file && !ws->error) {
        if (!fwrite (data, 1, length, ws->file)) {
            ws->error = 1;
            fclose (ws->file);
            ws->file = NULL;
        }
    }

    if (!ws->buffer_size)       // if no buffer, just swallow data silently
        return 1;

    wp_mutex_obtain (ws->mutex);

    while (length) {
        int32_t bytes_available = (int32_t) (ws->buffer_tail - ws->buffer_head - 1);
        int32_t bytes_to_copy = length;

        if (bytes_available < 0)
            bytes_available += ws->buffer_size;

        if (bytes_available < bytes_to_copy)
            bytes_to_copy = bytes_available;

        if (ws->buffer_head + bytes_to_copy > ws->buffer_base + ws->buffer_size)
            bytes_to_copy = (int32_t) (ws->buffer_base + ws->buffer_size - ws->buffer_head);

        if (!bytes_to_copy) {
            ws->full_waits++;
            wp_condvar_wait (ws->cond_read, ws->mutex);
            continue;
        }

        memcpy ((void *) ws->buffer_head, data_ptr, bytes_to_copy);

        if ((ws->buffer_head += bytes_to_copy) == ws->buffer_base + ws->buffer_size)
            ws->buffer_head = ws->buffer_base;

        data_ptr += bytes_to_copy;
        length -= bytes_to_copy;
        wp_condvar_signal (ws->cond_write);
    }

    wp_mutex_release (ws->mutex);

    return 1;
}

static int32_t read_bytes (void *id, void *data, int32_t bcount)
{
    StreamingFile *ws = (StreamingFile *) id;
    unsigned char *data_ptr = data;

    wp_mutex_obtain (ws->mutex);

    while (bcount) {
        if (ws->push_back) {
            *data_ptr++ = ws->push_back;
            ws->push_back = 0;
            bcount--;
            wp_condvar_signal (ws->cond_read);
        }
        else if (ws->buffer_head != ws->buffer_tail) {
            int bytes_available = (int) (ws->buffer_head - ws->buffer_tail);
            int32_t bytes_to_copy = bcount;

            if (bytes_available < 0)
                bytes_available += ws->buffer_size;

            if (bytes_available < bytes_to_copy)
                bytes_to_copy = bytes_available;

            if (ws->buffer_tail + bytes_to_copy > ws->buffer_base + ws->buffer_size)
                bytes_to_copy = (int32_t) (ws->buffer_base + ws->buffer_size - ws->buffer_tail);

            memcpy (data_ptr, (void *) ws->buffer_tail, bytes_to_copy);

            if ((ws->buffer_tail += bytes_to_copy) == ws->buffer_base + ws->buffer_size)
                ws->buffer_tail = ws->buffer_base;

            ws->bytes_read += bytes_to_copy;
            data_ptr += bytes_to_copy;
            bcount -= bytes_to_copy;
            wp_condvar_signal (ws->cond_read);
        }
        else if (ws->done)
            break;
        else {
            ws->empty_waits++;
            wp_condvar_wait (ws->cond_write, ws->mutex);
        }
    }

    wp_mutex_release (ws->mutex);

    return (int32_t) (data_ptr - (unsigned char *) data);
}

static uint32_t get_pos (void *id)
{
    return -1;
}

static int set_pos_abs (void *id, uint32_t pos)
{
    return 0;
}

static int set_pos_rel (void *id, int32_t delta, int mode)
{
    return -1;
}

static int push_back_byte (void *id, int c)
{
    StreamingFile *ws = (StreamingFile *) id;

    if (!ws->push_back)
        return ws->push_back = c;
    else
        return EOF;
}

static uint32_t get_length (void *id)
{
    return 0;
}

static int can_seek (void *id)
{
    return 0;
}

static WavpackStreamReader freader = {
    read_bytes, get_pos, set_pos_abs, set_pos_rel, push_back_byte, get_length, can_seek,
};

static void initialize_stream (StreamingFile *ws, int buffer_size)
{
    if (buffer_size) {
        ws->buffer_base = malloc (ws->buffer_size = buffer_size);
        ws->buffer_head = ws->buffer_tail = ws->buffer_base;
        wp_condvar_init (ws->cond_write);
        wp_condvar_init (ws->cond_read);
        wp_mutex_init (ws->mutex);
    }
}

static void flush_stream (StreamingFile *ws)
{
    if (ws->buffer_base) {
        wp_mutex_obtain (ws->mutex);
        ws->done = 1;
        wp_condvar_signal (ws->cond_write);
        wp_mutex_release (ws->mutex);
    }
}

static void free_stream (StreamingFile *ws)
{
    if (ws->file) {
        fclose (ws->file);
        ws->file = NULL;
    }

    if (ws->buffer_base) {
        free ((void *) ws->buffer_base);
        ws->buffer_base = NULL;
    }
}

// Helper utilities for generating the audio used for testing.

// Return a random value in the range: 0.0 <= n < 1.0

static double frandom (void)
{
    static uint64_t random = 0x3141592653589793ULL;
    random = ((random << 4) - random) ^ 1;
    random = ((random << 4) - random) ^ 1;
    random = ((random << 4) - random) ^ 1;
    return (random >> 32) / 4294967296.0;
}

static void tone_generator_init (struct audio_generator *cxt, int sample_rate, int low_freq, int high_freq)
{
    struct tone_generator *tone_cxt = &cxt->u.tone_cxt;

    memset (cxt, 0, sizeof (*cxt));
    cxt->type = tone;

    tone_cxt->sample_rate = sample_rate;
    tone_cxt->high_frequency = high_freq;
    tone_cxt->low_frequency = low_freq;
    tone_cxt->samples_per_update = sample_rate / low_freq * 4;
}

static void tone_generator_run (struct tone_generator *cxt, float *samples, int num_samples)
{
    double target_frequency, target_velocity;

    while (num_samples--) {
        if (!cxt->samples_left) {
            cxt->samples_left = cxt->samples_per_update;

            target_frequency = cxt->low_frequency * pow (cxt->high_frequency / cxt->low_frequency, frandom ());
            target_velocity = (M_PI * 2.0) / ((float) cxt->sample_rate / target_frequency);
            cxt->acceleration = (target_velocity - cxt->velocity) / cxt->samples_left;
        }

        *samples++ = (float) sin (cxt->angle += cxt->velocity += cxt->acceleration);
        if (cxt->angle > M_PI) cxt->angle -= M_PI * 2.0;
        cxt->samples_left--;
    }
}

static void noise_generator_init (struct audio_generator *cxt, double factor)
{
    struct noise_generator *noise_cxt = &cxt->u.noise_cxt;

    memset (cxt, 0, sizeof (*cxt));
    cxt->type = noise;

    noise_cxt->scalar = factor * factor * factor * sqrt (factor) / (2.0 + factor * factor);
    noise_cxt->factor = factor;
}

static void noise_generator_run (struct noise_generator *cxt, float *samples, int num_samples)
{
    while (num_samples--) {
        double source = (frandom () - 0.5) * cxt->scalar;
        cxt->sum1 += (source - cxt->sum1) / cxt->factor;
        cxt->sum2 += (cxt->sum1 - cxt->sum2) / cxt->factor;
        *samples++ = (float) (cxt->sum2 - cxt->sum2p);
        cxt->sum2p = cxt->sum2;
    }
}

static void audio_generator_run (struct audio_generator *cxt, float *samples, int num_samples)
{
    switch (cxt->type) {
        case noise:
            noise_generator_run (&cxt->u.noise_cxt, samples, num_samples);
            break;

        case tone:
            tone_generator_run (&cxt->u.tone_cxt, samples, num_samples);
            break;

        default:
            printf ("bad audio generator type!\n");
            exit (-1);
    }
}

static void mix_samples_with_gain (float *destin, float *source, int num_samples, int num_chans, double initial_gain, double final_gain)
{
    double delta_gain = (final_gain - initial_gain) / num_samples;
    double gain = initial_gain - delta_gain;

    while (num_samples--) {
        *destin += *source++ * (float) (gain += delta_gain);
        destin += num_chans;
    }
}

static void truncate_float_samples (float *samples, int num_samples, int bits)
{
    int isample, imin = -(1 << (bits - 1)), imax = (1 << (bits - 1)) - 1;
    double scalar = (double) (1 << (bits - 1));

    while (num_samples--) {
        if (*samples >= 1.0)
            isample = imax;
        else if (*samples <= -1.0)
            isample = imin;
        else
            isample = (int) floor (*samples * scalar);

        *samples++ = (float) (isample / scalar);
    } 
}

static void float_to_integer_samples (float *samples, int num_samples, int bits)
{
    int isample, imin = -(1 << (bits - 1)), imax = (1 << (bits - 1)) - 1;
    double scalar = (double) (1 << (bits - 1));
    int ishift = (8 - (bits & 0x7)) & 0x7;

    while (num_samples--) {
        if (*samples >= 1.0)
            isample = imax;
        else if (*samples <= -1.0)
            isample = imin;
        else
            isample = (int) floor (*samples * scalar);

        *(int32_t *)samples = (uint32_t) isample << ishift;
        samples++;
    } 
}

static void float_to_32bit_integer_samples (float *samples, int num_samples, int test_flags)
{
    int isample, imin = 0x8000000, imax = 0x7fffffff;
    double scalar = 2147483648.0;

    while (num_samples--) {
        if (*samples >= 1.0)
            isample = imax;
        else if (*samples <= -1.0)
            isample = imin;
        else
            isample = (int) floor (*samples * scalar);

        // if there are trailing zeros, fill them in with random data (if enabled)

        if ((test_flags & TEST_FLAG_INT32_FILL_LOW_BITS) && isample && !(isample & 1)) {
            int tzeros = 1;

            while (!((isample >>= 1) & 1))
                tzeros++;

            while (tzeros--)
                isample = ((unsigned int) isample << 1) + ((frandom() > 0.5) ? 1 : 0);
        }

        *(int32_t *)samples = isample;
        samples++;
    } 
}

// Code to store samples. Source is an array of int32_t data (which is what WavPack uses
// internally), but the destination can have from 1 to 4 bytes per sample. Also, the destination
// data is assumed to be little-endian and signed, except for byte data which is unsigned (these
// are WAV file defaults). The endian and signedness can be overridden with the qmode flags
// to support other formats.

static void *store_little_endian_unsigned_samples (void *dst, int32_t *src, int bps, int count);
static void *store_little_endian_signed_samples (void *dst, int32_t *src, int bps, int count);
static void *store_big_endian_unsigned_samples (void *dst, int32_t *src, int bps, int count);
static void *store_big_endian_signed_samples (void *dst, int32_t *src, int bps, int count);

static void *store_samples (void *dst, int32_t *src, int qmode, int bps, int count)
{
    if (qmode & QMODE_BIG_ENDIAN) {
        if ((qmode & QMODE_UNSIGNED_WORDS) || (bps == 1 && !(qmode & QMODE_SIGNED_BYTES)))
            return store_big_endian_unsigned_samples (dst, src, bps, count);
        else
            return store_big_endian_signed_samples (dst, src, bps, count);
    }
    else if ((qmode & QMODE_UNSIGNED_WORDS) || (bps == 1 && !(qmode & (QMODE_SIGNED_BYTES | QMODE_DSD_AUDIO))))
        return store_little_endian_unsigned_samples (dst, src, bps, count);
    else
        return store_little_endian_signed_samples (dst, src, bps, count);
}

static void *store_little_endian_unsigned_samples (void *dst, int32_t *src, int bps, int count)
{
    unsigned char *dptr = dst;
    int32_t temp;

    switch (bps) {

        case 1:
            while (count--)
                *dptr++ = *src++ + 0x80;

            break;

        case 2:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++ + 0x8000);
                *dptr++ = (unsigned char) (temp >> 8);
            }

            break;

        case 3:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++ + 0x800000);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) (temp >> 16);
            }

            break;

        case 4:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++ + 0x80000000);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) (temp >> 16);
                *dptr++ = (unsigned char) (temp >> 24);
            }

            break;
    }

    return dptr;
}

static void *store_little_endian_signed_samples (void *dst, int32_t *src, int bps, int count)
{
    unsigned char *dptr = dst;
    int32_t temp;

    switch (bps) {

        case 1:
            while (count--)
                *dptr++ = *src++;

            break;

        case 2:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++);
                *dptr++ = (unsigned char) (temp >> 8);
            }

            break;

        case 3:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) (temp >> 16);
            }

            break;

        case 4:
            while (count--) {
                *dptr++ = (unsigned char) (temp = *src++);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) (temp >> 16);
                *dptr++ = (unsigned char) (temp >> 24);
            }

            break;
    }

    return dptr;
}

static void *store_big_endian_unsigned_samples (void *dst, int32_t *src, int bps, int count)
{
    unsigned char *dptr = dst;
    int32_t temp;

    switch (bps) {

        case 1:
            while (count--)
                *dptr++ = *src++ + 0x80;

            break;

        case 2:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++ + 0x8000) >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;

        case 3:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++ + 0x800000) >> 16);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;

        case 4:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++ + 0x80000000) >> 24);
                *dptr++ = (unsigned char) (temp >> 16);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;
    }

    return dptr;
}

static void *store_big_endian_signed_samples (void *dst, int32_t *src, int bps, int count)
{
    unsigned char *dptr = dst;
    int32_t temp;

    switch (bps) {

        case 1:
            while (count--)
                *dptr++ = *src++;

            break;

        case 2:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++) >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;

        case 3:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++) >> 16);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;

        case 4:
            while (count--) {
                *dptr++ = (unsigned char) ((temp = *src++) >> 24);
                *dptr++ = (unsigned char) (temp >> 16);
                *dptr++ = (unsigned char) (temp >> 8);
                *dptr++ = (unsigned char) temp;
            }

            break;
    }

    return dptr;
}
