PortentaH7 + VisionShield: PDM MIC 16KHz just mono and left channel only - why?

I use PortentaH7 plus VisionShield. I want to get audio from PDM MICs.
All works fine, with 48KHz, 32KHz and 8KHz sampling rate.
Just:
with 16KHz I get just left channel and both channels (stereo) are identical - as mono. Why?

Remark:
I use the audio.c file from Arduino mbed library files (a bit modified and extended).
I believe that even this Arduino file should have the same issue (and some "strange" config done).

The code in "audio.c":

/*
 * This file is part of the OpenMV project.
 *
 * Copyright (c) 2013-2021 Ibrahim Abdelkader <iabdalkader@openmv.io>
 * Copyright (c) 2013-2021 Kwabena W. Agyeman <kwagyeman@openmv.io>
 *
 * This work is licensed under the MIT license, see the file LICENSE for details.
 *
 * Audio Python module.
 */
#ifdef TARGET_PORTENTA_H7

/** TODO:
 * why is 16KHz sampling rate mono and left MIC only?
 */

#include <stdio.h>
#include "stm32h7xx_hal.h"
#include "pdm2pcm_glo.h"
#include "audio.h"
#include "stdbool.h"

#include "VCP_UART.h"

#include "cmsis_os.h"
#include "lwip/opt.h"
#include "lwip/arch.h"
#include "lwip/api.h"
#include "lwip/err.h"

#include <math.h>

static CRC_HandleTypeDef hcrc;
static SAI_HandleTypeDef hsai;
static DMA_HandleTypeDef hdma_sai_rx;

static int g_i_channels = AUDIO_SAI_NBR_CHANNELS;
static int g_o_channels = AUDIO_SAI_NBR_CHANNELS;
static PDM_Filter_Handler_t  PDM_FilterHandler[2];
static PDM_Filter_Config_t   PDM_FilterConfig[2];

#define DMA_XFER_NONE   (0x00U)
#define DMA_XFER_HALF   (0x01U)
#define DMA_XFER_FULL   (0x04U)

#define AUDIO_FREQUENCY_192K          ((uint32_t)192000)
#define AUDIO_FREQUENCY_96K           ((uint32_t)96000)
#define AUDIO_FREQUENCY_64K           ((uint32_t)64000)
#define AUDIO_FREQUENCY_48K           ((uint32_t)48000)
#define AUDIO_FREQUENCY_44K           ((uint32_t)44100)
#define AUDIO_FREQUENCY_32K           ((uint32_t)32000)
#define AUDIO_FREQUENCY_22K           ((uint32_t)22050)
#define AUDIO_FREQUENCY_24K           ((uint32_t)24000)
#define AUDIO_FREQUENCY_16K           ((uint32_t)16000)
#define AUDIO_FREQUENCY_11K           ((uint32_t)11025)
#define AUDIO_FREQUENCY_8K            ((uint32_t)8000)

static volatile uint32_t xfer_status = 0;

int gGen_sine = 0;

// BDMA can only access D3 SRAM4 memory.
uint8_t PDM_BUFFER[PDM_BUFFER_SIZE] __attribute__ ((section(".pdm_buffer")));
int16_t PCM_BUFFER[PCM_BUFFER_SIZE] __attribute__ ((section(".pcm_buffer"))); //or: .pcm_buffer or .dtcmram
int16_t *g_pcmbuf = PCM_BUFFER;

//VBAN buffer: 2 * 5 of such frames plus each with the 28 byte header
//VBAN works better with 5 frames, minimum is 2 here
#define VBAN_NUM_FRAMES		5
#define VBAN_BUFFER_SIZE	(((48 * 2 * 2) * VBAN_NUM_FRAMES + 28) * 2) / 4		//as 32bit words
/* ATT: ETH does not work on RAM D3! */
uint32_t VBAN_BUFFER[VBAN_BUFFER_SIZE] /* __attribute__ ((section(".pcm_buffer"))) */;
int VBANBufIdx = 0;
uint32_t VBANSeqNum = 0;
uint8_t *VBANBufPtr = (uint8_t *)VBAN_BUFFER;
static ip_addr_t udpIPdest = {.addr = 0};
static struct netconn *connUDP;
int SendUDP(const unsigned char *b, unsigned int len);

/* from the PDM.cpp file */
void PDMIrqHandler(bool halftranfer);
uint32_t PDMgetBufferSize(void);
void PDMsetBufferSize(uint32_t size);

void audio_pendsv_callback(void);			//forward declaration

//-------------------------------------
uint32_t PDMgetBufferSize(void)
{
	return PDM_BUFFER_SIZE;
}

uint32_t PDMbufferSize = 0;
void PDMsetBufferSize(uint32_t size)
{
	PDMbufferSize = size;
}

int16_t *samples;

void __attribute__((section(".itcmram"))) PDMIrqHandler(bool halftransfer)
{
	audio_pendsv_callback();
}

/* get the MIC PDM buffer for USB streaming */
int16_t * __attribute__((section(".itcmram"))) PCM_GetBuffer(void)
{
	return samples;
}

