/*
 * voxsplit.c — Spectral mid-side vocal isolation tool.
 *
 * Separates vocals from instrumental in stereo WAV files using spectral
 * mid-side decomposition with Wiener masking. No ML dependencies.
 *
 * Algorithm: Vocals are almost always panned center in professional mixes.
 *   1. STFT both channels (Hann window, 4096 samples, 75% overlap)
 *   2. Decompose each bin into mid (L+R)/2 and side (L-R)/2
 *   3. Wiener mask: vocal_mask = |mid|^s / (|mid|^s + |side|^s + eps)
 *   4. Vocals = mask * mid, Instrumental = original - vocals
 *   5. ISTFT with overlap-add reconstruction
 *
 * Zero dependencies beyond libc + libm. Calls ffmpeg for MP3 encoding.
 *
 * Usage:
 *   voxsplit song.wav -o split
 *   voxsplit song.wav --mp3 --strength 1.5 -o karaoke
 *
 * Build:
 *   gcc -O2 -o voxsplit voxsplit.c -lm
 */

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define FFT_SIZE     4096
#define HOP_SIZE     1024   /* 75% overlap: FFT_SIZE / 4 */
#define EPSILON      1e-10

/* ── Complex Arithmetic ────────────────────────────────────────── */

typedef struct { double re, im; } cpx;

static cpx cpx_add(cpx a, cpx b) { return (cpx){a.re + b.re, a.im + b.im}; }
static cpx cpx_sub(cpx a, cpx b) { return (cpx){a.re - b.re, a.im - b.im}; }

