Winamp输入模块编写详解

Skin Winamp:个性化Winamp外观设计 Winamp作为经典的媒体播放器,提供了强大的皮肤自定义功能,使用户可以根据自己的喜好和审美来定制界面。本章将探讨Winamp如何实现皮肤自定义功能,以及用户如何利用这些功能来打造个性化的播放界面。Winamp的皮肤是一组包含了界面设计、颜色方案和其他视觉元素的文件集合。用户能够通过简单地下载和应用皮肤文件,改变Winamp的外观,使其更加贴合个人的使用习惯和视觉偏好。为了实现个性化,Winamp为用户提供了一个易于操作的皮肤管理器。 阅读详情

写给C/C++基础类的朋友:
    很长时间都没有认真的来版上和网友们聊聊了,偶尔上来也是随便转转,仅处理一下版务。这些日子里来你们之中的有些人给我发了短消息,问道“嘿,哥们(大多数时候用的是‘老大’这个词,但我并不怎么喜欢这个称呼,感觉有点像黑社会?),最近怎么不见你露面啊,忙什么呢?”而我在极为敷衍的回答道:“在忙自己的活呢,不好意思啊。”之后也感觉到非常内疚,但是每当我一想起现在做的工作,能真正的给那些在C/C++版里做出贡献的网友们留下点东西,我也便觉得心安理得了。

 


    在正式开始之间我还是想说一些废话,如果你是老手就向下拖动右边的滚动条30-50像素吧。Winamp是众所周知的世界上最好最流行的音乐(媒体)播放软件,它所支持的音频格式之多另其它同类软件望尘莫及。而这种广泛支持性又是由于它采用了非常合理巧妙的插件式设计,每个人都可以用winamp来播放自己发明的音频格式,只要你写出了相应的输入插件。但这篇文章不会教你用小波变换等高难度数学算法来编写比MP3或APE更为优秀的音频编码格式,如果你企图在文章里找到这些东西,我还是劝你到此为止,关掉浏览器,然后去图书馆翻看《信号学》。在这篇文章里我首先会教你怎样编写一个符合Winamp插件规范的输出插件,你一定会问,我刚才说的不是输入插件吗?其实这两种插件是一体的,你向下看便会得到答案。在练习编写输出插件的的同时,我还将帮你了解电声的基础原理、了解Winamp的设计结构、熟悉WAVE(PCM)的文件格式、声卡的基本工作机制等。最后,我们还会用DirectX来完成这个音频的输出模块。这是一系列非常令人激动的主题,但请你还是先不要急着热血沸腾,下面这几盆凉水是一定要泼的。在继续看下去之间请确定你有这些条件或能力:可以随时打开VC.net编写测试程序、装有DirectX8.1SDK、能够熟练的开发一个并不十分复杂的SDK程序,熟悉C++……好了,想来想去要求的条件就这么多了,我为了避免吓跑大批的读者还是尽量使用了较温合的词汇。还有一点要说的是,我希望你能在一个愉快的心境中阅读全文,而不是像蹲在厕所里读《量子力学》那种感觉一样,确定我们可以开始了吗?

    为了从哪里开始这个问题我伤透了脑筋,想来想去毕竟这偏文章是发表在网上的,那么就从你移动鼠标开始吧,现在再打开一个浏览器,进入下面的网址:http://www.winamp.com/nsdn/winamp2x/dev/plugins/out.jhtml这里是Winamp插件开发的主页,他美其名曰NSDN,可是和MSDN相比,它除了一点原代码之外什么也没有。你现在需要做的是下载那个OUT_MINISDK,链接在:http://ftpwa.newaol.com/customize/component/nsdn/winamp2x/out_minisdk.zip,你打开这个压缩包你会看到三个文件in2.h、out.h和out_raw.c,什么,你还看到了别的文件?哦,在那是还有一些别的文件,但我们现在只需要这三个。把它们解压出来,然后打开他们,因为你在听我讲述Winamp的设计结构的时候需要对应着看看实现的代码。

    当我第一次看到这个插件的封装结构时我惊呆了,原来Winamp比我想象中的更懒,它其实什么都没有做,仅仅是一个界面,调用了一些输入的接口,仅此而已。我们先看一下In_Module和Out_Module的结构:

typedef struct
{
 int nVer; // 模块版本号
 char *szDesc; //模块描述信息
 HWND hMainWnd; // Winamp的主窗体句柄(由Winamp来填写)
 HINSTANCE hDllInstance;  // DLL实例句柄(由Winamp来填写)
 char *szFileExt; // 扩展名过滤器,格式参见GetOpenFileName
 int nIsSeekable; // 是否可索引媒体,是-你可以拖动进度条,否-反之
 int UsesOutputPlug; // 是否使用输出插件?你想在这个模块里搞定一切?

// 下面都是函数指针,将被Winamp调用
 void (*Config)(HWND hwndParent); // 配置对话框
 void (*About)(HWND hwndParent);  // 关于对话框
 void (*Init)();  // 初始化
 void (*Quit)();  // 退出
 // szFile - 传入的文件名,szTitle - 传出的标题,nLen - 转出的时间长度,毫秒。
 // 如果szFile传NULL,则返回当前播放文件的信息
 void (*GetFileInfo)(char *szFile, char *szTitle, int *nLen);
 int (*InfoBox)(char *szFile, HWND hwndParent); // 弹出文件信息对话框
 int (*IsOurFile)(char *szFile); // 检查文件格式
 int (*Play)(char *szFile); // 开始播放文件szFile,返回0正常,-1错误
 void (*Pause)(); // 暂停处理
 void (*UnPause)(); // 取消暂停
 int (*IsPaused)(); // 是否斩停?1是暂停,0不是
 void (*Stop)();  // 停止播放
 int (*GetLength)(); // 取得长度,毫秒单位
 int (*GetOutputTime)();  // 获取当前时间,一般调用out模块的同名函数即可
 void (*SetOutputTime)(int nTime); // 索引到某一时刻
 void (*SetVolume)(int volume); // 音量调节,从0 - 255
 void (*SetPan)(int pan); // 左右声道平衡,从-127 - 127
 
//下面的函数多和AVS、可视化效果、均衡器等有关,具体咱们用不到,就暂时不讲了。
 void (*SAVSAInit)(int maxlatency_in_ms, int srate);
 void (*SAVSADeInit)(); // call in Stop()
 void (*SAAddPCMData)(void *PCMData, int nch, int bps, int timestamp);
 int (*SAGetMode)();
 void (*SAAdd)(void *data, int timestamp, int csa);
 void (*VSAAddPCMData)(void *PCMData, int nch, int bps, int timestamp);
 int (*VSAGetMode)(int *specNch, int *waveNch);
 void (*VSAAdd)(void *data, int timestamp);
 void (*VSASetInfo)(int nch, int srate);
 int (*dsp_isactive)();
 int (*dsp_dosamples)(short int *samples, int numsamples, int bps, int nch, int srate);
 void (*EQSet)(int on, char data[10], int preamp);
 void (*SetInfo)(int bitrate, int srate, int stereo, int synched);

 Out_Module *outMod; // 看看,Winamp终于露出马脚了吧?
} In_Module;


