#include #include #include #include #include #include #include #include #include // 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); } } } }