网页功能: 加入收藏 设为首页 网站搜索  
用VC开发WINAMP的音效插件
发表日期:2005-04-29作者:[转贴] 出处:  

本人应朋友之需要,动手写了一个基于WINAMP2的音效插件:消歌声处理。 相关的开发文档均来自于WINAMP的官方网站,如果你对些感兴趣,可访:http://www.winamp.com/来了解关于插件开发的详细资料, 下面我简单地介绍一下DSP(音效)插件的编写。并给出部分源代码,完整的代码请从这里下载:()。

WINAMP2的插件是一个WIN32的动态链接库,它位于WINAMP的安装目录下的plugins目录里,每个插件都符合一定的命名规则,并有一个指定的导出函数。WINAMP主程序枚举该目录下的这些DLL,来确定你安装了哪些插件。WINAMP的插件有许多种。你打开WINAMP的插件配置就看到了。音效插件是其中的一种。在WINAMP中简称为DSP。该类插件WINAMP规定其命名方式为dsp_*.dll,其中的“*”为你自已定义的一个任意名字。每个DSP插件都导出一个名为“winampDSPGetHeader2”的函数。它返回一个模块头信息结构的地址。WINAMP网站给开发者们提供了一个DSP模块的头文件。对该结构及函数作了定义,该头文件名为"dsp.h",内容如下:


#ifndef  _WINAMP_DSP_H_

#define  _WINAMP_DSP_H_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


// DSP plugin interface

// notes:
// any window that remains in foreground should optimally pass unused
// keystrokes to the parent (winamp's) window, so that the user
// can still control it. As for storing configuration,
// Configuration data should be stored in <dll directory>\plugin.ini
// (look at the vis plugin for configuration code)

typedef struct winampDSPModule {
  char *description;  // description
  HWND hwndParent;   // parent window (filled in by calling app)
  HINSTANCE hDllInstance; // instance handle to this DLL (filled in by calling app)

  void (*Config)(struct winampDSPModule *this_mod);  // configuration dialog (if needed)
  int (*Init)(struct winampDSPModule *this_mod);     // 0 on success, creates window, etc (if needed)

  // modify waveform samples: returns number of samples to actually write
  // (typically numsamples, but no more than twice numsamples, and no less than half numsamples)
  // numsamples should always be at least 128. should, but I'm not sure
  int (*ModifySamples)(struct winampDSPModule *this_mod, short int *samples, int numsamples, int bps, int nch, int srate);
  
  void (*Quit)(struct winampDSPModule *this_mod);    // called when unloading

  void *userData; // user data, optional
} winampDSPModule;

typedef struct {
  int version;       // DSP_HDRVER
  char *description; // description of library
  winampDSPModule* (*getModule)(int); // module retrieval function
} winampDSPHeader;

// exported symbols
typedef winampDSPHeader* (*winampDSPGetHeaderType)();

// header version: 0x20 == 0.20 == winamp 2.0
#define DSP_HDRVER 0x20


#endif /* _WINAMP_DSP_H_ */

 

该文件中也对一些东西进行了简要的描述。每个DSP DLL中都可以包含N个相互独立的不同插件。每个插件都对应一个数据结构。WINAMP通过一个函数并指定一个从0开始的索引号来取得这些插件的数据。

winampDSPModule结构描述了每个插件的数据。其实主要就是几个指向函数的指针。
Init函数在模块加载的时候被调用。你可以在这个函数的实现里初始化需要的数据。生成窗口等。
Config函数在需要显示一个配置对话的时候被调用。
ModifySamples函数是个主要的函数,它会在插件工作的时候被很快速地反复调用。在这里你对来自WINAMP的波形数据进行处理。该函数应该有足够高的效率。
Quit函数在模块卸载时被调用,你可以在这里做些清理工作。
userData是一个归你使用的指针,你可以让它指向你自己的任何数据,通常一般也用不到。
hwndParent是父窗口的句柄,WINAMP主程序将填充这个数据,它就是你工作的主窗口。
hDllInstance是模块DLL的实例句柄。WINAMP主程序会填充这个数据。


好了,也就这么多。我们所要做的,就是实现这些函数,并定义这些数据结构。WINAMP的网站提供了一个小小的DSP工程来给我们参考,建议你仔细看看。对你会的很大的帮助。值得一提的是,在该例子工程中包含了三个插件,其中之一就是一个简单的消歌声程序。它所做的就是简单的将左右声道相减。我们要做的当然不是这么简单的。我们要消声的同时最大限度地保持音乐的原来效果。