typedef struct
{
// 下面有些和In_Module一样,就不赘述了。
 int nVer;
 char *szDesc;
 int nId; // 自己给一个ID,不知道有什么用,反正大于65536就行了。
 HWND hMainWindow;
 HINSTANCE hDllInstance;
 void (*Config)(HWND hwndParent);
 void (*About)(HWND hwndParent);
 void (*Init)();
 void (*Quit)();
 // nSample - 采样率, nChannels - 声道数,1或2
 // nBitPerSamp - 每采样的位率,nBufLen、nPreBufLen - 缓冲长度,咱们用不到
 // 返回大于0正常播放,小于0失败
 int (*Open)(int nSample, int nChannels, int nBitPerSamp, int nBufLen, int nPreBufLen);
 void (*Close)(); // 关闭输出设备
 // pBuf - 内存数据块,nLen - 数据块的长度
 int (*Write)(char *pBuf, int len); // 返回0成功,其它不成功
 int (*CanWrite)(); // 表示当前状态是否可写
 int (*IsPlaying)(); // 表示是否正在播放
 int (*Pause)(int pause); // 暂停,稍后详释
 void (*SetVolume)(int volume);
 void (*SetPan)(int pan);
 void (*Flush)(int t); // 刷新缓冲
 int (*GetOutputTime)(); // 获取输出时间
 int (*GetWrittenTime)(); // 返回写入的时间
} Out_Module;


你应该还在out_raw.c里看到了这样的代码:
__declspec( dllexport ) Out_Module * winampGetOutModule()
{
 return &out;
}

    相信大多数有些经验的人看到这里就已经真像大白了,不过考虑到文章的“兼容性”我还是要详述一翻。就拿输出模块来讲,Winamp在启动的时候,会去Plugins目录中查找所有可用的输出插件,如果找到有可用的输出模块,就会先把这个DLL加载进来。而这个DLL被加载时会按照编写者的定义填写好一个Out_Module全局对象,里面的函数指针成员则指向已经实现的函数代码。而Winamp调用这些函数的方法就是通过这个DLL导出的winampGetOutModule()函数来得到那个Out_Module全局对象的指针,接着它就可以通过这个全局对象的成员函数指针来调用那些函数了。是不是很巧妙呢?而插件的反馈机制则是由Windows标准的消息机制来完成,PostMessage就是最简单的方法,这也正是每一个插件里都会有一个HWND hMainWindow主窗体句柄的原因。事实上Winamp在播放音乐时大多数调用的都只是In_Module的接口,因为In_Module里包括一个Out_Module的指针,所以In_Module自动调用Out_Module相应的函数,为Winamp完成所有的事情,Out_Module一般只是用来反馈。

    在了解了Winamp的工作原理之后,下面我们将开始编写第一个Winamp输出模块。为了方便调试,并且避免对你的winamp造成意外的伤害,我们会先写一个winamp模拟程序,它会来调用Winamp的插件。就照下面的步骤去做吧,在VC.net中新建一个MFC应用程序,记住选对话框。添加两个文件MusicPlayer.h和MusicPlayer.cpp。然后把刚才的那两个大结构体Out_Module和In_Module的声明加入头文件,接着写一些咱们需要调用的函数声明:

void OutInit();
void OutConfig( HWND hWnd );
void OutAbout( HWND hWnd );
int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen );
void OutClose( void );
void OutQuit( void );
int OutWrite( char *pBuf, int nLen );
int OutCanWrite( void );
int OutIsPlaying( void );
int OutPause( int nPause );
void OutFlush( int nT );
int OutGetWrittenTime( void );
int OutGetOutputTime( void );
void OutSetVolume( int nVolume );
void OutSetPan( int nPan );

    我们还需要在头文件里声明以下几个东东:

// 用于从DLL中获取上面说的那个接口的函数指针
typedef In_Module* (WINAPI *PWINAMPGETIN2MOD)( void );
// 用于方便的初始化对象
void InitModules( In_Module *pInModule, Out_Module *pOutModule );

    现在回到MusicPlayer.cpp里,实现上面声明的那些函数,先把大括号写好,有返回值的函数都返回0,让整个工程能够顺利的通过编译,稍后我们再回来填写这些函数里的代码。在这之前我们先得实现一些输入模块的结构体里没有填好而等着Winamp来填的函数指针,如果我们不填,输入模块就会调用空地址,结果可想而知。下面是这些函数的声明及实现,直接放到MusicPlayer.cpp文件的顶部即可,不会占太多空间吧? :)

void SAVSAInit(int maxlatency_in_ms, int srate){}
void SAVSADeInit(){}
void SAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int SAGetMode(){return 0;}
void SAAdd(void *data, int timestamp, int csa){}
void VSAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int VSAGetMode(int *specNch, int *waveNch){return 0;}
void VSAAdd(void *data, int timestamp){}
void VSASetInfo(int nch, int srate){}
int dsp_isactive(){return 0;}
int dsp_dosamples(short int *samples, int numsamples, int bps, int nch, int srate){return 0;}
void EQSet(int on, char data[10], int preamp){}
void SetInfo(int bitrate, int srate, int stereo, int synched){}

    上面这些函数都是用于可视化效果的,咱们当然不需要,所以用最简单的代码实现一下就行了!下面再实现一下InitModules函数:

void InitModules( In_Module *pInModule, Out_Module *pOutModule )
{
 // 如果你用MFC的话,可以用下面的方法得到主窗体指针
 // 如果不是,那你需要另想办法,比如全局变量
 g_hWnd = ::AfxGetMainWnd()->GetSafeHwnd();
 // 给传进来的输出模块结构体填值,使和在其被调用时将调用到上面相应的函数
 pOutModule->description = "Creamdog DirectSount Out Module v1.0";
 pOutModule->About = OutAbout;
 pOutModule->CanWrite = OutCanWrite;
 pOutModule->Open = OutOpen;
 pOutModule->Close = OutClose;
 pOutModule->Config = OutConfig;
 pOutModule->Flush = OutFlush;
 pOutModule->GetOutputTime = OutGetOutputTime;
 pOutModule->GetWrittenTime = OutGetWrittenTime;
 pOutModule->hDllInstance = ::AfxGetInstanceHandle();
 pOutModule->hMainWindow = g_hWnd;
 pOutModule->id = 32;
 pOutModule->Init = OutInit;
 pOutModule->IsPlaying = OutIsPlaying;
 pOutModule->Pause = OutPause;
 pOutModule->Quit = OutQuit;
 pOutModule->SetPan = OutSetPan;
 pOutModule->SetVolume = OutSetVolume;
 pOutModule->version = 100;
 pOutModule->Write = OutWrite;
 pOutModule->Init();

 // 输入模块的大部分功能函数在你加载DLL时已经填好
 // 这里只需要填一下那写“无用而危险”的函数指针
 pInModule->outMod = pOutModule;
 pInModule->hMainWindow = pOutModule->hMainWindow;
 pInModule->hDllInstance = pOutModule->hDllInstance;
 pInModule->SAVSAInit = SAVSAInit;
 pInModule->SAVSADeInit = SAVSADeInit;
 pInModule->SAAddPCMData = SAAddPCMData;
 pInModule->SAGetMode = SAGetMode;
 pInModule->SAAdd = SAAdd;
 pInModule->VSAAddPCMData = VSAAddPCMData;
 pInModule->VSAGetMode = VSAGetMode;
 pInModule->VSAAdd = VSAAdd;
 pInModule->VSASetInfo = VSASetInfo;
 pInModule->dsp_isactive = dsp_isactive;
 pInModule->dsp_dosamples = dsp_dosamples;
 pInModule->EQSet = EQSet;
 pInModule->SetInfo = SetInfo;
 // 最后初始化一下
 pInModule->Init();
}

看看程序能不能正常编译?如果可以,那么开始我们的主题吧,现在回过头来填写上面那些声明在头文件里的重要的函数。你还是不要指望这一次我们就能播出音乐来,因为对于一些初学者来说,他们的基础知识离此还差一些,所以我需要先讲述一下电声学的基本原理,如果你是老鸟,那么就敬请您再一次向下拖动右边的滚动条了。

众所周知,声音是由于空气的振动而使耳膜产生谐振而引起的,振动的频率便是声波的频率,一般人耳的接受能力是20Hz到20000Hz,低于此泛围的叫次声波,高于此泛围的叫超声波。不同的音调的频率当然不一样,比如do、re、mi就是三种不同的频率。那么,不同的乐器发出同要频率的声音,但我们却可以听出它们的不同,这又是怎么回事呢?这就是音色的差别,事实上不同的乐器之间的差别并不在频率上而是在波形上,比如同样频率的声波,在一个周期的1/4阶段部分内陡升而在3/4阶段内缓升和1/4阶段部分内缓升而在3/4阶段内陡升所产生的声音是截然不一样的。

音箱中喇叭的原理其实很简单,就是通过电磁感应现象,将变化的电流转为盆膜的振动而产生空气的振动,所以只要有在人耳能够接受的频率泛围并且足够强大的振动的电流输入喇叭,人耳在足够近的声场距离内就可以得到声压了(听到声音)。当然在多媒体有源音箱上除了喇叭还有运放(电流放大器)和功放(功率放大器)等器件,这些器件的大致作用就是将输入的电流和功率放大至喇叭可以发出声音的泛围内。电脑上的声卡的作用就是将传递给声卡的数字音频数据经过缓冲、处理、分流、转换等操作再通过接口将载有模拟信号的电流传送到音箱,令其发声。

现在对于电声的原理因该比较清楚了,可是声波电信号是一条光滑的曲线,做为只有1和0表示的计算机数据该如何表示这种模拟信号呢?下面我们将引入一个概念“采样率”。比如一种声波的频率是1000Hz,也就代表它一秒钟内将振动1000次,即1000个周期,那么如果需要使这个声音从数字波还原成声波之后还能听个响,那么每一个周期里你就需要至少记录8个采样点才能较为完好的重放。那么这段数据波的采样率就是8*1000 = 8000 Samp/Sec。再来看一个概念“位率”,这个很好理解,如果是8位,那么就是用一个char类型的数据来记录每一个采样点,最大振幅当然就是-128 – 127,如果是16位,那就用一个short来记录,泛围是-32768 – 32767,你不要担心用小的位率造成振幅减小,而导至声音减少,因为声卡得到数据后不管是什么类型的数据,它都会映射到一个泛围内,也就是说8位里127的音量和16位里32767的音量是一样的,只不过16位可以表现更多的动态细节。

    现在你是不是会很焦急的发问,说了这么多,那数据倒底是怎么存的呢?别急,我举个例子,比如一个1000Hz的正弦波,我们对它进行数字采样,采样率是8000,位率是16Bit(每一个采样点数据都是short型),单声道,那么它的采样点数据因该是0,16384,32768,16384,0,-16384,-32768,-16384、0、16384……这样循环1000次就代表1秒的1000Hz声波。目前的声卡一般都支持16和8位率,44100、22050、11025等采样率,至于为什么采样率不是整数,这是为了有效减少条幅波现象,这个原理有点复杂,如果你有兴趣可以参考一下相关书籍,我在此就不赘述了。下面我写的这个小程序可以生成指定频率、采样率、位率、时间长度的数据声波。

typedef struct tagWAVEINFO
{
 WORD cbSize;
 DWORD dwSamplePerSec;
 double dSeconds;
 WORD wChannels;
 WORD wBitsPerSample;
 DWORD dwHz;
 BYTE byVolume;
 DWORD dwMode;
} WAVEINFO, *PWAVEINFO;

// 用PWAVEINFO 指定你需要的声波的类型,函数将按照你的要求创建数据
// pwf用于传出符合WAVE标准的波型信息,pBufLen传出生成的数据长度
// 函数返回生成的数据的指针
void* CreateWaveData( const PWAVEINFO pwi, LPWAVEFORMATEX pwf, DWORD *pBufLen )
{
 if ( pwi->cbSize != sizeof(WAVEINFO) )
 {
  return NULL;
 }
 double dCurTime;
 int nCurValue;
 double dCircle = 1.0 / (double)pwi->dwHz;  // 周期时间
 double dSecPerSample = 1.0 / (double)pwi->dwSamplePerSec; // 采样点距
 WORD wBytePerSample = pwi->wBitsPerSample / 8; // 每采样字节数
 DWORD dwSampleCount = (DWORD)ceil( pwi->dSeconds * (double)pwi->dwSamplePerSec ); // 全部采样点数
 *pBufLen = dwSampleCount * wBytePerSample * pwi->wChannels;  // 数据长度
 void *pData = new char[*pBufLen];
 for ( DWORD i = 0; i < dwSampleCount; i++ )
 {
  dCurTime = dSecPerSample * (double)i / dCircle;
  dCurTime = ( dCurTime - (int)dCurTime ) * dCircle;
  nCurValue = (int)(sin( dCurTime * M_PI * 2 / dCircle ) *
   pow( 2.0, pwi->wBitsPerSample ) / 2.0 * (double)pwi->byVolume / 255.0);
  for ( WORD j = 0; j < pwi->wChannels; j++ )
  {
   for ( WORD k = 0; k < wBytePerSample; k++ )
   {
    ((char*)pData)[ ( i * pwi->wChannels + j ) * wBytePerSample + k ] =
     ( nCurValue << ( ( 3 - k ) * 8) ) >> 24;
   }
  }
 }
 pwf->cbSize = sizeof(WAVEFORMATEX);
 pwf->wFormatTag = WAVE_FORMAT_PCM;
 pwf->nChannels = pwi->wChannels;
 pwf->nSamplesPerSec = pwi->dwSamplePerSec;
 pwf->wBitsPerSample = pwi->wBitsPerSample;
 pwf->nBlockAlign = wBytePerSample * pwi->wChannels;
 pwf->nAvgBytesPerSec = pwi->dwSamplePerSec * pwf->nBlockAlign;
 return pData;
}

    在你确定你足够了解上面所说的电声原理之后,下面我将带你到Win32下的WAVE(PCM)格式中去游一遭。PCM的全称是:pulse code modulation(脉冲编码调制),说白了就是间隔采样的机制。WAVE文件的结构如下:
作用           长度    类型    默认值   注释
文件标识        4     char[4]   “RIFF”    Resource Interchange File Format(资源交换文件格式)WAVE是其中的一种
文件数据长度    4     DWORD  N/A      不包括文件标识和数据长度的裸数据的长度
格式标识        4     char[4]   “WAVE”   以此来判断是否WAVE文件
信息头标识      4     char[4]   “fmt ”     表示下面开始信息头描述
信息头长度      4     DWORD  16或12   表示下面的信息头数据有多长
声道数          2     WORD    1或2    表示单声道或立体声
采样率          4     DWORD  44100等
每秒数据长度    4     DWORD  N/A      位率 / 8 * 采样率 * 声道数
每数据包长度    4     WORD    N/A      位率 / 8 * 声道数
位率            2     WORD    16或8
数据标识        4     char[4]   “data”     表示下面这一块是数据块
数据长度        4     DWORD  N/A      表示数据有多长
数据           N/A    N/A      N/A      数据
其它信息       不需要

上面这些信息是顺序存储的,而且信息头刚好和WAVEFORMATEX结构体对映起来,只是顺序有点乱,可千万不要搞差了。很简单是吧?现在你可以用上面的那个声波生成器生成一段声音数据,然后写一个WAVE文件试一下,看能不能用Winamp放出来。

说了这么多,相信菜鸟也对这些东东有些了解了,那么现在我们回过头去继续编写我们的输出模块。先确定一下我们要编写怎样的一个输出模块?输出到声卡吗?那太难了,让我们先来点简单的测一下,输出一个WAVE文件。文件操作模块我个人认为使用STL库里的fstream是很方便的,所以你得先在MusicPlayer.cpp的顶上定一个文件全局变量来打开你需要写的文件:

ofstream file( “文件名”, ios_base::out | ios_base::binary );

    另外还需要两个全局变量:

DWORD g_dwWrited; // 当前写入的数据的长度
DWORD g_dwBytePerSec; // 每秒字节数

    在调用In_Module的Play函数后,也就是音乐开始播放后,In_Module会调用Out_Module的Open函数,并指定数据信息,所以我们应该在这里写下Wave 文件的头信息,按照上面说的WAVE文件格式,我们这样写Open函数:

int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen )
{
 DWORD dwFlag;
 dwFlag = (DWORD)'RIFF';
 file.write( (char*)&dwFlag, 4 );
 file.write( (char*)&dwFlag, 4 ); // 空出4个字节写数据长度
 dwFlag = (DWORD)'WAVE';
 file.write( (char*)&dwFlag, 4 );
 dwFlag = (DWORD)'fmt ';
 file.write( (char*)&dwFlag, 4 );

 WAVEFORMATEX wf;
 ZeroMemory( &wf, sizeof(wf) );
 wf.cbSize = sizeof(wf);
 wf.nSamplesPerSec = nSampleRate;
 wf.wBitsPerSample = nBitPerSample;
 wf.nChannels = nChannels;
 wf.nBlockAlign = nBitPerSample / 8 * nChannels;
 wf.nAvgBytesPerSec = wf.nBlockAlign * nSampleRate;
 wf.wFormatTag = WAVE_FORMAT_PCM;

 g_dwBytePerSec = wf.nAvgBytesPerSec;

 file.write( (char*)&wf.cbSize, sizeof(wf.cbSize) ); // 写低位
 wf.cbSize = 0;
 file.write( (char*)&wf.cbSize, sizeof(wf.cbSize) ); // 写高位,0
 file.write( (char*)&wf.wFormatTag, sizeof(wf)-sizeof(wf.cbSize) ); //跟据偏移量一次性写入
 dwFlag = (DWORD)'data';
 file.write( (char*)&dwFlag, 4 );
}

    在开始播放之后,In_Module会反复执行下面的过程:调用Out_Module的OutCanWrite函数确定是否可写后就会调用Out_Module的OutWrite函数向其写数据。因为在这里是文件操作,所以我们在OutCanWrite函数使终返回0x1000就行。

int OutCanWrite( void )
{
 return 0x1000;
}

    OutWrite函数当然更简单,只需要简单的把数据写入文件,并把数据长度累加就行了。

int OutWrite( char *pBuf, int nLen )
{
 file.write( pBuf, nLen );
 g_dwWrited += nLen;
}

