From 426de7d6ea172203560941a7887aad71b9ea4243 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 10 Nov 2026 16:22:47 +0200 Subject: [PATCH 1/8] topology2: production: drop obsolete sof-dmic-4ch-wov target Remove the standalone DMIC WoV target. Its KPB secondary output declares 16 kHz while the connected host copier only declares 48 kHz input formats, which F003 reports as incompatible. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/production/tplg-targets-ace3.cmake | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/topology/topology2/production/tplg-targets-ace3.cmake b/tools/topology/topology2/production/tplg-targets-ace3.cmake index 1be9cfe603fa..e4f2b22dd794 100644 --- a/tools/topology/topology2/production/tplg-targets-ace3.cmake +++ b/tools/topology/topology2/production/tplg-targets-ace3.cmake @@ -108,10 +108,6 @@ SDW_JACK_OUT_STREAM=Playback-SimpleJack,SDW_JACK_IN_STREAM=Capture-SimpleJack,\ PREPROCESS_PLUGINS=nhlt,NHLT_BIN=nhlt-sof-ptl-rt722-4ch.bin,DMIC0_ENHANCED_CAPTURE=true,\ EFX_DMIC0_TDFB_PARAMS=line4_pass,EFX_DMIC0_DRC_PARAMS=dmic_default" -# DMIC WoV feature topology; platform-agnostic, shared across PTL/WCL -"dmic-wov-feature\;sof-dmic-4ch-wov\;PLATFORM=none,NUM_DMICS=4,\ -DMIC0_ID=5,DMIC1_ID=6,DMIC1_ENABLE=false,DMIC1_RATE=16000,INCLUDE_WOV=true" - "cavs-sdw\;sof-ptl-rt722-2ch\;PLATFORM=ptl,SDW_DMIC=1,NUM_SDW_AMP_LINKS=1,NUM_DMICS=2,\ PDM1_MIC_A_ENABLE=0,PDM1_MIC_B_ENABLE=0,DMIC0_ID=5,DMIC1_ID=6,HDMI1_ID=7,HDMI2_ID=8,HDMI3_ID=9,\ SDW_AMP_FEEDBACK=false,SDW_SPK_STREAM=Playback-SmartAmp,SDW_DMIC_STREAM=Capture-SmartMic,\ From 4d62129bc01530e4601a7178d7ac90a6c8950e53 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 10 Nov 2026 16:25:29 +0200 Subject: [PATCH 2/8] topology2: intel: sdw-amp-xover: fix internal crossover sample types Include the sample-type bits in the selector input and playback copier input formats. These internal connections carry 32-bit LSB integer samples, matching the DRC and crossover FIR outputs. Leave the external SoundWire output format unchanged. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/platform/intel/sdw-amp-xover.conf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/topology/topology2/platform/intel/sdw-amp-xover.conf b/tools/topology/topology2/platform/intel/sdw-amp-xover.conf index 3347d4911bc4..3e2b75d7d2f4 100644 --- a/tools/topology/topology2/platform/intel/sdw-amp-xover.conf +++ b/tools/topology/topology2/platform/intel/sdw-amp-xover.conf @@ -72,7 +72,8 @@ Object.Pipeline { } in_bit_depth 32 in_valid_bit_depth 32 - in_fmt_cfg "$[($in_channels | ($in_valid_bit_depth * 256))]" + in_sample_type $SAMPLE_TYPE_LSB_INTEGER + in_fmt_cfg "$[($in_channels | ($in_valid_bit_depth * 256)) | ($in_sample_type * 65536)]" } ] num_output_audio_formats 1 @@ -127,6 +128,7 @@ Object.Pipeline { { in_bit_depth 32 in_valid_bit_depth 32 + in_fmt_cfg "$[($in_channels | ($in_valid_bit_depth * 256)) | ($in_sample_type * 65536)]" } ] num_output_audio_formats 1 @@ -442,7 +444,6 @@ IncludeByKey.NUM_SDW_AMP_LINKS { } in_bit_depth 32 in_valid_bit_depth 32 - in_fmt_cfg "$[($in_channels | ($in_valid_bit_depth * 256))]" } ] Object.Base.output_audio_format [ From f567add554e773732eae598fdf9d302f0058371f Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 10 Nov 2026 17:52:34 +0200 Subject: [PATCH 3/8] topology2: cavs-benchmark-hda: avoid duplicate generic benchmark DAI gateways The generic benchmark provides its own playback and capture DAI pipelines, but also instantiates the default component-benchmark DAI gateways. The overlapping pipeline IDs and duplicate widget names cause the wrong format declarations and pipeline ownership to be used. Instantiate the default gateways only with HDA_CONFIG=none. Keep the custom gateways for HDA_CONFIG=benchmark and document the distinction. Verify analog playback and capture at 48 kHz in S16_LE, S24_LE and S32_LE on a TGL HDA device with kernel 7.2.14. Both directions now run instead of playback timing out and capture failing at pipeline start. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/README.md | 7 + .../topology2/cavs-benchmark-hda.conf | 162 +++++++++--------- 2 files changed, 90 insertions(+), 79 deletions(-) diff --git a/tools/topology/topology2/README.md b/tools/topology/topology2/README.md index d120de60de98..4ef116a96a07 100644 --- a/tools/topology/topology2/README.md +++ b/tools/topology/topology2/README.md @@ -14,6 +14,13 @@ Topology v2 introduces an object-oriented pre-processing layer directly into the Building topologies requires `alsatplg` version 1.2.7 or later. The version check is enforced in `CMakeLists.txt` at configure time. +### HDA benchmarks + +The generic HDA benchmark (`BENCH_CONFIG=benchmark,HDA_CONFIG=benchmark`) +provides its own DAI pipelines. The default component-benchmark DAI gateways +are only instantiated with `HDA_CONFIG=none` to avoid duplicate widget names +and overlapping pipeline IDs. + ## Key Advantages - **Object-Oriented Syntax**: Topology v2 allows for the definition of classes (`Class.Widget`, `Class.Pipeline`) and object instantiation, making the topology files much easier to read, maintain, and extend. diff --git a/tools/topology/topology2/cavs-benchmark-hda.conf b/tools/topology/topology2/cavs-benchmark-hda.conf index fda41002de8f..767fb3ba39fa 100644 --- a/tools/topology/topology2/cavs-benchmark-hda.conf +++ b/tools/topology/topology2/cavs-benchmark-hda.conf @@ -213,102 +213,106 @@ Object.PCM.pcm [ } ] -Object.Pipeline { - io-gateway [ - { - index $BENCH_PLAYBACK_DAI_PIPELINE - direction playback - Object.Widget.dai-copier.1 { - node_type $HDA_LINK_OUTPUT_CLASS - stream_name $HDA_ANALOG_DAI_NAME - dai_type "HDA" - copier_type "HDA" - num_input_pins 1 - num_input_audio_formats 1 - num_output_audio_formats 1 - IncludeByKey.BENCH_MODULE_FORMAT { - "s16" { - Object.Base.input_audio_format [ - { - in_bit_depth 16 - in_valid_bit_depth 16 +IncludeByKey.HDA_CONFIG { + "none" { + Object.Pipeline { + io-gateway [ + { + index $BENCH_PLAYBACK_DAI_PIPELINE + direction playback + Object.Widget.dai-copier.1 { + node_type $HDA_LINK_OUTPUT_CLASS + stream_name $HDA_ANALOG_DAI_NAME + dai_type "HDA" + copier_type "HDA" + num_input_pins 1 + num_input_audio_formats 1 + num_output_audio_formats 1 + IncludeByKey.BENCH_MODULE_FORMAT { + "s16" { + Object.Base.input_audio_format [ + { + in_bit_depth 16 + in_valid_bit_depth 16 + } + ] } - ] - } - "s24" { - Object.Base.input_audio_format [ + "s24" { + Object.Base.input_audio_format [ + { + in_bit_depth 32 + in_valid_bit_depth 24 + } + ] + } + "s32" { + Object.Base.input_audio_format [ + { + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + } + } + Object.Base.output_audio_format [ { - in_bit_depth 32 - in_valid_bit_depth 24 + out_bit_depth 32 + out_valid_bit_depth 32 } ] } - "s32" { + } + ] + io-gateway-capture [ + { + index $BENCH_CAPTURE_DAI_PIPELINE + direction capture + Object.Widget.dai-copier.1 { + dai_type "HDA" + type "dai_out" + copier_type "HDA" + stream_name $HDA_ANALOG_DAI_NAME + node_type $HDA_LINK_INPUT_CLASS + num_output_pins 1 + num_input_audio_formats 1 + num_output_audio_formats 1 Object.Base.input_audio_format [ { in_bit_depth 32 in_valid_bit_depth 32 } ] - } - } - Object.Base.output_audio_format [ - { - out_bit_depth 32 - out_valid_bit_depth 32 - } - ] - } - } - ] - io-gateway-capture [ - { - index $BENCH_CAPTURE_DAI_PIPELINE - direction capture - Object.Widget.dai-copier.1 { - dai_type "HDA" - type "dai_out" - copier_type "HDA" - stream_name $HDA_ANALOG_DAI_NAME - node_type $HDA_LINK_INPUT_CLASS - num_output_pins 1 - num_input_audio_formats 1 - num_output_audio_formats 1 - Object.Base.input_audio_format [ - { - in_bit_depth 32 - in_valid_bit_depth 32 - } - ] - IncludeByKey.BENCH_MODULE_FORMAT { - "s16" { - Object.Base.output_audio_format [ - { - out_bit_depth 16 - out_valid_bit_depth 16 + IncludeByKey.BENCH_MODULE_FORMAT { + "s16" { + Object.Base.output_audio_format [ + { + out_bit_depth 16 + out_valid_bit_depth 16 + } + ] } - ] - } - "s24" { - Object.Base.output_audio_format [ - { - out_bit_depth 32 - out_valid_bit_depth 24 + "s24" { + Object.Base.output_audio_format [ + { + out_bit_depth 32 + out_valid_bit_depth 24 + } + ] } - ] - } - "s32" { - Object.Base.output_audio_format [ - { - out_bit_depth 32 - out_valid_bit_depth 32 + "s32" { + Object.Base.output_audio_format [ + { + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] } - ] + } } } - } + ] } - ] + } } IncludeByKey.BENCH_CONFIG { From 129af4670c6c747f56f9e8d6d73a3e0f3ec207cd Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 10 Nov 2026 17:53:01 +0200 Subject: [PATCH 4/8] topology2: cavs-benchmark-hda: use 16 kHz DAI formats for MFCC benchmarks MFCC and MFCC-mel benchmarks configure the host copiers and processing modules for 16 kHz, but their HDA DAI copiers inherit 48 kHz formats. F003 reports incompatible formats on both connections to the DAIs. Use the configured HDA playback and capture rates for both sides of the DAI copiers, and select 16 kHz for all six MFCC benchmark variants. Keep 48 kHz defaults for other component benchmarks. The copiers convert sample formats, not sample rates, so the HDA link must also use 16 kHz. Document the benchmark rate and feature-data output. Verify three-second playback and capture streams at 16 kHz for each variant in its native S16_LE, S24_LE or S32_LE format on a TGL HDA device with kernel 7.2.14. This checks stream operation, not MFCC algorithm correctness. Representative unaffected benchmark binaries remain byte-identical. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/README.md | 5 +++++ .../topology/topology2/cavs-benchmark-hda.conf | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tools/topology/topology2/README.md b/tools/topology/topology2/README.md index 4ef116a96a07..532800c56304 100644 --- a/tools/topology/topology2/README.md +++ b/tools/topology/topology2/README.md @@ -21,6 +21,11 @@ provides its own DAI pipelines. The default component-benchmark DAI gateways are only instantiated with `HDA_CONFIG=none` to avoid duplicate widget names and overlapping pipeline IDs. +The MFCC and MFCC-mel HDA benchmarks use 16 kHz throughout, including the HDA +link. Their copiers convert sample formats, not sample rates. Use 16 kHz when +running these benchmarks; MFCC output carries feature data, not listenable +audio. + ## Key Advantages - **Object-Oriented Syntax**: Topology v2 allows for the definition of classes (`Class.Widget`, `Class.Pipeline`) and object instantiation, making the topology files much easier to read, maintain, and extend. diff --git a/tools/topology/topology2/cavs-benchmark-hda.conf b/tools/topology/topology2/cavs-benchmark-hda.conf index 767fb3ba39fa..84c3c298e71e 100644 --- a/tools/topology/topology2/cavs-benchmark-hda.conf +++ b/tools/topology/topology2/cavs-benchmark-hda.conf @@ -80,6 +80,15 @@ IncludeByKey.PLATFORM { "ptl" "platform/intel/ptl.conf" } +IncludeByKey.BENCH_CONFIG { + "^mfcc(mel)?(16|24|32)$" { + Define { + HDA_ANALOG_CAPTURE_RATE 16000 + HDA_ANALOG_PLAYBACK_RATE 16000 + } + } +} + Object.Widget.virtual [ { name 'codec0_in' @@ -232,6 +241,7 @@ IncludeByKey.HDA_CONFIG { "s16" { Object.Base.input_audio_format [ { + in_rate $HDA_ANALOG_PLAYBACK_RATE in_bit_depth 16 in_valid_bit_depth 16 } @@ -240,6 +250,7 @@ IncludeByKey.HDA_CONFIG { "s24" { Object.Base.input_audio_format [ { + in_rate $HDA_ANALOG_PLAYBACK_RATE in_bit_depth 32 in_valid_bit_depth 24 } @@ -248,6 +259,7 @@ IncludeByKey.HDA_CONFIG { "s32" { Object.Base.input_audio_format [ { + in_rate $HDA_ANALOG_PLAYBACK_RATE in_bit_depth 32 in_valid_bit_depth 32 } @@ -256,6 +268,7 @@ IncludeByKey.HDA_CONFIG { } Object.Base.output_audio_format [ { + out_rate $HDA_ANALOG_PLAYBACK_RATE out_bit_depth 32 out_valid_bit_depth 32 } @@ -278,6 +291,7 @@ IncludeByKey.HDA_CONFIG { num_output_audio_formats 1 Object.Base.input_audio_format [ { + in_rate $HDA_ANALOG_CAPTURE_RATE in_bit_depth 32 in_valid_bit_depth 32 } @@ -286,6 +300,7 @@ IncludeByKey.HDA_CONFIG { "s16" { Object.Base.output_audio_format [ { + out_rate $HDA_ANALOG_CAPTURE_RATE out_bit_depth 16 out_valid_bit_depth 16 } @@ -294,6 +309,7 @@ IncludeByKey.HDA_CONFIG { "s24" { Object.Base.output_audio_format [ { + out_rate $HDA_ANALOG_CAPTURE_RATE out_bit_depth 32 out_valid_bit_depth 24 } @@ -302,6 +318,7 @@ IncludeByKey.HDA_CONFIG { "s32" { Object.Base.output_audio_format [ { + out_rate $HDA_ANALOG_CAPTURE_RATE out_bit_depth 32 out_valid_bit_depth 32 } From e95ac484917a6be0c417dcd71e313fa4900728b6 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Wed, 11 Nov 2026 08:01:23 +0200 Subject: [PATCH 5/8] topology2: sdw: use LSB formats on internal copier connections The standalone SRC-gain-mixin topology declares MSB integer samples on internal connections whose peers declare LSB integer samples. F003 reports mismatches at both the playback and capture copiers. Use LSB integer samples for the shared playback copier input and the standalone capture copier output. Remove their explicit fmt_cfg overrides so the base format classes encode the selected sample type. Keep the SoundWire-facing formats as MSB integer. These connections carry 32 valid bits in 32-bit containers, so sample placement is unchanged. This corrects the format declarations rather than an observed runtime audio failure. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/cavs-sdw-src-gain-mixin.conf | 3 +-- .../pipelines/cavs/mixout-gain-alh-dai-copier-playback.conf | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/topology/topology2/cavs-sdw-src-gain-mixin.conf b/tools/topology/topology2/cavs-sdw-src-gain-mixin.conf index 57f2fcc2a489..316ca5469a36 100644 --- a/tools/topology/topology2/cavs-sdw-src-gain-mixin.conf +++ b/tools/topology/topology2/cavs-sdw-src-gain-mixin.conf @@ -150,8 +150,7 @@ Object.Widget { { out_bit_depth 32 out_valid_bit_depth 32 - out_sample_type $SAMPLE_TYPE_MSB_INTEGER - out_fmt_cfg "$[($out_channels | ($out_valid_bit_depth * 256))]" + out_sample_type $SAMPLE_TYPE_LSB_INTEGER } ] } diff --git a/tools/topology/topology2/include/pipelines/cavs/mixout-gain-alh-dai-copier-playback.conf b/tools/topology/topology2/include/pipelines/cavs/mixout-gain-alh-dai-copier-playback.conf index d98997136537..719e1a40d9df 100644 --- a/tools/topology/topology2/include/pipelines/cavs/mixout-gain-alh-dai-copier-playback.conf +++ b/tools/topology/topology2/include/pipelines/cavs/mixout-gain-alh-dai-copier-playback.conf @@ -52,8 +52,7 @@ Class.Pipeline."mixout-gain-alh-dai-copier-playback" { { in_bit_depth 32 in_valid_bit_depth 32 - in_sample_type $SAMPLE_TYPE_MSB_INTEGER - in_fmt_cfg "$[($in_channels | ($in_valid_bit_depth * 256))]" + in_sample_type $SAMPLE_TYPE_LSB_INTEGER } ] Object.Base.output_audio_format [ From f6b4681437ebc2967c574141d45c4b43428e085d Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Wed, 11 Nov 2026 08:12:04 +0200 Subject: [PATCH 6/8] topology2: cavs-mixin-mixout-hda: configure analog paths for the selected rate The HDA analog PCM advertises only 48 kHz in its rate list even when its minimum and maximum rates are set to 96 kHz. The resulting empty rate constraints prevent the stream from opening. The playback gain, mixers and effects also retain 48 kHz defaults when 96 kHz is selected. Move the pre-mixer gain formats out of the mixer control and configure the internal playback chain at the selected rate. Allow the PCM rate list to include the configured rate while retaining the fixed minimum and maximum constraints. Keep the capture high-pass cutoff at 40 Hz for 96 kHz by using the 20 Hz / 48 kHz coefficients, which have the same normalized cutoff. All nine 96 kHz development targets pass F003 with the validator in this series. Playback and capture were tested on a TGL HDA device with S16_LE, S24_LE and S32_LE at 96 kHz and with the production topology at 48 kHz. The updated high-pass coefficients have not been tested on hardware. Signed-off-by: Peter Ujfalusi --- tools/topology/topology2/README.md | 4 + .../topology2/cavs-mixin-mixout-hda.conf | 122 +++++++++++++++--- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/tools/topology/topology2/README.md b/tools/topology/topology2/README.md index 532800c56304..e8863cb151b5 100644 --- a/tools/topology/topology2/README.md +++ b/tools/topology/topology2/README.md @@ -26,6 +26,10 @@ link. Their copiers convert sample formats, not sample rates. Use 16 kHz when running these benchmarks; MFCC output carries feature data, not listenable audio. +The HDA mix topology uses a 40 Hz capture high-pass at both 48 kHz and +96 kHz. At 96 kHz it reuses the 20 Hz / 48 kHz coefficients, which have +the same normalized cutoff. + ## Key Advantages - **Object-Oriented Syntax**: Topology v2 allows for the definition of classes (`Class.Widget`, `Class.Pipeline`) and object instantiation, making the topology files much easier to read, maintain, and extend. diff --git a/tools/topology/topology2/cavs-mixin-mixout-hda.conf b/tools/topology/topology2/cavs-mixin-mixout-hda.conf index e5ac78e505d1..1c7aa23ab413 100644 --- a/tools/topology/topology2/cavs-mixin-mixout-hda.conf +++ b/tools/topology/topology2/cavs-mixin-mixout-hda.conf @@ -17,6 +17,7 @@ Define { COMPR_FFMPEG_CODEC 'flac' HDA_ANALOG_CAPTURE_RATE 48000 HDA_ANALOG_PLAYBACK_RATE 48000 + HDA_CAPTURE_HIGHPASS '48k' EFX_FIR_PARAMS 'passthrough' EFX_IIR_PARAMS 'passthrough' EFX_DRC_PARAMS 'passthrough' @@ -34,6 +35,16 @@ Define { COMPR_ENC_SOURCE 'mixin.4.1' } +# The 20 Hz / 48 kHz coefficients have the same normalized cutoff as +# 40 Hz / 96 kHz. +IncludeByKey.HDA_ANALOG_CAPTURE_RATE { + "96000" { + Define { + HDA_CAPTURE_HIGHPASS '96k' + } + } +} + # include deep buffer config if buffer size is in 1 - 1000 ms. IncludeByKey.DEEPBUFFER_FW_DMA_MS{ "[1-1000]" "platform/intel/deep-buffer.conf" @@ -114,17 +125,75 @@ Object.Pipeline { } ] } + Object.Widget.mixout.1 { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } Object.Widget.eqiir.1 { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] Object.Control.bytes."1" { name 'Post Mixer $ANALOG_PLAYBACK_PCM IIR Eq bytes' } } Object.Widget.eqfir.1 { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] Object.Control.bytes."1" { name 'Post Mixer $ANALOG_PLAYBACK_PCM FIR Eq bytes' } } Object.Widget.drc.1 { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] Object.Control { bytes."1" { name 'Post Mixer $ANALOG_PLAYBACK_PCM DRC bytes' @@ -175,23 +244,37 @@ Object.Pipeline { Object.Widget.gain.1 { Object.Control.mixer.1 { name 'Pre Mixer $ANALOG_PLAYBACK_PCM Volume' - num_input_audio_formats 1 - num_output_audio_formats 1 - Object.Base.input_audio_format [ - { - in_rate $HDA_ANALOG_PLAYBACK_RATE - in_bit_depth 32 - in_valid_bit_depth 32 - } - ] - Object.Base.output_audio_format [ - { - out_rate $HDA_ANALOG_PLAYBACK_RATE - out_bit_depth 32 - out_valid_bit_depth 32 - } - ] } + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] + } + Object.Widget.mixin.1 { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 32 + } + ] + Object.Base.output_audio_format [ + { + out_rate $HDA_ANALOG_PLAYBACK_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] } } ] @@ -273,7 +356,10 @@ IncludeByKey.COMPR_FFMPEG_ENC { Object.Widget.eqiir.1 { Object.Control.bytes."1" { name '$ANALOG_CAPTURE_PCM IIR Eq' - + IncludeByKey.HDA_CAPTURE_HIGHPASS { + "48k" "include/components/eqiir/highpass_40hz_0db_48khz.conf" + "96k" "include/components/eqiir/highpass_20hz_0db_48khz.conf" + } } num_input_audio_formats 1 num_output_audio_formats 1 @@ -450,6 +536,7 @@ Object.PCM.pcm [ direction "playback" name $ANALOG_PLAYBACK_PCM formats 'S32_LE,S24_LE,S16_LE' + rates "8000,11025,16000,22050,24000,32000,44100,48000,64000,88200,96000,176400,192000" rate_min $HDA_ANALOG_PLAYBACK_RATE rate_max $HDA_ANALOG_PLAYBACK_RATE } @@ -457,6 +544,7 @@ Object.PCM.pcm [ direction "capture" name $ANALOG_CAPTURE_PCM formats 'S32_LE,S24_LE,S16_LE' + rates "8000,11025,16000,22050,24000,32000,44100,48000,64000,88200,96000,176400,192000" rate_min $HDA_ANALOG_CAPTURE_RATE rate_max $HDA_ANALOG_CAPTURE_RATE } From f65f858411be1bb74e35545e2b84e962d60b2eb0 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Wed, 11 Nov 2026 08:19:29 +0200 Subject: [PATCH 7/8] topology2: webrtc: align capture pipeline formats The nocodec WebRTC test topology declares a 16-bit capture DAI output feeding a copier configured for 32-bit input. Its VAD also declares 48 kHz formats while the following noise suppressor uses 16 kHz. Configure the DAI output as 32-bit, matching the first module copier. Make the VAD format rate selectable, defaulting to 48 kHz, and set it to 16 kHz in this test topology. This preserves the existing sample format and channel choices while aligning the connected formats. The rebuilt topology passes F003. Hardware testing requires an SSP nocodec loopback setup and has not been performed. Signed-off-by: Peter Ujfalusi --- .../development/cavs-nocodec-webrtc-ns.conf | 5 +++-- .../topology2/include/components/webrtc-vad.conf | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tools/topology/topology2/development/cavs-nocodec-webrtc-ns.conf b/tools/topology/topology2/development/cavs-nocodec-webrtc-ns.conf index 59c17845d940..017a9b7a2c30 100644 --- a/tools/topology/topology2/development/cavs-nocodec-webrtc-ns.conf +++ b/tools/topology/topology2/development/cavs-nocodec-webrtc-ns.conf @@ -58,6 +58,7 @@ Define { PLATFORM "none" SSP0_PCM_ID 0 SSP0_PCM_NAME "Port0" + WEBRTC_VAD_RATE 16000 } IncludeByKey.PLATFORM { @@ -216,8 +217,8 @@ Object.Pipeline.io-gateway-capture [ Object.Base.output_audio_format [ { out_rate 16000 - out_bit_depth 16 - out_valid_bit_depth 16 + out_bit_depth 32 + out_valid_bit_depth 32 } ] } diff --git a/tools/topology/topology2/include/components/webrtc-vad.conf b/tools/topology/topology2/include/components/webrtc-vad.conf index 03c7adbcd8f7..908a19dc3062 100644 --- a/tools/topology/topology2/include/components/webrtc-vad.conf +++ b/tools/topology/topology2/include/components/webrtc-vad.conf @@ -10,6 +10,7 @@ # index 1 # no_pm "true" # } +# Set WEBRTC_VAD_RATE to the pipeline rate; it defaults to 48000 Hz. # # UUID: c790b11d-5d14-e54e-be36ba4ad732cc14 (webrtc_vad in uuid-registry.txt) # Topology byte order (little-endian fields): @@ -18,6 +19,10 @@ # e54e → 4e:e5 # be36ba4ad732cc14 (last 8 bytes, as-is) +Define { + WEBRTC_VAD_RATE 48000 +} + Class.Widget."webrtc-vad" { # # Pipeline ID for the webrtc-vad widget @@ -60,23 +65,26 @@ Class.Widget."webrtc-vad" { unique "instance" } - # Supports S16 and S32 at any rate (8/16/32/48 kHz), mono or stereo. + # Supports S16 and S32 at the selected rate (8/16/32/48 kHz), mono or stereo. num_input_audio_formats 4 num_output_audio_formats 4 Object.Base.input_audio_format [ # S16 stereo { + in_rate $WEBRTC_VAD_RATE in_bit_depth 16 in_valid_bit_depth 16 } # S32 stereo { + in_rate $WEBRTC_VAD_RATE in_bit_depth 32 in_valid_bit_depth 32 } # S16 mono { + in_rate $WEBRTC_VAD_RATE in_bit_depth 16 in_valid_bit_depth 16 in_channels 1 @@ -85,6 +93,7 @@ Class.Widget."webrtc-vad" { } # S32 mono { + in_rate $WEBRTC_VAD_RATE in_bit_depth 32 in_valid_bit_depth 32 in_channels 1 @@ -95,14 +104,17 @@ Class.Widget."webrtc-vad" { Object.Base.output_audio_format [ { + out_rate $WEBRTC_VAD_RATE out_bit_depth 16 out_valid_bit_depth 16 } { + out_rate $WEBRTC_VAD_RATE out_bit_depth 32 out_valid_bit_depth 32 } { + out_rate $WEBRTC_VAD_RATE out_bit_depth 16 out_valid_bit_depth 16 out_channels 1 @@ -110,6 +122,7 @@ Class.Widget."webrtc-vad" { out_ch_map $CHANNEL_MAP_MONO } { + out_rate $WEBRTC_VAD_RATE out_bit_depth 32 out_valid_bit_depth 32 out_channels 1 From 01872cb8370db219e3f4648f8ff0b84ef06e4698 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Wed, 11 Nov 2026 13:26:28 +0200 Subject: [PATCH 8/8] topology: validator: check IPC4 pin format compatibility Resolve route endpoints to their bound input and output pins and report an error when declared format sets have no compatible format. Do not infer queue allocation for unbound multi-pin endpoints or constrain undeclared secondary copier outputs. Compare mixer outputs using the shared format set selected by the kernel for all pins. Check sample representations independently of compatibility normalization and reject invalid sample types and bit depths. Treat full-width 32-bit MSB and LSB integers as equivalent, and include container depth when evaluating declared sample types. These checks validate declared format compatibility, not the behavior of a particular kernel version or runtime sample alignment. Matching declarations do not simulate runtime selection, and module-specific configuration can require further review. Cover pin bindings, sample types, base topologies, and siggen echo reference with regression tests. Signed-off-by: Peter Ujfalusi --- tools/topology/validator/README.md | 40 ++- tools/topology/validator/checks/ipc4.py | 75 +++++- tools/topology/validator/checks/paths.py | 23 +- tools/topology/validator/checks/tokens.py | 31 +++ .../validator/tests/test_integration.py | 13 + tools/topology/validator/tests/test_rules.py | 233 +++++++++++++++++- 6 files changed, 398 insertions(+), 17 deletions(-) diff --git a/tools/topology/validator/README.md b/tools/topology/validator/README.md index 77fe4b218e01..cdbdd70e5b2d 100644 --- a/tools/topology/validator/README.md +++ b/tools/topology/validator/README.md @@ -58,16 +58,46 @@ Rules are grouped in layers by how closely they follow kernel code: | L3 | `topology.c`, `ipc4-topology.c` setup | routes, pipelines, chain DMA, DAI types, pins, bytes controls, PCM binding | | L4 | `sof-audio.c` path walk, format selection | PCM formats vs. host copier formats (advisory) | -Every finding names the rule and the kernel function it follows, e.g. +`F003` checks each firmware route's actual output/input pin pair. Single-pin +endpoints use pin 0; multi-pin endpoints require explicit bindings. Disjoint +declared format sets produce an error naming both pins and their formats, and +fail validation without `--werror`. +Comparison requires matching rate, channel count and valid-bit depth, with +sample types treated according to their declared representations. Signed +integer alignment variants share a format for 16/20/24 valid bits; at 32/32, +MSB and LSB integer placement is identical. Other integer layouts and +non-integer representations remain distinct. `T008` also rejects unknown +types, valid depths larger than their containers, float other than 32/32, and +A-law or mu-law other than 8/8. In SOF firmware, internal 24-bit MSB and LSB +integer declarations both use the same DSP format; copier gateway conversion +handles external alignment. The check validates declared format compatibility, +not the behavior of a particular kernel version or the sample alignment +performed at runtime. It does not validate container/layout compatibility +between pins or simulate runtime format selection: overlapping sets only +establish possible compatibility, not that every PCM configuration works. + +Unresolved pins or missing formats are informational (`-v`), not guessed. +Mixer outputs share the module's output formats across pins. Undeclared +secondary copier outputs are configured from the sink at bind time; +they are not constrained to pin 0. Process output pins without declarations +use the pin-0 fallback. Missing process extension formats remain covered by +`W008`. Chain-DMA and DAPM-only routes are skipped. An unbound siggen input +has no firmware route to check. Feature routes use widgets from `--base`. + +Every finding names the rule and the relevant kernel path it models. These +references explain how topology objects and connections are interpreted; they +do not make findings predictions about how a specific kernel version handles +an invalid or incompatible topology. For example: ``` foo.tplg: error: G001 route 'mixin.90.1' -> 'mixout.1.1': sink widget 'mixout.1.1' does not exist (kernel: snd_soc_dapm_add_routes, sof_route_load) ``` -Severities: `error` means the kernel fails to load the topology, or fails -when the affected PCM is used. `warning` means a likely mistake, or a failure -that depends on runtime state the validator cannot know. `info` lists -observations (`-v`). +Severities: `error` means the topology fails validation, usually because the +kernel fails to load it or an affected PCM fails when used. `F003` also treats +disjoint declared pin formats as errors, without simulating firmware behavior. +`warning` means a likely mistake, or a failure that depends on runtime state the +validator cannot know. `info` lists observations (`-v`). Not checked, because the data only exists on the target: firmware module UUIDs and module types from the firmware manifest, ACPI NHLT blobs, machine diff --git a/tools/topology/validator/checks/ipc4.py b/tools/topology/validator/checks/ipc4.py index a899b465eba0..c7f716812923 100644 --- a/tools/topology/validator/checks/ipc4.py +++ b/tools/topology/validator/checks/ipc4.py @@ -17,7 +17,7 @@ from tplgparse.binary import TplgType from . import ERROR, INFO, WARNING, rule -from .paths import host_copier, routes_on_pcm_paths +from .paths import fmt_key, host_copier, routes_on_pcm_paths VIRTUAL_TYPES = ("input", "output", "out_drv") # skipped by sof_route_setup() AUDIO_FMT_TYPES = ("aif_in", "aif_out", "dai_in", "dai_out", "buffer", "pga", "mixer", @@ -334,6 +334,79 @@ def process_pin_formats(ctx): f"required if the firmware module uses extended init") +def connection_pin(kw, peer, output): + "Resolve a queue without guessing the allocation order of unbound pins." + pins = kw.num_output_pins if output else kw.num_input_pins + binding = kw.output_pin_binding if output else kw.input_pin_binding + if pins == 1: + return 0 + if pins > 1 and binding is not None and binding.count(peer.name) == 1: + return binding.index(peer.name) + return None + + +def connection_formats(widget, kw, pin, output): + """Formats declared for this pin, accounting for shared module formats. + + An undeclared secondary copier output is configured from the sink at + bind time, not constrained by the copier's pin-0 output formats. + """ + _, _, ins, outs = kw.audio_formats() + fmts = outs if output else ins + rate, cfg, index, depth = (1930, 1937, TKN_OUT_PIN_INDEX, 1931) if output else \ + (1900, 1907, TKN_IN_PIN_INDEX, 1901) + if output and widget.type_name == "mixer": + # The kernel selects a single mixer output format without a pin filter. + selected = fmts + else: + selected = [f for f in fmts if f.get(index, 0) == pin] + if not selected and output and pin > 0 and widget.type_name in PROCESS_TYPES: + selected = [f for f in fmts if not f.get(index, 0)] + # Incomplete formats are diagnosed by T006; do not infer incompatibility. + if not selected or any(not f.get(rate) or not f.get(cfg) for f in selected): + return set() + def key(fmt): + stype = (fmt[cfg] >> 16) & 0xff + representation = 1 if stype in (0, 1, 2) else stype + return fmt_key(fmt, rate, cfg, depth) + (representation,) + + return {key(f) for f in selected} + + +def describe_formats(formats): + return ", ".join(f"{rate}Hz/{channels}ch/{bits}bit/kernel-type{stype}/repr{representation}" + for rate, channels, bits, stype, representation in sorted(formats)) + + +@rule("F003", "L4", ERROR, + "sof_ipc4_route_setup, sof_ipc4_set_copier_sink_format, sof_ipc4_get_input_pin_audio_fmt", + "connected output and input pins have a common declared format") +def connection_pin_formats(ctx): + for r, src, sink in kernel_routes(ctx): + ps, pk = ctx.pipeline_widget(src), ctx.pipeline_widget(sink) + if ps is None or pk is None or pipeline_info(ctx, ps)[0] or pipeline_info(ctx, pk)[0]: + continue + src_kw, sink_kw = ctx.kw(src), ctx.kw(sink) + if src_kw.error or sink_kw.error: + continue + out_pin = connection_pin(src_kw, sink, True) + in_pin = connection_pin(sink_kw, src, False) + if out_pin is None or in_pin is None: + yield r.label, ("cannot statically resolve the connected pins; explicit " + "bindings are needed for multi-pin endpoints"), INFO + continue + outs = connection_formats(src, src_kw, out_pin, True) + ins = connection_formats(sink, sink_kw, in_pin, False) + if not outs or not ins: + yield r.label, (f"output pin {out_pin} -> input pin {in_pin}: " + "no static format comparison; one or both pins have no " + "complete declared formats"), INFO + elif outs.isdisjoint(ins): + yield r.label, (f"output pin {out_pin} [{describe_formats(outs)}] has no " + f"compatible format with input pin {in_pin} " + f"[{describe_formats(ins)}]") + + # --- controls ---------------------------------------------------------------- @rule("C001", "L3", ERROR, "sof_control_load_volume, sof_control_load_enum", diff --git a/tools/topology/validator/checks/paths.py b/tools/topology/validator/checks/paths.py index 1a8ddc3f4dcb..071db6611512 100644 --- a/tools/topology/validator/checks/paths.py +++ b/tools/topology/validator/checks/paths.py @@ -117,12 +117,15 @@ def routes_on_pcm_paths(ctx) -> dict: TKN_OUT_RATE, TKN_OUT_FMT_CFG, TKN_OUT_PIN = 1930, 1937, 1939 -def fmt_key(fmt: dict, rate_tok, cfg_tok): - """(rate, channels, valid bits, type) as compared by - sof_ipc4_init_input_audio_fmt(), using sof_ipc4_fmt_cfg_to_type().""" +def fmt_key(fmt: dict, rate_tok, cfg_tok, depth_tok): + """Canonical rate/channel/valid-bit/type key for declared IPC4 formats.""" cfg = fmt.get(cfg_tok, 0) bits = (cfg >> 8) & 0xff - stype = (cfg >> 16) & 0xff if bits in (8, 32) else TYPE_LSB_INTEGER + stype = (cfg >> 16) & 0xff + if bits == 32 and fmt.get(depth_tok, 0) == 32 and stype == 0: + stype = TYPE_LSB_INTEGER + elif bits not in (8, 32): + stype = TYPE_LSB_INTEGER return fmt.get(rate_tok, 0), cfg & 0xff, bits, stype @@ -149,12 +152,12 @@ def pcm_host_formats(ctx): continue _ni, _no, ins, outs = kw.audio_formats() if path.direction == PLAYBACK: - fmts, keys = ins, (TKN_IN_RATE, TKN_IN_FMT_CFG, TKN_IN_PIN) + fmts, keys = ins, (TKN_IN_RATE, TKN_IN_FMT_CFG, TKN_IN_PIN, 1901) else: - fmts, keys = outs, (TKN_OUT_RATE, TKN_OUT_FMT_CFG, TKN_OUT_PIN) + fmts, keys = outs, (TKN_OUT_RATE, TKN_OUT_FMT_CFG, TKN_OUT_PIN, 1931) if not fmts: continue - avail = [fmt_key(f, keys[0], keys[1]) for f in fmts if not f.get(keys[2], 0)] + avail = [fmt_key(f, keys[0], keys[1], keys[3]) for f in fmts if not f.get(keys[2], 0)] if len({a[:3] for a in avail}) <= 1: continue # single format: the kernel uses it without matching bad = [(rate, ch, name) for rate, ch, name, bits, stype in @@ -177,10 +180,10 @@ def pcm_host_sample_formats(ctx): continue _ni, _no, ins, outs = kw.audio_formats() if path.direction == PLAYBACK: - fmts, keys = ins, (TKN_IN_RATE, TKN_IN_FMT_CFG, TKN_IN_PIN) + fmts, keys = ins, (TKN_IN_RATE, TKN_IN_FMT_CFG, TKN_IN_PIN, 1901) else: - fmts, keys = outs, (TKN_OUT_RATE, TKN_OUT_FMT_CFG, TKN_OUT_PIN) - avail = [fmt_key(f, keys[0], keys[1]) for f in fmts if not f.get(keys[2], 0)] + fmts, keys = outs, (TKN_OUT_RATE, TKN_OUT_FMT_CFG, TKN_OUT_PIN, 1931) + avail = [fmt_key(f, keys[0], keys[1], keys[3]) for f in fmts if not f.get(keys[2], 0)] if len({a[:3] for a in avail}) <= 1: continue have = {(bits, stype) for _r, _c, bits, stype in avail} diff --git a/tools/topology/validator/checks/tokens.py b/tools/topology/validator/checks/tokens.py index 3f156410c211..fd9939895df8 100644 --- a/tools/topology/validator/checks/tokens.py +++ b/tools/topology/validator/checks/tokens.py @@ -215,3 +215,34 @@ def unused_tokens(ctx): if t.token in kt.TOKEN_NAMES and t.token not in used}) if unused: yield w.label, f"{w.type_name}: {', '.join(map(tname, unused))}" + + +@rule("T008", "L2", ERROR, "sof_ipc4_get_audio_fmt, audio_stream_fmt_conversion", + "audio format sample types and bit depths are valid") +def audio_format_types(ctx): + for w in ctx.tplg.widgets: + kw = ctx.kw(w) + if kw.error: + continue + _, _, ins, outs = kw.audio_formats() + for direction, fmts, rate_token in (("input", ins, TKN_IN_RATE), + ("output", outs, TKN_OUT_RATE)): + _, depth_tok, _, cfg_tok, _ = FORMAT_FIELDS[rate_token] + for i, fmt in enumerate(fmts): + cfg, depth = fmt.get(cfg_tok, 0), fmt.get(depth_tok, 0) + if not cfg: + continue + bits, stype = (cfg >> 8) & 0xff, (cfg >> 16) & 0xff + if stype not in SAMPLE_TYPES: + problem = f"unknown sample type {stype}" + elif not depth or not bits: + continue + elif bits > depth: + problem = f"valid bit depth {bits} exceeds container depth {depth}" + elif stype == 4 and (depth, bits) != (32, 32): + problem = f"float requires 32/32 bits, got {depth}/{bits}" + elif stype in (5, 6) and (depth, bits) != (8, 8): + problem = f"{SAMPLE_TYPES[stype].strip()} requires 8/8 bits, got {depth}/{bits}" + else: + continue + yield w.label, f"{direction} format {i}: {problem}" diff --git a/tools/topology/validator/tests/test_integration.py b/tools/topology/validator/tests/test_integration.py index 392792ea0e53..1fe4fe2a5c96 100644 --- a/tools/topology/validator/tests/test_integration.py +++ b/tools/topology/validator/tests/test_integration.py @@ -70,6 +70,19 @@ def test_cli_exit_status(tmp_path): assert res.returncode == 0, res.stdout + res.stderr +def test_siggen_echo_reference_pin_formats(tmp_path): + out = build_topology( + tmp_path, "cavs-sdw.conf", + "PLATFORM=mtl,NUM_SDW_AMP_LINKS=1,SDW_DMIC=1,SDW_AMP_FEEDBACK=false," + "SDW_SPK_STREAM=Playback-SmartAmp,SDW_DMIC_STREAM=Capture-SmartMic," + "SDW_JACK_OUT_STREAM=Playback-SimpleJack,SDW_JACK_IN_STREAM=Capture-SimpleJack," + "COMPRESSED=true,SDW_JACK_ECHO_REF=true,SDW_ECHO_REF_DAI=true") + t = model.load(out) + assert t.widget("siggen.12.1") is not None + findings = checks.run(t, checks.Options(), selected={"F003"}) + assert not [f.format() for f in findings if f.severity != checks.INFO] + + def test_kernel_schema_is_current(): kernel = os.environ.get("KERNEL_TREE") if not kernel: diff --git a/tools/topology/validator/tests/test_rules.py b/tools/topology/validator/tests/test_rules.py index 5d736aa49ed7..b5c6415e20fb 100644 --- a/tools/topology/validator/tests/test_rules.py +++ b/tools/topology/validator/tests/test_rules.py @@ -5,6 +5,7 @@ """One positive and at least one negative case per rule.""" import struct +import sys import pytest @@ -270,11 +271,241 @@ def test_m001_manifest_tlv_overflow(): def test_f002_pcm_format_without_host_format(): t = playback_ssp() widget(t, "host-copier.0.playback").priv = module_tuples( - n_in=2, in_pins=0, ins=[in_format(), in_format(depth=16, valid=16)]) + n_in=2, in_pins=0, + ins=[sample_type(in_format(), 1907, 0), in_format(depth=16, valid=16)]) t.pcms[0].caps[0].formats = 1 << 10 | 1 << 14 # S32_LE and FLOAT_LE found = run(t.build()) assert "F002" in rules(found, checks.WARNING) assert any("FLOAT_LE" in f.message for f in found if f.rule == "F002") + assert not any("S32_LE" in f.message for f in found if f.rule == "F002") + + +def pin_connection(outs=None, ins=None, out_pins=1, in_pins=1, + out_binding=None, in_binding=None, source_type="buffer"): + t = Topology() + t.pipeline(1) + outs = [out_format()] if outs is None else outs + ins = [in_format()] if ins is None else ins + t.widget("source", source_type, 1, module_tuples( + n_out=len(outs), out_pins=out_pins, outs=outs, + extra=[] if out_binding is None else + [(TupleType.STRING, [(414, peer)]) for peer in out_binding])) + t.widget("sink", "effect", 1, module_tuples( + n_in=len(ins), in_pins=in_pins, ins=ins, + extra=[] if in_binding is None else + [(TupleType.STRING, [(413, peer)]) for peer in in_binding])) + t.route("source", "sink") + return t + + +def f003(t): + return [f for f in run(t.build()) if f.rule == "F003"] + + +def test_f003_compatible_single_pins(): + assert f003(pin_connection()) == [] + + +@pytest.mark.parametrize("change", [ + {"rate": 16000}, {"channels": 1}, {"depth": 16, "valid": 16}, +]) +def test_f003_incompatible_single_pins(change): + found = f003(pin_connection(ins=[in_format(**change)])) + assert len(found) == 1 and found[0].severity == checks.ERROR + assert "output pin 0" in found[0].message and "input pin 0" in found[0].message + + +def test_f003_checks_actual_input_binding_not_matching_other_pin(): + t = pin_connection(outs=[out_format(channels=1)], + ins=[in_format(in_pin=0), in_format(channels=1, in_pin=1)], + in_pins=2, in_binding=["source", "other"]) + assert len(f003(t)) == 1 + assert f003(t)[0].severity == checks.ERROR + t = pin_connection(outs=[out_format(channels=1)], + ins=[in_format(in_pin=0), in_format(channels=1, in_pin=1)], + in_pins=2, in_binding=["other", "source"]) + assert f003(t) == [] + + +def test_f003_checks_actual_output_binding(): + t = pin_connection(outs=[out_format(out_pin=0), out_format(channels=1, out_pin=1)], + out_pins=2, out_binding=["other", "sink"]) + found = f003(t) + assert len(found) == 1 and "output pin 1" in found[0].message + assert found[0].severity == checks.ERROR + + +@pytest.mark.parametrize("rate,expected", [(48000, None), (16000, checks.ERROR)]) +def test_f003_mixer_secondary_output_uses_shared_format(rate, expected): + t = pin_connection(source_type="mixer", out_pins=2, + out_binding=["other", "sink"], ins=[in_format(rate=rate)]) + found = f003(t) + if expected is None: + assert found == [] + else: + assert len(found) == 1 and found[0].severity == expected + + +def test_f003_mixer_secondary_output_uses_all_declared_formats(): + t = pin_connection(source_type="mixer", out_pins=2, + out_binding=["other", "sink"], + outs=[out_format(), out_format(rate=16000, out_pin=1)], + ins=[in_format(rate=16000)]) + assert f003(t) == [] + + +def test_f003_format_overlap_is_not_a_mismatch(): + assert f003(pin_connection(outs=[out_format(), out_format(rate=16000)])) == [] + + +@pytest.mark.parametrize("depth,valid,stype,mismatch", [ + (32, 32, 4, True), # float vs integer + (32, 32, 0, False), # full-width MSB vs LSB integer + (64, 32, 0, True), # padded MSB integer remains distinct + (32, 24, 0, False), # internal sub-32-bit integer representation +]) +def test_f003_declared_format_semantics(depth, valid, stype, mismatch): + fmt = sample_type(in_format(depth=depth, valid=valid), 1907, stype) + found = f003(pin_connection(outs=[out_format(depth=depth, valid=valid)], ins=[fmt])) + assert bool(found) == mismatch + assert all(f.severity == checks.ERROR for f in found) + + +def sample_type(fmt, token, stype): + return (fmt[0], [(tok, (value & 0xffff) | (stype << 16) if tok == token else value) + for tok, value in fmt[1]]) + + +@pytest.mark.parametrize("stype", [3, 4, 5, 6]) +def test_f003_normalization_does_not_hide_representation_mismatch(stype): + fmt = sample_type(in_format(depth=16, valid=16), 1907, stype) + found = f003(pin_connection(outs=[out_format(depth=16, valid=16)], ins=[fmt])) + assert len(found) == 1 and found[0].severity == checks.ERROR + + +@pytest.mark.parametrize("depth,valid", [(16, 16), (32, 24), (24, 24), (32, 32)]) +def test_f003_equivalent_msb_lsb_integer_formats(depth, valid): + fmt = sample_type(in_format(depth=depth, valid=valid), 1907, 0) + assert f003(pin_connection(outs=[out_format(depth=depth, valid=valid)], ins=[fmt])) == [] + + +@pytest.mark.parametrize("depth,valid,stype,problem", [ + (32, 24, 4, "float requires 32/32"), + (16, 16, 4, "float requires 32/32"), + (32, 32, 5, "A-law requires 8/8"), + (16, 16, 6, "mu-law requires 8/8"), + (16, 24, 1, "exceeds container depth"), + (32, 32, 7, "unknown sample type"), +]) +@pytest.mark.parametrize("output", [False, True]) +def test_t008_rejects_invalid_sample_types(depth, valid, stype, problem, output): + if output: + fmt = sample_type(out_format(depth=depth, valid=valid), 1937, stype) + t = pin_connection(outs=[fmt]) + else: + fmt = sample_type(in_format(depth=depth, valid=valid), 1907, stype) + t = pin_connection(ins=[fmt]) + found = [f for f in run(t.build()) if f.rule == "T008"] + assert len(found) == 1 and found[0].severity == checks.ERROR + assert problem in found[0].message + + +@pytest.mark.parametrize("depth,valid,stype", [ + (8, 8, 3), (8, 8, 5), (8, 8, 6), (16, 16, 0), (16, 16, 1), + (32, 24, 0), (32, 24, 1), (32, 32, 1), (32, 32, 4), +]) +def test_t008_accepts_valid_sample_types(depth, valid, stype): + fmt = sample_type(in_format(depth=depth, valid=valid), 1907, stype) + assert "T008" not in rules(run(pin_connection(ins=[fmt]).build())) + + +def test_t008_checks_unknown_type_with_missing_depth(): + fmt = sample_type(in_format(depth=0), 1907, 7) + found = [f for f in run(pin_connection(ins=[fmt]).build()) if f.rule == "T008"] + assert len(found) == 1 and "unknown sample type 7" in found[0].message + + +def test_f003_both_endpoints_use_secondary_pins(): + t = pin_connection( + outs=[out_format(out_pin=0), out_format(channels=1, out_pin=1)], + ins=[in_format(in_pin=0), in_format(channels=1, in_pin=1)], + out_pins=2, in_pins=2, out_binding=["other", "sink"], + in_binding=["other", "source"]) + assert f003(t) == [] + + +def test_f003_input_binding_is_not_inferred_from_output_binding(): + t = pin_connection(out_pins=2, in_pins=2, out_binding=["other", "sink"]) + found = f003(t) + assert len(found) == 1 and found[0].severity == checks.INFO + + +def test_f003_incomplete_format_is_not_a_mismatch(): + fmt = in_format() + fmt = (fmt[0], [(tok, value) for tok, value in fmt[1] if tok != 1900]) + found = f003(pin_connection(ins=[fmt])) + assert len(found) == 1 and found[0].severity == checks.INFO + + +def test_f003_unbound_pins_are_not_guessed(): + found = f003(pin_connection(out_pins=2)) + assert len(found) == 1 and found[0].severity == checks.INFO + assert "cannot statically resolve" in found[0].message + + +def test_f003_copier_undeclared_secondary_output_is_runtime_defined(): + found = f003(pin_connection(out_pins=2, out_binding=["other", "sink"], + ins=[in_format(channels=1)])) + assert len(found) == 1 and found[0].severity == checks.INFO + + +def test_f003_process_undeclared_secondary_output_uses_pin_zero(): + found = f003(pin_connection(source_type="effect", out_pins=2, + out_binding=["other", "sink"], + ins=[in_format(channels=1)])) + assert len(found) == 1 and found[0].severity == checks.ERROR + + +def test_f003_skips_chain_dma(): + t = pin_connection(ins=[in_format(channels=1)]) + widget(t, "pipeline.1").priv = tuples((TupleType.BOOL, [(209, 1)])) + assert f003(t) == [] + + +def test_f003_virtual_route_and_unbound_siggen_input(): + t = Topology() + t.pipeline(1) + t.widget("virtual", "output", 1) + t.widget("siggen", "effect", 1, module_tuples()) + t.widget("host", "aif_out", 1, module_tuples()) + t.route("virtual", "siggen") + t.route("siggen", "host") + assert f003(t) == [] + + +def test_f003_feature_route_uses_base_pin_formats(): + base = pin_connection(outs=[out_format(channels=1)]) + feature = Topology() + feature.pipeline(2) + feature.widget("feature", "effect", 2, module_tuples()) + feature.route("source", "feature") + t = model.build(binary.parse_bytes(feature.build()), "feature.tplg") + t.add_base(model.build(binary.parse_bytes(base.build()), "base.tplg")) + found = [f for f in checks.run(t, checks.Options()) if f.rule == "F003"] + assert len(found) == 1 and found[0].severity == checks.ERROR + + +@pytest.mark.parametrize("topology,status,expected", [ + (pin_connection(ins=[in_format(rate=16000)]), 1, "error: F003"), + (pin_connection(out_pins=2), 0, "info: F003"), + (pin_connection(), 0, "0 errors, 0 warnings, 0 info"), +]) +def test_f003_cli_exit_status(tmp_path, monkeypatch, capsys, topology, status, expected): + path = tmp_path / "test.tplg" + path.write_bytes(topology.build()) + monkeypatch.setattr(sys, "argv", ["tplg_validate.py", "--rules", "F003", "-v", str(path)]) + assert tplg_validate.main() == status + assert expected in capsys.readouterr().out @pytest.mark.parametrize("rule_id", [r.id for r in checks.load_rules()])