//-------------------------------------

void __attribute__((section(".itcmram"))) AUDIO_SAI_DMA_IRQHandler(void)
{
    HAL_DMA_IRQHandler(hsai.hdmarx);
}

void __attribute__((section(".itcmram"))) HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef *hsai)
{
    xfer_status |= DMA_XFER_HALF;
#ifdef CORE_CM7
    //necessary to do
    SCB_InvalidateDCache_by_Addr((uint32_t *)(&PDM_BUFFER[0]), PDM_BUFFER_SIZE / 2);
#endif
    PDMIrqHandler(true);
}

void __attribute__((section(".itcmram"))) HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai)
{
    xfer_status |= DMA_XFER_FULL;
#ifdef CORE_CM7
    SCB_InvalidateDCache_by_Addr((uint32_t *)(&PDM_BUFFER[PDM_BUFFER_SIZE / 2]), PDM_BUFFER_SIZE / 2);
#endif
    PDMIrqHandler(false);
}

static uint32_t get_decimation_factor(uint32_t decimation)
{
    switch (decimation) {
        case 16:    return PDM_FILTER_DEC_FACTOR_16;
        case 24:    return PDM_FILTER_DEC_FACTOR_24;
        case 32:    return PDM_FILTER_DEC_FACTOR_32;
        case 48:    return PDM_FILTER_DEC_FACTOR_48;
        case 64:    return PDM_FILTER_DEC_FACTOR_64;
        case 80:    return PDM_FILTER_DEC_FACTOR_80;
        case 128:   return PDM_FILTER_DEC_FACTOR_128;
        default: return 0;
    }
}

static uint8_t get_mck_div(uint32_t frequency)
{
#define DIVIDER_48K		8
	/* REMARK: 11K, 22K, 44K, 64K, (96K, 192K) not tested, not working */
    switch(frequency){
        case AUDIO_FREQUENCY_8K:     return DIVIDER_48K * 6;  				//48: SCK_x = sai_x_ker_ck/48 =  1024KHz  Ffs = SCK_x/64 =  16KHz stereo - OK
        case AUDIO_FREQUENCY_11K:    return  8;  							//SCK_x = sai_x_ker_ck/8  =  1411KHz  Ffs = SCK_x/64 =  22KHz stereo
        case AUDIO_FREQUENCY_16K:    return DIVIDER_48K * 3;	 			//24: !! SCK_x = sai_x_ker_ck/24 =  2048KHz  Ffs = SCK_x/64 =  32KHz stereo - OK
        /* 16K works just as single MIC mono, VBAN is correct, with 16 or 20 - it is stereo but VBAN has an error! */
        case AUDIO_FREQUENCY_22K:    return  4;  							//SCK_x = sai_x_ker_ck/4  =  2822KHz  Ffs = SCK_x/64 =  44KHz stereo
        case AUDIO_FREQUENCY_24K:    return DIVIDER_48K * 2;  				//16: SCK_x = sai_x_ker_ck/4  =  xxxxKHz  Ffs = SCK_x/64 =  48KHz stereo
        case AUDIO_FREQUENCY_32K:    return DIVIDER_48K + DIVIDER_48K / 2;	//12: SCK_x = sai_x_ker_ck/12 =  4096KHz  Ffs = SCK_x/64 =  64KHz stereo
        case AUDIO_FREQUENCY_44K:    return  2;  							//SCK_x = sai_x_ker_ck/2  =  5644KHz  Ffs = SCK_x/64 =  88KHz stereo
        case AUDIO_FREQUENCY_48K:    return DIVIDER_48K;  					//8: SCK_x = sai_x_ker_ck/8  =  6144KHz  Ffs = SCK_x/64 =  96KHz stereo - OK
        case AUDIO_FREQUENCY_64K:    return  6;  							//SCK_x = sai_x_ker_ck/6  =  8192KHz  Ffs = SCK_x/64 = 128KHz stereo
        case AUDIO_FREQUENCY_96K:    return DIVIDER_48K / 2;  				//4: SCK_x = sai_x_ker_ck/4  = 12288KHz  Ffs = SCK_x/64 = 192KHz stereo
        case AUDIO_FREQUENCY_192K:   return DIVIDER_48K / 4;  				//2: SCK_x = sai_x_ker_ck/2  = 24576KHz  Ffs = SCK_x/64 = 384KHz stereo
        default:                     return  0;  							//same as 1
   }
}

// TODO: this needs to become a library function
bool isBoardRev2(void) {
#if 0
  uint32_t hse_speed;
  uint8_t* bootloader_data = (uint8_t*)(0x801F000);
  if (bootloader_data[0] != 0xA0 || bootloader_data[1] < 14) {
    hse_speed = 27000000;
  } else {
    hse_speed = bootloader_data[10] * 1000000;
  }
  return (hse_speed == 25000000);
#else
  return 1;		/* return as 25 MHz on board */
#endif
}