这样数据就会一段一段的写入创建的WAVE文件,但是仍然存在问题,Out_Module不知道何时结束,而In_Module也不可以在写入结束后便马上终断正在拨放缓冲中的内容的Out_Module。但In_Module在写入结束后会向主窗体发送一个USER+2的消息,表示写入结束,然后主窗体在这里就因该使用计时器一类的机制调用Out_Module的GetOutputTime来获取输出模块的当前播放的时间,所以这个函数因该这样写:

int OutGetOutputTime( void )
{
 return g_dwWrited / g_dwBytePerSec * 1000;
}

    当主窗体发现输出的时间入全部时间长度相等时,则因立即调用Out_Module的OutClose事件。OutClose因该文件中填写两个空缺的数据,一个是文件数据长度,一个是裸数据长度。
void OutClose( void )
{
 DWORD dwSize = (DWORD)file.tellg() - 8;
 file.seekg( 4, ios_base::beg );
 file.write( (char*)&dwSize, sizeof(dwSize) );
 file.seekg( 32, ios_base::cur );
 file.write( (char*)&g_dwWrited, sizeof(g_dwWrited) );
file.close();
}

至此一个WAVE文件就完全写好了。主程序的调用还有一些代码,不过很简单。在应用程序初始化的时候,你需要加载Winamp的in_mp3.dll。

 In_Module *pInModule;
 HMODULE hIn2Mod;
 hIn2Mod = LoadLibrary( "in_mpg123.dll" );
 if ( NULL == hIn2Mod )
 {
  MessageBox( NULL, "未能加载输入模块!", "错误", MB_ICONHAND );
  return FALSE;
 }
 PWINAMPGETIN2MOD pInFunc = (PWINAMPGETIN2MOD)GetProcAddress( hIn2Mod, "winampGetInModule2" );
 pInModule = pInFunc();
// 这样你就或得了来自Winamp的插件in_mp3.dll的In_Module对象。
// 而Out_Module是刚才我们写好的东东,实例化一个就行。
 Out_Module OutModule;
// 下面用我们先前写好的函数进行初始化。
 InitModules( pInModule, &OutModule );

    现在你就可以调用pInModule->Play( “文件名” );来播放——事实上是复制音乐了。结束后你可以用Winamp打开写好的文件试听一下,因该没有什么问题。如果你觉得你看完上面这么长的内容仍然感觉到饥饿的话,下面将是更加令人振奋的主题,让我们用DirectX来写一个基于测试程序的真正的媒体播放器。首先要确定是的,你了解DirectX吗?如果你从来没有写过DirectX的程序,可能下面的内容对你来说就有点难了,至少你要确定你在VC.net下写过COM应用的程序。但是问题又出现了,这么长的代码我该怎么讲呢?干脆就先贴出的原代码,再在后面讲一下难点算啦。


//MusicPlayer.cpp
#include "StdAfx.h"
#include "MusicPlayer.h"

HWND g_hWnd;
HANDLE g_hThread;
IDirectSound8 *g_pDS8; // DirectSount 的指针
IDirectSoundBuffer8 *g_pDSB; // DirectSound缓冲的指针
volatile DWORD dwWritePointer;
bool bStop, bPause;
DWORD dwBytePerSec;
DWORD dwWrapCount;
DWORD dwBufLen = 0x200000; // 缓冲的长度

DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
 DWORD dwStatus, dwCurPlay;
 while( !bStop && g_pDSB )
 {
  Sleep( 50 );
  g_pDSB->GetStatus( &dwStatus );
  if ( bPause || 0 == ( dwStatus & DSBSTATUS_PLAYING ) )
  {
   continue;
  }
  g_pDSB->GetCurrentPosition( &dwCurPlay, NULL );
  double dTemp = ( (double)dwWrapCount * dwBufLen + dwCurPlay ) * 1000.0 / dwBytePerSec;
  PostMessage( g_hWnd, WM_USER + 4, dwWrapCount, (DWORD)dTemp );
 }
 PostMessage( g_hWnd, WM_USER + 4, 0, 0 );
 g_hThread = NULL;
 return 0;
}

// 配置对话框
void OutConfig( HWND hWnd )
{
 MessageBox( hWnd, "Config", "About", MB_OK );
}

// 关于对话框
void OutAbout( HWND hWnd )
{
 MessageBox( hWnd,"About Creamdog Message!", "About", MB_OK );
}

// 初始化
void OutInit()
{
 CoInitialize(NULL);
 DirectSoundCreate8( NULL, &g_pDS8, NULL );
 g_pDS8->SetCooperativeLevel( g_hWnd, DSSCL_PRIORITY );
}

// 打开设备
int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen )
{
 if ( !OutIsPlaying() )
 {
  return -1;
 }

 bStop = false;
 bPause = false;
 dwWritePointer = 0;
 dwWrapCount = 0;

 WAVEFORMATEX wf;
 ZeroMemory( &wf, sizeof(wf) );
 wf.cbSize = sizeof(wf);
 wf.nSamplesPerSec = nSampleRate;
 wf.wBitsPerSample = nBitPerSample;
 wf.nChannels = nChannels;
 wf.nBlockAlign = nBitPerSample / 8 * nChannels;
 wf.nAvgBytesPerSec = wf.nBlockAlign * nSampleRate;
 wf.wFormatTag = WAVE_FORMAT_PCM;
 dwBytePerSec = wf.nAvgBytesPerSec;

 DSBUFFERDESC BufDesc;
 ZeroMemory( &BufDesc, sizeof(BufDesc) );
 BufDesc.dwSize = sizeof(BufDesc);
 BufDesc.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_LOCSOFTWARE | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_STICKYFOCUS;
 BufDesc.lpwfxFormat = &wf;
 BufDesc.dwBufferBytes = dwBufLen;
 BufDesc.guid3DAlgorithm = DS3DALG_DEFAULT;

 HRESULT hr = g_pDS8->CreateSoundBuffer( &BufDesc, (LPDIRECTSOUNDBUFFER*)&g_pDSB, NULL );
 if ( S_OK != hr )
 {
  return -1;
 }

 g_hThread = CreateThread( NULL, 0, ThreadProc, 0, 0, NULL );

 if( FAILED( g_pDSB->Play( 0, 0, DSBPLAY_LOOPING ) ) )
 {
  return -1;
 }
 PostMessage( g_hWnd, WM_USER + 3, (WPARAM)wf.nAvgBytesPerSec, (LPARAM)0 );
 return 0;
}

