从多通道wav文件中读取单个通道

本文关键字:读取 单个 通道 文件 多通道 wav | 更新日期: 2023-09-27 18:17:06

我需要从包含多达12个(11.1格式)通道的wav文件中提取单个通道的样本。我知道在一个正常的立体声文件中采样是交错的,先向左,然后向右,像这样,

[1st L] [1st R] [2nd L] [2nd R]...

要读取左通道,我要这样做,

for (var i = 0; i < myByteArray.Length; i += (bitDepth / 8) * 2)
{
    // Get bytes and convert to actual samples.
}

要获得正确的通道,我只需执行for (var i = (bitDepth / 8)...

但是,对于超过2个通道的文件使用什么顺序?

从多通道wav文件中读取单个通道

微软已经创建了一个覆盖多达18个频道的标准。根据他们的说法,wav文件需要有一个特殊的元子块(在"可扩展格式"部分下),指定一个"通道掩码"(dwChannelMask)。这个字段是4字节长(a uint),它包含了每个通道的对应位,因此指示了在文件中使用的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(1599),则表示该文件包含8个通道(FL, FR, FC, LFE, BL, BR, SL &SR)。

读取和检查通道掩码

要获得掩码,您需要读取40th, 41st, 42nd和43rd字节(假设基本索引为0,并且您正在读取标准wav标头)。例如,
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
}

最后检查通道是否存在。

如果通道掩码不存在怎么办?

自己创建一个!根据文件的通道数,您将不得不猜测使用了哪些通道,或者只是盲目地遵循MCL。在下面的代码片段中,我们同时做了一点

public static uint GetSpeakerMask(int channelCount)
{
    // Assume 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 = Enum.GetValues(typeof(Channels)).Cast<uint>().ToArray();
    for (var i = 0; i < channelCount; i++)
    {
        mask += 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).

总结

所以,要读取单个通道的字节你需要做类似的事情,

// This code will only return the bytes of a particular channel. It's up to you to convert the bytes to actual samples.
public static byte[] GetChannelBytes(byte[] audioBytes, 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] = audioBytes[(k - startByteIncIndex) + chOffset];
            i++;
        }
    }
    return outputBytes;
}
private static 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();
}
private static int GetChannelNumber(Channels input, Channels[] existingChannels)
{
    for (var i = 0; i < existingChannels.Length; i++)
    {
        if (existingChannels[i] == input)
        {
            return i;
        }
    }
    return -1;
}