Added audio libs
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 13s

This commit is contained in:
2026-02-15 18:47:37 +01:00
parent 794d8e36c9
commit fae79ad8b0
23 changed files with 676 additions and 45 deletions

View File

@@ -0,0 +1,5 @@
if(CONFIG_AUDIO)
zephyr_library()
zephyr_sources(src/audio.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,76 @@
menuconfig AUDIO
bool "Audio Support"
depends on FS_MGMT
select I2S
select I2S_NRFX
help
Library for initializing and managing the audio subsystem.
if AUDIO
config AUDIO_DEFAULT_VOLUME
int "Default Audio Volume (0..255)"
default 128
range 0 255
help
Set the default audio volume level. 0 is silent, 255 is maximum volume. Default is 128 (50% volume).
config AUDIO_SAMPLE_RATE
int "Audio Sample Rate (Hz)"
default 16000
range 8000 48000
help
Set the audio sample rate in Hz. Common values are 8000, 16000, 44100, and 48000 Hz. Default is 16000 Hz.
config AUDIO_BIT_WIDTH
int "Audio Bit Depth"
default 16
range 8 32
help
Set the audio bit depth. Common values are 8, 16, 24, and 32 bits. Default is 16 bits.
config AUDIO_BLOCK_COUNT
int "Audio Block Count"
default 4
range 1 16
help
Set the number of audio blocks for buffering. More blocks can help with smoother audio but use more memory. Default is 4 blocks.
config AUDIO_BLOCK_SIZE
int "Audio Block Size (bytes)"
default 1024
range 256 4096
help
Set the size of each audio block in bytes. Larger blocks can reduce CPU overhead but increase latency. Default is 1024 bytes.
config AUDIO_THREAD_PRIORITY
int "Audio Thread Priority"
default 5
range 0 255
help
Set the thread priority for audio processing. Lower numbers indicate higher priority. Default is 5
config AUDIO_STACK_SIZE
int "Audio Thread Stack Size (bytes)"
default 2048
range 256 8192
help
Set the stack size for the audio processing thread in bytes. Default is 2048 bytes.
config AUDIO_SAMPLE_FOLDER
string "Audio Sample Folder"
default "a"
help
Set the folder path where audio sample files are stored. No leading or trailing slashes. Default is "a".
config AUDIO_MAX_PATH_LEN
int "Maximum Audio File Path Length"
default 16
range 8 128
help
Set the maximum length for audio file paths. Default is 16 characters.
# Logging configuration for the Audio module
module = AUDIO
module-str = audio
source "subsys/logging/Kconfig.template.log_config"
endif # AUDIO

View File

@@ -0,0 +1,10 @@
#ifndef AUDIO_H
#define AUDIO_H
int audio_init(void);
int audio_play_sound(const char* file);
int audio_play_file(const char* file);
void audio_stop(void);
#endif // AUDIO_H

View File

@@ -0,0 +1,244 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/i2s.h>
#include <zephyr/fs/fs.h>
#include <audio.h>
#include <string.h>
LOG_MODULE_REGISTER(audio, CONFIG_AUDIO_LOG_LEVEL);
/* Get the I2S device from the devicetree */
#define I2S_NODE DT_NODELABEL(i2s0)
static const struct device *i2s_dev = DEVICE_DT_GET(I2S_NODE);
/* Memory Slab for I2S DMA */
K_MEM_SLAB_DEFINE(audio_slab, CONFIG_AUDIO_BLOCK_SIZE, CONFIG_AUDIO_BLOCK_COUNT, 4);
/* Globals */
static volatile bool abort_playback = false;
static volatile uint8_t audio_volume = CONFIG_AUDIO_DEFAULT_VOLUME;
static void wait_for_i2s_drain(void)
{
const uint32_t frames_per_block = CONFIG_AUDIO_BLOCK_SIZE / 4;
const uint32_t block_ms = (frames_per_block * 1000U) / CONFIG_AUDIO_SAMPLE_RATE;
const uint32_t max_wait_ms = (block_ms + 1U) * CONFIG_AUDIO_BLOCK_COUNT + 5U;
int64_t deadline = k_uptime_get() + max_wait_ms;
while (k_mem_slab_num_free_get(&audio_slab) < CONFIG_AUDIO_BLOCK_COUNT)
{
if (k_uptime_get() >= deadline)
{
LOG_WRN("Timeout waiting for I2S drain");
break;
}
k_sleep(K_MSEC(1));
}
}
/* Message Queue: transfers the file path to the thread */
K_MSGQ_DEFINE(audio_msgq, CONFIG_AUDIO_MAX_PATH_LEN, 10, 4);
/* Audio thread function */
void audio_thread_fn(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
char file_path[CONFIG_AUDIO_MAX_PATH_LEN];
struct fs_file_t file;
fs_file_t_init(&file);
LOG_DBG("Audio thread started, priority %d", k_thread_priority_get(k_current_get()));
while (1)
{
bool trigger_started = false;
if (k_msgq_get(&audio_msgq, &file_path, K_FOREVER) == 0)
{
abort_playback = false;
if (fs_open(&file, file_path, FS_O_READ) < 0)
{
LOG_ERR("thread: could not open %s", file_path);
continue;
}
LOG_DBG("thread: preparing %s...", file_path);
uint32_t queued_blocks = 0;
while (!abort_playback)
{
void *mem_block;
if (k_mem_slab_alloc(&audio_slab, &mem_block, K_MSEC(10)) < 0)
continue;
int16_t *data_ptr = (int16_t *)mem_block;
const uint32_t max_mono_samples = CONFIG_AUDIO_BLOCK_SIZE / 4;
ssize_t bytes_read = fs_read(&file, data_ptr, max_mono_samples * sizeof(int16_t));
if (bytes_read <= 0)
{
k_mem_slab_free(&audio_slab, mem_block);
break;
}
uint32_t samples_read = bytes_read / sizeof(int16_t);
// Padding with zeros if we read less than a full block of mono samples
if (samples_read < max_mono_samples)
{
memset(&data_ptr[samples_read], 0, (max_mono_samples - samples_read) * sizeof(int16_t));
}
uint32_t *stereo_dst = (uint32_t *)mem_block;
for (int32_t i = max_mono_samples - 1; i >= 0; i--)
{
int32_t scaled = (int32_t)data_ptr[i] * audio_volume;
int16_t sample = (int16_t)(scaled >> 8);
stereo_dst[i] = ((uint16_t)sample << 16) | (uint16_t)sample;
}
if (i2s_write(i2s_dev, mem_block, CONFIG_AUDIO_BLOCK_SIZE) < 0)
{
k_mem_slab_free(&audio_slab, mem_block);
break;
}
queued_blocks++;
// We start playback only when 2 blocks are in the DMA queue to avoid underruns
if (!trigger_started && queued_blocks >= 2)
{
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
{
trigger_started = true;
LOG_DBG("thread: playback started.");
}
}
if (samples_read < max_mono_samples)
{
// Short sample: start with a single queued block so DRAIN can play it.
if (!trigger_started)
{
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
{
trigger_started = true;
LOG_DBG("thread: playback started (short sample).");
}
}
break;
}
}
if (abort_playback)
{
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
trigger_started = false;
LOG_DBG("thread: playback aborted.");
}
else
{
if (k_msgq_num_used_get(&audio_msgq) > 0)
{
LOG_DBG("thread: play request pending, not draining I2S to minimize latency...");
}
else
{
LOG_DBG("thread: sample finished, waiting for I2S to drain...");
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
trigger_started = false;
wait_for_i2s_drain();
LOG_DBG("thread: playback finished.");
}
}
fs_close(&file);
}
}
}
K_THREAD_DEFINE(audio_thread, CONFIG_AUDIO_STACK_SIZE, audio_thread_fn, NULL, NULL, NULL, CONFIG_AUDIO_THREAD_PRIORITY, 0, 0);
int audio_init(void)
{
LOG_DBG("Initializing audio subsystem...");
if (!device_is_ready(i2s_dev))
{
LOG_ERR("I2S device not ready");
return -ENODEV;
}
/* Initial configuration of the I2S peripheral */
struct i2s_config config = {
.word_size = CONFIG_AUDIO_BIT_WIDTH,
.channels = 2,
.format = I2S_FMT_DATA_FORMAT_I2S,
.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER,
.frame_clk_freq = CONFIG_AUDIO_SAMPLE_RATE,
.mem_slab = &audio_slab,
.block_size = CONFIG_AUDIO_BLOCK_SIZE,
.timeout = SYS_FOREVER_MS,
};
int ret = i2s_configure(i2s_dev, I2S_DIR_TX, &config);
if (ret < 0)
{
LOG_ERR("Failed to configure I2S: %d", ret);
return ret;
}
LOG_DBG("I2S driver initialized and configured for %d Hz", CONFIG_AUDIO_SAMPLE_RATE);
return 0;
}
int audio_play_file(const char *file)
{
if (file == NULL)
{
LOG_ERR("audio_play_file: file path is NULL");
return -EINVAL;
}
size_t len = strnlen(file, CONFIG_AUDIO_MAX_PATH_LEN);
if (len >= CONFIG_AUDIO_MAX_PATH_LEN)
{
LOG_ERR("audio_play_file: file path too long");
return -ENAMETOOLONG;
}
char path_item[CONFIG_AUDIO_MAX_PATH_LEN];
memcpy(path_item, file, len + 1);
if (k_msgq_put(&audio_msgq, path_item, K_NO_WAIT) < 0)
{
LOG_ERR("audio_play_file: message queue full");
return -EAGAIN;
}
LOG_DBG("Queued file for playback: %s", file);
return 0;
}
int audio_play_sound(const char *file)
{
char path[CONFIG_AUDIO_MAX_PATH_LEN];
snprintf(path, sizeof(path), "%s/%s/%s",
CONFIG_FS_MGMT_MOUNT_POINT,
CONFIG_AUDIO_SAMPLE_FOLDER,
file);
return audio_play_file(path);
}
void audio_stop(void)
{
abort_playback = true;
k_msgq_purge(&audio_msgq);
LOG_DBG("Playback stop requested, message queue purged");
}