在学习笔记1中
1)我们知道了声卡抽象层audiodev.c封装了各种声卡的操作,通过pjmedia_aud_stream_op接口函数族实现对声卡设备的操作
2)对于alsa声卡来说,alsa_dev.c对操作系统提供的alsa API进行了封装, 定义了pjmedia_aud_stream_op接口函数族供声卡抽象层audiodev.c调用
3)对于alsa声卡来说,当声卡抽象层audiodev.c调用start_steam启动声卡时,会创建一个PCM的抓取线程和一个PCM回放线程, 线程中分别调用回调ca_cb和pb_cb
4)回调函数是声卡抽象层audiodev.c的“上层”提供的, 在pjmedia体系中, 这个“上层”是叫做pjmedia_snd_port
5)pjmedia_snd_port是pjmedia_port抽象层的一个派生(sound_port.c), 专门用于与声卡抽象层audiodev.c建立绑定的
6)pjmedia_snd_port通过调用pjmedia_aud_stream_create,调用声卡抽象层audiodev的opnstream函数,把回调接口告诉alsa封装层的alsa_dev.c,并把自己作为回调参数userdata
---------------------------下面来研究一下这个pjmedia_port抽象层
参考:https://blog.csdn.net/gregcheng/article/details/5800564
pjmedia_port定义如下(来自源码pjmedia/port.h):
可以看到, 结构体中甚至对接口函数的调用者都作出了具体的规定,实际上, 这些调用入口就是pjmedia_por的抽象层port.c所提供接口函数
也就是说, pjmedia不希望应用层直接越过pjmedia_por的抽象层port.c去操作pjmedia_port的具体实现,而希望使用port.c所提供的统一接口去操作
typedef struct pjmedia_port
{
pjmedia_port_info info; /**< Port information. */
/** Port data can be used by the port creator to attach arbitrary
* value to be associated with the port.
*/
struct port_data {
void *pdata; /**< Pointer data. */
long ldata; /**< Long data. */
} port_data;
/**
* Get clock source.
* This should only be called by #pjmedia_port_get_clock_src().
*/
pjmedia_clock_src* (*get_clock_src)(struct pjmedia_port *this_port, pjmedia_dir dir);
/**
* Sink interface.
* This should only be called by #pjmedia_port_put_frame().
*/
pj_status_t (*put_frame)(struct pjmedia_port *this_port, pjmedia_frame *frame);
/**
* Source interface.
* This should only be called by #pjmedia_port_get_frame().
*/
pj_status_t (*get_frame)(struct pjmedia_port *this_port, pjmedia_frame *frame);
/**
* Called to destroy this port.
*/
pj_status_t (*on_destroy)(struct pjmedia_port *this_port);
} pjmedia_port;
pjmedia系统中, 存在着各种各样的port, 分别用于与不同的媒体格式打交道, 例如:
sound_port.c 专门与声卡抽象层打交到
wave_player.c 专门处理文件媒体流播放
wave_wav_writer.c 专门处理文件媒体流录制
wav_playlist.c 专门处理文件列表媒体流播放
还有一些是用于媒体流数据转换的
resample_port.c 采样频率转换
echo_port.c 回声消除处理
下面重点关注一下与VOIP声卡接口的pjmedia_snd_port
一) pjmedia_snd_port (sound_port.c)
关于 pjmedia_snd_port在voip媒体流中的位置,可以参考: https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
图中,pjmedia_snd_port位于声卡抽象层和会议桥之间, pjmedia_snd_port本身还挂载了一个回声消除模块, 先看看pjmedia_snd_port定义:
struct pjmedia_snd_port
{
int rec_id;
int play_id;
pj_uint32_t aud_caps; //
pjmedia_aud_param aud_param;
pjmedia_aud_stream *aud_stream;
pjmedia_dir dir;
pjmedia_port *port; //pjmedia_port 统一接口
pjmedia_clock_src cap_clocksrc,
play_clocksrc;
/*PCM采样相关, 这些参数最终会送到alsa声卡的驱动里面去*/
unsigned clock_rate;
unsigned channel_count;
unsigned samples_per_frame;
unsigned bits_per_sample;
unsigned options;
unsigned prm_ec_options; //回声消除选项
/* 回声消除相关*/
pjmedia_echo_state *ec_state;
unsigned ec_options;
unsigned ec_tail_len;
pj_bool_t ec_suspended;
unsigned ec_suspend_count;
unsigned ec_suspend_limit;
/* audio frame preview callbacks
这个实际上是提供给 pjmedia_snd_port的上层的接口
也就是声卡抽象层audiodev.c的更上层的接口, 例如会议桥
参考图: https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
*/
void *user_data; //一般指向pjmedia_snd_port的上层对象
pjmedia_aud_play_cb on_play_frame; //jmedia_snd_port的上层定义的播放函数
pjmedia_aud_rec_cb on_rec_frame; //jmedia_snd_port的上层定义的本地声音处理函数
};
在sound_port.c中, 定义了alsa声卡封装层alsa_dev.c所需要的回调函数play_cb和rec_cb
回调函数play_cb和rec_cb被调用时, pjmedia_snd_port由提供了两个回调函数on_play_frame/on_rec_frame对帧数据进行预处理
play_cb调用pjmedia_port_get_frame获得数据,进行回声消除处理后,数据交给on_play_frame进一步处理,也就是数据由pjmedia_snd_port的上层对象提供
rec_cb把数据进行回声消除处理后,调用pjmedia_port_put_frame把数据on_rec_frame进一步处理, 也就是交给pjmedia_snd_port的上层对象来处理
//
2) pjmedia_snd_port的回声消除功能
libsip提供的回声消除的例子aectest.c
使用pjmedia_wav_player_port_create建立"两个"wav文件播放port
使用pjmedia_wav_writer_port_create建立"一个"wav录制port
pjmedia_echo_create2建立回声消除上下文
第一个文件获取数据,送入回声消除port: pjmedia_echo_playback(ec, (short*)play_frame.buf);
第二个文件获取数据,并合并回声消除数据: pjmedia_echo_capture(ec, (short*)rec_frame.buf, 0);
获得的数据位于rec_frame.buf, 使用pjmedia_port_put_frame(wav_out, &rec_frame); 写入新的wav文件
在pjmedia_snd_port中, 根据pjmedia初始参数: pjmedia_snd_port_create2->start_sound_device->pjmedia_snd_port_set_ec将启动软件ACE功能
回调函数play_cb中进行消声处理(消声数据输出)后再执行on_play_frame进行放音
回调函数rec_cb中本地发出的声音由pjmedia_echo_capture进行消声数据输入
3) pjmedia_clock_src cap_clocksrc和play_clocksrc;
struct pjmedia_snd_port结构体中包含两个时钟源: cap_clocksrc和play_clocksrc
回调函数play_cb每调用一次, 使用frame中的时间标签对play_clocksrc时钟源的时间标签进行一次更新,并记录本次更新时间
回调函数cap_cb每调用一次, 使用frame中的时间标签对cap_clocksrc时钟源的时间标签进行一次更新,并记录本次更新时间
这个时钟源有什么功能?音画同步用的?会议桥用?以后再研究吧!
1)我们知道了声卡抽象层audiodev.c封装了各种声卡的操作,通过pjmedia_aud_stream_op接口函数族实现对声卡设备的操作
2)对于alsa声卡来说,alsa_dev.c对操作系统提供的alsa API进行了封装, 定义了pjmedia_aud_stream_op接口函数族供声卡抽象层audiodev.c调用
3)对于alsa声卡来说,当声卡抽象层audiodev.c调用start_steam启动声卡时,会创建一个PCM的抓取线程和一个PCM回放线程, 线程中分别调用回调ca_cb和pb_cb
4)回调函数是声卡抽象层audiodev.c的“上层”提供的, 在pjmedia体系中, 这个“上层”是叫做pjmedia_snd_port
5)pjmedia_snd_port是pjmedia_port抽象层的一个派生(sound_port.c), 专门用于与声卡抽象层audiodev.c建立绑定的
6)pjmedia_snd_port通过调用pjmedia_aud_stream_create,调用声卡抽象层audiodev的opnstream函数,把回调接口告诉alsa封装层的alsa_dev.c,并把自己作为回调参数userdata
---------------------------下面来研究一下这个pjmedia_port抽象层
参考:https://blog.csdn.net/gregcheng/article/details/5800564
pjmedia_port定义如下(来自源码pjmedia/port.h):
可以看到, 结构体中甚至对接口函数的调用者都作出了具体的规定,实际上, 这些调用入口就是pjmedia_por的抽象层port.c所提供接口函数
也就是说, pjmedia不希望应用层直接越过pjmedia_por的抽象层port.c去操作pjmedia_port的具体实现,而希望使用port.c所提供的统一接口去操作
typedef struct pjmedia_port
{
pjmedia_port_info info; /**< Port information. */
/** Port data can be used by the port creator to attach arbitrary
* value to be associated with the port.
*/
struct port_data {
void *pdata; /**< Pointer data. */
long ldata; /**< Long data. */
} port_data;
/**
* Get clock source.
* This should only be called by #pjmedia_port_get_clock_src().
*/
pjmedia_clock_src* (*get_clock_src)(struct pjmedia_port *this_port, pjmedia_dir dir);
/**
* Sink interface.
* This should only be called by #pjmedia_port_put_frame().
*/
pj_status_t (*put_frame)(struct pjmedia_port *this_port, pjmedia_frame *frame);
/**
* Source interface.
* This should only be called by #pjmedia_port_get_frame().
*/
pj_status_t (*get_frame)(struct pjmedia_port *this_port, pjmedia_frame *frame);
/**
* Called to destroy this port.
*/
pj_status_t (*on_destroy)(struct pjmedia_port *this_port);
} pjmedia_port;
pjmedia系统中, 存在着各种各样的port, 分别用于与不同的媒体格式打交道, 例如:
sound_port.c 专门与声卡抽象层打交到
wave_player.c 专门处理文件媒体流播放
wave_wav_writer.c 专门处理文件媒体流录制
wav_playlist.c 专门处理文件列表媒体流播放
还有一些是用于媒体流数据转换的
resample_port.c 采样频率转换
echo_port.c 回声消除处理
下面重点关注一下与VOIP声卡接口的pjmedia_snd_port
一) pjmedia_snd_port (sound_port.c)
关于 pjmedia_snd_port在voip媒体流中的位置,可以参考: https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
图中,pjmedia_snd_port位于声卡抽象层和会议桥之间, pjmedia_snd_port本身还挂载了一个回声消除模块, 先看看pjmedia_snd_port定义:
struct pjmedia_snd_port
{
int rec_id;
int play_id;
pj_uint32_t aud_caps; //
pjmedia_aud_param aud_param;
pjmedia_aud_stream *aud_stream;
pjmedia_dir dir;
pjmedia_port *port; //pjmedia_port 统一接口
pjmedia_clock_src cap_clocksrc,
play_clocksrc;
/*PCM采样相关, 这些参数最终会送到alsa声卡的驱动里面去*/
unsigned clock_rate;
unsigned channel_count;
unsigned samples_per_frame;
unsigned bits_per_sample;
unsigned options;
unsigned prm_ec_options; //回声消除选项
/* 回声消除相关*/
pjmedia_echo_state *ec_state;
unsigned ec_options;
unsigned ec_tail_len;
pj_bool_t ec_suspended;
unsigned ec_suspend_count;
unsigned ec_suspend_limit;
/* audio frame preview callbacks
这个实际上是提供给 pjmedia_snd_port的上层的接口
也就是声卡抽象层audiodev.c的更上层的接口, 例如会议桥
参考图: https://trac.pjsip.org/repos/wiki/media-flow#IncomingRTPRTCPPackets
*/
void *user_data; //一般指向pjmedia_snd_port的上层对象
pjmedia_aud_play_cb on_play_frame; //jmedia_snd_port的上层定义的播放函数
pjmedia_aud_rec_cb on_rec_frame; //jmedia_snd_port的上层定义的本地声音处理函数
};
在sound_port.c中, 定义了alsa声卡封装层alsa_dev.c所需要的回调函数play_cb和rec_cb
回调函数play_cb和rec_cb被调用时, pjmedia_snd_port由提供了两个回调函数on_play_frame/on_rec_frame对帧数据进行预处理
play_cb调用pjmedia_port_get_frame获得数据,进行回声消除处理后,数据交给on_play_frame进一步处理,也就是数据由pjmedia_snd_port的上层对象提供
rec_cb把数据进行回声消除处理后,调用pjmedia_port_put_frame把数据on_rec_frame进一步处理, 也就是交给pjmedia_snd_port的上层对象来处理
//
2) pjmedia_snd_port的回声消除功能
libsip提供的回声消除的例子aectest.c
使用pjmedia_wav_player_port_create建立"两个"wav文件播放port
使用pjmedia_wav_writer_port_create建立"一个"wav录制port
pjmedia_echo_create2建立回声消除上下文
第一个文件获取数据,送入回声消除port: pjmedia_echo_playback(ec, (short*)play_frame.buf);
第二个文件获取数据,并合并回声消除数据: pjmedia_echo_capture(ec, (short*)rec_frame.buf, 0);
获得的数据位于rec_frame.buf, 使用pjmedia_port_put_frame(wav_out, &rec_frame); 写入新的wav文件
在pjmedia_snd_port中, 根据pjmedia初始参数: pjmedia_snd_port_create2->start_sound_device->pjmedia_snd_port_set_ec将启动软件ACE功能
回调函数play_cb中进行消声处理(消声数据输出)后再执行on_play_frame进行放音
回调函数rec_cb中本地发出的声音由pjmedia_echo_capture进行消声数据输入
3) pjmedia_clock_src cap_clocksrc和play_clocksrc;
struct pjmedia_snd_port结构体中包含两个时钟源: cap_clocksrc和play_clocksrc
回调函数play_cb每调用一次, 使用frame中的时间标签对play_clocksrc时钟源的时间标签进行一次更新,并记录本次更新时间
回调函数cap_cb每调用一次, 使用frame中的时间标签对cap_clocksrc时钟源的时间标签进行一次更新,并记录本次更新时间
这个时钟源有什么功能?音画同步用的?会议桥用?以后再研究吧!
补充: 在研究会议桥时, 发现会议桥创建时调用
pjmedia_snd_port_connect( conf->snd_dev_port, conf->master_port);
实现了对pjmedia_send_port中的pjmedia_port 接口对象赋值, 于是又看了看pjmedia_send_port代码,发现port成员并没有在pjmedia_send_port中被实现, 它就是一个接口指针。jmedia_send_port对象必须使用pjmedia_snd_port_connect函数给自己关联一个pjmedia_port对象实例,才能够打通声卡到应用层的声音数据通道。
因此,pjmedia_send_port中只是“包含“了一个pjmedia_port对象, 但并没“实现“这个pjmedia_port对象。