我的消哥声基于同样的原理:通常立体声歌曲的人声在左右声道的分量上相同的,而大部分乐器声则以不同的分量分布在左右声道。但是有一点:由于人耳对低频段的声音定位不准,所以低频乐声通常也是左右声道均匀的。直接左右声道虽然能很好地消除人声但对原音乐的低频分量也会有严重的衰减。消声后的音乐就听起来很空,没有力度。


于是消声采用以下方法:分别从左右声首提取出人声部分(300-5000Hz,大部分人声都包含在这个频段内)然后相减,这个差值再和已经滤掉人声部分的左右声道分别进行混合。得出新的左右声道数据。在这里一个最主要的算法也就是离散音频数据的滤波算法,我采用的是简单而快速有效的双二次滤波算法。


该算法被封装成一个类:


// Filter1.h: interface for the CFilter class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_FILTER1_H__A8AD654E_7587_41C5_BE3F_D3E266EA5788__INCLUDED_)
#define AFX_FILTER1_H__A8AD654E_7587_41C5_BE3F_D3E266EA5788__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

// 基本双二次滤波器

class CFilter 
{
public:
 CFilter();
 virtual ~CFilter();

 // 设置滤波器参数
 void SetFilterParament(int frequency, float Q = 1.0f);
 // 滤波函数
 virtual void Filter(short * pData, int num, int nch, int srate);
protected:

 // 计算滤波参数
 virtual void CalcFilterParament() = 0;

 int _sr;     // 采样频率
 int _f;      // 截止频率
 float _Q;    // 品质因数

 float _a0, _a1, _a2;     // 滤波器参数
 float _b1, _b2;
private:
 // 历史值
 int _xn1_1, _xn2_1, _yn1_1, _yn2_1;
 int _xn1_2, _xn2_2, _yn1_2, _yn2_2;
};

const float PI = 3.1415926f; // π值

// 截断函数
//template<class T>
__inline  int BOUND(int x,int min, int max)
{
 return (x<min)?min:((x>max)?max:x);
}

#endif // !defined(AFX_FILTER1_H__A8AD654E_7587_41C5_BE3F_D3E266EA5788__INCLUDED_)

 

//////////////////////////////////////////////////////////////////////
// Filter1.cpp: implementation of the CFilter class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "Filter1.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CFilter::CFilter()
{
 // 初始化参数
 _xn1_1 = 0, _xn2_1 = 0, _yn1_1 = 0, _yn2_1 = 0;
 _xn1_2 = 0, _xn2_2 = 0, _yn1_2 = 0, _yn2_2 = 0;
 _a0 = 1.0f, _a1 = 1.0f, _a2 = 1.0f;
 _b1 = 1.0f, _b2 = 1.0f;
 _sr = 44100;
 _f = 1000;
 _Q = 1.0f;
}

CFilter::~CFilter()
{

}

// 设置滤波参数
void CFilter::SetFilterParament(int frequency, float Q)
{
 _f = frequency;
 _Q = Q;
 CalcFilterParament();
}


// 滤波函数
void CFilter::Filter(short *pData, int num, int nch, int srate)
{
 // 如果采样率改变,应重新计算参数
 if(srate != _sr)
 {
  _sr = srate;
  CalcFilterParament();
 }
 if(nch == 2) // 双声道
 {
  short * l = pData;
  short * r = pData + 1;
  int i = num;
  while (i-->0)
  {
   // 计算输出
   int lyn = (int)(_a0 * (*l) + _a1 * _xn1_1 + _a2 * _xn2_1
       - _b1 * _yn1_1 - _b2 * _yn2_1);

   int ryn = (int)(_a0 * (*r) + _a1 * _xn1_2 + _a2 * _xn2_2
       - _b1 * _yn1_2 - _b2 * _yn2_2);

   // 更新历史数据
   _xn2_1 = _xn1_1; _xn1_1 = *l;
   _yn2_1 = _yn1_1; _yn1_1 = lyn;

   _xn2_2 = _xn1_2; _xn1_2 = *r;
   _yn2_2 = _yn1_2; _yn1_2 = ryn;

   // 截断至16位
   lyn = BOUND(lyn,-32768,32767);
   ryn = BOUND(ryn,-32768,32767);

   *l = lyn;
   *r = ryn;

   l+=2;
   r+=2;
  }

 }
 else if(nch == 1) // 单声道
 {
  short * m = pData;
  int i = num;
  while (i-->0)
  {
   int myn = (int)(_a0 * (*m) + _a1 * _xn1_1 + _a2 * _xn2_1
       - _b1 * _yn1_1 - _b2 * _yn2_1);

   if(myn > 32767) myn = 32767;
   else if(myn < -32768) myn = -32768;

   _xn2_1 = _xn1_1; _xn1_1 = *m;
   _yn2_1 = _yn1_1; _yn1_1 = myn;

   *m = myn;
   m++;

  }
 }
}

 

