/*
Convert Jamulus .w64 recordings from float to 16-bit integer.

I usually run TimeMachine alongside Jamulus so I can capture the audio being
received when I've heard something interesting. TimeMachine writes output using
libsndfile as .w64 files containing 32-bit float samples by default. However,
Jamulus only works in 16-bit signed integers internally, so there's no
advantage in the extra precision -- and FLAC can't handle float files.

(The recordings often also have a chunk of digital silence at the start from
TimeMachine's buffer, but if you're going to compress the files this will be
packed efficiently anyway.)
*/

#include <sndfile.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define warn(...) \
    do { \
        fprintf(stderr, "warning: " __VA_ARGS__); \
        fprintf(stderr, "\n"); \
    } while (false)
#define die(...) \
    do { \
        fprintf(stderr, "error: " __VA_ARGS__); \
        fprintf(stderr, "\n"); \
        exit(1); \
    } while (false)

void convert_file(const char *infn, const char *outfn) {
    // Open the input file.
    SF_INFO ininfo = { 0 };
    SNDFILE *inf = sf_open(infn, SFM_READ, &ininfo);
    if (inf == NULL) {
        die("cannot open (%s): %s", sf_strerror(NULL), infn);
    }

    // Check the format is what we expect
    if ((ininfo.format & SF_FORMAT_SUBMASK) != SF_FORMAT_FLOAT) {
        die("unexpected format: %s", infn);
    }

    // Disable automatic normalisation.
    sf_command(inf, SFC_SET_NORM_FLOAT, NULL, SF_FALSE);

    // Open the output file, in RF64 WAV format.
    SF_INFO outinfo = ininfo;
    outinfo.format = SF_FORMAT_RF64 | SF_FORMAT_PCM_16;
    SNDFILE *outf = sf_open(outfn, SFM_WRITE, &outinfo);
    if (outf == NULL) {
        die("cannot open (%s): %s", sf_strerror(NULL), outfn);
    }

    // Write small files as regular WAV.
    sf_command(outf, SFC_RF64_AUTO_DOWNGRADE, NULL, SF_TRUE);

    size_t clipped = 0;

    const size_t blocksize = (1 << 20) * ininfo.channels;
    float *fbuf = malloc(blocksize * sizeof *fbuf);
    short *sbuf = malloc(blocksize * sizeof *sbuf);
    if (fbuf == NULL || sbuf == NULL) {
        die("malloc failed");
    }

    while (true) {
        sf_count_t icount = sf_read_float(inf, fbuf, blocksize);
        if (icount == 0) {
            break;
        }

        for (int i = 0; i < icount; i++) {
            // Jamulus sends to JACK by doing (float) shortval / 32767,
            // which means that -32768 will be less than -1.0.
            // Convert back to the original samples, clipping any that were
            // outside the expected range.
            float scaled = fbuf[i] * 32767.0;
            if (scaled < -32768.0) {
                sbuf[i] = -32768;
                clipped++;
            } else if (scaled > 32767.0) {
                sbuf[i] = 32767;
                clipped++;
            } else {
                sbuf[i] = scaled;
            }
        }

        sf_count_t ocount = sf_write_short(outf, sbuf, icount);
        if (ocount != icount) {
            die("writing failed (%s): %s", sf_strerror(outf), outfn);
        }
    }

    free(fbuf);
    free(sbuf);

    if (sf_error(inf) != SF_ERR_NO_ERROR) {
        die("reading failed (%s): %s", sf_strerror(inf), infn);
    }

    // Close both files.
    if (sf_close(inf) != 0) {
        die("cannot close (%s): %s", sf_strerror(inf), infn);
    }
    if (sf_close(outf) != 0) {
        die("cannot close (%s): %s", sf_strerror(outf), outfn);
    }

    if (clipped > 0) {
        warn("%zd samples clipped: %s", clipped, infn);
    }
}

int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++) {
        char *outfn = strdup(argv[i]);

        // Replace .w64 with .wav.
        size_t len = strlen(outfn);
        if (len < 4 || strcmp(&outfn[len - 4], ".w64") != 0) {
            die("doesn't end in .w64: %s", outfn);
        }
        outfn[len - 2] = 'a';
        outfn[len - 1] = 'v';

        if (access(outfn, F_OK) == 0) {
            warn("already exists: %s", outfn);
            continue;
        }

        convert_file(argv[i], outfn);
        free(outfn);
    }

    return 0;
}