void sai_init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    AUDIO_SAI_CLK_ENABLE();
    __GPIOB_CLK_ENABLE();
    __GPIOE_CLK_ENABLE();

    GPIO_InitStruct.Pin = AUDIO_SAI_CK_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = AUDIO_SAI_CK_AF;
    HAL_GPIO_Init(AUDIO_SAI_CK_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = AUDIO_SAI_D1_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = AUDIO_SAI_D1_AF;
    HAL_GPIO_Init(AUDIO_SAI_D1_PORT, &GPIO_InitStruct);
}

int py_audio_init(size_t channels, uint32_t frequency, int gain_db, float highpass, int gen_sine)
{
#if 1
    RCC_PeriphCLKInitTypeDef rcc_ex_clk_init_struct;

    HAL_RCCEx_GetPeriphCLKConfig(&rcc_ex_clk_init_struct);

    if((frequency == AUDIO_FREQUENCY_11K) || (frequency == AUDIO_FREQUENCY_22K) || (frequency == AUDIO_FREQUENCY_44K))
    {
    	/* ATTENTION: not tested, not trimmed ! 22K etc. does not work! */

        /* SAI clock config:
        PLL3_VCO Input = HSE_VALUE/PLL3M = 1 Mhz
        PLL3_VCO Output = PLL3_VCO Input * PLL3N = 429 Mhz
        SAI_CLK_x = PLL3_VCO Output/PLL3P = 429/38 = 11.289 Mhz */
        rcc_ex_clk_init_struct.PeriphClockSelection = RCC_PERIPHCLK_SAI4A;
        rcc_ex_clk_init_struct.Sai4AClockSelection = RCC_SAI4ACLKSOURCE_PLL3;
        rcc_ex_clk_init_struct.PLL3.PLL3P = 38;
        rcc_ex_clk_init_struct.PLL3.PLL3Q = 1;
        rcc_ex_clk_init_struct.PLL3.PLL3R = 1;
        rcc_ex_clk_init_struct.PLL3.PLL3N = 429;
        rcc_ex_clk_init_struct.PLL3.PLL3M = isBoardRev2() ? 25 : 27;

    } else {
        /* SAI clock config:
        PLL3_VCO Input = HSE_VALUE/PLL3M = 1 Mhz
        PLL3_VCO Output = PLL3_VCO Input * PLL3N = 344 Mhz
        sai_x_ker_ck = PLL3_VCO Output/PLL3P = 344/7 = 49.142 Mhz */
        rcc_ex_clk_init_struct.PeriphClockSelection = RCC_PERIPHCLK_SAI4A;
        rcc_ex_clk_init_struct.Sai4AClockSelection = RCC_SAI4ACLKSOURCE_PLL3;
#if 0
        //original config:
        rcc_ex_clk_init_struct.PLL3.PLL3P = 7;
        rcc_ex_clk_init_struct.PLL3.PLL3Q = 1;
        rcc_ex_clk_init_struct.PLL3.PLL3R = 2;
        rcc_ex_clk_init_struct.PLL3.PLL3N = 344;
        rcc_ex_clk_init_struct.PLL3.PLL3M = isBoardRev2() ? 25 : 27;
#else
        /* trimmed! my config */
        rcc_ex_clk_init_struct.PLL3.PLL3M = 2;
        rcc_ex_clk_init_struct.PLL3.PLL3N = 23;
        rcc_ex_clk_init_struct.PLL3.PLL3P = 6;					//SAI4, PDM MIC., trimmed for 48KHz
        rcc_ex_clk_init_struct.PLL3.PLL3Q = 6;					//USB 48 KHz
        rcc_ex_clk_init_struct.PLL3.PLL3R = 4;
        rcc_ex_clk_init_struct.PLL3.PLL3RGE = RCC_PLL3VCIRANGE_1;
        rcc_ex_clk_init_struct.PLL3.PLL3VCOSEL = RCC_PLL3VCOWIDE;
        rcc_ex_clk_init_struct.PLL3.PLL3FRACN = 4860;			//we trim here the SAI to be in sync with USB 48 MHz - it depends on PC USB audio clock!
#endif
    }
    HAL_RCCEx_PeriphCLKConfig(&rcc_ex_clk_init_struct);
#endif

    sai_init();

    // Sanity checks
    if ((frequency != AUDIO_FREQUENCY_8K)  &&
        (frequency != AUDIO_FREQUENCY_11K) &&
        (frequency != AUDIO_FREQUENCY_16K) &&
        (frequency != AUDIO_FREQUENCY_22K) &&
		(frequency != AUDIO_FREQUENCY_24K) &&
        (frequency != AUDIO_FREQUENCY_32K) &&
        (frequency != AUDIO_FREQUENCY_44K) &&
        (frequency != AUDIO_FREQUENCY_48K) &&
        (frequency != AUDIO_FREQUENCY_64K) &&
        (frequency != AUDIO_FREQUENCY_96K))
    	{
        	return 0;
    	}

    if (channels != 1 && channels != 2) {
        return 0;
    } else {
        g_o_channels = channels;
        g_i_channels = AUDIO_SAI_NBR_CHANNELS;
    }

    uint32_t decimation_factor = 64; // Fixed decimation factor
    uint32_t decimation_factor_const = get_decimation_factor(decimation_factor);
    if (decimation_factor_const == 0) {
        return 0;
    }
    uint32_t samples_per_channel = (PDM_BUFFER_SIZE * 8) / (decimation_factor * g_i_channels * 2); // Half a transfer

    hsai.Instance                    = AUDIO_SAI;
    hsai.Init.Protocol               = SAI_FREE_PROTOCOL;
    hsai.Init.AudioMode              = SAI_MODEMASTER_RX;
    hsai.Init.DataSize               = (g_i_channels == 1) ? SAI_DATASIZE_8 : SAI_DATASIZE_16;
    hsai.Init.FirstBit               = SAI_FIRSTBIT_LSB;
    hsai.Init.ClockStrobing          = SAI_CLOCKSTROBING_FALLINGEDGE;	//SAI_CLOCKSTROBING_RISINGEDGE;
    hsai.Init.Synchro                = SAI_ASYNCHRONOUS;
    hsai.Init.OutputDrive            = SAI_OUTPUTDRIVE_DISABLE;
    hsai.Init.NoDivider              = SAI_MASTERDIVIDER_DISABLE;
    hsai.Init.FIFOThreshold          = SAI_FIFOTHRESHOLD_EMPTY;			//SAI_FIFOTHRESHOLD_1QF;
    hsai.Init.SynchroExt             = SAI_SYNCEXT_DISABLE;
    hsai.Init.AudioFrequency         = SAI_AUDIO_FREQUENCY_MCKDIV;
    hsai.Init.MonoStereoMode         = (g_i_channels == 1)  ? SAI_MONOMODE: SAI_STEREOMODE;
    hsai.Init.CompandingMode         = SAI_NOCOMPANDING;
    hsai.Init.TriState               = SAI_OUTPUT_RELEASED;

    // The master clock output (MCLK_x) is disabled and the SAI clock
    // is passed out to SCK_x bit clock. SCKx frequency = SAI_KER_CK / MCKDIV
    hsai.Init.Mckdiv                 = get_mck_div(frequency);
    hsai.Init.MckOutput              = SAI_MCK_OUTPUT_DISABLE;
    hsai.Init.MckOverSampling        = SAI_MCK_OVERSAMPLING_DISABLE;

    // Enable and configure PDM mode.
    hsai.Init.PdmInit.Activation     = ENABLE;
    hsai.Init.PdmInit.MicPairsNbr    = 1;
    hsai.Init.PdmInit.ClockEnable    = SAI_PDM_CLOCK1_ENABLE;

    hsai.FrameInit.FrameLength       = 16;
    hsai.FrameInit.ActiveFrameLength = 1;
    hsai.FrameInit.FSDefinition      = SAI_FS_STARTFRAME;
    hsai.FrameInit.FSPolarity        = SAI_FS_ACTIVE_HIGH;
    hsai.FrameInit.FSOffset          = SAI_FS_FIRSTBIT;

    hsai.SlotInit.FirstBitOffset     = 0;
    hsai.SlotInit.SlotSize           = SAI_SLOTSIZE_DATASIZE;
    //the same
    //why this setting?
    ////hsai.SlotInit.SlotNumber         = (g_i_channels == 1) ? 2 : 1;
    ////hsai.SlotInit.SlotActive         = (g_i_channels == 1) ? (SAI_SLOTACTIVE_0 | SAI_SLOTACTIVE_1) : SAI_SLOTACTIVE_0;
    // this looks more reasonable and works as well
    hsai.SlotInit.SlotNumber         = (g_i_channels == 2) ? 2 : 1;
    hsai.SlotInit.SlotActive         = (g_i_channels == 2) ? (SAI_SLOTACTIVE_0 | SAI_SLOTACTIVE_1) : SAI_SLOTACTIVE_0;

    // Initialize the SAI
    HAL_SAI_DeInit(&hsai);
    if (HAL_SAI_Init(&hsai) != HAL_OK) {
        return 0;
    }

    // Enable the DMA clock
    AUDIO_SAI_DMA_CLK_ENABLE();

    // Configure the SAI DMA
    hdma_sai_rx.Instance                 = AUDIO_SAI_DMA_STREAM;
    hdma_sai_rx.Init.Request             = AUDIO_SAI_DMA_REQUEST;
    hdma_sai_rx.Init.Direction           = DMA_PERIPH_TO_MEMORY;
    hdma_sai_rx.Init.PeriphInc           = DMA_PINC_DISABLE;
    hdma_sai_rx.Init.MemInc              = DMA_MINC_ENABLE;
    hdma_sai_rx.Init.PeriphDataAlignment = (g_i_channels == 1) ? DMA_PDATAALIGN_BYTE : DMA_PDATAALIGN_HALFWORD;
    hdma_sai_rx.Init.MemDataAlignment    = (g_i_channels == 1) ? DMA_MDATAALIGN_BYTE : DMA_MDATAALIGN_HALFWORD;
    hdma_sai_rx.Init.Mode                = DMA_CIRCULAR;
    hdma_sai_rx.Init.Priority            = DMA_PRIORITY_HIGH;
    hdma_sai_rx.Init.FIFOMode            = DMA_FIFOMODE_ENABLE;
    hdma_sai_rx.Init.FIFOThreshold       = DMA_FIFO_THRESHOLD_FULL;
    hdma_sai_rx.Init.MemBurst            = DMA_MBURST_SINGLE;
    hdma_sai_rx.Init.PeriphBurst         = DMA_MBURST_SINGLE;
    __HAL_LINKDMA(&hsai, hdmarx, hdma_sai_rx);

    // Initialize the DMA stream
    HAL_DMA_DeInit(&hdma_sai_rx);
    if (HAL_DMA_Init(&hdma_sai_rx) != HAL_OK) {
        return 0;
    }

    // Configure and enable SAI DMA IRQ Channel
    HAL_NVIC_SetPriority(AUDIO_SAI_DMA_IRQ, AUDIO_IN_IRQ_PREPRIO, 0);
    HAL_NVIC_EnableIRQ(AUDIO_SAI_DMA_IRQ);

    // Init CRC for the PDM library
    hcrc.Instance = CRC;
    hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE;
    hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE;
    hcrc.Init.InputDataInversionMode = CRC_INPUTDATA_INVERSION_NONE;
    hcrc.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_DISABLE;
    hcrc.InputDataFormat = CRC_INPUTDATA_FORMAT_BYTES;
    if (HAL_CRC_Init(&hcrc) != HAL_OK) {
        return 0;
    }
    __HAL_CRC_DR_RESET(&hcrc);

    // Configure PDM filters
    for (int i = 0; i < g_i_channels; i++)
    {
    	PDM_FilterHandler[i].bit_order  = PDM_FILTER_BIT_ORDER_MSB;
    	PDM_FilterHandler[i].endianness = PDM_FILTER_ENDIANNESS_LE;
    	PDM_FilterHandler[i].high_pass_tap = (uint32_t) (highpass * 2147483647U); // coff * (2^31-1)
    	PDM_FilterHandler[i].out_ptr_channels = g_o_channels;
    	PDM_FilterHandler[i].in_ptr_channels  = g_i_channels;
    	PDM_Filter_Init(&PDM_FilterHandler[i]);

    	PDM_FilterConfig[i].mic_gain = gain_db;
    	PDM_FilterConfig[i].output_samples_number = samples_per_channel;
    	PDM_FilterConfig[i].decimation_factor = decimation_factor_const;
    	PDM_Filter_setConfig(&PDM_FilterHandler[i], &PDM_FilterConfig[i]);
    }

    uint32_t min_buff_size = samples_per_channel * g_o_channels * sizeof(int16_t);
    uint32_t buff_size = PDMgetBufferSize();
    if(buff_size < min_buff_size) {
      PDMsetBufferSize(min_buff_size);
    }

#if 0
    //depends on (NOLOAD) used in linker script */
    memset(PDM_BUFFER, 0, sizeof(PDM_BUFFER));
    memset(PCM_BUFFER, 0, sizeof(PCM_BUFFER));
#endif

    //initialize the VBAN buffer headers (2x for double buffering)
    {
    	uint8_t *p = (uint8_t *)VBAN_BUFFER;
#define VBAN_OFF	(VBAN_NUM_FRAMES * 48 * 2 * 2 + 28)	//offset to second buffer

    	*p = 'V'; *(p + VBAN_OFF) = 'V'; p++;
    	*p = 'B'; *(p + VBAN_OFF) = 'B'; p++;
    	*p = 'A'; *(p + VBAN_OFF) = 'A'; p++;
    	*p = 'N'; *(p + VBAN_OFF) = 'N'; p++;

    	switch (frequency)
    	{
    	case AUDIO_FREQUENCY_48K :
    				*p = 3; *(p +VBAN_OFF) = 3; p++;
    				break;
    	case AUDIO_FREQUENCY_8K :
    	    		*p = 7; *(p +VBAN_OFF) = 7; p++;
    	    		break;
    	case AUDIO_FREQUENCY_11K :
    	    		*p = 14; *(p +VBAN_OFF) = 14; p++;
    	    		break;
    	case AUDIO_FREQUENCY_16K :
    	    		*p = 8; *(p +VBAN_OFF) = 8; p++;
    	    		break;
    	case AUDIO_FREQUENCY_22K :
    	    		*p = 15; *(p +VBAN_OFF) = 15; p++;
    	    		break;
    	case AUDIO_FREQUENCY_24K :
    	    	    *p = 2; *(p +VBAN_OFF) = 2; p++;
    	    	    break;
    	case AUDIO_FREQUENCY_32K :
    	    		*p = 9; *(p +VBAN_OFF) = 9; p++;
    	    		break;
    	case AUDIO_FREQUENCY_44K :
    	    		*p = 16; *(p +VBAN_OFF) = 16; p++;
    	    		break;
    	case AUDIO_FREQUENCY_64K :
    	    	    *p = 10; *(p +VBAN_OFF) = 10; p++;
    	    	    break;
    	case AUDIO_FREQUENCY_96K :
    	    	    *p = 4; *(p +VBAN_OFF) = 4; p++;
    	    	    break;
    	}

    	*p = VBAN_NUM_FRAMES*48-1; *(p + VBAN_OFF) = VBAN_NUM_FRAMES*48-1; p++;	//number samples (per channel?)
    	*p = 1; *(p + VBAN_OFF) = 1; p++;			//number channels: 1 = two channels
    	*p = 0x01; *(p + VBAN_OFF) = 0x01; p++;		//16bit signed samples

    	*p = 'S'; *(p + VBAN_OFF) = 'S'; p++;		//the name of the stream
    	*p = 't'; *(p + VBAN_OFF) = 't'; p++;
    	*p = 'r'; *(p + VBAN_OFF) = 'r'; p++;
    	*p = 'e'; *(p + VBAN_OFF) = 'e'; p++;
    	*p = 'a'; *(p + VBAN_OFF) = 'a'; p++;
    	*p = 'm'; *(p + VBAN_OFF) = 'm'; p++;
    	*p = '1'; *(p + VBAN_OFF) = '1'; p++;
    	*p = '\0'; *(p + VBAN_OFF) = '\0'; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;
    	*p = ' '; *(p + VBAN_OFF) = ' '; p++;

    	//SeqNum: we set later on index 24..27

    	if (gen_sine)
    	{
    		gGen_sine = 1;
    		//preset samples as 1KHz sine wave, for 48 KHz sample rate
    		p++; p++; p++; p++;
    		int16_t *s = (int16_t *)p;
    		int16_t v;
    		double d;
    		int i;

    		if (frequency == AUDIO_FREQUENCY_32K)
    			frequency -= frequency / 2;			//32KHz does not fit in buffer, increase to *1.5 - a higher pitch!
    		frequency /= 1000;

    		if (gain_db > 50)
    			gain_db = 50;
    		double dGain_dB;

    		if (gain_db == 1)
    			dGain_dB = 0.00006;
    		else if (gain_db < 3)
    		    dGain_dB = 0.000078;
    		else if (gain_db < 4)
    			dGain_dB = 0.0001;
    		else if (gain_db < 5)
    			dGain_dB = 0.001;
    		else if (gain_db < 6)
    			dGain_dB = 0.01;
    		else if (gain_db < 7)
    			dGain_dB = 0.1;
    		else if (gain_db < 8)
    			dGain_dB = 0.2;
    		else if (gain_db < 9)
    		    dGain_dB = 0.3;
    		else if (gain_db < 10)
    			dGain_dB = 0.4;
    		else if (gain_db < 20)
    			dGain_dB = 0.5;
    		else if (gain_db < 30)
    		    dGain_dB = 0.6;
    		else if (gain_db < 40)
    		    dGain_dB = 0.7;
    		else if (gain_db < 55)
    		    dGain_dB = 0.8;
    		else
    		    dGain_dB = 0.9;

    		for (i = 0; i < ((VBAN_NUM_FRAMES * 48 * 2 * 2) / (sizeof(int16_t) * 2)); i++)
    		{
    			d = sin((2 * M_PI * i) / frequency);
    			v = (int16_t)(d * 32767.0 * dGain_dB);		//amplitude scaling
    			*s = v; *(s + (VBAN_OFF / 2)) = v; s++;
    			*s = v; *(s + (VBAN_OFF / 2)) = v; s++;		//both channels
    		}
    	}
    	else
    		gGen_sine = 0;
    }

    return 1;
}