这是一个基本的滤波器,它是一个虚拟基类,因为他不知道如何计算滤波参数。所以不能单独使用它的。我们从这个类派生出三种最常用的滤波器:高通滤波器,低通滤波器和带通滤波器:

下面是高通滤波器类:


// HighPassFilter.h: interface for the CHighPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_HIGHPASSFILTER_H__1A792751_B704_43D5_AEA6_9747710D0AD8__INCLUDED_)
#define AFX_HIGHPASSFILTER_H__1A792751_B704_43D5_AEA6_9747710D0AD8__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "Filter1.h"

// 高通滤波器
class CHighPassFilter : public CFilter 
{
public:
 CHighPassFilter();
 virtual ~CHighPassFilter();

 virtual void CalcFilterParament();
};

#endif // !defined(AFX_HIGHPASSFILTER_H__1A792751_B704_43D5_AEA6_9747710D0AD8__INCLUDED_)

 

//////////////////////////////////////////////////////////////////////
// HighPassFilter.cpp: implementation of the CHighPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "HighPassFilter.h"
#include "math.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CHighPassFilter::CHighPassFilter()
{
}

CHighPassFilter::~CHighPassFilter()
{

}

// 计算高通滤波器参数
void CHighPassFilter::CalcFilterParament()
{
 float omega = (2.0f * PI * _f) / _sr;
 float sin_omega = sinf(omega);
 float cos_omega = cosf(omega);

 float alpha = sin_omega / (2.0f * _Q);
 float scalar = 1.0f / (1.0f + alpha);

 _a0 = 0.5f * (1.0f + cos_omega) * scalar;
 _a1 = -(1.0f + cos_omega) * scalar;
 _a2 = _a0;
 _b1 = -2.0f * cos_omega * scalar;
 _b2 = (1.0f - alpha) * scalar;
}

 


下面这个是低通滤波器类:

// LowPassFilter.h: interface for the CLowPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_LOWPASSFILTER_H__5AA8CBCC_1043_4093_B023_C3F4869D7163__INCLUDED_)
#define AFX_LOWPASSFILTER_H__5AA8CBCC_1043_4093_B023_C3F4869D7163__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "Filter1.h"

//  低通滤波器
class CLowPassFilter  : public CFilter
{
public:
 CLowPassFilter();
 virtual ~CLowPassFilter();
 virtual void CalcFilterParament();

};

#endif // !defined(AFX_LOWPASSFILTER_H__5AA8CBCC_1043_4093_B023_C3F4869D7163__INCLUDED_)

 

//////////////////////////////////////////////////////////////////////
// LowPassFilter.cpp: implementation of the CLowPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "LowPassFilter.h"
#include "math.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CLowPassFilter::CLowPassFilter()
{

}

CLowPassFilter::~CLowPassFilter()
{

}

// 计算低通滤波器参数
void CLowPassFilter::CalcFilterParament()
{
 float omega = (2.0f * PI * _f) / _sr;
 float sin_omega = sinf(omega);
 float cos_omega = cosf(omega);

 float alpha = sin_omega / (2.0f * _Q);
 float scalar = 1.0f / (1.0f + alpha);

 _a0 = 0.5f * (1.0f - cos_omega) * scalar;
 _a1 = (1.0f - cos_omega) * scalar;
 _a2 = _a0;
 _b1 = -2.0f * cos_omega * scalar;
 _b2 = (1.0f - alpha) * scalar;
}

 

带通滤波器其实是一个高通滤波器和一个低通滤波器的串联,所以它从这两个类派生:

// BandPassFilter.h: interface for the CBandPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_BANDPASSFILTER_H__207E376D_965B_47F9_A8EE_0ED9F14998D6__INCLUDED_)
#define AFX_BANDPASSFILTER_H__207E376D_965B_47F9_A8EE_0ED9F14998D6__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "HighPassFilter.h"

#include "LowPassFilter.h"

