228 lines
6.8 KiB
C++
228 lines
6.8 KiB
C++
#include "speaker.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_audio_clip_t> 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_immediatly, 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_immediatly,
|
|
};
|
|
|
|
if (play_immediatly) {
|
|
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);
|
|
|
|
return xQueueSend(play_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, 3, 0);
|
|
|
|
ESP_LOGI(TAG, "Speaker initialized!");
|
|
}
|