#include #include #include #include #include #include #include #include #include #include #define AUDIO_THREAD_STACK_SIZE 2048 #define AUDIO_THREAD_PRIORITY 5 #define AUDIO_PATH "/lfs/a" #define AUDIO_BLOCK_SIZE 8192 /* 512 Samples Stereo (16-bit) = 8192 Bytes */ #define AUDIO_BLOCK_COUNT 4 #define AUDIO_WORD_WIDTH 16 #define AUDIO_SAMPLE_RATE 16000 LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF); /* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */ #define BLOCK_DURATION_MS ((AUDIO_BLOCK_SIZE * 1000) / (AUDIO_SAMPLE_RATE * 2 * (AUDIO_WORD_WIDTH / 8))) #define MAX_WAIT_TIME_MS (3 * BLOCK_DURATION_MS) /* Slab für I2S. Keine weiteren Queues oder Threads nötig. */ K_MEM_SLAB_DEFINE(audio_slab, AUDIO_BLOCK_SIZE, AUDIO_BLOCK_COUNT, 4); /* Message Queue für Play-Kommandos (auf 10 erhöht für Aneinanderreihung) */ K_MSGQ_DEFINE(audio_play_msgq, 64, 10, 4); /* Startup-Sicherung */ K_SEM_DEFINE(audio_ready_sem, 0, 1); #define I2S_NODE DT_ALIAS(audio_i2s) #if !DT_NODE_EXISTS(I2S_NODE) #error "Audio I2S alias not defined in devicetree" #endif #define AUDIO_AMP_ENABLE_NODE DT_ALIAS(audio_amp_en) #if !DT_NODE_EXISTS(AUDIO_AMP_ENABLE_NODE) #error "Audio Amplifier Enable alias not defined in devicetree" #endif static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE); static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios); static volatile int current_volume = 8; static volatile bool abort_playback = false; static char next_random_filename[64] = {0}; static uint32_t audio_file_count = 0; static char cached_404_path[] = "/lfs/sys/404"; static struct k_mutex i2s_lock; static struct k_work audio_stop_work; static void audio_stop_work_handler(struct k_work *work) { ARG_UNUSED(work); k_mutex_lock(&i2s_lock, K_FOREVER); enum pm_device_state state; pm_device_state_get(i2s_dev, &state); if (state == PM_DEVICE_STATE_ACTIVE) { LOG_DBG("Triggering I2S DROP to stop ongoing transmission"); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); } k_mutex_unlock(&i2s_lock); } void i2s_suspend(void) { k_mutex_lock(&i2s_lock, K_FOREVER); // Sperren pm_device_action_run(i2s_dev, PM_DEVICE_ACTION_SUSPEND); k_mutex_unlock(&i2s_lock); // Freigeben } void i2s_resume(void) { k_mutex_lock(&i2s_lock, K_FOREVER); pm_device_action_run(i2s_dev, PM_DEVICE_ACTION_RESUME); k_mutex_unlock(&i2s_lock); } void audio_refresh_file_count(void) { static struct fs_dir_t dirp; static struct fs_dirent entry; uint32_t count = 0; fs_dir_t_init(&dirp); if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0) { audio_file_count = 0; return; } while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') { if (entry.type == FS_DIR_ENTRY_FILE) { count++; } } fs_pm_closedir(&dirp); audio_file_count = count; LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH); } static void wait_for_i2s_drain(void) { /* Maximale Wartezeit berechnen */ int64_t deadline = k_uptime_get() + (AUDIO_BLOCK_COUNT * BLOCK_DURATION_MS) + 100; while (k_mem_slab_num_free_get(&audio_slab) < AUDIO_BLOCK_COUNT) { if (k_uptime_get() >= deadline) { LOG_WRN("Timeout waiting for I2S drain, used slabs: %u", AUDIO_BLOCK_COUNT - k_mem_slab_num_free_get(&audio_slab)); break; } k_sleep(K_MSEC(10)); } } int get_random_file(char *out_filename, size_t max_len) { if (audio_file_count == 0) { /* Fallback auf System-Sound, wenn Ordner leer */ strncpy(out_filename, cached_404_path, max_len); return 0; } struct fs_dir_t dirp; struct fs_dirent entry; uint32_t target_index = k_cycle_get_32() % audio_file_count; uint32_t current_index = 0; fs_dir_t_init(&dirp); if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0) return -ENOENT; while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') { if (entry.type == FS_DIR_ENTRY_FILE) { if (current_index == target_index) { snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name); break; } current_index++; } } fs_pm_closedir(&dirp); return 0; } void audio_system_ready(void) { k_sem_give(&audio_ready_sem); } void audio_stop(void) { LOG_DBG("Playback abort requested"); abort_playback = true; k_msgq_purge(&audio_play_msgq); if (k_is_in_isr()) { LOG_DBG("audio_stop called from ISR, deferring I2S DROP"); } k_work_submit(&audio_stop_work); } void audio_play(const char *filename) { char buf[64] = {0}; if (filename != NULL) { strncpy(buf, filename, sizeof(buf) - 1); } if (k_msgq_put(&audio_play_msgq, &buf, K_NO_WAIT) < 0) { LOG_WRN("Audio queue full, dropping request"); } } void audio_thread(void *arg1, void *arg2, void *arg3) { LOG_DBG("Audio thread started"); k_sem_take(&audio_ready_sem, K_FOREVER); i2s_suspend(); /* Ersten zufälligen Dateinamen beim Booten vorab cachen */ get_random_file(next_random_filename, sizeof(next_random_filename)); char filename[64]; while (1) { if (k_msgq_get(&audio_play_msgq, &filename, K_FOREVER) == 0) { abort_playback = false; i2s_resume(); /* 2. Datei bestimmen (aus Cache oder synchron als Fallback) */ if (filename[0] == '\0') { if (next_random_filename[0] != '\0') { /* Cache Hit: Sofort in den lokalen Puffer übernehmen */ strncpy(filename, next_random_filename, sizeof(filename)); next_random_filename[0] = '\0'; /* Cache als 'leer' markieren */ } else { /* Cache Miss (z.B. bei extrem schnellem Dauerfeuer): Synchron suchen */ if (get_random_file(filename, sizeof(filename)) < 0) { LOG_ERR("No file found in %s", AUDIO_PATH); continue; } } } struct fs_file_t file; fs_file_t_init(&file); if (fs_pm_open(&file, filename, FS_O_READ) < 0) { LOG_ERR("Failed to open %s", filename); continue; } ssize_t file_size = fs_get_audio_data_len(&file); LOG_INF("Playing: %s", filename); io_status(true); bool trigger_started = false; int queued_blocks = 0; uint8_t factor = MIN(255, current_volume * 0xFF / 100); LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume); while (!abort_playback) { void *block; /* Self-Healing Timeout bei I2S-Hängern */ if (k_mem_slab_alloc(&audio_slab, &block, K_MSEC(MAX_WAIT_TIME_MS)) < 0) { LOG_ERR("I2S stall or slab timeout - resetting I2S"); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); audio_init(); // Setzt Hardware hart zurück break; } if (abort_playback) { k_mem_slab_free(&audio_slab, block); break; } ssize_t bytes_read = fs_read_audio(&file, block, AUDIO_BLOCK_SIZE / 2, file_size); if (bytes_read <= 0) { k_mem_slab_free(&audio_slab, block); break; } /* In-Place Konvertierung Mono -> Stereo */ int16_t *samples = (int16_t *)block; int samples_read = bytes_read / 2; for (int i = samples_read - 1; i >= 0; i--) { int16_t sample = (samples[i] * factor) >> 8; // Lautstärkeanpassung samples[i * 2] = sample; samples[i * 2 + 1] = sample; } /* Bei partiellem Block (Dateiende) den Rest mit Stille füllen */ if (bytes_read < (AUDIO_BLOCK_SIZE / 2)) { size_t valid_bytes = bytes_read * 2; memset((uint8_t *)block + valid_bytes, 0, AUDIO_BLOCK_SIZE - valid_bytes); } /* Block in die DMA-Queue schieben */ if (i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE) < 0) { k_mem_slab_free(&audio_slab, block); break; } /* HIER werden die Variablen verwendet: */ queued_blocks++; /* Regulärer Start: Erst wenn 2 Blöcke in der DMA-Queue liegen */ if (!trigger_started && queued_blocks >= 2) { i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); trigger_started = true; LOG_DBG("I2S transmission started after queuing %d blocks", queued_blocks); } /* Kurze Datei (kleiner als 1 Block): Sofort starten und Schleife verlassen */ if (bytes_read < (AUDIO_BLOCK_SIZE / 2)) { if (!trigger_started) { i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); trigger_started = true; LOG_DBG("I2S transmission started for short file (bytes read: %zd)", bytes_read); } break; } } if (abort_playback) { i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); LOG_DBG("Playback aborted via audio_stop()"); } else { if (k_msgq_num_used_get(&audio_play_msgq) > 0) { LOG_DBG("Play request pending, skipping DRAIN"); } else { LOG_DBG("Sample finished, starting DRAIN"); i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); wait_for_i2s_drain(); } } fs_pm_close(&file); if (k_msgq_num_used_get(&audio_play_msgq) == 0) { i2s_suspend(); io_status(false); } if (k_msgq_num_used_get(&audio_play_msgq) == 0 && next_random_filename[0] == '\0') { if (get_random_file(next_random_filename, sizeof(next_random_filename)) == 0) { LOG_DBG("Pre-cached next random file: %s", next_random_filename); } } } } } K_THREAD_DEFINE(audio_thread_id, AUDIO_THREAD_STACK_SIZE, audio_thread, NULL, NULL, NULL, AUDIO_THREAD_PRIORITY, 0, 0); int audio_init(void) { LOG_DBG("Initializing audio subsystem..."); if (!device_is_ready(i2s_dev)) return -ENODEV; struct i2s_config config = { .word_size = AUDIO_WORD_WIDTH, .channels = 2, .format = I2S_FMT_DATA_FORMAT_I2S, .options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER, .frame_clk_freq = AUDIO_SAMPLE_RATE, .mem_slab = &audio_slab, .block_size = AUDIO_BLOCK_SIZE, .timeout = SYS_FOREVER_MS, }; int ret = i2s_configure(i2s_dev, I2S_DIR_TX, &config); if (ret < 0) return ret; if (!gpio_is_ready_dt(&_en_dev)) { LOG_DBG("Amplifier enable GPIO device not ready"); return -ENODEV; } ret = gpio_pin_configure_dt(&_en_dev, GPIO_OUTPUT_ACTIVE); if (ret < 0) { LOG_ERR("Failed to configure amplifier enable GPIO: %d", ret); return ret; } gpio_pin_configure_dt(&_en_dev, 0); k_mutex_init(&i2s_lock); k_work_init(&audio_stop_work, audio_stop_work_handler); audio_refresh_file_count(); LOG_INF("Audio initialized: %u bits, %u.%03u kHz", config.word_size, config.frame_clk_freq / 1000, config.frame_clk_freq % 1000); return 0; }