// 带通滤波器
class CBandPassFilter :
 public CHighPassFilter,
 public CLowPassFilter 
{
public:
 CBandPassFilter();
 virtual ~CBandPassFilter();
 // 滤波函数
 void Filter(short *pData, int num, int nch, int srate);
 // 设置滤波参数
 void SetFilterParament(int hf, int lf, float hQ = 1.0f, float lQ = 1.0f);
};

#endif // !defined(AFX_BANDPASSFILTER_H__207E376D_965B_47F9_A8EE_0ED9F14998D6__INCLUDED_)

 

//////////////////////////////////////////////////////////////////////
// BandPassFilter.cpp: implementation of the CBandPassFilter class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "filter.h"
#include "BandPassFilter.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CBandPassFilter::CBandPassFilter()
{

}

CBandPassFilter::~CBandPassFilter()
{

}

// 滤波函数
void CBandPassFilter::Filter(short * pData, int num, int nch, int srate)
{
 // 一个高通和一个低能滤波器串联操作
 CHighPassFilter::Filter(pData, num, nch, srate);
 CLowPassFilter::Filter(pData, num, nch, srate);
}

void CBandPassFilter::SetFilterParament(int hf, int lf, float hQ, float lQ)
{
 CHighPassFilter::SetFilterParament(hf, hQ);
 CLowPassFilter::SetFilterParament(lf, lQ);
}

 

好了,有了这三个滤波器,下面将来构建这个插件DLL:

首先,实现DLL的模块导出函数及数据结构:

#ifndef _MODULE_H_

#define _MODULE_H_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


#include "dsp.h"

void Config(struct winampDSPModule *this_mod);
int Init(struct winampDSPModule *this_mod);
void Quit(struct winampDSPModule *this_mod);

int DoFilter(struct winampDSPModule *this_mod,
    short int *samples, int numsamples,
    int bps, int nch, int srate);


#endif // _MODULE_H_


头文件中说明了插件用到的几个函数, 这些函数将在其它文件中给出其实现。


#include "stdafx.h"
#include "module.h"
#include "BandPassFilter.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif


winampDSPModule *GetModule(int which);

winampDSPHeader hdr = {
 DSP_HDRVER,
 "金燕专用效果处理V1.00",
 GetModule
};

// this is the only exported symbol. returns our main header.
extern "C"
__declspec( dllexport ) winampDSPHeader *winampDSPGetHeader2()
{
 return &hdr;
}

winampDSPModule mod1 =
{
 "消歌声处理(★特别送给金燕★)",
 NULL, // hwndParent
 NULL, // hDllInstance
 Config,
 Init,
 DoFilter,
 Quit,
 NULL
};

winampDSPModule *GetModule(int index)
{
 switch(index)
 {
 case 0: return &mod1;
 default:return NULL;
 }
}


由于只包含一个插件,所以GetModule的实现在index为1 的时候返回NULL,告诉WINAMP主程序,仅仅只有一个,下面没有了。

我们需要一个对话框来和用户交互:


#if !defined(AFX_FILTERDLG_H__F2F4BB76_FB95_4ACE_831B_C13F8E11FD7E__INCLUDED_)
#define AFX_FILTERDLG_H__F2F4BB76_FB95_4ACE_831B_C13F8E11FD7E__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// FilterDlg.h : header file
//
#include "BandPassFilter.h"
/////////////////////////////////////////////////////////////////////////////
// CFilterDlg dialog

// 消人声调节对话框
class CFilterDlg : public CDialog
{
// Construction
public:
 CFilterDlg(CWnd* pParent = NULL);   // standard constructor

// Dialog Data
 //{{AFX_DATA(CFilterDlg)
 enum { IDD = IDD_FILTER };
 int  m_nch;       // 声道数
 int  m_srate;     // 采样频率
 int  m_bytes;     // 已经处理的字节统计
 int  m_calls;     // 处理函数调用次数统计
 int  m_low;       // 低频频率
 int  m_high;      // 高频频率
 int  m_balance;   // 平衡补偿值
 //}}AFX_DATA
 BOOL    m_bEnable;  // 开关

 BOOL Create();        // 创建对话框
 void Filter(short* pData, int num, int nch, int srate);  // 处理函数
// Overrides
 // ClassWizard generated virtual function overrides
 //{{AFX_VIRTUAL(CFilterDlg)
 public:
 virtual BOOL PreTranslateMessage(MSG* pMsg);
 virtual BOOL DestroyWindow();
 protected:
 virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
 //}}AFX_VIRTUAL

// Implementation
protected:
 CWnd*   m_pParentWnd;
 CBandPassFilter    m_filterB;    // 带通滤波器
 CHighPassFilter    m_filterH;    // 高通滤波器
 CLowPassFilter     m_filterL;    // 低能滤波器
 float    m_ls, m_rs;
 // Generated message map functions
 //{{AFX_MSG(CFilterDlg)
 afx_msg void OnCheck1();
 virtual BOOL OnInitDialog();
 afx_msg void OnDestroy();
 afx_msg void OnTimer(UINT nIDEvent);
 afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_FILTERDLG_H__F2F4BB76_FB95_4ACE_831B_C13F8E11FD7E__INCLUDED_)


