vendor voxsplit.c into lib/ — was pointing at local dev path
This commit is contained in:
parent
f77d06a80b
commit
be40ab432b
2 changed files with 719 additions and 1 deletions
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
718
make_post_sell/lib/voxsplit.c
Normal file
718
make_post_sell/lib/voxsplit.c
Normal file
|
|
@ -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 <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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue