2004年08月31日

不知道大家用过WSockExpert没有, 它可以用来截获指定进程网络数据的传输.
前面我还以为它是通过实时远程注入DLL来更改IAT. 不过后来发现在程序一运行时,
它就已经将DLL插入所有进程了,这个跟冰哥写的那个模拟SOCKCAP的程序很相似.
似乎是将DLL注入所有进程, 不过再想一下, 如果是这样的话,那么后来启动的程序
应该不会被注入DLL(除非用定时^_^,这样就太麻烦了), 考虑到这些, 我估计它是
用的HOOK,用HOOK的话就有一点方便:不必考虑有没有读写权限的问题. 也就免
了一些麻烦.
    我在BCB环境中用APIHOOK模拟了一个类似的程序,通过HOOK将DLL插入所有
进程,然后截获WINSOCK API.中间遇到了一些问题,参考了冰哥的SOCKCAP和
EYAS大哥的XHOOK. 在XHOOK中将原始DLL做了一个备份, 在执行API时并没有先
将API地址还原,而是直接调用了备份的函数, 这样提高了执行效率.厉害, :-)
以后再改,先放上一个简单的演示,大家可以对它进行修改扩展功能:

DLL代码:

//—————————————————————————
// Mady By ZwelL
// 2004.8
// zwell@sohu.com
//—————————————————————————
#include <Winsock2.h>
#include <stdio.h>

#pragma argsused

//自定义APIHOOK结构
typedef struct
{
    FARPROC funcaddr;
    BYTE    olddata[5];
    BYTE    newdata[5];
}HOOKSTRUCT;

HHOOK       g_hHook;
HINSTANCE   g_hinstDll;
HMODULE     hModule ;
HANDLE      g_hForm;    //接收信息窗口句柄
DWORD       dwIdOld, dwIdNew;

//————————————————————————
// 由于要截获两个库里面的函数,所以每个函数定义了两个HOOK结构
// 在编程过程中因为没有考虑到这个问题,导致很多包没有截获到,
// 后来想到了冰哥在模仿SOCKCAP的程序中每个函数截了两次才明白
// 一个是wsock32.dll, 一个是ws2_32.dll
//————————————————————————
HOOKSTRUCT  recvapi;
HOOKSTRUCT  recvapi1;
HOOKSTRUCT  sendapi;
HOOKSTRUCT  sendapi1;
HOOKSTRUCT  sendtoapi;
HOOKSTRUCT  sendtoapi1;
HOOKSTRUCT  WSASendapi;

void HookOn();
void HookOff();
BOOL Init();
extern “C” __declspec(dllexport) __stdcall
BOOL InstallHook();
extern “C” __declspec(dllexport) __stdcall
BOOL UninstallHook();

BOOL hookapi(char *dllname, char *procname, DWORD myfuncaddr, HOOKSTRUCT *hookfunc);
int WINAPI Myrecv(SOCKET s, char FAR *buf, int len, int flags);
int WINAPI Myrecv1(SOCKET s, char FAR *buf, int len, int flags);
int WINAPI Mysend(SOCKET s, char FAR *buf, int len, int flags);
int WINAPI Mysend1(SOCKET s, char FAR *buf, int len, int flags);
int WINAPI Mysendto(SOCKET s, const char FAR * buf, int len,
    int flags, const struct sockaddr FAR * to, int tolen);
int WINAPI Mysendto1(SOCKET s, const char FAR * buf, int len,
    int flags, const struct sockaddr FAR * to, int tolen);
int WINAPI MyWSASend(
  SOCKET s,
  LPWSABUF lpBuffers,
  DWORD dwBufferCount,
  LPDWORD lpNumberOfBytesSent,
  DWORD dwFlags,
  LPWSAOVERLAPPED lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
void sndmsg(char *buf);

//—————————————————————————
// 入口函数
// 在一载入库时就进行API截获
// 释放时还原
//—————————————————————————
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
    switch (reason)
    {
        case DLL_PROCESS_ATTACH:
            g_hinstDll = hinst;
            g_hForm = FindWindow(NULL, “ZwelL”);
            if(!Init())
            {
                MessageBoxA(NULL,”Init”,”ERROR”,MB_OK);
                return(false);
            }
            break;
        case DLL_THREAD_ATTACH:
            break;
        case DLL_THREAD_DETACH:
            break;
        case DLL_PROCESS_DETACH:
            UninstallHook();
            break;
      }
    return TRUE;
}

//———————————————————————–
BOOL Init()
{
    hookapi(“wsock32.dll”, “recv”, (DWORD)Myrecv, &recvapi);
    hookapi(“ws2_32.dll”, “recv”, (DWORD)Myrecv1, &recvapi1);
    hookapi(“wsock32.dll”, “send”, (DWORD)Mysend, &sendapi);
    hookapi(“ws2_32.dll”, “send”, (DWORD)Mysend1, &sendapi1);
    hookapi(“wsock32.dll”, “sendto”, (DWORD)Mysendto, &sendtoapi);
    hookapi(“ws2_32.dll”, “sendto”, (DWORD)Mysendto1, &sendtoapi1);
    hookapi(“wsock32.dll”, “WSASend”, (DWORD)MyWSASend, &WSASendapi);
    dwIdNew = GetCurrentProcessId(); // 得到所属进程的ID
    dwIdOld = dwIdNew;
    HookOn(); // 开始拦截
    return(true);
}
//—————————————————————————
LRESULT WINAPI Hook(int nCode, WPARAM wParam, LPARAM lParam)
{
    return(CallNextHookEx(g_hHook, nCode, wParam, lParam));
}
//—————————————————————————
extern “C” __declspec(dllexport) __stdcall
BOOL InstallHook()
{
    g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)Hook, g_hinstDll, 0);
    if (!g_hHook)
    {
        MessageBoxA(NULL, “SET ERROR”, “ERROR”, MB_OK);
        return(false);
    }
    return(true);
}
//—————————————————————————
extern “C” __declspec(dllexport) __stdcall
BOOL UninstallHook()
{
    HookOff();
    if(g_hHook == NULL)
        return true;
    return(UnhookWindowsHookEx(g_hHook));
}

//—————————————————————————
// 根据输入结构截获API
//—————————————————————————
BOOL hookapi(char *dllname, char *procname, DWORD myfuncaddr, HOOKSTRUCT *hookfunc)
{
    hModule = LoadLibrary(dllname);
    hookfunc->funcaddr = GetProcAddress(hModule, procname);
    if(hookfunc->funcaddr == NULL)
        return false;

    memcpy(hookfunc->olddata, hookfunc->funcaddr, 6);
    hookfunc->newdata[0] = 0xe9;
    DWORD jmpaddr = myfuncaddr – (DWORD)hookfunc->funcaddr – 5;
    memcpy(&hookfunc->newdata[1], &jmpaddr, 5);
    return true;
}
//—————————————————————————
void HookOnOne(HOOKSTRUCT *hookfunc)
{
    HANDLE hProc;
    dwIdOld = dwIdNew;
    hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, dwIdOld);
    VirtualProtectEx(hProc, hookfunc->funcaddr, 5, PAGE_READWRITE,&dwIdOld);
    WriteProcessMemory(hProc, hookfunc->funcaddr, hookfunc->newdata, 5, 0);
    VirtualProtectEx(hProc, hookfunc->funcaddr, 5, dwIdOld, &dwIdOld);
}
//—————————————————————————
void HookOn()
{
    HookOnOne(&recvapi);
    HookOnOne(&sendapi);
    HookOnOne(&sendtoapi);
    HookOnOne(&recvapi1);
    HookOnOne(&sendapi1);
    HookOnOne(&sendtoapi1);
    HookOnOne(&WSASendapi);
}
//—————————————————————————
void HookOffOne(HOOKSTRUCT *hookfunc)
{
    HANDLE hProc;
    dwIdOld = dwIdNew;
    hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, dwIdOld);
    VirtualProtectEx(hProc, hookfunc->funcaddr,5, PAGE_READWRITE, &dwIdOld);
    WriteProcessMemory(hProc, hookfunc->funcaddr, hookfunc->olddata, 5, 0);
    VirtualProtectEx(hProc, hookfunc->funcaddr, 5, dwIdOld, &dwIdOld);
}

//—————————————————————————
void HookOff()
{
    HookOffOne(&recvapi);
    HookOffOne(&sendapi);
    HookOffOne(&sendtoapi);
    HookOffOne(&recvapi1);
    HookOffOne(&sendapi1);
    HookOffOne(&sendtoapi1);
    HookOffOne(&WSASendapi);
}
//—————————————————————————
int WINAPI Myrecv(SOCKET s, char FAR *buf, int len, int flags)
{
    int nReturn;
    HookOffOne(&recvapi);
    nReturn = recv(s, buf, len, flags);
    HookOnOne(&recvapi);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “recv|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);
}
//—————————————————————————
int WINAPI Myrecv1(SOCKET s, char FAR *buf, int len, int flags)
{
    int nReturn;
    HookOffOne(&recvapi1);
    nReturn = recv(s, buf, len, flags);
    HookOnOne(&recvapi1);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “recv1|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);
}
//—————————————————————————
int WINAPI Mysend(SOCKET s, char FAR *buf, int len, int flags)
{
    int nReturn;
    HookOffOne(&sendapi);
    nReturn = send(s, buf, len, flags);
    HookOnOne(&sendapi);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “send|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);
}
//—————————————————————————
int WINAPI Mysend1(SOCKET s, char FAR *buf, int len, int flags)
{
    int nReturn;
    HookOffOne(&sendapi1);
    nReturn = send(s, buf, len, flags);
    HookOnOne(&sendapi1);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “send1|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);
}
//————————————————————————–
int WINAPI Mysendto(SOCKET s, const char FAR * buf, int len,
    int flags, const struct sockaddr FAR * to, int tolen)
{
    int nReturn;
    HookOffOne(&sendtoapi);
    nReturn = sendto(s, buf, len, flags, to, tolen);
    HookOnOne(&sendtoapi);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “sendto|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);    
}
//————————————————————————–
int WINAPI Mysendto1(SOCKET s, const char FAR * buf, int len,
    int flags, const struct sockaddr FAR * to, int tolen)
{
    int nReturn;
    HookOffOne(&sendtoapi1);
    nReturn = sendto(s, buf, len, flags, to, tolen);
    HookOnOne(&sendtoapi1);

    char *tmpbuf=new char[len+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “sendto1|%d|%d|%s”,
            GetCurrentProcessId(),
            len,
            buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);    
}
//—————————————————————————-
int WINAPI MyWSASend(
  SOCKET s,
  LPWSABUF lpBuffers,
  DWORD dwBufferCount,
  LPDWORD lpNumberOfBytesSent,
  DWORD dwFlags,
  LPWSAOVERLAPPED lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
)
{
    int nReturn;
    HookOffOne(&WSASendapi);
    nReturn = WSASend(s, lpBuffers, dwBufferCount,
                lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine);
    HookOnOne(&WSASendapi);

    char *tmpbuf=new char[*lpNumberOfBytesSent+100];
    memset(tmpbuf, 0, sizeof(tmpbuf));
    sprintf(tmpbuf, “WSASend|%d|%d|%s”,
            GetCurrentProcessId(),
            lpNumberOfBytesSent,
            lpBuffers->buf);
    sndmsg(tmpbuf);
    delete tmpbuf;
    return(nReturn);  
}

//—————————————————————–
// 向窗口发送消息
// 考虑到简单性,用了COPYDATASTRUCT结构
// 用内存映射应该会快一点
//—————————————————————–
void sndmsg(char *buf)
{
    COPYDATASTRUCT cds;
    cds.dwData=sizeof(COPYDATASTRUCT);
    cds.cbData=strlen(buf);
    cds.lpData=buf;
    SendMessage(g_hForm,WM_COPYDATA,(WPARAM)NULL,(LPARAM)&cds);
}

主窗体代码:
//—————————————————————————

#include <vcl.h>
#pragma hdrstop

