我有一个非常简单的程序,根据按下的按钮播放4种不同的音调。我发现如果我快速连续播放多个音调或相同的音调,就会产生令人不快的咔哒声。我确保我的音频样本中没有这些点击;它肯定是由一个接一个地快速播放剪辑引起的。
谷歌搜索后,我非常确定点击是由于剪辑之间音高的快速变化造成的。从有问题的音频看回放的波形,看起来剪辑在开始下一个剪辑之前首先被取消几分之一秒。我已经强调了这一点似乎特别明显的部分。
还可以下载展示这些音频点击的剪辑here。
我的代码非常简单。我正在使用XInput从连接的控制器读取输入,该控制器确定要播放的音调,我使用WinMM从wav文件输出声音。它是用D编程语言编写的,但我修改它以使用没有D特定的功能使其尽可能像C一样,并避免混淆。
SHORT keyPressed(int vkey)
{
enum highBit { val = 0x8000 }
return cast(SHORT)(GetKeyState(vkey) & highBit.val);
}
enum Button
{
DPAD_UP = 0x0001,
DPAD_DOWN = 0x0002,
DPAD_LEFT = 0x0004,
DPAD_RIGHT = 0x0008,
START = 0x0010,
BACK = 0x0020,
LEFT_THUMB = 0x0040,
RIGHT_THUMB = 0x0080,
LEFT_SHOULDER = 0x0100,
RIGHT_SHOULDER = 0x0200,
A = 0x1000,
B = 0x2000,
X = 0x4000,
Y = 0x8000,
}
struct XINPUT_GAMEPAD
{
WORD wButtons;
BYTE bLeftTrigger;
BYTE bRightTrigger;
SHORT sThumbLX;
SHORT sThumbLY;
SHORT sThumbRX;
SHORT sThumbRY;
}
struct XINPUT_STATE
{
DWORD dwPacketNumber;
XINPUT_GAMEPAD Gamepad;
bool isPressed(int button)
{
return cast(bool)(Gamepad.wButtons & button);
}
}
int main()
{
HANDLE xinputDLL = initXinput();
XINPUT_STATE oldState;
XINPUT_STATE newState;
while (!keyPressed(VK_ESCAPE))
{
oldState = newState;
XInputGetState(0, &newState);
enum flags { val = SND_ASYNC | SND_FILENAME | SND_NODEFAULT }
if (newState.isPressed(Button.A) && !oldState.isPressed(Button.A))
{
PlaySoundA(toStringz("Piano.ff.A4.wav"), null, flags.val);
}
if (newState.isPressed(Button.B) && !oldState.isPressed(Button.B))
{
PlaySoundA(toStringz("Piano.ff.B4.wav"), null, flags.val);
}
if (newState.isPressed(Button.X) && !oldState.isPressed(Button.X))
{
PlaySoundA(toStringz("Piano.ff.C5.wav"), null, flags.val);
}
if (newState.isPressed(Button.Y) && !oldState.isPressed(Button.Y))
{
PlaySoundA(toStringz("Piano.ff.F4.wav"), null, flags.val);
}
}
denitXinput(xinputDLL);
return 0;
}
假设我在点击声音的来源方面是正确的,我认为解决方案是让每个样本淡入下一个样本。但是,我不知道如何做到这一点,因为WinMM documentation似乎相对稀疏,我对此缺乏经验。
在播放音频样本时,是否解决了我的点击问题,让每个样本淡入下一个样本?如果是这样,我如何使用WinMM完成此操作?如果没有,我可以尝试另一种解决方案吗?
答案 0 :(得分:0)
我知道如何在理论上解决这个问题,但我还没有针对所有情况的实际工作代码。 (当我这样做时,我会编辑它。)
首先,有点有效的简单案例:不要使用PlaySound,请尝试mciSendStringA:
if(auto err = mciSendStringA("play test.wav", null, 0, null))
writeln(err);
我没有说明这一点,Windows实际上有这个功能,它实际上可以使用很多小命令字符串和文件格式(虽然如果你的程序终止,所有声音停止,所以确保程序继续运行,例如逗留在你的控制器循环或调用睡眠(某事))。
我已经使用了很多Win32,有时我会对它有多少东西感到惊讶。原型:
extern(Windows) uint mciSendStringA(in char*,char*,uint,void*);
在winmm.lib
中找到。
这基本上有效,但在我的测试中,同时播放两次相同的文件没有任何效果。虽然播放不同的文件混合在一起。所以这是一个部分解决方案。
下一步是使用mciSendCommand函数 - 比发送字符串低一点,所以你可以打开多个设备并尝试以这种方式获得更多重叠:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd743675%28v=vs.85%29.aspx
我还没有尝试过,但它看起来相当简单,我怀疑它对你来说可能已经足够了。为每个按钮打开一些设备,这样你就可以快速点击它们几次并循环播放它们,希望在需要的时候多次混合相同的声音。
原型是:
extern(Windows) uint /*MCIERROR*/ mciSendCommandA(MCIDEVICEID,UINT,DWORD,DWORD);
是的,它在msdn示例中转换为void *然后转换为DWORD。 Blargh。相关结构:
struct MCI_OPEN_PARMSA {
DWORD dwCallback;
MCIDEVICEID wDeviceID; // aka uint
LPCSTR lpstrDeviceType;
LPCSTR lpstrElementName;
LPCSTR lpstrAlias;
}
struct MCI_PLAY_PARMS {
DWORD dwCallback;
DWORD dwFrom;
DWORD dwTo;
}
你也可以从这里借一些常数:
https://github.com/AndrejMitrovic/DWinProgramming/blob/master/WindowsAPI/win32/mmsystem.d#L693
(如果你已经在使用win32绑定了,太棒了!但是我觉得它们对小事情有点痛苦所以我试图避免它们,更喜欢在MSDN中复制/粘贴原型+结构+常量,因为我需要它们。 )
您应该能够使用这些定义和core.sys.windows.windows获取MSDN示例。不要忘记pragma(lib, "winmm");
。
我认为一个完整的解决方案肯定会起作用,但也相当困难,它将使用低级接口在发生声音时自行混合并将结果发送到设备。我还没有这个工作,今天我没时间了,但希望明天我可以得到一些东西。
基本步骤是:
1)调用waveOutOpen获取设备。设置一个回调函数,当需要更多数据时调用它。
2)使用waveOutPrepareHeader准备缓冲区 - 或者可能不止一个
3)当回调请求(可能希望在单独的线程中使用当前注释)时,使用waveOutWrite提供数据。混合两个样本只是将值加在一起的情况(如果它们溢出则剪裁 - 听起来很糟糕,但希望实际上不会发生)所以如果您正在做多个声音,只需添加它们就可以了。
不要忘记任何回调函数的extern(Windows)!
4)加载样本可能意味着要读取.wav文件。这不是特别难,Windows有辅助功能,或者你可以自己动手。我也会为此展示代码。
到目前为止,我的simpleaudio.d https://github.com/adamdruppe/arsd/blob/master/simpleaudio.d找到struct AudioOutput和WinMM版本。它现在有一个可怕的API,必须彻底改变 - 它在Linux上是可以接受的,但在Windows上很糟糕。回调馈线而不是写入(数据)应该在两个平台上都能更好地工作,这就是我要做的。
我现在正在使用该演示的问题是缓冲区之间的间隙......导致咔嗒声。是啊。但我确信只需要通过适当的回调方法和缓冲区大小来解决延迟。
MCI功能可能会为您提供下一步,如果多个设备可以工作,甚至可能是最后一步。