static cpx cpx_mul(cpx a, cpx b)
{
    return (cpx){a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}

static cpx cpx_scale(cpx a, double s) { return (cpx){a.re * s, a.im * s}; }
static double cpx_mag2(cpx a) { return a.re * a.re + a.im * a.im; }
static double cpx_mag(cpx a) { return sqrt(cpx_mag2(a)); }

/* ── FFT — Cooley-Tukey Radix-2 In-Place ──────────────────────── */

static void bit_reverse(cpx *buf, int n)
{
    for (int i = 1, j = 0; i < n; i++) {
        int bit = n >> 1;
        while (j & bit) { j ^= bit; bit >>= 1; }
        j ^= bit;
        if (i < j) {
            cpx tmp = buf[i];
            buf[i] = buf[j];
            buf[j] = tmp;
        }
    }
}

/* Forward FFT (sign = -1) or inverse FFT (sign = +1). */
static void fft_impl(cpx *buf, int n, int sign)
{
    bit_reverse(buf, n);

    for (int len = 2; len <= n; len <<= 1) {
        double angle = sign * 2.0 * M_PI / len;
        cpx wn = {cos(angle), sin(angle)};
        for (int i = 0; i < n; i += len) {
            cpx w = {1.0, 0.0};
            for (int j = 0; j < len / 2; j++) {
                cpx u = buf[i + j];
                cpx v = cpx_mul(w, buf[i + j + len / 2]);
                buf[i + j] = cpx_add(u, v);
                buf[i + j + len / 2] = cpx_sub(u, v);
                w = cpx_mul(w, wn);
            }
        }
    }

    if (sign == 1) {
        for (int i = 0; i < n; i++) {
            buf[i].re /= n;
            buf[i].im /= n;
        }
    }
}

static void fft_forward(cpx *buf, int n) { fft_impl(buf, n, -1); }
static void fft_inverse(cpx *buf, int n) { fft_impl(buf, n, 1); }

/* ── Window Functions ──────────────────────────────────────────── */

static void make_hann_window(double *win, int n)
{
    for (int i = 0; i < n; i++)
        win[i] = 0.5 * (1.0 - cos(2.0 * M_PI * i / n));
}

/* ── WAV I/O ───────────────────────────────────────────────────── */

static uint16_t read_le16(FILE *f)
{
    uint8_t b[2] = {0};
    if (fread(b, 1, 2, f) < 2) return 0;
    return b[0] | (b[1] << 8);
}

static uint32_t read_le32(FILE *f)
{
    uint8_t b[4] = {0};
    if (fread(b, 1, 4, f) < 4) return 0;
    return b[0] | (b[1] << 8) | (b[2] << 16) | ((uint32_t)b[3] << 24);
}

static void write_le16(FILE *f, uint16_t v)
{
    uint8_t b[2] = {v & 0xFF, (v >> 8) & 0xFF};
    fwrite(b, 1, 2, f);
}

static void write_le32(FILE *f, uint32_t v)
{
    uint8_t b[4] = {v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF};
    fwrite(b, 1, 4, f);
}

typedef struct {
    int      sample_rate;
    int      channels;
    int      bits_per_sample;
    int      n_samples;     /* total samples (all channels) */
    double  *left;          /* normalized -1..1 */
    double  *right;         /* normalized -1..1 */
    int      frames;        /* samples per channel */
} WavFile;

static int wav_read(const char *path, WavFile *wav)
{
    FILE *f = fopen(path, "rb");
    if (!f) { fprintf(stderr, "error: cannot open '%s'\n", path); return -1; }

    /* RIFF header */
    char riff[4] = {0};
    if (fread(riff, 1, 4, f) < 4 || memcmp(riff, "RIFF", 4) != 0) {
        fprintf(stderr, "error: '%s' is not a WAV file\n", path);
        fclose(f); return -1;
    }
    read_le32(f); /* file size */
    char wave[4] = {0};
    if (fread(wave, 1, 4, f) < 4 || memcmp(wave, "WAVE", 4) != 0) {
        fprintf(stderr, "error: '%s' is not a WAV file\n", path);
        fclose(f); return -1;
    }

    /* Find fmt and data chunks */
    int got_fmt = 0;
    uint32_t data_size = 0;
    int audio_format = 0;

    while (!feof(f)) {
        char chunk_id[4];
        if (fread(chunk_id, 1, 4, f) != 4) break;
        uint32_t chunk_size = read_le32(f);

        if (memcmp(chunk_id, "fmt ", 4) == 0) {
            audio_format = read_le16(f);
            wav->channels = read_le16(f);
            wav->sample_rate = read_le32(f);
            read_le32(f); /* byte rate */
            read_le16(f); /* block align */
            wav->bits_per_sample = read_le16(f);
            /* Skip extra fmt bytes */
            if (chunk_size > 16) fseek(f, chunk_size - 16, SEEK_CUR);
            got_fmt = 1;
        } else if (memcmp(chunk_id, "data", 4) == 0) {
            data_size = chunk_size;
            break; /* data follows */
        } else {
            fseek(f, chunk_size, SEEK_CUR);
        }
    }

    if (!got_fmt || data_size == 0) {
        fprintf(stderr, "error: '%s' missing fmt or data chunk\n", path);
        fclose(f); return -1;
    }

    if (audio_format != 1) {
        fprintf(stderr, "error: '%s' is not PCM (format=%d)\n", path, audio_format);
        fclose(f); return -1;
    }

    if (wav->channels != 2) {
        fprintf(stderr, "error: '%s' has %d channel(s) — voxsplit requires stereo input\n",
                path, wav->channels);
        fclose(f); return -1;
    }

    int bytes_per_sample = wav->bits_per_sample / 8;
    int block = bytes_per_sample * wav->channels;
    wav->frames = data_size / block;
    wav->n_samples = wav->frames * wav->channels;

    wav->left = malloc(wav->frames * sizeof(double));
    wav->right = malloc(wav->frames * sizeof(double));
    if (!wav->left || !wav->right) {
        fprintf(stderr, "error: out of memory (%.1f MB needed)\n",
                wav->frames * 2.0 * sizeof(double) / (1024 * 1024));
        fclose(f); return -1;
    }

    /* Read and normalize samples */
    uint8_t *raw = malloc(data_size);
    if (!raw) {
        fprintf(stderr, "error: out of memory for raw audio\n");
        fclose(f); return -1;
    }
    if (fread(raw, 1, data_size, f) < data_size) {
        fprintf(stderr, "warning: truncated audio data in '%s'\n", path);
    }
    fclose(f);

    for (int i = 0; i < wav->frames; i++) {
        double l, r;
        uint8_t *p = raw + i * block;

        if (bytes_per_sample == 2) {
            int16_t sl = (int16_t)(p[0] | (p[1] << 8));
            int16_t sr = (int16_t)(p[2] | (p[3] << 8));
            l = sl / 32768.0;
            r = sr / 32768.0;
        } else if (bytes_per_sample == 3) {
            int32_t sl = (p[0] << 8) | (p[1] << 16) | ((int32_t)(int8_t)p[2] << 24);
            int32_t sr_v = (p[3] << 8) | (p[4] << 16) | ((int32_t)(int8_t)p[5] << 24);
            l = sl / 2147483648.0;
            r = sr_v / 2147483648.0;
        } else if (bytes_per_sample == 4) {
            int32_t sl = (int32_t)(p[0] | (p[1] << 8) | (p[2] << 16) | ((uint32_t)p[3] << 24));
            int32_t sr_v = (int32_t)(p[4] | (p[5] << 8) | (p[6] << 16) | ((uint32_t)p[7] << 24));
            l = sl / 2147483648.0;
            r = sr_v / 2147483648.0;
        } else {
            fprintf(stderr, "error: unsupported bit depth %d\n", wav->bits_per_sample);
            free(raw); free(wav->left); free(wav->right);
            return -1;
        }

        wav->left[i] = l;
        wav->right[i] = r;
    }

    free(raw);
    return 0;
}

static int wav_write_stereo(const char *path, const double *left, const double *right,
                            int frames, int sample_rate)
{
    FILE *f = fopen(path, "wb");
    if (!f) { fprintf(stderr, "error: cannot create '%s'\n", path); return -1; }

    int bytes_per_sample = 2;
    int channels = 2;
    int data_size = frames * channels * bytes_per_sample;

    /* RIFF header */
    fwrite("RIFF", 1, 4, f);
    write_le32(f, 36 + data_size);
    fwrite("WAVE", 1, 4, f);

    /* fmt chunk */
    fwrite("fmt ", 1, 4, f);
    write_le32(f, 16);
    write_le16(f, 1);                                      /* PCM */
    write_le16(f, channels);
    write_le32(f, sample_rate);
    write_le32(f, sample_rate * channels * bytes_per_sample); /* byte rate */
    write_le16(f, channels * bytes_per_sample);             /* block align */
    write_le16(f, bytes_per_sample * 8);                    /* bits */

    /* data chunk */
    fwrite("data", 1, 4, f);
    write_le32(f, data_size);

    for (int i = 0; i < frames; i++) {
        double l = left[i];
        double r = right[i];

        /* Clamp */
        if (l > 1.0) l = 1.0;
        if (l < -1.0) l = -1.0;
        if (r > 1.0) r = 1.0;
        if (r < -1.0) r = -1.0;

        int16_t sl = (int16_t)(l * 32767.0);
        int16_t sr = (int16_t)(r * 32767.0);
        write_le16(f, (uint16_t)sl);
        write_le16(f, (uint16_t)sr);
    }

    fclose(f);
    return 0;
}

static void wav_free(WavFile *wav)
{
    free(wav->left);
    free(wav->right);
    wav->left = wav->right = NULL;
}

/* ── STFT / ISTFT ──────────────────────────────────────────────── */

/*
 * Compute STFT of a mono signal.
 * Returns a 2D array of complex spectra: n_frames x FFT_SIZE.
 * Sets *out_frames to the number of frames.
 */
static cpx *stft(const double *signal, int n_samples, const double *window,
                 int *out_frames)
{
    int n_frames = (n_samples - FFT_SIZE) / HOP_SIZE + 1;
    if (n_frames < 1) n_frames = 1;
    *out_frames = n_frames;

    cpx *spectra = calloc((size_t)n_frames * FFT_SIZE, sizeof(cpx));
    if (!spectra) return NULL;

    for (int f = 0; f < n_frames; f++) {
        int offset = f * HOP_SIZE;
        cpx *frame = spectra + (size_t)f * FFT_SIZE;

        for (int i = 0; i < FFT_SIZE; i++) {
            int idx = offset + i;
            double val = (idx < n_samples) ? signal[idx] : 0.0;
            frame[i] = (cpx){val * window[i], 0.0};
        }

        fft_forward(frame, FFT_SIZE);
    }

    return spectra;
}

/*
 * Inverse STFT with overlap-add.
 * Takes n_frames x FFT_SIZE complex spectra, returns reconstructed signal.
 * Sets *out_samples to length of output.
 */
static double *istft(const cpx *spectra, int n_frames, const double *window,
                     int *out_samples)
{
    int out_len = (n_frames - 1) * HOP_SIZE + FFT_SIZE;
    *out_samples = out_len;

    double *output = calloc(out_len, sizeof(double));
    double *norm = calloc(out_len, sizeof(double));
    if (!output || !norm) { free(output); free(norm); return NULL; }

    cpx *frame = malloc(FFT_SIZE * sizeof(cpx));

    for (int f = 0; f < n_frames; f++) {
        int offset = f * HOP_SIZE;

        memcpy(frame, spectra + (size_t)f * FFT_SIZE, FFT_SIZE * sizeof(cpx));
        fft_inverse(frame, FFT_SIZE);

        for (int i = 0; i < FFT_SIZE; i++) {
            int idx = offset + i;
            if (idx < out_len) {
                output[idx] += frame[i].re * window[i];
                norm[idx] += window[i] * window[i];
            }
        }
    }

    free(frame);

    /* Normalize by window sum */
    for (int i = 0; i < out_len; i++) {
        if (norm[i] > EPSILON)
            output[i] /= norm[i];
    }

    free(norm);
    return output;
}

/* ── Separation ────────────────────────────────────────────────── */

typedef struct {
    double *vocal_left;
    double *vocal_right;
    double *inst_left;
    double *inst_right;
    int     frames;
} SplitResult;

static SplitResult separate(const WavFile *wav, double strength)
{
    SplitResult result = {0};
    int n = wav->frames;

    fprintf(stderr, "  input: %d frames, %d Hz, %.1fs\n",
            n, wav->sample_rate, (double)n / wav->sample_rate);

    /* Analysis window */
    double window[FFT_SIZE];
    make_hann_window(window, FFT_SIZE);

    /* STFT both channels */
    fprintf(stderr, "  computing STFT...\n");
    int n_frames_l, n_frames_r;
    cpx *spec_l = stft(wav->left, n, window, &n_frames_l);
    cpx *spec_r = stft(wav->right, n, window, &n_frames_r);

    int n_frames = n_frames_l < n_frames_r ? n_frames_l : n_frames_r;
    fprintf(stderr, "  %d STFT frames (%d bins each)\n", n_frames, FFT_SIZE);

    /* Allocate vocal spectra (same dimensions) */
    cpx *vocal_spec_l = calloc((size_t)n_frames * FFT_SIZE, sizeof(cpx));
    cpx *vocal_spec_r = calloc((size_t)n_frames * FFT_SIZE, sizeof(cpx));

    /* Mid-side decomposition + Wiener masking */
    fprintf(stderr, "  applying Wiener mask (strength=%.2f)...\n", strength);

    for (int f = 0; f < n_frames; f++) {
        cpx *fl = spec_l + (size_t)f * FFT_SIZE;
        cpx *fr = spec_r + (size_t)f * FFT_SIZE;
        cpx *vl = vocal_spec_l + (size_t)f * FFT_SIZE;
        cpx *vr = vocal_spec_r + (size_t)f * FFT_SIZE;

        for (int b = 0; b < FFT_SIZE; b++) {
            /* Mid = (L+R)/2, Side = (L-R)/2 */
            cpx mid  = cpx_scale(cpx_add(fl[b], fr[b]), 0.5);
            cpx side = cpx_scale(cpx_sub(fl[b], fr[b]), 0.5);

            double mid_mag = cpx_mag(mid);
            double side_mag = cpx_mag(side);

            /* Wiener mask: |mid|^s / (|mid|^s + |side|^s + eps) */
            double mid_pow = pow(mid_mag, strength);
            double side_pow = pow(side_mag, strength);
            double mask = mid_pow / (mid_pow + side_pow + EPSILON);

            /* Vocal component: mask applied to mid, duplicated to both channels */
            cpx vocal = cpx_scale(mid, mask);
            vl[b] = vocal;
            vr[b] = vocal;
        }
    }

    /* ISTFT to recover vocal signals */
    fprintf(stderr, "  reconstructing vocals...\n");
    int out_l, out_r;
    double *vocal_left  = istft(vocal_spec_l, n_frames, window, &out_l);
    double *vocal_right = istft(vocal_spec_r, n_frames, window, &out_r);

    free(vocal_spec_l);
    free(vocal_spec_r);
    free(spec_l);
    free(spec_r);

    /* Output length = original length */
    int out_frames = n;
    if (out_l < out_frames) out_frames = out_l;
    if (out_r < out_frames) out_frames = out_r;

    /* Instrumental = original - vocals */
    fprintf(stderr, "  computing instrumental...\n");
    double *inst_left  = malloc(out_frames * sizeof(double));
    double *inst_right = malloc(out_frames * sizeof(double));

    for (int i = 0; i < out_frames; i++) {
        inst_left[i]  = wav->left[i]  - vocal_left[i];
        inst_right[i] = wav->right[i] - vocal_right[i];
    }

    result.vocal_left  = vocal_left;
    result.vocal_right = vocal_right;
    result.inst_left   = inst_left;
    result.inst_right  = inst_right;
    result.frames      = out_frames;
    return result;
}

static void split_free(SplitResult *r)
{
    free(r->vocal_left);
    free(r->vocal_right);
    free(r->inst_left);
    free(r->inst_right);
}

/* ── MP3 via ffmpeg ────────────────────────────────────────────── */

static int wav_to_mp3(const char *wav_path, const char *mp3_path)
{
    char cmd[2048];
    snprintf(cmd, sizeof(cmd),
             "ffmpeg -y -i \"%s\" -codec:a libmp3lame -b:a 192k \"%s\" 2>/dev/null",
             wav_path, mp3_path);
    int ret = system(cmd);
    if (ret != 0) {
        fprintf(stderr, "error: ffmpeg failed (is it installed?)\n");
        return -1;
    }
    return 0;
}

/* ── Input conversion via ffmpeg ────────────────────────────────── */

static int needs_conversion(const char *path)
{
    const char *dot = strrchr(path, '.');
    if (!dot) return 0;
    /* WAV is native — everything else gets converted */
    if (strcasecmp(dot, ".wav") == 0) return 0;
    return 1;
}

/*
 * Convert any audio/video file to stereo 16-bit WAV via ffmpeg.
 * Writes to tmp_path. Returns 0 on success.
 */
static int convert_to_wav(const char *input, char *tmp_path, int tmp_len)
{
    snprintf(tmp_path, tmp_len, "/tmp/voxsplit_%d.wav", (int)getpid());
    char cmd[2048];
    snprintf(cmd, sizeof(cmd),
             "ffmpeg -y -i \"%s\" -vn -ac 2 -ar 44100 -sample_fmt s16 \"%s\" 2>/dev/null",
             input, tmp_path);
    int ret = system(cmd);
    if (ret != 0) {
        fprintf(stderr, "error: ffmpeg could not extract audio from '%s'\n", input);
        return -1;
    }
    return 0;
}

/* ── CLI ───────────────────────────────────────────────────────── */

static void usage(const char *prog)
{
    fprintf(stderr,
        "usage: %s <input> [options]\n"
        "\n"
        "  Spectral mid-side vocal isolation. Splits stereo audio into\n"
        "  vocals and instrumental tracks.\n"
        "\n"
        "  Accepts WAV directly. MP3, MP4, FLAC, etc. converted via ffmpeg.\n"
        "\n"
        "  --strength F          separation aggressiveness (default: 1.0)\n"
        "  --mp3                 also encode MP3 via ffmpeg\n"
        "  --vocals-only         only output vocals\n"
        "  --instrumental-only   only output instrumental\n"
        "  -o PATH               output base path (default: input stem)\n"
        "\n"
        "  Output:\n"
        "    %s song.wav -o split\n"
        "      → split-vocals.wav, split-instrumental.wav\n"
        "\n"
        "    %s video.mp4 --mp3 --strength 1.5 -o karaoke\n"
        "      → extracts audio, splits, writes .wav + .mp3\n"
        "\n",
        prog, prog, prog);
}

/* Strip directory and extension from path to get stem */
static void get_stem(const char *path, char *stem, int stem_len)
{
    /* Find last slash */
    const char *base = strrchr(path, '/');
    base = base ? base + 1 : path;

    /* Find last dot */
    const char *dot = strrchr(base, '.');
    int len = dot ? (int)(dot - base) : (int)strlen(base);

    if (len >= stem_len) len = stem_len - 1;
    memcpy(stem, base, len);
    stem[len] = '\0';
}

int main(int argc, char **argv)
{
    const char *input = NULL;
    const char *output = NULL;
    double strength = 1.0;
    int do_mp3 = 0;
    int vocals_only = 0;
    int instrumental_only = 0;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--strength") == 0 && i + 1 < argc) {
            strength = atof(argv[++i]);
        } else if (strcmp(argv[i], "--mp3") == 0) {
            do_mp3 = 1;
        } else if (strcmp(argv[i], "--vocals-only") == 0) {
            vocals_only = 1;
        } else if (strcmp(argv[i], "--instrumental-only") == 0) {
            instrumental_only = 1;
        } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
            output = argv[++i];
        } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
            usage(argv[0]);
            return 0;
        } else if (argv[i][0] == '-') {
            fprintf(stderr, "error: unknown flag '%s'\n", argv[i]);
            usage(argv[0]);
            return 1;
        } else {
            input = argv[i];
        }
    }

    if (!input) {
        fprintf(stderr, "error: no input file\n");
        usage(argv[0]);
        return 1;
    }

    /* Default output base = input stem */
    char stem[512];
    if (!output) {
        get_stem(input, stem, sizeof(stem));
        output = stem;
    }

    if (strength <= 0.0) {
        fprintf(stderr, "error: --strength must be positive\n");
        return 1;
    }

    fprintf(stderr, "voxsplit: %s → %s-{vocals,instrumental}.wav\n", input, output);

    /* Convert non-WAV input via ffmpeg */
    char tmp_wav[512] = {0};
    int converted = 0;
    const char *wav_path = input;

    if (needs_conversion(input)) {
        fprintf(stderr, "  converting via ffmpeg...\n");
        if (convert_to_wav(input, tmp_wav, sizeof(tmp_wav)) != 0) return 1;
        wav_path = tmp_wav;
        converted = 1;
    }

    /* Read input */
    WavFile wav = {0};
    if (wav_read(wav_path, &wav) != 0) {
        if (converted) remove(tmp_wav);
        return 1;
    }

    /* Separate */
    SplitResult split = separate(&wav, strength);
    if (!split.vocal_left) {
        fprintf(stderr, "error: separation failed\n");
        wav_free(&wav);
        return 1;
    }

    /* Build output paths */
    char voc_wav[1024], inst_wav[1024], voc_mp3[1024], inst_mp3[1024];
    snprintf(voc_wav, sizeof(voc_wav), "%s-vocals.wav", output);
    snprintf(inst_wav, sizeof(inst_wav), "%s-instrumental.wav", output);
    snprintf(voc_mp3, sizeof(voc_mp3), "%s-vocals.mp3", output);
    snprintf(inst_mp3, sizeof(inst_mp3), "%s-instrumental.mp3", output);

    double duration = (double)split.frames / wav.sample_rate;
    int wrote = 0;

    /* Write vocals */
    if (!instrumental_only) {
        if (wav_write_stereo(voc_wav, split.vocal_left, split.vocal_right,
                             split.frames, wav.sample_rate) == 0) {
            fprintf(stderr, "  wrote %s (%.1fs)\n", voc_wav, duration);
            wrote++;
        }
        if (do_mp3) {
            if (wav_to_mp3(voc_wav, voc_mp3) == 0)
                fprintf(stderr, "  wrote %s\n", voc_mp3);
        }
    }

    /* Write instrumental */
    if (!vocals_only) {
        if (wav_write_stereo(inst_wav, split.inst_left, split.inst_right,
                             split.frames, wav.sample_rate) == 0) {
            fprintf(stderr, "  wrote %s (%.1fs)\n", inst_wav, duration);
            wrote++;
        }
        if (do_mp3) {
            if (wav_to_mp3(inst_wav, inst_mp3) == 0)
                fprintf(stderr, "  wrote %s\n", inst_mp3);
        }
    }

    fprintf(stderr, "done — %d file(s)\n", wrote + (do_mp3 ? wrote : 0));

    /* Cleanup */
    split_free(&split);
    wav_free(&wav);
    if (converted) remove(tmp_wav);
    return 0;
}