void py_audio_gain_set(int gain_db)
{
    // Configure PDM filters
	for (int i = 0; i < g_i_channels;i++)
	{
		PDM_FilterConfig[i].mic_gain = gain_db;
		//This will be called only after init so PDM_FilterConfig structure is already filled
		//PDM_FilterConfig.output_samples_number = samples_per_channel;
		//PDM_FilterConfig.decimation_factor = decimation_factor_const;
		PDM_Filter_setConfig(&PDM_FilterHandler[i], &PDM_FilterConfig[i]);
	}
}

void py_audio_deinit(void)
{
    // Stop SAI DMA.
    if (hdma_sai_rx.Instance != NULL) {
        HAL_SAI_DMAStop(&hsai);
    }

    // Disable IRQs
    HAL_NVIC_DisableIRQ(AUDIO_SAI_DMA_IRQ);

    if (hsai.Instance != NULL) {
        HAL_SAI_DeInit(&hsai);
        hsai.Instance = NULL;
    }

    if (hdma_sai_rx.Instance != NULL) {
        HAL_DMA_DeInit(&hdma_sai_rx);
        hdma_sai_rx.Instance = NULL;
    }

    g_i_channels = AUDIO_SAI_NBR_CHANNELS;
    g_o_channels = AUDIO_SAI_NBR_CHANNELS;
}

