Edited:

All right, here's a "Studio Simulation" that tries to create a binaural simulation of a sound source in a studio room with wood walls, also a Reaper .jsfx file. I had posted a version earlier, but it wasn't that great. The prior version could only cast a limited number of rays, while bakes the rays into an impulse response, which means the result is much more detailed.

This is an interesting technology, but it's a bit like a 3D printer - I'm not sure there are a lot of use cases for it in my life. It's easy to get lost in making a tool that - at the end of the day - just copies something else that's out there. It's way too easy for this sort of thing to become a timesink. I'm really no more the author of this tool as I am something I bought off the shelf. The difference is that something off the shelf had a lot more work go into it to make sure it was useful to lots of people.

I've restarted my voice synthesis using LLMs to help with the core functions, but I doubt that is going to be used by anyone other than me, either.

But if you're hankering for your own FX, Reaper's [b]jsfx[/i] is a really convenient way to do it.

I'm posting the code in case anyone want to try it out for grins and giggles.

// ---------------------------------------------------------------------------
// Plugin: Professional Studio Spatializer
// Author: Gemini AI Collaboration
// Date: March 2026
//
// PHYSICS LOGIC:
// 1. Stochastic Ray Tracing: Bakes 320 unique paths into a lookup table.
// 2. Inverse Square Law: Natural volume drop-off based on 1/Distance.
// 3. Air Absorption: High-frequency damping as a function of distance.
// 4. Wood Warmth: Frequency-dependent absorption (Low-Pass) on reflections.
// 5. Physical Diffusion: Stochastic time-jitter (up to 5ms) to smear transients.
//
// TECHNICAL CONSTRAINTS:
// - No scientific notation supported (e.g., use 0.00001, NOT 1e-5).
// - Circular Buffer: Power-of-two size (262144) for bitwise masking (& B_MSK).
// - Memory: Tables (0+) and Audio (20,000+) are separated to avoid corruption.
// ---------------------------------------------------------------------------

desc: Pro-Studio Spatializer (Final Build)

// --- Sliders ---
slider1:0<-60, 12, 0.1>Output Gain (dB)
slider2:2.5<0.1, 20.0, 0.01>Distance (m)
slider3:0<-90, 90, 1>Source Angle (deg)
slider4:10<4, 60, 0.1>Room Width (m)
slider5:12<4, 60, 0.1>Room Depth (m)
slider6:0.6<0, 1, 0.01>Diffusion (Physical Smear)
slider7:0.5<0, 1, 0.01>Wall Warmth (Wood Damping)

@init
// --- Memory Allocation ---
IR_DATA_PTR = 0; // Start of physics tables
AUDIO_BUF_PTR = 20000; // Start of audio delay line (prevents clicking)

B_SZ = 262144; // ~5.9 seconds at 44.1kHz
B_MSK = B_SZ - 1; // Bitwise mask for fast wrapping
buf_pos = 0; // Write head for audio buffer

// Table Pointers (1024 slots each for safety)
impL_T = IR_DATA_PTR; // Left Delay Times (samples)
impR_T = IR_DATA_PTR + 1024; // Right Delay Times (samples)
impL_G = IR_DATA_PTR + 2048; // Left Gains (linear)
impR_G = IR_DATA_PTR + 3072; // Right Gains (linear)

// Constants
PI = 3.1415926535;
C = 343; // Speed of sound m/s
DENORM = 0.000000000000001; // Prevents CPU "underflow" spikes

function bake_room() (
d = slider2;
a_rad = slider3 * (PI / 180);
hw = 0.24; // Human head width (meters)

// Virtual source coordinates
sx = sin(a_rad) * d; sy = cos(a_rad) * d;
exL = -hw * 0.5; exR = hw * 0.5;

active_rays = 320;
seed = 888; // Fixed seed for deterministic acoustics

// Calculate base decay based on Wall Warmth
base_decay = 0.88 - (slider7 * 0.35);

i = 0;
loop(active_rays,
seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF;

i == 0 ? (
// Direct Path logic
vx = sx; vy = sy;
decay = 1.0;
jit = 0;
) : (
// Reflection logic
rand_ang = (seed / 0x7FFFFFFF) * 2 * PI;
path_base = d + (i / active_rays) * ((slider4 + slider5) * 0.5);

// Diffusion: Inject random timing jitter to the rays
jit = ((seed % 200 / 100) - 1.0) * slider6 * 0.005;

vx = sx + cos(rand_ang) * path_base;
vy = sy + sin(rand_ang) * path_base;
decay = pow(base_decay, (path_base - d) / 2.5);
);

// Distances to virtual ears
rdL = sqrt((vx - exL)^2 + vy^2);
rdR = sqrt((vx - exR)^2 + vy^2);

// Convert to samples + apply diffusion jitter
impL_T[i] = ((rdL / C) + jit) * srate;
impR_T[i] = ((rdR / C) + jit) * srate;

// Inverse Square Law Gain * Decay Coefficient
impL_G[i] = (decay / max(1.0, rdL)) * 0.15;
impR_G[i] = (decay / max(1.0, rdR)) * 0.15;

i += 1;
);

ray_cnt = i;

// Pre-calculate filter coefficients for @sample
shadowL = min(1.0, 1.0 - max(0, slider3 / 100));
shadowR = min(1.0, 1.0 - max(0, -slider3 / 100));
air_loss = max(0.2, 1.0 - (d * 0.03));
wood_lp = 0.03 + (1.0 - slider7) * 0.5;
);

