Using BiQuad filter with external PCM data stream

Rezo

Well-known member
I have a need to do some analysis on a PCM data stream from a WAV file, to create a 3-band waveform of an audio track.

I'm reading the data directly from the SD card, and looping though samples.
I am not playing the audio back during this analysis, and when I do, I'm not using the Audio lib at all, but rather directly feeding data to SAI and loading samples though the SAI interrupt.

I need to use the biquad filter to create three bandpass filters to extract Low (20-200Hz) Mid (200-2000Hz) and High (2000-20000Hz) of each track.

Is there a way to run the data though the biquad filter and read the output at the same time?

Below is an example of how I create a single band waveform with amplitude and rms view. I've noted where I want to add the three bandpass filters

Code:
void createWaveform(ExFile file) {
  // Skip WAV header (44 bytes)
  file.seekSet(44);
  uint32_t numSamplesProcessed = 0;

  // Loop through PCM data
  while (file.available()) {
      
      int32_t maxAmplitude = 0;
      int64_t sumSquares = 0;

      for (uint16_t i = 0; i < NUM_SAMPLES; i++) {
          int16_t leftChannel, rightChannel;

          if (file.read(&leftChannel, sizeof(int16_t)) != sizeof(int16_t)) break;
          if (file.read(&rightChannel, sizeof(int16_t)) != sizeof(int16_t)) break;

          int32_t combinedSample = (leftChannel + rightChannel) / 2;
          int32_t amplitude = abs(combinedSample);  // Ensure amplitude is positive

          if (amplitude > maxAmplitude) {
              maxAmplitude = amplitude;
          }

          sumSquares += (int64_t)amplitude * amplitude;
        }
        
      //EXTRACT THREE BANDS HERE   

      // Calculate RMS
      int32_t rms = sqrt(sumSquares / NUM_SAMPLES);

      // Scale amplitude and RMS to 8 bits each (0-WAVEFORM_HEIGHT/2-1)
      uint8_t scaledAmplitude = map(maxAmplitude,0, INT16_MAX, 0, WAVEFORM_HEIGHT/2-1);
      uint8_t scaledRMS = map(rms,0, INT16_MAX, 0, WAVEFORM_HEIGHT/2-1);

      // Combine amplitude and RMS into a uint16_t value
      uint16_t combinedValue = (scaledAmplitude << 8) | scaledRMS;

      if (numSamplesProcessed < WFORMDYNAMIC_SIZE) {
          WFORMDYNAMIC[numSamplesProcessed] = combinedValue;
          numSamplesProcessed++;
      }
  }
  Serial.printf("Number of samples proccessed: %d \n", numSamplesProcessed);
  //Skip back to end of WAV header/start of PCM data
  file.seekSet(44);
}

If this can be done using the audio library, I'd highly appreciate some guidance here.
If not, then can this be done with CMSIS-DSP lib?
 
Some time ago I wrote an "update grabber" which takes over the audio system so you can graph or log its output in slower than real time. Here's an example I hacked together quickly - not sure if it's along the lines you were thinking, and I couldn't be bothered to figure out BiQuad coefficients so dropped the state variable filter in instead. The dummy I2S2 object is only in there to force the Design Tool to export it, it's not connected.
C++:
// Demonstrate use of AudioUpdateGrabber class

#include <Audio.h>

//==========================================================================================
// Class to "grab" update responsibility so that audio engine updates can be run
// at a speed convenient for debugging.
extern void software_isr(void); // usually triggered by Audio interrupt
class AudioUpdateGrabber : public AudioStream
{
    bool update_responsibility;
  public:
    AudioUpdateGrabber() : AudioStream(0,NULL), update_responsibility(update_setup()) { }
    void do_update_all() {software_isr();} // call directly, so it's not actually an interrupt
    bool has_responsibility() {return update_responsibility;}
    void update() {} // needed to ensure class isn't abstract
};

//* delete first character of this line to un-grab the update responsibility
#define UPDATE_GRABBED
AudioUpdateGrabber grbr; // putting this before GUItool code should ensure we get the update
//*/
//==========================================================================================

// GUItool: begin automatically generated code
AudioPlaySdWav           playSdWav1;     //xy=236,311
AudioFilterStateVariable filter1;        //xy=454,360
AudioRecordQueue         queueInput; //xy=574,218
AudioRecordQueue         queueLow;         //xy=660,369
AudioRecordQueue         queueMid; //xy=665,409
AudioRecordQueue         queueHigh;  //xy=682,449
AudioOutputI2S2          i2s2_dummy;         //xy=844,298