void __attribute__((section(".itcmram"))) audio_pendsv_callback(void)
{
	//TODO: do it directly to USB buffer
	extern int16_t *GET_USBBuffer(void);

    // Check for half transfer complete.
    if ((xfer_status & DMA_XFER_HALF)) {
        // Clear buffer state.
        xfer_status &= ~(DMA_XFER_HALF);

        // Convert PDM samples to PCM
        if ( ! gGen_sine)
        	for (int i = 0; i < g_i_channels; i++)
        	{
        		PDM_Filter(&((uint8_t*)PDM_BUFFER)[i], &((int16_t*)g_pcmbuf)[i], &PDM_FilterHandler[i]);
        	}
#if 0
        samples = PCM_BUFFER;
#endif
#if TIMING_DEBUG
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
#endif
#ifdef CORE_CM7
        //not needed and it makes it too slow
        ////SCB_InvalidateDCache_by_Addr((uint32_t *)(&PDM_BUFFER[0]), PDM_BUFFER_SIZE / 2);
#endif
    } else if ((xfer_status & DMA_XFER_FULL)) { // Check for transfer complete.
        // Clear buffer state.
        xfer_status &= ~(DMA_XFER_FULL);

        // Convert PDM samples to PCM
        if ( ! gGen_sine)
        	for (int i = 0; i < g_i_channels; i++)
        	{
        		PDM_Filter(&((uint8_t*)PDM_BUFFER)[PDM_BUFFER_SIZE / 2 + i], &((int16_t*)g_pcmbuf)[i], &PDM_FilterHandler[i]);
        	}
#if 0
        samples = &PCM_BUFFER[PCM_BUFFER_SIZE / 2];
#endif
#if TIMING_DEBUG
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
#endif
#ifdef CORE_CM7
        ////SCB_InvalidateDCache_by_Addr((uint32_t *)(&PDM_BUFFER[PDM_BUFFER_SIZE / 2]), PDM_BUFFER_SIZE / 2);
#endif
    }

    //for VBAN - copy from USB buffer into VBAN double buffer
    if (udpIPdest.addr != 0)
    {
    	uint8_t *vbanp = (uint8_t *)VBAN_BUFFER;
    	if (VBANBufIdx >= VBAN_NUM_FRAMES)
    		vbanp += (28 + VBAN_NUM_FRAMES * PCM_BUFFER_SIZE) + 28 + (VBANBufIdx - VBAN_NUM_FRAMES) * PCM_BUFFER_SIZE;
    	else
    		vbanp += 28 + VBANBufIdx * PCM_BUFFER_SIZE;
    	if ( ! gGen_sine)
    		/* copy PCM samples to output buffer */
    		memcpy(vbanp, g_pcmbuf, PCM_BUFFER_SIZE);

    	//update VBAN SeqNum (index 24..27)
    	if (VBANBufIdx == 0)
    	{
    		*(VBAN_BUFFER + (24/4)) = VBANSeqNum++;
    	}
    	else if (VBANBufIdx == VBAN_NUM_FRAMES)
    	{
    		*(VBAN_BUFFER + ((VBAN_NUM_FRAMES * PCM_BUFFER_SIZE + 28)/4) + (24/4)) = VBANSeqNum++;
    	}

    	//trigger the audio thread, every N frames, N ms
    	if (VBANBufIdx == (VBAN_NUM_FRAMES -1))
		{
    		//osSemaphoreRelease(xSemaphoreAudio);
    		VBANBufPtr = (uint8_t *)VBAN_BUFFER;
    		SendUDP(VBANBufPtr, (VBAN_BUFFER_SIZE * 4) / 2);
		}
    	else if (VBANBufIdx == (VBAN_NUM_FRAMES * 2 -1))
    	{
    		VBANBufPtr = (uint8_t *)( VBAN_BUFFER + ((VBAN_NUM_FRAMES * PCM_BUFFER_SIZE + 28)/4) );
    		SendUDP(VBANBufPtr, (VBAN_BUFFER_SIZE * 4) / 2);
    	}

    	//move the buffer index and wrap around
    	VBANBufIdx++;
    	if (VBANBufIdx >= (VBAN_NUM_FRAMES * 2))
    		VBANBufIdx = 0;
    }
}

