@@ -248,19 +248,6 @@ const (
248248 speechnormPeakTarget = 0.95 // Headroom for limiter
249249 speechnormSmoothingFast = 0.001 // Fast response time
250250
251- // Bleed gate parameters
252- // Catches bleed/crosstalk that was amplified by speechnorm/dynaudnorm
253- // Threshold is calculated from: predicted_output_bleed = silence_peak_level + worst_case_gain
254- bleedGateMarginDB = 6.0 // dB above predicted bleed to set threshold (safety margin)
255- bleedGateEnableThresholdDB = - 40.0 // dBFS - only enable if predicted output bleed is above this
256- bleedGateMinThresholdDB = - 50.0 // dBFS - minimum threshold (never gate below this)
257- bleedGateMaxThresholdDB = - 20.0 // dBFS - maximum threshold (never gate above this, would cut speech)
258- bleedGateDefaultRatio = 4.0 // Gentler than pre-gate (suppress rather than cut)
259- bleedGateDefaultAttack = 15.0 // ms - faster than pre-gate
260- bleedGateDefaultRelease = 200.0 // ms - smooth release
261- bleedGateDefaultRange = 0.125 // -18dB reduction (less aggressive than pre-gate)
262- bleedGateDefaultKnee = 3.0 // Soft knee
263-
264251 // Mains hum filter parameters
265252 humEntropyThreshold = 0.7 // Below this = tonal noise detected (hum/buzz)
266253 humFreq50Hz = 50.0 // UK/EU mains fundamental frequency
@@ -482,7 +469,6 @@ func AdaptConfig(config *FilterChainConfig, measurements *AudioMeasurements) {
482469 tuneLA2ACompressor (config , measurements )
483470 tuneDynaudnorm (config )
484471 tuneSpeechnorm (config , measurements , lufsGap )
485- tuneBleedGate (config , measurements , lufsGap ) // Bleed gate for amplified bleed/crosstalk
486472
487473 // Final safety checks
488474 sanitizeConfig (config )
@@ -1664,111 +1650,6 @@ func tuneSpeechnormDenoise(config *FilterChainConfig, expansion float64) {
16641650 }
16651651}
16661652
1667- // tuneBleedGate adapts the bleed gate based on predicted output bleed level.
1668- //
1669- // The bleed gate catches bleed/crosstalk that was amplified by speechnorm/dynaudnorm
1670- // after the denoisers have run. Denoisers preserve speech-like content (which is what
1671- // they're designed to do), but headphone bleed IS speech-like content from other speakers.
1672- //
1673- // Strategy:
1674- // - Use silence sample PEAK level (not noise floor) - captures bleed bursts, not just hiss
1675- // - Calculate worst-case gain: speechnorm normalises each half-cycle to peak target
1676- // - For silence with bleed, this can mean 40-50dB of gain applied
1677- // - Use crest factor to detect presence of bleed (high crest = impulsive content in silence)
1678- // - Adjust ratio/range based on how much bleed is detected
1679- //
1680- // Key insight: Speechnorm applies VARIABLE gain per half-cycle. For quiet sections
1681- // (like silence with bleed), it applies much more gain than the "expansion" factor
1682- // suggests. The actual gain on silence can be:
1683- //
1684- // silence_input_peak → target_peak (0.95 = -0.45 dBFS)
1685- //
1686- // Measurements used:
1687- // - NoiseProfile.PeakLevel: captures the loudest bleed burst
1688- // - NoiseProfile.MeasuredNoiseFloor: captures the background hiss
1689- // - NoiseProfile.CrestFactor: high crest = impulsive content (bleed), low = steady hiss
1690- // - NoiseProfile.Entropy: low entropy = tonal (hum), high = broadband (hiss/bleed)
1691- func tuneBleedGate (config * FilterChainConfig , measurements * AudioMeasurements , lufsGap float64 ) {
1692- // Respect user's intent: if filter is disabled, don't touch it
1693- if ! config .BleedGateEnabled {
1694- return
1695- }
1696-
1697- // Need noise profile with measurements to calculate threshold
1698- if measurements .NoiseProfile == nil {
1699- config .BleedGateEnabled = false
1700- return
1701- }
1702-
1703- np := measurements .NoiseProfile
1704-
1705- // Calculate worst-case gain: speechnorm can apply gain to bring quiet content to peak
1706- // The target peak is typically 0.95 (-0.45 dBFS)
1707- targetPeakDB := - 0.45 // 20 * log10(0.95)
1708- if config .SpeechnormEnabled && config .SpeechnormPeak > 0 {
1709- targetPeakDB = 20.0 * math .Log10 (config .SpeechnormPeak )
1710- }
1711-
1712- // Worst-case gain = what's needed to bring silence peak to target peak
1713- // This is the maximum gain speechnorm could apply to the silence content
1714- worstCaseGainDB := targetPeakDB - np .PeakLevel
1715-
1716- // Calculate predicted output level for the silence PEAK (the bleed content)
1717- predictedOutputPeakDB := np .PeakLevel + worstCaseGainDB
1718-
1719- // Calculate predicted output noise floor
1720- predictedOutputNoiseDB := np .MeasuredNoiseFloor + worstCaseGainDB
1721-
1722- // Detect bleed presence using crest factor and peak-to-floor ratio
1723- // Crest factor = peak - RMS; high crest means impulsive content in silence
1724- // For pure hiss, crest factor is ~10-12dB; for bleed it's typically 20-30dB
1725- peakToFloorDB := np .PeakLevel - np .MeasuredNoiseFloor
1726- hasSignificantBleed := np .CrestFactor > 15.0 || peakToFloorDB > 20.0
1727-
1728- // Determine threshold strategy based on bleed detection
1729- var thresholdDB float64
1730- if hasSignificantBleed {
1731- // Bleed detected - use peak-based threshold (more aggressive)
1732- // Set threshold to catch the amplified bleed peaks
1733- thresholdDB = predictedOutputPeakDB - 3.0 // 3dB below predicted peak
1734- } else {
1735- // No significant bleed - use noise floor based threshold (standard approach)
1736- thresholdDB = predictedOutputNoiseDB + bleedGateMarginDB
1737- }
1738-
1739- // Only enable bleed gate if predicted output would be audible
1740- if thresholdDB < bleedGateEnableThresholdDB {
1741- config .BleedGateEnabled = false
1742- return
1743- }
1744-
1745- // Enable bleed gate
1746- config .BleedGateEnabled = true
1747-
1748- // Clamp threshold to safety limits
1749- thresholdDB = clamp (thresholdDB , bleedGateMinThresholdDB , bleedGateMaxThresholdDB )
1750-
1751- // Convert to linear for agate filter
1752- config .BleedGateThreshold = dbToLinear (thresholdDB )
1753-
1754- // Adapt ratio and range based on bleed severity
1755- if hasSignificantBleed {
1756- // Significant bleed - use stronger settings
1757- config .BleedGateRatio = 6.0 // Stronger ratio for bleed
1758- config .BleedGateRange = 0.063 // -24dB reduction (more aggressive)
1759- config .BleedGateAttack = 10.0 // Faster attack to catch bleed transients
1760- config .BleedGateRelease = 150.0
1761- } else {
1762- // Mild bleed or just noise amplification - use gentler settings
1763- config .BleedGateRatio = bleedGateDefaultRatio
1764- config .BleedGateRange = bleedGateDefaultRange
1765- config .BleedGateAttack = bleedGateDefaultAttack
1766- config .BleedGateRelease = bleedGateDefaultRelease
1767- }
1768-
1769- config .BleedGateKnee = bleedGateDefaultKnee
1770- }
1771-
17721653// sanitizeConfig ensures no NaN or Inf values remain after adaptive tuning
17731654func sanitizeConfig (config * FilterChainConfig ) {
17741655 // DS201-inspired highpass filter
@@ -1801,11 +1682,6 @@ func sanitizeConfig(config *FilterChainConfig) {
18011682 if config .DS201HumHarmonics < 1 || config .DS201HumHarmonics > 8 {
18021683 config .DS201HumHarmonics = defaultHumHarmonics
18031684 }
1804-
1805- // BleedGateThreshold needs additional check for zero/negative (like pre-gate)
1806- if math .IsNaN (config .BleedGateThreshold ) || math .IsInf (config .BleedGateThreshold , 0 ) || config .BleedGateThreshold <= 0 {
1807- config .BleedGateThreshold = ds201DefaultGateThreshold // Use same default as pre-gate
1808- }
18091685}
18101686
18111687// sanitizeFloat returns defaultVal if val is NaN or Inf
0 commit comments