DEV Community

Super Funicular
Super Funicular

Posted on

Does VOICE_RECOGNITION Make Breaking Glass Sound Duller? What a 4 kHz Ceiling Costs a Phone Camera's Sound Detection

Last week I wrote about why a phone security camera's audio hisses in a quiet room. In short: automatic gain control (AGC) turns an empty room up. Of the audio sources a camera app would pick, VOICE_RECOGNITION is the one with a written promise that AGC and noise reduction are off by default.

Ahmed Znouda left a comment that deserves its own post. He asked two things:

"Did you try VOICE_RECOGNITION for the camera case? The flat-response target only covers 100 Hz to 4 kHz, so I'd be curious whether a slamming door or breaking glass sounds duller than with UNPROCESSED."

He also suggested a check that costs almost nothing at runtime: create the effect on the recorder's session and read getEnabled() before changing anything, then log the result per phone model.

This post does both. First it tests his frequency question with a reproducible simulation. Then it turns his runtime check into a log line that also catches effects your code didn't create.

What the compatibility rules promise, by frequency

The two sources are specified differently. Both figures come from Android's Compatibility Definition (CDD):

Source Flatness the CDD asks for What it says above that band
VOICE_RECOGNITION approximately flat, ±3 dB from 100 Hz to 4000 Hz nothing
UNPROCESSED ±10 dB from 100 Hz to 7000 Hz, for every mic used within ±30 dB from 7000 Hz to 22 kHz of the mid band

So Ahmed's reading is right, with one precise point. The CDD doesn't say a voice-recognition path cuts above 4 kHz. It just doesn't promise anything there. A phone can pass compatibility with a voice path that rolls off steeply at 4 kHz, or with one that runs flat to 20 kHz. You can't know which from the spec.

The useful question, then, is: if the phone does roll off at 4 kHz, what does that cost?

The simulation

I built three synthetic events and put each into the same 30-second pink-noise room at the 10-second mark:

  • "Door": brown noise, low-passed at 400 Hz, 0.4 s with a fade. Almost all of its energy is a low thump.
  • "Glass (high)": white noise, high-passed twice at 3 kHz, 0.6 s with a fade. This is a deliberately unfavourable case: nearly all of its energy sits above the ceiling.
  • "Glass (broad)": the same, but high-passed at 1 kHz, so part of its energy falls under 4 kHz.

These are not recordings of a real door or real glass. Real breaking glass has energy across a wide band, and how much sits above 4 kHz depends on the pane, the room and the microphone. The two glass versions bracket that range. They don't measure it.

Then I ran each file through two stand-ins:

  • The 4 kHz ceiling: four cascaded lowpass=f=4000 filters, which gives a steep roll-off. This is the worst case the CDD allows for a voice path, not a description of any real phone.
  • AGC: dynaudnorm=f=500:g=31:m=30, the same stand-in as last week. It isn't any phone's AGC. It's a dynamic normaliser that raises quiet passages the way AGC does.
# Room + one event at t=10 s (shown for the high glass; door and broad glass are built the same way)
ffmpeg -f lavfi -i "anoisesrc=color=pink:amplitude=0.003:duration=30:sample_rate=48000:seed=7" room.wav
ffmpeg -f lavfi -i "anoisesrc=color=white:amplitude=0.5:duration=0.6:sample_rate=48000:seed=11" \
       -af "highpass=f=3000,highpass=f=3000,afade=t=out:st=0:d=0.6" glass.wav
ffmpeg -f lavfi -i "anoisesrc=color=brown:amplitude=0.9:duration=0.4:sample_rate=48000:seed=5" \
       -af "lowpass=f=400,afade=t=out:st=0:d=0.4" door.wav
ffmpeg -i room.wav -i glass.wav -filter_complex \
  "[1]adelay=10000,apad=whole_len=1440000[e];[0][e]amix=inputs=2:normalize=0" -ac 1 mix_glass.wav

# 4 kHz ceiling stand-in
ffmpeg -i mix_glass.wav -af "lowpass=f=4000,lowpass=f=4000,lowpass=f=4000,lowpass=f=4000" lp_glass.wav
# AGC stand-in
ffmpeg -i mix_glass.wav -af dynaudnorm=f=500:g=31:m=30 agc_mix_glass.wav

# Levels: the event (10.0–10.4 s) and quiet room (12–21 s), RMS dBFS
ffmpeg -ss 10 -t 0.4 -i lp_glass.wav -af astats=measure_perchannel=none:measure_overall=RMS_level -f null - 2>&1 | grep "RMS level"
ffmpeg -ss 12 -t 9   -i lp_glass.wav -af astats=measure_perchannel=none:measure_overall=RMS_level -f null - 2>&1 | grep "RMS level"
Enter fullscreen mode Exit fullscreen mode

Results

Contrast below is the event's RMS level minus the quiet room's RMS level. That gap is what a sound detector lives on. ffmpeg 4.4.2, run 2026-11-04.

Event Raw 4 kHz ceiling only AGC stand-in only Both
Door (low thump) 43.4 dB 45.5 dB 28.5 dB 30.5 dB
Glass (broad, >1 kHz) 50.4 dB 40.1 dB 31.7 dB 29.7 dB
Glass (high, >3 kHz) 49.9 dB 30.6 dB 31.4 dB 26.0 dB

