android Audio 详解( 一 )

1  Linux ALSA声卡驱动


   众所周知,android是基于linux的。讲android的audio的系统,就不得不从linux的声卡驱动说起。为了更好的支持嵌入式CPU,linux在标准的ALSA驱动上建立了ASoC(ALSA System on Chip)。下面我们就从ASoC说起。
    ASoC的驱动代码位于sound\soc\目录下。ASoC音频系统可以被划分为Machine、Platform、Codec三大部分。Codec驱动通常都在sound\soc\codecs\目录下。Machine、Platform的驱动通常在sound\soc\于CPU相关的目录下,例如,飞思卡尔的Machine、Platform的驱动就在sound\soc\imx\目录下,全志的就在sound\soc\sunxi\目录下。
    .Codec驱动主要是针对音频CODEC的驱动,主要是进行AD、DA转换,对音频通路的控制,音量控制、EQ控制等等。
    .Platform驱动主要是针对CPU端的驱动,主要包括DMA的设置,数据音频接口的配置,时钟频率、数据格式等等。
    .Machine驱动主要是针对设备的,实现Codec和Platform耦合。


    1.1 Machine驱动
    不同的设备硬件形态是各式各样的,不同的CPU、不同的CODEC芯片。Machine驱动就是负责将不同的Platform驱动和Codec驱动关联起来,形成一个完整的音频驱动。没有Machine驱动,Platform驱动和Codec驱动是无法独立工作的。下面就来看看Machine驱动是如何实现的。
    首先来看看两个重要的结构:
    struct snd_soc_dai_link {
const char *name;  
const char *stream_name;  
const char *codec_name;  
const struct device_node *codec_of_node;
const char *platform_name;  
const struct device_node *platform_of_node;
const char *cpu_dai_name;
const struct device_node *cpu_dai_of_node;
const char *codec_dai_name;
unsigned int dai_fmt;            
unsigned int ignore_suspend:1;
unsigned int symmetric_rates:1;
unsigned int ignore_pmdown_time:1;
int (*init)(struct snd_soc_pcm_runtime *rtd);
struct snd_soc_ops *ops;
   };
   在这个结构里面我们重点需要关注下面几个成员
   codec_name :codec的名称,系统将根据这个名字匹配相应的CODEC驱动
   codec_dai_name :codec的数字音频接口(DAI)的名称,系统根据这个匹配codec_dai驱动。
   platform_name : Platform的名称,用来匹配Platform驱动的。
   cpu_dai_name :Platform的数字音频接口(DAI)的名称,系统根据这个匹配Platform_dai驱动。(codec_dai和Platform_dai将在codec和Platform驱动里面说明)。
   ops 回调函数指针的结构,这这里可以定义对设备的硬件设置的一些代码,比较常用的是ops.hw_params实现设备级硬件的一些配置。


   struct snd_soc_card {
const char *name;
const char *long_name;
const char *driver_name;
struct device *dev;
struct snd_card *snd_card;
struct module *owner;


struct list_head list;
struct mutex mutex;
struct mutex dapm_mutex;


bool instantiated;


int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);


/* the pre and post PM functions are used to do any PM work before and
* after the codec and DAI's do any PM work. */
int (*suspend_pre)(struct snd_soc_card *card);
int (*suspend_post)(struct snd_soc_card *card);
int (*resume_pre)(struct snd_soc_card *card);
int (*resume_post)(struct snd_soc_card *card);


/* callbacks */
int (*set_bias_level)(struct snd_soc_card *,
     struct snd_soc_dapm_context *dapm,
     enum snd_soc_bias_level level);
int (*set_bias_level_post)(struct snd_soc_card *,
  struct snd_soc_dapm_context *dapm,
  enum snd_soc_bias_level level);


long pmdown_time;


/* CPU <--> Codec DAI links  */
struct snd_soc_dai_link *dai_link;
int num_links;
struct snd_soc_pcm_runtime *rtd;
int num_rtd;


/* optional codec specific configuration */
struct snd_soc_codec_conf *codec_conf;
int num_configs;


/*
* optional auxiliary devices such as amplifiers or codecs with DAI
* link unused
*/
struct snd_soc_aux_dev *aux_dev;
int num_aux_devs;
struct snd_soc_pcm_runtime *rtd_aux;
int num_aux_rtd;


const struct snd_kcontrol_new *controls;
int num_controls;