#include “main_Form.h”
//—————————————————————————
#pragma package(smart_init)
#pragma link “HexEdit”
#pragma resource “*.dfm”
TForm1 *Form1;

HINSTANCE hdll;
BOOL __stdcall (*InstallHook)();
BOOL __stdcall (*UninstallHook)();
//—————————————————————————
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
    Application->OnHint=DisplayHint;
}
//—————————————————————————
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    g_dindex=0;
    
    hdll = LoadLibrary(“dll.dll”);
    if(hdll == NULL)
        MessageBox(NULL, “LoadLibrary”, “Error”, MB_OK|MB_ICONERROR);
    InstallHook = GetProcAddress(hdll, “InstallHook”);
    if(!InstallHook)
    {
        MessageBox(NULL, “InstallHook”, “Error”, MB_OK|MB_ICONERROR);
    }
    UninstallHook = GetProcAddress(hdll, “UninstallHook”);
    if(!UninstallHook)
    {
        MessageBox(NULL, “UninstallHook”, “Error”, MB_OK|MB_ICONERROR);
    }
    InstallHook();

    startBtn->Enabled=false;
    stopBtn->Enabled=true;
}
//—————————————————————————
void __fastcall TForm1::Button2Click(TObject *Sender)
{
    g_dindex=0;
    UninstallHook();
    FreeLibrary(hdll);
    startBtn->Enabled=true;
    stopBtn->Enabled=false;
}
//—————————————————————————
void __fastcall TForm1::OnCopyData(TMessage &Msg)
{
    COPYDATASTRUCT *cds=(COPYDATASTRUCT*)Msg.LParam;
    AnsiString tmpbuf = (char *)cds->lpData;
    TListItem *li=lv->Items->Add();
    li->Caption=g_dindex;
    if(tmpbuf.SubString(1, tmpbuf.Pos(“|”)-1).Pos(“send”)>0)
    {
        li->ImageIndex=1;
    }
    else
    {
        li->ImageIndex=0;
    }
    
    li->SubItems->Add(tmpbuf.SubString(1, tmpbuf.Pos(“|”)-1));
    tmpbuf=tmpbuf.SubString(tmpbuf.Pos(“|”)+1, tmpbuf.Length());
    li->SubItems->Add(tmpbuf.SubString(1, tmpbuf.Pos(“|”)-1));
    tmpbuf=tmpbuf.SubString(tmpbuf.Pos(“|”)+1, tmpbuf.Length());
    li->SubItems->Add(tmpbuf.SubString(1, tmpbuf.Pos(“|”)-1));
    li->SubItems->Add(tmpbuf.SubString(tmpbuf.Pos(“|”)+1, tmpbuf.Length()));
}

void __fastcall TForm1::lvInsert(TObject *Sender, TListItem *Item)
{
    g_dindex++;
    lv->Perform(LVM_SCROLL,0,10);
}
//—————————————————————————

void __fastcall TForm1::lvClick(TObject *Sender)
{
    if(lv->ItemIndex < 0)
        return;
    HexEdit1->LoadFromBuffer(lv->Items->Item[lv->ItemIndex]->SubItems->Strings[3].c_str(),
        lv->Items->Item[lv->ItemIndex]->SubItems->Strings[3].Length());
}
//—————————————————————————

void __fastcall TForm1::SpeedButton3Click(TObject *Sender)
{
    lv->Clear();    
}
//—————————————————————————

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
    if(stopBtn->Enabled)
        Button2Click(Sender);
}
//—————————————————————————

void __fastcall TForm1::lvAdvancedCustomDrawItem(TCustomListView *Sender,
      TListItem *Item, TCustomDrawState State, TCustomDrawStage Stage,
      bool &DefaultDraw)
{
    if(Item->ImageIndex==0)
    {
        lv->Canvas->Brush->Color = 0×00FFF5EC;
    }
}
//—————————————————————————

void __fastcall TForm1::lvKeyUp(TObject *Sender, WORD &Key,
      TShiftState Shift)
{
    if(lv->ItemIndex < 0)
        return;
    HexEdit1->LoadFromBuffer(lv->Items->Item[lv->ItemIndex]->SubItems->Strings[3].c_str(),
        lv->Items->Item[lv->ItemIndex]->SubItems->Strings[3].Length());    
}
//—————————————————————————
void __fastcall TForm1::DisplayHint(TObject *Sender)
{
    StatusBar1->SimpleText=GetLongHint(Application->Hint);
}

程序截图:

程序有什么不足,还请大家一起讨论.

点对点视频会议程序:VideoNet
作者:Nagareshwar Talekar
翻译:POWERCPP

下载源代码

该程序可以用于两个人在LAN/Intranet(或者Internet)上进行视频会议。现在有许多视频会议程序,每个都有各自的性能提升技术。主要的问题是视频会议视频帧的尺寸对于传输来说太大。因此,性能依赖于对帧的编解码。我使用快速h263编码库来达到更好的压缩率提高速度。该程序做些小改动也可以在Internet上使用。

音频的录制与播放

我在以前的语音会议程序中使用了RecordSound和PlaySound类,这里我将提供摘要说明RecordSound和PlaySound类的使用。

// Create and Start Recorder Thread record=new RecordSound(this); record->CreateThread(); // Create and Start Player Thread play=new PlaySound1(this); play->CreateThread(); // Start Recording record->PostThreadMessage(WM_RECORDSOUND_STARTRECORDING,0,0); // Start Playing play->PostThreadMessage(WM_PLAYSOUND_STARTPLAYING,0,0); // During audio recording, data will be available in the OnSoundData // callback function of the RecordSound class. Here, you can place // your code to send the data to remote host... // To play the data received from the remote host play->PostThreadMessage(WM_PLAYSOUND_PLAYBLOCK,size,(LPARAM)data); // Stop Recording record->PostThreadMessage(WM_RECORDSOUND_STOPRECORDING,0,0); // Stop Playing play->PostThreadMessage(WM_PLAYSOUND_STOPPLAYING,0,0); // At last, to Stop the Recording Thread record->PostThreadMessage(WM_RECORDSOUND_ENDTHREAD,0,0); // To stop playing thread... play->PostThreadMessage(WM_PLAYSOUND_ENDTHREAD,0,0); 

视频捕获

使用VFW(Video For Windows)API进行视频捕获,它提供了通过webcam进行视频捕获。 VideoCapture.h 和VideoCapture.cpp包含了处理视频捕获的代码。

如下代码说明了如何使用该类:

// Create instance of Class vidcap=new VideoCapture(); // This is later used to call display function of the main // dialog class when the frame is captured... vidcap->SetDialog(this); // This does lot of work, including connecting to the driver // and setting the desired video format. Returns TRUE if // successfully connected to videocapture device. vidcap->Initialize(); // If successfully connected, you can get the BITMAPINFO // structure associated with the video format. This is later // used to display the captured frame... this->m_bmpinfo=&vidcap->m_bmpinfo; // Now you can start the capture.... vidcap->StartCapture(); // Once capture is started, frames will arrive in the "OnCaptureVideo" // callback function of the VideoCapture class. Here you call the // display function to display the frame. // To stop the capture vidcap->StopCapture(); // If your job is over....just destroy it.. vidcap->Destroy(); 

要使以上代码通过编译,你应该链接适当的库:

#pragma comment(lib,"vfw32") #pragma comment(lib,"winmm") 

显示捕获的视频帧

有许多方法和API可以显示捕获的视频。你可以使用SetDIBitsToDevice()方法直接显示,但给予GDI的函数非常的慢。更好的方法是使用DrawDib API 显示。DrawDib函数为设备无关位图(DIBs)提供了高性能的图形绘制能力。DrawDib函数直接写入视频内存,因此性能更好。

以下代码摘要演示了使用DrawDib API显示视频帧。

// Initialize DIB for drawing... HDRAWDIB hdib=::DrawDibOpen(); // Then call this function with suitable parameters.... ::DrawDibBegin(hdib,...); // Now, if you are ready with the frame data, just invoke this // function to display the frame ::DrawDibDraw(hdib,...); // Finally, termination... ::DrawDibEnd(hdib); ::DrawDibClose(hdib); 

编解码库

编码器:
我使用快速h.263编码库进行编码。该库是使其实时编码更快的 Tmndecoder 修改版。我已经将该库从C转换到C++,这样可以很容易用于任何Windows应用程序。我移除了快速h263编码库中一些不必要的代码与文件,并在.h和.cpp文件中移除了一些定义与申明。
以下是H263编码库的使用方法:

// Initialize the compressor CParam cparams; cparams.format = CPARAM_QCIF; InitH263Encoder(&cparams); //If you need conversion from RGB24 to YUV420, call this InitLookupTable(); // Set up the callback function // OwnWriteFunction is the global function called during // encoding to return the encoded data... WriteByteFunction = OwnWriteFunction; // For compression, data must be in the YUV420 format... // Hence, before compression, invoke this method ConvertRGB2YUV(IMAGE_WIDTH,IMAGE_HEIGHT,data,yuv); // Compress the frame..... cparams.format = CPARAM_QCIF; cparams.inter = CPARAM_INTRA; cparams.Q_intra = 8; cparams.data=yuv; // Data in YUV format... CompressFrame(&cparams, &bits); // You can get the compressed data from the callback function // that you have registerd at the begining... // Finally, terminate the encoder // ExitH263Encoder(); 

解码器:

这是tmndecoder(H.263解码器)的修改版。使用ANSI C编写,我将它转换到C++使其方便在Windows应用程序中使用。我移除了一些用于显示和文件处理的文件,移除了不必要的代码并增加了一些新文件。

原始的库中一些文件不适合于实时的解码。我已经做了修改使其适合实时的解码处理。现在,可以使用该库来解码H263帧,该库非常快,性能不错。

解码的使用方法:

//Initialize the decoder InitH263Decoder(); // Decompress the frame.... // > rgbdata must be large enough to hold the output data... // > decoder produces the image data in YUV420 format. After // decoding, it is converted into RGB24 format... DecompressFrame(data,size,rgbdata,buffersize); // Finaly, terminate the decoder ExitH263Decoder(); 

如何运行程序
拷贝可执行文件到局域网上两台不同的机器中:A和B,运行他们。在机器A(或B)中选择connect菜单条,在弹出的对话框中输入机器B的名字或IP地址然后按connect按钮,在另外一台机器(B)显示出accept/reject对话框,按accept按钮。在机器A将显示一个通知对话框,按OK后开始会议。

That”’’s it….Enjoy……!!!

致谢:

我感谢 Paul Cheffers 提供了他的音频录制播放类。因为有了开源人士奉献的开源库才有你所看到的videonet程序,我感激Tmndecoder的开发者Karl Lillevold和h.263快速编码库的开发者Roalt Aalmoes 免费提供这些开发库。

如果你有任何问题或建议,可以发邮件给我 nsry2002@yahoo.co.in

文本语音转换入门

作者:Suyu

下载源代码

内容简介
    文本语音(Text-to-Speech,以下简称TTS),它的作用就是把通过TTS引擎把文本转化为语音输出。本文不是讲述如何建立自己的TTS引擎,而是简单介绍如何运用Microsoft Speech SDK 建立自己的文本语音转换应用程序。

Microsoft Speech SDK简介
    Microsoft Speech SDK是微软提供的软件开发包,提供的Speech API (SAPI)主要包含两大方面:

  • 1. API for Text-to-Speech
  • 2. API for Speech Recognition

    其中API for Text-to-Speech,就是微软TTS引擎的接口,通过它我们可以很容易地建立功能强大的文本语音程序,金山词霸的单词朗读功能就用到了这写API,而目前几乎所有的文本朗读工具都是用这个SDK开发的。至于API for Speech Recognition就是与TTS相对应的语音识别,语音技术是一种令人振奋的技术,但由于目前语音识别技术准确度和识别速度不太理想,还未达到广泛应用的要求。
    Microsoft Speech SDK可以在微软的网站免费下载,目前的版本是5.1,为了支持中文,还要把附加的语言包(LangPack)一起下载。
    为了在VC中使用这SDK,必需在工程中添加SDK的include和lib目录,为免每个工程都添加目录,最好的办法是在VC的
