Spaces:
Sleeping
Sleeping
| """The opinionated part. | |
| Measurements are neutral; this module is where the taste lives. Each rule | |
| turns a number into a diagnosis with a *reason* and a *move*, scoped to what | |
| the material is supposed to be (a master behaves nothing like a solo'd sub). | |
| Targets are expressed as ratios between bands wherever possible. Absolute | |
| band fractions drift with arrangement density; ratios like "low mid against | |
| mid" survive it and are closer to how an engineer actually judges a balance. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, asdict, field | |
| from . import dsp | |
| # -------------------------------------------------------------------------- | |
| # context profiles | |
| # -------------------------------------------------------------------------- | |
| class Profile: | |
| name: str | |
| lufs: tuple[float, float] | |
| crest: tuple[float, float] | |
| sub_vs_bass: tuple[float, float] | |
| mud: tuple[float, float] | |
| harsh: tuple[float, float] | |
| air: tuple[float, float] | |
| tilt: tuple[float, float] | |
| width: tuple[float, float] | |
| bpm_hint: str | |
| voice: str | |
| GENRES: dict[str, Profile] = { | |
| "Dubstep / Riddim": Profile( | |
| "Dubstep / Riddim", | |
| lufs=(-9.0, -5.0), crest=(6.0, 11.0), | |
| sub_vs_bass=(-4.0, 2.0), mud=(-5.0, 0.0), harsh=(-6.0, -1.0), | |
| air=(-14.0, -6.0), tilt=(2.0, 14.0), width=(-14.0, -4.0), | |
| bpm_hint="140 / 150 (half-time feel at 70β75)", | |
| voice="Sub weight and mid aggression are the product. Protect the " | |
| "20β60 Hz shelf, keep it mono, and make room at 200β400 Hz so " | |
| "the growls read as bite instead of mud.", | |
| ), | |
| "Melodic Dubstep / Future Bass": Profile( | |
| "Melodic Dubstep / Future Bass", | |
| lufs=(-10.0, -6.0), crest=(7.0, 12.0), | |
| sub_vs_bass=(-5.0, 1.0), mud=(-6.0, -1.0), harsh=(-5.0, 0.0), | |
| air=(-11.0, -4.0), tilt=(-1.0, 9.0), width=(-11.0, -2.0), | |
| bpm_hint="140β150", | |
| voice="Wide, bright and emotional β but the chord stack is the usual " | |
| "mud source. Keep supersaws high-passed and let the sub own " | |
| "everything under 80 Hz alone.", | |
| ), | |
| "Drum & Bass": Profile( | |
| "Drum & Bass", | |
| lufs=(-9.0, -5.0), crest=(7.0, 12.0), | |
| sub_vs_bass=(-3.0, 3.0), mud=(-6.0, -1.0), harsh=(-5.0, 0.0), | |
| air=(-12.0, -5.0), tilt=(1.0, 12.0), width=(-13.0, -4.0), | |
| bpm_hint="172β176", | |
| voice="Breaks need transient survival. Watch crest β if it drops " | |
| "under 7 dB the drums stop moving air even though the meter " | |
| "says loud.", | |
| ), | |
| "House / Techno": Profile( | |
| "House / Techno", | |
| lufs=(-10.0, -6.0), crest=(7.0, 12.0), | |
| sub_vs_bass=(-6.0, 0.0), mud=(-6.0, -1.0), harsh=(-6.0, -1.0), | |
| air=(-12.0, -5.0), tilt=(0.0, 10.0), width=(-13.0, -4.0), | |
| bpm_hint="120β135", | |
| voice="The kick is the anchor. Everything else earns its place around " | |
| "it; sidechain depth matters more than EQ here.", | |
| ), | |
| "Trap / Hip-Hop": Profile( | |
| "Trap / Hip-Hop", | |
| lufs=(-10.0, -6.0), crest=(7.0, 13.0), | |
| sub_vs_bass=(-2.0, 5.0), mud=(-6.0, -1.0), harsh=(-6.0, -1.0), | |
| air=(-13.0, -5.0), tilt=(3.0, 15.0), width=(-16.0, -5.0), | |
| bpm_hint="130β150 (half-time 65β75)", | |
| voice="808 and vocal are the two things that must never fight. If " | |
| "they share 100β250 Hz, one of them has to move.", | |
| ), | |
| "Pop / Vocal-led": Profile( | |
| "Pop / Vocal-led", | |
| lufs=(-11.0, -7.0), crest=(8.0, 13.0), | |
| sub_vs_bass=(-8.0, -1.0), mud=(-7.0, -2.0), harsh=(-4.0, 1.0), | |
| air=(-10.0, -3.0), tilt=(-4.0, 6.0), width=(-12.0, -3.0), | |
| bpm_hint="90β130", | |
| voice="The vocal is the mix. Every decision is 'does this help the " | |
| "voice sit forward without getting harsh at 3 kHz'.", | |
| ), | |
| "Ambient / Cinematic": Profile( | |
| "Ambient / Cinematic", | |
| lufs=(-20.0, -13.0), crest=(11.0, 22.0), | |
| sub_vs_bass=(-8.0, 2.0), mud=(-6.0, 0.0), harsh=(-8.0, -2.0), | |
| air=(-12.0, -3.0), tilt=(-4.0, 8.0), width=(-9.0, 0.0), | |
| bpm_hint="free / rubato", | |
| voice="Dynamic range is the point. Loudness rules invert here β a " | |
| "high LRA is a feature, not a fault.", | |
| ), | |
| "Reference master (streaming)": Profile( | |
| "Reference master (streaming)", | |
| lufs=(-15.0, -12.0), crest=(8.0, 14.0), | |
| sub_vs_bass=(-7.0, 0.0), mud=(-6.0, -1.0), harsh=(-5.0, 0.0), | |
| air=(-12.0, -4.0), tilt=(-2.0, 8.0), width=(-12.0, -3.0), | |
| bpm_hint="β", | |
| voice="Targets Spotify/Apple normalisation. Anything louder than " | |
| "-12 LUFS just gets turned down with the transients already " | |
| "spent.", | |
| ), | |
| } | |
| # What is being listened to. Changes which rules are even allowed to fire. | |
| SOURCES = [ | |
| "Full mix / master", | |
| "Drum bus", | |
| "Bass / 808", | |
| "Lead / synth", | |
| "Vocal", | |
| "Pad / atmosphere", | |
| ] | |
| _SEVERITY_RANK = {"critical": 0, "warn": 1, "note": 2, "good": 3} | |
| class Diagnosis: | |
| id: str | |
| severity: str # critical | warn | note | good | |
| headline: str | |
| evidence: str | |
| why: str | |
| move: str | |
| band: str = "" | |
| freq: float = 0.0 | |
| amount: float = 0.0 | |
| tags: list[str] = field(default_factory=list) | |
| def to_dict(self) -> dict: | |
| return asdict(self) | |
| def _below(value: float, window: tuple[float, float]) -> float: | |
| return window[0] - value | |
| def _above(value: float, window: tuple[float, float]) -> float: | |
| return value - window[1] | |
| # -------------------------------------------------------------------------- | |
| # rules | |
| # -------------------------------------------------------------------------- | |
| def diagnose(rep: dsp.Report, genre: str, source: str) -> list[Diagnosis]: | |
| p = GENRES.get(genre, GENRES["Dubstep / Riddim"]) | |
| out: list[Diagnosis] = [] | |
| is_master = source == "Full mix / master" | |
| r = rep.ratios | |
| st = rep.stereo | |
| # -- headroom ----------------------------------------------------------- | |
| if rep.true_peak > 0.0: | |
| out.append(Diagnosis( | |
| "clipping", "critical", | |
| f"Clipping at {rep.true_peak:+.1f} dBTP", | |
| f"True peak {rep.true_peak:+.2f} dBTP, sample peak {rep.sample_peak:+.2f} dBFS", | |
| "Inter-sample peaks above 0 dBTP distort in every lossy encoder even " | |
| "when the file itself looks clean. Spotify and YouTube both " | |
| "reconstruct those peaks and clip them.", | |
| "Pull the master output down until true peak lands at -1.0 dBTP. If " | |
| "that costs you loudness, take it out of the limiter's ceiling, not " | |
| "the mix gain.", | |
| tags=["headroom"], | |
| )) | |
| elif rep.true_peak > -0.3: | |
| out.append(Diagnosis( | |
| "no_headroom", "warn", | |
| f"Only {abs(rep.true_peak):.1f} dB of true-peak headroom", | |
| f"True peak {rep.true_peak:+.2f} dBTP", | |
| "Anything above -1 dBTP is a coin flip after MP3/AAC encoding.", | |
| "Set the limiter ceiling to -1.0 dBTP and leave it there.", | |
| tags=["headroom"], | |
| )) | |
| # -- loudness ----------------------------------------------------------- | |
| if rep.lufs_i > -60.0 and rep.duration > 3.0: | |
| over = _above(rep.lufs_i, p.lufs) | |
| under = _below(rep.lufs_i, p.lufs) | |
| if over > 1.5 and is_master: | |
| out.append(Diagnosis( | |
| "too_loud", "warn", | |
| f"Pushed {over:.1f} LU past the {p.name} window", | |
| f"Integrated {rep.lufs_i:.1f} LUFS vs target {p.lufs[0]:.0f}β¦{p.lufs[1]:.0f}", | |
| "Streaming platforms normalise down to roughly -14 LUFS. Loudness " | |
| "above the target does not get louder on playback β it only " | |
| "arrives with less transient left.", | |
| f"Back the limiter off by {over:.1f} dB and recover the " | |
| "perceived weight with saturation on the low mids instead.", | |
| amount=over, tags=["loudness"], | |
| )) | |
| elif under > 3.0 and is_master: | |
| out.append(Diagnosis( | |
| "too_quiet", "note", | |
| f"{under:.1f} LU under the {p.name} window", | |
| f"Integrated {rep.lufs_i:.1f} LUFS vs target {p.lufs[0]:.0f}β¦{p.lufs[1]:.0f}", | |
| "Not a problem in itself, but it will feel small next to " | |
| "references in a playlist.", | |
| "Add gain into the limiter in 1 dB steps and stop the moment " | |
| "crest factor drops below the genre floor.", | |
| amount=under, tags=["loudness"], | |
| )) | |
| # -- dynamics ----------------------------------------------------------- | |
| if rep.duration > 2.0: | |
| if rep.crest < p.crest[0] - 1.0: | |
| out.append(Diagnosis( | |
| "over_limited", "critical" if rep.crest < p.crest[0] - 3 else "warn", | |
| f"Crest factor {rep.crest:.1f} dB β the transients are gone", | |
| f"Peak-to-RMS {rep.crest:.1f} dB, PSR {rep.psr:.1f} dB, " | |
| f"target {p.crest[0]:.0f}β¦{p.crest[1]:.0f} dB", | |
| "Under roughly 6 dB of crest the drums stop reading as hits and " | |
| "start reading as level. This is the single most common way a " | |
| "loud master ends up sounding smaller than a quiet one.", | |
| "Take 2β3 dB off the limiter, then reclaim density with parallel " | |
| "compression on the drum bus rather than more ceiling.", | |
| amount=p.crest[0] - rep.crest, tags=["dynamics"], | |
| )) | |
| elif rep.crest > p.crest[1] + 3.0 and is_master: | |
| out.append(Diagnosis( | |
| "uncontrolled", "note", | |
| f"Crest factor {rep.crest:.1f} dB β peaks are running free", | |
| f"Peak-to-RMS {rep.crest:.1f} dB vs target {p.crest[0]:.0f}β¦{p.crest[1]:.0f}", | |
| "A few isolated peaks are eating all the headroom, so the body " | |
| "of the track sits far lower than it could.", | |
| "Clip or soft-limit the worst transients before the bus " | |
| "compressor so the compressor stops chasing them.", | |
| tags=["dynamics"], | |
| )) | |
| if rep.lra > 12.0 and is_master and genre != "Ambient / Cinematic": | |
| out.append(Diagnosis( | |
| "loud_range", "note", | |
| f"Loudness range {rep.lra:.1f} LU β sections are uneven", | |
| f"LRA {rep.lra:.1f} LU", | |
| "Large section-to-section swings make club or car playback " | |
| "feel like it keeps changing volume.", | |
| "Automate section gain before the bus, or set a slow 1.5:1 " | |
| "compressor across the master to glue the arrangement.", | |
| tags=["dynamics"], | |
| )) | |
| # -- low end ------------------------------------------------------------ | |
| sub_vs_bass = r.get("sub_vs_bass", 0.0) | |
| if _above(sub_vs_bass, p.sub_vs_bass) > 2.0: | |
| amt = _above(sub_vs_bass, p.sub_vs_bass) | |
| out.append(Diagnosis( | |
| "sub_heavy", "warn", | |
| f"Sub is {amt:.1f} dB hotter than it should be", | |
| f"20β60 Hz sits {sub_vs_bass:+.1f} dB against 60β120 Hz " | |
| f"(target {p.sub_vs_bass[0]:+.0f}β¦{p.sub_vs_bass[1]:+.0f})", | |
| "Energy this low is felt, not heard. On laptop and phone speakers " | |
| "it vanishes entirely, so the track reads thin there while eating " | |
| "all the limiter's work on a big system.", | |
| f"High-pass the sub at 28β30 Hz and shelve 20β60 Hz down {amt:.1f} dB. " | |
| "Check it on a phone speaker before trusting the change.", | |
| band="sub", freq=45.0, amount=amt, tags=["lowend"], | |
| )) | |
| elif _below(sub_vs_bass, p.sub_vs_bass) > 3.0 and is_master: | |
| out.append(Diagnosis( | |
| "no_sub", "warn", | |
| "No real sub under the track", | |
| f"20β60 Hz sits {sub_vs_bass:+.1f} dB against 60β120 Hz " | |
| f"(target {p.sub_vs_bass[0]:+.0f}β¦{p.sub_vs_bass[1]:+.0f})", | |
| "The weight you are hearing is all upper bass. It will feel " | |
| "adequate on monitors and completely gutless on a club rig.", | |
| "Layer a clean sine sub an octave under the bass, mono, and " | |
| "sidechain it hard to the kick.", | |
| band="sub", freq=45.0, tags=["lowend"], | |
| )) | |
| # -- mud / boxiness ----------------------------------------------------- | |
| mud = r.get("mud", 0.0) | |
| mud_over = _above(mud, p.mud) | |
| if mud_over > 1.0: | |
| sev = "critical" if mud_over > 4.0 else "warn" | |
| out.append(Diagnosis( | |
| "mud", sev, | |
| f"Low mids are {mud_over:.1f} dB thick", | |
| f"120β350 Hz sits {mud:+.1f} dB against 350 Hzβ1.5 kHz " | |
| f"(target {p.mud[0]:+.0f}β¦{p.mud[1]:+.0f})", | |
| "This is the band every instrument has energy in and nobody needs. " | |
| "It builds up silently across a layered arrangement and eats the " | |
| "clarity of everything above it.", | |
| f"Narrow bell cut of {min(mud_over, 5.0):.1f} dB at 250 Hz " | |
| "(Q β 1.4) on the offending bus. If the cut makes the track thin, " | |
| "the problem is the arrangement, not the EQ β mute layers until " | |
| "you find which one owns 250 Hz.", | |
| band="lowmid", freq=250.0, amount=min(mud_over, 5.0), | |
| tags=["mud", "clarity"], | |
| )) | |
| elif _below(mud, p.mud) > 3.0: | |
| out.append(Diagnosis( | |
| "hollow", "note", | |
| "Scooped low mids β sounds hi-fi, plays thin", | |
| f"120β350 Hz sits {mud:+.1f} dB against the mids " | |
| f"(target {p.mud[0]:+.0f}β¦{p.mud[1]:+.0f})", | |
| "Over-cutting 200β400 Hz is the classic overcorrection. It sounds " | |
| "clean in isolation and disappears in a mix or a playlist.", | |
| "Give 2 dB back with a wide bell at 220 Hz (Q β 0.7).", | |
| band="lowmid", freq=220.0, tags=["clarity"], | |
| )) | |
| # -- harshness / dullness ---------------------------------------------- | |
| harsh = r.get("harsh", 0.0) | |
| if _above(harsh, p.harsh) > 1.5: | |
| amt = min(_above(harsh, p.harsh), 4.0) | |
| out.append(Diagnosis( | |
| "harsh", "warn", | |
| f"Upper mids {_above(harsh, p.harsh):.1f} dB hot β this will fatigue", | |
| f"1.5β4 kHz sits {harsh:+.1f} dB against the mids " | |
| f"(target {p.harsh[0]:+.0f}β¦{p.harsh[1]:+.0f})", | |
| "The ear is most sensitive right here. Excess reads as 'loud and " | |
| "exciting' for thirty seconds and as 'painful' for three minutes.", | |
| f"Dynamic EQ at 3 kHz, {amt:.1f} dB of downward movement, only when " | |
| "it crosses threshold β a static cut here kills presence.", | |
| band="himid", freq=3000.0, amount=amt, tags=["harsh", "tone"], | |
| )) | |
| air = r.get("air", 0.0) | |
| if _below(air, p.air) > 3.0: | |
| out.append(Diagnosis( | |
| "dull", "note", | |
| "Top end is closed in", | |
| f"8β16 kHz sits {air:+.1f} dB against the mids " | |
| f"(target {p.air[0]:+.0f}β¦{p.air[1]:+.0f})", | |
| "Nothing above 8 kHz means no sense of air or space, which usually " | |
| "reads to listeners as 'demo' rather than 'dark'.", | |
| "High shelf +2 dB at 10 kHz. If it turns harsh instead of open, " | |
| "the top end is distortion artefacts, not content β fix the source.", | |
| band="air", freq=10000.0, tags=["tone"], | |
| )) | |
| elif _above(air, p.air) > 3.0: | |
| out.append(Diagnosis( | |
| "brittle", "warn", | |
| "Top end is brittle", | |
| f"8β16 kHz sits {air:+.1f} dB against the mids " | |
| f"(target {p.air[0]:+.0f}β¦{p.air[1]:+.0f})", | |
| "Usually the fingerprint of an exciter or aggressive limiting " | |
| "rather than real content.", | |
| "High shelf -2 dB at 9 kHz and back off whatever is generating it.", | |
| band="air", freq=9000.0, tags=["tone"], | |
| )) | |
| # -- stereo ------------------------------------------------------------- | |
| corr = st.get("correlation", 1.0) | |
| sub_corr = st.get("sub_correlation", 1.0) | |
| if corr < -0.1: | |
| out.append(Diagnosis( | |
| "out_of_phase", "critical", | |
| f"Phase correlation {corr:+.2f} β this cancels in mono", | |
| f"L/R correlation {corr:+.2f}, mono fold loses " | |
| f"{abs(st.get('mono_loss_db', 0.0)):.1f} dB", | |
| "Negative correlation means the channels fight each other. Any mono " | |
| "playback β club sub, phone, most PA systems β loses that material.", | |
| "Find the widener or the inverted duplicate causing it. Check every " | |
| "stereo effect's phase before reaching for a corrector.", | |
| tags=["stereo", "phase"], | |
| )) | |
| if sub_corr < 0.85: | |
| out.append(Diagnosis( | |
| "stereo_sub", "critical" if sub_corr < 0.6 else "warn", | |
| f"Low end is not mono (correlation {sub_corr:+.2f} below 120 Hz)", | |
| f"20β120 Hz correlation {sub_corr:+.2f}", | |
| "Stereo information below 120 Hz cancels unpredictably on club " | |
| "systems and wastes cutter headroom on vinyl. There is no upside.", | |
| "Utility with Bass Mono engaged at 120 Hz, before the limiter.", | |
| band="sub", freq=120.0, tags=["stereo", "phase", "lowend"], | |
| )) | |
| width = st.get("width_db", -12.0) | |
| if _below(width, p.width) > 4.0 and is_master: | |
| out.append(Diagnosis( | |
| "narrow", "note", | |
| "Image is narrow", | |
| f"Side/mid ratio {width:+.1f} dB (target {p.width[0]:+.0f}β¦{p.width[1]:+.0f})", | |
| "Almost everything is centred, so the mix has no depth to move " | |
| "into when the drop hits.", | |
| "Widen the elements that can afford it β reverb returns, pads, hat " | |
| "layers β not the bass and not the kick.", | |
| tags=["stereo"], | |
| )) | |
| elif _above(width, p.width) > 4.0: | |
| out.append(Diagnosis( | |
| "over_wide", "warn", | |
| "Over-widened", | |
| f"Side/mid ratio {width:+.1f} dB (target {p.width[0]:+.0f}β¦{p.width[1]:+.0f})", | |
| "More side energy than mid means the centre is hollow and the mono " | |
| "fold will be a different mix entirely.", | |
| "Pull the widener back and check the mono fold before committing.", | |
| tags=["stereo"], | |
| )) | |
| # -- source-specific ---------------------------------------------------- | |
| if source == "Bass / 808" and rep.bands.get("air", -99.0) > -12.0: | |
| out.append(Diagnosis( | |
| "bass_top", "note", | |
| "Bass is carrying a lot of top end", | |
| f"8β16 kHz at {rep.bands['air']:.1f} dB relative", | |
| "Fine if it is intentional grit; a problem if it is aliasing from " | |
| "distortion or a resampled sample.", | |
| "Low-pass at 12 kHz and A/B. If nothing is lost, it was noise.", | |
| tags=["bass"], | |
| )) | |
| if source == "Vocal" and _above(r.get("harsh", 0.0), (-6.0, 0.0)) > 1.0: | |
| out.append(Diagnosis( | |
| "sibilance", "warn", | |
| "Sibilance range is hot", | |
| f"1.5β4 kHz {r.get('harsh', 0.0):+.1f} dB against the mids", | |
| "Reads as 'ess' and 'tuh' jumping out of the line.", | |
| "De-esser at 6β8 kHz, 3 dB of range, then a dynamic bell at 3 kHz " | |
| "for the hardness underneath it.", | |
| band="himid", freq=6500.0, tags=["vocal"], | |
| )) | |
| if source == "Drum bus" and rep.crest < 8.0 and rep.duration > 2.0: | |
| out.append(Diagnosis( | |
| "flat_drums", "warn", | |
| f"Drum bus crest is only {rep.crest:.1f} dB", | |
| f"Peak-to-RMS {rep.crest:.1f} dB", | |
| "Drums are the one bus where crest is the whole point.", | |
| "Slower attack on the bus compressor (30 ms) so the hits get " | |
| "through before gain reduction starts.", | |
| tags=["drums", "dynamics"], | |
| )) | |
| if not out: | |
| out.append(Diagnosis( | |
| "clean", "good", | |
| "Nothing is fighting you", | |
| f"Balance, dynamics and phase all inside the {p.name} window", | |
| "The measurable problems are absent, which means the remaining " | |
| "decisions are taste, not repair.", | |
| "Move on to arrangement and sound selection.", | |
| tags=["ok"], | |
| )) | |
| out.sort(key=lambda d: (_SEVERITY_RANK.get(d.severity, 9), -abs(d.amount))) | |
| return out | |
| def headline_verdict(rep: dsp.Report, diags: list[Diagnosis]) -> tuple[str, str]: | |
| """One-line status for the live meter strip.""" | |
| if not diags: | |
| return "listening", "ok" | |
| worst = diags[0] | |
| if worst.severity == "critical": | |
| return worst.headline, "critical" | |
| if worst.severity == "warn": | |
| return worst.headline, "warn" | |
| if worst.severity == "good": | |
| return "Balance is holding", "ok" | |
| return worst.headline, "note" | |
| def profile_for(genre: str) -> Profile: | |
| return GENRES.get(genre, GENRES["Dubstep / Riddim"]) | |
| def band_target_windows(genre: str) -> dict[str, tuple[float, float]]: | |
| """Approximate absolute band windows, for the ghost range behind each bar. | |
| Drawing only β the rules themselves never fire on these numbers. | |
| """ | |
| p = profile_for(genre) | |
| a = -6.5 # nominal mid-band anchor for a full-range balance | |
| return { | |
| "sub": (a + p.sub_vs_bass[0] - 1.0, a + p.sub_vs_bass[1] + 2.0), | |
| "bass": (a - 4.0, a + 2.0), | |
| "lowmid": (a + p.mud[0], a + p.mud[1]), | |
| "mid": (a - 2.5, a + 2.5), | |
| "himid": (a + p.harsh[0], a + p.harsh[1]), | |
| "presence": (a + p.harsh[0] - 4.0, a + p.harsh[1] - 2.0), | |
| "air": (a + p.air[0], a + p.air[1]), | |
| } | |