0

I'm trying to play a sound, on a single speaker (mono), from a .wav file in SD card using a STM32H7 controller and freertos environment. I currently managed to generate sound but it is very dirty and jerky.

I'd like to show the parsed header content of my wav file but my reputation score is below 10. Most important data are : format : PCM 1 Channel Sample rate : 44100 Bit per sample : 16

I initialize the SAI2 block A this way :

void MX_SAI2_Init(void)
{

  /* USER CODE BEGIN SAI2_Init 0 */

  /* USER CODE END SAI2_Init 0 */

  /* USER CODE BEGIN SAI2_Init 1 */

  /* USER CODE END SAI2_Init 1 */

  hsai_BlockA2.Instance = SAI2_Block_A;
  hsai_BlockA2.Init.AudioMode = SAI_MODEMASTER_TX;
  hsai_BlockA2.Init.Synchro = SAI_ASYNCHRONOUS;
  hsai_BlockA2.Init.OutputDrive = SAI_OUTPUTDRIVE_DISABLE;
  hsai_BlockA2.Init.NoDivider = SAI_MASTERDIVIDER_ENABLE;
  hsai_BlockA2.Init.FIFOThreshold = SAI_FIFOTHRESHOLD_EMPTY;
  hsai_BlockA2.Init.AudioFrequency = SAI_AUDIO_FREQUENCY_44K;
  hsai_BlockA2.Init.SynchroExt = SAI_SYNCEXT_DISABLE;
  hsai_BlockA2.Init.MonoStereoMode = SAI_MONOMODE;
  hsai_BlockA2.Init.CompandingMode = SAI_NOCOMPANDING;
  hsai_BlockA2.Init.TriState = SAI_OUTPUT_NOTRELEASED;
  if (HAL_SAI_InitProtocol(&hsai_BlockA2, SAI_I2S_STANDARD, SAI_PROTOCOL_DATASIZE_16BIT, 2) != HAL_OK)
  {
    Error_Handler();
  }

  /* USER CODE BEGIN SAI2_Init 2 */

  /* USER CODE END SAI2_Init 2 */

}

I think I set the clock frequency correctly, as I measure a frame synch clock of 43Khz (closest I can get to 44,1Khz) The file indicate it's using PCM protocol. My init function indicate SAI_I2S_STANDARD but it's only because I was curious of the result with this parameter value. I have bad result in both cases.

And here is the part where I read the file + send data to the SAI DMA

//Before infinite loop I extract the overall file size in bytes.

// Infinite Loop
    for(;;)
    {
        if(drv_sdcard_getDmaTransferComplete()==true)
        {
//          BufferRead[0]=0xAA;
//          BufferRead[1]=0xAA;
//
//          ret = HAL_SAI_Transmit_DMA(&hsai_BlockA2, (uint8_t*)BufferRead, 2);
//          drv_sdcard_resetDmaTransferComplete();
            if((firstBytesDiscarded == true)&& (remainingBytes>0))
            {
                //read the next BufferRead size audio samples
                if(remainingBytes < sizeof(BufferAudio))
                {
                    remainingBytes -= drv_sdcard_readDataNoRewind(file_audio1_index, BufferAudio, remainingBytes);
                }
                else
                {
                    remainingBytes -= drv_sdcard_readDataNoRewind(file_audio1_index, BufferAudio, sizeof(BufferAudio));
                }
                //send them by the SAI through DMA
                ret = HAL_SAI_Transmit_DMA(&hsai_BlockA2, (uint8_t*)BufferAudio, sizeof(BufferAudio));
                //reset transmit flag for forbidding next transmit
                drv_sdcard_resetDmaTransferComplete();
            }
            else
            {
                //discard header size first bytes
                //I removed this part here because it works properly on my side
                firstBytesDiscarded = true;
            }
        }

I have one track of sound quality improvment : it is to filter speaker input. Yesterday I tried cutting @ 20Khz and 44khz but it cut too much the signal... So I want to try different cutting frequencies until I find the sound is of good quality. It is a simple RC filter.

But to fix the jerky part, I dont know what to do. To give you an idea on how the sound comes out, I would describe it like this :

we can hear a bit of melody then scratchy sound [krrrrrrr] then short silence

and this looping until the end of the file.

Buffer Audio size is 16*1024 bytes.

Thank you for your help

Raphlouk
  • 1
  • 1
  • Are you using MCU DAC to drive speaker amplifier? If so you need high order low pass filter with cutof 20k. I think you must test different sine wave as source file until finding bug. by scoping output signal. no one other than yourself could do this better. – mohammadsdtmnd Mar 09 '23 at 06:32

1 Answers1

1

Problems

  1. No double-buffering. You are reading data from the SD-card into the same buffer that you are playing from. So you'll get some samples from the previous read, and some samples from the new read.

  2. Not checking when the DMA is complete. HAL_SAI_Transmit_DMA() returns immediately, and you cannot call it again until the previous DMA has completed.

  3. Not checking return values of HAL functions. You assign ret = HAL_SAI_Transmit_DMAbut then never check what ret is. You should check if there is an error and take appropriate action.

  4. You seem to be driving things from how fast the SD-card can DMA the data. It needs to be based on how fast the SAI is consuming it, otherwise you will have glitches.

Possible solution

The STM32's DMA controller can be configured to run in circular-buffer mode. In this mode, it will DMA all the data given to it, and then start again from the beginning.

It also provides interrupts for when the DMA is half complete, and when it is fully complete.

These two things together can provide a smooth data transfer with no gaps and glitches, if used with the SAI DMA. You'd read data into the entire buffer to start with, and kick off the DMA. When you get the half-complete interrupt, read half a buffer's worth of data into the first half of the buffer. When you get a fully complete interrupt, read half a buffer's worth of data into the second half of the buffer.

This is psuedo-code-ish, but hopefully shows what I mean:

const size_t buff_len = 16u * 1024u;
uint16_t     buff[buff_len];

void start_playback(void)
{
    read_from_file(buff, buff_len);
    if HAL_SAI_Transmit_DMA(&hsai_BlockA2, buff, buff_len) != HAL_OK)
    {
        // Handle error
    }
}

void sai_dma_tx_half_complete_interrupt(void)
{
    read_from_file(buff, buff_len / 2u);
}

void sai_dma_tx_full_complete_interrupt(void)
{
    read_from_file(buff + buff_len / 2u, buff_len / 2u);
}

You'd need to detect when you have consumed the entire file, and then stop the DMA (with something like HAL_SAI_DMAStop()).

You might want to read this similar question where I gave a similar answer. They were recording to SD-card rather than playing back, but the same principles apply. They also supplied their actual code for the solution they employed.

pmacfarlane
  • 3,057
  • 1
  • 7
  • 24
  • Thank you @pmacfarlane, I talked with a colleague, he advised me to do the same : to use double buffering. I implemented it friday afternoon and it's a bit bugged. I'll inform you when my double buffering will work properly and how sound will come out once it's done. – Raphlouk Feb 20 '23 at 08:44