Initial release
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
||||
#pragma once
|
||||
// audio-io.h: WAV read/write for qwentts.cpp.
|
||||
// Reads any WAV (PCM16 / PCM24 / float32, mono or stereo, any rate).
|
||||
// Writes mono WAV in S16, S24 or F32 at the source sample rate.
|
||||
// Internal pipelines : planar stereo float [L:T][R:T] for reads,
|
||||
// flat mono float [T] for writes (qwen output is mono only).
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// wav.h: WAV reader (returns interleaved, we deinterleave below)
|
||||
#include "wav.h"
|
||||
|
||||
// audio-resample.h: sample rate conversion
|
||||
#include "audio-resample.h"
|
||||
|
||||
// case-insensitive extension check
|
||||
static bool audio_io_ends_with(const char * str, const char * suffix) {
|
||||
int slen = (int) strlen(str);
|
||||
int xlen = (int) strlen(suffix);
|
||||
if (slen < xlen) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < xlen; i++) {
|
||||
char a = str[slen - xlen + i];
|
||||
char b = suffix[i];
|
||||
if (a >= 'A' && a <= 'Z') {
|
||||
a += 32;
|
||||
}
|
||||
if (b >= 'A' && b <= 'Z') {
|
||||
b += 32;
|
||||
}
|
||||
if (a != b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load entire file into memory. Caller frees the returned pointer.
|
||||
static uint8_t * audio_io_load_file(const char * path, size_t * size_out) {
|
||||
*size_out = 0;
|
||||
FILE * fp = fopen(path, "rb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "[Audio] Cannot open %s\n", path);
|
||||
return NULL;
|
||||
}
|
||||
fseek(fp, 0, SEEK_END);
|
||||
long fsize = ftell(fp);
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
uint8_t * buf = (uint8_t *) malloc((size_t) fsize);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
size_t nr = fread(buf, 1, (size_t) fsize, fp);
|
||||
fclose(fp);
|
||||
if (nr != (size_t) fsize) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*size_out = (size_t) fsize;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Decode WAV from memory buffer. Returns planar stereo float [L:T][R:T].
|
||||
static float * audio_io_read_wav_buf(const uint8_t * data, size_t size, int * T_out, int * sr_out) {
|
||||
*T_out = 0;
|
||||
*sr_out = 0;
|
||||
|
||||
int T = 0, sr = 0;
|
||||
float * interleaved = read_wav_buf(data, size, &T, &sr);
|
||||
if (!interleaved) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
float * planar = (float *) malloc((size_t) T * 2 * sizeof(float));
|
||||
if (!planar) {
|
||||
free(interleaved);
|
||||
return NULL;
|
||||
}
|
||||
for (int t = 0; t < T; t++) {
|
||||
planar[t] = interleaved[t * 2 + 0];
|
||||
planar[T + t] = interleaved[t * 2 + 1];
|
||||
}
|
||||
free(interleaved);
|
||||
|
||||
*T_out = T;
|
||||
*sr_out = sr;
|
||||
return planar;
|
||||
}
|
||||
|
||||
// Read WAV file. Returns planar stereo float [L: T][R: T]. Caller frees.
|
||||
static float * audio_read(const char * path, int * T_out, int * sr_out) {
|
||||
size_t size = 0;
|
||||
uint8_t * buf = audio_io_load_file(path, &size);
|
||||
if (!buf) {
|
||||
*T_out = 0;
|
||||
*sr_out = 0;
|
||||
return NULL;
|
||||
}
|
||||
float * result = audio_io_read_wav_buf(buf, size, T_out, sr_out);
|
||||
free(buf);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read WAV, resample to target_sr, downmix to mono.
|
||||
// Returns a flat buffer of T floats at target_sr mono. Caller frees.
|
||||
static float * audio_read_mono(const char * path, int target_sr, int * T_out) {
|
||||
int T = 0;
|
||||
int sr = 0;
|
||||
float * raw = audio_read(path, &T, &sr);
|
||||
if (!raw) {
|
||||
*T_out = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Resample planar stereo to target_sr first to keep both channels
|
||||
// coherent when the source rate differs.
|
||||
float * stereo_rs = raw;
|
||||
int T_rs = T;
|
||||
if (sr != target_sr) {
|
||||
fprintf(stderr, "[Audio-Resample] %d Hz -> %d Hz, %d samples...\n", sr, target_sr, T);
|
||||
int T_new = 0;
|
||||
float * resampled = audio_resample(raw, T, sr, target_sr, 2, &T_new);
|
||||
free(raw);
|
||||
if (!resampled) {
|
||||
fprintf(stderr, "[Audio-Resample] Resample failed\n");
|
||||
*T_out = 0;
|
||||
return NULL;
|
||||
}
|
||||
fprintf(stderr, "[Audio-Resample] Done: %d -> %d samples\n", T, T_new);
|
||||
stereo_rs = resampled;
|
||||
T_rs = T_new;
|
||||
}
|
||||
|
||||
// Downmix planar [L:T][R:T] to mono = 0.5 * (L + R).
|
||||
float * mono = (float *) malloc((size_t) T_rs * sizeof(float));
|
||||
if (!mono) {
|
||||
free(stereo_rs);
|
||||
*T_out = 0;
|
||||
return NULL;
|
||||
}
|
||||
const float * left = stereo_rs;
|
||||
const float * right = stereo_rs + (size_t) T_rs;
|
||||
for (int i = 0; i < T_rs; i++) {
|
||||
mono[i] = 0.5f * (left[i] + right[i]);
|
||||
}
|
||||
free(stereo_rs);
|
||||
|
||||
*T_out = T_rs;
|
||||
return mono;
|
||||
}
|
||||
|
||||
// WAV output format
|
||||
enum WavFormat {
|
||||
WAV_S16, // 16-bit signed integer PCM (classic RIFF, default)
|
||||
WAV_S24, // 24-bit signed integer PCM (classic RIFF)
|
||||
WAV_F32, // 32-bit IEEE 754 float (classic RIFF, fmt_tag=3)
|
||||
};
|
||||
|
||||
// Parse a CLI format string into a WavFormat. Accepts: wav16, wav24, wav32.
|
||||
// Returns false on unknown format.
|
||||
static bool audio_parse_format(const char * s, WavFormat & wav_fmt) {
|
||||
if (!s) {
|
||||
return false;
|
||||
}
|
||||
if (!strcmp(s, "wav16")) {
|
||||
wav_fmt = WAV_S16;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "wav24")) {
|
||||
wav_fmt = WAV_S24;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "wav32")) {
|
||||
wav_fmt = WAV_F32;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Byte-level write helpers (endian-safe)
|
||||
|
||||
static void wav_write_u16le(char *& p, uint16_t x) {
|
||||
*p++ = (char) (x & 0xff);
|
||||
*p++ = (char) ((x >> 8) & 0xff);
|
||||
}
|
||||
|
||||
static void wav_write_u24le(char *& p, uint32_t x) {
|
||||
*p++ = (char) (x & 0xff);
|
||||
*p++ = (char) ((x >> 8) & 0xff);
|
||||
*p++ = (char) ((x >> 16) & 0xff);
|
||||
}
|
||||
|
||||
static void wav_write_u32le(char *& p, uint32_t x) {
|
||||
*p++ = (char) (x & 0xff);
|
||||
*p++ = (char) ((x >> 8) & 0xff);
|
||||
*p++ = (char) ((x >> 16) & 0xff);
|
||||
*p++ = (char) ((x >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static float wav_clamp1(float x) {
|
||||
return x < -1.0f ? -1.0f : (x > 1.0f ? 1.0f : x);
|
||||
}
|
||||
|
||||
static float wav_sanitize(float x) {
|
||||
return std::isfinite(x) ? x : 0.0f;
|
||||
}
|
||||
|
||||
// Classic RIFF header: fmt_tag 1 (PCM int) or 3 (IEEE float), 16-byte fmt chunk
|
||||
static void wav_write_header_basic(char *& p, int T_audio, int sr, int n_channels, int bits, uint16_t fmt_tag) {
|
||||
uint32_t bytes_per_sample = (uint32_t) bits / 8;
|
||||
uint32_t byte_rate = (uint32_t) sr * (uint32_t) n_channels * bytes_per_sample;
|
||||
uint16_t block_align = (uint16_t) (n_channels * (int) bytes_per_sample);
|
||||
uint32_t data_size = (uint32_t) T_audio * (uint32_t) n_channels * bytes_per_sample;
|
||||
uint32_t file_size = 36 + data_size;
|
||||
|
||||
memcpy(p, "RIFF", 4);
|
||||
p += 4;
|
||||
wav_write_u32le(p, file_size);
|
||||
memcpy(p, "WAVE", 4);
|
||||
p += 4;
|
||||
|
||||
memcpy(p, "fmt ", 4);
|
||||
p += 4;
|
||||
wav_write_u32le(p, 16);
|
||||
wav_write_u16le(p, fmt_tag);
|
||||
wav_write_u16le(p, (uint16_t) n_channels);
|
||||
wav_write_u32le(p, (uint32_t) sr);
|
||||
wav_write_u32le(p, byte_rate);
|
||||
wav_write_u16le(p, block_align);
|
||||
wav_write_u16le(p, (uint16_t) bits);
|
||||
|
||||
memcpy(p, "data", 4);
|
||||
p += 4;
|
||||
wav_write_u32le(p, data_size);
|
||||
}
|
||||
|
||||
// Encode mono float to WAV 16-bit signed integer PCM in memory.
|
||||
// 44-byte classic RIFF header (fmt_tag=1) + int16 samples.
|
||||
// Clamps to [-1, +1], coerces NaN/Inf to zero.
|
||||
static std::string audio_encode_wav_s16(const float * audio, int T_audio, int sr) {
|
||||
int n_channels = 1;
|
||||
int data_size = T_audio * n_channels * 2;
|
||||
|
||||
std::string out;
|
||||
out.resize(44 + (size_t) data_size);
|
||||
char * p = &out[0];
|
||||
|
||||
wav_write_header_basic(p, T_audio, sr, n_channels, 16, 1);
|
||||
|
||||
for (int t = 0; t < T_audio; t++) {
|
||||
int16_t s = (int16_t) (wav_clamp1(wav_sanitize(audio[t])) * 32767.0f);
|
||||
wav_write_u16le(p, (uint16_t) s);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Encode mono float to WAV 24-bit signed integer PCM in memory.
|
||||
// 44-byte classic RIFF header (fmt_tag=1) + int24 samples.
|
||||
// Clamps to [-1, +1], coerces NaN/Inf to zero.
|
||||
static std::string audio_encode_wav_s24(const float * audio, int T_audio, int sr) {
|
||||
int n_channels = 1;
|
||||
int data_size = T_audio * n_channels * 3;
|
||||
|
||||
std::string out;
|
||||
out.resize(44 + (size_t) data_size);
|
||||
char * p = &out[0];
|
||||
|
||||
wav_write_header_basic(p, T_audio, sr, n_channels, 24, 1);
|
||||
|
||||
for (int t = 0; t < T_audio; t++) {
|
||||
int32_t s = (int32_t) (wav_clamp1(wav_sanitize(audio[t])) * 8388607.0f);
|
||||
wav_write_u24le(p, (uint32_t) s);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Encode mono float to WAV 32-bit IEEE 754 float in memory.
|
||||
// 44-byte classic RIFF header (fmt_tag=3) + float32 samples.
|
||||
// Coerces NaN/Inf to zero. No clamping: output may exceed [-1, +1].
|
||||
static std::string audio_encode_wav_f32(const float * audio, int T_audio, int sr) {
|
||||
int n_channels = 1;
|
||||
int data_size = T_audio * n_channels * 4;
|
||||
|
||||
std::string out;
|
||||
out.resize(44 + (size_t) data_size);
|
||||
char * p = &out[0];
|
||||
|
||||
wav_write_header_basic(p, T_audio, sr, n_channels, 32, 3);
|
||||
|
||||
for (int t = 0; t < T_audio; t++) {
|
||||
float f = wav_sanitize(audio[t]);
|
||||
uint32_t u;
|
||||
memcpy(&u, &f, 4);
|
||||
wav_write_u32le(p, u);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Encode mono float to WAV in memory in the requested format.
|
||||
// audio is flat mono [T], pre-normalized by caller.
|
||||
// NaN and Inf are coerced to zero. S16/S24 clamp to [-1, +1].
|
||||
static std::string audio_encode_wav(const float * audio, int T_audio, int sr, WavFormat fmt = WAV_S16) {
|
||||
switch (fmt) {
|
||||
case WAV_S16:
|
||||
return audio_encode_wav_s16(audio, T_audio, sr);
|
||||
case WAV_S24:
|
||||
return audio_encode_wav_s24(audio, T_audio, sr);
|
||||
case WAV_F32:
|
||||
return audio_encode_wav_f32(audio, T_audio, sr);
|
||||
}
|
||||
return audio_encode_wav_s16(audio, T_audio, sr);
|
||||
}
|
||||
|
||||
// Write mono float audio to WAV file in the requested format. path "-"
|
||||
// streams the encoded WAV to stdout (pipe friendly). S16/S24 hard clip
|
||||
// to [-1, +1], F32 preserves the full range.
|
||||
static bool audio_write_wav(const char * path, const float * audio, int T_audio, int sr, WavFormat fmt = WAV_S16) {
|
||||
std::string wav = audio_encode_wav(audio, T_audio, sr, fmt);
|
||||
if (wav.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool to_stdout = (path[0] == '-' && path[1] == '\0');
|
||||
FILE * fp = to_stdout ? stdout : fopen(path, "wb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "[WAV] Cannot open %s for writing\n", path);
|
||||
return false;
|
||||
}
|
||||
if (fwrite(wav.data(), 1, wav.size(), fp) != wav.size()) {
|
||||
fprintf(stderr, "[WAV] Failed to write %s\n", path);
|
||||
if (!to_stdout) {
|
||||
fclose(fp);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (to_stdout) {
|
||||
fflush(fp);
|
||||
} else {
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
const char * fmt_name = (fmt == WAV_S16) ? "S16" : (fmt == WAV_S24) ? "S24" : "F32";
|
||||
fprintf(stderr, "[WAV] Wrote %s: %d samples, %d Hz, mono %s\n", to_stdout ? "<stdout>" : path, T_audio, sr,
|
||||
fmt_name);
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user