@slider
g_out = 10^(slider1/20);
bake_room(); // Re-calculate acoustics on parameter change

@sample
// Input + Denormal noise
sig_in = (spl0 + spl1) * 0.5 + DENORM;
AUDIO_BUF_PTR[buf_pos] = sig_in;

outL = 0; outR = 0;
idx = 0;
loop(ray_cnt,
// Linear Interpolation for high-fidelity fractional delays
tL = impL_T[idx]; itL = floor(tL); ftL = tL - itL;
vL = AUDIO_BUF_PTR[(buf_pos - itL) & B_MSK] * (1 - ftL) +
AUDIO_BUF_PTR[(buf_pos - itL - 1) & B_MSK] * ftL;
outL += vL * impL_G[idx];

tR = impR_T[idx]; itR = floor(tR); ftR = tR - itR;
vR = AUDIO_BUF_PTR[(buf_pos - itR) & B_MSK] * (1 - ftR) +
AUDIO_BUF_PTR[(buf_pos - itR - 1) & B_MSK] * ftR;
outR += vR * impR_G[idx];

idx += 1;
);

// Wood Absorption Stage
reflL += wood_lp * (outL - reflL);
reflR += wood_lp * (outR - reflR);

// Head Shadowing + Air Absorption Stage
fL += (shadowL * air_loss * 0.6) * (reflL - fL);
fR += (shadowR * air_loss * 0.6) * (reflR - fR);

// Final smoothing smear
dfL += 0.5 * (fL - dfL);
dfR += 0.5 * (fR - dfR);

resL = fL * 0.4 + dfL * 0.6;
resR = fR * 0.4 + dfR * 0.6;

buf_pos = (buf_pos + 1) & B_MSK;

spl0 = resL * g_out;
spl1 = resR * g_out;

@gfx 500 450
// --- GUI Background ---
gfx_r=0.08; gfx_g=0.07; gfx_b=0.06; gfx_rect(0,0,gfx_w,gfx_h);

// --- Auto-Scaling Logic ---
cx = gfx_w / 2; cy = (gfx_h / 2) + 20;
max_dim = max(slider4, slider5);
view_scale = (min(gfx_w, gfx_h) * 0.75) / max_dim;
rw = slider4 * view_scale; rd = slider5 * view_scale;

// Draw Room Boundary
gfx_r=0.3; gfx_g=0.2; gfx_b=0.1;
gfx_rect(cx - rw/2, cy - rd/2, rw, rd, 0);

// Metrics
gfx_r=0.8; gfx_g=0.8; gfx_b=0.7;
gfx_x = 20; gfx_y = 20;
gfx_printf("Room Dimensions: %0.1f m x %0.1f m", slider4, slider5);
gfx_y += 18;
gfx_printf("Path Distance: %0.2f meters", slider2);

// Listener (Mics)
gfx_r=1; gfx_g=1; gfx_b=1;
gfx_circle(cx - 5, cy, 2, 1); gfx_circle(cx + 5, cy, 2, 1);
gfx_x = cx - 28; gfx_y = cy + 12; gfx_drawstr("Mics");

// Source Position
src_x = cx + sin(slider3 * PI / 180) * slider2 * view_scale;
src_y = cy - cos(slider3 * PI / 180) * slider2 * view_scale;
// Clip icons to room walls
src_x = max(cx - rw/2, min(cx + rw/2, src_x));
src_y = max(cy - rd/2, min(cy + rd/2, src_y));

gfx_r=1; gfx_g=0.5; gfx_b=0;
gfx_circle(src_x, src_y, 6, 1);
gfx_r=1; gfx_g=0.9; gfx_b=0;
gfx_line(cx, cy, src_x, src_y); // Distance visualization

// Data Label
gfx_x = src_x + 10; gfx_y = src_y - 10;
gfx_printf("%d deg", slider3);

Last edited by dcuny; 03/15/26 02:11 PM. Reason: New and better code.

-- David Cuny

My virtual singer development blog
Vocal control, you say. Never heard of it. Is that some kind of ProTools thing?

BiaB 2025 | Windows 11 | Reaper | Way too many VSTis.