From 915510c938d1477f8ce670977c15fd39b8ebb13f Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 13 Jun 2026 00:21:09 -0400 Subject: [PATCH] fix(engine): correct PCMs16 audio FourCC constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PCMs16 FourCC was 0x73334d50, which is the little-endian packing of the bytes 'P','M','3','s' — not the intended 'P','C','M','s'. NDI packs FourCCs little-endian (cf. BGRA = 0x41524742 = bytes 'B','G','R','A'), so 'P','C','M','s' is 0x734D4350. Effect of the bug: any legacy Teams/NDI sender delivering 16-bit PCM audio fell through AudioPeakComputer.ComputePeak's switch to the unknown branch and reported 0.0, so the operator VU meter read silent for those sources even when audio was present. FLTP (the common NDI 6 format) was unaffected. The existing tests passed because they referenced the symbol rather than the literal, so the wrong value was self-consistent in-test. --- .../Pipeline/AudioPeakComputer.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Dragon-ISO.Engine/Pipeline/AudioPeakComputer.cs b/src/Dragon-ISO.Engine/Pipeline/AudioPeakComputer.cs index c011ff8..fc7b599 100644 --- a/src/Dragon-ISO.Engine/Pipeline/AudioPeakComputer.cs +++ b/src/Dragon-ISO.Engine/Pipeline/AudioPeakComputer.cs @@ -5,29 +5,34 @@ namespace DragonISO.Engine.Pipeline; /// /// Computes a single peak amplitude (in [0.0, 1.0]) from one NDI audio frame. /// -/// NDI 6's preferred audio format is NDIlib_FourCC_audio_type_FLTP — +/// NDI 6's preferred audio format is NDIlib_FourCC_audio_type_FLTP — /// 32-bit IEEE float, planar (one contiguous chunk per channel). Values are /// nominally normalized to [-1, 1]; brief excursions past 1 during transient /// clipping are clamped here. We compute a max-absolute peak across every /// sample of every channel rather than RMS so the UI VU bar reads -/// "loudest part of the buffer" — the same convention OBS / Resolve / Studio +/// "loudest part of the buffer" — the same convention OBS / Resolve / Studio /// Monitor use for their meters. /// /// Pulled out of so the math is unit-testable /// without an NDI runtime; the heavy work (FLTP decode) runs entirely on /// managed memory the caller has already copied across the P/Invoke /// boundary, so tests exercise the same code path that production does. +/// +/// FourCC values are packed little-endian, matching the NDI SDK and +/// 's video FourCCs: the first ASCII +/// character occupies the least-significant byte. So 'P','C','M','s' +/// is 0x73('s')4D('M')43('C')50('P') = 0x734D4350. /// public static class AudioPeakComputer { - /// FourCC for FLTP — 32-bit float, planar layout. 'F','L','T','p'. + /// FourCC for FLTP — 32-bit float, planar layout. 'F','L','T','p'. public const uint FourCC_FLTP = 0x70544c46; - /// FourCC for FLT — 32-bit float, interleaved. 'F','L','T',' '. Rarely seen but cheap to handle. + /// FourCC for FLT — 32-bit float, interleaved. 'F','L','T',' '. Rarely seen but cheap to handle. public const uint FourCC_FLT = 0x20544c46; /// FourCC for PCM 16-bit signed integer, interleaved. Some legacy senders use this. 'P','C','M','s'. - public const uint FourCC_PCMs16 = 0x73334d50; + public const uint FourCC_PCMs16 = 0x734D4350; /// /// Returns the largest absolute sample value found in the buffer, @@ -38,7 +43,7 @@ public static class AudioPeakComputer /// The NDI audio FourCC (see the constants on this class). /// /// Total sample count across all channels (e.g. no_samples * no_channels - /// for FLTP — channels are concatenated planes, but every sample contributes). + /// for FLTP — channels are concatenated planes, but every sample contributes). /// public static double ComputePeak(ReadOnlySpan data, uint fourCC, int totalSamples) { @@ -48,7 +53,7 @@ public static class AudioPeakComputer { FourCC_FLTP or FourCC_FLT => ComputePeakFloat32(data, totalSamples), FourCC_PCMs16 => ComputePeakInt16(data, totalSamples), - _ => 0.0, // unknown format — surface silence rather than throw + _ => 0.0, // unknown format — surface silence rather than throw }; } @@ -56,7 +61,7 @@ public static class AudioPeakComputer { // 4 bytes per sample. Cap by what's actually in the buffer in case // the caller's totalSamples disagrees with the byte length (defensive - // — a misreporting source shouldn't take down the receiver loop). + // — a misreporting source shouldn't take down the receiver loop). var available = Math.Min(totalSamples, data.Length / 4); if (available <= 0) return 0.0;