chore: migrate project into clean repository

This commit is contained in:
yuuux
2026-08-13 16:50:52 +08:00
commit d1d25a09e7
27405 changed files with 9422808 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2025, LISTENAI
# SPDX-License-Identifier: Apache-2.0
if(CONFIG_APP_PLAYER)
listenai_library_named(app_player)
listenai_library_sources(
app_player.c
app_player_core.c
pa_manager.c
)
if (CONFIG_APP_PLAYER_AUDIO_FOCUS)
listenai_library_sources(app_player_focus.c)
endif()
listenai_include_directories(.)
add_subdirectory(player)
add_subdirectory(tone)
if (CONFIG_APP_PLAYER_AUDIO_FOCUS)
add_subdirectory(focus_mgr)
endif()
endif() # CONFIG_APP_PLAYER

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2025, LISTENAI
# SPDX-License-Identifier: Apache-2.0
menuconfig APP_PLAYER
bool "Enable APP Player"
default n
select LISA_OS
select LOG
select LISA_AUDIO_DEVICE
select LISA_DEVICE
select SDK_MODULE_SPEEXDSP
select SDK_MODULE_MP3_DECODER
select SDK_MODULE_AAC_DECODER
select SDK_MODULE_MEMPOOL
select MODULE_ZE_TLS
select MODULE_HTTPCLIENT
select SDK_MODULE_COREHTTP
select LISA_HTTP
select LWIP
select SYSLOG_PRINTK_REDIRECT
help
Enable the APP Player component.
if APP_PLAYER
config APP_PLAYER_PA_OFF_DELAY_MS
int "PA off delay time in milliseconds"
default 1000
range 0 10000
help
Delay time before turning off PA (Power Amplifier) after playback stops.
Set to 0 for immediate PA off, or set a delay to avoid pop noise.
Default is 1000ms (1 second).
config APP_PLAYER_CALLBACK_THREAD_STACK_SIZE
int "Callback thread stack size"
default 4096
range 1024 16384
help
Stack size for the callback thread that handles player events.
Default is 4096 bytes.
config APP_PLAYER_CALLBACK_THREAD_PRIORITY
int "Callback thread priority"
default 5
range 0 32
help
Priority for the callback thread that handles player events.
Lower values indicate higher priority.
Default is 5.
config APP_PLAYER_AUDIO_FOCUS
bool "Enable audio focus management"
default n
help
Enable audio focus management for coordinating multiple player instances.
When enabled, players can have different priorities and automatically
handle focus acquisition and release (e.g., pause music when playing tone).
Features:
- Priority-based focus arbitration
- Automatic focus state transitions (FOREGROUND/BACKGROUND/NONE)
- Customizable focus change callbacks
- Capture rules for channel preemption
If disabled, all focus-related APIs will be unavailable and the code
footprint will be reduced.
endif # APP_PLAYER

View File

@@ -0,0 +1,350 @@
.. _components_app_player:
app_player 音频播放组件
=======================
app_player 是一个轻量级的音频播放器模块,支持多种音频源的播放。
核心能力
::::::::
- **多种音频源支持**
- **网络音频文件**:支持 HTTP/HTTPS 协议的网络音频播放
- **本地文件系统音频**:支持从 FAT32 文件系统SD 卡)播放音频文件
- **本地自定义打包音频**:支持从 Flash 播放预打包的提示音文件
- **PCM 流式播放**:支持实时 PCM 数据流播放
- **多种音频格式**:支持 PCM、MP3、WAV、AAC、M4A 格式
- **内置重采样**:支持多种采样率自动转换,推荐 16 kHz 以获得最佳性能
- **播放控制**播放、暂停、恢复、停止、跳转seek
- **事件回调**:支持准备完成、播放中、暂停、停止、播放完成、错误等事件通知
- **PA 功放管理**:外部注册 PA 硬件控制函数
- **高级选项**:支持跳过音频开头、跳过低能量段等高级播放选项
- **音量控制**1-100 级音量调节
- **音频焦点管理**:通过 ``CONFIG_APP_PLAYER_AUDIO_FOCUS`` 启用,支持多播放器实例的焦点协调,基于优先级自动管理播放状态
Kconfig 配置
:::::::::::::
在使用 app_player 之前,需要通过 Kconfig 进行配置。
启用组件
--------
``menuconfig`` 中启用 APP Player 组件:
.. code-block:: none
Component config --->
[*] Enable APP Player
或在 ``prj.conf`` 中添加:
.. code-block:: none
CONFIG_APP_PLAYER=y
可配置项
--------
.. list-table:: 主要配置项
:header-rows: 1
:widths: 30 30 20 20
* - 配置项
- 说明
- 默认值
- 范围
* - ``CONFIG_APP_PLAYER_PA_OFF_DELAY_MS``
- PA 功放关闭延迟时间(毫秒)
- 1000
- 0-10000
* - ``CONFIG_APP_PLAYER_CALLBACK_THREAD_STACK_SIZE``
- 回调线程栈大小(字节)
- 4096
- 1024-16384
* - ``CONFIG_APP_PLAYER_CALLBACK_THREAD_PRIORITY``
- 回调线程优先级
- 5
- 0-32
* - ``CONFIG_APP_PLAYER_AUDIO_FOCUS``
- 启用音频焦点管理
- n
- -
配置说明
--------
1. **PA 功放关闭延迟 ``APP_PLAYER_PA_OFF_DELAY_MS``**
- 播放停止后延迟关闭 PA 的时间,避免爆音
- 设置为 0 表示立即关闭
- 推荐保持默认值 1000 ms
2. **回调线程栈大小 ``APP_PLAYER_CALLBACK_THREAD_STACK_SIZE``**
- 事件回调线程的栈空间大小
- 如果回调函数中有复杂逻辑,可适当增大
- 默认 4096 字节对大多数场景足够
3. **回调线程优先级 ``APP_PLAYER_CALLBACK_THREAD_PRIORITY``**
- 数值越小优先级越高
- 根据系统中其他任务的优先级进行调整
- 默认优先级为 5
4. **音频焦点管理 ``APP_PLAYER_AUDIO_FOCUS``(可裁剪)**
- 启用后支持多播放器实例的焦点协调
- 基于优先级自动管理播放状态(前景/背景/无焦点)
- 默认禁用(单播放器场景无需此功能)
- 禁用后可减少代码体积,焦点相关 API 将不可用
依赖组件
--------
启用 ``CONFIG_APP_PLAYER`` 后,以下组件会自动被选中:
- LISA_OS - 操作系统抽象层
- LOG - 日志组件
- LISA_AUDIO_DEVICE - 音频设备驱动
- LISA_DEVICE - 设备抽象层
- SDK_MODULE_SPEEXDSP - SpeexDSP 音频处理库
- SDK_MODULE_MP3_DECODER - MP3 解码器
- SDK_MODULE_AAC_DECODER - AAC 解码器
- SDK_MODULE_MEMPOOL - 内存池管理
- MODULE_ZE_TLS - TLS 安全传输层
- MODULE_HTTPCLIENT - HTTP 客户端(用于网络音频)
- SDK_MODULE_COREHTTP - 核心 HTTP 库
- LISA_HTTP - HTTP 抽象层
- LWIP - 轻量级 TCP/IP 协议栈
- SYSLOG_PRINTK_REDIRECT - 系统日志重定向
快速开始
::::::::
1. 初始化模块
-------------
.. code-block:: c
#include "app_player.h" /* 公开 API 头文件 */
/* PA 控制回调函数(控制功放开关) */
static int pa_control_callback(int onoff) {
/* onoff: 1=打开 PA, 0=关闭 PA */
/* 返回 0 表示成功 */
return lisa_gpio_write_pin(gpio_dev, PA_PIN_NUM,
onoff ? LISA_GPIO_HIGH : LISA_GPIO_LOW);
}
/* 基本初始化(未启用焦点管理) */
app_player_config_t config = {
.pa_ctrl_callback = pa_control_callback /* 必填 */
};
app_player_init(&config);
2. 创建播放器实例
-----------------
.. code-block:: c
/* 创建播放器 */
app_player_t *player = app_player_create("my_player");
/* 注册事件回调(可选,建议在首次播放前调用) */
app_player_register_callback(player, event_callback, user_data);
3. 播放音频
-----------
.. code-block:: c
/* 播放网络音频 */
app_player_play(player, "https://example.com/audio.mp3");
/* 播放本地文件系统音频 */
app_player_play(player, "/SD:/music.mp3");
/* 播放 Flash 提示音(需先初始化提示音系统) */
app_tone_init(0x30100000);
const char *url = app_tone_get_url(TONE_ID_0);
app_player_play(player, url);
4. 播放控制
-----------
.. code-block:: c
/* 暂停 */
app_player_pause(player);
/* 恢复 */
app_player_resume(player);
/* 停止 */
app_player_stop(player);
/* 跳转到指定位置 */
app_player_seek(player, 5000); /* 跳转到 5 秒位置 */
/* 设置音量 */
app_player_set_volume(player, 80); /* 音量 80/100 */
5. 事件处理
-----------
.. code-block:: c
static void event_callback(app_player_t *player,
app_player_event_t event,
void *user_data) {
switch (event) {
case APP_PLAYER_EVENT_PREPARED:
/* 准备完成,即将开始播放 */
break;
case APP_PLAYER_EVENT_PLAYING:
/* 正在播放 */
break;
case APP_PLAYER_EVENT_COMPLETED:
/* 播放完成 */
break;
case APP_PLAYER_EVENT_ERROR:
/* 播放出错 */
break;
}
}
6. 清理资源
-----------
.. code-block:: c
/* 销毁播放器 */
app_player_destroy(player);
播放器状态机
::::::::::::::
.. code-block:: text
IDLE → PREPARING → PREPARED → PLAYING ⇄ PAUSED
↓ ↓
STOPPED ← ─ ─ ┘
ERROR
- **IDLE**:初始空闲状态
- **PREPARING**:正在准备音频资源
- **PREPARED**:准备完成,即将开始播放
- **PLAYING**:正在播放
- **PAUSED**:已暂停
- **STOPPED**:已停止
- **ERROR**:错误状态
主要 API
::::::::
.. list-table:: 核心 API
:header-rows: 1
:widths: 40 60
* - 函数
- 说明
* - ``app_player_init()``
- 初始化模块(必须最先调用)
* - ``app_player_create()``
- 创建播放器实例
* - ``app_player_destroy()``
- 销毁播放器实例
* - ``app_player_register_callback()``
- 注册事件回调
* - ``app_player_play()``
- 播放指定 URL
* - ``app_player_play_ex()``
- 高级播放(支持更多选项)
* - ``app_player_stop()``
- 停止播放(异步)
* - ``app_player_stop_sync()``
- 停止播放(同步)
* - ``app_player_pause()``
- 暂停播放
* - ``app_player_resume()``
- 恢复播放(异步)
* - ``app_player_resume_sync()``
- 恢复播放(同步)
* - ``app_player_seek()``
- 跳转到指定位置
* - ``app_player_set_volume()``
- 设置音量1-100
* - ``app_player_get_state()``
- 获取播放器状态
* - ``app_player_get_position()``
- 获取当前播放位置
* - ``app_player_get_duration()``
- 获取音频总时长
* - ``app_player_reset()``
- 重置播放器到初始状态
注意事项
::::::::
1. **必须先初始化**:调用 ``app_player_init()`` 必须在创建播放器实例之前。
2. **PA 回调必填**:初始化时必须提供 ``pa_ctrl_callback``,用于控制功放。
3. **多实例支持**
- 支持创建多个播放器实例。
- **不支持混音**:同一时刻只能有一个播放器处于播放状态。
- 多播放器场景建议启用音频焦点管理(``CONFIG_APP_PLAYER_AUDIO_FOCUS=y``)来自动协调播放状态。
- 单播放器场景建议禁用焦点管理以减少代码体积。
4. **音频格式要求**
- **位深度**16 bit硬件固定不可更改
- **采样率**16 kHz推荐。播放器内部支持重采样可处理其他采样率但为了最佳性能建议使用 16 kHz。
- **格式**:支持 PCM、MP3、WAV、AAC、M4A。
5. **同步 vs 异步**``stop_sync()````resume_sync()`` 会阻塞等待操作完成,适合需要确保状态切换完成的场景。
6. **音量范围**:音量值范围是 1-100。
7. **资源清理**:使用完毕后记得调用 ``app_player_destroy()`` 释放资源。
错误码
::::::
.. list-table:: 常见错误码
:header-rows: 1
:widths: 40 60
* - 错误码
- 说明
* - ``APP_PLAYER_OK``
- 成功
* - ``APP_PLAYER_ERR_INVALID_PARAM``
- 无效参数
* - ``APP_PLAYER_ERR_NO_MEMORY``
- 内存不足
* - ``APP_PLAYER_ERR_INVALID_STATE``
- 状态错误
* - ``APP_PLAYER_ERR_NOT_SUPPORTED``
- 不支持的操作
* - ``APP_PLAYER_ERR_TIMEOUT``
- 超时
* - ``APP_PLAYER_ERR_IO``
- IO 错误
扩展文档
::::::::
.. toctree::
:maxdepth: 1
audio_focus.md
streaming.md
local_tone.md
示例代码
::::::::
完整示例代码请参考:
- ``samples/modules/app_player/`` —— 演示用例。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,435 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __LISTENAI_APP_PLAYER_H__
#define __LISTENAI_APP_PLAYER_H__
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// ==================== 前置声明 ====================
typedef struct app_player_s app_player_t;
/**
* @brief 播放器错误码
*/
typedef enum {
APP_PLAYER_OK = 0, /**< 成功 */
APP_PLAYER_ERR_INVALID_PARAM = -1, /**< 无效参数 */
APP_PLAYER_ERR_NO_MEMORY = -2, /**< 内存不足 */
APP_PLAYER_ERR_INVALID_STATE = -3, /**< 状态错误 */
APP_PLAYER_ERR_NOT_SUPPORTED = -4, /**< 不支持的操作 */
APP_PLAYER_ERR_TIMEOUT = -5, /**< 超时 */
APP_PLAYER_ERR_IO = -6, /**< IO 错误 */
} app_player_err_t;
/**
* @brief PA 控制回调函数类型
* @param onoff 1: 打开PA, 0: 关闭PA
* @return 0: 成功, 其他值: 失败
*/
typedef int (*app_player_pa_ctrl_cb_t)(int onoff);
// ==================== 音频焦点管理类型(可选功能) ====================
#ifdef CONFIG_APP_PLAYER_AUDIO_FOCUS
/**
* @brief 播放器焦点状态
*/
typedef enum {
APP_PLAYER_FOCUS_FOREGROUND = 0, /**< 前景焦点:正常播放 */
APP_PLAYER_FOCUS_BACKGROUND, /**< 背景焦点:降级播放(暂停或降音量) */
APP_PLAYER_FOCUS_NONE /**< 无焦点:停止播放 */
} app_player_focus_state_t;
/**
* @brief 焦点丢失策略
*/
typedef enum {
APP_PLAYER_FOCUS_LOSS_IGNORE = 0, /**< 忽略焦点变化,继续播放 */
APP_PLAYER_FOCUS_LOSS_PAUSE, /**< 暂停播放(可自动恢复) */
APP_PLAYER_FOCUS_LOSS_STOP, /**< 停止播放(不可自动恢复) */
APP_PLAYER_FOCUS_LOSS_DUCK, /**< 降低音量(暂不支持) */
} app_player_focus_loss_policy_t;
/**
* @brief 焦点行为配置
*/
typedef struct {
app_player_focus_loss_policy_t on_background; /**< 变为后景时的策略 */
app_player_focus_loss_policy_t on_focus_lost; /**< 完全失去焦点时的策略 */
uint8_t duck_volume_percent; /**< 降低音量时的百分比(0-100)(暂不支持) */
} app_player_focus_behavior_t;
/**
* @brief 焦点变化回调函数
* @param player 播放器实例
* @param new_state 新的焦点状态
* @param by_which 触发焦点变化的播放器句柄
* @param user_data 用户自定义数据
* @return true=完全接管处理app_player不执行任何操作false=执行默认策略
*
* @note 此回调在焦点状态变化时由焦点管理器调用,用于通知应用层焦点变化事件
* @note 返回值控制行为:
* - 返回 falseapp_player 根据 behavior 配置自动执行策略pause/stop/ignore
* - 返回 trueapp_player 完全跳过处理,由应用层自行处理
* @warning 回调函数应快速返回,避免阻塞焦点管理器
* 耗时操作应提交到任务队列异步执行
*/
typedef bool (*app_player_focus_change_cb_t)(app_player_t *player,
app_player_focus_state_t new_state,
app_player_t *by_which,
void *user_data);
/**
* @brief 焦点通道配置(静态属性)
*/
typedef struct {
const char *name; /**< 通道名称(需与 app_player_create 的 name 匹配) */
int priority; /**< 优先级数值越小优先级越高0 = 最高) */
const char **capture_names; /**< 可抢占的通道名称列表(通过 name 引用其他播放器) */
int capture_count; /**< 抢占列表长度 */
app_player_focus_behavior_t behavior; /**< 焦点丢失时的行为配置 */
} app_player_focus_channel_config_t;
#endif // CONFIG_APP_PLAYER_AUDIO_FOCUS
/**
* @brief app_player 初始化配置结构体
*/
typedef struct {
app_player_pa_ctrl_cb_t pa_ctrl_callback; /**< PA 控制回调函数(必填) */
#ifdef CONFIG_APP_PLAYER_AUDIO_FOCUS
const app_player_focus_channel_config_t *focus_configs; /**< 焦点通道配置数组(可选) */
int focus_config_count; /**< 焦点通道配置数量 */
#endif
} app_player_config_t;
/**
* @brief 播放器状态
*/
typedef enum {
APP_PLAYER_STATE_IDLE = 0, /**< 空闲状态 */
APP_PLAYER_STATE_PREPARING, /**< 准备中 */
APP_PLAYER_STATE_PREPARED, /**< 准备完成 */
APP_PLAYER_STATE_PLAYING, /**< 播放中 */
APP_PLAYER_STATE_PAUSED, /**< 已暂停 */
APP_PLAYER_STATE_STOPPED, /**< 已停止 */
APP_PLAYER_STATE_ERROR /**< 错误状态 */
} app_player_state_t;
/**
* @brief 播放器事件类型
*/
typedef enum {
APP_PLAYER_EVENT_ERROR = 0, /**< 播放错误 */
APP_PLAYER_EVENT_PREPARED, /**< 准备完成 */
APP_PLAYER_EVENT_PLAYING, /**< 播放中 */
APP_PLAYER_EVENT_PAUSED, /**< 已暂停 */
APP_PLAYER_EVENT_STOPPED, /**< 已停止 */
APP_PLAYER_EVENT_COMPLETED, /**< 播放完成 */
APP_PLAYER_EVENT_SEEK_COMPLETE, /**< Seek 完成 */
} app_player_event_t;
/**
* @brief 播放器事件回调函数
* @param player 播放器实例
* @param event 事件类型
* @param user_data 用户自定义数据
*/
typedef void (*app_player_event_cb_t)(app_player_t *player, app_player_event_t event, void *user_data);
/**
* @brief 高级播放选项
*/
typedef struct {
const char *url; /**< 播放 URL必填 */
uint32_t throw_time_ms; /**< 开始指定时长内的低能量段毫秒0=不跳过) */
} app_player_play_opt_t;
/**
* @brief 初始化 app_player 模块
* @param config 初始化配置参数
* @return APP_PLAYER_OK 成功,其他表示错误
* @note 必须在创建播放器实例前调用此函数
* @note config 中的 pa_ctrl_callback 不能为 NULL
*/
int app_player_init(const app_player_config_t *config);
/**
* @brief 创建播放器实例
* @param name 播放器名称
* @return 播放器实例指针,失败返回 NULL
*/
app_player_t *app_player_create(const char *name);
/**
* @brief 销毁播放器实例
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_destroy(app_player_t *player);
/**
* @brief 注册事件回调
* @param player 播放器实例
* @param event_cb 事件回调函数
* @param user_data 用户数据
* @return APP_PLAYER_OK 成功,其他表示错误
* @note 建议在首次播放前调用
*/
int app_player_register_callback(app_player_t *player, app_player_event_cb_t event_cb, void *user_data);
/**
* @brief 播放指定 URL
* @param player 播放器实例
* @param url 播放地址(支持 http://, https://等)
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_play(app_player_t *player, const char *url);
/**
* @brief 高级播放接口(支持更多选项)
* @param player 播放器实例
* @param opt 播放选项
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_play_ex(app_player_t *player, const app_player_play_opt_t *opt);
/**
* @brief 停止播放(异步)
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_stop(app_player_t *player);
/**
* @brief 停止播放(同步,等待停止完成)
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_stop_sync(app_player_t *player);
/**
* @brief 暂停播放
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_pause(app_player_t *player);
/**
* @brief 恢复播放(异步)
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_resume(app_player_t *player);
/**
* @brief 恢复播放(同步,等待恢复完成)
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_resume_sync(app_player_t *player);
/**
* @brief 重置播放器到初始状态
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_reset(app_player_t *player);
/**
* @brief 跳转到指定位置
* @param player 播放器实例
* @param seek_ms 跳转位置(毫秒)
* @return APP_PLAYER_OK 成功,其他表示错误
* @warning 流式播放模式下不支持此操作
*/
int app_player_seek(app_player_t *player, uint32_t seek_ms);
/**
* @brief 获取播放器状态
* @param player 播放器实例
* @return 播放器状态
*/
app_player_state_t app_player_get_state(app_player_t *player);
/**
* @brief 获取当前播放位置
* @param player 播放器实例
* @param position 输出参数,当前位置(毫秒)
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_get_position(app_player_t *player, uint32_t *position);
/**
* @brief 获取总时长
* @param player 播放器实例
* @param duration 输出参数,总时长(毫秒)
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_get_duration(app_player_t *player, uint32_t *duration);
/**
* @brief 设置音量
* @param player 播放器实例
* @param volume 音量值1-100
* @return APP_PLAYER_OK 成功,其他表示错误
*/
int app_player_set_volume(app_player_t *player, uint8_t volume);
/**
* @brief 开始流式播放
* @param player 播放器实例
* @param sample_rate 采样率Hz如 8000, 16000, 48000
* @param channels 声道数(当前仅支持 1
* @param bits 位深度(当前仅支持 16
* @return APP_PLAYER_OK 成功,其他表示错误
* @note 调用此函数后,使用 app_player_write_stream 写入 PCM 数据
* @note 流式播放适用于实时音频合成、网络音频流等场景
* @warning 流式播放模式仅支持以下操作流程:
* app_player_play_stream -> app_player_write_stream -> app_player_finish_stream
* 流式播放模式下不支持 seek/pause/stop/resume 等操作
*/
int app_player_play_stream(app_player_t *player, uint32_t sample_rate, uint8_t channels, uint8_t bits);
/**
* @brief 写入流数据
* @param player 播放器实例
* @param data 数据指针
* @param size 数据大小(字节)
* @param timeout_ms 超时时间(毫秒)
* @return 实际写入的字节数,<0 表示错误
* @note 必须先调用 app_player_play_stream 开启流式播放
* @note 支持多次调用以逐块写入数据
* @warning 仅在流式播放模式下有效,必须先调用 app_player_play_stream
*/
int app_player_write_stream(app_player_t *player, const uint8_t *data, uint32_t size, uint32_t timeout_ms);
/**
* @brief 结束流式播放
* @param player 播放器实例
* @return APP_PLAYER_OK 成功,其他表示错误
* @note 通知播放器所有流数据已写入完毕,等待播放完成
* @note 调用此函数后将收到 APP_PLAYER_EVENT_COMPLETED 事件
* @warning 仅在流式播放模式下有效,调用后退出流式播放模式
*/
int app_player_finish_stream(app_player_t *player);
// ==================== 音频焦点管理 API可选功能 ====================
#ifdef CONFIG_APP_PLAYER_AUDIO_FOCUS
/**
* @brief 注册播放器焦点变化回调
* @param player 播放器实例
* @param on_focus_change 焦点状态变化回调函数(可为 NULL 仅使用默认策略)
* @param user_data 用户自定义数据(传递给回调)
* @return APP_PLAYER_OK 成功,其他表示错误
*
* @note 需要在 app_player_init() 时配置过焦点通道focus_configs
* @note 如果未配置焦点通道,该播放器将不参与焦点管理
* @note 可以在播放器创建后的任意时刻调用,甚至可以动态更换回调
* @note 焦点变化时app_player 会根据 behavior 配置自动执行策略,
* 除非回调返回 true 表示完全接管
*
* @par 示例1仅监听焦点变化使用默认策略
* @code
* bool music_focus_cb(app_player_t *player, app_player_focus_state_t state,
* app_player_t *by_which, void *user_data) {
* LOGI("Focus changed to %d by player %p", state, by_which);
* return false; // 让 app_player 执行配置的策略
* }
*
* app_player_register_focus_cb(music, music_focus_cb, NULL);
* @endcode
*
* @par 示例2完全自定义焦点处理
* @code
* bool music_focus_cb(app_player_t *player, app_player_focus_state_t state,
* app_player_t *by_which, void *user_data) {
* if (state == APP_PLAYER_FOCUS_BACKGROUND) {
* // 自定义:降低音量而不是暂停
* app_player_set_volume(player, 30);
* return true; // 阻止 app_player 执行默认策略
* }
* return false; // 其他情况使用默认策略
* }
* @endcode
*/
int app_player_register_focus_cb(app_player_t *player,
app_player_focus_change_cb_t on_focus_change,
void *user_data);
/**
* @brief 设置播放器的焦点行为策略(运行时动态修改)
* @param player 播放器实例
* @param behavior 新的焦点行为配置
* @return APP_PLAYER_OK 成功,其他表示错误
*
* @note 此函数允许在运行时动态修改播放器的焦点行为策略
* @note 仅对已注册焦点通道的播放器有效
* @note 不会影响其他播放器的配置
* @note 修改后的配置立即生效,影响后续的焦点变化处理
* @warning 建议在播放器空闲状态下调用,避免影响正在进行的焦点处理
*
* @par 使用示例:
* @code
* // 修改 MUSIC 播放器的焦点行为
* app_player_focus_behavior_t new_behavior = {
* .on_background = APP_PLAYER_FOCUS_LOSS_IGNORE, // 后景继续播放
* .on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP, // 完全失焦停止
* };
* app_player_set_focus_behavior(g_music_player, &new_behavior);
* @endcode
*/
int app_player_set_focus_behavior(app_player_t *player,
const app_player_focus_behavior_t *behavior);
/**
* @brief 获取播放器当前的焦点行为策略
* @param player 播放器实例
* @param behavior 输出参数,当前的焦点行为配置
* @return APP_PLAYER_OK 成功,其他表示错误
*
* @note 可用于保存和恢复焦点行为配置
*
* @par 使用示例:
* @code
* // 保存原始配置
* app_player_focus_behavior_t original_behavior;
* app_player_get_focus_behavior(g_music_player, &original_behavior);
*
* // 修改配置进行测试
* app_player_focus_behavior_t test_behavior = { ... };
* app_player_set_focus_behavior(g_music_player, &test_behavior);
*
* // 恢复原始配置
* app_player_set_focus_behavior(g_music_player, &original_behavior);
* @endcode
*/
int app_player_get_focus_behavior(app_player_t *player,
app_player_focus_behavior_t *behavior);
#endif // CONFIG_APP_PLAYER_AUDIO_FOCUS
#ifdef __cplusplus
}
#endif
#endif // __LISTENAI_APP_PLAYER_H__