The event levels themselves show the same thing. Under the ceiling, the door's RMS didn't move (−21.3 → −21.3 dBFS). The high glass dropped 21.3 dB (−14.8 → −36.1). The broad glass dropped 12.4 dB (−14.2 → −26.6).

What that answers

A slamming door: no, it doesn't get duller. Its energy is under 4 kHz, so the ceiling takes nothing from it. Contrast actually rose 2.1 dB, because the ceiling also trimmed some of the room's hiss (the quiet floor fell from −64.7 to −66.7 dBFS). For low events, a voice-recognition path that keeps its no-AGC promise is a pure gain: you skip the ~15 dB the AGC stand-in costs.

Breaking glass: possibly, and by about as much as AGC costs. In the unfavourable case, a hard 4 kHz ceiling cut the glass's contrast by 19.3 dB. The AGC stand-in cut it by 18.5 dB. So for a high-pitched event, a worst-case voice path doesn't remove the loss. It swaps one ~19 dB loss for another. With broader glass the ceiling cost 10.3 dB, about half as much.

Both together is the worst row. A phone that applies AGC and rolls off high (which a camcorder-style path can do, since its tuning is vendor-specific) left the high glass at 26.0 dB.

With the ceiling alone, a fixed −50 dB silencedetect threshold (2 s minimum) still isolated every event: silence before 10 s, silence again right after the event. With the AGC stand-in, the room itself rose to about −42 to −45 dBFS, above that threshold. The detector then reported one long stretch of "activity" running from about 4–10 s to 27 s, with the event lost inside it. That's last week's finding again: a fixed threshold can't follow a room that AGC keeps moving. The ceiling shrinks the margin, and AGC moves the floor up into it. Put in a louder room, a fan, or a phone farther away, and the case that starts with the least margin is the one that drops out first.

So there's no single best source for a camera. It depends on what you're listening for:

  • Doors, footsteps, voices, knocks: VOICE_RECOGNITION is the safe choice. Everything that matters is under 4 kHz, and AGC is promised off.
  • Glass, alarms, smoke-detector chirps: these sit high (a typical smoke alarm tone is around 3 kHz, with harmonics above it). UNPROCESSED, where supported, is the source whose spec actually covers the upper band. Check AudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED) first: it returns null when there's no support, and the source then behaves like DEFAULT.

Ahmed's runtime check, extended

The audiofx docs say this directly: an AGC, noise suppressor or echo canceller "can be inserted by default in the capture path by the platform according to the MediaRecorder.AudioSource used." The docs tell the app to create the effect and read its enabled state to learn the default for that session. That's Ahmed's check:

// Per-effect check (API 16+). null = this phone doesn't expose that effect through audiofx.
val session = audioRecord.audioSessionId
val agc = AutomaticGainControl.create(session)
val ns  = NoiseSuppressor.create(session)
Log.i(TAG, "agcDefaultOn=${agc?.enabled} nsDefaultOn=${ns?.enabled}")
agc?.release(); ns?.release()
Enter fullscreen mode Exit fullscreen mode

It has one blind spot. It only answers for effect types you think to ask about. Since API 29 there's a second view that lists whatever is running. AudioRecord and MediaRecorder both implement AudioRecordingMonitor. Once capture has started, getActiveRecordingConfiguration() returns an AudioRecordingConfiguration with two lists:

  • getClientEffects(): the effects on your capture client.
  • getEffects(): the effects on the capture stream. Android's audio-sharing guide notes that these two can differ when another app is capturing at the same time with higher priority.
// API 29+. Call after start(); returns null before capture is active.
fun logCaptureEffects(monitor: AudioRecordingMonitor) {
    val cfg = monitor.activeRecordingConfiguration ?: return
    fun label(list: List<AudioEffect.Descriptor>) = list.joinToString { d ->
        when (d.type) {
            AudioEffect.EFFECT_TYPE_AGC -> "AGC"
            AudioEffect.EFFECT_TYPE_NS  -> "NS"
            AudioEffect.EFFECT_TYPE_AEC -> "AEC"
            else -> d.name
        }
    }.ifEmpty { "none" }
    Log.i(TAG, "${Build.MANUFACTURER} ${Build.MODEL} source=${cfg.clientAudioSource} " +
               "client=[${label(cfg.clientEffects)}] stream=[${label(cfg.effects)}]")
}
Enter fullscreen mode Exit fullscreen mode

Logged once per session, that gives you a per-model table of which source actually came with which processing. It's the measurement the spec can't give you. Two cautions:

  • An empty list isn't proof of a flat path. These lists report effects the framework knows about. A vendor can still tune gain or equalisation inside the HAL without listing it as an effect, so the descriptor list is a floor, not a ceiling. The ten-minute offline recording test from the previous post is still the final check.
  • None of this tells you the frequency response. For that, record one known high sound (a phone playing a 6 kHz tone works) with each source on the same device, and compare levels with astats as above.

Where this fits

I build Background Camera RemoteStream, an Android app that turns a spare phone into a camera that keeps recording with the screen off. Recordings stay on the phone, there's a live view from any browser on the same Wi-Fi, and YouTube Live is there when you want an off-site copy. I'm not claiming a particular audio source for it here. The point of this post is that the right source depends on what you want to hear, and the behaviour depends on the phone. Both are things you measure, not things you assume.

Thanks to Ahmed for a question that turned out to have a two-sided answer. More at superfunicular.com.

Sources

Top comments (0)