diff --git a/make_post_sell/lib/karaoke.py b/make_post_sell/lib/karaoke.py index 2ab6aa5..3e6841f 100644 --- a/make_post_sell/lib/karaoke.py +++ b/make_post_sell/lib/karaoke.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) VOXSPLIT_SOURCE = os.environ.get( "VOXSPLIT_SOURCE_PATH", - os.path.expanduser("~/git/uncloseai-cli/audiotoys/voxsplit.c"), + os.path.join(os.path.dirname(__file__), "voxsplit.c"), ) diff --git a/make_post_sell/lib/voxsplit.c b/make_post_sell/lib/voxsplit.c new file mode 100644 index 0000000..a0a9dd0 --- /dev/null +++ b/make_post_sell/lib/voxsplit.c @@ -0,0 +1,718 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#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 [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; +}