This commit is contained in:
5
firmware/libs/audio/CMakeLists.txt
Normal file
5
firmware/libs/audio/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
if(CONFIG_AUDIO)
|
||||
zephyr_library()
|
||||
zephyr_sources(src/audio.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
76
firmware/libs/audio/Kconfig
Normal file
76
firmware/libs/audio/Kconfig
Normal 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
|
||||
10
firmware/libs/audio/include/audio.h
Normal file
10
firmware/libs/audio/include/audio.h
Normal 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
|
||||
|
||||
244
firmware/libs/audio/src/audio.c
Normal file
244
firmware/libs/audio/src/audio.c
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user