Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ projects:
idf_version: release-v5.5
target: esp32s3

- path: examples/audio/ns4150_player
idf_version: release-v5.5
target: esp32c5

- path: examples/audio/record_play
idf_version: release-v5.5
target: esp32s3
Expand Down
6 changes: 6 additions & 0 deletions examples/audio/ns4150_player/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ns4150_player)
4 changes: 4 additions & 0 deletions examples/audio/ns4150_player/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
idf_component_register(SRCS "ns4150_player.c"
INCLUDE_DIRS ".")

spiffs_create_partition_image(storage ../spiffs FLASH_IN_PROJECT)
42 changes: 42 additions & 0 deletions examples/audio/ns4150_player/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# NS4150 Player pin and audio configuration

menu "NS4150 Player Configuration"

config NS4150_PDM_P_GPIO
int "PDM P (data) GPIO"
default 7
range 0 48
help
GPIO connected to NS4150 PDM data input (P side).

config NS4150_USE_PDM_N
bool "Enable PDM N output (differential)"
default y
help
Enable inverted PDM N output for differential drive.
Disable if your board uses single-ended output only (PDM N not connected).

config NS4150_PDM_N_GPIO
int "PDM N GPIO"
default 8
depends on NS4150_USE_PDM_N
range 0 48
help
GPIO for NS4150 PDM N (inverted output). Ignored when "Enable PDM N" is off.

config NS4150_USE_PA_CTL
bool "Enable PA control GPIO"
default y
help
Enable GPIO to control amplifier power/shutdown.
Disable if PA is always on or not controlled by MCU.

config NS4150_PA_CTL_GPIO
int "PA control GPIO"
default 1
depends on NS4150_USE_PA_CTL
range 0 48
help
GPIO for amplifier enable (high = on).

endmenu
18 changes: 18 additions & 0 deletions examples/audio/ns4150_player/main/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/esp_codec_dev: ==1.5.4
chmorgan/esp-audio-player: ^1.1.0
271 changes: 271 additions & 0 deletions examples/audio/ns4150_player/main/ns4150_player.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "driver/i2s_pdm.h"
#include "esp_codec_dev.h"
#include "esp_codec_dev_defaults.h"
#include "soc/gpio_sig_map.h"
#include "soc/io_mux_reg.h"
#include "hal/rtc_io_hal.h"
#include "hal/gpio_ll.h"
#include "driver/gpio.h"
#include "audio_player.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "sdkconfig.h"

/* Audio timing */
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_PDM_UPSAMPLE_FS 480

/* Pins from Kconfig (see menu "NS4150 Player Configuration") */
#define AUDIO_PDM_SPEAK_P_GPIO ((gpio_num_t)CONFIG_NS4150_PDM_P_GPIO)
#if CONFIG_NS4150_USE_PDM_N
#define AUDIO_PDM_SPEAK_N_GPIO ((gpio_num_t)CONFIG_NS4150_PDM_N_GPIO)
#endif
#if CONFIG_NS4150_USE_PA_CTL
#define AUDIO_PA_CTL_GPIO ((gpio_num_t)CONFIG_NS4150_PA_CTL_GPIO)
#endif

static const char *TAG = "NS4150_PLAYER";

static const audio_codec_data_if_t *i2s_data_if = NULL;
static i2s_chan_handle_t i2s_tx_chan;
static esp_codec_dev_handle_t play_dev_handle;

/* Queue for signalling "play finished" (IDLE/SHUTDOWN) so we can play next file */
static QueueHandle_t s_play_done_queue = NULL;

static void audio_app_callback(audio_player_cb_ctx_t *ctx)
{
switch (ctx->audio_event) {
case 0:
ESP_LOGI(TAG, "PLAYER IDLE");
if (s_play_done_queue != NULL) {
uint8_t done = 1;
xQueueSend(s_play_done_queue, &done, 0);
}
break;
case 1:
ESP_LOGI(TAG, "PLAYER NEXT");
break;
case 2:
ESP_LOGI(TAG, "PLAYER PLAYING");
break;
case 3:
ESP_LOGI(TAG, "PLAYER PAUSE");
break;
case 4:
ESP_LOGI(TAG, "PLAYER SHUTDOWN");
if (s_play_done_queue != NULL) {
uint8_t done = 1;
xQueueSend(s_play_done_queue, &done, 0);
}
break;
case 5:
ESP_LOGI(TAG, "PLAYER UNKNOWN FILE");
break;
default:
ESP_LOGI(TAG, "PLAYER UNKNOWN EVENT: %d", ctx->audio_event);
break;
}
}

static esp_err_t app_mute_function(AUDIO_PLAYER_MUTE_SETTING setting)
{
return ESP_OK;
}

static esp_err_t bsp_audio_write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = esp_codec_dev_write(play_dev_handle, audio_buffer, len);
*bytes_written = len;
return ret;
Comment on lines +80 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't report a full write on error.

