#include "speaker.h" #include "state_tracking.h" static const char *TAG = "speaker"; static size_t audio_block_size = 4096; typedef struct { /// The path to the file being played. char* file_name; /// The handle to the file being played. FILE* file_handle; /// A number that all samples will be shifted right by. uint8_t prescaler; /// A flag for repeating. bool repeat; } playing_audio_clip_t; // It's hard to get the tx_chan's internal state, so instead, we keep track of it here. bool channel_enabled = false; i2s_chan_handle_t tx_chan; /// A queue of owned `char*` to be stopped from playing. /// /// the speaker driver system will free the given char* when it is done with it. QueueHandle_t stop_clip_queue; /// A queue of `audio_clip_t`s to be played once there is nothing being played QueueHandle_t play_clip_queue; /// The clips that are currently playing std::vector playing_clips; static bool push_clip(audio_clip_t clip) { ESP_LOGD(TAG, "playing %s", clip.file_name); FILE* fh = fopen(clip.file_name, "rb"); if (fh == NULL) { ESP_LOGW(TAG, "failed to open %s", clip.file_name); return false; } else { // skip the .wav header fseek(fh, 44, SEEK_SET); playing_audio_clip_t playing_clip = { .file_name = clip.file_name, .file_handle = fh, .prescaler = clip.prescaler, .repeat = clip.repeat, }; playing_clips.push_back(playing_clip); return true; } } /// @brief Disables the channel if neccesary. static void disable_channel() { if (channel_enabled) { ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_disable(tx_chan)); channel_enabled = false; } } /// @brief Enables the channel if neccesary static void enable_channel() { if (!channel_enabled) { ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_enable(tx_chan)); channel_enabled = true; } } static void speaker_task(void* arg) { audio_clip_t next_clip; char* clip_to_stop; int16_t* audio_buf = (int16_t*) malloc(audio_block_size * sizeof(int16_t)); int16_t* file_buf = (int16_t*) malloc(audio_block_size * sizeof(int16_t)); while (1) { // first, take all "play immediatly" clips from the queue. while (xQueuePeek(play_clip_queue, &next_clip, 0) == pdTRUE && next_clip.play_immediatly) { if (xQueueReceive(play_clip_queue, &next_clip, 0) == pdTRUE) { push_clip(next_clip); } } // handle any stop requests while (xQueueReceive(stop_clip_queue, &clip_to_stop, 0) == pdTRUE) { // delete clip from list bool found = false; for (int clip_idx = playing_clips.size()-1; clip_idx >= 0; clip_idx--) { playing_audio_clip_t& clip = playing_clips.at(clip_idx); if (strcmp(clip.file_name, clip_to_stop) == 0) { found = true; ESP_LOGV(TAG, "stopping %s", clip.file_name); fclose(clip.file_handle); free(clip.file_name); playing_clips.erase(playing_clips.begin() + clip_idx); } } if (!found) { ESP_LOGW(TAG, "Could not find clip \"%s\" to stop", clip_to_stop); } // free the string that was passed into the queue free(clip_to_stop); } // if we aren't playing any clips, wait for the next one. if (playing_clips.empty()) { if (xQueueReceive(play_clip_queue, &next_clip, 0) == pdTRUE) { push_clip(next_clip); } else { // we must wait before our next clip disable_channel(); // now wait for next clip if (xQueueReceive(play_clip_queue, &next_clip, portMAX_DELAY) == pdTRUE) { push_clip(next_clip); } } } // send 1 block std::memset(audio_buf, 0, audio_block_size * sizeof(int16_t)); for (int clip_idx = playing_clips.size()-1; clip_idx >= 0; clip_idx--) { playing_audio_clip_t& clip = playing_clips.at(clip_idx); size_t samples_read = fread(file_buf, sizeof(uint16_t), audio_block_size, clip.file_handle); if (samples_read == 0) { if (clip.repeat) { ESP_LOGV(TAG, "repeating %s", clip.file_name); fseek(clip.file_handle, 44, SEEK_SET); } else { ESP_LOGV(TAG, "finishing %s", clip.file_name); fclose(clip.file_handle); free(clip.file_name); playing_clips.erase(playing_clips.begin() + clip_idx); } continue; } for (int i = 0; i < samples_read; i++) { audio_buf[i] += (file_buf[i] >> clip.prescaler); } } size_t bytes_to_write = audio_block_size * sizeof(int16_t); size_t bytes_written; if (!channel_enabled) { ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_preload_data(tx_chan, audio_buf, bytes_to_write, &bytes_written)); enable_channel(); } else { ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_write(tx_chan, audio_buf, bytes_to_write, &bytes_written, portMAX_DELAY)); } if (bytes_written != bytes_to_write) { ESP_LOGW(TAG, "only %d bytes written to the i2s channel! (expected %d)", bytes_written, bytes_to_write); } } vTaskDelete(NULL); } bool play_clip_wav(const char* file_name, bool play_immediately, bool repeat, uint8_t prescaler, TickType_t ticks_to_wait) { // clone the passed in string to the heap. size_t len = strlen(file_name); char* dynamic_file_name = (char*) malloc(len+1); strcpy(dynamic_file_name, file_name); audio_clip_t clip = { .file_name = dynamic_file_name, .prescaler = prescaler, .repeat = repeat, .play_immediatly = play_immediately, }; if (is_state_tracking()) { char buf[64]; sprintf(buf, "%s:%d:%d:%d", file_name, play_immediately, repeat, prescaler); event_occured("PLAY_WAV", buf); } if (play_immediately) { return xQueueSendToFront(play_clip_queue, &clip, ticks_to_wait) == pdTRUE; } return xQueueSend(play_clip_queue, &clip, ticks_to_wait) == pdTRUE; } bool stop_clip(const char* file_name, TickType_t ticks_to_wait) { // clone the passed in string to the heap. size_t len = strlen(file_name); char* dynamic_file_name = (char*) malloc(len+1); strcpy(dynamic_file_name, file_name); if (is_state_tracking()) { event_occured("STOP_WAV", file_name); } return xQueueSend(stop_clip_queue, &dynamic_file_name, ticks_to_wait) == pdTRUE; } void init_speaker(void) { ESP_LOGI(TAG, "Initializing speaker..."); i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER); ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_chan, NULL)); i2s_std_config_t tx_std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = SPEAKER_PIN_BCLK, .ws = SPEAKER_PIN_WS, .dout = SPEAKER_PIN_DOUT, .din = GPIO_NUM_NC, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false, }, }, }; ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_chan, &tx_std_cfg)); i2s_chan_info_t info = {}; i2s_channel_get_info(tx_chan, &info); if (info.total_dma_buf_size != 0) { audio_block_size = info.total_dma_buf_size / sizeof(int16_t); } // init queues play_clip_queue = xQueueCreate(CLIP_QUEUE_SIZE, sizeof(audio_clip_t)); stop_clip_queue = xQueueCreate(CLIP_QUEUE_SIZE, sizeof(char*)); // start task xTaskCreate(speaker_task, "play_audio", 4096, NULL, 5, NULL); play_clip_wav(MOUNT_POINT "/startup.wav", true, false, 4, 0); ESP_LOGI(TAG, "Speaker initialized!"); }