Audio Event Detection — AudioEventDetectorBlock¶
AudioEventDetectorBlock is an audio-only Media Block from VisioForge.DotNet.Core.AI. It taps the audio stream, resamples it to 16 kHz mono, and runs a YAMNet (Google, AudioSet) ONNX classifier over short windows to recognize real-world sounds — siren, dog bark, glass break, alarm, music, speech and hundreds more (521 AudioSet classes). Audio passes through unchanged. The block implements IAudioProcessingBlock, so it can be inserted into a manual pipeline or registered on VideoCaptureCoreX/MediaPlayerCoreX. This is a natural companion to the object-analytics and PTZ features for surveillance and monitoring.
using VisioForge.Core.MediaBlocks;
using VisioForge.Core.MediaBlocks.AI;
using VisioForge.Core.MediaBlocks.Sources;
using VisioForge.Core.MediaBlocks.Special;
using VisioForge.Core.Types.X.AI;
using VisioForge.Core.Types.X.Sources;
Basic block setup¶
var settings = new AudioEventDetectorSettings(yamnetModelPath)
{
Threshold = 0.5f, // confidence a class must reach to fire
SmoothingWindows = 3, // moving average that suppresses one-window blips
};
var detector = new AudioEventDetectorBlock(settings);
detector.OnAudioEvent += (sender, e) =>
{
// Raised on a background worker thread — marshal to the UI thread before touching UI.
Console.WriteLine($"{e.Label} ({e.Confidence:F2}) [{e.Start:mm\\:ss} - {e.End:mm\\:ss}]");
};
Wire it between an audio source and a sink (or any downstream audio block):
var source = new UniversalSourceBlock(
await UniversalSourceSettings.CreateAsync(mediaFile, renderVideo: false, renderAudio: true));
// Unsynced: an offline file is analyzed as fast as the model allows.
// Keep IsSync = true (the default) for a live source you also want to monitor.
var sink = new NullRendererBlock(MediaBlockPadMediaType.Audio) { IsSync = false };
pipeline.Connect(source.AudioOutput, detector.Input);
pipeline.Connect(detector.Output, sink.Input);
await pipeline.StartAsync();
Using it with VideoCaptureCoreX and MediaPlayerCoreX¶
AudioEventDetectorBlock implements IAudioProcessingBlock, so instead of building a manual pipeline you can register it on an X engine. The block must be added before the session starts — the processing block list is consumed while the pipeline is built, and a block added later is ignored.
For capture, terminate the audio chain with a non-synced null renderer if you want analysis without speaker monitoring or recording:
core.Audio_Source = microphoneSettings;
core.Audio_OutputBlock = new NullRendererBlock(MediaBlockPadMediaType.Audio)
{
IsSync = false,
};
var detector = new AudioEventDetectorBlock(settings);
detector.OnAudioEvent += Detector_OnAudioEvent;
core.Audio_Processing_AddBlock(detector); // before StartAsync
await core.StartAsync();
For playback, Audio_Play must be true for the audio chain to be built:
player.Audio_Play = true;
player.Audio_OutputBlock = new NullRendererBlock(MediaBlockPadMediaType.Audio)
{
IsSync = false,
};
var detector = new AudioEventDetectorBlock(settings);
detector.OnAudioEvent += Detector_OnAudioEvent;
player.Audio_Processing_AddBlock(detector); // before OpenAsync / PlayAsync
await player.OpenAsync(source);
await player.PlayAsync();
See Using AI blocks with VideoCaptureCoreX and MediaPlayerCoreX for the full Audio_Processing_*/Audio_OutputBlock API and lifecycle rules.
How events are raised¶
The block classifies each WindowSeconds window, averages the model's per-frame scores, and smooths them with a moving average over SmoothingWindows windows. A per-class hysteresis (Schmitt trigger) then collapses one continuous sound into a single event: a class enters at Threshold and leaves at Threshold * ReleaseRatio (0.7 by default). OnAudioEvent fires once per sound, when it ends (drops below the release threshold) or at end-of-stream, carrying the full Start..End span and the peak Confidence. This means one continuous siren yields a single event with a duration, not a stream of duplicates.
Start and End are measured over the audio the block has actually analysed, counting from zero when the pipeline starts — they are not media positions. For straight-through playback or capture from the beginning the two are the same. They part company once audio is skipped or replayed underneath the block: seeking a MediaPlayerCoreX does not reset this clock, so after a seek the reported times no longer line up with positions in the file, and a gap in a live source leaves every later time early by the missing duration.
Inference runs on a dedicated background worker fed by an unbounded sample queue, so the streaming thread is never blocked and the queue never discards samples to bound its own growth: a source that outruns the worker (a live source on a slow provider, or a file decoded at full speed by an unsynced renderer) simply grows the backlog — 64 KB per buffered second at 16 kHz mono — which drains as soon as the worker catches up. Two designed exits stop the analysis instead of quietly dropping a fraction of it; both are described in the notes below. YAMNet is tiny — on the CPU, roughly 0.6 ms for the default 0.975 s window and 1.8 ms for the 5 s maximum (the cost follows the window's frame count), over 1000× real time either way, so the CPU provider is usually sufficient.
Key settings¶
AudioEventDetectorSettings(yamnetModelPath). Unlike the vision AI settings, this type does not derive from OnnxInferenceSettings — YAMNet consumes an audio waveform, not an image. Every property below is read once, when the block is built during StartAsync — set them before you start the pipeline; mutating the settings object afterwards has no effect.
| Property | Default | Description |
|---|---|---|
ModelPath | — | Absolute path to the YAMNet ONNX model. Required. |
Threshold | 0.5 | Confidence a class must reach to start an event, clamped to 0.01–1 (a threshold of 0 could never be released and would latch every class). |
ReleaseRatio | 0.7 | An active event ends below Threshold * ReleaseRatio (hysteresis), clamped to 0.01–1 for the same reason. |
SmoothingWindows | 3 | Windows the scores are averaged over (1 disables smoothing). |
WindowSeconds | 0.975 | Inference window length — one 0.96 s YAMNet patch plus a small margin; clamped 0.96–5 s. |
ClassFilter | null | Optional allowlist of AudioSet class names (e.g. "Siren", "Dog"). |
Provider | Auto | ONNX execution provider (CPU is usually enough). |
EnableScoresEvent | false | Raise OnScores (top-K) for every window, for a live meter. |
TopK | 5 | Number of classes reported in the scores event. |
Filtering to specific sounds¶
Pass a ClassFilter allowlist to report only the sounds you care about — for example an alarm watchdog that ignores everything else:
var settings = new AudioEventDetectorSettings(yamnetModelPath)
{
ClassFilter = new[] { "Siren", "Emergency vehicle", "Police car (siren)", "Alarm", "Glass", "Gunshot, gunfire" },
};
Class names are the AudioSet display names and must match one exactly (case is ignored, but a typo, a singular/plural difference or an abbreviation is not). Unknown names are ignored with a warning as long as at least one name resolves. A non-empty filter in which no name resolves fails the block's build — StartAsync returns false and an error naming the unresolved entries is reported — rather than silently falling back to detecting all 521 classes.
Live scores meter¶
Enable OnScores to receive the top-K class scores for every processed window (useful for a live level meter or a debugging view):
var settings = new AudioEventDetectorSettings(yamnetModelPath)
{
EnableScoresEvent = true, // read once, when the pipeline starts — set it before StartAsync
TopK = 5,
};
var detector = new AudioEventDetectorBlock(settings);
detector.OnScores += (sender, e) =>
{
foreach (var s in e.Scores)
{
Console.WriteLine($" {s.Label}: {s.Confidence:F2}");
}
};
Model¶
The block uses YAMNet, Google's MobileNet-v1 audio classifier trained on AudioSet, predicting 521 classes from the AudioSet ontology. The model consumes a 16 kHz mono waveform (the block handles resampling and down-mixing) and returns per-frame class scores. The weights are Apache-2.0. Your application supplies the model file and points ModelPath at it: the SDK NuGet packages ship neither the weights nor a downloader, and the block never fetches anything itself. The demo below has a Download button that fetches the model once and caches it; nothing is downloaded unless you click it.
For production, convert the official Google TF-Hub model to ONNX yourself rather than relying on a third-party ONNX export. The conversion is a format change only and does not alter the Apache-2.0 weights license:
pip install tensorflow tensorflow-hub tf2onnx onnx
python -c "import tensorflow_hub as hub, tensorflow as tf; m=hub.load('https://tfhub.dev/google/yamnet/1'); tf.saved_model.save(m, 'yamnet_tf')"
python -m tf2onnx.convert --saved-model yamnet_tf --output yamnet.onnx --opset 13
If you use a community ONNX export instead, pin it to an immutable revision and verify its SHA-256 — the WPF Audio Event Detection Demo downloads a pinned export of the Apache-2.0 weights and verifies the hash after download.
Sound event recognition uses the AudioSet ontology and the YAMNet model by Google, released under the Apache-2.0 license.
Demos¶
The Audio Event Detection Demo (WPF Media Blocks — model download with SHA-256 verification, class filter, live event log) is in the SDK's demo set and will be linked here once published to the public samples repository.
Notes¶
OnAudioEventandOnScoresare raised on the inference worker thread; marshal to the UI thread before updating UI. A throwing handler is caught and logged, never propagated onto the worker.- At end-of-stream the trailing window and any still-active events are flushed before teardown.
- Under normal operation the block never drops audio — neither from the pass-through path nor from the analysis path. The internal analysis queue is unbounded: a source that outruns the worker grows the backlog instead of discarding samples, and the audio still passes downstream untouched.
- Two designed exits end the analysis rather than lose audio silently, and neither ever touches the pass-through path: if the backlog no longer fits in memory, the block reports an error once and stops analyzing that stream (use a GPU execution provider or faster hardware); and an explicit stop abandons the backlog the worker has not reached yet — only a natural end-of-stream drains it in full.