diff --git a/tools/topology/topology2/README.md b/tools/topology/topology2/README.md index d120de60de98..e8863cb151b5 100644 --- a/tools/topology/topology2/README.md +++ b/tools/topology/topology2/README.md @@ -14,6 +14,22 @@ 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. + +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. + +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-benchmark-hda.conf b/tools/topology/topology2/cavs-benchmark-hda.conf index fda41002de8f..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' @@ -213,102 +222,114 @@ 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_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 16 + in_valid_bit_depth 16 + } + ] } - ] - } - "s24" { - Object.Base.input_audio_format [ + "s24" { + Object.Base.input_audio_format [ + { + in_rate $HDA_ANALOG_PLAYBACK_RATE + in_bit_depth 32 + in_valid_bit_depth 24 + } + ] + } + "s32" { + 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 [ { - in_bit_depth 32 - in_valid_bit_depth 24 + out_rate $HDA_ANALOG_PLAYBACK_RATE + 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_rate $HDA_ANALOG_CAPTURE_RATE 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_rate $HDA_ANALOG_CAPTURE_RATE + 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_rate $HDA_ANALOG_CAPTURE_RATE + 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_rate $HDA_ANALOG_CAPTURE_RATE + out_bit_depth 32 + out_valid_bit_depth 32 + } + ] } - ] + } } } - } + ] } - ] + } } IncludeByKey.BENCH_CONFIG { 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 } 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/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 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 [ 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 [ 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,\ 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()])