*bytes_written is set to len unconditionally. If esp_codec_dev_write() fails, the player is told the entire buffer was consumed even though nothing reached the sink.

🛠️ Minimal fix
 static esp_err_t bsp_audio_write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
 {
     esp_err_t ret = ESP_OK;
     ret = esp_codec_dev_write(play_dev_handle, audio_buffer, len);
-    *bytes_written = len;
+    *bytes_written = (ret == ESP_OK) ? len : 0;
     return ret;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static esp_err_t bsp_audio_write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = esp_codec_dev_write(play_dev_handle, audio_buffer, len);
*bytes_written = len;
return ret;
static esp_err_t bsp_audio_write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = esp_codec_dev_write(play_dev_handle, audio_buffer, len);
*bytes_written = (ret == ESP_OK) ? len : 0;
return ret;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/audio/ns4150_player/main/ns4150_player.c` around lines 80 - 85, In
bsp_audio_write, do not unconditionally set *bytes_written = len; instead check
the result of esp_codec_dev_write(play_dev_handle, audio_buffer, len) and only
set *bytes_written to len when that call returns ESP_OK (on error set
*bytes_written = 0 or an appropriate partial count), and return the esp_err_t
from esp_codec_dev_write; update the function body around the
esp_codec_dev_write call and the *bytes_written assignment to reflect this
conditional behavior.

}

esp_err_t app_audio_write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;

if (bsp_audio_write(audio_buffer, len, bytes_written, 1000) != ESP_OK) {
ESP_LOGE(TAG, "Write Task: i2s write failed");
ret = ESP_FAIL;
}

return ret;
}

static esp_err_t bsp_audio_reconfig_clk(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
esp_err_t ret = ESP_OK;

esp_codec_dev_sample_info_t fs = {
.sample_rate = rate,
.channel = ch,
.bits_per_sample = bits_cfg,
};

ret = esp_codec_dev_close(play_dev_handle);
if (ret != ESP_OK) {
return ret;
}
ret = esp_codec_dev_open(play_dev_handle, &fs);
if (ret != ESP_OK) {
return ret;
}

/* Reconfig PDM TX clock to match rate and up_sample_fs */
esp_err_t err_abort = i2s_channel_disable(i2s_tx_chan);
if (err_abort == ESP_OK) {
i2s_pdm_tx_clk_config_t clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(rate);
clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
err_abort = i2s_channel_reconfig_pdm_tx_clock(i2s_tx_chan, &clk_cfg);
if (err_abort == ESP_OK) {
err_abort = i2s_channel_enable(i2s_tx_chan);
}
if (err_abort != ESP_OK) {
ESP_LOGE(TAG, "PDM reconfig failed: %s", esp_err_to_name(err_abort));
ret = err_abort;
}
}
return ret;
}

void app_main(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &i2s_tx_chan, NULL));

i2s_pdm_tx_config_t pdm_cfg_default = {
.clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(AUDIO_OUTPUT_SAMPLE_RATE),
.slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.clk = GPIO_NUM_NC,
.dout = AUDIO_PDM_SPEAK_P_GPIO,
.invert_flags = {
.clk_inv = false,
},
},
};

pdm_cfg_default.clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
pdm_cfg_default.slot_cfg.sd_scale = I2S_PDM_SIG_SCALING_MUL_4;
pdm_cfg_default.slot_cfg.hp_scale = I2S_PDM_SIG_SCALING_MUL_4;
pdm_cfg_default.slot_cfg.lp_scale = I2S_PDM_SIG_SCALING_MUL_4;
pdm_cfg_default.slot_cfg.sinc_scale = I2S_PDM_SIG_SCALING_MUL_4;

ESP_ERROR_CHECK(i2s_channel_init_pdm_tx_mode(i2s_tx_chan, &pdm_cfg_default));
ESP_ERROR_CHECK(i2s_channel_enable(i2s_tx_chan));
gpio_set_drive_capability(AUDIO_PDM_SPEAK_P_GPIO, GPIO_DRIVE_CAP_0);

audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0,
.rx_handle = NULL,
.tx_handle = i2s_tx_chan,
};
i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg);

esp_codec_dev_cfg_t codec_dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = NULL,
.data_if = i2s_data_if,
};

play_dev_handle = esp_codec_dev_new(&codec_dev_cfg);
if (play_dev_handle == NULL) {
printf("Failed to create codec device\n");
return;
}

/* Set speaker output volume to a reasonable level */
esp_codec_dev_set_out_vol(play_dev_handle, 75);

#if CONFIG_NS4150_USE_PA_CTL
/* PA control GPIO (high = amplifier on) */
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << AUDIO_PA_CTL_GPIO);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
gpio_set_level(AUDIO_PA_CTL_GPIO, 1);
#endif

#if CONFIG_NS4150_USE_PDM_N
/* PDM N: inverted SD OUT for differential output */
PIN_FUNC_SELECT(IO_MUX_GPIO10_REG, PIN_FUNC_GPIO);
gpio_set_direction(AUDIO_PDM_SPEAK_N_GPIO, GPIO_MODE_OUTPUT);
esp_rom_gpio_connect_out_signal(AUDIO_PDM_SPEAK_N_GPIO, I2SO_SD_OUT_IDX, 1, 0);
gpio_set_drive_capability(AUDIO_PDM_SPEAK_N_GPIO, GPIO_DRIVE_CAP_0);
Comment on lines +198 to +203
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The PDM N mux ignores the configured GPIO.

AUDIO_PDM_SPEAK_N_GPIO comes from Kconfig, and examples/audio/ns4150_player/main/Kconfig.projbuild Line 21 even defaults it to 8, but this code still programs IO_MUX_GPIO10_REG. At minimum that touches the wrong pad, and on targets where explicit IOMUX selection matters it breaks non-10 mappings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/audio/ns4150_player/main/ns4150_player.c` around lines 198 - 203,
The code hardcodes IO_MUX_GPIO10_REG when calling PIN_FUNC_SELECT which ignores
the actual Kconfig-controlled AUDIO_PDM_SPEAK_N_GPIO; change the IOMUX selection
to use the pad register for AUDIO_PDM_SPEAK_N_GPIO instead of IO_MUX_GPIO10_REG
(e.g. use the GPIO_PIN_MUX_REG/GPIO_IOMUX macros or resolve the pad register via
a helper for AUDIO_PDM_SPEAK_N_GPIO) so PIN_FUNC_SELECT targets the configured
pin, keep the following calls (gpio_set_direction(AUDIO_PDM_SPEAK_N_GPIO,...),
esp_rom_gpio_connect_out_signal(AUDIO_PDM_SPEAK_N_GPIO, I2SO_SD_OUT_IDX,...),
gpio_set_drive_capability(...)) intact but referencing the correct mux/pad for
AUDIO_PDM_SPEAK_N_GPIO.

#endif

audio_player_config_t config = {
.mute_fn = app_mute_function,
.write_fn = app_audio_write,
.clk_set_fn = bsp_audio_reconfig_clk,
.priority = 5
};
ESP_ERROR_CHECK(audio_player_new(config));

s_play_done_queue = xQueueCreate(2, sizeof(uint8_t));
if (s_play_done_queue == NULL) {
ESP_LOGE(TAG, "Failed to create play-done queue");
return;
}
audio_player_callback_register(audio_app_callback, NULL);

/* SPIFFS */
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = false,
};
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));

