
Stereo · 6 October 2026 · 8 min
Wide mixes fail in mono. Check mid/side before you bounce
Stereo width that sounds expensive on headphones can cancel on a phone speaker. Mid/side balance and L/R correlation tell you whether the width is musical or just anti-phase. Measure it, fold to mono once, then bounce.
You’ve heard it. Headphones, the pad feels huge. Car stereo, fine. Then somebody plays it off a phone on a kitchen counter and the hook disappears. Not quieter overall — thinner. The part that lived in the sides never made it into one speaker.
That’s not a mystery genre thing. It’s mid and side fighting each other. Mid is what L and R share. Side is what’s different between them. Phones, club mono fills, Instagram stories, and a lot of Bluetooth speakers lean hard on the mid. If your side energy is huge and out of phase with the mid, mono isn’t a gentle fold. It’s a disappearing act.
Width isn’t the enemy. Unchecked width is.
A wide mix can be mono-safe. A narrow mix can still have a bad bar. What you want is a read on three things at once: how much side you have relative to mid, how correlated left and right are, and how much loudness you lose when you sum to mono. This script does that on a stereo bounce. It also walks the file in 400 ms windows so you can find the one thin moment that the whole-file average hides.
#!/usr/bin/env python3
"""midside_width.py — mid/side balance, stereo width, and mono-safe check.
Usage: python3 midside_width.py stereo.wav
Needs: numpy + ffmpeg on PATH.
Prints integrated mid vs side energy, a simple width ratio, Pearson
correlation (same idea as a phase meter), and how much loudness you
lose when the mix folds to mono. Wide ≠ broken; negative correlation
or a big mono LUFS drop is the red flag."""
import subprocess, sys
import numpy as np
SR = 48000
path = sys.argv[1]
raw = subprocess.run(
["ffmpeg", "-v", "error", "-i", path, "-ac", "2", "-ar", str(SR),
"-f", "f32le", "-"],
capture_output=True, check=True,
).stdout
x = np.frombuffer(raw, dtype=np.float32).reshape(-1, 2)
L, R = x[:, 0], x[:, 1]
# Mid/side (equal-power encode; factor 1/sqrt(2) keeps energy)
mid = (L + R) * (0.5 ** 0.5)
side = (L - R) * (0.5 ** 0.5)
# Energy (mean square)
e_m = float(np.mean(mid ** 2) + 1e-20)
e_s = float(np.mean(side ** 2) + 1e-20)
mid_db = 10 * np.log10(e_m)
side_db = 10 * np.log10(e_s)
# Width: side energy / mid energy (0 = mono, 1 = equal M/S, >1 = side-heavy)
width = e_s / e_m
# Pearson correlation ≈ phase-correlation style reading on the whole file
corr = float(np.corrcoef(L, R)[0, 1])
# Mono fold (L+R)/2 — how much integrated energy you keep
mono = (L + R) * 0.5
stereo_e = float(np.mean(0.5 * (L ** 2 + R ** 2)) + 1e-20)
mono_e = float(np.mean(mono ** 2) + 1e-20)
mono_drop_db = 10 * np.log10(mono_e / stereo_e)
# Short-term correlation every 400 ms to find the thinnest moments
hop = int(0.4 * SR)
wins = []
for i in range(0, len(L) - hop, hop):
a, b = L[i:i + hop], R[i:i + hop]
if np.std(a) < 1e-6 or np.std(b) < 1e-6:
continue
c = float(np.corrcoef(a, b)[0, 1])
wins.append((i / SR, c))
print(f"file : {path}")
print(f"duration : {len(L) / SR:.2f} s")
print(f"mid energy : {mid_db:+.1f} dB (rel arbitrary)")
print(f"side energy : {side_db:+.1f} dB")
print(f"side/mid width : {width:.3f} (0=mono, ~0.3–0.7 typical mix, >1 side-heavy)")
print(f"L/R correlation : {corr:+.3f} (≥0.3 usually mono-safe; <0.2 risky; <0 cancels)")
print(f"mono energy drop: {mono_drop_db:+.2f} dB vs stereo mean energy")
if wins:
worst = min(wins, key=lambda t: t[1])
best = max(wins, key=lambda t: t[1])
print(f"worst 400 ms : {int(worst[0] // 60)}:{worst[0] % 60:05.2f} corr {worst[1]:+.3f}")
print(f"best 400 ms : {int(best[0] // 60)}:{best[0] % 60:05.2f} corr {best[1]:+.3f}")
thin = [w for w in wins if w[1] < 0.2]
print(f"thin windows : {len(thin)} of {len(wins)} under corr 0.2")
What the numbers looked like on synthetic tests
All of these are synthetic WAVs built on the box — labeled that way on purpose so nobody mistakes them for a real record.
A balanced “mix” with a centered kick/bass, a lightly detuned stereo pad, and hard-panned hat bursts measured side/mid width 0.30, correlation +0.54, and about −1.1 dB of mean-energy drop into mono. Integrated loudness still moved −4.8 LU stereo → mono on the ebur128 meter, mostly because the wide hats don’t survive the fold. That’s normal and musical. Three of nineteen 400 ms windows dipped under correlation 0.2 — the hat-only moments — and the worst sat at 0:03.20 with corr −0.09. The average looked fine. The thin bar was real.
A deliberately broken version — near-anti-phase pads with only a weak center kick — measured width 103, correlation −0.98, and a −20 dB energy drop into mono. Integrated loudness fell −26.6 LU stereo → mono. On a phone, that pad is gone. Pulling the side down 24 dB while leaving the mid alone brought width to 0.41, correlation to +0.42, and the mono loudness hit to about −5.8 LU. Another 6 dB of side cut (30 dB total) landed at width 0.10, corr +0.81, mono hit −3.9 LU. You don’t need mono. You need the mid to still own the song.
A near-mono control measured width 0.00, corr +1.00, and no energy drop. Safe, and also flat. The point isn’t maximum correlation. It’s “does the hook still exist when L and R become one speaker?”
How to listen for it without a script
| Check | What you’re hearing | What to do |
|---|---|---|
| Flip the master to mono for one chorus | Hook thins, pad vanishes, vocal doubles collapse | Find which bus is wide: pads, FX, delayed ad-libs |
| Solo mid, then solo side | Side is louder than mid on the parts that matter | Turn the widener down, or high-pass the side so low end stays mid |
| Correlation meter under ~0.2 | Phase meter hugs the middle or flips negative | Shorten stereo delays, flip a polarity, collapse a Haas throw |
| Club / phone / Bluetooth check | Beat stays, atmosphere disappears | Keep kick, bass, lead vocal, and snare body in the mid |
Low end wants to be mid. A stereo 808 or a widened kick is a classic mono trap. Keep the weight centered and let the air be wide.
Haas throws are side candy. A 10–30 ms delay on one side feels huge in headphones and cancels in mono. If you love the throw, duck it in the hook or keep a mono dry version underneath.
Doubles and ad-libs. Hard-panned stacked vocals are fine until every consonant lives only on the sides. Leave a solid lead in the mid; let the stacks decorate.
Screw and slow edits change the picture. Pitching and stretching can nudge phase relationships. Check mono on the final ScrewAI render, not only on the full-speed bounce.
Fix order that doesn’t kill the vibe
- Identify the bus, not the master. StemSplit the bounce if you don’t have the session — pads and FX are usually the culprits, not the kick.
- Cut side, don’t nuke stereo. A mid/side EQ or a width control that only turns down side above 200 Hz often fixes the mono hole without making the record feel small.
- Re-check correlation and a mono fold. Aim for whole-file correlation roughly above 0.3, and make sure no section you care about sits negative for long.
- Then master. A limiter will not repair anti-phase width. It will just make the thin parts louder and thinner.
Once the mono fold still carries the hook, run the bounce through CoProducer and glance at the phase/correlation row before upload. Width is a taste call. Mono survival is delivery hygiene.
- Why does my mix sound thin on a phone?
- Phones are basically mono. If too much of the hook lives in out-of-phase side information, it cancels when left and right collapse. Check mid/side balance and listen in mono.
- What’s a “good” stereo width number?
- There isn’t one magic width. Correlation above about 0.3 and a mono fold that still holds the hook are safer targets than chasing maximum width on a meter.