为什么 SoundEffect.FromStream() 在读取 "valid" WAV 文件时抛出 InvalidOperationException?

Why does SoundEffect.FromStream() throw InvalidOperationException when reading "valid" WAV files?

我正在尝试将声音效果加载到我的游戏中(C#/XNA 4.0,Win8.1 上的 Visual Studio 2013)。该游戏是现有 MMORPG 客户端的 "clone",应与现有目录结构兼容 - 这意味着所有音效都存储在游戏工作目录中名为 'sfx' 的目录中。

我在尝试完成此操作时遇到了 SoundEffect.FromStream。它适用于我拥有的大多数文件,但有些文件会抛出 InvalidOperationException。堆栈跟踪显示错误位于 XNA DLL 的内部 Wav 文件构造函数中。

我很清楚 restrictions on wav files 用于 SoundEffect.FromStream 方法:8 位或 16 位、单声道或立体声、PCM,采样率在 8kHz 和 48kHz 之间。我遇到的问题是,据我所知,无法加载的特定 Wav 文件满足所有这些要求。

这是我加载 wav 文件的方式:

//SoundInfo is a wrapper around SoundEffect
//  implementation details are not relevant to my question
private readonly List<SoundInfo> m_guitarSounds;
private readonly List<SoundInfo> m_harpSounds;
private readonly List<SoundInfo> m_sounds;

//... additional initialization

foreach (string sfx in soundFiles)
{
    using (FileStream fs = new FileStream(sfx, FileMode.Open,
            FileAccess.Read, FileShare.Read))
    {
        int samples = getSamplesPerSecond(sfx);

        SoundEffect nextEffect;
        try
        {
            nextEffect = SoundEffect.FromStream(fs);
        }
        catch (InvalidOperationException)
        {
            //I got this idea from another Whosebug answer
            nextEffect = new SoundEffect(File.ReadAllBytes(sfx), samples,
                                          AudioChannels.Mono);
        }

        if (sfx.ToLower().Contains("gui"))
            m_guitarSounds.Add(new SoundInfo(nextEffect));
        else if (sfx.ToLower().Contains("har"))
            m_harpSounds.Add(new SoundInfo(nextEffect));
        else
            m_sounds.Add(new SoundInfo(nextEffect));
    }
}

如果失败,效果是从文件中读取的数据字节数组构建的,并强制为 Mono。这似乎适用于由于 InvalidOperationException 引起的所有错误。在这些情况下,声音通常会非常失真。

下面是 getSamplesPerSecond 的代码(在上面的代码中引用):

private int getSamplesPerSecond(string filename)
{
    //this method was in place for debugging purposes but is used to get
    //  the samplesPerSec for the audio files that can't be
    //  loaded using FromStream
    byte[] wav = File.ReadAllBytes(filename);

    // Get past all the other sub chunks to get to the data subchunk:
    int pos = 12;   // First Subchunk ID from 12 to 16

    //get fmt chunk
    while (!((char)wav[pos] == 'f' && (char)wav[pos + 1] == 'm' && 
            (char)wav[pos + 2] == 't' && (char)wav[pos + 3] == ' '))
    {
        pos += 4;
        int chunkSize = wav[pos] + wav[pos + 1] * 256 + 
                        wav[pos + 2] * 65536 + wav[pos + 3] * 16777216;
        pos += 4 + chunkSize;
    }
    pos += 8;

    int format = wav[pos] + wav[pos + 1]*256;
    pos += 2;
    int channels = wav[pos] + wav[pos + 1]*256;
    pos += 2;
    int samplesPerSec = wav[pos] + wav[pos + 1]*256 + wav[pos + 2]*65536
                        + wav[pos + 3]*16777216;
    pos += 4;
    int avgBytesPerSec = wav[pos] + wav[pos + 1]*256 + wav[pos + 2]*65536
                        + wav[pos + 3] * 16777216;
    pos += 4;
    int blockAlign = wav[pos] + wav[pos + 1]*256;
    pos += 2;

    int bitsPerSample = 0;
    if (format == 1)
    {
        bitsPerSample = wav[pos] + wav[pos + 1]*256;
        //pos += 2; //necessary if we want to look at other chunks
    }

    return samplesPerSec;
}

通过插入一些调试日志记录语句,我得到了这些文件内容。这是唯一失败的部分,总共只有9次失败(向右滚动!):

sfx\sfx001.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx002.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx003.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx004.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=22050, BlockAlign=1, BitsPerSample=8  
sfx\sfx005.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx006.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx007.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx008.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=22050, BlockAlign=1, BitsPerSample=8  
sfx\sfx009.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx010.wav: Format=1, Channels=2, SamplesPerSec=22050, AvgBytesPerSec=88200, BlockAlign=4, BitsPerSample=16    [ FAILED ]
sfx\sfx011.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx012.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx013.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx014.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=22050, BlockAlign=1, BitsPerSample=8  
sfx\sfx015.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  
sfx\sfx016.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx017.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx018.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16    [ FAILED ]
sfx\sfx019.wav: Format=1, Channels=1, SamplesPerSec=22050, AvgBytesPerSec=44100, BlockAlign=2, BitsPerSample=16  

打印到上述日志的值是在读取 WAV 文件头('fmt ' 块)后直接从 getSamplesPerSecond 方法打印的。我使用http://www.neurophys.wisc.edu/auditory/riff-format.txt作为判断WAV文件中数据含义的参考。

根据我的日志,每个文件都是 PCM、8 位或 16 位、22050Hz 采样率以及单声道或立体声(基于通道数)。所有文件都可以在原始游戏客户端中完美打开,并且可以在 Audacity 和 Windows Media Player 中运行,因此我可以排除文件本身的问题。

我在这里错过了什么?如果它与我的源音频文件不兼容,是否有另一种方法可以在不使用内容管道的情况下加载和播放它们?

由于文件格式不正确,加载失败的每个文件都抛出 InvalidOperationException,但这并不是因为任何 WAV 格式限制。

错误发生在文件的第 4、5、6 和 7 字节中报告的长度与 RIFF 数据的实际长度 不同的文件中。

Windows Media Player 和 Audacity 正在补偿此错误,但 SoundEffect.FromStream() 正在检测它并抛出异常。

这是我用来解决问题的工作方法:

private void _correctTheFileLength(string filename)
{
    byte[] wav = File.ReadAllBytes(filename);

    string riff = Encoding.ASCII.GetString(wav.SubArray(0, 4));
    if (riff != "RIFF" || wav.Length < 8) //check for RIFF tag and length
        return;

    int reportedLength = wav[4] + wav[5]*256 + wav[6]*65536 + wav[7]*16777216;
    int actualLength = wav.Length - 8;
    if (reportedLength != actualLength)
    {
        wav[4] = (byte) (actualLength & 0xFF);
        wav[5] = (byte) ((actualLength >> 8) & 0xFF);
        wav[6] = (byte) ((actualLength >> 16) & 0xFF);
        wav[7] = (byte) ((actualLength >> 24) & 0xFF);
        File.WriteAllBytes(filename, wav);
    }
}