Option->Directoris立加上SDK的include和lib目录。

一个最简单的例子
    先看一个入门的例子:

#include <sapi.h> #pragma comment(lib,"ole32.lib") //CoInitialize CoCreateInstance需要调用ole32.dll #pragma comment(lib,"sapi.lib") //sapi.lib在SDK的lib目录,必需正确配置 int main(int argc, char* argv[]) { ISpVoice * pVoice = NULL; //COM初始化: if (FAILED(::CoInitialize(NULL))) return FALSE; //获取ISpVoice接口: HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice); if( SUCCEEDED( hr ) ) { hr = pVoice->Speak(L"Hello world", 0, NULL); pVoice->Release(); pVoice = NULL; } //千万不要忘记: ::CoUninitialize(); return TRUE; }

    短短20几行代码就实现了文本语音转换,够神奇吧。SDK提供的SAPI是基于COM封装的,无论你是否熟悉COM,只要按部就班地用CoInitialize(), CoCreateInstance()获取IspVoice接口就够了,需要注意的是初始化COM后,程序结束前一定要用CoUninitialize()释放资源。

IspVoice接口主要函数

  上述程序的流程是获取IspVoice接口,然后用ISpVoice::Speak()把文本输出为语音,可见,程序的核心就是IspVoice接口。除了Speak外IspVoice接口还有许多成员函数,具体用法请参考SDK的文档。下面择要说一下几个主要函数的用法:

HRESULT Speak(const WCHAR *pwcs,DWORD dwFlags,ULONG *pulStreamNumber); 功能:就是speak了 参数: *pwcs 输入的文本字符串,必需为Unicode,如果是ansi字符串必需先转换为Unicode。 dwFlags 用来标志Speak的方式,其中SPF_IS_XML 表示输入文本含有XML标签,这个下文会讲到。 PulStreamNumber 输出,用来获取去当前文本输入的等候播放队列的位置,只有在异步模式才有用。 
HRESULT Pause ( void ); HRESULT Resume ( void ); 功能:一看就知道了。 
HRESULT SetRate(long RateAdjust ); HRESULT GetRate(long *pRateAdjust); 功能:设置/获取播放速度,范围:-10 to 10 
HRESULT SetVolume(USHORT usVolume); HRESULT GetVolume(USHORT *pusVolume); 功能:设置/获取播放音量,范围:0 to 100 
HRESULT SetSyncSpeakTimeout(ULONG msTimeout); HRESULT GetSyncSpeakTimeout(ULONG *pmsTimeout); 功能:设置/获取同步超时时间。由于在同步模式中,电泳Speak后程序就会进入阻塞状态等待Speak返回,为免程序长时间没相应,应该设置超时时间, msTimeout单位为毫秒。 
 HRESULT SetOutput(IUnknown *pUnkOutput,BOOL fAllowFormatChanges); 功能:设置输出,下文会讲到用SetOutput把Speak输出问WAV文件。 

这些函数的返回类型都是HRESULT,如果成功则返回S_OK,错误有各自不同的错误码。

使用XML
    个人认为这个TTS api功能最强大之处在于能够分析XML标签,通过XML标签设置音量、音调、延长、停顿,几乎可以使输出达到自然语音效果。前面已经提过,把Speak参数dwFlags设为SPF_IS_XML,TTS引擎就会分析XML文本,输入文本并不需要严格遵守W3C的标准,只要含有XML标签就行了,下面举个例子:

…… pVoice->Speak(L"<VOICE REQUIRED=''NAME=Microsoft Mary''/>volume<VOLUME LEVEL=''100''>turn up</VOLUME>", SPF_IS_XML, NULL); …… <VOICE REQUIRED=''NAME=Microsoft Mary''/>

标签把声音设为Microsoft Mary,英文版SDK中一共含有3种声音,另外两种是Microsoft Sam和Microsoft Mike。

…… <VOLUME LEVEL=''100''> 

把音量设为100,音量范围是0~100。

另外:标志音调(-10~10):

<PITCH MIDDLE="10">text</PITCH> 

注意:” 号在C/C++中前面要加 \ ,否则会出错。 标志语速(-10~10):

<RATE SPEED="-10">text</RATE>

逐个字母读:

<SPELL>text</SPELL>

强调:

<EMPH>text</EMPH>

停顿200毫秒(最长为65,536毫秒):

<SILENCE MSEC="200" />

控制发音:

<PRON SYM = ''h eh - l ow 1''/>

    这个标签的功能比较强,重点讲一下:所有的语言发音都是由基本的音素组成,拿中文发音来说,拼音是组成发音的最基本的元素,只要知道汉字的拼音,即使不知道怎么写,我们可知道这个字怎么都,对于TTS引擎来说,它不一定认识所有字,但是你把拼音对应的符号(SYM)给它,它就一定能够读出来,而英语发音则可以用音标表示,”h eh – l ow 1”就是hello这个单词对应的语素。至于发音与符号SYM具体对应关系请看SDK文档中的Phoneme Table。
    再另外,数字、日期、时间的读法也有一套规则,SDK中有详细的说明,这里不说了(懒得翻译了),下面随便抛个例子:

<context ID = "date_ ymd">1999.12.21</context> 

会读成

"December twenty first nineteen ninety nine" 

XML标签可以嵌套使用,但是一定要遵守XML标准。XML标签确实好用,效果也不错,但是……缺点:一个字―――”烦”,如果给一大段文字加标签,简直痛不欲生。

把文本语音输出为WAV文件

#include <sapi.h> #include <sphelper.h> #pragma comment(lib,"ole32.lib") #pragma comment(lib,"sapi.lib") int main(int argc, char* argv[]) { ISpVoice * pVoice = NULL; if (FAILED(::CoInitialize(NULL))) return FALSE; HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice); if( SUCCEEDED( hr ) ) { CComPtr<ISpStream> cpWavStream; CComPtr<ISpStreamFormat> cpOldStream; CSpStreamFormat OriginalFmt; pVoice->GetOutputStream( &cpOldStream ); OriginalFmt.AssignFormat(cpOldStream); hr = SPBindToFile( L"D:\\output.wav",SPFM_CREATE_ALWAYS, &cpWavStream,&OriginalFmt.FormatId(), OriginalFmt.WaveFormatExPtr() ); if( SUCCEEDED( hr ) ) { pVoice->SetOutput(cpWavStream,TRUE); WCHAR WTX[] = L"<VOICE REQUIRED=''NAME=Microsoft Mary''/>text to wave"; pVoice->Speak(WTX, SPF_IS_XML, NULL); pVoice->Release(); pVoice = NULL; } } ::CoUninitialize(); return TRUE; }

SPBindToFile把文件绑定到输出流上,而SetOutput把输出设为绑定文件的流上。

最后
    看完本文后,是不是觉得很简单,微软把强大的功能封装的太好了。其实SDK中另外一个API,SR(语音识别)更有趣,有兴趣不妨试试,你会有意外的收获的。

基于API的录音机程序

作者/栾义明

下载源代码

一、数字音频基础知识

  • Fourier级数:

任何周期的波形可以分解成多个正弦波,这些正弦波的频率都是整数倍。级数中其他正线波的频率是基础频率的整数倍。基础频率称为一级谐波。

  • PCM:

pulse code modulation,脉冲编码调制,即对波形按照固定周期频率采样。为了保证采样后数据质量,采样频率必须是样本声音最高频率的两倍,这就是Nyquist频率。
样本大小:采样后用于存储振幅级的位数,实际就是脉冲编码的阶梯数,位数越大表明精度越高,这一点学过数字逻辑电路的应该清楚。

  • 声音强度:

波形振幅的平方。两个声音强度上的差常以分贝(db)为单位来度量,

  • 计算公式如下:

20*log(A1/A2)分贝。A1,A2为两个声音的振幅。如果采样大小为8位,则采样的动态范围为20*log(256)分贝=48db。如果样本大小为16位,则采样动态范围为20*log(65536)大约是96分贝,接近了人听觉极限和痛苦极限,是再线音乐的理想范围。windows同时支持8位和16位的采样大小。

二、相关API函数,结构,消息
对于录音设备来说,windows 提供了一组wave***的函数,比较重要的有以下几个:

  • 打开录音设备函数
MMRESULT waveInOpen( LPHWAVEIN phwi, //输入设备句柄 UINT uDeviceID, //输入设备ID LPWAVEFORMATEX pwfx, //录音格式指针 DWORD dwCallback, //处理MM_WIM_***消息的回调函数或窗口句柄,线程ID DWORD dwCallbackInstance, DWORD fdwOpen //处理消息方式的符号位 );
  • 为录音设备准备缓存函数
MMRESULT waveInPrepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT bwh ); 
  • 给输入设备增加一个缓存
MMRESULT waveInAddBuffer( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh ); 
  • 开始录音
MMRESULT waveInStart( HWAVEIN hwi ); 
  • 清除缓存
MMRESULT waveInUnprepareHeader( HWAVEIN hwi,LPWAVEHDR pwh, UINT cbwh); 
  • 停止录音
MMRESULT waveInReset( HWAVEIN hwi ); 
  • 关闭录音设备
MMRESULT waveInClose( HWAVEIN hwi ); 
  • Wave_audio数据格式
