在快速连续播放剪辑时单击声音

时间:2014-12-25 07:02:49

标签: audio d winmm

我有一个非常简单的程序,根据按下的按钮播放4种不同的音调。我发现如果我快速连续播放多个音调或相同的音调,就会产生令人不快的咔哒声。我确保我的音频样本中没有这些点击;它肯定是由一个接一个地快速播放剪辑引起的。

谷歌搜索后,我非常确定点击是由于剪辑之间音高的快速变化造成的。从有问题的音频看回放的波形,看起来剪辑在开始下一个剪辑之前首先被取消几分之一秒。我已经强调了这一点似乎特别明显的部分。

Waveform of the clip that exhibits clicking between tones

还可以下载展示这些音频点击的剪辑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完成此操作?如果没有,我可以尝试另一种解决方案吗?

1 个答案:

答案 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功能可能会为您提供下一步,如果多个设备可以工作,甚至可能是最后一步。


顺便说一下:你也可以多做它的MIDI命令,而不是玩wavs,并获得各种很酷的东西。 Simpleaudio.d的低级midi已经正常运行 - 演示主要甚至显示钢琴音阶。将它装入xbox控制器应该不会太难...注意按下按钮的时候,释放后注意关闭,甚至不考虑时间..不是问题的答案,而是一个很酷的事情。以同样的方式!