2024A-SW/components/Audio/I2S_Audio.c
2025-01-25 14:04:42 -06:00

462 lines
No EOL
13 KiB
C

#include <SystemK.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <driver/i2s_std.h>
#include <esp_spiffs.h>
#include <audio_player.h>
// I2S Configuration
#define I2S_DOUT GPIO_NUM_11
#define I2S_BCLK GPIO_NUM_12
#define I2S_LRC GPIO_NUM_13
#define I2S_DIN I2S_GPIO_UNUSED
#define I2S_SCLK I2S_GPIO_UNUSED
#define AUDIO_TASK_STACK_SIZE 4096
static StackType_t xStack[AUDIO_TASK_STACK_SIZE];
static StaticTask_t xTaskBuffer;
static QueueHandle_t xQueueAudio;
static TaskHandle_t xAudioTaskHandle;
void I2SAudioTask(void *pvParameters);
static const char *TAG = "I2S Audio";
static i2s_chan_handle_t tx_handle;
#define I2S_GPIO_CFG \
{ \
.mclk = I2S_SCLK, \
.bclk = I2S_BCLK, \
.ws = I2S_LRC, \
.dout = I2S_DOUT, \
.din = I2S_DIN, \
.invert_flags = { \
.mclk_inv = false, \
.bclk_inv = false, \
.ws_inv = false, \
}, \
}
#define I2S_DUPLEX_STEREO_CFG(_sample_rate) \
{ \
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sample_rate), \
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), \
.gpio_cfg = I2S_GPIO_CFG, \
}
static esp_err_t I2S_Audio_Set_Mute(AUDIO_PLAYER_MUTE_SETTING setting)
{
if (setting == AUDIO_PLAYER_MUTE)
{
KLOG_DEBUG(TAG, "Muted.");
}
else
{
KLOG_DEBUG(TAG, "Muted.");
}
return ESP_OK;
}
static esp_err_t I2S_Audio_Write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = i2s_channel_write(tx_handle, (char *)audio_buffer, len, bytes_written, timeout_ms);
return ret;
}
static esp_err_t I2S_Audio_Reconfigure_Clock(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
KLOG_INFO(TAG, "Reconfiguring clock for %lu sps, %lu bps, %d channels.", rate, bits_cfg, ch);
esp_err_t ret = ESP_OK;
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bits_cfg, (i2s_slot_mode_t)ch),
.gpio_cfg = I2S_GPIO_CFG};
ret |= i2s_channel_disable(tx_handle);
ret |= i2s_channel_reconfig_std_clock(tx_handle, &std_cfg.clk_cfg);
ret |= i2s_channel_reconfig_std_slot(tx_handle, &std_cfg.slot_cfg);
ret |= i2s_channel_enable(tx_handle);
return ret;
}
void Initialize_Audio(void)
{
KLOG_INFO(TAG, "Initializing I2S Audio...");
// Setup a standard configuration and the channel.
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer.
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
// Setup the I2S configuration.
i2s_std_config_t std_cfg = I2S_DUPLEX_STEREO_CFG(44100);
esp_err_t ret = i2s_channel_init_std_mode(tx_handle, &std_cfg);
if (ret != ESP_OK)
{
if (ret == ESP_ERR_NO_MEM)
{
KLOG_ERROR(TAG, "No memory for storing the channel information");
}
else if (ret == ESP_ERR_INVALID_ARG)
{
KLOG_ERROR(TAG, "NULL pointer or invalid configuration");
}
else if (ret == ESP_ERR_INVALID_STATE)
{
KLOG_ERROR(TAG, "This channel is not registered");
}
else
{
KLOG_ERROR(TAG, "Failed to initialize I2S Audio (%s)", esp_err_to_name(ret));
}
}
i2s_channel_enable(tx_handle);
xQueueAudio = xQueueCreate(5, sizeof(AudioAction_T));
if (xQueueAudio == NULL)
{
KLOG_ERROR(TAG, "Couldn't create the I2S Audio queue.");
}
audio_player_config_t config = {.mute_fn = I2S_Audio_Set_Mute,
.write_fn = I2S_Audio_Write,
.clk_set_fn = I2S_Audio_Reconfigure_Clock,
.priority = 10,
.coreID = APP_CPU_NUM};
ret = audio_player_new(config);
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Couldn't create the audio player.");
}
xAudioTaskHandle = xTaskCreateStaticPinnedToCore(
I2SAudioTask, // Function that implements the task.
"I2S Audio", // Text name for the task.
AUDIO_TASK_STACK_SIZE, // Stack size in words, not bytes.
NULL, // Parameter passed into the task.
1, // Priority at which the task is created.
xStack, // Array to use as the task's stack.
&xTaskBuffer, // Variable to hold the task's data structure.
APP_CPU_NUM); // Core where the task should run.
if (xAudioTaskHandle == NULL)
{
KLOG_ERROR(TAG, "Couldn't create the I2S Audio task.");
}
}
esp_err_t Play_Audio_File(const char *filename)
{
KLOG_INFO(TAG, "Attempting to play file: %s", filename);
KLOG_DEBUG(TAG, "Free heap: %ld bytes", esp_get_free_heap_size());
FILE *fh = fopen(filename, "rb");
if (fh == NULL)
{
KLOG_ERROR(TAG, "Failed to open audio file %s! Error: %s", filename, strerror(errno));
return ESP_ERR_NOT_FOUND;
}
fseek(fh, 0, SEEK_END);
long file_size = ftell(fh);
fseek(fh, 0, SEEK_SET);
KLOG_DEBUG(TAG, "File size: %ld bytes", file_size);
esp_err_t ret = audio_player_play(fh);
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Couldn't play audio file %s! Error: %d", filename, ret);
fclose(fh);
return ret;
}
return ESP_OK;
}
SystemKResult_T Perform_Audio_Action(AudioAction_T *action)
{
if (xQueueSend(xQueueAudio, action, 0) == pdTRUE)
{
return SYSTEMK_RESULT_SUCCESS;
}
else
{
return SYSTEMK_RESULT_QUEUE_IS_FULL;
}
}
char *concat_path(const char *directory_path, const char *filename)
{
size_t needed = snprintf(NULL, 0, "%s/%s", directory_path, filename) + 1;
char *full_path = malloc(needed);
if (full_path != NULL)
{
snprintf(full_path, needed, "%s/%s", directory_path, filename);
}
return full_path;
}
char *find_filename(const char *directory_path, const char *prefix)
{
DIR *dir = opendir(directory_path);
if (dir == NULL)
{
KLOG_ERROR(TAG, "Failed to open directory '%s': %s!", directory_path, strerror(errno));
return NULL;
}
struct dirent *entry;
char *filename = NULL;
while ((entry = readdir(dir)) != NULL)
{
if (entry->d_type == DT_REG && strncmp(entry->d_name, prefix, strlen(prefix)) == 0)
{
filename = strdup(entry->d_name);
if (filename == NULL)
{
KLOG_ERROR(TAG, "Memory allocation failed!");
}
else
{
KLOG_DEBUG(TAG, "Found matching file: %s", filename);
}
break;
}
}
closedir(dir);
if (filename == NULL)
{
KLOG_WARN(TAG, "No file beginning with '%s' found in directory '%s'!", prefix, directory_path);
}
return filename;
}
SystemKResult_T Play_Sound_By_Prefix(const char *prefix)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
const char *sounds_dir = "/usb/01";
char *filename = find_filename(sounds_dir, prefix);
if (filename != NULL)
{
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
Play_Audio_File(concat_path(sounds_dir, filename));
}
else
{
result = SYSTEMK_RESULT_FILE_NOT_FOUND;
}
return result;
}
SystemKResult_T Play_Number_Sound(uint8_t number)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
char number_str[4];
snprintf(number_str, 4, "%03u", number);
const char *sounds_dir = "/usb/10";
char *filename = find_filename(sounds_dir, (const char *)number_str);
if (filename != NULL)
{
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
Play_Audio_File(concat_path(sounds_dir, filename));
}
else
{
result = SYSTEMK_RESULT_FILE_NOT_FOUND;
}
return result;
}
void I2SAudioTask(void *pvParameters)
{
// Wait for system initialization.
// TODO: Make this closed-loop!
vTaskDelay(1000 / portTICK_PERIOD_MS);
while (true)
{
AudioAction_T action;
if (xQueueReceive(xQueueAudio, &action, portMAX_DELAY) == pdPASS)
{
switch (action.ID)
{
case AUDIO_SET_VOLUME:
break;
case AUDIO_SILENCE:
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
break;
case AUDIO_PLAY_STARTUP_SOUND:
Play_Sound_By_Prefix("001");
break;
case AUDIO_PLAY_SHOT_FIRED:
Play_Sound_By_Prefix("002");
break;
case AUDIO_PLAY_TAG_RECEIVED:
Play_Sound_By_Prefix("003");
break;
case AUDIO_PLAY_TAGGED_OUT:
Play_Sound_By_Prefix("004");
break;
case AUDIO_PLAY_MISFIRE:
Play_Sound_By_Prefix("005");
break;
case AUDIO_PRONOUNCE_NUMBER_0_TO_100:
uint8_t number = *((uint8_t *)action.Data);
if (number > 100)
{
number = 100;
}
else if (number == 0)
{
number = 101;
}
Play_Number_Sound(number);
break;
case AUDIO_PLAY_MENU_PROMPT:
Play_Sound_By_Prefix("006");
break;
case AUDIO_PLAY_SELECTION_INDICATOR:
Play_Sound_By_Prefix("007");
break;
case AUDIO_PLAY_HEALTH_REMAINING:
Play_Sound_By_Prefix("008");
break;
case AUDIO_PLAY_ELECTRONIC_DANCE_MUSIC:
Play_Sound_By_Prefix("009");
break;
case AUDIO_PLAY_GENERIC_ERROR:
Play_Sound_By_Prefix("010");
break;
case AUDIO_PLAY_VOLUME_PROMPT:
Play_Sound_By_Prefix("011");
break;
case AUDIO_PLAY_RIGHT_HANDED:
Play_Sound_By_Prefix("012");
break;
case AUDIO_PLAY_LEFT_HANDED:
Play_Sound_By_Prefix("013");
break;
case AUDIO_PLAY_GAME_ON:
Play_Sound_By_Prefix("014");
break;
case AUDIO_PLAY_HARDWARE_SETTINGS_PROMPT:
Play_Sound_By_Prefix("015");
break;
case AUDIO_PLAY_GAME_SETTINGS_PROMPT:
Play_Sound_By_Prefix("016");
break;
case AUDIO_PLAY_BONK:
Play_Sound_By_Prefix("017");
break;
case AUDIO_PLAY_NEAR_MISS:
Play_Sound_By_Prefix("018");
break;
case AUDIO_PLAY_PLAYER_ID_PROMPT:
Play_Sound_By_Prefix("019");
break;
case AUDIO_PLAY_TEAM_ID_PROMPT:
Play_Sound_By_Prefix("020");
break;
case AUDIO_PLAY_FRIENDLY_FIRE:
KLOG_WARN(TAG, "\"Friendly Fire\" audio is disabled in this build.");
//Play_Sound_By_Prefix("021");
break;
case AUDIO_PLAY_STARTING_THEME:
Play_Sound_By_Prefix("022");
break;
case AUDIO_PLAY_BOOP:
Play_Sound_By_Prefix("023");
break;
case AUDIO_PLAY_BEEP:
Play_Sound_By_Prefix("024");
break;
case AUDIO_PLAY_REPROGRAMMING:
Play_Sound_By_Prefix("025");
break;
case AUDIO_PLAY_BOMB:
Play_Sound_By_Prefix("026");
break;
case AUDIO_PLAY_GAME_OVER:
Play_Sound_By_Prefix("027");
break;
default:
Play_Audio_File("/spiffs/bad.wav");
break;
}
if (action.Play_To_Completion == true)
{
// Allow some time for the audio to start.
vTaskDelay(100 / portTICK_PERIOD_MS);
while (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE)
{
vTaskDelay(100 / portTICK_PERIOD_MS);
}
KEvent_T command_received_event = {.ID = KEVENT_AUDIO_COMPLETED, .Data = (void *)action.ID};
Post_KEvent(&command_received_event);
}
}
}
}