typedef struct { WORD wFormatTag; //数据格式,一般为WAVE_FORMAT_PCM即脉冲编码 WORD nChannels; //声道 DWORD nSamplesPerSec; //采样频率 DWORD nAvgBytesPerSec; //每秒数据量 WORD nBlockAlign; WORD wBitsPerSample;//样本大小 WORD cbSize; } WAVEFORMATEX; 
  • waveform-audio 缓存格式 
typedef struct { LPSTR lpData; //内存指针 DWORD dwBufferLength;//长度 DWORD dwBytesRecorded; //已录音的字节长度 DWORD dwUser; DWORD dwFlags; DWORD dwLoops; //循环次数 struct wavehdr_tag * lpNext; DWORD reserved; } WAVEHDR; 
  • 相关消息 
MM_WIM_OPEN:打开设备时消息,在此期间我们可以进行一些初始化工作 MM_WIM_DATA:当缓存已满或者停止录音时的消息,处理这个消息可以对缓存进行重新分配,实现不限长度录音 MM_WIM_CLOSE:关闭录音设备时的消息。

相对于录音来说,回放就简单的多了,用到的函数主要有以下几个:

  • 打开回放设备 
MMRESULT waveOutOpen( LPHWAVEOUT phwo, UINT uDeviceID, LPWAVEFORMATEX pwfx, DWORD dwCallback, DWORD dwCallbackInstance, DWORD fdwOpen ); 
  • 为回放设备准备内存块 
MMRESULT waveOutPrepareHeader( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
  • 写数据(放音) 
MMRESULT waveOutWrite( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );

相应的也有三个消息,用法跟录音的类似:

三、程序设计

一个录音程序的简单流程:

打开录音设备waveInOpen===>准备wave数据头waveInPrepareHeader===> 准备数据块waveInAddBuffer===>开始录音waveInStart===>停止录音(waveInReset) ===> 关闭录音设备(waveInClose)

当开始录音后当buffer已满时,将收到MM_WIM_DATA消息,处理该消息可以保存已录好数据。

回放程序比这个要简单的多: 

打开回放设备waveOutOpen===>准备wave数据头waveOutPrepareHeader===>写wave数据waveOutWrite===> 停止放音(waveOutRest) ===>关闭回放设备(waveOutClose)

如何处理MM消息:

MSDN告诉我们主要有 CALLBACK_FUNCTION、CALL_BACKTHREAD、CALLBACK_WINDOW 三种方式,常用的是 Thread,window方式。

线程模式
waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,m_ThreadID,NULL,CALLBACK_THREAD),我们可以继承MFC的CwinThread类,只要相应的处理线程消息即可。
MFC线程消息的宏为:

 ON_THREAD_MESSAGE,

可以这样添加消息映射:

 ON_THREAD_MESSAGE(MM_WIM_CLOSE, OnMM_WIM_CLOSE) 

窗口模式
类似于线程模式,参见源程序即可。

实时语音通信的实现

作者:解放军炮兵学院 十四队 孔康

下载源代码
 
引言
  本人虽已学习VC++一年半载,仍觉捉襟见肘,好在有VCKBASE的帮忙,确实学到了不少东西,www.vckbase.com也成了我每次上民网必到之处(阁下有所不知,鄙人接受最为严格的管理,上民网是要申请的)。近日在做一个通信 方面的程序,实时的语音和视频通信当然是大家所喜欢的。本文将向您展示局域网环境下实时语音通信的的一个解决方案(视频这一块正在做,估计很快就能出炉),Winxp环境下测试效果良好,并且具有网络 拥塞处理机制,您不妨一看。
  本文以第26期 栾义明 先生的《基于API的录音机程序》为基础的,在此深表感谢。雷同之处将不再赘述,主要做了以下发展:

  • (1) 利用多线程机制,实现录音、网络传输、放音同时进行。
  • (2) 网络壅塞处理,保证数据不丢失。
  • 例子程序运行画面:

    下面且看我细细道来:

    (一)首先定义了一个声音数据“块”

    struct CAudioData { PBYTE lpdata; //指向语音数据,注意这里内存区域是动态申请释放的 DWORD dwLength;//语音数据长度 } 

    接下来申明两个循环队列和相关指针。

    //InBlocks,OutBlocks非别为两个常数 CAudioData m_AudioDataIn[InBlocks],m_AudioDataOut[OutBlocks]; int nAudioIn, nSend, //录入、发送指针 nAudioOut, nReceive;//接收、播放指针 

    // 对于录音和放音都存在和网络的同步问题,主要靠这些指针进行协调

    讨论:如图所示,几个指针的相互追逐,这种机制在处理网络拥塞上应该有普遍的应用意义
     

       

  • (1)正常网速下:nAudioIn 在 nSend 之前, nReceive 在 nAuioOu t之前,周而复始的走下去。
  • (2)超快网速下:发送端:–>nSend追上nAudioIn–>“空转”(绕了一圈又回来了)–〉
    接收端:因为录、放音的采样频率设置为相等,故不可能出现 nReceive 在n AudioOut 之后,
    即收到的声音文件太多,来不及播放的现象。
  • (3)超慢网速下:(极端情况,网速几乎为0也没关系)
    发送端:nAudioIn 绕一圈反追上 nSend,于是将数据接在当前块的尾部,以待发送
    接收端:nAudioOut 追上 nReceive 后,发现没有数据可播放了,就“空转”。
  • 综合以上情况,相关实现如下:

    (二)声音的录制与播放

    (1)录音处理

    void CRecTestDlg::OnMM_WIM_DATA(UINT wParam,LONG lParam) { int nextBlock = (nAudioIn+1)% InBlocks; if(m_AudioDataIn[nextBlock].dwLength!=0)//下一“块”没发走 { //把PWAVEHDR(即pBUfferi)里的数据接到当前“块”的末尾 m_AudioDataIn[nAudioIn].lpdata = (PBYTE)realloc (m_AudioDataIn[nAudioIn].lpdata , (((PWAVEHDR) lParam)->dwBytesRecorded+m_AudioDataIn[nAudioIn].dwLength)) ; if (m_AudioDataIn[nAudioIn].lpdata == NULL) {//...出错处理 return ; } CopyMemory ((m_AudioDataIn[nAudioIn].lpdata+m_AudioDataIn[nAudioIn].dwLength), ((PWAVEHDR) lParam)->lpData, ((PWAVEHDR) lParam)->dwBytesRecorded) ;//(*destination,*resource,nLen); m_AudioDataIn[nAudioIn].dwLength +=((PWAVEHDR) lParam)->dwBytesRecorded; } else //把PWAVEHDR(即pBUfferi)里的数据拷贝到下一“块”中 { nAudioIn = (nAudioIn+1)% InBlocks; m_AudioDataIn[nAudioIn].lpdata = (PBYTE)realloc (0,((PWAVEHDR) lParam)->dwBytesRecorded); CopyMemory(m_AudioDataIn[nAudioIn].lpdata, ((PWAVEHDR) lParam)->lpData, ((PWAVEHDR) lParam)->dwBytesRecorded) ; m_AudioDataIn[nAudioIn].dwLength =((PWAVEHDR) lParam)->dwBytesRecorded; } // Send out a new buffer waveInAddBuffer (hWaveIn, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ; return ; } 

    (2)放音处理

    void CRecTestDlg::OnMM_WOM_DONE(UINT wParam,LONG lParam) { //释放播放完的缓冲区,并准备新的数据 free(m_AudioDataOut[nAudioOut].lpdata); m_AudioDataOut[nAudioOut].lpdata = reinterpret_cast<PBYTE>(malloc(1)); m_AudioDataOut[nAudioOut].dwLength = 0; nAudioOut= (nAudioOut+1)%OutBlocks; ((PWAVEHDR)lParam)->lpData = (LPTSTR)m_AudioDataOut[nAudioOut].lpdata ; ((PWAVEHDR)lParam)->dwBufferLength = m_AudioDataOut[nAudioOut].dwLength ; waveOutPrepareHeader (hWaveOut,(PWAVEHDR)lParam,sizeof(WAVEHDR)); waveOutWrite(hWaveOut,(PWAVEHDR)lParam,sizeof(WAVEHDR)); return; } 

    (三)套接字发送、接收线程
      其实,经过刚才的讨论,现在这两个线程的运作很简单—只是循环地操作nReceive和nSend指针。首先发送(接收)声音块的长度,然后发送(接收)声音内容。注意:拿CSocket::Send(buffer,count)为例,其返回值(发送出去的字结数)只是1到count之间的某值,所以要添加检测机制,否则将出现错误,这也是socket编程必须注意的。本文是用一个循环,直到发送出去的字节总数等于“块”的长度才发送第二个数据块的信息。
    例外这两个线程稍加改动即可实现多人的语音会议。

    UINT Audio_Listen_Thread(LPVOID lParam) { CRecTestDlg *pdlg = (CRecTestDlg*)lParam; CSocket m_Server; DWORD length; if(!m_Server.Create(4002)) AfxMessageBox("Listen Socket create error"+pdlg->GetError(GetLastError())); if(!m_Server.Listen()) AfxMessageBox("m_server.Listen ERROR"+pdlg->GetError(GetLastError())); CSocket recSo; if(! m_Server.Accept(recSo)) AfxMessageBox("m_server.Accept() error"+pdlg->GetError(GetLastError())); m_Server.Close(); int ret ; while(1) { //开始循环接收声音文件,首先接收文件长度 ret = recSo.Receive(&length,sizeof(DWORD)); if(ret== SOCKET_ERROR ) AfxMessageBox("服务器端接收声音文件长度出错,原因: "+pdlg->GetError(GetLastError())); if(ret!=sizeof(DWORD)) { AfxMessageBox("接收文件头错误,将关闭该线程"); recSo.Close(); return -1; }//接下来开辟length长的内存空间 pdlg->m_AudioDataOut[pdlg->nReceive].lpdata =(PBYTE)realloc (0,length); if (pdlg->m_AudioDataOut[pdlg->nReceive].lpdata == NULL) { AfxMessageBox("erro memory_ReceiveAudio"); recSo.Close(); return -1; } else//内存申请成功,可以进行循环检测接受 { DWORD dwReceived = 0,dwret; while(length>dwReceived) { dwret = recSo.Receive((pdlg->m_AudioDataOut[pdlg->nReceive].lpdata+dwReceived), (length-dwReceived)); dwReceived +=dwret; if(dwReceived ==length) { pdlg->m_AudioDataOut[pdlg->nReceive].dwLength = length; break; } } }//本轮声音文件接收完毕 pdlg->nReceive=(pdlg->nReceive+1)%OutBlocks; } recSo.Close(); return 0; } UINT Audio_Send_Thread(LPVOID lParam) { CRecTestDlg *pdlg = (CRecTestDlg*)lParam; CSocket m_Client; m_Client.Create(); if( m_Client.Connect("127.0.0.1",4002)) { DWORD ret, length; int count=0; while(1)//循环使用指针nSend { length =pdlg->m_AudioDataIn[pdlg->nSend].dwLength; if(length !=0) { //首先发送块的长度 if(((ret = m_Client.Send(&length,sizeof(DWORD))) != sizeof(DWORD))||(ret==SOCKET_ERROR)) { AfxMessageBox("声音文件头传输错误!"+pdlg->GetError(GetLastError())); pdlg->OnOK(); break; }//其次发送块的内容,循环检测是否发送完毕 DWORD dwSent = 0;//已经发送掉的字节数 while(1)//==============================发送声音数据开始 { ret = m_Client.Send((pdlg->m_AudioDataIn[pdlg->nSend].lpdata+dwSent), (length-dwSent)); if(ret==SOCKET_ERROR)//检错 { AfxMessageBox("声音文件传输错误!"+pdlg->GetError(GetLastError())); break; } else //发送未发送完的 { dwSent += ret; if(dwSent ==length)//发送完毕,则释放当前“块” { free(pdlg->m_AudioDataIn[pdlg->nSend].lpdata); pdlg->m_AudioDataIn[pdlg->nSend].dwLength = 0; break; } } } //======================================发送声音数据结束 } pdlg->nSend = (pdlg->nSend +1)% InBlocks; } } else AfxMessageBox("Socket连接失败"+pdlg->GetError(GetLastError())); m_Client.Close(); return 0; } 

    存在的问题

  • (1) 一旦添加声音控制waveSetGetVolume(),耳机就变成单声的,打开系统的音量控制,发现“波形”选项完全不平衡。
  • (2) 声音的录入运用双缓冲技术,使得无懈可击,但是在播放时,采用双缓冲调试时未能取得成功,相反使用单缓冲却基本上能够满足一般的音效。
  • (3) 可能还有尚未暴露的错误,恳请广大朋友不吝赐教。E-mail: candy0624@163.com
  •   Finally,Thank Candy Lee(my special friend) for her help.

    老调重提,利用 SDK 实现迷宫算法

    作者:赖锋

    下载本文示例源代码

    我近来重看了数据结构的书,现在的教材还是使用C/C++的编写的算法,编译还是在console mode进行, 如果能把这些数据结构的算法使用在SDK上,那么就可以开发出 Windows 程序的算法程序提高学习,不用在 单调的console mode 中看着冷冰冰的字符来学习数据结构了,这样学习一方面可以学习调用 Windows API 和 Windows编程,另一方面可以学习数据结构. 希望我这样的学习方法对那些初学 Windows 的朋友有一些帮助.
    这是使用 SDK 开发出来的迷宫程序(F1 键开始).

    迷宫算法还是老路子,回溯法和堆栈实现,我采用的是堆栈实现. 使用双向链表摸拟堆栈,使用一个 ptrFirst 和一个 ptrLast 和作为堆栈的栈底和栈顶指针, 定义一 个堆栈元素结构, 这个结构保存迷宫中的位置.

    typedef struct _tagNode { int nRow; int nColumn; struct _tagNode* next; struct _tagNode* previou; } Node; 

    定义一个标记数组

    BOOL bPass[ Row ][ Column ]; // Row 和 Column 为迷宫大小. 

    迷宫算法的主要伪代码的实现方式.
    A.从开始位置开始,判断小球的各个方向是否可行,若一个方向可行,则向该方向移动.
    前进的位置进栈.
    条件: 前进方向是墙, 则该方向不能向前.
    前进和方向如果是经过的,则该方向不能向前.

     if ( CanMove( gnRow, gnColumn, right ) ) { gnColumn += moveRight; // 前进 bPass[ gnRow ][ gnColumn ] = TRUE; // 标记通过的位置 // gnRow, gnColumn 位置入栈. } else if ( CanMove( gnRow, gnColumn, left ) ) // right 方不通, 向左. { gnColumn += moveLeft; // 前进 bPass[ gnRow ][ gnColumn ] = TRUE; // 标记通过的位置 // gnRow, gnColumn 位置入栈. } else if ( CanMove( gnRow, gnColumn, forward ) ) // left 方不通, 向前. { gnColumn += moveForward; // 前进 bPass[ gnRow ][ gnColumn ] = TRUE; // 标记通过的位置 // gnRow, gnColumn 位置入栈. } else if ( CanMove( gnRow, gnColumn, back ) ) // forward 方不通, 向后. { gnColumn += moveBack; // 前进 bPass[ gnRow ][ gnColumn ] = TRUE; // 标记通过的位置 // gnRow, gnColumn 位置入栈. } 

    B.各个方向不能可行,退回前一个位置,利用退栈操作,回到 A.

     else if ( CanMove( gnRow, gnColumn, back ) ) {} 

    A, B 不断重复, 直到找到出口, 或遍历迷宫(栈空)

     if 为迷宫出口 bSearch = FALSE; if 栈为空 // 没有出口, bSearch = FALSE; 

    从这个算法我们就可以利用 SDK 来实现这个迷宫, 但是有几个问题必须要注意的,第一个, 在纯 C/C++开发中(不调用API) 是,我们的循环是使用 while( 1 ) { …… } 来实现的,但是在 Windows 编程之 中,每个 Windows 程序是消息驱动的( Event driven ), 它本身就是无限循环的,这样一来, 你要改变一 下你的想法, 我们不使用 while( 1 ){ …. } 来实现循环, 要用消息来实现循环, 这个消息是 Windows 程序自已发出的, 我们不用添加自定义的消息. ::SetTimer( …. ); 可以每隔固定时间发出一个 WM_TIMER 的消息, 这样我们就可以利用这个消息来实现循环, 因为这每隔固定时间就有一个消息, 所以我们可以利用这个消息控制小球的速度. 利用一个 flag 来判断循环是否可以结束, 而不使用 break.

    case WM_TIMER: if ( bStart ) Start(); if ( bSearch ) Search(); // bSearch 循环结束标志. 找到出口或栈为空, bSearch = FALSE. return 0; 

    其次第二个注意的问题, 在 SDK 中, 使用的数据都是 static 或 global 的, 所以对全局数据的操作地方(读写操作)最好能够在一个函数内完成, 避免过多的使用修改函数而使变量条理不清, 在源代码中你可以看 到, 在 Search() 中, 只对全局变量 ptrFirst, ptrLast, gnRow, gnColumn 操作, 其它函数不负责数据的 操作.

    第三个问题, 资源分配释放的问题, 对于占用的资源, 在接到 WM_DESTROY 消息进行内存释放.

    剩下的,就只需要学习贴图的技巧就行了, 这些比较简单, 只要看看源代码就可以明白的了, 我只是在这儿 说明怎样造成小球的运动效果, 小球一旦移动时, 把它的移动前位置屏蔽掉, 就这样地重复过程就可以造成 运动效果.

    附带的源代码中有两个贴图的小程序,写得尽量简单,对刚学习 Windows 编程的朋友一定有帮助. 编译时只要在 console mode 输入 nmake 命令即可进行编译, 把所学用到所用,这样的学习数据结构就是一个很有乐趣的过程.

    VC 调用ACM 音频压缩编程接口的方法

    王 琰

    —- 音 频 和 视 频 数 据 是 大 多 数 多 媒 体 应 用 程 序 向 用 户 提 供 信 息 的 主 要 方 式, 这 些 数 据 一 般 具 有 较 高 的 采 样 速 率, 如 果 不 经 过 压 缩 的 话, 保 存 它 们 需 要 消 耗 大 量 的 存 贮 空 间, 在 网 络 上 进 行 传 输 的 效 率 也 很 低, 因 此 音 频 视 频 数 字 压 缩 编 码 在 多 媒 体 技 术 中 占 有 很 重 要 的 地 位。 就 音 频 数 据 而 言, 目 前 常 用 的 压 缩 方 法 有 很 多 种, 不 同 的 方 法 具 有 不 同 的 压 缩 比 和 还 原 音 质, 编 码 的 格 式 和 算 法 也 各 不 相 同, 其 中 某 些 压 缩 算 法 相 当 复 杂, 普 通 程 序 不 可 能 去 实 现 其 编 解 码 算 法。 所 幸 的 是, 与Windows 3.x 相 比,Windows 95/NT 4.0 为 多 媒 体 应 用 程 序 提 供 了 更 强 的 支 持, 引 入 了ACM(Audio Compression Manager, 音 频 压 缩 管 理 器) 和VCM(Video Compression Manager, 视 频 压 缩 管 理 器), 它 们 负 责 管 理 系 统 中 所 有 音 频 和 视 频 编 解 码 器(Coder-Decoder, 简 称CODEC, 是 实 现 音 频 视 频 数 据 编 解 码 的 驱 动 程 序), 应 用 程 序 可 以 通 过ACM 或VCM 提 供 的 编 程 接 口 调 用 这 些 系 统 中 现 成 的 编 解 码 器 来 实 现 音 频 或 视 频 数 据 的 压 缩 和 解 压 缩。95/NT 4.0 系 统 自 带 的 音 频CODECs 支 持 一 些 早 期 的 音 频 数 据 压 缩 标 准, 如ADPCM 等,Internet Explorer 4.0 等 应 用 程 序 包 含 的 音 频CODECs 支 持 一 些 比 较 新 的 压 缩 标 准, 如MPEG Layer 3 等。 在 控 制 面 板 的 多 媒 体 组 件 中 选 择“ 高 级”, 打 开“ 音 频 压 缩 的 编 码 解 码 器”, 就 可 列 出 系 统 中 安 装 的 所 有 音 频CODECs。 本 文 所 要 介 绍 的 就 是ACM 音 频 压 缩 接 口 的 编 程 方 法, 所 用 编 程 工 具 为VC++ 5.0。

    获 取CODECs 的 信 息

    —- ACM 的API 函 数 定 义 在 头 文 件msacm.h 中, 除 此 之 外, 对ACM 编 程 还 必 须 包 含 头 文 件mmsystem.h,mmreg.h, 这 两 个 头 文 件 定 义 了 多 媒 体 编 程 中 最 基 本 的 常 量 和 数 据 结 构。 为 了 避 免 有 些 高 版 本ACM 才 提 供 的 函 数 和 功 能 在 较 低 版 本 的ACM 中 上 不 可 用, 程 序 中 应 调 用acmGetVersion 函 数 查 询 用 户 机 器 中ACM 的 版 本 信 息。

    —- 前 面 提 到, 在 控 制 面 板 中 可 以 查 看 系 统 中CODECs 的 信 息, 而 在 应 用 程 序 中 也 常 常 需 要 知 道 某 种 音 频CODECs 是 否 存 在, 并 获 取 其 编 解 码 参 数 等 信 息, 这 一 点 可 以 通 过 调 用 下 面 两 个 函 数 来 实 现。

    —- MMRESULT mmr=acmMetrics(NULL, ACM_METRIC_COUNT_CODECS, &dwCodecs);

    —- mmr = acmDriverEnum(CodecsEnumProc, 0, 0);

    —- acmMetrics() 函 数 可 以 获 取 许 多ACM 对 象 的 有 用 信 息, 例 如 向 其 中 传 递ACM_METRIC_COUNT_CODECS 可 以 查 询 系 统 中 安 装 的 音 频CODECs 总 数。 函 数acmDriverEnum() 的 功 能 是 枚 举 所 有 的 音 频CODECs, 在acmDriverEnum() 的 参 数 中 指 定 回 调 函 数CodecsEnumProc() 可 以 进 一 步 查 询 每 个CODEC 的 信 息。Windows 编 程 中 经 常 要 用 到 回 调 函 数, 下 面 是 枚 举 音 频CODECs 的 一 个 回 调 函 数 的 示 例。

    BOOL CALLBACK CodecsEnumProc(HACMDRIVERID hadid, DWORD dwInstance, DWORD fdwSupport) { DWORD dwSize = 0; if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CODEC) printf("多格式转换\n"); ACMDRIVERDETAILS add; acmdd.cbStruct = sizeof(acmdd); MMRESULT mmr = acmDriverDetails(hadid, &acmdd, 0); if (mmr) error_msg(mmr); else { printf(" 全称: %s\n", acmdd.szLongName); printf(" 描述: %s\n", acmdd.szFeatures); } HACMDRIVER had = NULL; mmr = acmDriverOpen(&had, hadid, 0); //打开驱动程序 if (mmr) error_msg(mmr); else { mmr = acmMetrics(had, ACM_METRIC_ MAX_SIZE_FORMAT, &dwSize); WAVEFORMATEX* pwf = (WAVEFORMATEX*) malloc(dwSize); memset(pwf, 0, dwSize); pwf->cbSize = LOWORD(dwSize) - sizeof(WAVEFORMATEX); pwf->wFormatTag = WAVE_FORMAT_UNKNOWN; ACMFORMATDETAILS fd; memset(&fd, 0, sizeof(fd)); fd.cbStruct = sizeof(fd); fd.pwfx = pwf; fd.cbwfx = dwSize; fd.dwFormatTag = WAVE_FORMAT_UNKNOWN; mmr = acmFormatEnum(had, &fd, FormatEnumProc, 0, 0); if (mmr) error_msg(mmr); free(pwf); acmDriverClose(had, 0); } return TRUE; } 

    —- CodecsEnumProc() 共 有 三 个 参 数。 第 一 个 参 数 是 驱 动 程 序 的ID 值; 第 二 个 参 数 是 实 例 数 据, 本 文 例 子 中 未 使 用; 第 三 个 参 数 描 述 该 驱 动 程 序 所 支 持 的 功 能, 它 由 一 组 标 识 进 行 或 运 算 构 成, 例 如, 如 果 设 置 了 标 识ACMDRIVERDETAILS_SUPPORTF_CODEC, 则 说 明 该 驱 动 程 序 可 以 将 一 种 编 码 格 式 的 音 频 信 号 转 换 成 另 一 种 编 码 格 式。 通 过acmDriverDetails() 函 数 可 以 获 得 对 该 驱 动 程 序 进 一 步 的 信 息, 如CODEC 的 名 称、 简 单 描 述 等。 以 上 信 息 实 际 上 是 由ACM 收 集, 并 保 存 在ACM 内 部, 所 以 查 询 以 上 信 息 时 并 未 真 正 将 驱 动 程 序 加 载 至 内 存。 而 要 获 得 每 一 种 驱 动 程 序 支 持 的 音 频 格 式 信 息, 则 必 须 将 驱 动 程 序 加 载 至 内 存, 这 是 通 过acmDriverOpen() 完 成 的, 在 退 出CodecsEnumProc() 前, 还 要 用acmDriverClose() 来 关 闭 已 打 开 的 驱 动 程 序。 在 使 用 音 频 格 式 枚 举 函 数 前, 需 要 先 分 配 一 块 缓 冲 区 存 置 格 式 信 息, 缓 冲 区 的 大 小 可 通 过 调 用acmMetrics() 查 询ACM_METRIC_MAX_SIZE_FORMAT 获 得, 格 式 信 息 中 的 音 频 格 式 标 识 设 为WAVE_FORMAT_UNKNOWN。 在 音 频 格 式 枚 举 中 同 样 使 用 了 回 调 函 数, 此 回 调 函 数 只 是 列 出 了 该 音 频 格 式 的 名 称 和 标 识 值。

    BOOL CALLBACK FormatEnumProc (HACMDRIVERID hadid, LPACMFORMATDETAILS pafd, DWORD dwInstance, DWORD fdwSupport) { printf("%4.4lXH, %s\n", pafd- >dwFormatTag, pafd- >szFormat); return TRUE; } 

    —- 上 面 介 绍 了 浏 览 系 统 中 所 有 音 频CODECs 及 每 种CODEC 所 支 持 的 音 频 格 式 的 方 法, 某 些 典 型 的 应 用 程 序 可 能 需 要 列 出 系 统 中 所 有 可 以 选 用 的CODECs, 并 由 用 户 来 选 择 使 用 哪 一 种CODEC 进 行 压 缩, 此 时 就 需 要 利 用 上 面 的 编 程 方 法 来 获 取CODECs 的 信 息。

    音 频 数 据 的 压 缩

    —- 下 面 说 明 使 用 某 一CODEC 实 现 音 频 压 缩 的 过 程, 读 者 朋 友 只 需 稍 加 改 动 就 可 编 写 出 相 应 的 解 压 程 序。 假 设 源 信 号 为8K 采 样、16bits PCM 编 码、 单 声 道、 长 度 为1 秒 的 音 频 信 号。 驱 动 程 序 采 用Windows 95 自 带 的TrueSpeech 音 频CODEC, 它 能 实 现 大 约10:1 的 压 缩。 在 此 例 中,TrueSpeech CODEC 支 持 从 源 音 频 格 式 到 目 标 格 式 的 转 换, 而 在 实 际 应 用 中, 可 能 某 种CODEC 不 支 持 直 接 将 源 音 频 格 式 转 换 成 目 标 格 式, 这 时 可 以 采 取 两 步 转 换 法, 即 先 将 源 格 式 转 换 成 一 种 中 间 格 式, 再 将 此 中 间 格 式 转 换 成 目 标 格 式, 因 为 线 性PCM 编 码 最 为 简 单, 且 为 绝 大 多 数CODEC 所 支 持, 所 以 一 般 中 间 格 式 都 选 为 线 性PCM 格 式 的 一 种。

    —- 在 进 行 压 缩 之 前 首 先 需 要 确 定TrueSpeech 驱 动 程 序 的ID 值。 为 此 需 要 用 到acmDriverEnum() 函 数, 对 枚 举 到 的 每 一 个 驱 动 程 序, 由acmDriverEnum() 指 定 的 回 调 函 数 将 检 查 其 支 持 的 所 有 音 频 格 式, 若 其 中 包 括wFormatTag 值 为WAVE_FORMAT_DSPGROUP_TRUESPEECH 的 音 频 格 式, 则 此 驱 动 程 序 就 是 要 寻 找 的TrueSpeech CODEC, 它 所 支 持 的 第 一 种WAVE_FORMAT_DSPGROUP_TRUESPEECH 音 频 格 式 即 为 目 标 音 频 压 缩 格 式。 查 询 所 需 的CODEC 及 其 支 持 的 音 频 格 式 的 方 法 见 前 一 小 节 的 介 绍。

    —- 根 据 查 询 的 结 果, 设hadID 为TrueSpeech CODEC 的ID 值,pwfDrv 为 指 向 目 标WAVEFORMATEX 结 构 的 指 针, 接 下 来 利 用 获 得 的ID 值 打 开 相 应 的 驱 动 程 序。

    HACMDRIVER had = NULL; mmr = acmDriverOpen(&had, hadID, 0); if(mmr) { printf(" 打开驱动程序失败\n"); exit(1); } 

    —- 压 缩 和 解 压 缩 一 样, 都 是 将 音 频 信 号 从 一 种 音 频 格 式 转 换 成 另 一 种 格 式, 要 完 成 这 一 过 程, 首 先 要 打 开 转 换 流。 在 用acmStreamOpen 打 开 转 换 流 时, 我 们 指 定 了ACM_STREAMOPENF_NONREALTIME 标 志, 它 表 示 转 换 无 需 实 时 进 行。 因 为 很 多 压 缩 算 法 的 计 算 量 是 相 当 大 的, 实 时 完 成 几 乎 是 不 可 能 的, 例 如 在 本 例 中, 如 果 不 指 定 此 标 志,TrueSpeech CODEC 就 会 返 回“ 无 法 完 成” 的 错 误。

    HACMSTREAM hstr = NULL; DWORD dwSrcBytes = dwSrcSamples * wfSrc.wBitsPerSample / 8; mmr = acmStreamOpen(&hstr,had, //驱动程序句柄 pwfSrc, //指向源音频格式的指针 pwfDrv, //指向目标音频格式的指针 NULL, //无过滤器 NULL, //无回调函数 0,ACM_STREAMOPENF_NONREALTIME); 

    —- 在 真 正 进 行 转 换 之 前, 还 必 须 准 备 转 换 流 的 信 息 头。 下 面 一 段 代 码 中, 先 利 用 源 数 据 的 大 小 以 及 目 标 格 式 的 平 均 数 据 率 估 算 目 标 数 据 的 缓 存 区 大 小, 然 后 调 用acmStreamPrepareHeader 为 转 换 准 备 信 息 头。

    —- DWORD dwDstBytes=pwfDrv->nAvgBytesPerSec*dwSrcSamples/wfSrc.nSamplesPerSec;

    —- dwDstBytes = dwDstBytes*3/2; // 计 算 压 缩 后 音 频 数 据 大 小, 并 依 此 适 当 增 加 输 出 缓 冲 区 的 大 小。

    BYTE* pDstData = new BYTE [dwDstBytes]; ACMSTREAMHEADER shdr; memset(&strhdr, 0, sizeof(shdr)); shdr.cbStruct = sizeof(shdr); shdr.pbSrc = pSrcData; //源音频数据区 shdr.cbSrcLength = dwSrcBytes; shdr.pbDst = pDstData; //压缩后音频数据缓冲区 shdr.cbDstLength = dwDstBytes; mmr = acmStreamPrepareHeader(hstr, &shdr, 0); 

    —- 语 音 数 据 真 正 的 压 缩 过 程 是 由 函 数acmStreamConvert() 完 成 的。 在 调 用acmStreamConvert() 时 可 以 指 定 回 调 函 数, 以 便 在 转 换 过 程 中 显 示 进 度 信 息 等。 在 本 例 中, 未 指 定 回 调 函 数, 只 是 简 单 地 等 待 压 缩 的 结 束。

    —- mmr = acmStreamConvert(hstr, &shdr, 0);

    —- 数 据 压 缩 完 毕 后, 应 用 程 序 就 可 以 把 缓 冲 区 中 的 数 据 写 入 目 标 文 件 中。

    —- 最 后, 必 须 关 闭 转 换 流 和 驱 动 程 序。

    mmr = acmStreamClose(hstr, 0); mmr = acmDriverClose(had, 0); 

    —- 本 文 介 绍 了 利 用ACM 获 取 音 频CODEC 的 信 息 以 及 实 现 音 频 压 缩 的 一 般 方 法 和 过 程, 对ACM 编 程 感 兴 趣 的 读 者 可 以 进 一 步 参 考VC++ 5 的 联 机 帮 助 中 关 于ACM 的 信 息。

    主流音频压缩格式之比较研究

    嗨!大家好,我是白勺,还记得我吗?就是搞电脑音乐的那个。好久没在《大众软件》上跟大家见面了,最近学业繁忙,为了毕业论文一直在拼命啃些大部头的学术著作,所以这篇文章的标题也带了一些“学术气息”,不知大家习惯否?

      不久前我抽空对当前最流行的四种音频压缩格式——MP3、REAL AUDIO、YAMAHA SOUNDVQ和MS AUDIO 4.0做了一次对比测试。起因主要有两个:一是我最近制作了自己的个人主页(http://whitespoon.163.net),放了一些自己的作品(MIDI)在上面,可是不放还好,一放很多朋友就纷纷来“妹儿”表示抗议,说是MIDI这东西太没谱儿,用什么音源听就是什么样,强烈要求我放些能反映作品原貌的东西(WAVE)上去——且不说上载要花多少时间和金钱,GZNET可是只给我提供了20MB的主页空间呀,这就牵涉到一个压缩的问题了;另一个原因是最近微软大张旗鼓地发布了它的最新音频压缩格式MS-AUDIO 4.0(又称Windows Media Technology 4.0、Advanced Streaming Format等等),号称“以MP3个头的一半提供与之相当质量的音频数据流”,国外很多知名多媒体开发工具的开发商,如CAKEWALK,SONIC FOUNDRY也都纷纷宣布支持不过这个新鲜玩意的性能到底怎么样,我还是准备用自己的耳朵去验证一下。

      测试时间:1999年5月8日测试地点:白勺的数字音频工作室测试平台:

      PⅡ450 128MB RAM IBM 10G硬盘(IBM DATA 351010)PWIN 98CREATIVE SB LIVE!声卡BEHRINGER EURORACK MX 2004调音台。

      ALESIS RA-100基准监听功放。

      ALESIS MONITOR ONE基准监听音箱。

      AKG K141监听耳机。

      数字式秒表一只(现跑到五道口商场购得,价值28元人民币)

      白勺的耳朵(与生俱来,无价)。

      

      出于实际使用的考虑,我将这次测试分为两档进行:一档是立体声高质模式,适用于音质要求相对较高的音乐压缩,以128Kbps MP3,96Kbps SOUNDVQ,96Kbps REAL AUDIO G2和96Kbps MS-AUDIO作为标准格式,分别对它们的压缩时间、压缩比及主观听感做出评测;另一档是单声道低质模式,适用于音质要求相对较低的网上实时收听。在这一档中,我把所有压缩软件都设成了20Kbps以保证在当前最主流的28800bps INTERNET接入速率的情况下可以正常收听。

      有一点要说明的是,所有关于压缩后音质好坏的评论性语词都是我个人的主观看法,仅供参考。但为了保持主观评价的客观性(这句话好象很矛盾呀),我采用了所谓“盲听测试”的方法:即我不看屏幕,由另一个人任意为我播放经过压缩或未经压缩的乐曲,由我作出判断并评价(很专业吧,嘻嘻)。

      供测试用的源文件选择的是我自己为即将发行的国产即时战略游戏《逐鹿中原》制作的一首配乐《浴血》,标准WAV格式,全长3分12秒5、32.3兆。选择这首乐曲的原因是一方面这首乐曲动态较大,很容易暴露出压缩软件的各种缺点;另一方面则是因为我对这首乐曲比较熟悉(还会有人比我更熟悉吗?)。

      高质部分

      首先上场的是大家已经非常熟悉的MP3,关于MP3的介绍相信大家已经听厌了,我也不想再罗嗦。在这里只强调一点:所谓MP3是MPEG-1 LAYER 3的简写,它所使用的技术是在VCD(MPEG-1)的音频压缩技术上发展出的第三代,而不是MPEG-3,大家可一定要搞清楚哦。

      MP3的压缩软件我选择的是.MP3 Producer Pro 2.1,使用1.5优化版的Fraunhofer IIS Mpeg Lyaer3 CODEC(编码解码器)。有一点也要在这里说明,目前有成百上千种的MP3压缩软件,各自使用不同的编码解码方式,虽然压出来的文件都是MP3,也都能正常播放,但是其压缩速度和音质(尤其是音质)可差着十万八千里!.mp3 Producer Pro 2.1是我用过的MP3压缩软件中最好的一个,虽然速度慢了一点,但是音质绝对没得说。在128Kbps高质模式下,.mp3 Producer Pro 2.1压缩《浴血》共耗时3分04秒,压缩后文件大小为2.93MB。音质基本没有走样,只是整个频响范围稍稍变窄,空间感也略差,感觉上好象把我原来加的“大厅”的混响效果变成了“中厅”——不过也还过得去啦!低频的弹性受到了一些影响,略显浑浊,一些长尾音的打击乐器的余音也稍稍变短,不过看在11倍的压缩比上,这一切都是可以接受的。

      SOUNDVQ是日本YAMAHA公司购买NTT公司的技术开发出来的一种音频压缩格式,矛头直指MP3。主要卖点是压缩比比MP3大,而且音质还比MP3好(YAMAHA自己和它的支持者们都是这样说的)。遗憾的是,YAMAHA一直不肯公开SOUNDVQ的技术细节,所以没有什么第三方厂商支持这种技术。

      我怀着极大的兴趣下载了SOUNDVQ ENCODER 2.42 Beta 3版及它的播放器,满心期望着它能够在盲听测试中把我拉下马来,谁知……唉!真是期望越大,失望越大。

      我将SOUNDVQ的压缩模式设为96Kbps(即每通道48Kbps,这是YAMAHA SOUNDVQ ENCODER所提供的最高模式),压缩质量选择了高质,压缩整首《浴血》共耗时5分49秒,这是所有压缩软件中最长的。

      SOUNDVQ的压缩效果可以用一个字来形容,那就是“散”:所有原本颗粒感极强的打击乐器都被SOUNDVQ压“散”了,本来应该是“嗒嗒”的声音变成了近似于“叭叭”的声音;整体的音响效果也变得比较混浑,原本十分鲜明的层次被压成了平板一块;低频在一定程度上保持了原有的厚重,但贝司声部变得比较“楞”,大鼓也缺乏弹性……总之,我的感觉是一首乐曲从CD上被以很高的电平值直接转到了普通卡座上,而没有适当地经过压限器(Compressor/Limiter)的处理,所以很多地方可以感觉得到是具有模拟特点的失真,就是说虽然音频失真了,却没有破——但同样令人非常不爽!

      REAL公司是INTERNET实时数字音频流技术的缔造者,它的REAL AUDIO格式已经成为“在线收听”的实际标准。前不久,REAL公司又推出了新一代的REAL G2,在保持原有高压缩比的情况下进一步改善了音质。

      REAL AUDIO的压缩软件我使用的是REAL PRODUCER PLUS G2,这个软件功能非常强大,除了支持音频视频各种带宽的压缩外,竟然还支持音频视频的直接录制压缩!也就是说只要电脑足够快,就可以一边放着音乐甚至录像一边就能在电脑上直接录成REAL格式,真是酷毙了!!

      REAL PRODUCER PLUS G2在96Kbps带宽下压缩《浴血》用了不到26秒钟,比MP3和SOUNDVQ快了好多倍,压缩后的文件大小是2.25MB。

      REAL G2的压缩效果同样可以用一个字来形容,那就是“比SOUNDVQ稍稍差那么一点点”(这……好象不止一个字吧?),在感觉上很象SOUNDVQ,把很多颗粒性的东西都弄得比较散。被它压缩过的鼓音色仿佛通过了一个特别调制过的噪声门效果器,发出一种近似于“嗤嗤”的声音,很象是电鼓。整首乐曲音响的感觉也不太干净,每种乐器的声音都显得比较干涩且有些浑浊。总而言之,与别的压缩格式相比,REAL的声音一点都不讨我耳朵喜欢。

      MS AUDIO 4.0是微软公司针对REAL AUDIO开发的新一代网上流式数字音频压缩技术,但愿在微软发起的强大攻势之下,REAL不要成为第二个NETSCAPE。

      压缩MS AUDIO的软件我使用的是MS自己的Windows Media Tools 4.0,这个家伙功能也不弱,支持各种压缩格式,还跟REAL PRODUCER一样支持音视频的实时录制压缩。但它留给我最深刻的印象是其令人难以置信的高速度,压缩整首曲子竟只用了15秒!当软件停止工作时我还以为是这个MS的“背她”版软件又把我弄死机了呢!半天才反应过来原来它已经把活干完了。

      MS AUDIO 4.0的效果稍稍优于REAL AUDIO,但显然不及MP3(嘻嘻,微软又说大话了),这种差别很难用语言精确表达。总的来说,MS AUDIO 4.0在每种乐器的清晰度上表现不太好,一些打击乐器也有相当程度上的发“散”的现象,但在整体效果方面,尤其在音乐的气势上,MS AUDIO 4.0表现得相当出色。由于低频的失真相对较小,音乐也没有象REAL AUDIO那样强烈的干涩和混浊感。

      在这一轮测试里,最令我吃惊的是(除了Windows Media Tools那闪电一般的压缩速度外)SOUNDVQ的压缩质量竟然不及MP3。为了公平起见,我也对MP3使用96Kbps高质模式进行了测试,压缩后文件大小为2.19MB。发现虽然其声音品质有比较大的下降,但仍然要优于“体积”基本相同的SOUNDVQ。

      那么,为什么这么多人都说SOUNDVQ好呢?经我分析有三种可能:第一种可能就是我的耳朵有问题……这个原因基本可以排除,不信的话你可以到中国音乐学院教务处去查我的视唱练耳的修毕分数;第二种可能是YAMAHA在骗人,但SOUNDVQ的支持者呢?那么多支持者难道都是“枪手”吗?太不可思议了!第三种可能是YAMAHA和大多数SOUNDVQ的“爱用者”们是将SOUNDVQ与使用别的压缩软件及编码解码器压缩出的MP3相比较,得出的结论自然大不一样了。

      总之,在高质部分的测试中,MP3以其强劲的实力稳踞榜首,SOUNDVQ令人失望地屈居第二,新生力量MS AUDIO 4.0名列第三,而老牌劲旅REAL AUDIO被挤至第四。唉,廉颇老矣,尚能饭否?

      

      低质部分

      低质部分的压缩测试搞得我非常难受,听着我花了那么多心血精心制作的音乐被“压”得“血肉模糊”,呜呜呜……真是让我欲哭无泪!

      与预料中的情况一样,无论经过哪种软件低质压缩后的音乐都与原始效果相去甚远,但有趣的是每种压缩软件都有自己的显著特征,即使在盲听测试中,也可以很容易地分辨出哪种效果是哪个软件压缩的。

      .mp3 Producer Pro 2.1的压缩速度与质量呈明显的反比,使用20Kbps压缩《浴血》只用了26秒,压缩后文件体积为468KB。由于MP3这种压缩算法本来就不是为这种低质音频服务的,其效果可以用四个字来形容(别害怕,这次真的是四个字):一塌糊涂。

      总的来说,所有乐器都失去了色彩,声场被压成了薄薄的一片,高音主奏音色象在尖叫……我只听了一遍就再也没有勇气听下去了。

      相对来说,SOUNDVQ的效果就要好得多。你可以非常明显地感觉到SOUNDVQ对压缩后的音频做了十分有效的听感补偿,这可能也是它将这个文件压缩成469KB耗时达1分45秒的原因。经它压缩后的《浴血》从整体上体现了一个“狠”字,而这正是乐曲本身所要表达的。大鼓有力的敲击甚至比原始效果还要夸张,混浊是不可避免的了,但是乐曲仍保持了其原有的气势,在低频十分饱满的情况下,高频的也相当难得地保持了比较小的失真。SOUNDVQ的效果真的很不错。

      REAL AUDIO则表现得比较忠于原作。这不是说经它压缩后的音质很好,而是说经它压缩后高、中、低各个频段的比例与原始效果很相似。而且难能可贵的是,经过如此程度的压缩,每种乐器也还都保持了相当(当然也是相对)的清晰度,只不过“通通鼓”变得象是用钢刷子打的军鼓。REAL PRODUCER PLUS G2压缩全曲用了12秒91,压缩后文件大小为为501KB。

      MS AUDIO 4.0的“忠实”程度明显不如REAL AUDIO,而且也没有象SOUNDVQ那样做有效的听感补偿,唯一值得骄傲的也就是它的压缩速度:11秒(怎么还比高质压缩慢?!),压缩后的文件大小为497KB。

      在这一轮测试中,MP3被首先踢了出去,SOUNDVQ与REAL AUDIO可以说各有所长、难分轩轾,而MS AUDIO只能以其飞快的速度略“逊”一畴了。

      

      后记

      这次测试做得非常艰苦,光想想要把一首已经听了上百遍的曲子再以不同的方式听上几十遍就够让人头痛的了,再加上笔者才疏学浅,要我用文字把声音这种我也搞不清楚到底算是具体还是抽象的东西描述出来,真是无异于要了我的小命。我再次声明:一切的评论都只是我个人的看法,我也不知道具不具有“普遍性”,如果您有任何意见,欢迎到“白勺的数字音频工作室”(http://whitespoon.163.net)坐坐,我会将一些测试音乐的片段上载上去,咱们大家一起来探讨。

       作者:白勺

    网址:http://winpcap.polito.it/install/default.htm

     

    Windump是Windows环境下一款经典的网络协议分析软件,其Unix版本名称为Tcpdump。它可以捕捉网络上两台电脑之间所有的数据包,供网络管理员/入侵分析员做进一步流量分析和入侵检测。在这种监视状态下,任何两台电脑之间都没有秘密可言,所有的流量、所有的数据都逃不过你的眼睛(当然加密的数据不在讨论范畴之内,而且,对数据包分析的结果依赖于你的TCP/IP知识和经验,不同水平的人得出的结果可能会大相径庭)。如果你做过DEBUG或者反汇编,你会发现二者是那么惊人的相似。在W.Richard Stevens的鼎鼎大作《TCP/IP详解》卷一中,通篇采用Tcpdump捕捉的数据包来向读者讲解TCP/IP;而当年美国最出色的电脑安全专家下村勉在追捕世界头号黑客米特尼克时,也使用了Tcpdump,Tcpdump/Windump的价值由此可见一斑。

    好了,言归正传,我们正式开始介绍Windump。该软件是免费软件,命令行下面使用,需要WinPcap驱动,该驱动可以在http://winpcap.polito.it/install/default.htm下载。因为Windump的下载非常方便,很多站点都有,在这里我就不提供了,请大家去网上搜索一下。

    现在我们打开一个命令提示符,运行windump后出现:

    D:\tools>windump

    windump: listening on \Device\NPF_{3B4C19BE-6A7E-4A20-9518-F7CA659886F3}

    这表示windump正在监听我的网卡,网卡的设备名称是:

    \Device\NPF_{3B4C19BE-6A7E-4A20-9518-F7CA659886F3}

    如果你看见屏幕上显示出这个信息,说明你的winpcap驱动已经正常安装,否则请下载并安装正确的驱动。Windump的参数很多,运行windump -h可以看到:

    Usage: windump [-aAdDeflnNOpqRStuvxX] [-B size] [-c count] [ -C file_size ] [ -F file ] [ -i interface ] [ -r file ] [ -s snaplen ] [ -T type ] [ -w file ] [ -E algo:secret ] [ expression ]

    下面我来结合TCP的三步握手来介绍Windump的使用,请接着往下看:

    D:\tools>windump -n

    windump: listening on \Device\NPF_{3B4C19BE-6A7E-4A20-9518-F7CA659886F3}

    09:32:30.977290 IP 192.168.0.226.3295 > 192.168.0.10.80: S 912144276:912144276(0) win 64240 <mss 1460,nop,nop,sackOK> (DF)//第一行

    09:32:30.978165 IP 192.168.0.10.80 > 192.168.0.226.3295: S 2733950406:2733950406(0) ack 912144277 win 8760 <nop,nop,sackOK,mss 1460> (DF)//第二行

    09:32:30.978191 IP 192.168.0.226.3295 > 192.168.0.10.80: . ack 1 win 64240 (DF)//第三行

    先看第一行。其中09:32:30.977290表示时间;192.168.0.226为源IP地址,端口3295,其实就是我自己的那台电脑;192.168.0.10是目的地址,端口80,我们可以判断这是连接在远程主机的WEB服务上面;S 912144276:912144276(0)表示我的电脑主动发起了一个SYN请求,这是第一步握手,912144276是请求端的初始序列号;win 64240 表示发端通告的窗口大小;mss 1460表示由发端指明的最大报文段长度。这一行所表示的含义是IP地址为192.168.0.226的电脑向IP地址为61.133.136.34的电脑发起一个TCP的连接请求。

    接下来我们看第二行,时间不说了;源IP地址为192.168.0.10,而目的IP地址变为192.168.0.226;后面是S 2733950406:2733950406(0) ack 912144277,这是第二步握手,2733950406是服务器端所给的初始序列号,ack 912144277是确认序号,是对第一行中客户端发起请求的初始序列号加1。该行表示服务器端接受客户端发起的TCP连接请求,并发出自己的初始序列号。

    再看第三行,这是三步握手的最后一步,客户端发送ack 1,表示三步握手已经正常结束,下面就可以传送数据了。

    在这个例子里面,我们使用了-n的参数,表示源地址和目的地址不采用主机名的形式显示而采用IP地址的形式。下面我们再来看看如果三步握手不成功会是怎么样。我先telnet到一台没有开telnet服务的计算机上面:

    C:\Documents and Settings\Administrator>telnet 192.168.0.10

    正在连接到192.168.0.10…不能打开到主机的连接, 在端口 23.

    由于目标机器积极拒绝,无法连接。

    这个时候我们再看windump所抓获的数据包:

    D:\tools>windump -n

    windump: listening on \Device\NPF_{3B4C19BE-6A7E-4A20-9518-F7CA659886F3}

    10:38:22.006930 arp who-has 192.168.0.10 tell 192.168.0.226//第三行

    10:38:22.007150 arp reply 192.168.0.10 is-at 0:60:8:92:e2:d//第四行

    10:38:22.007158 IP 192.168.0.226.3324 > 192.168.0.10.23: S 1898244210:1898244210

    (0) win 64240 <mss 1460,nop,nop,sackOK> (DF)

    //第五行

    10:38:22.007344 IP 192.168.0.10.23 > 192.168.0.226.3324: R 0:0(0) ack 1898244211 win 0

    //第六行

    10:38:22.478431 IP 192.168.0.226.3324 > 192.168.0.10.23: S 1898244210:1898244210(0) win 64240 <mss 1460,nop,nop,sackOK> (DF)

    10:38:22.478654 IP 192.168.0.10.23 > 192.168.0.226.3324: R 0:0(0) ack 1 win 0

    10:38:22.979156 IP 192.168.0.226.3324 > 192.168.0.10.23: S 1898244210:1898244210

    (0) win 64240 <mss 1460,nop,nop,sackOK> (DF)

    10:38:22.979380 IP 192.168.0.10.23 > 192.168.0.226.3324: R 0:0(0) ack 1 win 0

    从第三行中,我们可以看见192.168.0.226因为不知道192.168.0.10的MAC地址,所以首先发送ARP广播包;在第四行中,192.168.0.10回应192.168.0.226的请求,告诉192.168.0.226它的MAC地址是0:60:8:92:e2:d。

    第五行中,192.168.0.226向192.168.0.10发起SYN请求,但在第六行中,我们可以看见,因为目标主机拒绝了这一请求,故发送R 0:0(0)的响应,表示不接受192.168.0.226的请求。在接下来的几行中我们看见192.168.0.226连续向192.168.0.10发送SYN请求,但都被目标主机拒绝。

    好了,写了这么多不知道大家看累了没有,如果累了,说明你还需要了解更多的TCP/IP知识,只有深入了解TCP/IP才有可能成为一个合格的网络管理员。Windump的参数很多,功能也非常强大,以上我所介绍的仅仅是它冰山的一角,希望能起到抛砖引玉的作用,也希望有更多的网络管理员能关注协议分析,只有这样,我们才能在日常的网络管理和应急时期的入侵分析中立于不败之地,为我们的网络安全做出贡献。

    2004年08月30日

    Pro/TOOLKIT: Setting Up Your Environment in VC++ .NET

    By Vojin Jovanovi?, April 30, 2003

    An Application Programmers Interface (API), Pro/TOOLKIT allows Pro/ENGINEER functionality to be augmented and/or customized to meet the specific needs of PTC’s customer base using the “C” programming language. Specifically, Pro/TOOLKIT provides the ability to customize the standard Pro/ENGINEER user interface, automate processes involving repetitive steps, integrate proprietary or other external applications with Pro/ENGINEER, develop customized end-user application for model creation, design rule verification and drawing automation.

    Unfortunately, Pro/TOOLKIT deservingly gained a reputation of being hard to work with. Such a state of affairs is the result of PTC’s current  vision, which dictates where and how this product ought to augment the functionality of Pro/ENGINEER. This vision, as the series of the articles to follow will show, is by no means as “visionary” as toolkit users would hope for; rather, it leaves quite a few features to be desired. In these articles we will explore what it takes to work with this product as well as how one can go beyond what PTC supports today.

    A painful reality

    New users wanting to learn Pro/TOOLKIT programming too often get discouraged in their first days of struggle. One of the problems that a newcomer is faced with is the setting of the proper development environment so that one can work with Pro/TOOLKIT. The problem is so frequent that someone by now should have done something about it (especially PTC). Unfortunately, to my knowledge, as of today, nobody has.

    The development of Pro/TOOLKIT applications does not look so ugly on Unix OS since PTC has adequate support there for toolkit users. But it looks very forbidding on the Windows platform, where PTC’s support is minimal and where the normal way of developing applications is with IDE like Visual Studio. Therefore our focus in this article will be dedicated to those new users who often post pleas for help in toolkit user groups related to latter.

    First, it is worth mentioning, to make it easier for newcomers, that Pro/TOOLKIT consists of a few libraries: prodevelop.lib ,protoolkit.lib, pt_asynchronous.lib and protk_dll.lib as well as numerous include files. The libraries are precompiled by PTC and sold to the user (for about $20K) with a humongous size of over 2000 function calls. This is quite overwhelming for new users and for that reason PTC provides an API browser and a substantial number of examples that one can re/use for his/her own purpose. However, to work with these examples on the Windows platform is not a simple matter. The difficulty is in the form of nmake files, in which projects are offered by PTC. Even though VC++ supports this way of building modules, this is an archaic way (coming from Unix) of developing projects that lacks all of the benefits contained in GUI of Visual Studio.

    We’ll look at how one proceeds in setting up a project in VC++ .NET for running a toolkit application within Pro/ENGINEER as well as connecting to it from outside. Both ways have pros and cons and in the end it depends on what you want to achieve.

    Setting up a DLL project

    We begin with a first project of setting up a Windows DLL application that will get registered with Pro/ENGINEER and will run through a button from Pro/E menus.

    Start by creating a new project in VC++ .NET and choose a Win32 application. Enter a name of the project and choose Application Settings (see the required selection below).


    Click on image to enlarge

    This will give you the empty project into which you can start adding your C/C++ files. Now, the first thing to do, if you already haven’t done so, is to enter your Pro/TOOLKIT include and library directories paths. This is done under Tools/Options menu. Observe the paths below for include files …


    Click on image to enlarge

    and for the library files …


    Click on image to enlarge

    The next thing to deal with is the Project/Properties menu. There are a lot of parameters that Visual Studio allows you to set and that is the single most troublesome source of problems for Pro/TOOLKIT users. Sometimes even more experienced users of Pro/TOOLKIT have problems setting the right switches while trying to create a blank project.

    So here we go.

    Selecting the Project/Properties menu under C/C++ you’ll get the configuration panel (see below). You may accept what is in General by default, while in Optimization just select Disabled (/Od). Now the next selection is Preprocessor as shown below.


    Click on image to enlarge

    Enter the set of defines as follows:

    PRO_MACHINE=29
    PRO_OS=4
    hypot=_hypot
    MSB_LEFT
    far=ptc_far
    huge=p_huge
    near=p_near
    _X86_=1
    _WSTDIO_DEFINED

    You’ll find these defines in PTC’s nmake files and apparently they affect the code generation, so although it’s good to have them the project will build even without having them.

    The next is the most important selection related to Code Generation. There you need to make a choice regarding the run time libraries. When it comes to DLL’s, PTC provides a couple of libraries for linking which are single/multi-threaded; however, when you want to make an exe application you can only make a single-threaded (ML switch, but we’ll get to that later). Here, just choose as shown below.


    Click on image to enlarge

    Other default switches in C/C++ configuration section you may inspect, but they are not as relevant, so you are done here. Now, we go to the Linker section (see below). Under General just choose the name of your output file and then proceed to the Input section. This is the most important section. Here you’ll enter the libraries that you want your object files to be linked to. Enter as shown below and you are done.


    Click on image to enlarge

    Note that a standard libc.lib library is ignored. Often you’ll see that when you want to build Toolkit projects, errors such as “already defined …. in libc.lib” or something similar will show up. Now this is a sign that some of the pre-compiled libraries contain symbols that are already defined in Toolkit libraries. In such cases you’ll need to ignore some of the standard libraries as was done above. This is a very annoying part of building Pro/TOOLKIT projects, and it should have been handled better by PTC.

    That’s it. You should be able to be build your project now without any problems.

    Setting up an asynchronous application

    Now we come to building executable applications that can be used to connect to Pro/ENGINEER and do the job for us externally. This is the road out of the pre-designed development box that PTC would like to keep us in, but luckily they leave an exit door for the bravest, so we need to set up a template project for this kind of business too.

    The configuration parameters are almost the same as above except in a couple of places.

    First, start a new Win32 application and select it to be a console empty project as shown below.


    Click on image to enlarge

    Now, add your C/C++ files as usual and proceed to the Project/Properties panel and consider the following.

    The three main libraries supplied by PTC that you will use are pre-compiled as single-threaded. This is really a serious limitation for the user because you can’t make multi-threaded stand-alone Pro/TOOLKIT applications that can use other multi-threaded libraries (which is the standard today). Therefore you have no choice but to use the C/C++/Code Generation Single-threaded option as we did above. This is something to keep in mind when you experience a lot of errors in linking.

    The last thing is to set the toolkit libraries in the Input section of the Linker section of Additional Dependencies (see below). Note again that libc.lib is ignored.


    Click on image to enlarge

    All other switches in C/C++ and the linker sections are similarly set as in the DLL setup above.

    Finally, you can build this project and get an exe file that should run without problems.

    Additional help

    For those of you who are still having difficulty, consider downloading the two zipped projects below.

    The first zip file is a Visual Studio DLL project which is the install test provided by PTC. Extract the files into a directory of your choice (use folder names in the zip archive). Start Visual Studio and open the project. The project assumes that you installed Pro/E 2001 into its default location together with Pro/TOOLKIT, so that C project files will be picked up from that location (of course I can’t distribute PTC’s C files). If you didn’t install Pro/E into its default location you will see an error, and you’ll have to add to Source Files these four files: TestError.c TestInstall.c TestRunmode.c and UtilString.c yourself. After building the dll simply register it as an Auxiliary Application in Pro/E via install.dat provided, but don’t forget to adjust the path location in install.dat to reflect where the project resides (change PATH_TO_YOUR_LOCATION_OF in install.dat to wherever you unzipped the project). After registering it, to run the applications select the new button -Install Test from the File menu in Pro/E.

    The second zip file is my own application example that is a stand-alone executable. Besides providing you with a template exe project it also serves to teach you how to set up a notification business. After building it, just start Pro/E and start the built executable from the Debug directory. The executable will connect to Pro/E and react to your regeneration of models in the session. Study the code to learn more.

    This brings us to the end of setting up your environment in VC++ .NET. I hope this explanation makes it easier for new users to start working with Pro/TOOLKIT and relieves some of the frustrations most people initially experience with this product.

    About the Author

    Vojin Jovanovi? is an independent consultant with 5 years of experience developing Pro/TOOLKIT applications in the area of design optimization and automation as well as over 10 years of experience in developing scientific numerical applications. He has published in international journals and presented at various conferences. His area of expertise is in computational geometry related to MCAD and in development of financial applications. You can reach him at fractal97@hotmail.com and visit his website at: <http://www.purplerose.biz/Vin>.

    Related Articles

    Pro/SHEETMETAL Functionality Using Pro/E Wildfire 2.0 - Lamit
    Surface Replacement Using Pro/ENGINEER Wildfire 2.0 - Lamit
    Direct Modeling with Pro/ENGINEER Wildfire 2.0 - Lamit
    Pro/TOOLKIT: 1st Steps Are Hardest - Jovanovic
    Pro/TOOLKIT: Environments in VC++ .NET - Jovanovic