CI1303启英泰伦·

启英泰伦 CI1303 播放外部音频代码解析(一)

ronger

ronger

295 0

本文借助CI230X芯片SDK 的代码来学习如何实现 CI1303 播放外部音频功能。

CI1303 + Wi-Fi Combo = CI230X

cias_network_msg_protocol.c

cias_network_msg_protocol.c

network_recv_data_task 函数代码解析

这个函数是一个FreeRTOS任务,用于接收和处理网络数据。让我逐行分析它的功能:

void network_recv_data_task(void *parameter)
{
    static int32_t recv_package_length = 0;  // 存储接收包的长度
    static int32_t read_start_flag = 0;      // 标记是否开始读取数据
    static int32_t curr_net_player_cache = 0; // 当前网络播放器缓存大小
    int32_t package_length = 0;              // 当前包长度
    int8_t  msg_state = NET_MSG_IDE;         // 消息状态,初始为空闲状态
    BaseType_t err;                          // FreeRTOS错误代码
    cias_data_standard_head_t *data_header;  // 指向数据包头部的指针
    uint32_t cache_empty_num = 0;            // 缓存空计数器
    cur_play_stream = tts_player;            // 设置当前播放流为TTS播放器

函数开始初始化了多个变量,包括接收包长度、读取标志、缓存大小等。然后进入一个无限循环进行消息处理:

while (1)
    {
        /****play deal****/
        #if NETWORK_AUDIO_PLAYER_ENABLE

在循环中,首先处理音频播放相关逻辑(如果启用了网络音频播放器):

curr_net_player_cache = audio_play_stream_buffer_get_spaces_size(cur_play_stream);

获取当前播放流的可用缓冲区大小。

if ((curr_net_player_cache == IOT_AUDIO_PLAY_BUF_SIZE) && (get_net_player_status(get_net_play_type()) == NET_PLAYER_START))
        {
            cache_empty_num++;
            int stop_empt_num = 400;
            if (get_net_play_type() == PLAY_M4A)
            {
                stop_empt_num = 5;
            }

如果缓存完全空且播放器状态为已启动,增加空缓存计数器。根据播放类型设置不同的阈值。

if ((cache_empty_num >= stop_empt_num) && (!no_audio_data_flag))
            {
                cache_empty_num = 0;
                if (!recv_music_end_sem)
                {
                    no_audio_data_flag = true;

当空缓存计数超过阈值且未标记无音频数据时,重置计数器并处理不同播放类型的情况:

if ((get_net_play_type() == PLAY_M4A))
                    {
                        if (read_start_flag == 0)
                        {
                            pause_play(NULL, NULL);
                        }
                        else
                        {
                            no_audio_data_flag = false;
                        }
                    }
                    else if (get_net_play_type() != PLAY_TTS)
                    {
                        pause_play(NULL, NULL);
                    }
                    else
                    {
                        stop_play(NULL, NULL);
                    }

根据播放类型执行不同操作:M4A类型根据读取标志决定暂停或继续;非TTS类型暂停播放;TTS类型停止播放。

else if (get_net_player_status(get_net_play_type()) == NET_PLAYER_READY)
        {
            read_start_flag = 1;
        }
        else
        {
            cache_empty_num = 0;
        }

如果播放器状态为就绪,设置读取标志;否则重置空缓存计数。

if(start_recv_flag && (curr_net_player_cache >= 6*1024)) 
        {           
            cias_send_cmd(PLAY_DATA_RECV, DEF_FILL);
            start_recv_flag = false;
        }
        #endif //NETWORK_AUDIO_PLAYER_ENABLE

当需要接收数据且缓存空间大于6KB时,发送数据接收命令并重置接收标志。

接下来是网络消息接收和处理部分:

#if NEWORK_RECV_QUEUE_ENABLE
            package_length = 0;
            while(pdPASS == xQueueReceive(network_msg_recv_queue, &package_data[package_length], pdMS_TO_TICKS(50)))
            {
                package_length++;

从消息队列接收数据,每次接收一个字节,持续接收直到超时(50ms)。

if(msg_state == NET_MSG_IDE) 
                {
                    msg_state = NET_MSG_HEAD;
                }

如果当前为空闲状态,转为头部接收状态。

else if (msg_state == NET_MSG_HEAD && package_length > (sizeof(cias_data_standard_head_t)-2))
                {
                    data_header = (cias_data_standard_head_t *)package_data;
                    if(data_header->magic == CIAS_STANDARD_MAGIC)
                    {                        
                        msg_state = NET_MSG_DATE; 
                    }     
                    else
                        msg_state = NET_MSG_ERR;
                }

当接收足够数据形成头部后,验证魔数是否正确。若正确,状态转为数据接收;否则,标记为错误状态。

else if(msg_state == NET_MSG_DATE && package_length > (data_header->len + sizeof(cias_data_standard_head_t) - 1))
                {
                    data_header = (cias_data_standard_head_t *)package_data;
                    recv_package_length = data_header->len + sizeof(cias_data_standard_head_t);                   
                    
                    wifi_msg_deal((uint8_t *)package_data, recv_package_length);

                    package_length = 0;
                    memset(package_data,0,1050);
                    msg_state = NET_MSG_IDE; 
                }

当数据接收完成后,计算包总长度,调用wifi_msg_deal函数处理消息,然后重置缓冲区和状态。

if(msg_state == NET_MSG_ERR)
                {
                    ci_logdebug(LOG_USER, "ci_standard_head_t error\r\n");
                    package_length = 0;
                    memset(package_data,0,1050);
                    msg_state = NET_MSG_IDE; 
                }
            }

如果状态为错误,输出日志,重置缓冲区和状态。

if(msg_state != NET_MSG_IDE)
            {
                ci_logdebug(LOG_USER, "recv data timeout error(%d)\r\n",package_length);
                package_length = 0;
                memset(package_data, 0, 1050);
                msg_state = NET_MSG_IDE; 
            }
        #endif
    }
}

如果循环结束时状态不是空闲状态,说明接收超时,记录日志并重置所有状态。

network_recv_data_task 函数功能总结

该函数实现了一个网络数据接收和处理任务,主要功能包括:

  1. 音频播放管理
    • 监控音频缓冲区状态,根据缓存情况决定是否请求更多数据
    • 处理不同类型音频(M4A、TTS等)的播放控制逻辑
  2. 网络消息接收
    • 通过FreeRTOS消息队列接收数据
    • 实现了一个状态机处理消息格式(空闲→头部→数据)
    • 验证消息头部魔数确保数据有效性
  3. 异常处理
    • 处理数据格式错误情况
    • 处理接收超时情况
    • 重置相关缓冲区和状态

接下来我们通过阅读 wifi_msg_deal 函数代码看看当数据接收完成后是如何执行的。

wifi_msg_deal 函数代码解析

我们可以看到 wifi_msg_deal 函数接受两个参数:msg_buf(消息缓冲区)和 msg_len(消息长度),然后根据消息类型调用相应的回调函数(如tuya_ir_wifi_msg_callback、tencent_iot_light_network_callback、aux_aiot_aircondition_callback等)处理不同类型的消息。

/**
 * @brief WIFI 消息处理
 *
 * @param msg_buf 数据信息
 * @param msg_len 数据长度
 * @return int32_t RETURN_OK 处理成功,RETURN_ERR 数据异常
 */

int32_t wifi_msg_deal(uint8_t *msg_buf, int32_t msg_len)
{

    int8_t net_config_fail_state = 0;
    if ((msg_buf == NULL) || (msg_len <= 0))
    {
        return RETURN_ERR;
    }
#if TUYA_IR_CTRL_DEMO_ENABLE | CI_IR_CTRL_DEMO_ENABLE   //涂鸦红外遥控器demo
    tuya_ir_wifi_msg_callback(msg_buf);
#endif

#if TENCENT_IOT_LIGHT_DEMO_ENABLE
    tencent_iot_light_network_callback(msg_buf);
#endif
#if CIAS_DOUBLE_MIC_DEMO_ENABLE | CIAS_SIGNAL_MIC_DEMO_ENABLE
    aux_aiot_aircondition_callback(msg_buf);
#endif
#if CIAS_FLASH_IMAGE_UPGRADE_ENABLE
    cias_data_standard_head_t *pheader = (cias_data_standard_head_t *)(msg_buf);
    
    if(pheader->type == CIAS_OTA_START)
        dpmu_software_reset_system_config();
#endif
    int32_t ret = RETURN_OK;

    return ret;
}

接下来我们通过阅读 aux_aiot_aircondition_callback 函数代码看看后续是如何执行的。

aux_aiot_aircondition_callback.c

aux_aiot_aircondition_callback.c

aux_aiot_aircondition_callback 函数代码解析

这个函数处理从网络接收到的消息,并根据消息类型执行相应的操作。下面是详细的逐行分析:

int32_t aux_aiot_aircondition_callback(uint8_t *msg_buf)
{

函数定义,接收一个指向消息缓冲区的指针,返回int32_t类型的结果。

int32_t ret = RETURN_OK;

初始化返回值为RETURN_OK,表示默认处理成功。

static connext_cloud = false;

定义一个静态变量connext_cloud,初始值为false,用于跟踪设备是否连接到云端。静态变量会在函数调用之间保持其值。

/*这里将数据发送到需要的BUF中,例如下面*/

注释行,提供了代码意图的说明。

cias_data_standard_head_t *pheader = (cias_data_standard_head_t *)(msg_buf);

将输入的msg_buf转换为cias_data_standard_head_t类型的指针,以访问消息的头部信息。

wifi_communicate_cmd_t wifi_cmd = (wifi_communicate_cmd_t)pheader->type;

从消息头部提取命令类型,保存到wifi_cmd变量中。

if (wifi_cmd != PLAY_DATA_GET)
        ci_logdebug(LOG_MEDIA, "[qcloud aux:]recv type: %x\n", wifi_cmd);

如果命令不是PLAY_DATA_GET(可能这类命令太频繁),则记录日志输出接收到的命令类型。这有助于调试但避免了过多的日志信息。

switch (wifi_cmd)
    {

开始一个switch语句,根据命令类型执行不同的处理逻辑。

#if AUDIO_PLAYER_ENABLE
            case PLAY_DATA_GET:
            case NET_PLAY_START:
            case NET_PLAY_STOP:
            case NET_PLAY_PAUSE:
            case NET_PLAY_RESUME :
            case PLAY_DATA_END:
            {
                network_audio_processing(msg_buf);
                break;
            }
        #endif //AUDIO_PLAYER_ENABLE

如果启用了音频播放器,这段代码处理与音频播放相关的所有命令。当收到这些命令时,调用network_audio_processing函数处理消息。

case NET_VOLUME:
        {
            if(!connext_cloud)
                break;

处理网络音量命令。如果设备未连接到云端(connext_cloud为false),则跳过此处理。这可能是为了防止在未建立云连接时接收到不可信的音量控制信息。

int vol =  *((unsigned int *)(msg_buf + 16))/14;

从消息中提取音量值(位于消息缓冲区偏移16字节的位置),并将其除以14进行归一化处理。

if(vol < VOLUME_MIN)
            {
                vol = VOLUME_MIN;
            }

确保音量不低于最小值。如果计算出的音量小于最小值,则将其设为最小值。

vol_set(vol);
            break;
        }

设置系统音量为计算出的值,并退出当前case。

case GET_PROFILE:
        {
            ci_logdebug(LOG_MEDIA, "RECV GET PROFILE CMD\n");
            cias_send_profile();

处理获取配置文件的命令。记录日志并调用cias_send_profile函数发送设备配置文件。

while(CI_SS_PLAY_STATE_PLAYING == ciss_get(CI_SS_PLAY_STATE))
            {
                vTaskDelay(1000);
            }

等待当前播放完成。检查播放状态,如果处于播放中状态,则延迟1000ms后再次检查,直到播放完成。

prompt_play_by_cmd_id(1004, -1, play_done_callback, true);
            break;
         }

播放提示音(ID为1004),使用play_done_callback作为播放完成回调,最后退出当前case。

case NEED_PROFILE:
        {
            while(CI_SS_PLAY_STATE_PLAYING == ciss_get(CI_SS_PLAY_STATE))
            {
                vTaskDelay(1000);
            }

处理需要配置文件的命令。同样等待当前播放完成。

ci_logdebug(LOG_MEDIA, "RECV NEED PROFILE CMD\n");
            prompt_play_by_cmd_id(1005, -1, play_done_callback, true);
            break;
        }

记录日志并播放另一个提示音(ID为1005),然后退出当前case。

case QCLOUD_IOT_CMD:
        {
            unsigned short qcloud_cmd;

            qcloud_cmd = *((unsigned short *)(msg_buf + 16));

处理腾讯云IoT命令。从消息缓冲区偏移16字节的位置提取2字节的命令ID。

ci_logdebug(LOG_MEDIA, "[ qcloud ]recv cmd: %d\n", qcloud_cmd);
            // tanmi_uart_send_msg_to_elc(qcloud_cmd&0xff,0x02);

记录收到的命令ID,并注释掉了一行可能将命令发送到电器的代码。

pause_voice_in();
            prompt_play_by_cmd_id(qcloud_cmd, -1, play_done_callback, true);
            break;
        }

暂停语音输入,播放与命令ID对应的提示音,并退出当前case。

case VAD_END:  //云端VAD结束
        {
            // #if RECEIVE_VAD_FROM_THE_CLOUD_ENABLE
            #if 1
                voice_upload_disable();
                voice_upload_vad_enable = false;
                xQueueReset(pcm_speex_deal_queue);

处理云端VAD(语音活动检测)结束命令。禁用语音上传,清除语音上传VAD使能标志,并重置PCM/Speex处理队列。

ci_loginfo(LOG_VOICE_UPLOAD, "rev cloud vad end flag********************\n");
                break;
            #endif
        }

记录日志并退出当前case。

// case RECV_NET_CONNECTED_CMD:
        case CLOUD_CONNECTED:  //云端已连接
        {
            start_connect_wifi = false;
            connext_cloud = true;

处理云端连接成功的命令。将WiFi连接标志设为false(因为已经连接完成),将云连接标志设为true。

if(NETWORK_CONNECTED_STATE != wifi_current_state_get())
            {
                ci_loginfo(LOG_IR, "RECV_NET_CONNECTED_CMD cmd is 0x%x\n", wifi_cmd);
                wifi_current_state_set(NETWORK_CONNECTED_STATE);

如果当前网络状态不是NETWORK_CONNECTED_STATE,记录日志并更新网络状态。

//pause_voice_in();
                vTaskDelay(pdMS_TO_TICKS(1000));
                prompt_play_by_cmd_id(1002, -1, play_done_callback, true);      
            }
            
            break;
        }

延迟1000毫秒,然后播放连接成功提示音(ID为1002),最后退出当前case。

case WIFI_DISCONNECTED://配网失败,网络已断开
        {
            start_connect_wifi = false;
            connext_cloud = false;

处理WiFi断开连接命令。将WiFi连接标志和云连接标志都设为false。

if(WIFI_CONFIGING_STATE == wifi_current_state_get())
            {
                wifi_current_state_set(WIFI_DISCONNECT_STATE);
                pause_voice_in();
                prompt_play_by_cmd_id(1003, -1, play_done_callback, true);
            }

如果当前正处于配网状态,则将WiFi状态设为断开状态,暂停语音输入,并播放断开连接提示音(ID为1003)。

ci_loginfo(LOG_IR, "WIFI_DISCONNECTED cmd is 0x%x\n", wifi_cmd);
            break;
        }

记录日志并退出当前case。

case ENTER_NET_CONFIG://WIFI进入配网模式
        {
            mprintf("---进入配网模式\n");
            prompt_play_by_cmd_id(1001, -1, play_done_callback, true);
            break;
        }

处理进入配网模式命令。打印日志并播放进入配网模式提示音(ID为1001)。

case NET_CONFIG_SUCCESS://配网成功
        {
            mprintf("---配网成功\n");
            prompt_play_by_cmd_id(1007, -1, play_done_callback, true);  
            break;
        }

处理配网成功命令。打印日志并播放配网成功提示音(ID为1007)。

case NET_CONFIG_FAIL://配网失败
        {
            mprintf("---配网失败\n");
            start_connect_wifi = false;
            connext_cloud = false;

处理配网失败命令。打印日志并重置WiFi连接标志和云连接标志。

if(WIFI_CONFIGING_STATE == wifi_current_state_get())
            {
                wifi_current_state_set(WIFI_DISCONNECT_STATE);
                pause_voice_in();   
                prompt_play_by_cmd_id(1006, -1, play_done_callback, true);
            }   
            break;
        }

如果当前正处于配网状态,则将WiFi状态设为断开状态,暂停语音输入,并播放配网失败提示音(ID为1006)。

default:
        {
            ci_loginfo(LOG_IR, "other cmd is 0x%x\n", wifi_cmd);

处理所有未明确处理的命令。记录未知命令的类型。

if(wifi_cmd == PLAY_EMPTY)
            {
                //ci_logdebug(LOG_MEDIA, "MP3 play_end, change to bf *********************\n");
#if USE_BEAMFORMING_MODULE
                ciss_set(CI_SS_PLAY_TYPE,CI_SS_PLAY_TYPE_CMD_PROMPT);
#endif
                media_play.recv_media_stream_end = true;
            }

如果命令是PLAY_EMPTY(播放结束),在启用波束形成模块的情况下设置播放类型为命令提示,并标记媒体流接收结束。

/*             if((wifi_cmd == 0xeeee)&&(media_play.start_play == START_PLAY_TTS)&&(media_play.mp3_decode_fail))
            {
                media_play.mp3_decode_fail = false;
                ci_loginfo(LOG_USER, "\r\n TTS fail, NET_PLAY_RESTART=============\r\n");
                cias_send_cmd(NET_PLAY_RESTART, DEF_FILL);
            } */

注释掉的代码,可能是处理TTS播放失败的逻辑。

ret = RETURN_ERR;
            break;
        }
    }

对于未明确处理的命令,返回值设为RETURN_ERR,并退出switch语句。

return ret;
}

返回处理结果(RETURN_OK或RETURN_ERR)。

aux_aiot_aircondition_callback 函数功能总结

aux_aiot_aircondition_callback 函数是一个网络消息处理函数,主要用于处理智能空调与云端的通信。它处理多种类型的命令,包括:

  1. 音频播放控制:包括播放、暂停、停止等操作
  2. 音量控制:通过网络命令调整设备音量
  3. 配置文件管理:处理获取和需要配置文件的请求
  4. 云端命令处理:执行从腾讯云IoT平台发送的命令
  5. 语音控制:管理语音上传和VAD(语音活动检测)
  6. 网络状态管理:处理云连接、WiFi连接/断开和配网过程的各种状态

接下来我们阅读 network_audio_processing 函数代码看看后续是如何执行的。

cias_voice_plyer_handle.c

cias_voice_plyer_handle.c

network_audio_processing 函数详细解读

这个函数用于处理网络音频相关的消息,负责管理不同类型的音频播放(MP3、TTS、M4A和WAV)的状态和控制流程。下面是详细的逐行解读:

int32_t network_audio_processing(uint8_t *msg_buf)
{

函数定义,接收一个指向消息缓冲区的指针,返回一个int32_t类型的值。

int32_t ret = RETURN_OK;

初始化返回值为RETURN_OK,表示默认处理成功。

static int32_t recv_music_data_len = 0;

静态变量,用于跟踪已接收的音乐数据总长度。

static int32_t recv_net_mp3_num = 0;

静态变量,用于跟踪已接收的MP3数据量。

static int16_t recv_resume_cmd_num = 0;

静态变量,用于跟踪接收到的恢复播放命令数量。

cias_data_standard_head_t *pheader = (cias_data_standard_head_t *)(msg_buf);

从消息缓冲区提取标准消息头信息。

wifi_communicate_cmd_t wifi_cmd = (wifi_communicate_cmd_t)pheader->type;

从消息头中获取命令类型。

switch (wifi_cmd)
    {

根据命令类型执行不同的处理逻辑。

1. 处理音频数据接收 (PLAY_DATA_GET)

case PLAY_DATA_GET:
        {

处理获取播放数据的命令。

if (pheader->len == 0)
            {
                ci_logdebug(LOG_MEDIA, "pheader->len = %d\n", pheader->len);
                ret = RETURN_ERR;
                break;
            }

如果数据长度为0,记录日志并返回错误。

int ret_tts = outside_write_stream(cur_play_stream, (uint32_t)(msg_buf + 16), pheader->len, false);

将数据写入当前的播放流(从消息缓冲区偏移16字节的位置开始)。

if (RETURN_OK == ret_tts)
            {

如果写入成功:

if (recv_net_mp3_num == 0)
                {

如果这是第一次接收数据:

if ((get_net_player_status(get_net_play_type()) == NET_PLAYER_RESUME) && (recv_music_data_len == 0))
                    {

如果当前播放状态是恢复播放且尚未接收任何数据:

if (0 != memcmp(get_next_resume_play_data(), (uint8_t *)(msg_buf + 16), 8))
                        {

比较接收到的数据与预期的恢复播放数据,如果不匹配:

ci_loginfo(LOG_MEDIA, "error first packet\n");
                            for (int32_t w = 0; w < 8; w++)
                            {
                                ci_loginfo(LOG_MEDIA, "0x%x ", *(uint8_t *)(msg_buf + 16 + w));
                                mprintf("0x%x ", *(uint8_t *)(msg_buf + 16 + w));
                            }
                            ci_loginfo(LOG_MEDIA, "\n");
                            mprintf("\n");

记录错误和接收到的数据内容。

set_net_player_status(get_net_play_type(), NET_PLAYER_IDLE);
                            cias_send_cmd(NET_PLAY_NEXT, DEF_FILL);

将播放器状态设置为空闲,并发送播放下一首的命令。

}
                        else
                        {
                            ci_loginfo(LOG_MEDIA, "resume ok\n");
                        }
                    }
                }

如果数据匹配,记录"恢复成功"的日志。

recv_music_data_len += pheader->len;
                recv_net_mp3_num += pheader->len;
                recv_resume_cmd_num = 0;

更新已接收的数据量,并重置恢复命令计数。

}
            else
            {
                ci_logdebug(LOG_MEDIA, "write data fail!\n");
            }

如果写入失败,记录日志。

if(recv_net_mp3_num >= PLAY_BUF_SIZE_MAX) start_recv_flag = true;

如果接收的数据量超过缓冲区最大大小,设置接收标志为真,表示可以继续接收更多数据。

接下来的代码处理不同情况下的播放启动:

if ((recv_music_data_len >= PLAY_BUF_SIZE_MAX ) && (get_net_player_status(get_net_play_type()) == NET_PLAYER_READY))
            {

如果接收数据量足够且播放器状态为"准备就绪":

ci_logdebug(LOG_MEDIA, "write data get_net_play_type =%d\n", get_net_play_type());
                if (get_net_play_type() == PLAY_MP3)
                {
                    play_with_outside(0, "mp3", mp3_player_end_callbk);
                    set_net_player_status(PLAY_MP3, NET_PLAYER_START);
                }
                else if (get_net_play_type() == PLAY_TTS)
                {
                    play_with_outside(0, "mp3", tts_player_end_callbk);
                    set_net_player_status(PLAY_TTS, NET_PLAYER_START);
                    get_response_timer_cnt();
                }
                else if (get_net_play_type() == PLAY_M4A)
                {
                    play_with_outside(0, "m4a", m4a_player_end_callbk);
                    set_net_player_status(PLAY_M4A, NET_PLAYER_START);
                }
                else if (get_net_play_type() == PLAY_WAV)
                {
                    play_with_outside(0, "ms_wav", wav_player_end_callbk);
                    set_net_player_status(PLAY_WAV, NET_PLAYER_START);
                }

根据不同的播放类型使用相应的回调开始播放,并更新播放器状态。

no_audio_data_flag = false;
            }

清除无音频数据标志。

接下来处理恢复播放的两种情况:

else if (((recv_music_data_len >= 20 * 1024) && (get_net_player_status(get_net_play_type()) == NET_PLAYER_RESUME) && (no_audio_data_flag)))
            {

如果接收数据足够且状态为恢复播放,且之前标记为无音频数据:

if (get_net_play_type() == PLAY_MP3)
                {
                    play_with_outside(get_music_data_offset(), "history", mp3_player_end_callbk);
                    set_net_player_status(PLAY_MP3, NET_PLAYER_START);
                }
                // 其他音频类型类似处理...

使用历史偏移量恢复播放,并更新播放器状态。

ci_logdebug(LOG_MEDIA, "--start playing-- play_type2 = %d\n", get_net_play_type());
                no_audio_data_flag = false;
            }

清除无音频数据标志。

第三种情况与第二种类似,但区别是no_audio_data_flag为false的情况:

else if (((recv_music_data_len >= 20 * 1024) && (get_net_player_status(get_net_play_type()) == NET_PLAYER_RESUME) && (!no_audio_data_flag)))
            {
                // 类似的播放处理逻辑...
            }
            break;
        }

2. 处理开始播放命令 (NET_PLAY_START)

case NET_PLAY_START:
        {

处理开始播放的命令。

if (check_current_playing())
            {
                cias_send_cmd(NET_PLAY_LOCAL_TTS, NO_WAKEUP_FILL_DATA);
                ci_logdebug(LOG_MEDIA, "NET_PLAY_START error under playing\n");
            }

如果当前已有播放内容,则发送本地TTS播放命令,并记录日志。

recv_resume_cmd_num = 0;
            recv_music_end_sem = false;
            recv_music_data_len = 0;
            recv_net_mp3_num = 0;
            start_recv_flag = false;

重置相关变量。

根据填充数据类型进行不同处理:

if (pheader->fill_data == INVAILD_SPEAK)
            {

如果是无效语音:

ci_logdebug(LOG_MEDIA, "INVAILD_SPEAK\n");
                if (get_voice_invalid())
                {
                    cias_send_cmd(SKIP_INVAILD_SPEAK, DEF_FILL);
                    break;
                }

如果已设置语音无效标志,则跳过无效语音处理。

cur_play_stream = tts_player;
                set_net_play_type(PLAY_TTS);
                outside_clear_stream(tts_player, tts_player_end);
                set_curr_outside_handle(tts_player, tts_player_end);
                ci_logdebug(LOG_MEDIA, " receive TTS start 1 !\n");
                cias_send_cmd(PLAY_DATA_GET, DEF_FILL);
                set_net_player_status(PLAY_TTS, NET_PLAYER_READY);
                media_play.start_play = START_PLAY_TTS;
            }

准备TTS播放。

else if (pheader->fill_data == RECV_TTS_PLAY)
            {
                // 类似TTS播放准备...
            }
            else if (pheader->fill_data == RECV_MP3_PLAY)
            {
                cur_play_stream = mp3_player;
                set_net_play_type(PLAY_MP3);
                outside_clear_stream(mp3_player, mp3_player_end);
                set_curr_outside_handle(mp3_player, mp3_player_end);
                ci_logdebug(LOG_MEDIA, "receive MP3 start!\n");
                cias_send_cmd(PLAY_DATA_GET, DEF_FILL);
                set_net_player_status(PLAY_MP3, NET_PLAYER_READY);
                set_music_data_offset(0);
#if USE_BEAMFORMING_MODULE
                ciss_set(CI_SS_PLAY_TYPE,CI_SS_PLAY_TYPE_MUSIC);
#endif
            }

准备MP3播放,并设置波束形成模块为音乐播放类型(如果启用)。

else if (pheader->fill_data == RECV_M4A_PLAY)
            {
                // 类似M4A播放准备...
            }
            else if (pheader->fill_data == RECV_WAV_PLAY)
            {
                // 类似WAV播放准备...
            }
            break;
        }

3. 处理停止播放命令 (NET_PLAY_STOP)

case NET_PLAY_STOP:
        {
            ci_logdebug(LOG_MEDIA, "receive media stop!\n");
            if (SYS_STATE_WAKEUP != get_wakeup_state())
            {
                pause_play(NULL, NULL);
            }
            break;
        }

记录日志并暂停播放(如果不在唤醒状态)。

4. 处理暂停播放命令 (NET_PLAY_PAUSE)

case NET_PLAY_PAUSE:
        {
            ci_logdebug(LOG_MEDIA, "receive mp3 pause!\n");
            if (get_net_play_type() == PLAY_M4A)
            {
                pause_play(NULL, NULL);
            }
            break;
        }

记录日志并暂停M4A播放(仅对M4A类型有效)。

5. 处理恢复播放命令 (NET_PLAY_RESUME)

case NET_PLAY_RESUME:
        {
            ci_logdebug(LOG_MEDIA, "receive resume! fill_data = 0x%x\n", pheader->fill_data);

记录收到恢复命令及填充数据。

if (recv_resume_cmd_num > 0)
            {
                recv_resume_cmd_num = 0;
                cias_send_cmd(NET_PLAY_NEXT, DEF_FILL);
                ci_logdebug(LOG_MEDIA, "resume recv 2 times\n");
            }

如果已经连续收到多次恢复命令,则发送播放下一首的命令。

if (check_current_playing())
            {
                ci_logdebug(LOG_MEDIA, "resume error under playing\n");
                ret = RETURN_ERR;
                break;
            }

如果当前已有内容在播放,则记录错误并返回。

recv_music_data_len = 0;
            recv_music_end_sem = false;
            recv_net_mp3_num = 0;
            start_recv_flag = false;

重置相关变量。

根据不同的播放器状态和填充数据类型进行处理:

if ((get_net_player_status(PLAY_MP3) == NET_PLAYER_PAUSE) && ((pheader->fill_data == RECV_MP3_PLAY)))
            {
                set_net_play_type(PLAY_MP3);
                cur_play_stream = mp3_player;
                set_curr_outside_handle(mp3_player, mp3_player_end);
                outside_clear_stream(mp3_player, mp3_player_end);
                set_net_player_status(PLAY_MP3, NET_PLAYER_RESUME);
                cias_send_cmd(PLAY_DATA_GET, get_music_data_offset());
                ci_logdebug(LOG_MEDIA, "recv next data 2\n");
            }

如果MP3播放器处于暂停状态且填充数据为MP3播放,则准备恢复MP3播放。

类似地处理M4A和WAV播放恢复。

else
            {
                if ((get_net_player_status(PLAY_M4A) == NET_PLAYER_IDLE) && (get_net_player_status(PLAY_MP3) == NET_PLAYER_IDLE) && (get_net_player_status(PLAY_WAV) == NET_PLAYER_IDLE))
                {
                    cias_send_cmd(NET_PLAY_NEXT, DEF_FILL);
                    ci_logdebug(LOG_MEDIA, "resume error NET_PLAYER_IDLE\n");
                }
                else
                {
                    ci_logdebug(LOG_MEDIA, "resume not pasue error\n");
                    ++recv_resume_cmd_num;
                }
            }
            break;
        }

如果所有播放器都处于空闲状态,则发送播放下一首的命令;否则增加恢复命令计数。

6. 处理播放数据结束命令 (PLAY_DATA_END)

case PLAY_DATA_END:
        {
            rev_voice_end_flag = true; 
            recv_music_end_sem = true;
            recv_net_mp3_num = 0;

设置相关标志,表示数据接收完毕。

根据不同的播放器状态处理:

if (get_net_player_status(get_net_play_type()) == NET_PLAYER_START)
            {
                if (get_net_play_type() == PLAY_TTS)
                {
                    outside_send_end_sem(tts_player_end);
                }
                else if (get_net_play_type() == PLAY_MP3)
                {
                    outside_send_end_sem(mp3_player_end);
                }
                // 类似处理其他音频类型...
                ci_logdebug(LOG_MEDIA, "send_end_sem2 play_type = %d\n", get_net_play_type());
            }

如果播放器已经开始播放,则发送结束信号量。

else if (get_net_player_status(get_net_play_type()) == NET_PLAYER_RESUME)
            {
                if (get_net_play_type() == PLAY_MP3)
                {
                    play_with_outside(get_music_data_offset(), "history", mp3_player_end_callbk);
                    set_net_player_status(PLAY_MP3, NET_PLAYER_START);
                    outside_send_end_sem(mp3_player_end);
                }
                // 类似处理其他音频类型...
            }

如果播放器处于恢复状态,则开始播放并发送结束信号量。

else if (get_net_player_status(get_net_play_type()) == NET_PLAYER_READY)
            {
                if (get_net_play_type() == PLAY_MP3)
                {
                    set_net_player_status(PLAY_MP3, NET_PLAYER_START);
                    play_with_outside(0, "mp3", mp3_player_end_callbk);
                    outside_send_end_sem(mp3_player_end);
                }
                // 类似处理其他音频类型...
                ci_logdebug(LOG_MEDIA, "Start net player\n");
            }
            break;
        }
    }
    return 0;
}

如果播放器处于就绪状态,则开始播放并发送结束信号量。

network_audio_processing 函数功能总结

network_audio_processing 函数实现了一个完整的网络音频处理流程,包括:

  1. 数据接收与缓冲:接收网络音频数据,写入相应的播放流缓冲区
  2. 播放控制:支持不同格式音频(MP3、TTS、M4A、WAV)的开始、暂停、恢复和停止播放
  3. 状态管理:管理不同播放器的状态转换(空闲、准备、开始、暂停、恢复等)
  4. 数据验证:在恢复播放时验证数据的正确性
  5. 异常处理:处理各种异常情况,如数据不足、格式错误等

函数使用状态机模式处理不同类型的命令和不同状态下的播放控制,确保音频播放的连续性和稳定性。它是实现网络音频流媒体播放的核心组件,负责处理从网络接收到的音频数据并控制播放器的行为。

所属系列

从当前文章继续阅读它所在合集中的前后内容。

相关文章

优先推荐同专题、同标签和同作者内容,补足热门文章。

评论 0

登录 后参与评论

评论

成为第一个评论的人