-->
Page 1 of 1

Async webserver chunked audio 16bit@44.1kHz

PostPosted: Sat Feb 13, 2021 7:53 am
by ingooogni
There are several issues with the code below I can't get my head around.

When I set maxLength to 2048 all still runs fine. When I increase the buffer size from 1024 to 2048 it crashes. Why? How Can I increase the buffers?

Currently I can send audio 16bit@22kHz, but I'd the goal is 44.1kHz or even 48kHz. Hence the attempt to increase the buffer size. As I have no buffer under runs my guess the web server is the bottleneck. How can I measure that? Filling the data buffer in the sinewave function takes about 8ms.

Is looping the way to "fill" the webserver with data?

Are there other ways to speed things up?

Code: Select all#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <CircularBuffer.h>

const char *ssid = "***";
const char *password = "***";

AsyncWebServer server(80);
const uint16_t buffer_size = 1024; //256 - 1024. maxLen = 1064 but seems worse than 1024.
CircularBuffer<uint8_t, buffer_size> audio_buffer;
uint8_t makenoise = 1;
uint32_t t;

struct audio_spec{
  float bitdepth = 16.0;
  float samplerate = 22050; //44100.0; 32000;
  uint16_t num_channels = 2;
  float tick = 1/samplerate;
  float twopitick = 2*PI*tick;
  float amplitude = pow(2, bitdepth)/2.0; //0dB mind your ears!
} audio_res;

struct wave_header_pcm{
  char      riff_tag[4]{'R','I','F','F'};
  uint32_t  riff_length{4294967295};//4294967295
  char      wave_tag[4]{'W','A','V','E'};
  char      format_tag[4]{'f','m','t',' '}; //mind the space!!
  uint32_t  format_length{16};
  uint16_t  format_code{1};
  uint16_t  num_channels;
  uint32_t  sample_rate;
  uint32_t  byte_rate;
  uint16_t  block_align;
  uint16_t  bit_depth;
  char      data_tag[4]{'d','a','t','a'};
  uint32_t  data_length{4294967295}; //4294967295 max length as we stream. Should resend the header when max is reached.
} header_pcm;

void fill_header(uint16_t bit_depth, uint32_t sample_rate, uint16_t num_channels){
  header_pcm.num_channels = num_channels;
  header_pcm.sample_rate = sample_rate;
  header_pcm.byte_rate = sample_rate*(bit_depth/8)*num_channels;
  header_pcm.block_align = bit_depth/8;
  header_pcm.bit_depth = bit_depth;
}

