65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 .NET 2.0 (C#) 读取多声道 WAV 文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (8投票s)

2014年12月15日

GPL3

3分钟阅读

viewsIcon

28329

本技巧解释了如何使用 .Net 2.0+ (在 C# 中) 读取包含多个通道 (2 个以上) 的 WAV (波形) 文件。

引言

本技巧旨在为您提供必要的知识(和代码),以便在 C# 中使用 .NET 2.0 或更高版本读取具有多个通道(通常大于 2,但相同的原则适用于单声道/立体声文件)的 WAV 文件。 假设您对 WAV 的头结构有基本的了解(如果没有,请花点时间阅读 这个);并具备对 .NET Framework(和 C#)的中等程度的了解。

现在,我们都知道标准的立体声 WAV 文件是交错的,先是左声道,然后是右声道。但是,如果文件具有 2 个以上的声道怎么办?我们又如何知道哪个声道是什么?值得庆幸的是,为了解决这个问题,微软创建了一个 标准,涵盖了多达 18 个声道。

根据该标准,WAV 文件需要有一个 特殊的元子块(在“Extensible Format”部分下),也称为“WAVE_FORMAT_EXTENSIBLE”,它指定一个“声道掩码”(dwChannelMask)。该字段为 4 个字节长(一个无符号整数),其中包含每个存在的声道的相应位,因此指示文件中存在 18 个声道中的哪些声道。

主声道布局

以下是 MCL,即现有声道应交错的顺序,以及每个声道对应的位值。

Order |  Bit  | Channel

 1.        0x1 Front Left
 2.        0x2 Front Right
 3.        0x4 Front Center
 4.        0x8 Low Frequency (LFE)
 5.       0x10 Back Left (Surround Back Left)
 6.       0x20 Back Right (Surround Back Right)
 7.       0x40 Front Left of Center
 8.       0x80 Front Right of Center
 9.      0x100 Back Center
10.      0x200 Side Left (Surround Left)
11.      0x400 Side Right (Surround Right)
12.      0x800 Top Center
13.     0x1000 Top Front Left
14.     0x2000 Top Front Center
15.     0x4000 Top Front Right
16.     0x8000 Top Back Left
17.    0x10000 Top Back Center
18.    0x20000 Top Back Right

例如,声道掩码 0x63F 将指示该文件包含 8 个声道:FL、FR、FC、LFE、BL、BR、SL & SR。(请注意,超出此预定义集合的 18 个声道的位置被认为是“保留的”;您不应假设超出这些声道的声道排序。

读取声道掩码

现在,要读取标准 WAV 文件的掩码,您必须读取第 40th 到 43rd 个字节(包括;假设基索引为 0)。例如

var bytes = new byte[50];

using (var stream = new FileStream("filepath...", FileMode.Open))
{
    stream.Read(bytes, 0, 50);
}

var speakerMask = BitConverter.ToUInt32(new[] { bytes[40], bytes[41], bytes[42], bytes[43] }, 0);

然后您可以检查哪些声道存在。为此,我建议创建一个 enum(用 [Flags] 定义),其中包含所有声道(及其各自的值)。

[Flags]
public enum Channels : uint
{
    FrontLeft = 0x1,
    FrontRight = 0x2,
    FrontCenter = 0x4,
    Lfe = 0x8,
    BackLeft = 0x10,
    BackRight = 0x20,
    FrontLeftOfCenter = 0x40,
    FrontRightOfCenter = 0x80,
    BackCenter = 0x100,
    SideLeft = 0x200,
    SideRight = 0x400,
    TopCenter = 0x800,
    TopFrontLeft = 0x1000,
    TopFrontCenter = 0x2000,
    TopFrontRight = 0x4000,
    TopBackLeft = 0x8000,
    TopBackCenter = 0x10000,
    TopBackRight = 0x20000
}

最后,(如果您愿意)您可以填充一个包含所有现有声道的 List<Channels>

var foundChannels = new List<Channels>();

foreach (var ch in Enum.GetValues(typeof(Channels)))
{
    if ((speakerMask & (uint)ch) == (uint)ch)
    {
        foundChannels.Add((Channels)ch);
    } 
}

如果扬声器掩码不存在怎么办?

如果文件的 wFormatTag 字段(通常用于指定音频数据编码的字段)设置为 0xFFFE,您需要自己创建掩码!根据文件的声道计数,您要么必须猜测使用了哪些声道,要么只是盲目地遵循 MCL。在下面的代码片段中,我们做了两件事。

static uint GetSpeakerMask(int channelCount)
{
    // Assume a setup of: FL, FR, FC, LFE, BL, BR, SL & SR. 
    // Otherwise, MCL will use: FL, FR, FC, LFE, BL, BR, FLoC & FRoC.
    if (channelCount == 8)
    {
        return 0x63F;
    }

    // Otherwise follow MCL.
    uint mask = 0;
    var channels = new Channels[18];
    Enum.GetValues(typeof(Channels)).CopyTo(channels, 0);

    for (var i = 0; i < channelCount; i++)
    {
        mask += (uint)channels[i];
    }

    return mask;
}

提取样本

要实际读取特定声道的样本,您遵循与立体声文件完全相同的过程,也就是说,您按帧大小(以字节为单位)增加循环的计数器。

frameSize = (bitDepth / 8) * channelCount

就像您正在读取立体声文件的右声道一样,您也需要偏移循环的起始索引。这就是事情变得更加复杂的地方,因为您必须从声道序号基于现有声道的开始读取数据,乘以字节深度。

我说的“基于现有声道”是什么意思?好吧,您需要根据现有声道重新分配现有声道的序号,从 1 开始,为每个现有声道递增序号。例如,声道掩码 0x63F 指示使用了 FL、FR、FC、LFE、BL、BR、SL & SR 声道,因此相应声道的新的声道序号将如下所示(注意,位值不会也不会更改),

Order | Bit | Channel

 1.     0x1  Front Left
 2.     0x2  Front Right
 3.     0x4  Front Center
 4.     0x8  Low Frequency (LFE)
 5.    0x10  Back Left (Surround Back Left)
 6.    0x20  Back Right (Surround Back Right)
 7.   0x200  Side Left (Surround Left)
 8.   0x400  Side Right (Surround Right)

您会注意到 FLoC、FRoC & BC 都缺失了,因此 SL & SR 声道“下降”到下一个最低的可用序号中,而不是使用 SL & SR 的默认顺序 (10, 11)。

示例代码

因此,考虑到以上所有因素,在使用 .NET 2.0 时,这是一个完全可用的示例,用于检索单个指定声道的字节,

byte[] GetChannelBytes(byte[] fileAudioBytes, uint speakerMask, 
Channels channelToRead, int bitDepth, uint sampleStartIndex, uint sampleEndIndex)
{
    var channels = FindExistingChannels(speakerMask);
    var ch = GetChannelNumber(channelToRead, channels);
    var byteDepth = bitDepth / 8;
    var chOffset = ch * byteDepth;
    var frameBytes = byteDepth * channels.Length;
    var startByteIncIndex = sampleStartIndex * byteDepth * channels.Length;
    var endByteIncIndex = sampleEndIndex * byteDepth * channels.Length;
    var outputBytesCount = endByteIncIndex - startByteIncIndex;
    var outputBytes = new byte[outputBytesCount / channels.Length];
    var i = 0;

    startByteIncIndex += chOffset;

    for (var j = startByteIncIndex; j < endByteIncIndex; j += frameBytes)
    {
        for (var k = j; k < j + byteDepth; k++)
        {
            outputBytes[i] = fileAudioBytes[(k - startByteIncIndex) + chOffset];
            i++;
        }
    }

    return outputBytes;
}

Channels[] FindExistingChannels(uint speakerMask)
{
    var foundChannels = new List<Channels>();

    foreach (var ch in Enum.GetValues(typeof(Channels)))
    {
        if ((speakerMask & (uint)ch) == (uint)ch)
        {
            foundChannels.Add((Channels)ch);
        }
    }

    return foundChannels.ToArray();
}

int GetChannelNumber(Channels input, Channels[] existingChannels)
{
    for (var i = 0; i < existingChannels.Length; i++)
    {
        if (existingChannels[i] == input)
        {
            return i;
        }
    }

    throw new KeyNotFoundException();
}
© . All rights reserved.