// 准备数据块写入
int OutWrite( char *pBuf, int nLen )
{
 VOID *pMem1, *pMem2;
 DWORD dwSize1, dwSize2;
 if ( FAILED( g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 ) ) )
 {
  g_pDSB->Restore();
  g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 );
 }
 CopyMemory( pMem1, pBuf, dwSize1 );
 if ( pMem2 )
 {
  CopyMemory( pMem2, pBuf + dwSize1, dwSize2 );
 }
 g_pDSB->Unlock( pMem1, dwSize1, pMem2, dwSize2);
 dwWritePointer += nLen;
 return 0;
}

int OutCanWrite( void )
{
 DWORD dwCurPlay;
 static DWORD dwLastPlay = 0;
 if ( bPause || NULL == g_pDSB )
 {
  return 0;
 }
 g_pDSB->GetCurrentPosition( &dwCurPlay, NULL );
 if ( dwCurPlay < dwLastPlay )
 {
  dwWrapCount++;
 }
 dwLastPlay = dwCurPlay;
 long lPlayPos = (long)dwCurPlay + (long)dwWrapCount * (long)dwBufLen;
 long lWritePos = (long)dwWritePointer;
 if ( lWritePos - lPlayPos > (long)(dwBufLen / 2) )
 {
  return 0;
 }
 return dwBufLen / 2;
}

int OutIsPlaying( void )
{
 DWORD dwStatus = 0;
 if ( g_pDSB )
 {
  g_pDSB->GetStatus( &dwStatus );
 }
 return 0 == ( dwStatus & DSBSTATUS_PLAYING );
}

int OutPause( int nPause )
{
 if ( !g_pDSB )
 {
  return -1;
 }
 DWORD dwStatus;
 g_pDSB->GetStatus( &dwStatus );
 if ( 0 == ( dwStatus & DSBSTATUS_PLAYING ) )
 {
  bPause = false;
  if( FAILED( g_pDSB->Play( 0, 0, DSBPLAY_LOOPING ) ) )
  {
   return -1;
  }
 }
 else
 {
  bPause = true;
  g_pDSB->Stop();
 }

 static int nLastPause = 0;
 int nTemp = nLastPause;
 nLastPause = nPause;
 return nTemp;
}

// 关闭设备
void OutClose( void )
{
 if ( g_hThread )
 {
  bStop = true;
  WaitForSingleObject( g_hThread, INFINITE );
 }
 if ( g_pDSB )
 {
  g_pDSB->Stop();
  g_pDSB->Release();
  g_pDSB = NULL;
 }
}

void OutQuit()
{
 OutClose();
 if ( g_pDS8 )
 {
  g_pDS8->Release();
 }
}

void OutSetVolume( int nVolume )
{
 if ( g_pDSB )
 {
  g_pDSB->SetVolume( ( nVolume * ( DSBVOLUME_MAX - DSBVOLUME_MIN ) ) / 255 + DSBVOLUME_MIN );
 }
}

void OutSetPan( int nPan )
{
}

void OutFlush( int nT )
{
 DWORD dwFlushPos = ( nT / 1000 ) * dwBytePerSec;
 dwWritePointer = dwFlushPos;
 dwWrapCount = dwWritePointer / dwBufLen;
 g_pDSB->SetCurrentPosition( dwWritePointer % dwBufLen );
}

int OutGetWrittenTime( void )
{
 return 0;
}


void InitModules( In_Module *pInModule, Out_Module *pOutModule )
{
 g_hWnd = ::AfxGetMainWnd()->GetSafeHwnd();
 pOutModule->About = OutAbout;
 pOutModule->CanWrite = OutCanWrite;
 pOutModule->Open = OutOpen;
 pOutModule->Close = OutClose;
 pOutModule->Config = OutConfig;
 pOutModule->description = "Creamdog WaveOut v1.0";
 pOutModule->Flush = OutFlush;
 pOutModule->GetOutputTime = OutGetWrittenTime;
 pOutModule->GetWrittenTime = OutGetWrittenTime;
 pOutModule->hDllInstance = ::AfxGetInstanceHandle();
 pOutModule->hMainWindow = g_hWnd;
 pOutModule->id = 32;
 pOutModule->Init = OutInit;
 pOutModule->IsPlaying = OutIsPlaying;
 pOutModule->Pause = OutPause;
 pOutModule->Quit = OutQuit;
 pOutModule->SetPan = OutSetPan;
 pOutModule->SetVolume = OutSetVolume;
 pOutModule->version = 100;
 pOutModule->Write = OutWrite;
 pOutModule->Init();

 pInModule->outMod = pOutModule;
 pInModule->hMainWindow = pOutModule->hMainWindow;
 pInModule->hDllInstance = pOutModule->hDllInstance;
 pInModule->SAVSAInit = SAVSAInit;
 pInModule->SAVSADeInit = SAVSADeInit;
 pInModule->SAAddPCMData = SAAddPCMData;
 pInModule->SAGetMode = SAGetMode;
 pInModule->SAAdd = SAAdd;
 pInModule->VSAAddPCMData = VSAAddPCMData;
 pInModule->VSAGetMode = VSAGetMode;
 pInModule->VSAAdd = VSAAdd;
 pInModule->VSASetInfo = VSASetInfo;
 pInModule->dsp_isactive = dsp_isactive;
 pInModule->dsp_dosamples = dsp_dosamples;
 pInModule->EQSet = EQSet;
 pInModule->SetInfo = SetInfo;
 pInModule->Init();
}


void SAVSAInit(int maxlatency_in_ms, int srate){}
void SAVSADeInit(){}
void SAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int SAGetMode(){return 0;}
void SAAdd(void *data, int timestamp, int csa){}
void VSAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int VSAGetMode(int *specNch, int *waveNch){return 0;}
void VSAAdd(void *data, int timestamp){}
void VSASetInfo(int nch, int srate){}
int dsp_isactive(){return 0;}
int dsp_dosamples(short int *samples, int numsamples, int bps, int nch, int srate){return 0;}
void EQSet(int on, char data[10], int preamp){}
void SetInfo(int bitrate, int srate, int stereo, int synched){}


