add some code
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(audio-lyraT)
|
||||
spiffs_create_partition_image(storage spiffs FLASH_IN_PROJECT)
|
||||
@@ -0,0 +1,71 @@
|
||||
# ESP-DSP LyraT Board audio processing application
|
||||
|
||||
The demo applications are developed for the ESP32-LyraT development board and demonstrate the usage of IIR filters from the ESP-DSP library.
|
||||
This example showcases how to use IIR filter functionality to process audio stream data.
|
||||
To hear the audio please connect headphones or speakers to the ESP32-LyraT audio output.
|
||||
The example performs the following steps:
|
||||
|
||||
1. Read samples from a file
|
||||
* read samples from file
|
||||
* write audio samples to the triple buffer
|
||||
2. Process audio samples
|
||||
* process volume/bass/treble
|
||||
* apply digital limiter to the audio data
|
||||
* control audio buffer overflow
|
||||
3. Pass samples to the audio codec
|
||||
|
||||
To control the volume/bass/treble, please select the value by press 'Set' button and adjust the value by '+/-' buttons.
|
||||
|
||||
## The Triple Audio Buffer
|
||||
|
||||
In audion processing, it's possible to have situation when one task write data to the processing buffer (processing task), and another task read data from the same buffer, and still in reading process.
|
||||
Triple buffering is a technique used in audio processing to minimize latency and ensure smooth playback. It involves using three buffers to store audio data. Here's a description of how it works:
|
||||
|
||||
* Buffer A: This buffer holds the audio data that is currently being processed by the audio system. It is typically filled with samples from the audio source and processed in real-time.
|
||||
|
||||
* Buffer B: When Buffer A is full, the audio system begins reading from Buffer A and starts processing the data. At the same time, Buffer B is filled with new audio samples from the source.
|
||||
|
||||
* Buffer C: Once Buffer B is full, the audio system switches its attention to Buffer B and continues processing data. Buffer C is then filled with the latest audio samples.
|
||||
|
||||
The cycle continues, with the audio system always processing the data from a buffer while the other two buffers are being filled. This approach helps ensure a continuous and uninterrupted audio playback, as there is always a buffer ready to be processed. It reduces the chances of audio glitches or dropouts caused by delays in reading or processing the audio data.
|
||||
|
||||
Triple buffering is particularly useful when working with real-time audio processing applications, where low latency and uninterrupted playback are crucial.
|
||||
|
||||
## Audio Processing Flow
|
||||
|
||||
The audio processing flow contains next blocks:
|
||||
1. Audio processing task
|
||||
* Read data from file and store to the triple buffer
|
||||
* Read from triple buffer and concert data from int16_t to float
|
||||
* Process bass
|
||||
* Process treble
|
||||
* Process volume
|
||||
* Apply digital limiter
|
||||
* Convert data from float to int16_t and store to the triple buffer
|
||||
2. Audio output task
|
||||
* Write data from triple buffer to audio codec
|
||||
3. Buttons control task
|
||||
* React on buttons and adjust the control values
|
||||
* Calculates IIR filter coefficients
|
||||
|
||||
## How to use the example
|
||||
|
||||
### Hardware required
|
||||
|
||||
This example require LyraT development board.
|
||||
|
||||
### Configure the project
|
||||
|
||||
Under Component Config ---> DSP Library ---> DSP Optimization, it's possible to choose either the optimized or ANSI implementation, to compare them.
|
||||
|
||||
### Build and flash
|
||||
|
||||
Build the project and flash it to the board, then run monitor tool to view serial output (replace PORT with serial port name):
|
||||
|
||||
```
|
||||
idf.py flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(SRCS "audio_amp_main.c")
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <inttypes.h>
|
||||
#include <sdkconfig.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_dsp.h"
|
||||
#include "bsp/esp-bsp.h"
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
// Buffer for reading/writing to I2S driver. Same length as SPIFFS buffer and I2S buffer, for optimal read/write performance.
|
||||
// Recording audio data path:
|
||||
// I2S peripheral -> I2S buffer (DMA) -> App buffer (RAM) -> SPIFFS buffer -> External SPI Flash.
|
||||
// Vice versa for playback.
|
||||
#define BUFFER_SIZE (64)
|
||||
#define SAMPLE_RATE (16000) // For recording
|
||||
#define DEFAULT_VOLUME (100)
|
||||
|
||||
// Globals
|
||||
static const char *TAG = "example";
|
||||
static QueueHandle_t audio_button_q = NULL;
|
||||
|
||||
static void btn_handler(void *button_handle, void *usr_data)
|
||||
{
|
||||
int button_pressed = (int)usr_data;
|
||||
xQueueSend(audio_button_q, &button_pressed, 0);
|
||||
}
|
||||
|
||||
// Very simple WAV header, ignores most fields
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
uint8_t ignore_0[22];
|
||||
uint16_t num_channels;
|
||||
uint32_t sample_rate;
|
||||
uint8_t ignore_1[6];
|
||||
uint16_t bits_per_sample;
|
||||
uint8_t ignore_2[4];
|
||||
uint32_t data_size;
|
||||
uint8_t data[];
|
||||
} dumb_wav_header_t;
|
||||
|
||||
typedef enum audio_set {
|
||||
AUDIO_VOLUME,
|
||||
AUDIO_BASS,
|
||||
AUDIO_TREBLE,
|
||||
} audio_set_t;
|
||||
|
||||
static esp_codec_dev_handle_t spk_codec_dev = NULL;
|
||||
static FILE *play_file = NULL;
|
||||
// Pointer to a file that is going to be played
|
||||
static const char play_filename[] = BSP_SPIFFS_MOUNT_POINT"/16bit_mono_44_1_khz.wav";
|
||||
|
||||
// Definition for all tasks
|
||||
static void buttons_process_task(void *arg);
|
||||
static void audio_read_task(void *arg);
|
||||
static void audio_process_task(void *arg);
|
||||
|
||||
|
||||
// Wave file header
|
||||
static dumb_wav_header_t wav_header;
|
||||
static audio_set_t current_set = AUDIO_VOLUME;
|
||||
|
||||
// Data for IIR filters
|
||||
float iir_coeffs_lpf[5];
|
||||
float iir_w_lpf[5] = {0, 0};
|
||||
|
||||
float iir_coeffs_hpf[5];
|
||||
float iir_w_hpf[5] = {0, 0};
|
||||
|
||||
// IIR filters parameters
|
||||
// Parameters for low-pass filter (LPF)
|
||||
float lpf_gain = 0;
|
||||
float lpf_qFactor = 0.5;
|
||||
float lpf_freq = 0.01;
|
||||
|
||||
// Parameters for high-pass filter (HPF)
|
||||
float hpf_gain = 0;
|
||||
float hpf_qFactor = 1;
|
||||
float hpf_freq = 0.15;
|
||||
|
||||
// Volume control definitions
|
||||
float full_volume = 1;
|
||||
// Volume in dB
|
||||
int full_volume_db = -12;
|
||||
// Digital limiter envelope value
|
||||
float full_envelope = 0;
|
||||
|
||||
// processing audio buffer
|
||||
float processing_audio_buffer[BUFFER_SIZE] = {0};
|
||||
|
||||
// The triple_audio_buffer contains tree data arrays sizeof BUFFER_SIZE, for writing audio data to the codec
|
||||
int16_t triple_audio_buffer[3 * BUFFER_SIZE] = {0};
|
||||
// The write index shows the audio buffer that will be used to write data to the codec
|
||||
int audio_buffer_write_index = 0;
|
||||
// The read index shows the audio buffer that will be used to fill data from the data source to the processing buffer
|
||||
int audio_buffer_read_index = 0;
|
||||
|
||||
// Semaphore to synchronize the read and write
|
||||
static SemaphoreHandle_t sync_read_task;
|
||||
|
||||
// Convert input int16_t Q15 values array to the float values array
|
||||
static void convert_short2float(int16_t *int16_data, float *float_data, int len)
|
||||
{
|
||||
float multiplier = 1.0 / (float)(INT16_MAX + 1);
|
||||
for (int i = 0 ; i < len ; i++) {
|
||||
float_data[i] = (float)int16_data[i] * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert input float values to the int16_t Q15 values array
|
||||
static void convert_float2short(float *float_data, int16_t *int16_data, int len)
|
||||
{
|
||||
float multiplier = (float)(INT16_MAX + 1);
|
||||
for (int i = 0 ; i < len ; i++) {
|
||||
int16_data[i] = (int16_t)((float)multiplier * (float)float_data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// In the audio processing the valid output audio values for the floating point format should be in range +/- 1.0,
|
||||
// because these values will be converted to the 0x8000 and 0x7fff int16_t values, and will be accepted by the codec.
|
||||
// With additional amplification, for example bass, treble, and for other processing, it is possible that the audio
|
||||
// signal will reach the maximum values and will make an overflow at the DAC.
|
||||
// To avoid this situation the digital limiter analyze the output values and control the full amplification gain.
|
||||
//
|
||||
void digitalLimiter(float *input_signal, float *output_signal, int signal_length, float threshold, float attack_value, float release_value, float *in_envelope)
|
||||
{
|
||||
float envelope = *in_envelope;
|
||||
for (int i = 0; i < signal_length; i++) {
|
||||
// Calculate envelope
|
||||
float abs_input = fabsf(input_signal[i]);
|
||||
if (abs_input > envelope) {
|
||||
envelope = envelope * (1 - attack_value) + attack_value * abs_input;
|
||||
} else {
|
||||
envelope = envelope * (1 - release_value) + release_value * abs_input;
|
||||
}
|
||||
|
||||
// Apply compression
|
||||
if (envelope > threshold) {
|
||||
output_signal[i] = input_signal[i] * (threshold / envelope);
|
||||
} else {
|
||||
output_signal[i] = input_signal[i];
|
||||
}
|
||||
}
|
||||
*in_envelope = envelope;
|
||||
}
|
||||
|
||||
static void audio_process_task(void *arg)
|
||||
{
|
||||
// Init codeac and apply the initial volume to maximum
|
||||
spk_codec_dev = bsp_audio_codec_speaker_init();
|
||||
assert(spk_codec_dev);
|
||||
esp_codec_dev_set_out_vol(spk_codec_dev, DEFAULT_VOLUME);
|
||||
|
||||
// Open file and het the WAV header to set up the sample frequency, amount of channels and resolution
|
||||
play_file = fopen(play_filename, "rb");
|
||||
if (play_file == NULL) {
|
||||
ESP_LOGW(TAG, "%s file does not exist!", play_filename);
|
||||
}
|
||||
// Read WAV header
|
||||
if (fread((void *)&wav_header, 1, sizeof(wav_header), play_file) != sizeof(wav_header)) {
|
||||
ESP_LOGW(TAG, "Error in reading file");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Number of channels: %" PRIu16 "", wav_header.num_channels);
|
||||
ESP_LOGI(TAG, "Bits per sample: %" PRIu16 "", wav_header.bits_per_sample);
|
||||
ESP_LOGI(TAG, "Sample rate: %" PRIu32 "", wav_header.sample_rate);
|
||||
ESP_LOGI(TAG, "Data size: %" PRIu32 "", wav_header.data_size);
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.sample_rate = wav_header.sample_rate,
|
||||
.channel = wav_header.num_channels,
|
||||
.bits_per_sample = wav_header.bits_per_sample,
|
||||
};
|
||||
if (spk_codec_dev != NULL) {
|
||||
int result = esp_codec_dev_open(spk_codec_dev, &fs);
|
||||
}
|
||||
// Calculate initial volume value
|
||||
full_volume = exp10f((float)full_volume_db / 20);
|
||||
// Calculate initial state for LPF
|
||||
dsps_biquad_gen_lowShelf_f32(iir_coeffs_lpf, lpf_freq, lpf_gain, lpf_qFactor);
|
||||
// Calculate initial state for HPF
|
||||
dsps_biquad_gen_highShelf_f32(iir_coeffs_hpf, hpf_freq, hpf_gain, hpf_qFactor);
|
||||
|
||||
BaseType_t ret = xTaskCreate(buttons_process_task, "buttons_process_task", 4096, NULL, 4, NULL);
|
||||
assert(ret == pdPASS);
|
||||
|
||||
sync_read_task = xSemaphoreCreateCounting(1, 0);
|
||||
|
||||
ret = xTaskCreate(audio_read_task, "audio_read_task", 4096, NULL, 7, NULL);
|
||||
assert(ret == pdPASS);
|
||||
vTaskDelay(1);
|
||||
|
||||
ESP_LOGW(TAG, "To select volume/bass/treble please use the 'Set' button. And adjust the value with +/- buttons.");
|
||||
|
||||
for (;;) {
|
||||
/* Get data from SPIFFS and send it to codec */
|
||||
int16_t *wav_bytes = &triple_audio_buffer[audio_buffer_write_index * BUFFER_SIZE];
|
||||
// Write samples to audio codec
|
||||
esp_codec_dev_write(spk_codec_dev, wav_bytes, BUFFER_SIZE * sizeof(int16_t));
|
||||
audio_buffer_write_index++;
|
||||
if (audio_buffer_write_index >= 3) {
|
||||
audio_buffer_write_index = 0;
|
||||
}
|
||||
// Check the triple buffer overflow
|
||||
if (audio_buffer_write_index == audio_buffer_read_index) {
|
||||
// Call delay to switch the task to fill the buffer
|
||||
vTaskDelay(1);
|
||||
// Check and indicate overflow status
|
||||
if (audio_buffer_write_index == audio_buffer_read_index) {
|
||||
ESP_LOGW(TAG, "Audio buffer overflow!");
|
||||
}
|
||||
}
|
||||
// Generate synt event to read task:
|
||||
xSemaphoreGive(sync_read_task);
|
||||
}
|
||||
}
|
||||
|
||||
// The audio_read_task is responsible to read data from the file and fill it to the audio buffer.
|
||||
// The audio buffer is places inside of triple buffer, to avoid overflow situation in case if
|
||||
// the processing task busy for a while.
|
||||
static void audio_read_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
// Wait the sync semaphore
|
||||
if (xSemaphoreTake(sync_read_task, 100)) {
|
||||
// Get the pointer to the current audio buffer
|
||||
int16_t *wav_buffer = &triple_audio_buffer[audio_buffer_read_index * BUFFER_SIZE];
|
||||
// Read the data from the file
|
||||
uint32_t bytes_read_from_spiffs = fread(wav_buffer, sizeof(int16_t), BUFFER_SIZE, play_file);
|
||||
// Convert input samples from int16 to float for processing
|
||||
convert_short2float((int16_t *)wav_buffer, processing_audio_buffer, BUFFER_SIZE);
|
||||
// Apply bass
|
||||
dsps_biquad_f32(processing_audio_buffer, processing_audio_buffer, BUFFER_SIZE, iir_coeffs_lpf, iir_w_lpf);
|
||||
// Apply treble
|
||||
dsps_biquad_f32(processing_audio_buffer, processing_audio_buffer, BUFFER_SIZE, iir_coeffs_hpf, iir_w_hpf);
|
||||
// Apply voluve
|
||||
dsps_mulc_f32_ansi(processing_audio_buffer, processing_audio_buffer, BUFFER_SIZE, full_volume, 1, 1);
|
||||
// Apply limiter
|
||||
digitalLimiter(processing_audio_buffer, processing_audio_buffer, BUFFER_SIZE, 0.5, 0.5, 0.0001, &full_envelope);
|
||||
// Convert from float to int16 for audio codec
|
||||
convert_float2short(processing_audio_buffer, (int16_t *)wav_buffer, BUFFER_SIZE);
|
||||
|
||||
if (bytes_read_from_spiffs != BUFFER_SIZE) {
|
||||
// Rewind the file and read the WAV header
|
||||
rewind(play_file);
|
||||
fread((void *)&wav_header, 1, sizeof(wav_header), play_file);
|
||||
// Read data to the audio buffer
|
||||
bytes_read_from_spiffs = fread(wav_buffer, sizeof(int16_t), BUFFER_SIZE, play_file);
|
||||
}
|
||||
audio_buffer_read_index++;
|
||||
if (audio_buffer_read_index >= 3) {
|
||||
audio_buffer_read_index = 0;
|
||||
}
|
||||
} else {
|
||||
// Error in case of timeout
|
||||
ESP_LOGE(TAG, "Audio timeout!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void buttons_process_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
int btn_index = 0;
|
||||
if (xQueueReceive(audio_button_q, &btn_index, portMAX_DELAY) == pdTRUE) {
|
||||
switch (btn_index) {
|
||||
case BSP_BUTTON_SET: {
|
||||
current_set += 1;
|
||||
if (current_set > 2) {
|
||||
current_set = AUDIO_VOLUME;
|
||||
}
|
||||
switch (current_set) {
|
||||
case AUDIO_VOLUME:
|
||||
ESP_LOGW(TAG, "Select volume");
|
||||
break;
|
||||
case AUDIO_BASS:
|
||||
ESP_LOGW(TAG, "Select bass");
|
||||
break;
|
||||
case AUDIO_TREBLE:
|
||||
ESP_LOGW(TAG, "Select treble");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BSP_BUTTON_VOLDOWN: {
|
||||
switch (current_set) {
|
||||
case AUDIO_VOLUME:
|
||||
full_volume_db -= 3;
|
||||
if (full_volume_db < -36) {
|
||||
full_volume_db = -36;
|
||||
}
|
||||
full_volume = exp10f((float)full_volume_db / 20);
|
||||
ESP_LOGI(TAG, "Volume Down: %i dB", full_volume_db);
|
||||
break;
|
||||
case AUDIO_BASS:
|
||||
lpf_gain -= 1;
|
||||
if (lpf_gain < -12) {
|
||||
lpf_gain = -12;
|
||||
}
|
||||
ESP_LOGI(TAG, "Bass Down: %i", (int)lpf_gain);
|
||||
dsps_biquad_gen_lowShelf_f32(iir_coeffs_lpf, lpf_freq, lpf_gain, lpf_qFactor);
|
||||
break;
|
||||
case AUDIO_TREBLE:
|
||||
hpf_gain -= 1;
|
||||
if (hpf_gain < -12) {
|
||||
hpf_gain = -12;
|
||||
}
|
||||
ESP_LOGI(TAG, "Treble Down: %i", (int)hpf_gain);
|
||||
dsps_biquad_gen_highShelf_f32(iir_coeffs_hpf, hpf_freq, hpf_gain, hpf_qFactor);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BSP_BUTTON_VOLUP: {
|
||||
switch (current_set) {
|
||||
case AUDIO_VOLUME:
|
||||
full_volume_db += 3;
|
||||
if (full_volume_db > 0) {
|
||||
full_volume_db = 0;
|
||||
}
|
||||
full_volume = exp10f((float)full_volume_db / 20);
|
||||
ESP_LOGI(TAG, "Volume Up: %i dB", full_volume_db);
|
||||
break;
|
||||
case AUDIO_BASS:
|
||||
lpf_gain += 1;
|
||||
if (lpf_gain > 12) {
|
||||
lpf_gain = 12;
|
||||
}
|
||||
ESP_LOGI(TAG, "Bass Up: %i", (int)lpf_gain);
|
||||
dsps_biquad_gen_lowShelf_f32(iir_coeffs_lpf, lpf_freq, lpf_gain, lpf_qFactor);
|
||||
break;
|
||||
case AUDIO_TREBLE:
|
||||
hpf_gain += 1;
|
||||
if (hpf_gain > 12) {
|
||||
hpf_gain = 12;
|
||||
}
|
||||
ESP_LOGI(TAG, "Treble Up: %i", (int)hpf_gain);
|
||||
dsps_biquad_gen_highShelf_f32(iir_coeffs_hpf, hpf_freq, hpf_gain, hpf_qFactor);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGI(TAG, "No function for this button");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(bsp_spiffs_mount());
|
||||
|
||||
// Create FreeRTOS tasks and queues
|
||||
audio_button_q = xQueueCreate(10, sizeof(int));
|
||||
assert (audio_button_q != NULL);
|
||||
|
||||
BaseType_t ret = xTaskCreate(audio_process_task, "audio_process_task", 4096, NULL, 6, NULL);
|
||||
assert(ret == pdPASS);
|
||||
|
||||
// Init audio buttons
|
||||
button_handle_t btns[BSP_BUTTON_NUM];
|
||||
ESP_ERROR_CHECK(bsp_iot_button_create(btns, NULL, BSP_BUTTON_NUM));
|
||||
for (int i = 0; i < BSP_BUTTON_NUM; i++) {
|
||||
ESP_ERROR_CHECK(iot_button_register_cb(btns[i], BUTTON_PRESS_DOWN, btn_handler, (void *) i));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
espressif/esp32_lyrat: "^1.0.0"
|
||||
espressif/esp-dsp:
|
||||
version: '*'
|
||||
override_path: "../../../../esp-dsp"
|
||||
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
factory, app, factory, 0x10000, 1M,
|
||||
storage, data, spiffs, , 0x2f0000,
|
||||
|
@@ -0,0 +1,8 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_USE_MEMMAP=y
|
||||
CONFIG_SPIFFS_PAGE_SIZE=1024
|
||||
Binary file not shown.
Reference in New Issue
Block a user