快速连续播放剪辑时的点击声音

Posted

技术标签:

【中文标题】快速连续播放剪辑时的点击声音【英文标题】:Clicking Sounds When Playing Clips in Rapid Succession 【发布时间】:2014-12-25 07:02:48 【问题描述】:

我有一个非常简单的程序,可以播放 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 完成此操作?如果没有,我可以尝试其他解决方案吗?

【问题讨论】:

我不熟悉 WinMM,但如果你想让它听起来像钢琴,我认为你应该以某种方式将音符混合在一起,让它们在演奏时相互重叠同时。如果你只是切断声音,它会引起咔哒声。所以,如果我是你,我会研究如何创建混合在一起的通道,并在不同的通道中播放样本。您正在考虑的短暂褪色也可能有所帮助,但它很容易导致明显的延迟。 tbh,我没听到...您发布的 ogg 文件对我来说听起来不错。如果你同时播放两个声音,它们会重叠吗?我认为操作系统混合了它们,但不确定......并且剪辑已经有一些自然淡入淡出,所以我在想什么(用你自己的淡入淡出和混合做一个低级别的 wavOut 调用)可能不会有帮助。跨度> 顺便说一句,我昨天写了一些非常相似的东西:xbox360 controller support with xinput and js on linux and audio out。我先做了 linux,昨天启动了 Windows 端,所以我打开了所有低级 wavOut msdn 调用。今天可能没有时间完成它,但也许明天结束时我会有一个小的 D 库 - 和 WinMM 示例 - 用于游戏杆、wav 和音频准备就绪,这也可能会有所帮助。 @AdamD.Ruppe 这不是巧合 =) 我受到您的代码的启发,开始使用 WinMM 和 XInput。播放该文件时,您应该能够通过稍微调高音量来听到咔嗒声。当这 3 个样本快速连续播放时,这一点在最后尤其明显。不同的声音似乎没有重叠;当我播放另一个声音时,如果一个声音还没有结束,看起来一个很快就被剪掉了,然后第二个开始了。 我想我在 PlaySound (enum SND_NOSTOP = 16; btw) 上找到了 SND_NOSTOP 标志。尝试添加它,看看会发生什么。然后它可能会混合,或者它可能会返回错误,但来自 MSDN “如果未指定此标志,PlaySound 会尝试停止当前在同一进程中播放的任何声音。在其他进程中播放的声音不受影响。” - 这可以解释你得到什么。 【参考方案1】:

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

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

    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 复制/粘贴原型+结构+常量。 )

您应该能够让 MSDN 示例使用这些定义和 core.sys.windows.windows。不要忘记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 上很糟糕。回调馈送器而不是 write(data) 应该在两个平台上都能更好地工作,所以这就是我要做的。

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

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


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

【讨论】:

以上是关于快速连续播放剪辑时的点击声音的主要内容,如果未能解决你的问题,请参考以下文章

AVAudioPlayer 多重声音嘈杂

连续播放声音 onTouch()

按下按钮后立即停止并启动声音

HTML5 音频:如何快速停止和重新启动剪辑?

在 iPhone 上完美连续播放声音

SoundPlayer 并播放无缝连续的声音