View File

@@ -0,0 +1,319 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lisa_log.h"
#include "lisa_mem.h"
#include "app_player_internal.h"
#include "app_player_core.h"
#include <stdio.h>
#define TAG "APP_PLAYER_CORE"
/**
* @brief 检查并处理 preparing 状态
* @param player 播放器实例
* @return true 可以继续操作false 已处理 preparing 状态需要 reset
*/
bool app_player_core_prepare_check(app_player_t *player)
{
if (!player) {
return false;
}
if (player->is_preparing) {
// is_preparing判断完后打印后可能引起时间片由player回调线程调度
LISA_LOGD(TAG, "Wait %s prepare complete...", player->name);
lisa_player_pre_close(player->hld);
// 回调线程执行后is_preparing会被置为false
// 因此此处信号量可能会一直等待
if (player->is_preparing) {
player->wait_prepare_intercepted = true;
lisa_semaphore_take(player->preparing_sem, LISA_OS_WAIT_FOREVER);
player->wait_prepare_intercepted = false;
}
LISA_LOGD(TAG, "Pre %s prepare complete", player->name);
lisa_player_stop_sync(player->hld);
lisa_player_reset(player->hld);
LISA_LOGD(TAG, "Pre %s status check end", player->name);
return false;
}
return true;
}
/**
* @brief 核心层:播放音频
* @note 严格按照原 app_player_play_ex 的实现方式
*/
int app_player_core_play(app_player_t *player, const char *url, int throw_time)
{
if (!player || !url) {
return -1;
}
LISA_LOGD(TAG, "Core play: %s, url=%s, throw_time=%d", player->name, url, throw_time);
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 检查并处理preparing状态
if (app_player_core_prepare_check(player)) {
// 如果返回true需要reset
lisa_player_reset(player->hld);
}
// 设置流式模式标志为falseURL播放模式
player->is_stream_mode = false;
// 设置跳过开始指定时长内的低能量段
if (throw_time > 0) {
lisa_player_throw_low_energy(player->hld, throw_time);
}
// 设置preparing标志
player->is_preparing = true;
player->pause_preparing = false;
// 设置URL并准备seturl 会自动触发准备过程)
PlayerErr ret = lisa_player_seturl(player->hld, url);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "Core play failed: seturl error %d", ret);
player->is_preparing = false;
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:播放流式音频
* @note 严格按照原 app_player_play_stream 的实现方式
*/
int app_player_core_play_stream(app_player_t *player, uint32_t sample_rate, uint8_t channels, uint8_t bits)
{
if (!player) {
return -1;
}
LISA_LOGD(TAG, "Core play stream: %s, rate=%u, ch=%u, bits=%u",
player->name, sample_rate, channels, bits);
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 检查并处理preparing状态
if (app_player_core_prepare_check(player)) {
// 如果返回true需要reset
lisa_player_reset(player->hld);
}
// 设置流式模式标志
player->is_stream_mode = true;
// 设置preparing标志
player->is_preparing = true;
player->pause_preparing = false;
// 构造流式播放URLPCM格式
char stream_url[128];
snprintf(stream_url, sizeof(stream_url),
"stream://type=pcm&rate=%u&channel=%u&bits=%u",
sample_rate, channels, bits);
// 设置URL并准备seturl 会自动触发准备过程)
PlayerErr ret = lisa_player_seturl(player->hld, stream_url);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "Core play stream failed: seturl error %d", ret);
player->is_preparing = false;
player->is_stream_mode = false;
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:暂停播放
*/
int app_player_core_pause(app_player_t *player)
{
if (!player || player->is_stream_mode) {
return -1;
}
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 检查当前状态如果已经是PAUSED无需重复pause
PlayerState state = lisa_player_get_state(player->hld);
if (state == PLAYER_ST_PAUSED) {
LISA_LOGD(TAG, "Core pause: %s already paused, skip", player->name);
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
LISA_LOGD(TAG, "Core pause: %s", player->name);
// 如果正在准备中设置pause_preparing标志
if (player->is_preparing) {
player->pause_preparing = true;
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
// 调用底层暂停
PlayerErr ret = lisa_player_pause(player->hld);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "Core pause failed: lisa_player_pause error %d", ret);
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:恢复播放
*/
int app_player_core_resume(app_player_t *player)
{
if (!player || player->is_stream_mode) {
return -1;
}
LISA_LOGD(TAG, "Core resume: %s", player->name);
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 如果是在准备阶段被暂停的清除pause_preparing标志并开始播放
if (player->pause_preparing) {
player->pause_preparing = false;
PlayerErr ret = lisa_player_play(player->hld);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "Core resume failed: lisa_player_play error %d", ret);
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
// 调用底层的恢复接口
PlayerErr ret = lisa_player_resume(player->hld);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "Core resume failed: lisa_player_resume error %d", ret);
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:停止播放
*/
int app_player_core_stop(app_player_t *player)
{
if (!player) {
return -1;
}
LISA_LOGD(TAG, "Core stop: %s", player->name);
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 检查并处理preparing状态
if (app_player_core_prepare_check(player)) {
// 不在preparing状态调用同步stop
int ret = lisa_player_stop_sync(player->hld);
if (ret != PLAYER_OK) {
LISA_LOGW(TAG, "Core stop sync failed: %d, resetting player", ret);
lisa_player_reset(player->hld);
__enqueue_callback_event(player, APP_PLAYER_EVENT_STOPPED);
}
} else {
// 在preparing状态__player_prepare_check已经处理完毕
// 手动发送STOPPED事件因为已经reset不会有回调
__enqueue_callback_event(player, APP_PLAYER_EVENT_STOPPED);
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:同步停止播放
*/
int app_player_core_stop_sync(app_player_t *player)
{
if (!player) {
return -1;
}
LISA_LOGD(TAG, "Core stop sync: %s", player->name);
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
// 检查并处理preparing状态
if (app_player_core_prepare_check(player)) {
// 不在preparing状态调用同步stop
int ret = lisa_player_stop_sync(player->hld);
if (ret != PLAYER_OK) {
LISA_LOGW(TAG, "Core stop sync failed: %d, resetting player", ret);
lisa_player_reset(player->hld);
__enqueue_callback_event(player, APP_PLAYER_EVENT_STOPPED);
}
} else {
// 在preparing状态__player_prepare_check已经处理完毕
// 手动发送STOPPED事件因为已经reset不会有回调
__enqueue_callback_event(player, APP_PLAYER_EVENT_STOPPED);
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return 0;
}
/**
* @brief 核心层:写入流式数据
* @note 严格按照原 app_player_write_stream 的实现方式使用timeout_ms参数
*/
int app_player_core_stream_write(app_player_t *player, const uint8_t *data, size_t size, uint32_t timeout_ms)
{
if (!player) {
return -1;
}
// data 可以为 NULL用于结束流
if (!data && size > 0) {
LISA_LOGE(TAG, "Core stream write failed: data is NULL but size > 0");
return -1;
}
PLAYER_MUTEX_LOCK(player->core_lock, LISA_OS_WAIT_FOREVER);
if (!player->is_stream_mode) {
LISA_LOGE(TAG, "Core stream write failed: not in stream mode");
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
// 调用底层写入接口
int ret = lisa_player_put_stream_data(player->hld, (uint8_t *)data, size, timeout_ms);
if (ret < 0) {
LISA_LOGE(TAG, "Core stream write failed: %d", ret);
PLAYER_MUTEX_UNLOCK(player->core_lock);
return -1;
}
PLAYER_MUTEX_UNLOCK(player->core_lock);
return ret;
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef APP_PLAYER_CORE_H
#define APP_PLAYER_CORE_H
#include "app_player.h"
#include "lisa_player.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief lisa_player 封装层 - 核心接口
*
* 本模块专门处理 lisa_player 的各种限制和状态管理:
* - is_preparing 状态处理
* - pause_preparing 标志管理
* - prepare 中断机制
* - 纯粹的播放器控制,不涉及焦点管理和 PA 控制
* - 所有接口均已实现线程安全保护(使用 core_lock
*/
/**
* @brief 核心层:播放音频
* @param player 播放器实例
* @param url 音频 URL
* @param throw_time 跳转时间ms
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_play(app_player_t *player, const char *url, int throw_time);
/**
* @brief 核心层:播放流式音频
* @param player 播放器实例
* @param sample_rate 采样率
* @param channels 声道数
* @param bits 位宽
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_play_stream(app_player_t *player, uint32_t sample_rate, uint8_t channels, uint8_t bits);
/**
* @brief 核心层:暂停播放
* @param player 播放器实例
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_pause(app_player_t *player);
/**
* @brief 核心层:恢复播放
* @param player 播放器实例
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_resume(app_player_t *player);
/**
* @brief 核心层:停止播放
* @param player 播放器实例
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_stop(app_player_t *player);
/**
* @brief 核心层:同步停止播放
* @param player 播放器实例
* @return 0 成功,负数失败
* @note 不操作焦点,不操作 PA线程安全
*/
int app_player_core_stop_sync(app_player_t *player);
/**
* @brief 核心层:写入流式数据
* @param player 播放器实例
* @param data 数据缓冲区
* @param size 数据大小
* @param timeout_ms 超时时间(毫秒)
* @return 实际写入的字节数,负数表示错误
*/
int app_player_core_stream_write(app_player_t *player, const uint8_t *data, size_t size, uint32_t timeout_ms);
/**
* @brief 检查并处理 preparing 状态
* @param player 播放器实例
* @return true 可以继续操作false 已处理 preparing 状态需要 reset
* @note 内部函数,由 core 层调用
*/
bool app_player_core_prepare_check(app_player_t *player);
#ifdef __cplusplus
}
#endif
#endif /* APP_PLAYER_CORE_H */

View File

@@ -0,0 +1,468 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lisa_log.h"
#include "lisa_mem.h"
#include "app_player_internal.h"
#include "app_player_core.h"
#include "app_player_focus.h"
#include "focus_mgr/listen_audiomgr.h"
#include "pa_manager.h"
#define TAG "APP_PLAYER_FOCUS"
/**
* @brief 全局焦点管理器实例
*/
static listen_audiomgr_t *s_audio_focus_mgr = NULL;
/**
* @brief 焦点配置缓存
*/
static const app_player_focus_channel_config_t *s_focus_configs = NULL;
static int s_focus_config_count = 0;
// 前向声明
static void __audio_focus_change_bridge(focus_state_e state, int by_which_id, void *user_data);
extern app_player_t *__find_player_by_focus_channel_id(int focus_channel_id);
static int __find_focus_config_index_by_name(const char *name)
{
if (!name || !s_focus_configs || s_focus_config_count <= 0) {
return -1;
}
for (int i = 0; i < s_focus_config_count; i++) {
if (s_focus_configs[i].name && strcmp(s_focus_configs[i].name, name) == 0) {
return i;
}
}
return -1;
}
/**
* @brief 执行焦点丢失策略(内部函数)
* @param player 播放器实例
* @param policy 焦点丢失策略
* @return 0成功负数失败
*/
static int __execute_focus_loss_policy(app_player_t *player, app_player_focus_loss_policy_t policy)
{
if (!player) {
return -1;
}
switch (policy) {
case APP_PLAYER_FOCUS_LOSS_PAUSE:
LISA_LOGI(TAG, "Player %s: executing PAUSE policy", player->name);
player->paused_by_focus = true;
// 使用核心层暂停,不操作焦点
pa_manager_control(0, CONFIG_APP_PLAYER_PA_OFF_DELAY_MS);
app_player_core_pause(player);
break;
case APP_PLAYER_FOCUS_LOSS_STOP:
LISA_LOGI(TAG, "Player %s: executing STOP policy", player->name);
player->paused_by_focus = false; // STOP后不可恢复
// 使用核心层停止,不操作焦点
pa_manager_control(0, CONFIG_APP_PLAYER_PA_OFF_DELAY_MS);
app_player_core_stop(player);
break;
case APP_PLAYER_FOCUS_LOSS_DUCK:
LISA_LOGW(TAG, "Player %s: DUCK policy not supported yet", player->name);
// TODO: 实现音量降低逻辑
break;
case APP_PLAYER_FOCUS_LOSS_IGNORE:
default:
LISA_LOGD(TAG, "Player %s: ignoring focus loss", player->name);
break;
}
return 0;
}
/**
* @brief 焦点变化回调桥接函数
*/
static void __audio_focus_change_bridge(focus_state_e state, int by_which_id, void *user_data)
{
app_player_t *player = (app_player_t *)user_data;
if (!player) {
LISA_LOGE(TAG, "Focus callback: player is NULL");
return;
}
// 转换焦点状态
app_player_focus_state_t app_state;
switch (state) {
case FOREGROUND:
app_state = APP_PLAYER_FOCUS_FOREGROUND;
break;
case BACKGROUND:
app_state = APP_PLAYER_FOCUS_BACKGROUND;
break;
case FOCUS_NONE:
default:
app_state = APP_PLAYER_FOCUS_NONE;
break;
}
// 通过通道ID查找触发焦点变化的播放器
app_player_t *by_which_player = __find_player_by_focus_channel_id(by_which_id);
const char *by_which_name = by_which_player ? by_which_player->name :
listen_audiomgr_get_channel_name(s_audio_focus_mgr, by_which_id);
LISA_LOGI(TAG, "Player[%d] %s focus change: %s -> %s (by %s)",
player->id, player->name,
listen_audiomgr_get_state_name(player->last_focus_state),
listen_audiomgr_get_state_name(state),
by_which_name);
// 读取用户注册的焦点回调
PLAYER_MUTEX_LOCK(player->focus_cb_lock, LISA_OS_WAIT_FOREVER);
app_player_focus_change_cb_t user_cb = player->focus_cb;
void *focus_user_data = player->focus_user_data;
PLAYER_MUTEX_UNLOCK(player->focus_cb_lock);
// 调用用户回调
bool handled = false;
if (user_cb) {
handled = user_cb(player, app_state, by_which_player, focus_user_data);
}
// 如果用户回调返回 false执行默认策略
if (!handled) {
// 检查是否是用户主动调用stop/pause导致的焦点释放
if (player->user_initiated_stop &&
(app_state == APP_PLAYER_FOCUS_BACKGROUND || app_state == APP_PLAYER_FOCUS_NONE)) {
LISA_LOGI(TAG, "Player %s: skip focus policy (user-initiated stop/pause)", player->name);
// 用户主动操作,不执行焦点策略
} else if ((app_state == APP_PLAYER_FOCUS_BACKGROUND || app_state == APP_PLAYER_FOCUS_NONE) &&
by_which_player == player) {
LISA_LOGI(TAG, "Player %s: skip focus policy (self-triggered focus change)", player->name);
// 播放器自身触发的焦点变化,说明已完成相应控制
} else if (app_state == APP_PLAYER_FOCUS_FOREGROUND) {
// 获得焦点:检查是否需要自动恢复
if (player->paused_by_focus) {
LISA_LOGI(TAG, "Player %s: auto-resuming from focus pause", player->name);
player->paused_by_focus = false;
// 使用核心层恢复,不操作焦点
pa_manager_control(1, 0);
app_player_core_resume(player);
}
} else if (app_state == APP_PLAYER_FOCUS_BACKGROUND) {
// 变为后景:执行 on_background 策略
__execute_focus_loss_policy(player, player->behavior.on_background);
} else if (app_state == APP_PLAYER_FOCUS_NONE) {
// 失去焦点:执行 on_focus_lost 策略
__execute_focus_loss_policy(player, player->behavior.on_focus_lost);
}
} else {
LISA_LOGD(TAG, "Player %s: focus change handled by user callback", player->name);
}
// 更新上一次焦点状态
player->last_focus_state = app_state;
}
/**
* @brief 初始化焦点管理器
*/
int app_player_focus_init(const app_player_focus_channel_config_t *focus_configs, int focus_config_count)
{
if (s_audio_focus_mgr) {
LISA_LOGW(TAG, "Focus manager already initialized");
return 0;
}
if (!focus_configs || focus_config_count <= 0) {
LISA_LOGI(TAG, "No focus config provided, skip focus manager init");
return 0;
}
LISA_LOGI(TAG, "Initializing audio focus manager with %d channels", focus_config_count);
s_audio_focus_mgr = listen_audiomgr_create();
if (!s_audio_focus_mgr) {
LISA_LOGE(TAG, "Focus init failed: audio focus manager create failed");
return -1;
}
// 保存焦点配置
s_focus_configs = focus_configs;
s_focus_config_count = focus_config_count;
LISA_LOGI(TAG, "Focus manager initialized successfully");
return 0;
}
/**
* @brief 反初始化焦点管理器
*/
void app_player_focus_deinit(void)
{
if (s_audio_focus_mgr) {
listen_audiomgr_destroy(s_audio_focus_mgr);
s_audio_focus_mgr = NULL;
}
s_focus_configs = NULL;
s_focus_config_count = 0;
LISA_LOGI(TAG, "Focus manager deinitialized");
}
/**
* @brief 为播放器注册焦点通道
*/
int app_player_focus_register(app_player_t *player, const char *name)
{
if (!player || !name) {
return -1;
}
if (!s_audio_focus_mgr || !s_focus_configs || s_focus_config_count <= 0) {
LISA_LOGD(TAG, "Focus manager not initialized, skip registration for %s", name);
return 0;
}
// 查找与播放器名称匹配的焦点配置
int focus_index = __find_focus_config_index_by_name(name);
if (focus_index < 0) {
LISA_LOGD(TAG, "No focus config found for player %s", name);
return 0;
}
const app_player_focus_channel_config_t *focus_config = &s_focus_configs[focus_index];
int channel_id = focus_index;
// 转换 capture_names 为 capture_ids
int *capture_ids = NULL;
int capture_count = 0;
if (focus_config->capture_names && focus_config->capture_count > 0) {
capture_ids = (int *)lisa_mem_alloc(sizeof(int) * focus_config->capture_count);
if (!capture_ids) {
return -1;
}
for (int i = 0; i < focus_config->capture_count; i++) {
// 查找 capture_name 对应的通道ID
int target_id = __find_focus_config_index_by_name(focus_config->capture_names[i]);
if (target_id >= 0) {
capture_ids[capture_count++] = target_id;
}
}
}
// 注册通道
int ret = listen_audiomgr_register_channel(
s_audio_focus_mgr,
channel_id,
name,
focus_config->priority,
capture_ids,
capture_count,
__audio_focus_change_bridge,
player // 传递 player 指针作为 user_data
);
if (capture_ids) {
lisa_mem_free(capture_ids);
}
if (ret == 0) {
player->focus_channel_id = channel_id;
// 复制焦点行为配置到播放器实例
player->behavior = focus_config->behavior;
LISA_LOGI(TAG, "Registered focus channel for player %s (id=%d, priority=%d)",
name, channel_id, focus_config->priority);
} else {
LISA_LOGE(TAG, "Register focus channel failed for %s", name);
return -1;
}
return 0;
}
/**
* @brief 注销播放器的焦点通道
*/
int app_player_focus_unregister(app_player_t *player)
{
if (!player) {
return -1;
}
if (s_audio_focus_mgr && player->focus_channel_id >= 0) {
LISA_LOGD(TAG, "Releasing focus for player %s before destroy", player->name);
listen_audiomgr_release_channel(s_audio_focus_mgr, player->focus_channel_id);
player->focus_channel_id = -1;
}
return 0;
}
/**
* @brief 申请音频焦点
*/
int app_player_focus_acquire(app_player_t *player)
{
if (!player) {
return -1;
}
if (s_audio_focus_mgr && player->focus_channel_id >= 0) {
LISA_LOGD(TAG, "Acquiring audio focus for player %s", player->name);
listen_audiomgr_acquire_channel(s_audio_focus_mgr, player->focus_channel_id);
return 0;
}
return -1;
}
/**
* @brief 释放音频焦点
*/
int app_player_focus_release(app_player_t *player, bool is_user_initiated)
{
if (!player) {
return -1;
}
if (s_audio_focus_mgr && player->focus_channel_id >= 0) {
LISA_LOGD(TAG, "Releasing audio focus for player %s (user_initiated=%d)",
player->name, is_user_initiated);
listen_audiomgr_release_channel(s_audio_focus_mgr, player->focus_channel_id);
// 如果是用户主动操作,清除标志
if (is_user_initiated) {
player->user_initiated_stop = false;
}
return 0;
}
return -1;
}
/**
* @brief 注册焦点变化回调
*/
int app_player_focus_register_callback(app_player_t *player,
app_player_focus_change_cb_t callback,
void *user_data)
{
if (!player) {
return -1;
}
if (player->focus_channel_id < 0) {
LISA_LOGW(TAG, "Register focus callback failed: player %s not registered to focus manager",
player->name);
return -1;
}
PLAYER_MUTEX_LOCK(player->focus_cb_lock, LISA_OS_WAIT_FOREVER);
player->focus_cb = callback;
player->focus_user_data = user_data;
PLAYER_MUTEX_UNLOCK(player->focus_cb_lock);
LISA_LOGI(TAG, "Registered focus callback for player %s", player->name);
return 0;
}
/**
* @brief 设置用户主动操作标志
*/
void app_player_focus_set_user_initiated(app_player_t *player, bool initiated)
{
if (player) {
player->user_initiated_stop = initiated;
}
}
bool app_player_focus_is_user_initiated(const app_player_t *player)
{
if (!player) {
return false;
}
return player->user_initiated_stop;
}
/**
* @brief 设置焦点暂停标志
*/
void app_player_focus_set_paused_by_focus(app_player_t *player, bool paused)
{
if (player) {
player->paused_by_focus = paused;
}
}
/**
* @brief 获取焦点暂停标志
*/
bool app_player_focus_is_paused_by_focus(app_player_t *player)
{
return player ? player->paused_by_focus : false;
}
/**
* @brief 设置播放器的焦点行为策略(运行时动态修改)
*/
int app_player_set_focus_behavior(app_player_t *player,
const app_player_focus_behavior_t *behavior)
{
if (!player || !behavior) {
LISA_LOGE(TAG, "Set focus behavior failed: invalid parameter");
return -1;
}
if (player->focus_channel_id < 0) {
LISA_LOGW(TAG, "Player %s has no focus channel registered", player->name);
return -1;
}
// 更新行为配置(使用锁保护)
PLAYER_MUTEX_LOCK(player->focus_cb_lock, LISA_OS_WAIT_FOREVER);
player->behavior = *behavior;
PLAYER_MUTEX_UNLOCK(player->focus_cb_lock);
LISA_LOGI(TAG, "Updated focus behavior for player %s: on_background=%d, on_focus_lost=%d",
player->name, behavior->on_background, behavior->on_focus_lost);
return 0;
}
/**
* @brief 获取播放器当前的焦点行为策略
*/
int app_player_get_focus_behavior(app_player_t *player,
app_player_focus_behavior_t *behavior)
{
if (!player || !behavior) {
LISA_LOGE(TAG, "Get focus behavior failed: invalid parameter");
return -1;
}
if (player->focus_channel_id < 0) {
LISA_LOGW(TAG, "Player %s has no focus channel registered", player->name);
return -1;
}
// 读取行为配置(使用锁保护)
PLAYER_MUTEX_LOCK(player->focus_cb_lock, LISA_OS_WAIT_FOREVER);
*behavior = player->behavior;
PLAYER_MUTEX_UNLOCK(player->focus_cb_lock);
return 0;
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef APP_PLAYER_FOCUS_H
#define APP_PLAYER_FOCUS_H
#include "app_player.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 焦点管理层接口
*
* 本模块负责处理音频焦点相关的所有逻辑:
* - 焦点申请和释放
* - 焦点策略执行(暂停、停止、忽略等)
* - 焦点变化回调处理
* - 焦点状态管理
*/
/**
* @brief 初始化焦点管理器
* @param focus_configs 焦点配置数组
* @param focus_config_count 焦点配置数量
* @return 0 成功,负数失败
*/
int app_player_focus_init(const app_player_focus_channel_config_t *focus_configs, int focus_config_count);
/**
* @brief 反初始化焦点管理器
*/
void app_player_focus_deinit(void);
/**
* @brief 为播放器注册焦点通道
* @param player 播放器实例
* @param name 播放器名称(用于匹配焦点配置)
* @return 0 成功,负数失败
*/
int app_player_focus_register(app_player_t *player, const char *name);
/**
* @brief 注销播放器的焦点通道
* @param player 播放器实例
* @return 0 成功,负数失败
*/
int app_player_focus_unregister(app_player_t *player);
/**
* @brief 申请音频焦点
* @param player 播放器实例
* @return 0 成功,负数失败
*/
int app_player_focus_acquire(app_player_t *player);
/**
* @brief 释放音频焦点
* @param player 播放器实例
* @param is_user_initiated 是否是用户主动操作(影响是否清除 user_initiated_stop 标志)
* @return 0 成功,负数失败
*/
int app_player_focus_release(app_player_t *player, bool is_user_initiated);
/**
* @brief 注册焦点变化回调
* @param player 播放器实例
* @param callback 焦点变化回调函数
* @param user_data 用户数据
* @return 0 成功,负数失败
*/
int app_player_focus_register_callback(app_player_t *player,
app_player_focus_change_cb_t callback,
void *user_data);
/**
* @brief 设置用户主动操作标志
* @param player 播放器实例
* @param initiated 是否是用户主动操作
*/
void app_player_focus_set_user_initiated(app_player_t *player, bool initiated);
/**
* @brief 获取用户主动操作标志
* @param player 播放器实例
* @return true 用户主动操作false 否
*/
bool app_player_focus_is_user_initiated(const app_player_t *player);
/**
* @brief 设置焦点暂停标志
* @param player 播放器实例
* @param paused 是否因焦点而暂停
*/
void app_player_focus_set_paused_by_focus(app_player_t *player, bool paused);
/**
* @brief 获取焦点暂停标志
* @param player 播放器实例
* @return true 因焦点而暂停false 否
*/
bool app_player_focus_is_paused_by_focus(app_player_t *player);
#ifdef __cplusplus
}
#endif
#endif /* APP_PLAYER_FOCUS_H */

View File

@@ -0,0 +1,141 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef APP_PLAYER_INTERNAL_H
#define APP_PLAYER_INTERNAL_H
#include <FreeRTOS.h>
#include <semphr.h>
#include "lisa_semaphore.h"
#include "lisa_thread.h"
#include "lisa_typedef.h"
#include "lisa_player.h"
#include "app_player.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 内部共享定义
*
* 本头文件包含 app_player 各模块之间共享的数据结构和函数声明
*/
// 互斥锁操作宏
#define PLAYER_MUTEX_CREATE() xSemaphoreCreateRecursiveMutex()
#define PLAYER_MUTEX_LOCK(mutex, timeout) \
do { \
if (mutex) { \
TickType_t wait_tick = ((timeout) == LISA_OS_WAIT_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout); \
xSemaphoreTakeRecursive((mutex), wait_tick); \
} \
} while(0)
#define PLAYER_MUTEX_UNLOCK(mutex) \
do { \
if (mutex) { \
xSemaphoreGiveRecursive((mutex)); \
} \
} while(0)
#define PLAYER_MUTEX_DELETE(mutex) \
do { \
if (mutex) { \
vSemaphoreDelete((mutex)); \
(mutex) = NULL; \
} \
} while(0)
/**
* @brief 回调事件项
*/
typedef struct callback_event {
app_player_event_t event; /**< 事件类型 */
struct callback_event *next; /**< 链表指针 */
} callback_event_t;
/**
* @brief 回调事件队列
*/
typedef struct {
callback_event_t *head; /**< 队列头 */
callback_event_t *tail; /**< 队列尾 */
SemaphoreHandle_t lock; /**< 队列锁 */
lisa_semaphore_t *sem; /**< 队列信号量 */
bool running; /**< 线程运行标志 */
} callback_queue_t;
/**
* @brief 回调映射管理
*/
typedef struct {
SemaphoreHandle_t lock; /**< 回调锁 */
app_player_event_cb_t callback; /**< 事件回调函数 */
void *user_data; /**< 用户数据 */
} callback_map_t;
/**
* @brief 播放器实例内部结构
*/
struct app_player_s {
char *name; /**< 播放器名称 */
uint32_t id; /**< 播放器 ID */
PLAYER_HANDLE hld; /**< lisa_player 句柄 */
SemaphoreHandle_t operation_lock; /**< 实例操作互斥锁(保护状态和操作) */
SemaphoreHandle_t core_lock; /**< 核心层操作互斥锁(保护 core 层函数) */
callback_map_t cb_map; /**< 回调管理 */
app_player_state_t state; /**< 当前状态 */
PlayerEvt last_evt; /**< 最后一次事件 */
bool is_preparing; /**< 准备中标志 */
bool wait_prepare_intercepted; /**< 等待准备中断标志 */
bool pause_preparing; /**< 准备时暂停标志 */
lisa_semaphore_t *preparing_sem; /**< 准备同步信号量 */
uint8_t vol_min; /**< 音量最小值 */
uint8_t vol_max; /**< 音量最大值 */
bool is_stream_mode; /**< 流式播放模式标志 */
callback_queue_t *cb_queue; /**< 回调事件队列 */
lisa_thread_t *cb_thread; /**< 回调处理线程 */
#ifdef CONFIG_APP_PLAYER_AUDIO_FOCUS
int focus_channel_id; /**< 焦点通道 ID-1 表示未注册) */
app_player_focus_change_cb_t focus_cb; /**< 焦点变化回调函数 */
void *focus_user_data; /**< 焦点回调用户数据 */
SemaphoreHandle_t focus_cb_lock; /**< 焦点回调锁 */
app_player_focus_behavior_t behavior; /**< 焦点行为配置 */
bool paused_by_focus; /**< 是否因焦点策略而暂停 */
bool user_initiated_stop; /**< 用户主动调用stop/pause标志(用于跳过焦点策略) */
app_player_focus_state_t last_focus_state; /**< 上一次焦点状态 */
#endif
};
/**
* @brief 向回调队列添加事件(内部函数,由 core 层调用)
* @param player 播放器实例
* @param event 事件类型
*/
void __enqueue_callback_event(app_player_t *player, app_player_event_t event);
/**
* @brief 通过焦点通道ID查找播放器实例内部函数由 focus 层调用)
* @param focus_channel_id 焦点通道ID
* @return 播放器实例指针,未找到返回 NULL
*/
app_player_t *__find_player_by_focus_channel_id(int focus_channel_id);
#ifdef __cplusplus
}
#endif
#endif /* APP_PLAYER_INTERNAL_H */

View File

@@ -0,0 +1,636 @@
# 音频焦点管理Audio Focus Management
音频焦点管理是 app_player 的一个**可裁剪功能**,用于协调多个播放器实例之间的音频播放,避免音频冲突。
## 功能概述
**特性:**
- 默认**禁用**,启用需配置 `CONFIG_APP_PLAYER_AUDIO_FOCUS=y`
- 禁用后焦点相关 API 不可用,减少代码体积
- 适用于多播放器场景如音乐、提示音、TTS 需要协调播放)
当应用中存在多个播放器时,焦点管理可以根据预定义的优先级和抢占规则自动处理播放状态切换。
## 核心概念
### 1. 焦点状态Focus State
每个播放器可以处于以下三种焦点状态之一:
- **FOREGROUND前景焦点**: 播放器拥有最高优先级,应该正常播放
- **BACKGROUND背景焦点**: 播放器被更高优先级的播放器抢占,应该暂停或降低音量
- **NONE无焦点**: 播放器没有焦点,应该停止播放
### 2. 优先级Priority
- 每个播放器通道都有一个优先级值(整数)
- **数值越小,优先级越高**0 = 最高优先级)
- 优先级用于决定多个播放器申请焦点时的仲裁顺序
**重要说明:**
- 优先级和抢占列表是**两个独立的维度**
- 优先级主要用于:
1. 当抢占列表**不匹配**时,通过优先级决定谁是前景、谁是背景
2. 决定多个播放器同时竞争时的顺序
### 3. 焦点丢失策略Focus Loss Policy
当播放器失去焦点或降为背景时,可配置不同的处理策略:
| 策略 | 说明 | 使用场景 |
|------|------|---------|
| `APP_PLAYER_FOCUS_LOSS_IGNORE` | 忽略焦点变化,继续播放 | 特殊场景,需始终播放 |
| `APP_PLAYER_FOCUS_LOSS_PAUSE` | 暂停播放(可自动恢复) | 音乐播放被提示音打断 |
| `APP_PLAYER_FOCUS_LOSS_STOP` | 停止播放(不可自动恢复) | 提示音播放结束 |
| `APP_PLAYER_FOCUS_LOSS_DUCK` | 降低音量(暂不支持) | 保留,未来实现 |
**焦点行为配置:**
```c
app_player_focus_behavior_t behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE, // 变为后景时暂停
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP, // 完全失焦时停止
.duck_volume_percent = 30 // 暂不支持
};
```
### 4. 抢占规则Capture Rules
抢占规则是焦点管理的核心机制,通过 `capture_names` 数组定义。
**核心原理:**
1. **白名单机制**
- 每个播放器的 `capture_names` 定义了"我可以抢占哪些播放器"
- 这是一个**显式白名单**,只有在列表中的播放器才会被抢占
- **不在列表中的播放器不会被抢占**,即使优先级更低
2. **抢占与优先级的工作机制**
焦点管理器按以下顺序判断(**三个独立判断**
**判断 1检查是否能够强制抢占最高优先级**
```
if (B 在 A 的 capture_names 中) {
// A 可以强制抢占 B不考虑优先级数值
// B → NONEA 重新申请焦点
return;
}
```
**判断 2检查优先级是否相等**
```
if (A 的优先级 == B 的优先级) {
// 优先级相同,新申请者优先
// B → NONEA → 前景
return;
}
```
**判断 3通过优先级数值仲裁**
```
if (A 的优先级 < B 的优先级) { // 数值小 = 优先级高
// A 优先级更高
// B → 背景A → 前景
} else {
// A 优先级更低
// A → 背景(如果背景为空)
}
```
**关键点:**
- **三个判断是独立的**:抢占列表、优先级相等、优先级比较
- `capture_names` 提供**强制抢占**能力,优先级最高,无视优先级数值
- 不在抢占列表时,完全通过**优先级数值**仲裁
- 优先级相同时,新申请者优先(取代当前前景)
3. **典型配置示例**
**示例 1三级优先级提示音 > TTS > 音乐)**
```c
static const char *tone_captures[] = {"tts", "music"};
static const char *tts_captures[] = {"music"};
static const app_player_focus_channel_config_t focus_configs[] = {
{
.name = "tone",
.priority = 0,
.capture_names = tone_captures,
.capture_count = 2,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "tts",
.priority = 1,
.capture_names = tts_captures,
.capture_count = 1,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "music",
.priority = 2,
.capture_names = NULL,
.capture_count = 0,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
}
};
// 抢占关系:
// - tone (优先级0) 可以抢占 tts 和 music
// - tts (优先级1) 可以抢占 music
// - music(优先级2) 不抢占任何播放器
```
**示例 2选择性抢占理解抢占 vs 优先级仲裁)**
```c
static const char *call_captures[] = {"music"};
static const char *notif_captures[] = {"music"};
static const app_player_focus_channel_config_t focus_configs[] = {
{
.name = "call",
.priority = 0,
.capture_names = call_captures,
.capture_count = 1,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "notification",
.priority = 1,
.capture_names = notif_captures,
.capture_count = 1,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "music",
.priority = 2,
.capture_names = NULL,
.capture_count = 0,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
}
};
// 行为分析:
// 场景 1music 播放中call 申请焦点
// - call 能抢占 musicmusic 在 call 的 capture_names 中)
// - 结果music → NONEcall → 前景
// 场景 2call 播放中notification 申请焦点
// - notification 不能抢占 callcall 不在 notification 的 capture_names 中)
// - 进入优先级仲裁call(0) vs notification(1)
// - call 优先级更高(数值小)
// - 结果call → 前景notification → 背景
// 场景 3notification 播放中call 申请焦点
// - call 不能抢占 notificationnotification 不在 call 的 capture_names 中)
// - 进入优先级仲裁call(0) vs notification(1)
// - call 优先级更高
// - 结果notification → 背景call → 前景
```
4. **设计要点**
- **高优先级播放器**:应在 `capture_names` 中列出需要打断的播放器
- **低优先级播放器**:通常设置 `capture_names = NULL`,不主动抢占
- **避免循环抢占**:不要配置 A 抢占 B同时 B 也抢占 A
- **选择性抢占**:利用白名单机制实现特殊场景(如通话中允许通知音)
## 使用流程
### 第一步:定义焦点通道配置
在初始化 app_player 时,定义各个播放器的焦点配置:
```c
#include "app_player.h"
// 定义可抢占的通道列表
static const char *tone_captures[] = {"music", "tts"};
static const char *tts_captures[] = {"music"};
// 定义焦点通道配置数组
static const app_player_focus_channel_config_t focus_configs[] = {
{
.name = "tone", // 通道名称(需与 create 时的 name 匹配)
.priority = 0, // 最高优先级
.capture_names = tone_captures,
.capture_count = 2, // 可抢占 music 和 tts
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "tts",
.priority = 1, // 中等优先级
.capture_names = tts_captures,
.capture_count = 1, // 可抢占 music
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "music",
.priority = 2, // 最低优先级
.capture_names = NULL,
.capture_count = 0, // 不抢占任何播放器
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
}
};
// 初始化 app_player传入焦点配置
app_player_config_t config = {
.pa_ctrl_callback = pa_control_callback,
.focus_configs = focus_configs,
.focus_config_count = 3
};
app_player_init(&config);
```
### 第二步:创建播放器实例
```c
// 创建播放器(名称必须与焦点配置中的 name 匹配)
app_player_t *tone_player = app_player_create("tone");
app_player_t *tts_player = app_player_create("tts");
app_player_t *music_player = app_player_create("music");
// 如果创建的播放器名称在焦点配置中,会自动注册到焦点管理器
// 如果名称不在配置中,则该播放器不参与焦点管理
```
### 第三步:注册焦点变化回调(可选)
```c
// 定义焦点变化回调函数
// 返回值true=完全接管处理false=执行默认策略
bool music_focus_callback(app_player_t *player,
app_player_focus_state_t state,
app_player_t *by_which,
void *user_data) {
const char *by_name = by_which ? by_which->name : "unknown";
switch (state) {
case APP_PLAYER_FOCUS_FOREGROUND:
printf("Music got foreground (by %s)\n", by_name);
break;
case APP_PLAYER_FOCUS_BACKGROUND:
printf("Music moved to background (by %s)\n", by_name);
break;
case APP_PLAYER_FOCUS_NONE:
printf("Music lost focus (by %s)\n", by_name);
break;
}
// 返回 false让 app_player 根据 behavior 配置自动执行策略
return false;
}
// 注册回调(如果不注册,仍会执行默认策略,只是没有通知)
app_player_register_focus_cb(music_player, music_focus_callback, NULL);
```
**回调函数说明:**
- **返回 false**app_player 根据 `behavior` 配置自动执行策略pause/stop/ignore
- **返回 true**app_player 完全跳过处理,由应用层自行处理
- **不注册回调**:仍会执行 `behavior` 配置的默认策略,只是没有事件通知
### 第四步:播放时自动申请焦点
```c
// 播放时会自动申请焦点,无需手动调用
app_player_play(tone_player, "tone://0");
// → tone 优先级 0会抢占 tts 和 music
// → tts 和 music 会收到焦点变化回调
app_player_play(tts_player, "https://example.com/tts.mp3");
// → tts 优先级 1会抢占 music
// → music 会收到焦点变化回调
app_player_play(music_player, "https://example.com/music.mp3");
// → music 优先级 2不会抢占任何播放器
```
## 焦点管理内部判断流程
焦点管理器维护两个角色:**前景FOREGROUND** 和 **背景BACKGROUND**,同一时刻最多只有一个前景和一个背景。
### 焦点申请流程acquire
当播放器调用 `app_player_play()` 时,会自动申请焦点,焦点管理器按以下流程处理:
```
┌─────────────────────────────────────────────────────────────────┐
│ 播放器申请焦点listen_audiomgr_acquire_channel(requester_id) │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────▼─────────────┐
│ 是否存在前景播放器? │
└────┬─────────────────┬────┘
│是 │否
┌────────────▼───────┐ ┌───▼────────────────────┐
│ 判断与前景的关系 │ │ 是否存在背景播放器? │
└────┬───────────────┘ └───┬────────────────┬───┘
│ │是 │否
┌───────────▼───────────────┐ ┌──────▼─────────┐ ┌──▼──────────┐
│ 能抢占前景? │ │ 比较优先级 │ │ 直接成为前景 │
│ (前景在capture_names中) │ │ (数值小=高) │ └─────────────┘
└──┬────────────────────┬───┘ └──┬─────────────┘
│是 │否 │
┌────▼────────┐ ┌────────▼──────────────────┐
│ 前景→NONE │ │ 比较 requester 与前景优先级│
│ 递归申请 │ └──┬──────────┬───────────┬──┘
└─────────────┘ │ │ │
┌────▼─────┐ ┌──▼──────┐ ┌─▼──────────┐
│ 优先级 │ │ 优先级 │ │ 优先级 │
│ 更高 │ │ 相同 │ │ 更低 │
│ (数值小) │ │ │ │ (数值大) │
└────┬─────┘ └──┬──────┘ └─┬──────────┘
│ │ │
┌──────────▼────┐ ┌──▼──────┐ ┌──▼─────────────┐
│ 前景→背景 │ │前景→NONE│ │ requester→背景 │
│(背景→NONE) │ │req→前景 │ │(如背景为空) │
│ req→前景 │ └─────────┘ └────────────────┘
└───────────────┘
```
### 焦点释放流程release
当播放器停止或播放完成时,自动释放焦点:
```
┌──────────────────────────────────────────────────────┐
│ 播放器释放焦点listen_audiomgr_release_channel(id) │
└───────────────────────┬──────────────────────────────┘
┌─────────▼──────────┐
│ 设置该通道为 NONE │
└─────────┬──────────┘
┌─────────────▼──────────────┐
│ 该通道是否是前景播放器? │
└────┬─────────────────┬─────┘
│是 │否
┌──────────▼────────┐ ┌───▼────────────┐
│ 是否存在背景? │ │ 无需处理 │
└────┬─────────┬────┘ └────────────────┘
│是 │否
┌────▼───┐ ┌─▼────────┐
│ 背景→ │ │ 无前景 │
│ 前景 │ │ 无背景 │
└────────┘ └──────────┘
```
## 焦点状态转换示例
假设有三个播放器tone优先级0、tts优先级1、music优先级2
**场景 1音乐播放时播放提示音**
```
初始状态:
tone: NONE tts: NONE music: FOREGROUND (正在播放)
执行app_player_play(tone_player, ...)
结果:
tone: FOREGROUND (正在播放)
tts: NONE
music: BACKGROUND (被暂停,收到回调 by_which=tone播放器)
```
**场景 2提示音播放完成**
```
初始状态:
tone: FOREGROUND (正在播放)
music: BACKGROUND (被暂停)
事件tone 播放完成,自动释放焦点
结果:
tone: NONE
music: FOREGROUND (恢复播放,收到回调 by_which=tone播放器)
```
**场景 3音乐播放时播放 TTS然后播放提示音**
```
初始music: FOREGROUND
执行app_player_play(tts_player, ...)
结果tts: FOREGROUND, music: BACKGROUND
执行app_player_play(tone_player, ...)
结果tone: FOREGROUND, tts: BACKGROUND, music: NONE
tone 播放完成:
结果tone: NONE, tts: FOREGROUND, music: NONE
tts 播放完成:
结果tts: NONE, music: NONE (music 已被完全停止,不会自动恢复)
```
## API 参考
### 焦点相关数据结构
```c
// 焦点状态枚举
typedef enum {
APP_PLAYER_FOCUS_FOREGROUND = 0, // 前景焦点
APP_PLAYER_FOCUS_BACKGROUND, // 背景焦点
APP_PLAYER_FOCUS_NONE // 无焦点
} app_player_focus_state_t;
// 焦点丢失策略
typedef enum {
APP_PLAYER_FOCUS_LOSS_IGNORE = 0, // 忽略焦点变化
APP_PLAYER_FOCUS_LOSS_PAUSE, // 暂停播放(可恢复)
APP_PLAYER_FOCUS_LOSS_STOP, // 停止播放(不可恢复)
APP_PLAYER_FOCUS_LOSS_DUCK, // 降低音量(暂不支持)
} app_player_focus_loss_policy_t;
// 焦点行为配置
typedef struct {
app_player_focus_loss_policy_t on_background; // 变为后景时的策略
app_player_focus_loss_policy_t on_focus_lost; // 完全失焦时的策略
uint8_t duck_volume_percent; // 降低音量百分比(暂不支持)
} app_player_focus_behavior_t;
// 焦点变化回调函数类型
// 返回值true=完全接管处理false=执行默认策略
typedef bool (*app_player_focus_change_cb_t)(
app_player_t *player, // 播放器实例
app_player_focus_state_t new_state, // 新的焦点状态
app_player_t *by_which, // 触发焦点变化的播放器句柄
void *user_data // 用户自定义数据
);
// 焦点通道配置(在 app_player_init 时使用)
typedef struct {
const char *name; // 通道名称(需与 create 时的 name 匹配)
int priority; // 优先级0 = 最高)
const char **capture_names; // 可抢占的通道名称列表
int capture_count; // 抢占列表长度
app_player_focus_behavior_t behavior; // 焦点丢失时的行为配置
} app_player_focus_channel_config_t;
```
### 焦点相关 API
| 函数 | 说明 |
|------|------|
| `app_player_register_focus_cb()` | 注册焦点变化回调(可选) |
| `app_player_set_focus_behavior()` | 运行时动态修改焦点行为策略 |
| `app_player_get_focus_behavior()` | 获取当前的焦点行为策略 |
**注意:** 焦点的申请和释放是自动的:
- 调用 `app_player_play()` 时自动申请焦点
- 播放完成或停止时自动释放焦点
**焦点功能编译控制:**
- 仅在 `CONFIG_APP_PLAYER_AUDIO_FOCUS=y` 时,焦点相关 API 和数据结构才可用
- 禁用后可减少代码体积,适合单播放器场景
## 设计原则与注意事项
1. **通道名称必须匹配**
- 焦点配置中的 `name` 必须与 `app_player_create(name)` 的参数完全一致
- 不在焦点配置中的播放器不参与焦点管理
2. **优先级设计建议**
- 提示音(系统音效):优先级 0最高
- TTS/语音播报:优先级 1
- 音乐/背景音:优先级 2最低
3. **抢占规则设计**
- 只配置必要的抢占关系,避免循环依赖
- 高优先级播放器应该可以抢占低优先级播放器
- 低优先级播放器通常不抢占任何其他播放器
4. **焦点行为配置**
- 在 `app_player_init()` 时通过 `focus_configs` 配置每个通道的默认行为
- 运行时可通过 `app_player_set_focus_behavior()` 动态修改
- 不同播放器可配置不同的焦点丢失策略pause/stop/ignore
5. **回调函数注意事项**
- 回调函数应快速返回,避免阻塞焦点管理器
- 耗时操作(如网络请求)应提交到任务队列异步执行
- 回调返回 `false` 时app_player 自动根据 `behavior` 配置执行策略
- 回调返回 `true` 时app_player 当前不执行注册的策略,由应用层自行处理
6. **默认行为**
- 如果不注册焦点回调app_player 仍会根据 `behavior` 配置自动执行策略
- 大多数场景下,配置好 `behavior` 即可,无需注册回调
7. **禁用焦点管理**
- 如果应用只使用单个播放器,建议禁用 `CONFIG_APP_PLAYER_AUDIO_FOCUS` 减少代码体积
- 禁用后,焦点相关的 API 和数据结构将不可用
- 默认情况下焦点功能是禁用的
## 完整示例
```c
#include "app_player.h"
// 1. 定义焦点配置
static const char *tone_captures[] = {"music", "tts"};
static const char *tts_captures[] = {"music"};
static const app_player_focus_channel_config_t focus_configs[] = {
{
.name = "tone",
.priority = 0,
.capture_names = tone_captures,
.capture_count = 2,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "tts",
.priority = 1,
.capture_names = tts_captures,
.capture_count = 1,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
},
{
.name = "music",
.priority = 2,
.capture_names = NULL,
.capture_count = 0,
.behavior = {
.on_background = APP_PLAYER_FOCUS_LOSS_PAUSE,
.on_focus_lost = APP_PLAYER_FOCUS_LOSS_STOP,
}
}
};
// 2. 初始化 app_player
void audio_init(void) {
app_player_config_t config = {
.pa_ctrl_callback = pa_ctrl,
.focus_configs = focus_configs,
.focus_config_count = 3
};
app_player_init(&config);
}
// 3. 创建播放器并注册回调(可选)
app_player_t *tone, *tts, *music;
// 焦点变化通知回调(可选,仅用于监听焦点变化)
bool music_focus_cb(app_player_t *p, app_player_focus_state_t state,
app_player_t *by, void *ud) {
const char *by_name = by ? by->name : "unknown";
printf("Music focus: %d (by %s)\n", state, by_name);
return false; // 让 app_player 执行默认策略
}
void create_players(void) {
tone = app_player_create("tone");
tts = app_player_create("tts");
music = app_player_create("music");
// 注册回调(可选,仅用于监听焦点变化)
app_player_register_focus_cb(music, music_focus_cb, NULL);
}
// 4. 使用(焦点自动管理)
void play_samples(void) {
app_player_play(music, "https://example.com/music.mp3");
// music 获得焦点开始播放
app_player_play(tone, "tone://beep");
// tone 抢占 musicmusic 根据 behavior 配置自动暂停
// tone 播放完成后music 自动恢复
}
```

View File

@@ -0,0 +1,7 @@
listenai_library_sources(
listen_audiomgr.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,280 @@
#define TAG "audiomgr"
#include <stdio.h>
#include <string.h>
#include "listen_audiomgr.h"
#include "lisa_mem.h"
#include "lisa_log.h"
static const char *s_state_names[] = {
[FOREGROUND] = "FOREGROUND",
[BACKGROUND] = "BACKGROUND",
[FOCUS_NONE] = "NONE",
};
const char *listen_audiomgr_get_state_name(focus_state_e state)
{
if (state > FOCUS_NONE) {
return "UNKNOWN";
}
return s_state_names[state];
}
const char *listen_audiomgr_get_channel_name(listen_audiomgr_t *handle, int id)
{
if (handle == NULL) {
return "UNKNOWN";
}
for (int i = 0; i < handle->channel_count; i++) {
if (handle->channels[i].id == id && handle->channels[i].registered) {
return handle->channels[i].name ? handle->channels[i].name : "UNKNOWN";
}
}
return "UNKNOWN";
}
listen_audiomgr_t *listen_audiomgr_create(void)
{
listen_audiomgr_t *handle = (listen_audiomgr_t *)lisa_mem_calloc(1, sizeof(listen_audiomgr_t));
if (handle == NULL) {
LISA_LOGE(TAG, "Failed to allocate listen_audiomgr_t");
return NULL;
}
handle->channel_count = 0;
handle->foreground_id = -1;
handle->background_id = -1;
for (int i = 0; i < MAX_PLAYER_COUNT; i++) {
handle->channels[i].registered = false;
handle->channels[i].state = FOCUS_NONE;
}
LISA_LOGI(TAG, "listen_audiomgr created");
return handle;
}
void listen_audiomgr_destroy(listen_audiomgr_t *handle)
{
if (handle != NULL) {
lisa_mem_free(handle);
LISA_LOGI(TAG, "listen_audiomgr destroyed");
}
}
int listen_audiomgr_register_channel(listen_audiomgr_t *handle,
int id,
const char *name,
int priority,
int *capture_ids,
int capture_count,
void (*on_focus_change)(focus_state_e, int, void *),
void *user_data)
{
if (handle == NULL) {
LISA_LOGE(TAG, "handle is NULL");
return -1;
}
if (handle->channel_count >= MAX_PLAYER_COUNT) {
LISA_LOGE(TAG, "Max channel count reached");
return -1;
}
// 检查ID是否已存在
for (int i = 0; i < handle->channel_count; i++) {
if (handle->channels[i].id == id && handle->channels[i].registered) {
LISA_LOGE(TAG, "Channel id %d already registered", id);
return -1;
}
}
// 注册新通道
focus_channel_t *channel = &handle->channels[handle->channel_count];
channel->id = id;
channel->name = (char *)name;
channel->priority = priority;
channel->state = FOCUS_NONE;
channel->on_focus_change = on_focus_change;
channel->user_data = user_data;
channel->registered = true;
// 复制抢占列表
int copy_count = (capture_count > MAX_CAPTURE_COUNT) ? MAX_CAPTURE_COUNT : capture_count;
channel->capture_count = copy_count;
if (capture_ids != NULL && copy_count > 0) {
memcpy(channel->capture_ids, capture_ids, copy_count * sizeof(int));
}
handle->channel_count++;
LISA_LOGI(TAG, "Channel registered: id=%d, name=%s, priority=%d, capture_count=%d",
id, name ? name : "NULL", priority, copy_count);
return 0;
}
/**
* @brief 根据ID查找通道
*/
static focus_channel_t *_get_channel_by_id(listen_audiomgr_t *handle, int id)
{
for (int i = 0; i < handle->channel_count; i++) {
if (handle->channels[i].id == id && handle->channels[i].registered) {
return &handle->channels[i];
}
}
return NULL;
}
/**
* @brief 检查 requester 是否可以抢占 target
*/
static bool _can_capture(focus_channel_t *requester, focus_channel_t *target)
{
if (requester == NULL || target == NULL) {
return false;
}
for (int i = 0; i < requester->capture_count; i++) {
if (requester->capture_ids[i] == target->id) {
return true;
}
}
return false;
}
/**
* @brief 设置通道焦点状态
*/
static void _set_channel_focus(listen_audiomgr_t *handle, int id, focus_state_e state, int by_which_id)
{
focus_channel_t *channel = _get_channel_by_id(handle, id);
if (channel == NULL) {
LISA_LOGE(TAG, "Channel id %d not found", id);
return;
}
if (channel->state == state) {
return; // 状态未变化
}
// 更新前景/背景指针
if (handle->foreground_id == id && state != FOREGROUND) {
handle->foreground_id = -1;
} else if (handle->background_id == id && state != BACKGROUND) {
handle->background_id = -1;
}
if (state == FOREGROUND) {
handle->foreground_id = id;
} else if (state == BACKGROUND) {
handle->background_id = id;
}
channel->state = state;
// 获取触发焦点变化的通道名称(仅用于日志)
const char *by_which_name = listen_audiomgr_get_channel_name(handle, by_which_id);
LISA_LOGI(TAG, "Channel focus: id=%d, state=%s, by_which=%s",
id, listen_audiomgr_get_state_name(state), by_which_name);
// 调用回调,传递 by_which_id
if (channel->on_focus_change != NULL) {
channel->on_focus_change(state, by_which_id, channel->user_data);
}
}
void listen_audiomgr_acquire_channel(listen_audiomgr_t *handle, int id)
{
if (handle == NULL) {
LISA_LOGE(TAG, "handle is NULL");
return;
}
LISA_LOGI(TAG, "Channel acquire: id=%d, name=%s", id, listen_audiomgr_get_channel_name(handle, id));
focus_channel_t *requester = _get_channel_by_id(handle, id);
if (requester == NULL) {
LISA_LOGE(TAG, "Channel id %d not registered", id);
return;
}
focus_channel_t *foreground = (handle->foreground_id >= 0) ?
_get_channel_by_id(handle, handle->foreground_id) : NULL;
focus_channel_t *background = (handle->background_id >= 0) ?
_get_channel_by_id(handle, handle->background_id) : NULL;
if (foreground != NULL) {
// 存在前景通道
if (_can_capture(requester, foreground)) {
// 请求者可以抢占当前前景设置当前前景为NONE再重新请求
_set_channel_focus(handle, foreground->id, FOCUS_NONE, id);
listen_audiomgr_acquire_channel(handle, id);
} else if (requester->priority == foreground->priority) {
// 优先级相同设置当前前景为NONE请求者变为前景
_set_channel_focus(handle, foreground->id, FOCUS_NONE, id);
_set_channel_focus(handle, id, FOREGROUND, id);
} else if (requester->priority < foreground->priority) {
// 请求者优先级更高(数值小),当前前景变为背景,请求者变为前景
if (background != NULL) {
_set_channel_focus(handle, background->id, FOCUS_NONE, id);
}
_set_channel_focus(handle, foreground->id, BACKGROUND, id);
_set_channel_focus(handle, id, FOREGROUND, id);
} else {
// 请求者优先级更低,请求者只能变为背景
if (background == NULL) {
_set_channel_focus(handle, id, BACKGROUND, id);
} else if (requester->priority <= foreground->priority) {
_set_channel_focus(handle, background->id, FOCUS_NONE, id);
_set_channel_focus(handle, id, BACKGROUND, id);
}
}
} else if (background != NULL) {
// 无前景但有背景
if (requester->priority < background->priority) {
// 请求者优先级更高,直接变为前景
_set_channel_focus(handle, id, FOREGROUND, id);
} else if (requester->priority == background->priority) {
// 优先级相同
if (background->id == id) {
// 同一个通道,从背景升为前景
_set_channel_focus(handle, id, FOREGROUND, id);
} else {
// 不同通道当前背景变NONE请求者变前景
_set_channel_focus(handle, background->id, FOCUS_NONE, id);
_set_channel_focus(handle, id, FOREGROUND, id);
}
} else {
// 请求者优先级更低,背景升前景,请求者变背景
_set_channel_focus(handle, background->id, FOREGROUND, id);
_set_channel_focus(handle, id, BACKGROUND, id);
}
} else {
// 无前景也无背景,直接变为前景
_set_channel_focus(handle, id, FOREGROUND, id);
}
}
void listen_audiomgr_release_channel(listen_audiomgr_t *handle, int id)
{
if (handle == NULL) {
LISA_LOGE(TAG, "handle is NULL");
return;
}
LISA_LOGI(TAG, "Channel release: id=%d", id);
focus_channel_t *channel = _get_channel_by_id(handle, id);
focus_state_e prev_state = channel ? channel->state : FOCUS_NONE;
// 设置释放的通道为NONE
_set_channel_focus(handle, id, FOCUS_NONE, id);
// 如果无前景但有背景,背景升为前景
if (prev_state == FOREGROUND && handle->foreground_id < 0 && handle->background_id >= 0) {
_set_channel_focus(handle, handle->background_id, FOREGROUND, id);
}
}

View File

@@ -0,0 +1,97 @@
#ifndef __LISTENAI_AUDIOMGR_H__
#define __LISTENAI_AUDIOMGR_H__
#include <stdbool.h>
#define MAX_PLAYER_COUNT 8
#define MAX_CAPTURE_COUNT 8
typedef enum { FOREGROUND = 0, BACKGROUND, FOCUS_NONE } focus_state_e;
/**
* @brief 焦点通道结构体
*/
typedef struct focus_channel_s {
int id; // 通道ID
char *name; // 通道名称
int priority; // 优先级,数值越小优先级越高
int capture_ids[MAX_CAPTURE_COUNT]; // 可抢占的通道ID列表
int capture_count; // 抢占列表长度
focus_state_e state; // 当前焦点状态
void (*on_focus_change)(focus_state_e state, int by_which_id, void *user_data); // 焦点变化回调
void *user_data; // 回调用户数据
bool registered; // 是否已注册
} focus_channel_t;
/**
* @brief 焦点管理器结构体
*/
typedef struct listen_audiomgr_s {
focus_channel_t channels[MAX_PLAYER_COUNT]; // 通道数组
int channel_count; // 已注册通道数量
int foreground_id; // 当前前景通道ID-1表示无
int background_id; // 当前背景通道ID-1表示无
} listen_audiomgr_t;
/**
* @brief 创建焦点管理器
* @return 焦点管理器句柄
*/
listen_audiomgr_t *listen_audiomgr_create(void);
/**
* @brief 销毁焦点管理器
* @param handle 焦点管理器句柄
*/
void listen_audiomgr_destroy(listen_audiomgr_t *handle);
/**
* @brief 注册焦点通道
* @param handle 焦点管理器句柄
* @param id 通道ID
* @param name 通道名称
* @param priority 优先级
* @param capture_ids 可抢占的通道ID列表
* @param capture_count 抢占列表长度
* @param on_focus_change 焦点变化回调
* @return 0 成功,其他失败
*/
int listen_audiomgr_register_channel(listen_audiomgr_t *handle,
int id,
const char *name,
int priority,
int *capture_ids,
int capture_count,
void (*on_focus_change)(focus_state_e, int, void *),
void *user_data);
/**
* @brief 请求焦点
* @param handle 焦点管理器句柄
* @param id 通道ID
*/
void listen_audiomgr_acquire_channel(listen_audiomgr_t *handle, int id);
/**
* @brief 释放焦点
* @param handle 焦点管理器句柄
* @param id 通道ID
*/
void listen_audiomgr_release_channel(listen_audiomgr_t *handle, int id);
/**
* @brief 获取通道名称(用于日志)
* @param handle 焦点管理器句柄
* @param id 通道ID
* @return 通道名称
*/
const char *listen_audiomgr_get_channel_name(listen_audiomgr_t *handle, int id);
/**
* @brief 获取焦点状态名称(用于日志)
* @param state 焦点状态
* @return 状态名称
*/
const char *listen_audiomgr_get_state_name(focus_state_e state);
#endif

View File

@@ -0,0 +1,144 @@
# 本地提示音使用指南
本地提示音功能允许将音频文件打包到 Flash 中,实现快速离线播放。
## 功能概述
本地提示音系统提供:
- 将多个音频文件打包成单个二进制文件
- 烧录到 Flash 后快速访问
- 无需网络或文件系统,启动即可播放
- 适合系统提示音、语音提示等场景
## 使用流程
### 1. 准备音频文件
音频文件需满足以下格式要求:
- **位深度**: 16 bit硬件固定
- **采样率**: 16 kHz推荐其他采样率会自动重采样
- **格式**: 支持 PCM、MP3、WAV、AAC、M4A
### 2. 打包音频文件
使用 `tools/tone_tool` 工具将音频文件打包成二进制文件:
```bash
cd tools/tone_tool
# 将音频文件放入 ring/ 目录,使用数字前缀命名
# 例如000_greeting.mp3, 001_network_suc.mp3
# 执行打包脚本
./run.sh
```
打包工具会生成:
- `ring/tone.bin` - 音频二进制数据(需烧录到 Flash
- `ring/tone.h` - C 头文件,包含音频 ID 枚举定义(如 `TONE_ID_0`, `TONE_ID_1`
详细使用方法请参考:`tools/tone_tool/README.md`
### 3. 烧录到设备
将生成的 `tone.bin` 烧录到设备 Flash 的指定地址(例如 `0x30100000`)。
**注意:** 烧录地址需要根据实际硬件配置确定,确保不与其他数据冲突。
### 4. 代码中使用
```c
#include "app_tone.h"
#include "tone.h" // 包含工具生成的头文件
// 初始化本地提示音(指定 Flash 地址)
app_tone_init(0x30100000);
// 获取提示音 URL使用工具生成的 TONE_ID
const char *url = app_tone_get_url(TONE_ID_0); // 对应 000_greeting.mp3
// 播放提示音
app_player_play(player, url);
```
## 音频 ID 说明
工具会按音频文件名排序生成对应的枚举 ID
- `000_greeting.mp3``TONE_ID_0`
- `001_network_suc.mp3``TONE_ID_1`
- `002_network_fail.mp3``TONE_ID_2`
- ...
**命名规则:**
- 文件名必须以三位数字开头000-999
- 数字决定生成的 ID 顺序
- 数字后可跟任意描述性文本
## 完整示例
### 准备音频文件
```bash
# 音频文件列表
ring/
├── 000_boot_up.mp3 # 开机提示音
├── 001_wifi_connected.mp3 # WiFi 连接成功
├── 002_wifi_failed.mp3 # WiFi 连接失败
└── 003_shutdown.mp3 # 关机提示音
```
### 打包并烧录
```bash
cd tools/tone_tool
./run.sh
# 生成文件:
# ring/tone.bin - 烧录到 Flash
# ring/tone.h - 复制到项目中
```
### 代码集成
```c
#include "app_player.h"
#include "app_tone.h"
#include "tone.h"
void tone_example(void) {
// 初始化提示音系统
app_tone_init(0x30100000);
// 创建播放器
app_player_t *player = app_player_create("tone");
// 播放开机提示音
const char *boot_url = app_tone_get_url(TONE_ID_0);
app_player_play(player, boot_url);
// 等待播放完成...
// 播放 WiFi 连接成功提示音
const char *wifi_ok_url = app_tone_get_url(TONE_ID_1);
app_player_play(player, wifi_ok_url);
}
```
## 注意事项
1. **Flash 地址规划**
- 确保烧录地址不与其他数据(如固件、配置)冲突
- 建议在分区表中明确预留提示音区域
2. **音频文件大小**
- Flash 空间有限,注意控制音频文件总大小
- 建议使用 MP3 格式压缩音频
3. **初始化时机**
- 必须在播放提示音前调用 `app_tone_init()`
- 通常在系统初始化阶段调用一次即可
4. **URL 格式**
- `app_tone_get_url()` 返回的 URL 格式为 `tone://ID`
- 可以直接传递给 `app_player_play()`

View File

@@ -0,0 +1,182 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#define TAG "PA_MGR"
#include "pa_manager.h"
#include "lisa_log.h"
#include "lisa_mem.h"
#include "lisa_mutex.h"
#include "lisa_timer.h"
typedef struct {
pa_ctrl_callback_t ctrl_callback; /* PA控制回调函数 */
lisa_timer_t *timer; /* 延时定时器 */
lisa_mutex_t *mutex; /* 互斥锁 */
int current_state; /* 当前PA状态: 1=ON, 0=OFF */
int pending_state; /* 待执行的PA状态 */
} pa_manager_t;
static pa_manager_t *s_pa_mgr = NULL;
/**
* @brief 定时器超时回调函数
* @note 定时器回调执行时定时器已停止不会与pa_manager_control并发
*/
static void pa_timer_callback(lisa_timer_t *timer)
{
if (!s_pa_mgr || !timer) {
return;
}
/* 执行待执行的PA状态 */
if (s_pa_mgr->ctrl_callback) {
int ret = s_pa_mgr->ctrl_callback(s_pa_mgr->pending_state);
if (ret == 0) {
s_pa_mgr->current_state = s_pa_mgr->pending_state;
LISA_LOGI(TAG, "PA %s (delayed)", s_pa_mgr->current_state ? "ON" : "OFF");
} else {
LISA_LOGE(TAG, "PA control failed: %d", ret);
}
}
}
/**
* @brief 初始化 PA 管理器
*/
int pa_manager_init(const pa_manager_config_t *config)
{
if (!config || !config->ctrl_callback) {
LISA_LOGE(TAG, "Invalid config or callback is NULL");
return -1;
}
if (s_pa_mgr) {
LISA_LOGW(TAG, "PA manager already initialized");
return 0;
}
/* 分配内存 */
s_pa_mgr = (pa_manager_t *)lisa_mem_alloc(sizeof(pa_manager_t));
if (!s_pa_mgr) {
LISA_LOGE(TAG, "Failed to allocate memory");
return -2;
}
/* 初始化配置 */
s_pa_mgr->ctrl_callback = config->ctrl_callback;
s_pa_mgr->current_state = 0; /* 初始状态为关闭 */
s_pa_mgr->pending_state = 0;
/* 创建互斥锁 */
s_pa_mgr->mutex = lisa_mutex_create();
if (!s_pa_mgr->mutex) {
LISA_LOGE(TAG, "Failed to create mutex");
lisa_mem_free(s_pa_mgr);
s_pa_mgr = NULL;
return -3;
}
/* 创建定时器使用默认周期1000ms实际使用时会动态修改 */
s_pa_mgr->timer = lisa_timer_create(1000, pa_timer_callback, s_pa_mgr);
if (!s_pa_mgr->timer) {
LISA_LOGE(TAG, "Failed to create timer");
lisa_mutex_delete(s_pa_mgr->mutex);
lisa_mem_free(s_pa_mgr);
s_pa_mgr = NULL;
return -4;
}
LISA_LOGI(TAG, "PA manager initialized successfully");
return 0;
}
/**
* @brief 控制 PA 开关
*/
int pa_manager_control(int onoff, uint32_t delay_ms)
{
if (!s_pa_mgr) {
LISA_LOGE(TAG, "PA manager not initialized");
return -1;
}
lisa_mutex_lock(s_pa_mgr->mutex, LISA_OS_WAIT_FOREVER);
/* 停止之前的定时器 */
lisa_timer_stop(s_pa_mgr->timer);
if (delay_ms == 0) {
/* 立即执行 */
if (s_pa_mgr->ctrl_callback) {
int ret = s_pa_mgr->ctrl_callback(onoff);
if (ret == 0) {
s_pa_mgr->current_state = onoff;
LISA_LOGI(TAG, "PA %s (immediate)", onoff ? "ON" : "OFF");
} else {
LISA_LOGE(TAG, "PA control failed: %d", ret);
lisa_mutex_unlock(s_pa_mgr->mutex);
return ret;
}
}
} else {
/* 延时执行 */
s_pa_mgr->pending_state = onoff;
/* 修改定时器周期并启动 */
lisa_timer_change_period(s_pa_mgr->timer, delay_ms);
lisa_timer_start(s_pa_mgr->timer);
LISA_LOGI(TAG, "PA will %s after %u ms", onoff ? "ON" : "OFF", delay_ms);
}
lisa_mutex_unlock(s_pa_mgr->mutex);
return 0;
}
/**
* @brief 获取当前 PA 状态
*/
int pa_manager_get_state(void)
{
if (!s_pa_mgr) {
return 0;
}
int state;
lisa_mutex_lock(s_pa_mgr->mutex, LISA_OS_WAIT_FOREVER);
state = s_pa_mgr->current_state;
lisa_mutex_unlock(s_pa_mgr->mutex);
return state;
}
/**
* @brief 反初始化 PA 管理器
*/
void pa_manager_deinit(void)
{
if (!s_pa_mgr) {
return;
}
/* 停止并销毁定时器 */
if (s_pa_mgr->timer) {
lisa_timer_stop(s_pa_mgr->timer);
lisa_timer_destroy(s_pa_mgr->timer);
}
/* 销毁互斥锁 */
if (s_pa_mgr->mutex) {
lisa_mutex_delete(s_pa_mgr->mutex);
}
/* 释放内存 */
lisa_mem_free(s_pa_mgr);
s_pa_mgr = NULL;
LISA_LOGI(TAG, "PA manager deinitialized");
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __LISTENAI_PA_MANAGER_H__
#define __LISTENAI_PA_MANAGER_H__
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief PA 控制回调函数类型
* @param onoff 1: 打开PA, 0: 关闭PA
* @return 0: 成功, 其他值: 失败
*/
typedef int (*pa_ctrl_callback_t)(int onoff);
/**
* @brief PA 管理器配置结构体
*/
typedef struct {
pa_ctrl_callback_t ctrl_callback; /* PA控制回调函数 */
} pa_manager_config_t;
/**
* @brief 初始化 PA 管理器
* @param config 配置参数
* @return 0: 成功, 其他值: 失败
*
* @note 必须在使用其他PA管理器功能前调用此函数
* @note config中的ctrl_callback不能为NULL
*/
int pa_manager_init(const pa_manager_config_t *config);
/**
* @brief 控制 PA 开关
* @param onoff 1: 打开PA, 0: 关闭PA
* @param delay_ms 延时时间(毫秒)
* 0: 立即执行开/关动作
* 其他值: 延时指定毫秒数后执行开/关动作
* @return 0: 成功, 其他值: 失败
*
* @example
* // 立即打开PA
* pa_manager_control(1, 0);
*
* // 延时3秒后打开PA
* pa_manager_control(1, 3000);
*
* // 立即关闭PA
* pa_manager_control(0, 0);
*
* // 延时500毫秒后关闭PA
* pa_manager_control(0, 500);
*/
int pa_manager_control(int onoff, uint32_t delay_ms);
/**
* @brief 获取当前 PA 状态
* @return 1: PA已打开, 0: PA已关闭
*/
int pa_manager_get_state(void);
/**
* @brief 反初始化 PA 管理器
* @note 释放PA管理器占用的资源
*/
void pa_manager_deinit(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2025, LISTENAI
# SPDX-License-Identifier: Apache-2.0
# add_subdirectory(lisa_player)
listenai_link_libraries(m)
listenai_link_libraries(${CMAKE_CURRENT_SOURCE_DIR}/liblisa_player.a)
listenai_link_libraries(${CMAKE_CURRENT_SOURCE_DIR}/libplatform_track.a)
listenai_include_directories(./)
listenai_library_sources(lisa_player_adapter.c)

Binary file not shown.

View File

@@ -0,0 +1,298 @@
/**
* @brief 播放器接口
* @author mokee
* @date 2022-05-10
*/
#ifndef __LISTENAI_PLAYER_INTERFACE_H__
#define __LISTENAI_PLAYER_INTERFACE_H__
#include <stdint.h>
#define PLAYER_HANDLE void *
typedef enum PlayerState {
PLAYER_ST_ERROR = 0, // 错误
PLAYER_ST_NONE,
PLAYER_ST_READY_TO_PLAY, //准备去播
PLAYER_ST_PREPARED, // 缓冲完成
PLAYER_ST_PLAYING, // 播放中
PLAYER_ST_PAUSED, // 暂停
PLAYER_ST_STOPED, // 停止
PLAYER_ST_PLAYBACK_COMPLETE, // 播放完成
} PlayerState;
typedef enum PlayerEvt {
PLAYER_EVT_ERROR = 0, // 播放错误
PLAYER_EVT_PREPARED, // 缓冲完成
PLAYER_EVT_PLAYING, // 播放中
PLAYER_EVT_PAUSED, // 已暂停
PLAYER_EVT_STOPED, // 已停止
PLAYER_EVT_NEARLY_COMPLETE, // 即将播放完成
PLAYER_EVT_PLAYBACK_COMPLETE, // 播放完成
PLAYER_EVT_SEEKING, // Seek中
PLAYER_EVT_SEEK_COMPLETE, // Seek完成
PLAYER_EVT_INIT = 100, // 初始化状态
} PlayerEvt;
typedef enum PlayerErr {
PLAYER_OK = 0, // Normal
PLAYER_ERR = -1, // Common error
PLAYER_OP_FAIL = -2, // Operation fail
PLAYER_NO_MEMORY = -3, // Not enough memory
PLAYER_INVALID_HANDLE = -4, // Invalid handle
PLAYER_INVALID_STATE = -5, // Invalid state
PLAYER_INVALID_CTX = -6, // Invalid context
PLAYER_INVALID_PARAM = -7, // Invalid parameter
PLAYER_INVALID_FMT = -8, // Invalid resource
// ......
PLAYER_DEMUX_ERR = -100, // Demux error
PLAYER_DEMUX_ERR_SEEK = -101, // Demux seek fail
PLAYER_DEMUX_ERR_FORMAT = -102, // format error
PLAYER_DEMUX_ERR_READ = -103, // read error
PLAYER_DEMUX_SEEK_UNSUPPORT = -104, // unsupport seek
PLAYER_DEMUX_SEEK_ERRORTIME = -105, // seek time erroe
// ......
PLAYER_DECODE_ERR = -200, // Decode fail
PLAYER_DECODE_INIT_ERR = -201, // Decode init error
PLAYER_DECODE_FORMAT_ERR = -202, // Error format
PLAYER_DECODE_INIT_RESAMPLE_ERR = -203, // Init resample err
PLAYER_DECODE_UNSUPPORT_CHL_ERR = -204, // Unsupport channel
// ......
PLAYER_PLAY_ERR = -300, // Play fail
PLAYER_PLAY_TRACK_INIT_ERR = -301, // Play track init fail
PLAYER_PLAY_WRITE_ERR = -302, // Play track write error
// ......
PLAYER_PROBE_FORMAT_ERROR = -401, // probe format error
PLAYER_PROBE_FORMAT_UNKNOW = -402, // probe format unkonw
// ......
PLAYER_AUTH_FAIL = -501, // auth check fail
} PlayerErr;
/** 播放事件回调 */
typedef int (*player_callback)(PlayerEvt evt, int arg1, int arg2, int id);
/**
* @brief 设置LOG等级
* @param level log等级
* 0: 不打印LOG
* 1: 打印ERROR LOG
* 2: 打印WARNING LOG
* 3: 打印INFO LOG
* 4: 打印DEBUG LOG
* 5: 打印VERBOSE LOG
* @note 高等级包含低等级LOG打印
*/
void lisa_player_set_loglev(int level);
/**
* @brief 获取播放器版本号
* @return Lisa Player Version
*/
const char * lisa_player_get_version();
/**
* @brief 创建播放器
* @param name 播放器名称
* @param id 播放器id
* @return PLAYER_HANDLE
*/
PLAYER_HANDLE lisa_player_create(const char *name, int id);
/**
* @brief 创建播放器
* @param name 播放器名称
* @param id 播放器id
* @param card_name 声卡节点
* @return PLAYER_HANDLE
*/
PLAYER_HANDLE lisa_player_create_by_card(const char *name, int id, const char *const card_name);
/**
* @brief 销毁播放器
* @param h 句柄
*/
void lisa_player_destory(PLAYER_HANDLE h);
/**
* @brief 设置播放器回调
* @param h 句柄
*/
PlayerErr lisa_player_set_callback(PLAYER_HANDLE h, player_callback cb);
/**
* @brief 设置资源路径
* @param h 句柄
* @param url 资源路径
*/
PlayerErr lisa_player_seturl(PLAYER_HANDLE h, const char *url);
/**
* @brief 写音频数据
* 如果是PCM数据, seturl格式: stream://type=pcm&rate=%d&channel=%d&bits=%d
* 结束时, 直接传递data=NULL, size=0
* @param h 句柄
* @param data 数据指针可以为NULL
* @param size 数据大小
* @param wait_ms 等待时长
*/
int lisa_player_put_stream_data(PLAYER_HANDLE h, uint8_t *data, uint32_t size, uint32_t wait_ms);
/**
* @brief 开始播放
* @param h 句柄
*/
PlayerErr lisa_player_play(PLAYER_HANDLE h);
/**
* @brief 暂停操作
* @param h 句柄
*/
PlayerErr lisa_player_pause(PLAYER_HANDLE h);
/**
* @brief 恢复操作
* @param h 句柄
*/
PlayerErr lisa_player_resume(PLAYER_HANDLE h);
/**
* @brief 恢复同步操作
* @param h 句柄
*/
PlayerErr lisa_player_resume_sync(PLAYER_HANDLE h);
/**
* @brief 停止操作
* @param h 句柄
*/
PlayerErr lisa_player_stop(PLAYER_HANDLE h);
/**
* @brief 同步停止操作
* @param h 句柄
*/
PlayerErr lisa_player_stop_sync(PLAYER_HANDLE h);
/**
* @brief 播放器重置
* @param h 句柄
*/
PlayerErr lisa_player_reset(PLAYER_HANDLE h);
/**
* @brief Seek
* @param h 句柄
* @param seek_ms Seek时长(ms为单位)
*/
PlayerErr lisa_player_seek(PLAYER_HANDLE h, int seek_ms);
/**
* @brief Seek同步操作
* @param h 句柄
* @param seek_ms Seek时长(ms为单位)
*/
PlayerErr lisa_player_seek_sync(PLAYER_HANDLE h, int seek_ms);
/**
* @brief 获取播放器的状态
* @param h 句柄
* @return 播放器状态
*/
PlayerState lisa_player_get_state(PLAYER_HANDLE h);
/**
* @brief 获取播放资源总长度
* @param h 句柄
* @return >= 0 播放资源总长度
* @return < 0 操作错误
*/
int32_t lisa_player_get_duration(PLAYER_HANDLE h);
/**
* @brief 获取播放位置
* @param h 句柄
* @return >= 0 播放位置
* @return < 0 操作错误
*/
int32_t lisa_player_get_pos(PLAYER_HANDLE h);
/**
* @brief 设置播放器音量
* @param h 句柄
* @param vol 指定音量
* @return >= 0 设置后的音量
* @return < 0 设置失败
*/
PlayerErr lisa_player_set_vol(PLAYER_HANDLE h, int vol);
/**
* @brief 关闭连接前置处理
* @param h 句柄
* @return > 0 关闭成功
* @return < 0 关闭失败
*/
PlayerErr lisa_player_pre_close(PLAYER_HANDLE h);
/**
* @brief 抛掉开头固定时间的数据在lisa_player_seturl之前调用
* @param h 句柄
* @param throw_time_ms 多长时间的数据,单位毫秒
*/
PlayerErr lisa_player_throw_begin_data(PLAYER_HANDLE h, int throw_time_ms);
/**
* @brief 抛掉开头能量低的音频
* @param h 句柄
* @param max_ms 最大检测时间
* @return PlayerErr
*/
PlayerErr lisa_player_throw_low_energy(PLAYER_HANDLE h, int max_ms);
/**
* @brief 创建 Micro 播放器
* @param track_buf_size 播放缓存大小
* @return PLAYER_HANDLE
*/
PLAYER_HANDLE lisa_player_micro_create(uint32_t track_buf_size);
/**
* @brief 播放 Micro 播放器
* @param h 句柄
* @param url 播放链接
* @return PlayerErr
*/
PlayerErr lisa_player_micro_play(PLAYER_HANDLE h, const char *const url);
/**
* @brief 停止 Micro 播放器
* @param h 句柄
* @return PlayerErr
*/
PlayerErr lisa_player_micro_stop(PLAYER_HANDLE h);
/**
* @brief 设置 Micro 播放器音量
* @param h 句柄
* @param vol 指定音量
* @return >= 0 设置后的音量
* @return < 0 设置失败
*/
PlayerErr lisa_player_micro_set_vol(PLAYER_HANDLE h, int vol);
/**
* @brief 销毁 Micro 播放器
* @param h 句柄
*/
void lisa_player_micro_destory(PLAYER_HANDLE h);
/**
* @brief 抛掉开头能量低的音频
* @param h 句柄
* @param _time_ms 最大检测时间
* @return PlayerErr
*/
PlayerErr lisa_player_micro_throw_low_energy(PLAYER_HANDLE h, int throw_time_ms);
#endif

View File

@@ -0,0 +1,52 @@
#include <stdint.h>
#include "lisa_device.h"
#include "lisa_audio.h"
#define TAG "lisa_player_adapter"
#include "lisa_log.h"
#define AUDIO_DEVICE_NAME "audio0"
#define AUDIO_BUFFER_CNT 10
#define AUDIO_BUFFER_SAMPLES 256
void audio_play_send_pcm(char *data, int size)
{
static lisa_device_t *audio_dev = NULL;
int ret;
if (audio_dev == NULL) {
audio_dev = lisa_device_get(AUDIO_DEVICE_NAME);
if (!lisa_device_ready(audio_dev)) {
LOGE("Error: %s device not ready", AUDIO_DEVICE_NAME);
return;
}
lisa_audio_play_config_t play_config = {
.format =
{
.sample_rate = LISA_AUDIO_RATE_16K,
.channels = LISA_AUDIO_CH_LEFT,
.sample_bits = LISA_AUDIO_BIT_16,
},
.gain =
{
.analog_gain = 0,
.digital_gain = -12,
},
.buffer_count = AUDIO_BUFFER_CNT,
.buffer_samples = AUDIO_BUFFER_SAMPLES,
};
ret = lisa_audio_play_config(audio_dev, &play_config);
if (ret != LISA_DEVICE_OK) {
LOGE("play_config: %d", ret);
return;
}
ret = lisa_audio_play_start(audio_dev);
if (ret != LISA_DEVICE_OK) {
LOGE("audio play start fail: %d", ret);
return;
}
}
ret = lisa_audio_play_write(audio_dev, (int16_t *)data, size / 2);
}

View File

@@ -0,0 +1,141 @@
# 流式播放使用指南
流式播放功能支持实时写入 PCM 音频数据并播放,特别适用于以下场景:
- **实时 TTS 合成**:边合成边播放,降低延迟
- **网络音频流**:实时接收并播放网络音频数据
- **实时音频处理**:处理后的音频数据实时播放
## 使用流程
### 1. 开始流式播放
使用 `app_player_play_stream` 初始化流式播放:
```c
// 开始流式播放(16kHz, 单声道, 16位)
int ret = app_player_play_stream(player, 16000, 1, 16);
if (ret != APP_PLAYER_OK) {
// 处理错误
}
```
**参数说明:**
- `sample_rate`: 采样率(Hz),如 8000, 16000, 48000
- `channels`: 声道数(当前仅支持 1 = 单声道)
- `bits`: 位深度(当前仅支持 16 位)
### 2. 写入音频数据
使用 `app_player_write_stream` 逐块写入 PCM 数据:
```c
uint8_t pcm_buffer[1024];
int size = 1024;
// 写入PCM数据(超时时间1000ms)
int written = app_player_write_stream(player, pcm_buffer, size, 1000);
if (written < 0) {
// 写入失败
} else {
// written 为实际写入的字节数
}
```
**使用说明:**
- 可以多次调用以逐块写入数据
- `timeout_ms` 指定写入超时时间(毫秒)
- 返回值为实际写入的字节数,负数表示错误
### 3. 结束流式播放
数据写入完成后,调用 `app_player_finish_stream` 通知播放器:
```c
// 通知播放器数据已全部写入
int ret = app_player_finish_stream(player);
if (ret != APP_PLAYER_OK) {
// 处理错误
}
// 播放完成后会收到 APP_PLAYER_EVENT_COMPLETED 事件
```
## 使用限制(重要)
**流式播放仅支持以下操作流程:**
```
app_player_play_stream -> app_player_write_stream -> app_player_finish_stream
```
**流式播放模式下不支持以下操作:**
-`app_player_seek()` - 跳转操作
-`app_player_pause()` - 暂停操作
-`app_player_stop()` / `app_player_stop_sync()` - 停止操作
-`app_player_resume()` / `app_player_resume_sync()` - 恢复操作
如果在流式播放模式下调用上述接口,会返回 `APP_PLAYER_ERR_NOT_SUPPORTED` 错误。
## 注意事项
1. **音频格式限制**
- 当前仅支持单声道channels = 1
- 当前仅支持 16 位深度bits = 16
- 采样率可自定义(推荐 16000 Hz
2. **写入顺序**
- 必须先调用 `app_player_play_stream` 初始化
- 然后可多次调用 `app_player_write_stream` 写入数据
- 最后调用 `app_player_finish_stream` 结束
3. **错误处理**
- 如果未调用 `play_stream` 就调用 `write_stream`,会返回错误
- 写入超时会返回错误,需检查返回值
- 在流式模式下调用不支持的操作会返回 `APP_PLAYER_ERR_NOT_SUPPORTED`
4. **与普通播放的区别**
- 流式播放不能与 URL 播放同时使用
- 流式播放模式下不支持 seek/pause/stop/resume 等控制操作
- 适合实时数据,不适合已知完整文件的场景
- 如需中断流式播放,应调用 `app_player_finish_stream()` 正常结束
## 完整示例
```c
#include "app_player.h"
void stream_playback_example(app_player_t *player) {
// 1. 开始流式播放
int ret = app_player_play_stream(player, 16000, 1, 16);
if (ret != APP_PLAYER_OK) {
printf("Failed to start stream playback\n");
return;
}
// 2. 逐块写入 PCM 数据
uint8_t pcm_buffer[1024];
int total_bytes = 0;
while (has_more_data()) { // 假设有数据源
int size = get_pcm_data(pcm_buffer, sizeof(pcm_buffer));
int written = app_player_write_stream(player, pcm_buffer, size, 1000);
if (written < 0) {
printf("Failed to write stream data\n");
break;
}
total_bytes += written;
printf("Written %d bytes, total %d\n", written, total_bytes);
}
// 3. 结束流式播放
ret = app_player_finish_stream(player);
if (ret != APP_PLAYER_OK) {
printf("Failed to finish stream playback\n");
return;
}
printf("Stream playback completed\n");
}
```

View File

@@ -0,0 +1,7 @@
listenai_library_sources(
app_tone.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,214 @@
#include <stdio.h>
#include <string.h>
#include "app_tone.h"
#include "lisa_log.h"
#include "lisa_mem.h"
#define TAG "tone"
static tone_hdr_t *s_tone_hdr = NULL;
// 如果是多套音频打包成一个的情况
// 第二套音频需要跳过的数量
static uint32_t s_skip_count = 0;
int app_tone_default_init()
{
void *tone_addr = (void*)0x30100000;
int ret = app_tone_init((uint32_t)tone_addr);
if (ret != 0)
{
LISA_LOGE(TAG, "tone default init fail");
return -1;
}
return 0;
}
static void _app_tone_uninit()
{
if (s_tone_hdr)
{
if (s_tone_hdr->item)
{
for (int i = 0; i < s_tone_hdr->total_cnt; i++) {
if (s_tone_hdr->item[i].url) {
lisa_mem_free(s_tone_hdr->item[i].url);
}
}
lisa_mem_free(s_tone_hdr->item);
}
lisa_mem_free(s_tone_hdr);
s_tone_hdr = NULL;
}
}
int app_tone_init(uint32_t flash_addr)
{
// 先逆初始化
_app_tone_uninit();
uint32_t total_mem_size = 0;
s_tone_hdr = lisa_mem_calloc(1, sizeof(tone_hdr_t));
if (!s_tone_hdr)
{
return -1;
}
total_mem_size += sizeof(tone_hdr_t);
uint32_t buf_len;
uint32_t *p_buf_len = NULL;
uint32_t tone_offset = flash_addr;
uint32_t tone_len = 0;
//获取tone的个数
uint16_t buf[1] = {0};
// 用于组tone地址的临时Buffer
char tmp_buf[MAX_URL_LEN];
uint32_t tmp_size = 0;
memcpy((void *)buf, (void *)tone_offset, sizeof(buf));
s_tone_hdr->total_cnt = buf[0];
#if 0
if (s_tone_hdr->total_cnt > MAX_TONE_CNT)
{
LISA_LOGE(TAG, "tone count %d invalid", s_tone_hdr->total_cnt);
goto TONE_INIT_FAIL;
}
#endif
LISA_LOGI(TAG, "tone init addr: %p, count: %d", flash_addr, s_tone_hdr->total_cnt);
total_mem_size += (s_tone_hdr->total_cnt * sizeof(tone_dsc_t));
s_tone_hdr->item = lisa_mem_calloc(s_tone_hdr->total_cnt, sizeof(tone_dsc_t));
if (!s_tone_hdr->item)
{
LISA_LOGE(TAG, "tone item no mem...");
goto TONE_INIT_FAIL;
}
buf_len = buf[0] * sizeof(uint32_t);
p_buf_len = (uint32_t *)lisa_mem_alloc(buf_len);
if (!p_buf_len)
{
LISA_LOGE(TAG, "tone buf no mem...");
goto TONE_INIT_FAIL;
}
//此时取tone.bin的头信息;
tone_offset += sizeof(buf);
memcpy((void *)p_buf_len, (void *)tone_offset, buf_len);
tone_offset += buf_len;
for (int i = 0; i < s_tone_hdr->total_cnt; i++)
{
s_tone_hdr->item[i].tone_id = i;
tone_len = p_buf_len[i];
if (tone_len > 0) {
memset(tmp_buf, 0, MAX_URL_LEN);
sprintf(tmp_buf, "mem://addr=%ldsize=%d", (long)tone_offset, tone_len);
tmp_size = strlen(tmp_buf) + 1;
s_tone_hdr->item[i].url = lisa_mem_alloc(tmp_size);
LISA_ASSERT(s_tone_hdr->item[i].url, "alloc tone url fail");
strcpy(s_tone_hdr->item[i].url, tmp_buf);
total_mem_size += tmp_size;
} else {
s_tone_hdr->item[i].url = NULL;
}
tone_offset += tone_len;
}
if (p_buf_len)
{
lisa_mem_free(p_buf_len);
}
LISA_LOGI(TAG, "tone init use mem: %d", total_mem_size);
return 0;
TONE_INIT_FAIL:
_app_tone_uninit();
return -1;
}
static tone_dsc_t *__get_tone_by_id(uint16_t tone_id)
{
tone_dsc_t *item = NULL;
if (s_tone_hdr && s_tone_hdr->item)
{
if (tone_id >= s_tone_hdr->total_cnt)
{
return NULL;
}
item = &s_tone_hdr->item[tone_id];
if (item->url == NULL)
{
return NULL;
}
}
return item;
}
char * app_tone_get_url(uint16_t tone_id)
{
// 如果是第三种配置方案
#if (VOICEID_TYPE == 3)
tone_dsc_t *dsc_t = NULL;
if (s_skip_count > 0) {
uint16_t next_id = tone_id + s_skip_count;
LISA_LOGV(TAG, "tone_id: %d, next_id: %d", tone_id, next_id);
dsc_t = __get_tone_by_id(next_id);
if (dsc_t) {
return dsc_t->url;
} else {
LISA_LOGV(TAG, "get next_id: %d fail, use tone_id: %d", next_id, tone_id);
// 如果第二套音频获取失败情况下
// 从第一套音频获取, 比如公共的音频
dsc_t = __get_tone_by_id(tone_id);
if (dsc_t) {
return dsc_t->url;
}
}
} else {
// 不需要跳过时, 直接从第一套音频获取
dsc_t = __get_tone_by_id(tone_id);
if (dsc_t) {
return dsc_t->url;
}
}
return NULL;
#else
tone_dsc_t *dsc_t = __get_tone_by_id(tone_id);
if (dsc_t) {
return dsc_t->url;
}
return NULL;
#endif
}
int app_tone_reload(uint32_t custom_addr)
{
LISA_LOGD(TAG, "app tone reload by 0x%X", custom_addr);
int ret = app_tone_init(custom_addr);
if (ret == -1)
{
app_tone_default_init();
return -1;
}
return 0;
}
void app_tone_skip_count(int skip_count)
{
s_skip_count = skip_count;
}

View File

@@ -0,0 +1,62 @@
#ifndef __LISTENAI_APP_TONE_H__
#define __LISTENAI_APP_TONE_H__
#include <stdint.h>
#include <stdbool.h>
#define MAX_URL_LEN (32)
#define TONE_MAGIC_NONE (0x0)
#define TONE_MAGIC_DEF (0x30100000)
#define TONE_MAGIC_TWO (0x30100000)
#define TONE_SKIP_COUNT_NONE (0)
#define TONE_SKIP_COUNT_TWO (1000)
typedef struct {
uint16_t tone_id;
char *url;
} tone_dsc_t;
typedef struct {
uint16_t total_cnt;
tone_dsc_t *item;
} tone_hdr_t;
/**
* @brief 使用默认地址初始化
* @return 0: 初始化成功
* @return -1: 初始化失败
*/
int app_tone_default_init();
/**
* @brief 音频初始化
* @param flash_addr Flash起始地址
* @return 0: 加载成功
* @return -1: 加载失败
*/
int app_tone_init(uint32_t flash_offser);
/**
* @brief 根据离线音频 ID 获取离线音频 URL
* @param tone_id 音频 ID
* @return 离线音频 URL
*/
char *app_tone_get_url(uint16_t tone_id);
/**
* @brief 重新加载离线音频
* @param type 加载类型
* @param custom_addr 自定义地址
* @return 0: 重载成功
* @return -1: 重载失败
*/
int app_tone_reload(uint32_t custom_addr);
/**
* @brief 标记跳过多少音频
* @param skip_count 跳过数量
*/
void app_tone_skip_count(int skip_count);
#endif