This commit is contained in:
2026-02-26 14:08:17 +01:00
parent d48bc33530
commit b8b3e6ea44
9 changed files with 362 additions and 134 deletions

View File

@@ -2,19 +2,24 @@
#include <zephyr/logging/log.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2s.h>
#include <zephyr/pm/device.h>
#include <string.h>
#include <audio.h>
#include <fs.h>
#include <io.h>
LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF);
LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG);
/* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */
#define BLOCK_DURATION_MS 64
#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 (Pfade zu Dateien, max 64 Zeichen) */
K_MSGQ_DEFINE(audio_play_msgq, 64, 4, 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);
@@ -25,48 +30,84 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1);
#endif
static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE);
static volatile bool abort_playback = false;
static char next_random_filename[64] = {0};
int get_random_file(const char *path, char *out_filename, size_t max_len)
static uint32_t audio_file_count = 0;
static char cached_404_path[] = "/lfs/sys/404";
void audio_refresh_file_count(void)
{
struct fs_dir_t dirp;
struct fs_dirent entry;
int file_count = 0;
int rc;
uint32_t count = 0;
fs_dir_t_init(&dirp);
rc = fs_opendir(&dirp, path);
if (rc < 0)
return rc;
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
{
if (entry.type == FS_DIR_ENTRY_FILE)
file_count++;
audio_file_count = 0;
return;
}
fs_closedir(&dirp);
if (file_count == 0)
return -ENOENT;
uint32_t random_index = k_cycle_get_32() % file_count;
rc = fs_opendir(&dirp, path);
if (rc < 0)
return rc;
int current_index = 0;
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
{
if (entry.type == FS_DIR_ENTRY_FILE)
{
if (current_index == random_index)
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 (8 Blöcke * 64ms = 512ms + Toleranz) */
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");
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", path, entry.name);
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
break;
}
current_index++;
}
}
fs_closedir(&dirp);
fs_pm_closedir(&dirp);
return 0;
}
@@ -75,115 +116,189 @@ 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);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
}
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);
/* Ersten zufälligen Dateinamen beim Booten vorab cachen */
get_random_file(next_random_filename, sizeof(next_random_filename));
char filename[64];
while (1)
{
/* 1. Auf Play-Kommando warten */
k_msgq_get(&audio_play_msgq, &filename, K_FOREVER);
/* Sicherstellen, dass die I2S-Hardware nach einem vorherigen DRAIN
oder bei extrem schnellem Neudrücken garantiert gestoppt und leer ist. */
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
/* 2. Datei bestimmen */
if (filename[0] == '\0')
if (k_msgq_get(&audio_play_msgq, &filename, K_FOREVER) == 0)
{
if (get_random_file(AUDIO_PATH, filename, sizeof(filename)) < 0)
fs_pm_flash_resume();
abort_playback = false;
/* 2. Datei bestimmen (aus Cache oder synchron als Fallback) */
if (filename[0] == '\0')
{
LOG_ERR("No file found in %s", AUDIO_PATH);
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_open(&file, filename, FS_O_READ) < 0)
{
LOG_ERR("Failed to open %s", filename);
continue;
}
}
struct fs_file_t file;
fs_file_t_init(&file);
if (fs_open(&file, filename, FS_O_READ) < 0)
{
LOG_ERR("Failed to open %s", filename);
continue;
}
LOG_INF("Playing: %s", filename);
io_status(true);
LOG_INF("Playing: %s", filename);
io_status(true);
bool i2s_started = false;
bool aborted = false;
bool trigger_started = false;
int queued_blocks = 0;
/* 3. Synchrone Lese- und Abspiel-Schleife */
while (1)
{
/* WICHTIG: Prüfen, ob während des Abspielens ein neues Kommando in die Queue gelegt wurde */
if (k_msgq_num_used_get(&audio_play_msgq) > 0)
while (!abort_playback)
{
LOG_DBG("New play request received, aborting current playback");
aborted = true;
break;
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(&file, block, AUDIO_BLOCK_SIZE / 2);
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];
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;
}
}
void *block;
if (k_mem_slab_alloc(&audio_slab, &block, K_FOREVER) != 0)
if (abort_playback)
{
break;
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();
}
}
ssize_t bytes_read = fs_read(&file, block, AUDIO_BLOCK_SIZE / 2);
if (bytes_read <= 0)
fs_close(&file);
fs_pm_flash_suspend();
if (k_msgq_num_used_get(&audio_play_msgq) == 0)
{
k_mem_slab_free(&audio_slab, &block);
break; /* EOF oder Fehler */
io_status(false);
}
/* 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--)
if (k_msgq_num_used_get(&audio_play_msgq) == 0 && next_random_filename[0] == '\0')
{
int16_t sample = samples[i];
samples[i * 2] = sample;
samples[i * 2 + 1] = sample;
}
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);
}
if (i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE) < 0)
{
k_mem_slab_free(&audio_slab, &block);
break;
}
if (!i2s_started)
{
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START);
i2s_started = true;
if (get_random_file(next_random_filename, sizeof(next_random_filename)) == 0)
{
LOG_DBG("Pre-cached next random file: %s", next_random_filename);
}
}
}
/* 4. Aufräumen */
if (aborted)
{
/* Hart abbrechen, Puffer sofort verwerfen */
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
}
else
{
/* Sauber ausklingen lassen, bis der letzte I2S-Puffer leer ist */
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
}
fs_close(&file);
io_status(false);
LOG_DBG("Playback finished or aborted");
}
}
@@ -210,16 +325,7 @@ int audio_init(void)
if (ret < 0)
return ret;
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;
}
void audio_play(const char *filename)
{
char buf[64] = {0};
if (filename != NULL)
{
strncpy(buf, filename, sizeof(buf) - 1);
}
k_msgq_put(&audio_play_msgq, &buf, K_NO_WAIT);
}