void py_audio_start_streaming(void)
{
    // Clear DMA buffer status
    xfer_status &= DMA_XFER_NONE;

    // Start DMA transfer
    if (HAL_SAI_Receive_DMA(&hsai, (uint8_t*) PDM_BUFFER, PDM_BUFFER_SIZE / g_i_channels) != HAL_OK)
    {
    }
}

void py_audio_stop_streaming(void)
{
    // Stop SAI DMA.
    HAL_SAI_DMAStop(&hsai);
}

/* global function to init and start MIC audio, from command line */
void PDM_MIC_Init(unsigned long gain, unsigned long freq, int gen_sine)
{
	uint32_t rFreq;
	if (gain)
	{
		/* REMARK: 16000 is just single MIC (left) and mono - why?
		 *
		 * 44100, 22050 etc. not tested and not working!
		 * 8 KHz is OK (stereo)
		 */
		switch (freq)
		{
		case 1 : rFreq = 32000; break;
		case 2 : rFreq = 24000; break;
		case 3 : rFreq = 16000; break;
		case 4 : rFreq =  8000; break;
		default: rFreq = 48000;
		}
		if (py_audio_init(2, rFreq, (int)gain - 1, 0.9883f, gen_sine))
			py_audio_start_streaming();
	}
	else
	{
		py_audio_stop_streaming();
		py_audio_deinit();
	}
}