/*
* Card-specific routes and widgets.
*/
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;
bool fully_routed;


struct work_struct deferred_resume_work;


/* lists of probed devices belonging to this card */
struct list_head codec_dev_list;
struct list_head platform_dev_list;
struct list_head dai_dev_list;


struct list_head widgets;
struct list_head paths;
struct list_head dapm_list;
struct list_head dapm_dirty;


/* Generic DAPM context for the card */
struct snd_soc_dapm_context dapm;
struct snd_soc_dapm_stats dapm_stats;


#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs_card_root;
struct dentry *debugfs_pop_time;
#endif
u32 pop_time;


void *drvdata;
    };
    这个结构看起来非常复杂,实际上我们只要关注下面几个成员就好了
    name :为我们的声卡定义一个名字
    dai_link :前面介绍的snd_soc_dai_link结构的实例的地址。可以是一个snd_soc_dai_link的变量的地址,也可以是snd_soc_dai_link数组的首地址的指针。
    num_links : dai_link 的数量,例如我们定义了一个结构变量
    struct snd_soc_dai_link a;
    .dai_link = &a ;
    那么num_links就是1。
    如果定义了一个数值
    struct snd_soc_dai_link a[5]={......};
    .dai_link = a ;
    那么num_links就是5。


    下面看看怎么注册一个Machine驱动
    首先定义前面介绍的结构


    
    static struct snd_soc_dai_link a= {
.name = "audiocodec",
.stream_name = "CODEC",
.cpu_dai_name = "codec",
.codec_dai_name = "sndcodec",
.platform_name = "cpu-codec-audio",
.codec_name = "pcm-codec",
        //.ops = &sndpcm_ops,
   };
如果需要进行设备的一些硬件设置,可以定义.ops = &sndpcm_ops,如下:
   static struct snd_soc_ops sndpcm_ops = {
       .hw_params              = sndpcm_hw_params,
   };
sndpcm_hw_params进行一些硬件的设置。
   static struct snd_soc_card card= {
.name = "audiocodec",
.owner = THIS_MODULE,
.dai_link = &a,
.num_links = 1,
  };
  
  Machine驱动是一个Platform Device,所以我们要先定义一个Platform Device,这里就是实现一个标准的Platform Device,就不详细说明了。
  在Platform Device的probe函数中注册 Machine驱动。有两种写法
  1     struct snd_soc_card *mycard = &card;
mycard->dev = &pdev->dev;
ret = snd_soc_register_card(mycard);
  2     static struct platform_device * Machine;
Machine = platform_device_alloc("soc-audio", -1);//注意名称一定为soc-audio
platform_set_drvdata(Machine , &card);//一定要把snd_soc_card 保存到platform_device结构的dev.drvdata字段中
platform_device_add(Machine );
  
   
    1.2 Platform驱动
    Platform驱动分为两个部分:snd_soc_platform_driver和snd_soc_dai_driver。其中,platform_driver负责管理音频数据,简单的说就是对音频DMA的设置。dai_driver则主要完成cpu一侧的dai的参数配置,也就是对cpu端音频控制器的寄存器的设置,例如时钟频率、采样率、数据格式等等的设置。
    1.2.1 snd_soc_platform_driver的注册
    先介绍两个数据结构
    struct snd_soc_platform_driver {


int (*probe)(struct snd_soc_platform *);
int (*remove)(struct snd_soc_platform *);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);


/* pcm creation and destruction */
int (*pcm_new)(struct snd_soc_pcm_runtime *);
void (*pcm_free)(struct snd_pcm *);


/* Default control and setup, added after probe() is run */
const struct snd_kcontrol_new *controls;
int num_controls;
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;


/*
* For platform caused delay reporting.
* Optional.
*/
snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,
struct snd_soc_dai *);


/* platform stream ops */
struct snd_pcm_ops *ops;


/* platform stream completion event */
int (*stream_event)(struct snd_soc_dapm_context *dapm, int event);


/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;


