#include #include #include #include #include "audio.h" #include "fs_mgmt.h" #include "event_mgmt.h" LOG_MODULE_REGISTER(audio, CONFIG_AUDIO_LOG_LEVEL); const struct device *i2s_dev = DEVICE_DT_GET(DT_ALIAS(i2s_audio)); K_MSGQ_DEFINE(audio_cmd_q, sizeof(struct audio_cmd_msg), 10, 4); K_SEM_DEFINE(audio_files_count_sem, 0, 1); K_MEM_SLAB_DEFINE(audio_cache_slab, CONFIG_AUDIO_CACHE_SLAB_SIZE, CONFIG_AUDIO_CACHE_SLAB_COUNT, 4); enum audio_thread_state_t { AUDIO_IDLE, AUDIO_PRECACHING, AUDIO_WAIT_FOR_CACHE, AUDIO_PLAYING, AUDIO_DRAINING, }; #define EV_PLAY_RANDOM BIT(0) #define EV_MSGQ_NOT_EMPTY BIT(1) #define EV_CACHE_READY BIT(2) #define EV_CACHE_DONE BIT(3) #define EV_STATE_STEP BIT(4) #define EV_ALL (EV_PLAY_RANDOM | EV_MSGQ_NOT_EMPTY | EV_CACHE_READY | EV_CACHE_DONE | EV_STATE_STEP) K_EVENT_DEFINE(audio_events); #define AUDIO_CACHE_EVT_START BIT(0) #define AUDIO_CACHE_EVT_STOP BIT(1) K_EVENT_DEFINE(audio_cache_event); struct audio_ctx_t { char next_file_name[CONFIG_FS_MGMT_MAX_PATH_LENGTH]; struct fs_file_t file; bool is_file_open; bool cache_ready_signaled; uint8_t cached_blocks; ssize_t audio_size; ssize_t cached_bytes; } audio_ctx; static struct i2s_config i2s_cfg = { .word_size = 16, .channels = 1, .format = I2S_FMT_DATA_FORMAT_I2S, .options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER, .frame_clk_freq = 16000, .mem_slab = &audio_cache_slab, .block_size = CONFIG_AUDIO_CACHE_SLAB_SIZE, .timeout = SYS_FOREVER_MS, }; K_MUTEX_DEFINE(audio_ctx_mutex); atomic_t thread_state = ATOMIC_INIT(0); atomic_t num_files = ATOMIC_INIT(0); static const char *audio_state_name(enum audio_thread_state_t state) { switch (state) { case AUDIO_IDLE: return "IDLE"; case AUDIO_PRECACHING: return "PRECACHING"; case AUDIO_WAIT_FOR_CACHE: return "WAIT_FOR_CACHE"; case AUDIO_PLAYING: return "PLAYING"; case AUDIO_DRAINING: return "DRAINING"; default: return "UNKNOWN"; } } static void audio_set_state(enum audio_thread_state_t new_state, const char *reason) { enum audio_thread_state_t old_state = atomic_get(&thread_state); if (old_state != new_state) { LOG_INF("Audio state %s -> %s (%s)", audio_state_name(old_state), audio_state_name(new_state), reason); } atomic_set(&thread_state, new_state); } int audio_queue_play(const char *filename, bool is_interrupt) { if (is_interrupt) { /* Keep hardware state changes inside audio_thread to avoid cross-thread races. */ k_msgq_purge(&audio_cmd_q); } if (strlen(filename) == 0) { return 0; } if (k_msgq_num_free_get(&audio_cmd_q) == 0) { LOG_ERR("Audio command queue is full, cannot enqueue new command"); return -ENOMEM; } struct audio_cmd_msg cmd; strncpy(cmd.filename, filename, sizeof(cmd.filename) - 1); cmd.filename[sizeof(cmd.filename) - 1] = '\0'; cmd.is_interrupt = is_interrupt; if (k_msgq_put(&audio_cmd_q, &cmd, K_FOREVER) != 0) { LOG_ERR("Failed to enqueue audio command"); return -EFAULT; } /* * Wake immediately for interrupts or when idle. * Non-interrupt commands during active playback are picked up after drain. */ if (is_interrupt || (atomic_get(&thread_state) == AUDIO_IDLE)) { k_event_set(&audio_events, EV_MSGQ_NOT_EMPTY); } LOG_INF("Enqueued audio command: filename='%s', is_interrupt=%d, queued=%u", cmd.filename, cmd.is_interrupt, (unsigned int)k_msgq_num_used_get(&audio_cmd_q)); return 0; } static int audio_select_random_to_buf(char *buf, size_t buf_size) { if (k_sem_take(&audio_files_count_sem, K_FOREVER) != 0) { return -EFAULT; } int count = atomic_get(&num_files); if (count == 0) { LOG_WRN("No audio files available, using fallback sound"); snprintf(buf, buf_size, "%s/%s", FS_SYSTEM_PATH, CONFIG_AUDIO_NO_SAMPLES_SAMPLE); k_sem_give(&audio_files_count_sem); return -ENOENT; } int random_index = k_cycle_get_32() % count; struct fs_dir_t dir; struct fs_dirent entry; int rc; bool found = false; fs_dir_t_init(&dir); rc = fs_mgmt_pm_opendir(&dir, FS_AUDIO_PATH); if (rc < 0) { LOG_ERR("Failed to open audio directory '%s': %d", FS_AUDIO_PATH, rc); k_sem_give(&audio_files_count_sem); return rc; } int current_index = 0; while (1) { rc = fs_readdir(&dir, &entry); if (rc < 0 || entry.name[0] == '\0') { break; } if (entry.type == FS_DIR_ENTRY_FILE) { if (current_index == random_index) { snprintf(buf, buf_size, "%s/%.*s", FS_AUDIO_PATH, (int)(buf_size - sizeof(FS_AUDIO_PATH) - 1U), entry.name); LOG_DBG("Selected random audio file: %s", buf); found = true; break; } current_index++; } } fs_mgmt_pm_closedir(&dir); k_sem_give(&audio_files_count_sem); return found ? 0 : -ENOENT; } void audio_refresh_files(void) { // Lokale Strukturen verwenden, um Reentrancy-Probleme zu vermeiden struct fs_dir_t dir; struct fs_dirent entry; int count = 0; int rc; k_sem_reset(&audio_files_count_sem); fs_dir_t_init(&dir); // Nutze deinen PM-Wrapper für den Flash-Zugriff rc = fs_mgmt_pm_opendir(&dir, FS_AUDIO_PATH); if (rc < 0) { LOG_ERR("Failed to open audio directory '%s': %d", FS_AUDIO_PATH, rc); atomic_set(&num_files, 0); return; } while (1) { rc = fs_readdir(&dir, &entry); if (rc < 0) { LOG_ERR("Directory read error: %d", rc); break; } if (entry.name[0] == '\0') { break; } if (entry.type == FS_DIR_ENTRY_FILE) { count++; } } fs_mgmt_pm_closedir(&dir); k_sem_give(&audio_files_count_sem); atomic_set(&num_files, count); LOG_INF("Audio refresh found %d file(s) in %s", count, FS_AUDIO_PATH); } static int audio_init(void) { if (!device_is_ready(i2s_dev)) { LOG_ERR("I2S device not ready"); return -ENODEV; } int rc = i2s_configure(i2s_dev, I2S_DIR_TX, &i2s_cfg); if (rc < 0) { LOG_ERR("Failed to configure I2S: %d", rc); return rc; } LOG_DBG("Audio module initialized"); return 0; } SYS_INIT(audio_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); void audio_start_random_playback(void) { LOG_DBG("audio_start_random_playback called"); k_event_set(&audio_events, EV_PLAY_RANDOM); } void audio_thread(void *arg1, void *arg2, void *arg3) { ARG_UNUSED(arg1); ARG_UNUSED(arg2); ARG_UNUSED(arg3); LOG_INF("Audio thread waiting for filesystem ready event"); k_event_wait(&event_mgmt_events, EVENT_MGMT_FS_READY, false, K_FOREVER); LOG_INF("Audio thread received filesystem ready event"); audio_refresh_files(); audio_set_state(AUDIO_IDLE, "filesystem ready"); while (1) { enum audio_thread_state_t state = atomic_get(&thread_state); k_timeout_t timeout = (state == AUDIO_DRAINING) ? K_MSEC(10) : K_FOREVER; uint32_t active_events = k_event_wait(&audio_events, EV_ALL, false, timeout); if (active_events & EV_STATE_STEP) { k_event_clear(&audio_events, EV_STATE_STEP); } /* * EV_PLAY_RANDOM: button press requests a random sound. * If idle, start it immediately. Otherwise queue it as a non-interrupt command * so it plays after the current sound finishes. */ if (active_events & EV_PLAY_RANDOM) { k_event_clear(&audio_events, EV_PLAY_RANDOM); char random_file[CONFIG_FS_MGMT_MAX_PATH_LENGTH]; if (audio_select_random_to_buf(random_file, sizeof(random_file)) == 0) { if (state == AUDIO_IDLE) { k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, random_file, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); audio_set_state(AUDIO_PRECACHING, "play random while idle"); k_event_set(&audio_events, EV_STATE_STEP); } else { struct audio_cmd_msg cmd; strncpy(cmd.filename, random_file, sizeof(cmd.filename) - 1); cmd.filename[sizeof(cmd.filename) - 1] = '\0'; cmd.is_interrupt = false; if (k_msgq_put(&audio_cmd_q, &cmd, K_NO_WAIT) != 0) { LOG_WRN("Random play queue full, discarding"); } else { LOG_INF("Random play queued: '%s'", random_file); } } } continue; } switch (state) { case AUDIO_IDLE: if (active_events & EV_MSGQ_NOT_EMPTY) { k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); struct audio_cmd_msg cmd; if (k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT) != 0) { LOG_WRN("EV_MSGQ_NOT_EMPTY set but queue empty"); break; } k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); LOG_INF("Dequeued command while idle: '%s'", cmd.filename); audio_set_state(AUDIO_PRECACHING, "command while idle"); k_event_set(&audio_events, EV_STATE_STEP); } break; case AUDIO_PRECACHING: LOG_INF("Precaching '%s'", audio_ctx.next_file_name); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_PREPARE); k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_START); audio_set_state(AUDIO_WAIT_FOR_CACHE, "cache start requested"); break; case AUDIO_WAIT_FOR_CACHE: if (active_events & EV_MSGQ_NOT_EMPTY) { k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); struct audio_cmd_msg cmd; if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt) { /* Interrupt while caching: flush and restart for the new file */ k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_PREPARE); k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT); k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); LOG_INF("Interrupt while caching, switching to '%s'", cmd.filename); audio_set_state(AUDIO_PRECACHING, "interrupt while caching"); k_event_set(&audio_events, EV_STATE_STEP); break; } /* Non-interrupt: already queued, will play after current */ } if (active_events & EV_CACHE_READY) { k_event_clear(&audio_events, EV_CACHE_READY); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); audio_set_state(AUDIO_PLAYING, "cache primed, starting"); LOG_INF("Playback started: '%s'", audio_ctx.next_file_name); } break; case AUDIO_PLAYING: if (active_events & EV_MSGQ_NOT_EMPTY) { k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); struct audio_cmd_msg cmd; if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt) { k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT); k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); LOG_INF("Interrupting playback with '%s'", cmd.filename); audio_set_state(AUDIO_PRECACHING, "interrupt during playback"); k_event_set(&audio_events, EV_STATE_STEP); } else { LOG_DBG("Non-interrupt command queued; will play after drain"); } break; } if (active_events & EV_CACHE_DONE) { k_event_clear(&audio_events, EV_CACHE_DONE); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); audio_set_state(AUDIO_DRAINING, "cache complete"); } break; case AUDIO_DRAINING: { if (active_events & EV_MSGQ_NOT_EMPTY) { k_event_clear(&audio_events, EV_MSGQ_NOT_EMPTY); struct audio_cmd_msg cmd; if (k_msgq_peek(&audio_cmd_q, &cmd) == 0 && cmd.is_interrupt) { k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT); k_event_set(&audio_cache_event, AUDIO_CACHE_EVT_STOP); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); LOG_INF("Interrupt during drain, switching to '%s'", cmd.filename); audio_set_state(AUDIO_PRECACHING, "interrupt during drain"); k_event_set(&audio_events, EV_STATE_STEP); break; } } if (k_mem_slab_num_free_get(&audio_cache_slab) == CONFIG_AUDIO_CACHE_SLAB_COUNT) { LOG_DBG("Drain complete"); if (k_msgq_num_used_get(&audio_cmd_q) > 0) { struct audio_cmd_msg cmd; k_msgq_get(&audio_cmd_q, &cmd, K_NO_WAIT); k_mutex_lock(&audio_ctx_mutex, K_FOREVER); strncpy(audio_ctx.next_file_name, cmd.filename, sizeof(audio_ctx.next_file_name) - 1); audio_ctx.next_file_name[sizeof(audio_ctx.next_file_name) - 1] = '\0'; k_mutex_unlock(&audio_ctx_mutex); LOG_INF("Drain complete, next: '%s'", cmd.filename); audio_set_state(AUDIO_PRECACHING, "drain complete, queued command"); k_event_set(&audio_events, EV_STATE_STEP); } else { audio_set_state(AUDIO_IDLE, "drain complete, no more commands"); } } break; } } } } K_THREAD_DEFINE(audio_thread_id, CONFIG_AUDIO_THREAD_STACK_SIZE, audio_thread, NULL, NULL, NULL, CONFIG_AUDIO_THREAD_PRIORITY, 0, 0); void audio_pump_thread(void *arg1, void *arg2, void *arg3) { ARG_UNUSED(arg1); ARG_UNUSED(arg2); ARG_UNUSED(arg3); void *mem_slab; while (1) { uint32_t events = k_event_wait(&audio_cache_event, AUDIO_CACHE_EVT_START | AUDIO_CACHE_EVT_STOP, false, K_FOREVER); if (events & AUDIO_CACHE_EVT_STOP) { k_mutex_lock(&audio_ctx_mutex, K_FOREVER); if (audio_ctx.is_file_open) { fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; } k_mutex_unlock(&audio_ctx_mutex); k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START | AUDIO_CACHE_EVT_STOP); continue; } int rc = k_mem_slab_alloc(&audio_cache_slab, &mem_slab, K_NO_WAIT); if (rc == -ENOMEM) { k_event_set(&audio_events, EV_CACHE_READY); rc = k_mem_slab_alloc(&audio_cache_slab, &mem_slab, K_FOREVER); if (k_event_wait(&audio_cache_event, AUDIO_CACHE_EVT_STOP, false, K_NO_WAIT)) { k_mem_slab_free(&audio_cache_slab, &mem_slab); continue; } } if (rc == 0) { bool signal_cache_ready = false; k_mutex_lock(&audio_ctx_mutex, K_FOREVER); if (!audio_ctx.is_file_open) { LOG_INF("Opening audio file '%s' for playback", audio_ctx.next_file_name); rc = fs_mgmt_pm_open(&audio_ctx.file, audio_ctx.next_file_name, FS_O_READ); if (rc < 0) { LOG_ERR("Failed to open audio file '%s': %d", audio_ctx.next_file_name, rc); k_mem_slab_free(&audio_cache_slab, &mem_slab); k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); continue; } audio_ctx.is_file_open = true; audio_ctx.cache_ready_signaled = false; audio_ctx.cached_blocks = 0; /* File length and cache position are initialized once per playback start. */ audio_ctx.audio_size = fs_mgmt_get_audio_data_len(&audio_ctx.file); audio_ctx.cached_bytes = 0; LOG_DBG("Audio file '%s' opened for caching, size: %d", audio_ctx.next_file_name, (int)audio_ctx.audio_size); } if (audio_ctx.audio_size <= 0) { LOG_ERR("Invalid audio size for '%s': %d", audio_ctx.next_file_name, (int)audio_ctx.audio_size); k_mem_slab_free(&audio_cache_slab, &mem_slab); fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); continue; } ssize_t remaining_bytes = audio_ctx.audio_size - audio_ctx.cached_bytes; if (remaining_bytes <= 0) { k_mem_slab_free(&audio_cache_slab, &mem_slab); fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); k_event_set(&audio_events, EV_CACHE_DONE); continue; } ssize_t bytes_to_read = MIN(CONFIG_AUDIO_CACHE_SLAB_SIZE, remaining_bytes); ssize_t bytes_read = fs_read(&audio_ctx.file, mem_slab, bytes_to_read); if (bytes_read <= 0) // <= 0, um EOF (0) und Fehler (< 0) abzufangen { if (bytes_read < 0) { LOG_ERR("Failed to read audio data: %d", (int)bytes_read); } k_mem_slab_free(&audio_cache_slab, &mem_slab); // EOF erreicht -> Datei schließen und START-Bit löschen fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); k_event_set(&audio_events, EV_CACHE_DONE); continue; } audio_ctx.cached_bytes += bytes_read; // LOG_DBG("Cached %u%% ", (int)((audio_ctx.cached_bytes * 100) / audio_ctx.audio_size)); if (bytes_read > CONFIG_AUDIO_CACHE_SLAB_SIZE) { LOG_ERR("Read size %d exceeds slab size %d", (int)bytes_read, CONFIG_AUDIO_CACHE_SLAB_SIZE); k_mem_slab_free(&audio_cache_slab, &mem_slab); fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); continue; } if ((bytes_read & 0x1) != 0) { if (bytes_read >= (CONFIG_AUDIO_CACHE_SLAB_SIZE)) { LOG_ERR("Odd PCM byte count at slab boundary: %d", (int)bytes_read); k_mem_slab_free(&audio_cache_slab, &mem_slab); fs_close(&audio_ctx.file); audio_ctx.is_file_open = false; k_event_clear(&audio_cache_event, AUDIO_CACHE_EVT_START); k_mutex_unlock(&audio_ctx_mutex); continue; } ((uint8_t *)mem_slab)[bytes_read] = 0; bytes_read++; } if (bytes_read < CONFIG_AUDIO_CACHE_SLAB_SIZE) { memset((uint8_t *)mem_slab + bytes_read, 0, (CONFIG_AUDIO_CACHE_SLAB_SIZE) - bytes_read); bytes_read = CONFIG_AUDIO_CACHE_SLAB_SIZE; } audio_ctx.cached_blocks++; if (!audio_ctx.cache_ready_signaled && (audio_ctx.cached_blocks >= 2 || audio_ctx.cached_bytes >= audio_ctx.audio_size)) { audio_ctx.cache_ready_signaled = true; signal_cache_ready = true; } k_mutex_unlock(&audio_ctx_mutex); if (i2s_write(i2s_dev, mem_slab, CONFIG_AUDIO_CACHE_SLAB_SIZE) < 0) { LOG_ERR("Failed to write audio data to I2S"); k_mem_slab_free(&audio_cache_slab, &mem_slab); continue; } if (signal_cache_ready) { LOG_DBG("Cache ready for '%s' after %u block(s)", audio_ctx.next_file_name, (unsigned int)audio_ctx.cached_blocks); k_event_set(&audio_events, EV_CACHE_READY); } } } } K_THREAD_DEFINE(audio_cache_thread_id, CONFIG_AUDIO_PUMP_THREAD_STACK_SIZE, audio_pump_thread, NULL, NULL, NULL, CONFIG_AUDIO_PUMP_THREAD_PRIORITY, 0, 0);