AudioConnection          patchCord1{playSdWav1, 0, filter1, 0};
AudioConnection          patchCord2{playSdWav1, 0, queueInput, 0};
AudioConnection          patchCord3{filter1, 0, queueLow, 0};
AudioConnection          patchCord4{filter1, 1, queueMid, 0};
AudioConnection          patchCord5{filter1, 2, queueHigh, 0};

// GUItool: end automatically generated code



void setup()
{
  while(!Serial)
    ;
  if (CrashReport)
    Serial.print(CrashReport);
 
  AudioMemory(24);

  while (!SD.begin(BUILTIN_SDCARD))
  {
    Serial.println("SD.begin() failed");
    delay(500);
  }
  Serial.println("SD.begin() success");
 
  queueInput.begin();
  queueLow.begin();
  queueMid.begin();
  queueHigh.begin();

  filter1.frequency(1000.0f);

  playSdWav1.play("SDTEST2.wav");
  delay(5); // because AudioPlaySdWav insists...
 
  Serial.println("playing ... input low mid high");
}


void loop()
{
#if defined(UPDATE_GRABBED)
  grbr.do_update_all(); // each update corresponds to 128 samples, or 2.9ms

  // output latest results: plotter is good to watch,
  // serial monitor for dropping results into a spreadsheet
  while (queueInput.available()) // should be one per loop
  {
    short* p1 = queueInput.readBuffer();
    short* p2 = queueLow.readBuffer();
    short* p3 = queueMid.readBuffer();
    short* p4 = queueHigh.readBuffer();

    // Output results, adding
    // block boundary markers which serve to prevent
    // the plotter auto-scaling and confusing you. And me.
    for (int i=000;i<128;i+=1)
    {
      Serial.printf("%d %d %d %d %d %d\n",i==0?0:32767,i==0?0:-32767,
                                    *p1++, *p2++, *p3++, *p4++);
      delay(4);
    }

    queueInput.freeBuffer();
    queueLow.freeBuffer();
    queueMid.freeBuffer();
    queueHigh.freeBuffer();
  }
#endif // defined(UPDATE_GRABBED)
}
This looks like
1744492824001.png

The serial output is best viewed on the Arduino Serial Plotter, but of course you could log it to a file. Adjust the delay to suit - it takes a while for anything interesting to appear from a real audio file, then it tends to flash past...

Hope this is something like what you were after.
 
Thanks for the response!

Is there any way to run this without the direct patch to the playSdWave()? So I can just directly feed it from a small buffer with several hundred or thousand of samples at a time?

Im planning on analyzing at 48khz stereo PCM file.
I first average the L+R channels, then I downsample from each 320 samples into a waveform line.
 
Yes, the AudioPlaySdWav is only in there because you said you were starting from a WAV file, so it seemed the easiest and most relevant way to put the demo together. If you're starting from an existing buffer of samples, you'd probably replace that with an AudioPlayQueue and feed your samples in using the usual 128-sample blocks. The 48kHz file might well be rejected by the playback object anyway, as having the wrong sample rate. I don't know for sure, haven't used it for ages.

Note that this is only a tool used to observe / debug what the audio library will do with your code. So for example you could do your averaging and band-splitting in the audio library, then use queues to extract the 3 bands to your averaging / downsampling code. You could then play with BiQuad filter coefficients until you get the result you want.
 
An issue with using the Audio library to do this is that it is locked to a physical input / output device at a fixed rate, whereas just to pull spectra off a file you might as well read the file at full pelt and do filtering without such delays.

It should be possible to use an AudioFilterBiquad object directly by calling the update() method directly, but I'm not sure how easy that is - the audio blocks have to be managed explicitly and its possible that's tricky.
 
The problem with using any AudioStream-derived object is that it’ll get linked into the update list at construction time, so there’s no way to run it independently apart from something like the bodge I posted in #2, which tramples on the audio but at least gets you a way to debug your code in non-real time.

Without a better understanding of what @Rezo wants to achieve (multi-channel 3-band peak and RMS display, updated every 320 samples?) it’s hard to give much guidance as to the best way to do it. 3x BiQuads with a peak and RMS on each one’s output, per channel, does feel a bit like overkill … but maybe it’d be a lot of work to make a significant improvement on what the Audio library can already do. If you were dropping 50% CPU down to 25% then it’s worth it; 5% to 2.5%, not so much…
 
In that case you'd have to borrow the code from AudioFilterBiquad class to do the work off-line. (Second-order digital filter sections aren't that complicated really!)

Thinking about it retrospectively it might have been more flexible to define the audio classes so they can function standalone, or perhaps provide an alternative harness to connect them together independent of the interrupt-driven main loop stuff... But I guess its a very niche circumstance like this that benefits from it.
 
Back
Top