Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 87 additions & 17 deletions internal/logging/recording_tips.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func GenerateRecordingTips(m *processor.AudioMeasurements, config *processor.Fil
tipDynamicRange,
tipOverCompressed,
tipPoorSNR,
tipHighCrestFactor,
}

for _, rule := range rules {
Expand Down Expand Up @@ -74,7 +75,13 @@ func applyExclusions(tips []RecordingTip, fired map[string]bool) []RecordingTip
for _, tip := range tips {
switch tip.RuleID {
case "level_too_quiet", "level_quiet":
if fired["level_clipping"] || fired["level_near_clipping"] || fired["too_far_from_mic"] {
// Suppress when "too far from mic" fires (it addresses the root cause)
if fired["too_far_from_mic"] {
continue
}
// Suppress when clipping/near-clipping fires, UNLESS high_crest_factor
// also fired (compound problem: quiet speech + loud transients)
if (fired["level_clipping"] || fired["level_near_clipping"]) && !fired["high_crest_factor"] {
continue
}
case "poor_snr":
Expand Down Expand Up @@ -116,27 +123,39 @@ func wrapText(text string, maxWidth int, indent string) string {
// falling back to InputI < -30 LUFS when no speech profile exists.
// Gain target is -24 dBFS for speech RMS, -18 LUFS for InputI fallback.
func tipLevelTooQuiet(m *processor.AudioMeasurements, _ *processor.FilterChainConfig) *RecordingTip {
var gainNeeded float64
if m.SpeechProfile != nil {
speechRMS := m.SpeechProfile.RMSLevel
if speechRMS >= -42.0 {
return nil
}
gainNeeded := -24.0 - speechRMS
gainNeeded = -24.0 - speechRMS
} else {
if m.InputI >= -30.0 {
return nil
}
gainNeeded = -18.0 - m.InputI
}
// Clamp to available peak headroom (keep peaks below -1 dBTP)
maxSafeGain := -1.0 - m.InputTP
wasClamped := gainNeeded > maxSafeGain
gainNeeded = min(gainNeeded, maxSafeGain)
// If almost no headroom available, the problem is peak-to-average ratio, not gain
if gainNeeded < 2.0 {
return &RecordingTip{
Priority: 10,
RuleID: "level_too_quiet",
Message: fmt.Sprintf("Your microphone gain is too low - try increasing it by about %.0f dB.", gainNeeded),
Message: "Your speech is quiet but peaks are already near the ceiling - this usually means plosives or handling noise are using up your headroom. Try a pop filter or check for vibrations reaching your microphone.",
}
}
// Fallback: no speech profile, use integrated LUFS
if m.InputI >= -30.0 {
return nil
msg := fmt.Sprintf("Your microphone gain is too low - try increasing it by about %.0f dB.", gainNeeded)
if wasClamped {
msg = fmt.Sprintf("Your microphone gain is too low - try increasing it by about %.0f dB (accounting for your existing peak levels).", gainNeeded)
}
gainNeeded := -18.0 - m.InputI
return &RecordingTip{
Priority: 10,
RuleID: "level_too_quiet",
Message: fmt.Sprintf("Your microphone gain is too low - try increasing it by about %.0f dB.", gainNeeded),
Message: msg,
}
}

Expand All @@ -145,27 +164,39 @@ func tipLevelTooQuiet(m *processor.AudioMeasurements, _ *processor.FilterChainCo
// falling back to InputI between -30 and -24 LUFS when no speech profile exists.
// Gain target is -24 dBFS for speech RMS, -18 LUFS for InputI fallback.
func tipLevelQuiet(m *processor.AudioMeasurements, _ *processor.FilterChainConfig) *RecordingTip {
var gainNeeded float64
if m.SpeechProfile != nil {
speechRMS := m.SpeechProfile.RMSLevel
if speechRMS < -42.0 || speechRMS >= -36.0 {
return nil
}
gainNeeded := -24.0 - speechRMS
gainNeeded = -24.0 - speechRMS
} else {
if m.InputI < -30.0 || m.InputI >= -24.0 {
return nil
}
gainNeeded = -18.0 - m.InputI
}
// Clamp to available peak headroom (keep peaks below -1 dBTP)
maxSafeGain := -1.0 - m.InputTP
wasClamped := gainNeeded > maxSafeGain
gainNeeded = min(gainNeeded, maxSafeGain)
// If almost no headroom available, the problem is peak-to-average ratio, not gain
if gainNeeded < 2.0 {
return &RecordingTip{
Priority: 8,
RuleID: "level_quiet",
Message: fmt.Sprintf("Your recording is a bit quiet - increasing your microphone gain by about %.0f dB would improve quality.", gainNeeded),
Message: "Your recording is a bit quiet but peaks are already near the ceiling - this usually means plosives or handling noise are using up your headroom. Try a pop filter or check for vibrations reaching your microphone.",
}
}
// Fallback: no speech profile, use integrated LUFS
if m.InputI < -30.0 || m.InputI >= -24.0 {
return nil
msg := fmt.Sprintf("Your recording is a bit quiet - increasing your microphone gain by about %.0f dB would improve quality.", gainNeeded)
if wasClamped {
msg = fmt.Sprintf("Your recording is a bit quiet - increasing your microphone gain by about %.0f dB would improve quality (accounting for your existing peak levels).", gainNeeded)
}
gainNeeded := -18.0 - m.InputI
return &RecordingTip{
Priority: 8,
RuleID: "level_quiet",
Message: fmt.Sprintf("Your recording is a bit quiet - increasing your microphone gain by about %.0f dB would improve quality.", gainNeeded),
Message: msg,
}
}

Expand All @@ -176,16 +207,35 @@ func tipLevelTooHot(m *processor.AudioMeasurements, _ *processor.FilterChainConf
return nil
}
if m.InputTP > 0.0 {
// Clipping case
if m.InputI < -28.0 {
// Simultaneously quiet and clipping - compound problem
return &RecordingTip{
Priority: 10,
RuleID: "level_clipping",
Message: "Your recording is clipping on peak moments but is otherwise very quiet. This usually means plosives or transient noise - a pop filter and consistent mic distance will help more than changing gain.",
}
}
// reduction is always > 3.0 here because InputTP > 0.0
reduction := m.InputTP + 3.0 // bring peaks to -3 dBTP
return &RecordingTip{
Priority: 10,
RuleID: "level_clipping",
Message: "Your recording is clipping - turn your microphone gain down by 6-10 dB to prevent distortion.",
Message: fmt.Sprintf("Your recording is clipping - turn your microphone gain down by about %.0f dB to prevent distortion.", reduction),
}
}
// Near-clipping case (InputTP between -1.0 exclusive and 0.0 inclusive)
reduction := m.InputTP + 3.0 // bring peaks to -3 dBTP
var msg string
if reduction < 3.0 {
msg = "Your recording is very close to clipping - try turning your microphone gain down slightly to give yourself some headroom."
} else {
msg = fmt.Sprintf("Your recording is very close to clipping - turn your microphone gain down by about %.0f dB to give yourself some headroom.", reduction)
}
return &RecordingTip{
Priority: 9,
RuleID: "level_near_clipping",
Message: "Your recording is very close to clipping - turn your microphone gain down by 3-6 dB to give yourself some headroom.",
Message: msg,
}
}

Expand Down Expand Up @@ -355,3 +405,23 @@ func tipPoorSNR(m *processor.AudioMeasurements, _ *processor.FilterChainConfig)
Message: "The gap between your voice and the background noise is very small. Move closer to your microphone and reduce background noise if possible.",
}
}

// tipHighCrestFactor fires when the peak-to-average ratio is very high,
// indicating plosives, handling noise, or inconsistent mic distance.
// Threshold: CrestFactor > 20 dB (well above spoken word optimal 9-14 dB).
// CrestFactor == 0 is treated as unmeasured and skipped.
// Prefers SpeechProfile.CrestFactor when available, falling back to full-file CrestFactor.
func tipHighCrestFactor(m *processor.AudioMeasurements, _ *processor.FilterChainConfig) *RecordingTip {
crest := m.CrestFactor
if m.SpeechProfile != nil && m.SpeechProfile.CrestFactor > 0 {
crest = m.SpeechProfile.CrestFactor
}
if crest <= 20.0 || crest == 0 {
return nil
}
return &RecordingTip{
Priority: 7,
RuleID: "high_crest_factor",
Message: "Your recording has a large gap between peak levels and average speech volume. This is usually caused by plosives, handling noise, or varying distance from the microphone. Try using a pop filter and keeping a consistent distance from your mic.",
}
}
Loading