上面是对话框类的说明, 实现不再列出,请参考完整工程。只给出几个关键的实现:

// 缓冲区
short  buf1[65536];  // 带通滤波缓冲器
short  buf2[65536];  // 低通滤波缓冲器
short  buf3[65536];  // 高通滤波缓冲器

// 消人声函数
void CFilterDlg::Filter(short* pData, int num, int nch, int srate)
{
 // 更新统计数据
 m_nch = nch;
 m_srate = srate;
 m_bytes+=num;
 m_calls++;
 // 应该在双声道下工作
 if(nch == 2 && m_bEnable)
 {
  // 将音频数据分解成高频段,低频段,及包含人声的中频段
  memcpy(buf1, pData, num * nch * sizeof(short));
  memcpy(buf2, pData, num * nch * sizeof(short));
  memcpy(buf3, pData, num * nch * sizeof(short));

  m_filterB.Filter(buf1, num, nch, srate);
  m_filterL.Filter(buf2, num, nch, srate);
  m_filterH.Filter(buf3, num, nch, srate);

  short * a = buf1;
  short * b = buf2;
  short * c = buf3;
  short * out = pData;
  for(int i = 0; i < num; ++i)
  {
   //
   int isb = (int)(a[0] * m_ls - a[1] * m_rs + 0.5f);
   // 合成
   int l = isb + b[0] + c[0];
   int r = isb + b[1] + c[1];

   // 衰减一些
   //l = (int)(l*0.8);     
   //r = (int)(r*0.8);

   // 截断至16位
   l = BOUND(l,-32768, 32767);
   r = BOUND(r,-32768, 32767);

   // 输出
   out[0] = l;
   out[1] = r;
  
   a+=2;
   b+=2;
   c+=2;
   out += 2;
  }
 }
}

// 显示配置对话
void Config(struct winampDSPModule *this_mod)
{
 ::MessageBox(this_mod->hwndParent, "使用双二次滤波器的消歌声程序\n"
 "    Copyright (c) 2004\n版权归边城浪子和爱吃女孩共同拥有","★特别送给金燕★",MB_OK|MB_ICONWARNING);
}

// 初始化函数(显示调节对话框)
int Init(struct winampDSPModule *this_mod)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState());

 if(g_pDlg != NULL) delete g_pDlg;

 g_pDlg = new CFilterDlg(CWnd::FromHandle(this_mod->hwndParent));
 g_pDlg->Create();
 g_pDlg->ShowWindow(SW_SHOW);
 return 0;
}

// 退出处理
void Quit(struct winampDSPModule *this_mod)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState());
 if(g_pDlg)
  g_pDlg->DestroyWindow();
 g_pDlg = NULL;
}

// 音频处理函数
int DoFilter(struct winampDSPModule *this_mod,
    short int *samples, int numsamples,
    int bps, int nch, int srate)
{
 if(bps ==16)
 {
  if(g_pDlg!=NULL)
   g_pDlg->Filter(samples, numsamples, nch, srate);

 }

 return numsamples;
}


 

我来说两句】 【加入收藏】 【返加顶部】 【打印本页】 【关闭窗口
中搜索 用VC开发WINAMP的音效插件
本类热点文章
  C++新手必问之头文件
  基于VFW的视频应用程序开发
  VC入门宝典
  Visual C++ 入门精解
  送给正在学习C++朋友的50条建议
  用c++实现的"贪吃蛇"游戏源码
  对VC的批判,我要打击大家学VC的积极性了
  VC常用数据类型使用转换详解
  学好VC++的十大良好习惯
  用Debug函数实现API函数的跟踪
  用VC作的一个扑克游戏
  DirectX游戏开发入门
最新分类信息我要发布 
最新招聘信息

关于我们 / 合作推广 / 给我留言 / 版权举报 / 意见建议 / 广告投放  
Copyright ©2003-2019 Lihuasoft.net webmaster(at)lihuasoft.net
网站编程QQ群   京ICP备05001064号 页面生成时间:0.00461