409 lines
12 KiB
C
409 lines
12 KiB
C
#include <zephyr/kernel.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/i2s.h>
|
|
#include <zephyr/pm/device.h>
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <string.h>
|
|
|
|
#include <audio.h>
|
|
#include <fs.h>
|
|
#include <io.h>
|
|
|
|
#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;
|
|
} |