size_t total = 0, used = 0;
esp_err_t ret_val = esp_spiffs_info(conf.partition_label, &total, &used);
if (ret_val != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret_val));
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}

/* Play all clips in sequence (player will fclose each FILE when done) */
static const char *play_list[] = {
"/spiffs/start_work.wav",
"/spiffs/need_approve.wav",
"/spiffs/fail.wav",
"/spiffs/success.wav",
};
const size_t play_count = sizeof(play_list) / sizeof(play_list[0]);

uint8_t drain;
while (xQueueReceive(s_play_done_queue, &drain, 0) == pdTRUE) { }

for (size_t i = 0; i < play_count; i++) {
FILE *fp = fopen(play_list[i], "rb");
if (fp == NULL) {
ESP_LOGE(TAG, "Failed to open: %s", play_list[i]);
continue;
}
esp_err_t ret = audio_player_play(fp);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to play %s (%s)", play_list[i], esp_err_to_name(ret));
fclose(fp);
continue;
}
uint8_t done = 0;
if (xQueueReceive(s_play_done_queue, &done, pdMS_TO_TICKS(30000)) != pdTRUE) {
ESP_LOGW(TAG, "Play timeout: %s", play_list[i]);
}
Comment on lines +250 to +265
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

A timeout can desynchronize the rest of the playlist.

When the 30-second wait expires, the loop immediately starts the next file without stopping playback or invalidating the old completion token. If the previous track finishes later, that late queue byte is consumed by the next iteration and the sequence gets out of sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/audio/ns4150_player/main/ns4150_player.c` around lines 250 - 265,
The timeout can cause a late completion byte to be consumed by the next file and
desynchronize playback; modify the timeout branch inside the loop so that when
xQueueReceive on s_play_done_queue times out you explicitly stop the current
playback (call the project's stop function such as audio_player_stop() or
equivalent after audio_player_play()), close the file handle (fclose(fp)), and
purge/reset the s_play_done_queue (use xQueueReset(s_play_done_queue) or drain
remaining bytes with non-blocking xQueueReceive) before continuing to the next
item to ensure no stale completion token is carried over.

}

while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
6 changes: 6 additions & 0 deletions examples/audio/ns4150_player/partitions.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 3400K,
storage, data, spiffs, , 400K,
6 changes: 6 additions & 0 deletions examples/audio/ns4150_player/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.3 Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c5"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
Binary file added examples/audio/ns4150_player/spiffs/fail.wav
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added examples/audio/ns4150_player/spiffs/success.wav
Binary file not shown.
Loading
Loading