视频参考:
https://www.bilibili.com/video/BV1ouUPYAErK/
理解下面的代码
关于虚函数
代码分解
-
结构体
foo
的定义:struct foo {int32 X;int64 Y;virtual void Bar(int c); };
foo
结构体有两个成员变量:X
(int32
类型)和Y
(int64
类型)。foo
还定义了一个虚拟函数Bar(int c)
。在 C++ 中,虚拟函数会在类的对象中创建虚拟函数表(vtable)的一部分。
-
虚拟函数
Bar
的定义:void foo::Bar(int c){c += this->X; }
- 这是
foo
结构体中的虚拟函数Bar
的实现。在函数中,c
变量的值会增加this->X
的值。this
指针指向当前对象,因此this->X
访问的是当前对象的成员变量X
。
- 这是
-
创建
foo
类型的对象并调用Bar
函数:Foo foo; foo.Bar(5);
- 创建一个
foo
类型的对象foo
。 - 调用
foo.Bar(5)
,这将触发虚函数机制。虚函数Bar
会通过foo
对象的虚拟函数表来调用。
- 创建一个
虚函数表(vtable)机制
-
虚函数表(vtable):每个包含虚拟函数的类在运行时都会创建一个虚函数表,虚函数表的每个条目对应类中定义的虚拟函数。类的每个对象会持有一个指向虚函数表的指针(通常被称为
vptr
)。 -
vtable 在上面代码中的作用:当你调用
foo.Bar(5)
时,C++ 会查找foo
对象的虚拟函数表(vtable)。因为Bar
是一个虚函数,所以它会根据foo
对象的vptr
查找Bar
的地址,并执行相应的函数体。这里,Bar
通过 vtable 被动态查找并执行。
vtable 查找过程
在内存中,编译器会为包含虚拟函数的类分配一个虚函数表(vtable)。每个类的对象会有一个指向其虚函数表的指针 vptr
,指向这个类的虚拟函数表。虚函数表的内容是类中每个虚拟函数的地址。
-
创建对象时,编译器为
foo
类型的对象foo
插入一个指针vptr
,该指针指向foo
类型的 vtable。 -
调用
foo.Bar(5)
时,编译器使用对象foo
的vptr
查找Bar
函数的地址,并执行Bar
函数。
this->X
和 vtable 结合
this->X
是通过this
指针访问当前对象的成员变量X
。当foo.Bar(5)
被调用时,this
指针指向的是当前foo
类型的对象,因此this->X
访问的是foo
对象中的成员X
。
总结
vtable
是 C++ 用于支持多态的机制。每个包含虚拟函数的类都有一个虚拟函数表,表中存储了虚拟函数的地址。foo
对象有一个指向 vtable 的指针,这个指针使得虚函数能够被动态查找和调用。- 当调用
foo.Bar(5)
时,C++ 会通过foo
对象的 vtable 查找并调用Bar
函数。
写入声音数据通常是将数据放入 GlobalSecondaryBuffer 中
生成方波数据
要用到的变量
锁定缓冲区
以下是带有注释的 Lock
函数声明和解释:
STDMETHOD(Lock) (THIS_ DWORD dwOffset, // 缓冲区偏移量,指定从缓冲区起始位置偏移多少字节开始锁定区域DWORD dwBytes, // 锁定的字节数,指定要锁定多少字节的数据_Outptr_result_bytebuffer_(*pdwAudioBytes1) LPVOID *ppvAudioPtr1, // 输出参数,返回锁定区域的内存指针,应用程序可以访问此内存区进行读取或写入_Out_ LPDWORD pdwAudioBytes1, // 输出参数,返回第一个锁定区域实际锁定的字节数_Outptr_opt_result_bytebuffer_(*pdwAudioBytes2) LPVOID *ppvAudioPtr2, // 可选的输出参数,返回第二个锁定区域的内存指针(用于双缓冲或环形缓冲区)_Out_opt_ LPDWORD pdwAudioBytes2, // 可选的输出参数,返回第二个锁定区域实际锁定的字节数DWORD dwFlags // 标志参数,用于指定锁定缓冲区时的行为,如锁定写光标位置、读取光标位置或环形缓冲区等
) PURE; // 纯虚函数,需要在实际的派生类中实现
在音频缓冲区中,DirectSound 使用一种“环形缓冲区”机制来允许应用程序连续不断地填充音频数据,避免播放的中断。Region1
和 Region2
是 DirectSound 锁定(Lock
)时返回的两个数据区域,用于在不同情形下灵活填充音频数据。
环形缓冲区与 Region1 和 Region2 的含义
在 DirectSound 中,环形缓冲区是一个逻辑上连续但物理上循环的缓冲区。我们可以把它想象成一个圆形的数据结构,数据在头尾连接起来,因此称为环形。
当需要写入的区域未跨越缓冲区尾部
-
Region1 和 Region2 的理解:
- 当
BytesToLock
和BytesToWrite
(需要写入的数据量)都在缓冲区的中间位置,并未跨越缓冲区尾部时,DirectSound 的Lock
操作仅返回一个写入区域Region1
,并且Region2
的大小为零。 - 此时,
Region1
包含所有需要写入的数据的空间。
这种情况相当于只需要在缓冲区中间写入,不需要“环回”。
- 当
-
代码示例:
- 当需要写入的数据量没有跨越缓冲区的尾部时,
Lock
会将Region1
设置为包含全部写入数据的区域。
- 当需要写入的数据量没有跨越缓冲区的尾部时,
当需要写入的区域跨越缓冲区尾部
-
Region1 和 Region2 的使用:
- 当
BytesToLock
和BytesToWrite
跨越缓冲区尾部时,DirectSound 将返回两个区域:Region1
和Region2
。 Region1
从BytesToLock
开始,到缓冲区末尾结束。Region2
则是缓冲区的开头到所需数据写入完毕的区域,连接着Region1
的结束位置,形成环形的写入。
- 当
-
代码意义:
- 通过这种方式,
Region1
和Region2
实际上可以有效地填充到缓冲区的尾部并从头开始填充,确保数据的无缝循环。 - 这在实时音频流播放中十分重要,因为它可以有效避免音频播放的中断,实现无缝的声音输出。
- 通过这种方式,
小结
Region1
和Region2
的分区方式帮助代码处理缓冲区的“尾部到开头”的情况,使得数据填充可以跨越缓冲区的边界,保证连续不断的音频输出。- 这样做确保了缓冲区中的音频数据在循环播放时的流畅性,避免音频在缓冲区边界处产生中断和延迟。
像返回的Region1 和 Region2 中写入方波数据
播放音频
Play
方法用于启动音频缓冲区的播放。它是 DirectSound 接口中的一个方法,允许开发者控制音频缓冲区的播放行为。
STDMETHOD(Play) (THIS_ DWORD dwReserved1, DWORD dwPriority, DWORD dwFlags) PURE;
参数解释
-
dwReserved1
:保留参数,通常设置为0
,因为 DirectSound 对该参数未作具体定义。 -
dwPriority
:缓冲区播放的优先级。此参数仅在使用声音管理的环境中有意义。若不使用声音管理,一般也设置为0
。 -
dwFlags
:用于控制播放模式的标志。常用标志有:DSBPLAY_LOOPING
:让缓冲区中的音频数据循环播放,即当音频播放到结尾时会自动返回开头重新播放,适合背景音乐或持续播放的音效。DSBPLAY_TERMINATEBY_PRIORITY
:此标志用于声音管理模式下,通过优先级中止其他音频缓冲区的播放。未使用声音管理时通常不设置。
使用场景
调用 Play
方法后,缓冲区开始播放指定的音频数据,应用场景包括:
- 音效播放:用于播放短暂的音效,比如按键声或提示音,通常是非循环播放。
- 背景音乐:通过
DSBPLAY_LOOPING
标志,让背景音乐循环播放。
代码示例
假设我们要在一个音频缓冲区中播放背景音乐,可以按如下设置:
GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
- 解释:
0
表示dwReserved1
和dwPriority
不使用特定设置。DSBPLAY_LOOPING
让音频循环播放。
修改bug
// game.cpp : Defines the entry point for the application.
//#include <cstdint>
#include <dsound.h>
#include <minwindef.h>
#include <processenv.h>
#include <stdint.h>
#include <windows.h>
#include <winerror.h>
#include <xinput.h>#define internal static // 用于定义内翻译单元内部函数
#define local_persist static // 局部静态变量
#define global_variable static // 全局变量typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef uint64_t uint64;typedef int8_t int8;
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;
typedef int32 bool32;struct win32_offscreen_buffer {BITMAPINFO Info;void *Memory;// 后备缓冲区的宽度和高度int Width;int Height;int Pitch;int BytesPerPixel;
};
// 添加这个去掉重复的冗余代码
struct win32_window_dimension {int Width;int Height;
};// TODO: 全局变量
// 用于控制程序运行的全局布尔变量,通常用于循环条件
global_variable bool GloblaRunning;
// 用于存储屏幕缓冲区的全局变量
global_variable win32_offscreen_buffer GlobalBackbuffer;
global_variable LPDIRECTSOUNDBUFFER GlobalSecondaryBuffer;/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
#define X_INPUT_GET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, \XINPUT_STATE *pState) // 定义一个宏,将指定名称设置为// XInputGetState 函数的类型定义/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration // 要发送到控制器的震动信息*/
#define X_INPUT_SET_STATE(name) \DWORD WINAPI name( \DWORD dwUserIndex, \XINPUT_VIBRATION *pVibration) // 定义一个宏,将指定名称设置为// XInputSetState 函数的类型定义typedef X_INPUT_GET_STATE(x_input_get_state); // 定义了 x_input_get_state 类型,为 `XInputGetState`// 函数的类型
typedef X_INPUT_SET_STATE(x_input_set_state); // 定义了 x_input_set_state 类型,为 `XInputSetState`// 函数的类型// 定义一个 XInputGetState 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_GET_STATE(XInputGetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 定义一个 XInputSetState 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_SET_STATE(XInputSetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 设置全局变量 XInputGetState_ 和 XInputSetState_ 的初始值为打桩函数
global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;// 定义宏将 XInputGetState 和 XInputSetState 重新指向 XInputGetState_ 和
// XInputSetState_
#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_// 加载 XInput DLL 并获取函数地址
internal void Win32LoadXInput(void) { //HMODULE XInputLibrary = LoadLibrary("xinput1_4.dll");if (!XInputLibrary) {// 如果无法加载 xinput1_4.dll,则回退到 xinput1_3.dllXInputLibrary = LoadLibrary("xinput1_3.dll");} else {// TODO:Diagnostic}if (XInputLibrary) { // 检查库是否加载成功XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState"); // 获取 XInputGetState 函数地址if (!XInputGetState) { // 如果获取失败,使用打桩函数XInputGetState = XInputGetStateStub;}XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState"); // 获取 XInputSetState 函数地址if (!XInputSetState) { // 如果获取失败,使用打桩函数XInputSetState = XInputSetStateStub;}} else {// TODO:Diagnostic}
}#define DIRECT_SOUND_CREATE(name) \HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS, \LPUNKNOWN pUnkOuter);
// 定义一个宏,用于声明 DirectSound 创建函数的原型typedef DIRECT_SOUND_CREATE(direct_sound_create);
// 定义一个类型别名 direct_sound_create,代表
// DirectSound 创建函数internal void Win32InitDSound(HWND window, int32 SamplesPerSecond,int32 BufferSize) {// 注意: 加载 dsound.dll 动态链接库HMODULE DSoundLibrary = LoadLibraryA("dsound.dll");if (DSoundLibrary) {// 注意: 获取 DirectSound 创建函数的地址// 通过 GetProcAddress 函数查找 "DirectSoundCreate" 函数在 dsound.dll// 中的地址,并将其转换为 direct_sound_create 类型的函数指针direct_sound_create *DirectSoundCreate =(direct_sound_create *)GetProcAddress(DSoundLibrary,"DirectSoundCreate");// 定义一个指向 IDirectSound 接口的指针,并初始化为 NULLIDirectSound *DirectSound = NULL;if (DirectSoundCreate && SUCCEEDED(DirectSoundCreate(0,// 传入 0 作为设备 GUID,表示使用默认音频设备&DirectSound,// 将创建的 DirectSound 对象的指针存储到// DirectSound 变量中0// 传入 0 作为外部未知接口指针,通常为 NULL))) //{// clang-format offWAVEFORMATEX WaveFormat = {};WaveFormat.wFormatTag = WAVE_FORMAT_PCM; // 设置格式标签为 WAVE_FORMAT_PCM,表示使用未压缩的 PCM 格式WaveFormat.nChannels = 2; // 设置声道数为 2,表示立体声(两个声道:左声道和右声道)WaveFormat.nSamplesPerSec = SamplesPerSecond; // 采样率 表示每秒钟的样本数,常见值为 44100 或 48000 等WaveFormat.wBitsPerSample = 16; // 16位音频 设置每个样本的位深为 16 位WaveFormat.nBlockAlign = (WaveFormat.nChannels * WaveFormat.wBitsPerSample) / 8;// 计算数据块对齐大小,公式为:nBlockAlign = nChannels * (wBitsPerSample / 8)// 这里除以 8 是因为每个样本的大小是按字节来计算的,nChannels 是声道数// wBitsPerSample 是每个样本的位数,除以 8 转换为字节WaveFormat.nAvgBytesPerSec = WaveFormat.nSamplesPerSec * WaveFormat.nBlockAlign;// 计算每秒的平均字节数,公式为:nAvgBytesPerSec = nSamplesPerSec * nBlockAlign// 这表示每秒音频数据流的字节数,它帮助估算缓冲区大小// clang-format on// 函数用于设置 DirectSound 的协作等级if (SUCCEEDED(DirectSound->SetCooperativeLevel(window, DSSCL_PRIORITY))) {// 注意: 创建一个主缓冲区// 使用 DirectSoundCreate 函数创建一个 DirectSound// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_PRIMARYBUFFER,指定我们要创建的是主缓冲区,而不是次缓冲区。BufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;LPDIRECTSOUNDBUFFER PrimaryBuffer = NULL;if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, // 指向缓冲区描述结构体的指针&PrimaryBuffer, // 指向创建的缓冲区对象的指针NULL // 外部未知接口,通常传入 NULL))) {if (SUCCEEDED(PrimaryBuffer->SetFormat(&WaveFormat))) {// NOTE:we have finally set the formatOutputDebugString("SetFormat 成功");} else {// NOTE:OutputDebugString("SetFormat 失败");}} else {}} else {}// 注意: 创建第二个缓冲区// 创建次缓冲区来承载音频数据,并在播放时使用// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_GETCURRENTPOSITION2 |// DSBCAPS_GLOBALFOCUS两个标志会使次缓冲区在播放时更加精确,同时在应用失去焦点时保持音频输出BufferDescription.dwFlags =DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;BufferDescription.dwBufferBytes = BufferSize; // 缓冲区大小BufferDescription.lpwfxFormat = &WaveFormat; // 指向音频格式的指针if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, // 指向缓冲区描述结构体的指针&GlobalSecondaryBuffer, // 指向创建的缓冲区对象的指针NULL // 外部未知接口,通常传入 NULL))) {OutputDebugString("SetFormat 成功");} else {OutputDebugString("SetFormat 失败");}// 注意: 开始播放!// 调用相应的 DirectSound API 开始播放音频} else {}} else {}
}internal win32_window_dimension Win32GetWindowDimension(HWND Window) {win32_window_dimension Result;RECT ClientRect;GetClientRect(Window, &ClientRect);// 计算绘制区域的宽度和高度Result.Height = ClientRect.bottom - ClientRect.top;Result.Width = ClientRect.right - ClientRect.left;return Result;
}// 渲染一个奇异的渐变图案
internal void RenderWeirdGradient(win32_offscreen_buffer Buffer, int BlueOffset,int GreenOffset) {// TODO:让我们看看优化器是怎么做的uint8 *Row = (uint8 *)Buffer.Memory; // 指向位图数据的起始位置for (int Y = 0; Y < Buffer.Height; ++Y) { // 遍历每一行uint32 *Pixel = (uint32 *)Row; // 指向每一行的起始像素for (int X = 0; X < Buffer.Width; ++X) { // 遍历每一列uint8 Blue = (X + BlueOffset); // 计算蓝色分量uint8 Green = (Y + GreenOffset); // 计算绿色分量*Pixel++ = ((Green << 8) | Blue); // 设置当前像素的颜色}Row += Buffer.Pitch; // 移动到下一行}
}// 这个函数用于重新调整 DIB(设备独立位图)大小
internal void Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int width,int height) {// device independent bitmap(设备独立位图)// TODO: 进一步优化代码的健壮性// 可能的改进:先不释放,先尝试其他方法,再如果失败再释放。if (Buffer->Memory) {VirtualFree(Buffer->Memory, // 指定要释放的内存块起始地址0, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0MEM_RELEASE); // MEM_RELEASE:释放整个内存块,将内存和地址空间都归还给操作系统}// 赋值后备缓冲的宽度和高度Buffer->Width = width;Buffer->Height = height;Buffer->BytesPerPixel = 4;// 设置位图信息头(BITMAPINFOHEADER)Buffer->Info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 位图头大小Buffer->Info.bmiHeader.biWidth = Buffer->Width; // 设置位图的宽度Buffer->Info.bmiHeader.biHeight =-Buffer->Height; // 设置位图的高度(负号表示自上而下的方向)Buffer->Info.bmiHeader.biPlanes = 1; // 设置颜色平面数,通常为 1Buffer->Info.bmiHeader.biBitCount =32; // 每像素的位数,这里为 32 位(即 RGBA)Buffer->Info.bmiHeader.biCompression =BI_RGB; // 无压缩,直接使用 RGB 颜色模式// 创建 DIBSection(设备独立位图)并返回句柄// TODO:我们可以自己分配?int BitmapMemorySize =(Buffer->Width * Buffer->Height) * Buffer->BytesPerPixel;Buffer->Memory = VirtualAlloc(0, // lpAddress:指定内存块的起始地址。// 通常设为 NULL,由系统自动选择一个合适的地址。BitmapMemorySize, // 要分配的内存大小,单位是字节。MEM_COMMIT, // 分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。PAGE_READWRITE // 内存可读写);Buffer->Pitch = width * Buffer->BytesPerPixel; // 每一行的字节数// TODO:可能会把它清除成黑色
}// 这个函数用于将 DIBSection 绘制到窗口设备上下文
internal void Win32DisplayBufferInWindow(HDC DeviceContext, int WindowWidth,int WindowHeight,win32_offscreen_buffer Buffer, int X,int Y, int Width, int Height) {// 使用 StretchDIBits 将 DIBSection 绘制到设备上下文中StretchDIBits(DeviceContext, // 目标设备上下文(窗口或屏幕的设备上下文)/*X, Y, Width, Height, // 目标区域的 x, y 坐标及宽高X, Y, Width, Height,*/0, 0, WindowWidth, WindowHeight, //0, 0, Buffer.Width, Buffer.Height, //// 源区域的 x, y 坐标及宽高(此处源区域与目标区域相同)Buffer.Memory, // 位图内存指针,指向 DIBSection 数据&Buffer.Info, // 位图信息,包含位图的大小、颜色等信息DIB_RGB_COLORS, // 颜色类型,使用 RGB 颜色SRCCOPY); // 使用 SRCCOPY 操作符进行拷贝(即源图像直接拷贝到目标区域)
}LRESULT CALLBACK
Win32MainWindowCallback(HWND hwnd, // 窗口句柄,表示消息来源的窗口UINT Message, // 消息标识符,表示当前接收到的消息类型WPARAM wParam, // 与消息相关的附加信息,取决于消息类型LPARAM LParam) { // 与消息相关的附加信息,取决于消息类型LRESULT Result = 0; // 定义一个变量来存储消息处理的结果switch (Message) { // 根据消息类型进行不同的处理case WM_CREATE: {OutputDebugStringA("WM_CREATE\n");};case WM_SIZE: { // 窗口大小发生变化时的消息} break;case WM_DESTROY: { // 窗口销毁时的消息// TODO: 处理错误,用重建窗口GloblaRunning = false;} break;case WM_SYSKEYDOWN: // 系统按键按下消息,例如 Alt 键组合。case WM_SYSKEYUP: // 系统按键释放消息。case WM_KEYDOWN: // 普通按键按下消息。case WM_KEYUP: { // 普通按键释放消息。uint64 VKCode = wParam; // `wParam` 包含按键的虚拟键码(Virtual-Key Code)bool WasDown = ((LParam & (1 << 30)) != 0);bool IsDown = ((LParam & (1 << 30)) == 0);bool32 AltKeyWasDown = (LParam & (1 << 29)); // 检查Alt键是否被按下// bool AltKeyWasDown = ((LParam & (1 << 29)) != 0); //// 检查Alt键是否被按下if (IsDown != WasDown) {if (VKCode == 'W') { // 检查是否按下了 'W' 键} else if (VKCode == 'A') {} else if (VKCode == 'S') {} else if (VKCode == 'D') {} else if (VKCode == 'Q') {} else if (VKCode == 'E') {} else if (VKCode == VK_UP) {} else if (VKCode == VK_DOWN) {} else if (VKCode == VK_LEFT) {} else if (VKCode == VK_RIGHT) {} else if (VKCode == VK_ESCAPE) {OutputDebugStringA("ESCAPE: ");if (IsDown) {OutputDebugString(" IsDown ");}if (WasDown) {OutputDebugString(" WasDown ");}} else if (VKCode == VK_SPACE) {}}if ((VKCode == VK_F4) && AltKeyWasDown) {GloblaRunning = false;}} break;case WM_CLOSE: { // 窗口关闭时的消息// TODO: 像用户发送消息进行处理GloblaRunning = false;} break;case WM_ACTIVATEAPP: { // 应用程序激活或失去焦点时的消息OutputDebugStringA("WM_ACTIVATEAPP\n"); // 输出调试信息,表示应用程序激活或失去焦点} break;case WM_PAINT: { // 处理 WM_PAINT 消息,通常在窗口需要重新绘制时触发PAINTSTRUCT Paint; // 定义一个 PAINTSTRUCT 结构体,保存绘制的信息// 调用 BeginPaint 开始绘制,并获取设备上下文 (HDC),同时填充 Paint 结构体HDC DeviceContext = BeginPaint(hwnd, &Paint);// 获取当前绘制区域的左上角坐标int X = Paint.rcPaint.left;int Y = Paint.rcPaint.top;// 计算绘制区域的宽度和高度int Height = Paint.rcPaint.bottom - Paint.rcPaint.top;int Width = Paint.rcPaint.right - Paint.rcPaint.left;win32_window_dimension Dimension = Win32GetWindowDimension(hwnd);Win32DisplayBufferInWindow(DeviceContext, Dimension.Width, Dimension.Height,GlobalBackbuffer, X, Y, Width, Height);// 调用 EndPaint 结束绘制,并释放设备上下文EndPaint(hwnd, &Paint);} break;default: { // 对于不处理的消息,调用默认的窗口过程Result = DefWindowProc(hwnd, Message, wParam, LParam);// 调用默认窗口过程处理消息} break;}return Result; // 返回处理结果
}int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //PSTR cmdline, int cmdshow) {Win32LoadXInput();WNDCLASS WindowClass = {};// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptrWin32ResizeDIBSection(&GlobalBackbuffer, 1280, 720);// WindowClass.style:表示窗口类的样式。通常设置为一些 Windows// 窗口样式标志(例如 CS_HREDRAW, CS_VREDRAW)。WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;// CS_HREDRAW 当窗口的宽度发生变化时,窗口会被重绘。// CS_VREDRAW 当窗口的高度发生变化时,窗口会被重绘// WindowClass.lpfnWndProc:指向窗口过程函数的指针,窗口过程用于处理与窗口相关的消息。WindowClass.lpfnWndProc = Win32MainWindowCallback;// WindowClass.hInstance:指定当前应用程序的实例句柄,Windows// 应用程序必须有一个实例句柄。WindowClass.hInstance = hInst;// WindowClass.lpszClassName:指定窗口类的名称,通常用于创建窗口时注册该类。WindowClass.lpszClassName = "gameWindowClass"; // 类名if (RegisterClass(&WindowClass)) { // 如果窗口类注册成功HWND Window = CreateWindowEx(0, // 创建窗口,使用扩展窗口风格WindowClass.lpszClassName, // 窗口类的名称,指向已注册的窗口类"game", // 窗口标题(窗口的名称)WS_OVERLAPPEDWINDOW |WS_VISIBLE, // 窗口样式:重叠窗口(带有菜单、边框等)并且可见CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(X坐标)CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(Y坐标)CW_USEDEFAULT, // 窗口的初始宽度:使用默认宽度CW_USEDEFAULT, // 窗口的初始高度:使用默认高度0, // 父窗口句柄(此处无父窗口,传0)0, // 菜单句柄(此处没有菜单,传0)hInst, // 当前应用程序的实例句柄0 // 额外的创建参数(此处没有传递额外参数));// 如果窗口创建成功,Window 将保存窗口的句柄if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环// 图像测试int xOffset = 0;int yOffset = 0;// 音频测试uint32 RunningSampleIndex = 0; // 样本索引int SquareWaveCounter = 0; // 方波数int16 ToneVolume = 3000; // 音量int SamplesPerSecond = 48000; // 采样率:每秒采样48000次int ToneHz = 256; // 方波频率:256 Hzint SquareWavePeriod = SamplesPerSecond / ToneHz; // 方波周期(样本数)int HalfSquareWavePeriod = SquareWavePeriod / 2; // 方波半周期(样本数)int BytesPerSample = sizeof(int16) * 2; // 一个样本的大小int SecondaryBufferSize = SamplesPerSecond * BytesPerSample; // 缓冲区大小bool32 SoundIsPlaying = false;Win32InitDSound(Window, SamplesPerSecond, SecondaryBufferSize);GloblaRunning = true;while (GloblaRunning) { // 启动一个无限循环,等待和处理消息MSG Message; // 声明一个 MSG 结构体,用于接收消息while (PeekMessage(&Message,// 指向一个 `MSG` 结构的指针。`PeekMessage`// 将在 `lpMsg` 中填入符合条件的消息内容。0,// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;// 如果设置为特定的窗口句柄,则只检查该窗口的消息。0, //0, // 用于设定消息类型的范围PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。)) {if (Message.message == WM_QUIT) {GloblaRunning = false;}TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息}// TODO: 我们应该频繁的轮询吗for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY;ControllerIndex++) {// 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态XINPUT_STATE ControllerState;// 调用 XInputGetState 获取控制器的状态if (XInputGetState(ControllerIndex, &ControllerState) ==ERROR_SUCCESS) {// 如果获取控制器状态成功,提取 Gamepad 的数据// NOTE:// 获取方向键的按键状态XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);// 获取肩部按钮的按键状态bool LeftShoulder = (Pad->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER);bool RightShoulder =(Pad->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER);// 获取功能按钮的按键状态bool Start = (Pad->wButtons & XINPUT_GAMEPAD_START);bool Back = (Pad->wButtons & XINPUT_GAMEPAD_BACK);bool AButton = (Pad->wButtons & XINPUT_GAMEPAD_A);bool BButton = (Pad->wButtons & XINPUT_GAMEPAD_B);bool XButton = (Pad->wButtons & XINPUT_GAMEPAD_X);bool YButton = (Pad->wButtons & XINPUT_GAMEPAD_Y);// std::cout << "AButton " << AButton << " BButton " << BButton// << " XButton " << XButton << " YButton " << YButton// << std::endl;// 获取摇杆的 X 和 Y 坐标值(-32768 到 32767)int16 StickX = Pad->sThumbLX;int16 StickY = Pad->sThumbLY;if (AButton) {yOffset += 2;}} else {}}DWORD PlayCursor = 0; // 播放游标,指示当前播放位置DWORD WriteCursor = 0; // 写入游标,指示当前写入位置// 获取音频缓冲区的当前播放和写入位置if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor))) {// 计算需要锁定的字节位置,基于当前样本索引和每样本字节数DWORD BytesToLock =RunningSampleIndex * BytesPerSample % SecondaryBufferSize;DWORD BytesToWrite = 0; // 需要写入的字节数// 判断 BytesToLock 与 PlayCursor 的位置关系以确定写入量if (BytesToLock == PlayCursor) {// 如果锁定位置正好等于播放位置,写入整个缓冲区if (!SoundIsPlaying) {BytesToWrite = SecondaryBufferSize;}} else if (BytesToLock > PlayCursor) {// 如果锁定位置在播放位置之后,写入从锁定位置到缓冲区末尾,再加上开头到播放位置的字节数BytesToWrite = (SecondaryBufferSize - BytesToLock) + PlayCursor;} else {// 如果锁定位置在播放位置之前,写入从锁定位置到播放位置之间的字节数BytesToWrite = PlayCursor - BytesToLock;}VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址DWORD Region1Size; // 第一段区域的大小(字节数)VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址DWORD Region2Size; // 第二段区域的大小(字节数)if (SUCCEEDED(GlobalSecondaryBuffer->Lock(BytesToLock, // 缓冲区偏移量,指定开始锁定的字节位置BytesToWrite, // 锁定的字节数,指定要锁定的区域大小&Region1, // 输出,返回锁定区域的内存指针(第一个区域)&Region1Size, // 输出,返回第一个锁定区域的实际字节数&Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)&Region2Size, // 输出,返回第二个锁定区域的实际字节数0 // 标志,控制锁定行为(如从光标位置锁定等)))) {// int16 int16 int16// 左 右 左 右 左 右 左 右 左 右DWORD Region1SampleCount =Region1Size / BytesPerSample; // 计算第一段区域中的样本数量int16 *SampleOut = (int16 *)Region1; // 将第一段区域指针转换为 16// 位整型指针,准备写入样本数据if (Region2Size > 48000 && BytesToLock != PlayCursor) {OutputDebugStringA("test");}// 循环写入样本到第一段区域for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount;++SampleIndex) {// 计算每个样本的值,使用方波产生音频数据// RunningSampleIndex++ / HalfSquareWavePeriod) % 2// 用于生成方波,隔半个周期翻转振幅int16 SampleValue =((RunningSampleIndex++ / HalfSquareWavePeriod) % 2)? ToneVolume // 如果为偶数周期,输出正振幅: -ToneVolume; // 如果为奇数周期,输出负振幅*SampleOut++ = SampleValue; // 左声道*SampleOut++ = SampleValue; // 右声道}DWORD Region2SampleCount =Region2Size / BytesPerSample; // 计算第二段区域中的样本数量SampleOut = (int16 *)Region2; // 将第二段区域指针转换为 16// 位整型指针,准备写入样本数据// 循环写入样本到第二段区域for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount;++SampleIndex) {// 使用相同逻辑生成方波样本数据int16 SampleValue =((RunningSampleIndex++ / HalfSquareWavePeriod) % 2)? ToneVolume // 偶数周期,输出正振幅: -ToneVolume; // 奇数周期,输出负振幅*SampleOut++ = SampleValue; // 左声道*SampleOut++ = SampleValue; // 右声道}// 解锁音频缓冲区,将数据提交给音频设备GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2,Region2Size);}}if (!SoundIsPlaying) {GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);SoundIsPlaying = true;}RenderWeirdGradient(GlobalBackbuffer, xOffset, yOffset);// 这个地方需要渲染一下不然是黑屏a{HDC DeviceContext = GetDC(Window);win32_window_dimension Dimension = Win32GetWindowDimension(Window);RECT WindowRect;GetClientRect(Window, &WindowRect);int WindowWidth = WindowRect.right - WindowRect.left;int WindowHeigh = WindowRect.bottom - WindowRect.top;Win32DisplayBufferInWindow(DeviceContext, Dimension.Width,Dimension.Height, GlobalBackbuffer, 0, 0,WindowWidth, WindowHeigh);ReleaseDC(Window, DeviceContext);}++xOffset;}} else { // 如果窗口创建失败// 这里可以处理窗口创建失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}} else { // 如果窗口类注册失败// 这里可以处理注册失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}return 0;
}