void SetUDPDest(unsigned long ipAddr)
{
	udpIPdest.addr = ipAddr;

	/* Create UDP connection handle */
	connUDP = netconn_new(NETCONN_UDP);
	netconn_set_nonblocking(connUDP, 0);
}

int __attribute__((section(".itcmram"))) SendUDP(const unsigned char *b, unsigned int len)
{
	err_t err;
	struct netbuf *nb;
	if (udpIPdest.addr != 0)
	{
		////SCB_CleanDCache_by_Addr((uint32_t *)b, (int32_t)(((len + 32)/32) * 32));

		nb = netbuf_new();
		netbuf_ref(nb, b, len /*+ 1*/);

		err = netconn_sendto(connUDP, nb, &udpIPdest, 6980);
		if (err != ERR_OK)
		{
		}

		netbuf_delete(nb);

		return (int)err;
	}
	return -100;		/* special, to separate from err codes */
}

/* for TOF sensor, just place it here */
int __attribute__((section(".itcmram"))) SendTOFUDP(const unsigned char *b, unsigned int len)
{
	err_t err;
	struct netbuf *nb;
	if (udpIPdest.addr != 0)
	{
		////SCB_CleanDCache_by_Addr((uint32_t *)b, (int32_t)(((len + 32)/32) * 32));

		nb = netbuf_new();
		netbuf_ref(nb, b, len /*+ 1*/);

		err = netconn_sendto(connUDP, nb, &udpIPdest, 8080);
		if (err != ERR_OK)
		{
		}

		HAL_GPIO_TogglePin(GPIOK, GPIO_PIN_6);

		netbuf_delete(nb);

		return (int)err;
	}
	return -100;		/* special, to separate from err codes */
}

