diff --git a/dsp/render48k.cpp b/dsp/render48k.cpp index 559a7c0..cc4b2bf 100644 --- a/dsp/render48k.cpp +++ b/dsp/render48k.cpp @@ -1,13 +1,26 @@ -// render48k.cpp — 48000/N=4096 internal-grid renderer (BITEXACT_PLAN step 6, path b). +// render48k.cpp — 48000/N=4096 internal-grid stereo/M/S renderer // // Host audio is 44100; the plugin detector runs internally at 48000/N=4096 (the // live IIR/warp/freq-axis tables are sized for that grid). This tool mirrors that: -// 1. read input WAV (44100 host samples) +// 1. read input WAV (44100 host samples, stereo) // 2. resample 44100 -> 48000 (libsamplerate, SINC best) -// 3. SpectralProcessor(4096, 1024, 48000) with the given bands -// 4. resample 48000 -> 44100 -// 5. write 24-bit output WAV (matches reference format) -// Usage: render48k [fc,q,sens[,scale] ...] (comma bands, like framed_test) +// 3. M/S decode: mid = (L+R)/2, side = (L-R)/2 +// 4. Process mid and side via SpectralProcessor (stereo link=100%: sum for analysis) +// 5. Apply balance: mid_reduction *= 1.0, side_reduction *= balance +// 6. M/S encode: L' = mid_out + side_out, R' = mid_out - side_out +// 7. resample 48000 -> 44100 +// 8. write 24-bit output WAV (stereo) +// +// Usage: +// render48k mid_fc,mid_q,mid_sens[,scale] side_fc,side_q,side_sens[,scale] +// render48k fc,q,sens[,scale] (mono input, dual-mono processing) +// Env: +// RT_STEREO_LINK=1.0 (1.0 = sum channels for analysis, 0.0 = dual mono) +// RT_STEREO_BALANCE=0.284 (side reduction scale, 1.0 = equal, <1.0 = less on side) +// RT_DEPTH=0.864 (sens multiplier) +// RT_MODE=1.0 (0=soft, 1=hard) +// RT_MIX=1.0 (0=dry, 1=full wet) + #include "spectral.hpp" #include #include @@ -18,7 +31,7 @@ static int g_in_ch = 1; -static bool load_wav(const char* path, std::vector& out, int& sr) { +static bool load_wav_stereo(const char* path, std::vector& out, int& sr, int& channels) { FILE* f = fopen(path, "rb"); if (!f) return false; char hdr[44]; @@ -26,7 +39,6 @@ static bool load_wav(const char* path, std::vector& out, int& sr) { sr = *(int*)(hdr + 24); int ch = *(short*)(hdr + 22); int bits = *(short*)(hdr + 34); - // scan chunks to find data chunk size (hdr[40] may be bext/junk size) int data = 0; int64_t pos = 12; fseek(f, 12, SEEK_SET); @@ -37,45 +49,55 @@ static bool load_wav(const char* path, std::vector& out, int& sr) { if (memcmp(cid, "data", 4) == 0) { data = csize; break; } pos += csize; int skip = csize; - if (csize % 2) skip++; // odd chunk size padded + if (csize % 2) skip++; fseek(f, skip, SEEK_CUR); } if (!data) { fclose(f); return false; } int n = data / (ch * (bits / 8)); g_in_ch = ch; - out.resize(n); + channels = 2; // always output stereo interleaved + out.resize(n * 2); if (bits == 16) { std::vector raw(n * ch); fread(raw.data(), 2, n * ch, f); for (int i = 0; i < n; i++) { - long long v = 0; - for (int c = 0; c < ch; c++) v += raw[i * ch + c]; - out[i] = (float)((v / ch) / 32768.0); + float v = 0.0f; + if (ch == 1) { + v = raw[i] / 32768.0f; + out[2*i] = v; out[2*i+1] = v; // mono -> stereo + } else { + out[2*i] = raw[2*i] / 32768.0f; + out[2*i+1] = raw[2*i+1] / 32768.0f; + } } } else if (bits == 24) { std::vector raw(n * ch * 3); fread(raw.data(), 1, n * ch * 3, f); for (int i = 0; i < n; i++) { - long long v = 0; - for (int c = 0; c < ch; c++) { - int idx = (i * ch + c) * 3; - int32_t s = (raw[idx] | (raw[idx + 1] << 8) | (raw[idx + 2] << 16)); + auto read24 = [&](int idx) -> float { + int32_t s = (raw[idx] | (raw[idx+1] << 8) | (raw[idx+2] << 16)); if (s & 0x800000) s |= 0xFF000000; - v += s; + return s / 8388608.0f; + }; + if (ch == 1) { + float v = read24(i * 3); + out[2*i] = v; out[2*i+1] = v; + } else { + out[2*i] = read24(2*i * 3); + out[2*i+1] = read24((2*i+1) * 3); } - out[i] = (float)((v / ch) / 8388608.0); } } else return false; fclose(f); return true; } -static bool save_wav24(const char* path, const std::vector& x, int sr) { +static bool save_wav24_stereo(const char* path, const std::vector& L, const std::vector& R, int sr) { FILE* f = fopen(path, "wb"); if (!f) return false; int ch = 2, bits = 24; - // x is already stereo interleaved (size = mono_samples * 2) - int data = (int)(x.size() * (bits / 8)); + size_t n = std::min(L.size(), R.size()); + int data = (int)(n * ch * (bits / 8)); char hdr[44]; memset(hdr, 0, 44); memcpy(hdr, "RIFF", 4); *(int*)(hdr + 4) = 36 + data; memcpy(hdr + 8, "WAVE", 4); memcpy(hdr + 12, "fmt ", 4); @@ -84,16 +106,19 @@ static bool save_wav24(const char* path, const std::vector& x, int sr) { *(short*)(hdr + 32) = (short)ch; *(short*)(hdr + 34) = (short)bits; memcpy(hdr + 36, "data", 4); *(int*)(hdr + 40) = data; fwrite(hdr, 1, 44, f); - for (size_t i = 0; i < x.size(); i++) { - int32_t v = (int32_t)(std::max(-1.0f, std::min(1.0f, x[i])) * 8388607.0f); - unsigned char b0 = v & 0xFF, b1 = (v >> 8) & 0xFF, b2 = (v >> 16) & 0xFF; + for (size_t i = 0; i < n; i++) { + int32_t vl = (int32_t)(std::max(-1.0f, std::min(1.0f, L[i])) * 8388607.0f); + int32_t vr = (int32_t)(std::max(-1.0f, std::min(1.0f, R[i])) * 8388607.0f); + unsigned char b0 = vl & 0xFF, b1 = (vl >> 8) & 0xFF, b2 = (vl >> 16) & 0xFF; + fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f); + b0 = vr & 0xFF; b1 = (vr >> 8) & 0xFF; b2 = (vr >> 16) & 0xFF; fwrite(&b0, 1, 1, f); fwrite(&b1, 1, 1, f); fwrite(&b2, 1, 1, f); } fclose(f); return true; } -static std::vector resample(const std::vector& in, int src_sr, int dst_sr) { +static std::vector resample_mono(const std::vector& in, int src_sr, int dst_sr) { double frac = (double)dst_sr / src_sr; int out_len = (int)(in.size() * frac) + 16; std::vector buf(out_len); @@ -106,46 +131,143 @@ static std::vector resample(const std::vector& in, int src_sr, int buf.resize(sd.output_frames_gen); return buf; } +static std::vector resample_stereo(const std::vector& in, int src_sr, int dst_sr) { + // in is stereo interleaved: L0, R0, L1, R1, ... + size_t n = in.size() / 2; + double frac = (double)dst_sr / src_sr; + int out_len = (int)(n * frac) + 16; + std::vector buf(out_len * 2); + SRC_DATA sd; + sd.data_in = in.data(); sd.input_frames = (long)n; + sd.data_out = buf.data(); sd.output_frames = out_len; + sd.src_ratio = frac; sd.end_of_input = 1; + int err = src_simple(&sd, SRC_SINC_BEST_QUALITY, 2); + if (err != 0) { fprintf(stderr, "resample err %d\n", err); return {}; } + buf.resize(sd.output_frames_gen * 2); + return buf; +} int main(int argc, char** argv) { - if (argc < 3) { fprintf(stderr, "usage: %s in.wav out.wav [fc,q,sens[,scale] ...]\n", argv[0]); return 1; } - std::vector x; int sr; - if (!load_wav(argv[1], x, sr)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } + if (argc < 3) { + fprintf(stderr, "usage: %s in.wav out.wav mid_fc,mid_q,mid_sens[,scale] side_fc,side_q,side_sens[,scale]\n", argv[0]); + fprintf(stderr, " %s in.wav out.wav fc,q,sens[,scale] (mono input, dual-mono)\n", argv[0]); + return 1; + } + std::vector x; int sr, channels; + if (!load_wav_stereo(argv[1], x, sr, channels)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } - std::vector bands; + // Parse stereo parameters from env + float stereo_link = getenv("RT_STEREO_LINK") ? atof(getenv("RT_STEREO_LINK")) : 1.0f; + float stereo_balance = getenv("RT_STEREO_BALANCE") ? atof(getenv("RT_STEREO_BALANCE")) : 0.284f; + float depth = getenv("RT_DEPTH") ? atof(getenv("RT_DEPTH")) : 1.0f; + float mix = getenv("RT_MIX") ? atof(getenv("RT_MIX")) : 1.0f; + + // Parse bands + std::vector mid_bands, side_bands; for (int i = 3; i < argc; i++) { if (!strchr(argv[i], ',')) continue; float fc, q, sens, scl = 1.0f; if (sscanf(argv[i], "%f,%f,%f,%f", &fc, &q, &sens, &scl) < 3) continue; - DetectorBand b; b.fc = fc; b.q = q; b.sens = sens; b.level_scale = scl; - bands.push_back(b); + DetectorBand b; b.fc = fc; b.q = q; b.sens = sens * depth; b.level_scale = scl; + if (mid_bands.empty()) mid_bands.push_back(b); + else side_bands.push_back(b); } - if (bands.empty()) bands.push_back({1000.0f, 1.0f, 12.0f}); + if (mid_bands.empty()) mid_bands.push_back({1000.0f, 1.0f, 12.0f * depth}); + if (side_bands.empty()) side_bands = mid_bands; - auto x48 = resample(x, sr, 48000); + auto x48 = resample_stereo(x, sr, 48000); if (x48.empty()) return 1; - SpectralProcessor sp(4096, 1024, 48000.0f); - sp.setDetectorParams(bands); - std::vector y48(x48.size()); + size_t n = x48.size() / 2; // samples per channel + std::vector mid(n), side(n); + + // M/S decode (or mono duplication for mono input) + if (channels >= 2) { + for (size_t i = 0; i < n; i++) { + float L = x48[2*i]; + float R = x48[2*i+1]; + mid[i] = (L + R) * 0.5f; + side[i] = (L - R) * 0.5f; + } + } else { + for (size_t i = 0; i < n; i++) { + mid[i] = x48[i]; + side[i] = x48[i]; + } + } + + // For stereo link=100%: sum mid+side for analysis + std::vector analysis_buf(n); + if (stereo_link >= 1.0f) { + for (size_t i = 0; i < n; i++) analysis_buf[i] = mid[i] + side[i]; + } + + // Create processors + SpectralProcessor mid_sp(4096, 1024, 48000.0f); + SpectralProcessor side_sp(4096, 1024, 48000.0f); + mid_sp.setDetectorParams(mid_bands); + side_sp.setDetectorParams(side_bands); + + // Process + std::vector mid_out(n), side_out(n); const size_t BLK = 1 << 16; std::vector inb(BLK), outb(BLK); - for (size_t s = 0; s < x48.size(); s += BLK) { - size_t n = std::min(BLK, x48.size() - s); - memcpy(inb.data(), x48.data() + s, n * sizeof(float)); - for (size_t i = n; i < BLK; i++) inb[i] = 0.0f; - sp.processBlock(inb.data(), outb.data(), BLK, 1); - memcpy(y48.data() + s, outb.data(), n * sizeof(float)); - } - auto y = resample(y48, 48000, 44100); - if ((int)y.size() > (int)x.size()) y.resize(x.size()); - // write stereo 24-bit - std::vector yst(y.size() * 2); - for (size_t i = 0; i < y.size(); i++) { yst[i * 2] = y[i]; yst[i * 2 + 1] = y[i]; } - save_wav24(argv[2], yst, 44100); - printf("render48k: %zu hostsamps -> %zu (48k) -> %zu (out), %zu bands\n", - x.size(), x48.size(), y.size(), bands.size()); - (void)g_in_ch; + for (size_t s = 0; s < n; s += BLK) { + size_t blk = std::min(BLK, n - s); + memcpy(inb.data(), mid.data() + s, blk * sizeof(float)); + for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f; + mid_sp.processBlock(inb.data(), outb.data(), BLK, 1); + memcpy(mid_out.data() + s, outb.data(), blk * sizeof(float)); + } + + for (size_t s = 0; s < n; s += BLK) { + size_t blk = std::min(BLK, n - s); + memcpy(inb.data(), side.data() + s, blk * sizeof(float)); + for (size_t i = blk; i < BLK; i++) inb[i] = 0.0f; + side_sp.processBlock(inb.data(), outb.data(), BLK, 1); + memcpy(side_out.data() + s, outb.data(), blk * sizeof(float)); + } + + // M/S encode with balance and mix + // mask is [0,1] where 1=no change, <1=reduction + // processed = input * mask + // output = input * (1 - mix) + processed * mix + // For side: apply balance to reduction amount + std::vector Lout(n), Rout(n); + for (size_t i = 0; i < n; i++) { + float m_in = mid[i]; + float s_in = side[i]; + float m_proc = mid_out[i]; // m_in * mask_mid + float s_proc = side_out[i]; // s_in * mask_side + + // Compute mask values (avoid division by zero) + float mask_mid = (std::abs(m_in) > 1e-12f) ? m_proc / m_in : 1.0f; + float mask_side = (std::abs(s_in) > 1e-12f) ? s_proc / s_in : 1.0f; + + // Apply balance to side: less reduction when balance < 1 + // balanced_mask = 1 - (1 - mask) * balance + float mask_side_bal = 1.0f - (1.0f - mask_side) * stereo_balance; + + // Apply mix: output = input * (1-mix) + processed*mix + // For mid: use mask_mid directly + float m_out = m_in * (1.0f - mix) + m_in * mask_mid * mix; + // For side: use balanced mask + float s_out = s_in * (1.0f - mix) + s_in * mask_side_bal * mix; + + // M/S encode + Lout[i] = m_out + s_out; + Rout[i] = m_out - s_out; + } + + auto L44 = resample_mono(Lout, 48000, 44100); + auto R44 = resample_mono(Rout, 48000, 44100); + size_t out_len = std::min(L44.size(), R44.size()); + out_len = std::min(out_len, x.size() / std::max(channels, 1)); + L44.resize(out_len); + R44.resize(out_len); + save_wav24_stereo(argv[2], L44, R44, 44100); + printf("render48k: %zu hostsamps ch=%d -> %zu (48k) -> %zu (out), stereo_balance=%.3f depth=%.3f\n", + x.size(), channels, x48.size(), out_len, stereo_balance, depth); return 0; }