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驅動就已經可以正常工作了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章