// EOF


    在初始化函数里,下面这三句是初始化COM和DirectSound8的,我就不多说了。

 CoInitialize(NULL);
 DirectSoundCreate8( NULL, &g_pDS8, NULL );
 g_pDS8->SetCooperativeLevel( g_hWnd, DSSCL_PRIORITY );

    在OutOpen函数里完成的工作是创建一个循环播放的缓存,其中要格外注意的是创建的参数。

 BufDesc.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_LOCSOFTWARE | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_STICKYFOCUS;

    DSBCAPS_CTRLPAN和DSBCAPS_CTRLVOLUME无所谓,指定了的话你就可以通过g_pDSB->SetVolume改变音量。DSBCAPS_LOCSOFTWARE是为更广泛的兼容性,在很多板载声卡和老式的ISA声卡上并不存在很大的硬缓存,所以用这个标记强制缓存创建在内存中。DSBCAPS_GETCURRENTPOSITION2指定后,你可以能过g_pDSB->GetCurrentPos()来得到更精确的数值。DSBCAPS_STICKYFOCUS保证你的应用程序在未激活状态仍然能够放出声来。谨记,这三个标记缺一不可。

g_pDSB->Play( 0, 0, DSBPLAY_LOOPING )

    这一句是让声音开始循环播放。比如你创建的Buffer有0x200000字节,如果指定了DSBPLAY_LOOPING,当播放到0x200000时,又会循环到一开始进行播放。当然,在Out_Module里并不容易得到媒体的长度,所以我们创建一个可以循环播放的缓存,写的速度当然要比播放的速度快,OutWrite函数在前面写,g_pDSB就跟在后面播,为了避免OutWrite写了一圈又赶上g_pDSB的播放,我们必须限定g_pDSB的写入速度,这当然就由OutCanWrite来实现。

 VOID *pMem1, *pMem2;
 DWORD dwSize1, dwSize2;
 if ( FAILED( g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 ) ) )
 {
  g_pDSB->Restore();
  g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 );
 }
 CopyMemory( pMem1, pBuf, dwSize1 );
 if ( pMem2 )
 {
  CopyMemory( pMem2, pBuf + dwSize1, dwSize2 );
 }
 g_pDSB->Unlock( pMem1, dwSize1, pMem2, dwSize2);
 dwWritePointer += nLen;
 return 0;

上面OutWrite函数中的代码很清楚的表示出写入缓冲区的方法和播放一样是循环的,比如你的缓冲区有0x30000字节,而你当然写入的位置是第0x20000字节,这时你锁定了0x20000字节长度的内容,那么剩下的0x10000字节将从缓冲区的头部通过pMem2返回出来。dwWritePointer记录的是所有写入的字节数,为了不越界的循环写入,我用dwWritePointer % dwBufLen来计算在循环中到底该开始写哪个位置。另外,那个线程是至关重要的,它将通过消息向主窗体发送当前播放的时间和状态。主窗体也只是依靠此消息来实现对Out_Module的管理。
至于其它的暂停、结束后的释放、获取信息等函数的代码实现并不复杂,有兴趣的你看看源代码就因该可以理解了。如果你会写DLL的话,那么你现在就因该已经知道符合Winamp插件接口标准的DLL是怎么编写的了。
至此全文即将结束,你是不是有点郁闷呢?是因为错别字太多?文法太臭?还是跟本就狗屁不通胡说八道?我已经准备好挨批评了,发短消息告诉我吧!

Creamdog于2003年9月10日星期三,七小时未进米食,一气呵成。

声明:
1. 这篇文章所提及Winamp为NullSoft公司Winamp2.X系列产品,该系列产品版权完全由NullSoft公司所有,如果文章任何地方侵犯了您的版权,敬请来函告知。
2. 本文所涉及源代码并未经过测试,本人不保证其正确性和稳定性,如果对您的计算机及数据造成任何破坏性影响,本人概不负责。
3. 如果未经过NullSoft公司的许可,擅自发布使用Winamp专用插件的应用程序,将不受到法律的保护,并可能受到NullSoft公司的指控。
4. 如果文中的讲解或代码有错漏之处,敬请指正!
5. 本文版权归Creamdog所有,并由Creamdog首次将本文发在优快云 C/C++基础类论坛上,如果你想将本文转贴到别的地方,请保留原文的所有格式和字符。
6. 感谢优快云 C/C++基础类论坛众网友长期以来对我工作的支持。



 

 


 

winamp音效插件DSP_XFiSound 内含CFG插件默认配置文件,将DLL和CFG文件同时拷入安装文件/plugin里,即可完美使用。 立即下载

相关推荐

Java Winamp API-开源

该项目是一个Java API,用于控制Winamp的基本功能。 它与JNI库(C语言)一起使用,可与Winamp SDK通信(可从http://www.winamp.com/development/sdk获得)。 使用下面的LInk下载库。

Winamp 插件技术

    我想:在回答“你用什么播放器听 MP3”这个问题时,90%的人都会回答 Winamp!那么你一定用过 Winamp 的插件功能吧,正是多样化的插件使这个“老”播放器不断地焕发青春。不管新推出什么音频格式(MP4,VQF,RM...),只要插件一装就能播放。还有形形色色的可视插件,比如 Giess 等等,将音乐的节奏感表现的可谓淋漓尽致!    既然插件是用程序编写的,那么我们何不来一试身手

Compile failed 2380

宋浩线性代数笔记

Miao-A-SongHao-Linear-Algebra-Notes bilibili 宋浩老师 “惊叹号” 系列 《线性代数》网课 笔记及时间点目录 Github项目地址:https://github.com/cy69855522/Miao-A-SongHao-Linear-Algebra-Notes ???? 前言 我发现吧,线代没记笔记真不行。 浩浩学习,天天向上。数学网课推荐????宋浩老师的免...

Knife丶小刀博客 4万+

Winamp "in_cdda" 模块的调试以及SHELLCODE的编写

作者:ZwelL日期:2004.11.29团队:shadoweyes      上一个星期Brett Moore在BUGTRAG上面公布了WINAMP的溢出漏洞,说在in_cdda模块中存在strack溢出,受影响的系统为5.05和5.06. 后来k-otik又公布了针对该漏洞的messagbox测试,并给出了相关代码。但是在后来的测试中,发现k-otik给出的代码没有试验通过。于是对该漏洞进行了

1833

Unity-Chan Toon Shader卡通渲染学习

2.0 (UTS2)是一款用于图像和视频的卡通着色器,旨在满足从事卡通着色 3DCG 动画的创作者的需求。二、从0-1复刻UTS21.漫反射UTS2的漫反射部分主要有三个层组成,分别是基本颜色、第一阴影色和第二阴影色,分别对应着美术理论中的白、灰、黑。

qq_63133691的博客 2539

【高低温试验】试验报告模板

高低温循环试验,验证被测设备(DUT)在极端高温、低温环境下的性能稳定性、功能可靠性及安全性,确保其符合设计规范和相关标准(如GB/T 2423.1、GB/T 2423.2)。:验证设备在剧烈温度变化下的耐受能力(如-40℃↔+85℃,温变速率≥10℃/min)。低温测试标准:GB/T 2423.2-2008(温度-40℃,保持时间4小时):模拟高温高湿环境(如温度+60℃,湿度95%RH),测试防潮性能。

电路巫师把一切不可能变成不可能 1565

Winamp头上动土

Winamp头上动土 遭遇 近日使用Winamp时发生极度不爽的事情,就是我打CS的时候习惯听着Winamp来打,但我的播放列表又很乱,有些我不想听的歌也在播放列表里面,我又懒得去整理,所以有时正要杀人的时候刚好播到我不喜欢听的歌,导致战斗力大减,被人凌辱,我总不能用Alt+Tab切换出桌面换了歌再进去厮杀吧,所以我只好拿Winamp开刀进行改造。 我想在打着CS的时候可以按键盘的某一个键就可以跳

Lingch's articles 2300

机器学习系列(1)_逻辑回归初步

1、总述 逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。 2、由来     要说逻辑回归,我们得追溯到线性回归,想必大家对线性回归都有一定的了解,即对于多维空间中存在的样本点,我们用特征的线性组合去拟合空间中点的分布和轨迹。如下图所示:     线性回归能对连续值结果进行预测,

寒小阳 13万+

WINAMP 配置文件

                           WINAMP 配置文件很简单,就没有翻译了 ;-)Configuration Files Any skin file is not complete without first finishing up the configuration files. These configuration files control such things

