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

Posted

技术标签:

【中文标题】从多通道 wav 文件中读取单个通道【英文标题】:Reading a single channel from a multi-channel wav file 【发布时间】:2014-09-30 10:31:00 【问题描述】:

我需要从包含多达 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 个通道的文件使用什么顺序?

【问题讨论】:

【参考方案1】:

Microsoft 创建了一个standard,最多可覆盖 18 个频道。根据他们的说法,wav 文件需要有一个special meta sub-chunk(在“可扩展格式”部分下)指定一个“通道掩码”(dwChannelMask)。该字段长 4 个字节(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

如果频道存在,最后是check。

如果 Channel Mask 不存在怎么办?

自己创建一个!根据文件的通道数,您要么必须猜测使用了哪些通道,要么只是盲目地遵循 MCL。在下面的代码 sn-p 中,我们都做了一点,

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;

【讨论】:

有趣的事实:您即将将Stack Overflow 上的最高赏金答案增加到 700...data.stackexchange.com/***/query/5400/… 为什么要在mask += channels[i + 1]; 中添加i + 1?如果channels.Length 是 18,您将点击IndexOutOfRangeException。我认为你不应该改变索引。对于 2 个频道,您有 FrontRight, FrontCenter 而不是 FrontLeft, FrontRight。无论如何 +1 代码 sn-p ;) 好收获!这很可能是一个错字? (这段代码现在相当老了,我不知道我在做什么。)

以上是关于从多通道 wav 文件中读取单个通道的主要内容,如果未能解决你的问题,请参考以下文章

如何在java中将Wav文件拆分为通道?

如何使用流体合成器将单个 midi 通道导出为 wav?

gstreamer 将多通道 wav 文件拆分为单独的通道并将每个通道编码为 mp3、alac 等并保存到文件

C代码创建多通道WAV音频文件

C代码创建多通道WAV音频文件

使用 libsndfile 为 MATLAB 编写多声道音频