1

我有一个非常简单的程序,可以播放 4 种不同的音调,具体取决于按下的按钮。我发现如果我快速连续演奏多个音调或相同的音调,会产生令人不快的咔嗒声。我已确保我的音频样本中不存在这些点击;这肯定是由于一个接一个地快速播放剪辑造成的。

在谷歌搜索之后,我相当确定点击是由于剪辑之间音高的快速变化。查看来自违规音频的播放波形,看起来一个剪辑在开始下一个剪辑之前首先被取消了几分之一秒。我已经强调了这似乎特别明显的部分。

显示音调之间咔嗒声的剪辑波形

展示这些音频点击的剪辑也可以在这里下载。

我的代码非常简单。我正在使用 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 文档似乎相对稀疏,而且我对它缺乏经验。

播放音频样本以使每个样本淡入下一个样本时,是否可以解决我的点击问题?如果是这样,我如何使用 WinMM 完成此操作?如果没有,我可以尝试另一种解决方案吗?

4

1 回答 1

0

我知道我们如何在理论上解决这个问题,但我还没有适用于所有情况的实际工作代码。(当我这样做时,我会编辑它。)

首先,一个可行的简单案例:尝试使用 mciSendStringA,而不是使用 PlaySound:

    if(auto err = mciSendStringA("play test.wav", null, 0, null)) 
            writeln(err);     

我不是在编造,Windows 实际上具有该功能,并且它实际上适用于许多小命令字符串和文件格式(尽管如果您的程序终止,所有声音都会停止,因此请确保程序继续运行,例如留在您的控制器中循环或调用 Sleep(something))。

我使用了很多 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。布拉格。相关结构:

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 提供数据。混合两个样本只是将值加在一起的情况(如果它们溢出,则剪辑 - 听起来很糟糕,但希望这不会真正发生)所以如果你做的声音不止一个,只需在你去的时候添加它们。

不要忘记任何回调函数的外部(Windows)!

4) 加载您的样本可能意味着读取 .wav 文件。这并不难,Windows 有辅助功能,或者你可以自己做。我也会为此展示代码。

到目前为止,我在 simpleaudio.d https://github.com/adamdruppe/arsd/blob/master/simpleaudio.d中找到了 struct AudioOutput 和 WinMM 版本。它现在有一个可怕的 API,必须彻底改变——它在 Linux 上是可以接受的,但在 Windows 上很糟糕。回调馈送器而不是 write(data) 应该在两个平台上都能更好地工作,所以这就是我要做的。

我现在在演示中遇到的问题是缓冲区之间的间隙......导致咔哒声。是的。但我确信应该通过适当的回调方法和缓冲区大小来解决延迟问题。

不过,该 MCI 功能可能会为您提供下一步,如果多个设备都可以工作,甚至可能是最后一步。


顺便说一句:你也可以让它执行 MIDI 命令而不是播放 wav 并获得各种很酷的东西。Simpleaudio.d 的低级 MIDI 已经在运行 - 演示主程序甚至显示了钢琴音阶。将它安装到 xbox 控制器中应该不会太难......按下按钮时记下,释放时记下,甚至不考虑时间......不是问题的真正答案,而是一个很酷的东西同理!

于 2014-12-26T04:57:02.090 回答