软件开发资料汇总 382

T113-S3-RTL8211网口phy芯片调试

在嵌入式系统开发中,网络连接是至关重要的一部分。T113-S3开发板搭载了RTL8211系列的网口PHY芯片,用于实现以太网连接。在开发过程中,对于网口的稳定性和性能的调试至关重要。本篇博客将介绍如何调试T113-S3开发板中的RTL8211网口PHY芯片,以确保网络连接的正常工作。

flypig has a dream! 4614

172、摄像头振动与异响分析:VCM 共振、OIS 机械噪声的用户体验改善

摘要: 本文深入探讨了手机摄像头模组中VCM(音圈马达)与OIS(光学防抖)系统产生的振动异响问题及其解决方案。通过实际案例,分析了VCM弹簧片共振、OIS悬丝机械噪声以及系统级共振的成因,提出了硬件改进(如弹簧片厚度调整、阻尼材料应用)、驱动优化(PWM频率微调、软启动配置)和算法避让等综合解决策略。特别强调了量产阶段隐蔽性问题的排查技巧,包括扫频测试、机械阻尼处理和用户体验优化方法(如频率掩蔽)。作者建议工程师避免过度依赖仿真数据,重视实物测试,并与声学团队协作,以解决这类"玄学"问题,提升用户感知体验

sundong_81的博客 128

winamp 珍藏_Winamp发生了什么,您现在可以使用吗?

winamp 珍藏() Twenty years ago Winamp was the future. Now it’s a distant memory. What happened? 二十年前,Winamp是未来。 现在是遥远的记忆。 发生了什么? Winamp (Windows Advanced Multimedia Products) came out on April 21, 199...

culingluan4376的博客 524

YOLOv10改进策略【注意力机制篇】| 2024 蒙特卡罗注意力(MCAttn)模块,提高小目标的关注度

本文记录的是基于蒙特卡罗注意力(MCAttn)模块的YOLOv10目标检测改进方法研究。利用提高的跨尺度特征提取能力,使模型能够更好地传递和融合提取的多尺度特征,提高对小目标的关注度。Exploiting Scale-Variant Attention for Segmenting Small Medical Objects使用一种基于随机采样的池化操作来生成尺度无关的注意力图。它从三个不同尺度(3×33×33×3、2×22×22×2和1×11×11×1,即池化张量)中随机选择一个1×11×11×1注意力图

Limiiiing的博客 2186

最好的WINAMP的插件

 迷你歌词(MiniLyrics) 注册版 WINAMP的插件http://www.tjvnet.com/SoftView/SoftView_110.html超好 显示下载 歌词DXF 7.3 汉化注册   WINAMP的插件http://www.tjvnet.com/SoftView/SoftView_854.html超好 增强音质最佳插件 找了好久

indexroot blog 2945

用 ADT 连接 SAP S/4HANA Public Cloud 开发租户的完整落地指南

摘要:在SAP S/4HANA公有云中进行ABAP开发时,必须使用Development Tenant(客户端080)而非Customizing Tenant(客户端100)连接ADT工具。开发用户需具备SAP_BR_DEVELOPER角色且写权限不受限,开发对象应创建在结构包的子包中。连接前需确认正确的ABAP服务URL(通常为Launchpad URL去掉"/ui"),并通过运行控制台程序验证全链路功能。典型开发场景如通过BAdI实现业务校验时,需遵循Clean Core原则在指定包

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 337

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

如何使Winamp看起来像iTunes

Winamp is a terrific, customizable media player that keeps getting better with each new release. If you are tired of the default skins included with it, then join us as we show you how to make Winamp ...

culunxun2863的博客 171

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

使用Windows消息控制Winamp(VB)

第一部分:得到Winamp的窗口Winamp是一个32位的Windows应用程序。也就是说,我们可以使用32位的Windows编程的一些基础技术来实现控制Winamp,即使用Windows的消息系统。在你给Winamp发送消息之前,你必须得到Winamp窗口的句柄。这里有个方法,使用外部应用程序(例如你自己编写的VB应用)就可以达到要求。Public Declare Function Fi

40Star的专栏 1644

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

winamp 珍藏_分配键盘媒体键以在Winamp中工作

winamp 珍藏Have you ever wondered how to make the media keys on your keyboard actually work for Winamp? Reader Shawn was asking me this question a while back, but then he figured it out on his own and s...

culinxia2707的博客 216

Winamp Plugins

in_VID4WA.dll         是Mpg 视频文件 (MPG;MPEG;M2V;AVI;ASF;WMV;DAT) 默认用官方的 In_dshow.dll 插件播放;在安装 In_VID4WA.dll 插件后,改由 In_VID4WA.dll 播放mpg123 解码器 1.18y ot75c SSE  来源:http://www3.cypress.ne.jp/otachan/in_mpg

♂Blue Butterfly - by ynz' network test 优快云 weblog 1317
上一篇: 深入GetMessage和PeekMessage
下一篇: OpenGL极速入门宝典
fireseed
博客等级 码龄25年 89粉丝 41原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值