/* platform IO - used for platform DAPM */
unsigned int (*read)(struct snd_soc_platform *, unsigned int);
int (*write)(struct snd_soc_platform *, unsigned int, unsigned int);
    };
    这个结构通常我们需要关注下面的几个成员
    .pcm_new :函数指针,在驱动创建的时候由系统回调。
    .pcm_free:函数指针,在驱动销毁的时候由系统回调。
    .ops     :snd_pcm_ops结构的指针,定义了一系列回调函数。


    struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream);
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream * substream, unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream);
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
int (*copy)(struct snd_pcm_substream *substream, int channel,snd_pcm_uframes_t pos, void __user *buf, snd_pcm_uframes_t count);
int (*silence)(struct snd_pcm_substream *substream, int channel, snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
struct page *(*page)(struct snd_pcm_substream *substream, unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
    };
    .open :打开设备,准备开始播放的时候调用,这个函数主要是调用snd_soc_set_runtime_hwparams设置支持的音频参数。snd_dmaengine_pcm_open打开DMA引擎。
    .close:关闭播放设备的时候回调。该函数负责关闭DMA引擎。释放相关的资源。
    .ioctl:应用层调用的ioctl会调用这个回调。
    .hw_params:在open后,应用设置播放参数的时候调用,根据设置的参数,设置DMA,例如数据宽度,传输块大小,DMA地址等。
    .hw_free :  关闭设备前被调用,释放缓冲。
    .trigger:  DAM开始时传输,结束传输,暂停传世,恢复传输的时候被回调。
    .pointer: 返回DMA缓冲的当前指针。
    .mmap :   建立内存映射。


    介绍完数据结构,下面介绍如何注册snd_soc_platform_driver。首先定义一个snd_soc_platform_driver 
    struct snd_soc_platform_driver soc_platform = {
.ops = &ops,
.pcm_new = xxx_pcm_new,
.pcm_free = xxx_pcm_free,
    };
    snd_soc_platform_driver也是一个platform driver ,所以首先要定义一个platform driver ,这里要注意的是我们定义的这个platform driver的name一定要和前面snd_soc_dai_link 结构中定义的platform_name相同,这样我们定义的snd_soc_platform_driver才会被关联。
    然后再这个驱动的probe函数中,调用snd_soc_register_platform(&pdev->dev, &soc_platform );就完成了snd_soc_platform_driver的注册。


    1.2.1 snd_soc_dai_driver驱动的注册
    
    struct snd_soc_dai_driver {
/* DAI description */
const char *name;
unsigned int id;
int ac97_control;


/* DAI driver callbacks */
int (*probe)(struct snd_soc_dai *dai);
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);


/* ops */
const struct snd_soc_dai_ops *ops;


/* DAI capabilities */
struct snd_soc_pcm_stream capture;
struct snd_soc_pcm_stream playback;
unsigned int symmetric_rates:1;


/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
    };
    主要的成员如下:
    .probe   :回调函数,分别在声卡加载时被调用; 
    .remove  :回调函数,分别在声卡卸载时被调用;
    .suspend .resume:  分别在休眠唤醒的时候被调用
    .ops     :指向snd_soc_dai_ops结构,用于配置和控制该dai;
    .playback:  snd_soc_pcm_stream结构,用于说明播放时支持的声道数,码率,数据格式等能力;
    .capture : snd_soc_pcm_stream结构,用于说明录音时支持的声道数,码率,数据格式等能力;


    snd_soc_dai_driver中的ops字段介绍,这个字段是一个
    struct snd_soc_dai_ops {
/*
* DAI clocking configuration, all optional.
* Called by soc_card drivers, normally in their hw_params.
*/
int (*set_sysclk)(struct snd_soc_dai *dai,
int clk_id, unsigned int freq, int dir);
int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out);
int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);


/*
* DAI format configuration
* Called by soc_card drivers, normally in their hw_params.
*/
int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);
int (*set_tdm_slot)(struct snd_soc_dai *dai,
unsigned int tx_mask, unsigned int rx_mask,
int slots, int slot_width);
int (*set_channel_map)(struct snd_soc_dai *dai,
unsigned int tx_num, unsigned int *tx_slot,
unsigned int rx_num, unsigned int *rx_slot);
int (*set_tristate)(struct snd_soc_dai *dai, int tristate);


/*
* DAI digital mute - optional.
* Called by soc-core to minimise any pops.
*/
int (*digital_mute)(struct snd_soc_dai *dai, int mute);