void sinewave(float frequency){
  static float f_twopitick = frequency*audio_res.twopitick;
  static uint32_t cnt = 0;
  while(makenoise){
    while(!audio_buffer.isFull()){ 
      int16_t amplitude = (audio_res.amplitude * sin(cnt*f_twopitick));
      audio_buffer.push((amplitude >> 8 & 0xff));
      audio_buffer.push((amplitude & 0xff));
      if(audio_res.num_channels == 2){
        audio_buffer.push((amplitude >> 8 & 0xff));
        audio_buffer.push((amplitude & 0xff));
      }
      cnt++; //takes ~8ms
    }
    while(audio_buffer.isFull()){
      //Serial.println("Buffer Full!");
      yield();
     // 2-5ms with every now and then a peak 1000+ and 3000+
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
  Serial.println("Connected to the WiFi network\nIP Address: ");
  Serial.println(WiFi.localIP());

  fill_header(audio_res.bitdepth, audio_res.samplerate, audio_res.num_channels);

  server.on("/sinewave", HTTP_GET, [](AsyncWebServerRequest * request)  {
    AsyncWebServerResponse* response = request->beginChunkedResponse(
      "audio/wave",
      [](uint8_t* buffer, size_t maxLen, size_t index){
        size_t len;
        // first chunk does not have the full maxLen available
        // http-headers(?) Send these plus the WAVE header first.
        // after that just the audio data
        if(index == 0){
          uint8_t* pcm_bytes = static_cast<uint8_t*>(static_cast<void*>(&header_pcm));
          for (size_t i = 0; i < sizeof(header_pcm); i++){
              buffer[i] = pcm_bytes[i];
          }
          len = (1064 - maxLen) + sizeof(header_pcm);
          return len;
        }
        else{
          //maxLen = 2048;
          size_t len = buffer_size;
          if (!audio_buffer.isEmpty()){
            for (size_t i = 0; i < len; i++){
              buffer[i] = audio_buffer.shift();
            }
          }
          else{
            while(audio_buffer.isEmpty()){
              Serial.println("Buffer Empty!");
            }       
          }
         return len;
        }
      }
    );
    response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    response->addHeader("Pragma", "no-cache");
    response->addHeader("Expires", "-1");
    request->send(response);
  });
  server.begin();
  Serial.println("HTTP server started");
  sinewave(250.0);
}

void loop() {
}

Re: Async webserver chunked audio 16bit@44.1kHz

PostPosted: Fri Feb 19, 2021 11:02 am
by ingooogni
Got it going, actually using the non async server. The main gain was in replacing the sine wave generator with a faster one. It actually has to be throttled as liquidsoap is pushed into buffer over runs. A few audio players I tested deal well with the input as is.

Question, how do I check whether a client is still connected? Currently when the client disconnects the ESP resets. Also I don't like the "while(1)" in the code and would like to replace it with a "connected check".


Code: Select all#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "***";
const char* password = "***";
ESP8266WebServer server(80);

const int16_t buff_size = 1024;
int16_t       audio_buffer[buff_size];
uint16_t      numsamples = 500;

struct audio_resolution{
  float   bitdepth = 16.0;
  float   samplerate = 44100; //44100.0;
  uint8_t num_channels = 2;
  float   tick = 1/samplerate;
  float   twopitick = 2*PI*tick;
  float   amplitude = pow(2, bitdepth)/2.0;
} audio_res;

struct wave_header_pcm{
  char      riff_tag[4]{'R','I','F','F'};
  uint32_t  riff_length{4294967295};//4294967295
  char      wave_tag[4]{'W','A','V','E'};
  char      format_tag[4]{'f','m','t',' '}; //mind the space!
  uint32_t  format_length{16};
  uint16_t  format_code{1};
  uint16_t  num_channels;
  uint32_t  sample_rate;
  uint32_t  byte_rate;
  uint16_t  block_align;
  uint16_t  bit_depth;
  char      data_tag[4]{'d','a','t','a'};
  uint32_t  data_length{4294967295}; //4294967295
} header_pcm;

void fill_header(uint16_t bit_depth, uint32_t sample_rate, uint16_t num_channels){
  header_pcm.num_channels = num_channels;
  header_pcm.sample_rate = sample_rate;
  header_pcm.byte_rate = sample_rate*(bit_depth/8)*num_channels;
  header_pcm.block_align = bit_depth/8;
  header_pcm.bit_depth = bit_depth;
}

void sin_sample(float frequency){
  //https://www.musicdsp.org/en/latest/Synthesis/10-fast-sine-and-cosine-calculation.html
  static const float c = (pow(2, audio_res.bitdepth)/2.0)-1;
  static int16_t sample;
  static float a = 2.f*sin(PI*frequency/audio_res.samplerate);
  static float s[2]{0.90, 0.f};
  uint16_t buff_cnt = 0;
  uint16_t samplecount=0;
  while(1){    //samplecount<numsamples){
    while(buff_cnt<buff_size){
      s[0] = s[0] - a*s[1];
      s[1] = s[1] + a*s[0];   
      sample = s[0]*c;
      audio_buffer[buff_cnt] = sample;
      buff_cnt++;
      audio_buffer[buff_cnt] = sample;
      buff_cnt++;
    }
    server.sendContent_P((char *)&audio_buffer, sizeof(audio_buffer));
    buff_cnt = 0;
    samplecount++;
  }
}

void handleRoot() {
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "-1");
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, "audio/wave", "");
  server.sendContent_P((char *)&header_pcm, sizeof(header_pcm));
  sin_sample(250.0);
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.hostname("testgeval");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
  Serial.println("Connected to the WiFi network\nIP Address: ");
  Serial.println(WiFi.localIP());
  fill_header(audio_res.bitdepth, audio_res.samplerate, audio_res.num_channels);
  server.begin();
  server.on("/", handleRoot);
}

void loop() {
    server.handleClient();
}