#endif

The "audio.h" has just this:

#ifdef TARGET_PORTENTA_H7

#include "stdbool.h"

// SAI4
#define AUDIO_SAI                   (SAI4_Block_A)
// SCKx frequency = SAI_KER_CK / MCKDIV / 2
#define AUDIO_SAI_MCKDIV            (12)
#define AUDIO_SAI_FREQKHZ           (2048U) // 2048KHz
#define AUDIO_SAI_NBR_CHANNELS      (2) 	// Default number of channels.

#if defined(TARGET_PORTENTA_H7)
#define AUDIO_SAI_CK_PORT           (GPIOE)
#define AUDIO_SAI_CK_PIN            (GPIO_PIN_2)
#define AUDIO_SAI_CK_AF             (GPIO_AF10_SAI4)

#define AUDIO_SAI_D1_PORT           (GPIOB)
#define AUDIO_SAI_D1_PIN            (GPIO_PIN_2)
#define AUDIO_SAI_D1_AF             (GPIO_AF10_SAI4)
#endif

#define AUDIO_SAI_DMA_STREAM        BDMA_Channel1
#define AUDIO_SAI_DMA_REQUEST       BDMA_REQUEST_SAI4_A
#define AUDIO_SAI_DMA_IRQ           BDMA_Channel1_IRQn
#define AUDIO_SAI_DMA_IRQHandler    BDMA_Channel1_IRQHandler

#define AUDIO_SAI_CLK_ENABLE()      __HAL_RCC_SAI4_CLK_ENABLE()
#define AUDIO_SAI_CLK_DISABLE()     __HAL_RCC_SAI4_CLK_DISABLE()
#define AUDIO_SAI_DMA_CLK_ENABLE()  __HAL_RCC_BDMA_CLK_ENABLE()

#define AUDIO_IN_IRQ_PREPRIO        ((uint32_t)15)

#define PDM_BUFFER_SIZE     (48 * 32)		//48KHz, aligned with USB streaming, DoubleBuffer
#define PCM_BUFFER_SIZE     (48 * 2 * 2)	//2 channels, 2bytes per PCM samples (16bit signed PCM)

void py_audio_deinit();
int py_audio_init(size_t g_channels, uint32_t frequency, int gain_db, float highpass, int gen_sine);
void py_audio_gain_set(int gain_db);
void audio_pendsv_callback(void);
void py_audio_start_streaming();
void py_audio_stop_streaming();
bool isBoardRev2();

#endif

Why is 16KHz sampling rate resulting in a left MIC and mono (both channels identical)?