/*
* ALSA PCM audio operations - all optional.
* Called by soc-core during audio PCM operations.
*/
int (*startup)(struct snd_pcm_substream *,
struct snd_soc_dai *);
void (*shutdown)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*hw_params)(struct snd_pcm_substream *,
struct snd_pcm_hw_params *, struct snd_soc_dai *);
int (*hw_free)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*prepare)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
/*
* For hardware based FIFO caused delay reporting.
* Optional.
*/
snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,
struct snd_soc_dai *);
    };的结构体。
    .set_sysclk : 设置dai的主时钟;
    .set_pll : 设置PLL参数;
    .set_clkdiv : 设置分频系数;
    .set_fmt   :设置dai的数据格式;
    .set_tdm_slot : 如果dai支持时分复用,用于设置时分复用的slot;
    .set_channel_map :声道的时分复用映射设置;
    .set_tristate  :设置dai引脚的状态,当与其他dai并联使用同一引脚时需要使用该回调;
    .sunxi_i2s_hw_params:设置硬件的相关参数。
    .startup :打开设备,设备开始工作的时候回调。
    .shutdown:关闭设备前调用。
    .trigger:  DAM开始时传输,结束传输,暂停传世,恢复传输的时候被回调。
    首先要定义一个结构体
    static struct snd_soc_dai_driver pcm_dai = {
.playback = {
.channels_min = 1,
.channels_max = 2,
.rates = SUNXI_PCM_RATES,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.channels_min = 1,
.channels_max = 2,
.rates = SUNXI_PCM_RATES,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &dai_ops,

    };
    同样的snd_soc_dai_driver也是一个platform driver ,所以首先要定义一个platform driver ,这里要注意的是我们定义的这个platform driver的name一定要和前面snd_soc_dai_link 结构中定义的cpu_dai_name相同,这样我们定义的snd_soc_dai_driver才会被关联。
    然后再这个驱动的probe函数中,调用snd_soc_register_dai(&pdev->dev, &pcm_dai );就完成了snd_soc_dai_driver的注册。


    1.3 Codec驱动
    还是先介绍下相关的数据结构。
    struct snd_soc_codec_driver {


/* driver ops */
int (*probe)(struct snd_soc_codec *);
int (*remove)(struct snd_soc_codec *);
int (*suspend)(struct snd_soc_codec *);
int (*resume)(struct snd_soc_codec *);


/* Default control and setup, added after probe() is run */
const struct snd_kcontrol_new *controls;
int num_controls;
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;


/* codec wide operations */
int (*set_sysclk)(struct snd_soc_codec *codec,
 int clk_id, int source, unsigned int freq, int dir);
int (*set_pll)(struct snd_soc_codec *codec, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out);


/* codec IO */
unsigned int (*read)(struct snd_soc_codec *, unsigned int);
int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);
int (*display_register)(struct snd_soc_codec *, char *,
size_t, unsigned int);
int (*volatile_register)(struct snd_soc_codec *, unsigned int);
int (*readable_register)(struct snd_soc_codec *, unsigned int);
int (*writable_register)(struct snd_soc_codec *, unsigned int);
unsigned int reg_cache_size;
short reg_cache_step;
short reg_word_size;
const void *reg_cache_default;
short reg_access_size;
const struct snd_soc_reg_access *reg_access_default;
enum snd_soc_compress_type compress_type;


/* codec bias level */
int (*set_bias_level)(struct snd_soc_codec *,
     enum snd_soc_bias_level level);
bool idle_bias_off;


void (*seq_notifier)(struct snd_soc_dapm_context *,
    enum snd_soc_dapm_type, int);


/* codec stream completion event */
int (*stream_event)(struct snd_soc_dapm_context *dapm, int event);


bool ignore_pmdown_time;  /* Doesn't benefit from pmdown delay */


/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
    };
    .probe : codec 的probe函数,由snd_soc_instantiate_card回调
    .remove:驱动卸载的时候调用
    .suspend .resume :电源管理,休眠唤醒的时候调用
    .controls :codec控制接口的指针,例如控制音量的调节、通道的选择等等
    .num_controls:codec控制接口的个数。也就是snd_kcontrol_new 的数量。
    .set_sysclk :设置时钟函数指针
    .set_pll :设置锁相环的函数指针
    .set_bias_level : 设置偏置电压。
    .read :读codec寄存器的函数
    .write:些codec寄存器的函数


     另外一个重要的结构
     struct snd_kcontrol_new {
snd_ctl_elem_iface_t iface; /* interface identifier */
unsigned int device; /* device/client number */
unsigned int subdevice; /* subdevice (substream) number */
const unsigned char *name; /* ASCII name of item */
unsigned int index; /* index of item */
unsigned int access; /* access rights */
unsigned int count; /* count of same elements */
snd_kcontrol_info_t *info;
snd_kcontrol_get_t *get;
snd_kcontrol_put_t *put;
union {
snd_kcontrol_tlv_rw_t *c;
const unsigned int *p;
} tlv;
unsigned long private_value;
    };
    这个结构是codec驱动工作的核心,通过这个结构控制许多开关(switch)和调节器(slider)等等,从而读写Codec相关寄存器,实现几乎codec支持的所有功能。下面介绍下这个结构的成员。
    .iface : 定义了control的类型,形式为SNDRV_CTL_ELEM_IFACE_XXX,对于mixer是SNDRV_CTL_ELEM_IFACE_MIXER,对于不属于mixer的全局控制,使用CARD;如果关联到某类设备,则是PCM、RAWMIDI、TIMER或SEQUENCER。
    .name :名称标识,这个字段非常重要,因为control的作用由名称来区分(如果名称相同需要通过index来区分,且后加的index的值要大于之前的index)。上层应用就是根据name名称标识来找到底层相应的control(上层应用也可以通过id来匹配,id对应的就是每一个control的下标)。name定义的标准是“SOURCE DIRECTION FUNCTION”即“源 方向 功能”,SOURCE定义了control的源,如“Master”、“PCM”等;DIRECTION 则为“Playback”、“Capture”等,如果DIRECTION忽略,意味着Playback和capture双向;FUNCTION则可以是“Switch”、“Volume”和“Route”等。
    .access :访问控制权限。SNDRV_CTL_ELEM_ACCESS_READ意味着只读,这时put()函数不必实 现;SNDRV_CTL_ELEM_ACCESS_WRITE意味着只写,这时get()函数不必实现。若control值频繁变化,则需定义 VOLATILE标志。当control处于非激活状态时,应设置INACTIVE标志。
    .private_value:包含1个长整型值,可以通过它给info()、get()和put()函数传递参数。在通常的使用中是一个指针。
    .info : 函数指针,获取相应的控制项的参数,例如取值范围
    .get :函数指正,获取相关控制项的值
    .put :函数指正,设置相关的寄存器。


    在include/sound/soc.h文件中定义了一些宏,来实现snd_kcontrol_new 的定义,有兴趣的话可以自己看看。


    下面来介绍如何注册一个codec驱动
    首先需要实现相关的数据结构
    const struct snd_kcontrol_new codec_controls[] = {......};
    struct snd_soc_dai_driver sndcodec_dai ={
    .name = "sndcodec",//注意这里的name一定要和machine驱动中的snd_soc_dai_link结构的codec_dai_name 相同,这样才能匹配上。
    ......
    };
    
    struct snd_soc_codec_driver soc_codec_dev_sndpcm = {...};


    同样的codec驱动也是一个platform driver ,所以首先要定义一个platform driver ,这里要注意的是我们定义的这个platform driver的name一定要和前面snd_soc_dai_link 结构中定义的codec_name相同,这样我们定义的codec驱动才会被关联。
    然后再这个驱动的probe函数中,调用snd_soc_register_codec(&pdev->dev, &soc_codec_dev_sndpcm , &sndcodec_dai , 1);就完成了snd_soc_dai_driver的注册。
    snd_soc_register_codec的最后一个参数是snd_soc_dai_driver 的个数,我们只定义了一个所以就是1,如果是一个snd_soc_dai_driver 的数组,那么这个参数就是数组元素的个数。
    讲到这里,我们的驱动都已经注册好了,ALSA已经可以正常工作了,但是如果这个时候播放一段音乐,我们是听不到声音的。为什么呢?因为我们还没有添加control呢,所以实际上codec还没有工作呢!下面介绍下添加control的方法。
    第一个办法是,直接在snd_soc_codec_driver 的结构中添加两个字段
    .controls =  codec_controls,
    .num_controls = ARRAY_SIZE(codec_controls),
    这样调用snd_soc_register_codec 的时候control就添加了。
    第二个方法是在snd_soc_codec_driver 结构定义的probe函数中添加。
    probe函数会在调用snd_soc_register_codec后被系统回调,我们实现下面的代码就好了。
    static int sndpcm_soc_probe(struct snd_soc_codec *codec)
    {
/* Add virtual switch */
snd_soc_add_codec_controls(codec, codec_controls,
ARRAY_SIZE(codec_controls));


return 0;
    }
    这样整个ALSA驱动就已经可以正常工作了。
  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值