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

C# RIFF 解析器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (18投票s)

2005 年 6 月 7 日

7分钟阅读

viewsIcon

167611

downloadIcon

9105

使用此纯 C# 解析器解码资源交换文件 (AVI, WAV, RMID...)。

RiffParserDemo2 Windows GUI

Windows 应用程序演示

RiffParserDemo console app

控制台应用程序演示

引言

您是否曾想过如何访问 AVI 文件中的信息,或者想直接从 WAV 文件中提取信息?大多数人会引用外部 COM 对象(例如 AVIFIL32.DLL)来访问信息(请参阅 一个简单的 C# AviFile 库包装器)。RIFF 解析器允许您直接通过 C# 访问文件中存储的资源信息。

背景

什么是 RIFF 文件?

RIFF 是资源交换文件格式(Resource Interchange File Format)。这是一种由微软和 IBM 在他们早已被遗忘的联盟时期定义的、用于交换多媒体数据类型的通用格式。

RIFF 文件格式

RIFF 文件由 RIFF 头部后跟零个或多个列表(包含块和其他列表)和块(包含数据)组成。有关具体示例,请参阅下面的 AVI RIFF 形式说明。

RIFF 头部具有以下形式

'RIFF' fileSize fileType (data)

其中 'RIFF' 是文字的 FourCC 代码“RIFF”,fileSize 是一个四字节值,表示文件中的数据大小,fileType 是一个 FourCC,用于标识特定文件类型。fileSize 的值包括 fileType FourCC 的大小以及后面的数据大小,但不包括“RIFF”FourCC 的大小或 fileSize 的大小。文件数据由块和列表组成,顺序任意。

块具有以下形式

ckID ckSize ckData

其中 ckID 是一个 FourCC,用于标识块中包含的数据;ckData 是一个四字节值,表示 ckData 中的数据大小;ckData 是零个或多个字节的数据。数据总是填充到最近的 WORD 边界。ckSize 表示块中有效数据的字节数;它不包括填充、ckID 的大小或 ckSize 的大小。

列表具有以下形式

'LIST' listSize listType listData

其中 'LIST' 是文字的 FourCC 代码“LIST”,listSize 是一个四字节值,表示列表的大小;listType 是一个 FourCC 代码;listData 由块或列表组成,顺序任意。listSize 的值包括 listType 的大小以及 listData 的大小;它不包括“LIST”FourCClistSize 的大小。

FourCC

FourCC(四字符代码)是一个 32 位无符号整数,通过连接四个 ASCII 字符创建。例如,在小端系统中,FourCC 'abcd' 表示为 0x64636261FourCC 可以包含空格字符,因此 ' abc' 是一个有效的 FourCC。RIFF 文件格式使用 FourCC 代码来标识流类型、数据块、索引条目和其他信息。

什么是“AVI”文件格式?

AVI RIFF 形式

AVI 文件在 RIFF 头部中通过 FourCC 'AVI ' 标识。所有 AVI 文件都包含两个强制的 LIST 块,它们分别定义了流的格式和流数据。AVI 文件还可以包含一个索引块,该块提供了文件中数据块的位置。包含这些组件的 AVI 文件具有以下形式

RIFF ('AVI '
      LIST ('hdrl' ... )
      LIST ('movi' ... )
      ['idx1' () ]
     )

'hdrl' 列表定义了数据格式,是第一个必需的 LIST 块。'movi' 列表包含 AVI 序列的数据,是第二个必需的 LIST 块。'idx1' 列表包含索引。AVI 文件必须按正确的顺序保留这三个组件。

注意:OpenDML 扩展定义了另一种类型的索引,由 FourCC 'indx' 标识。

'hdrl''movi' 列表使用子块来存储其数据。以下示例显示了扩展后的 AVI RIFF 形式,其中包含完成这些列表所需的块。

RIFF ('AVI '
      LIST ('hdrl'
            'avih'(<MAIN AVI Header>)
            LIST ('strl'
                  'strh'(<STREAM header>)
                  'strf'(<STREAM format>)
                  [ 'strd'(<ADDITIONAL header data>) ]
                  [ 'strn'(<STREAM name>) ]
                  ...
                 )
             ...
           )
      LIST ('movi'
            {SubChunk | LIST ('rec '
                              SubChunk1
                              SubChunk2
                              ...
                             )
               ...
            }
            ...
           )
      ['idx1' (<AVI Index>) ]
     )

有关 AVI 格式的更多信息,请参阅 John McGowan 的 AVI 概述OpenDML AVI 扩展

RIFF 解析器做什么?

给定一个 RIFF 文件,解析器会遍历文件中的各种元素,在遇到元素时调用您指定的委托。

提供了两个示例程序(均作为 Visual Studio .NET 2003 解决方案)

  • RIFFParserDemo – 一个控制台应用程序,它输出给定 RIFF 文件中的所有元素。
  • RIFFParserDemo2 – 一个 Windows 应用程序,用于检查 RIFF 文件。如果检查的文件是 AVIWAV,该应用程序将显示从 RIFF 元素中提取的附加信息。

使用 RIFF 解析器

首先,创建一个新的 RiffParser 对象。

rp = new RiffParser();

然后,尝试打开 RIFF 文件。

rp.OpenFile(filename);

如果没有抛出异常,则该文件是有效的 RIFF 文件,您可以通过访问 FileRIFFFileType 来访问文件类型和格式信息。请注意,文件 RIFF 格式和文件类型是 FourCC 代码。要以 string 格式读取代码,请使用 FromFourCC 静态方法。

public static string FromFourCC(int FourCC)

例如

txtFileFormat.Text = RiffParser.FromFourCC(rp.FileRIFF);
txtFileType.Text = RiffParser.FromFourCC(rp.FileType);

一旦确定了文件类型,就可以使用 ReadElement() 方法读取文件中的元素。

public bool ReadElement(ref int bytesleft, 
         ProcessChunkElement chunk, ProcessListElement list)

ReadElement() 方法具有以下参数

  • 一个 ref int,指定当前数据块中剩余的字节数(最初是文件中的数据长度)。
  • 当遇到块元素时调用的 delegate
  • 当遇到列表元素时调用的 delegate

当到达数据末尾时,该方法返回 false

为什么 bytesleft 参数是按引用传递的?字节计数被减少,以正确表示当前列表/块中剩余的数据量。按引用传递字节计数允许方法调用者有可能跳过当前“子”级别的剩余数据,然后继续读取下一个“父”级别的元素。

使用 ReadElement() 的示例

int length = Parser.DataSize;

RiffParser.ProcessChunkElement pdc = 
     new RiffParser.ProcessChunkElement(ProcessAVIChunk);
RiffParser.ProcessListElement pal = 
    new RiffParser.ProcessListElement(ProcessAVIList);

while (length > 0) 
{
    if (false == Parser.ReadElement(ref length, pdc, pal)) break;
}

处理完文件后,调用 CloseFile()

处理 RIFF 元素

处理块数据

public delegate void ProcessChunkElement(RiffParser rp, int FourCCType, 
       int unpaddedLength, int paddedLength);

当调用 ProcessChunkElement 委托时,该方法将使用四个参数进行调用

  • 对正在进行调用的 RiffParser 的引用。
  • 一个 int,指定块的 FourCC 代码。
  • 两个 int,指定块数据的未填充长度和已填充长度。RIFF 数据始终是 WORD 对齐的,因此即使块包含奇数个字节,也必须跳过偶数个字节才能访问下一个元素。

块数据可以根据情况读取或跳过。

读取块

if (AviRiffData.ckidAVIISFT == FourCC)
{
    Byte[] ba = new byte[paddedLength];
    rp.ReadData(ba, 0, paddedLength);
    StringBuilder sb = new StringBuilder(unpaddedLength);
    for (int i = 0; i < unpaddedLength; ++i) 
    {
        if (0 != ba[i]) sb.Append((char)ba[i]);
    }

    m_isft = sb.ToString();
}

跳过块

// Unknon chunk - skip
rp.SkipData(paddedLength);

处理 LIST 数据

public delegate void ProcessListElement(RiffParser rp, int FourCCType, int length);

当调用 ProcessListElement() 委托时,该方法将使用三个参数进行调用

  • 对调用 RiffParser 的引用。
  • 一个 int,指定列表的 FourCC 代码。
  • 一个 int,包含列表数据的大小。

然后可以跳过列表,

rp.SkipData(length);

或者可以通过调用 ReadElement() 来处理每个元素,可能使用新的委托来处理列表中的元素。

RiffParser.ProcessChunkElement pnc = 
    new RiffParser.ProcessChunkElement(ProcessNestedChunk);
RiffParser.ProcessListElement pnl = 
    new RiffParser.ProcessListElement(ProcessNestedList);

while (length > 0) 
{
    if (false == rp.ReadElement(ref length, pnc, pnl)) break;
}

FourCC 转换

有四个静态方法可用于方便地将 FourCC ints 转换为 FourCC ints,反之亦然。

public static string FromFourCC(int FourCC)
public static int ToFourCC(string FourCC)
public static int ToFourCC(char[] FourCC)
public static int ToFourCC(char c0, char c1, char c2, char c3)

我最常使用的方法是 FromFourCC()

// AVI section FourCC codes
public static readonly int ckidAVIHeaderList = RiffParser.ToFourCC("hdrl");
public static readonly int ckidMainAVIHeader = RiffParser.ToFourCC("avih");
public static readonly int ckidODML = RiffParser.ToFourCC("odml");
public static readonly int ckidAVIExtHeader = RiffParser.ToFourCC("dmlh");
public static readonly int ckidAVIStreamList = RiffParser.ToFourCC("strl");
public static readonly int ckidAVIStreamHeader = RiffParser.ToFourCC("strh");
public static readonly int ckidStreamFormat = RiffParser.ToFourCC("strf");
public static readonly int ckidAVIOldIndex = RiffParser.ToFourCC("idx1");
public static readonly int ckidINFOList = RiffParser.ToFourCC("INFO");
public static readonly int ckidAVIISFT = RiffParser.ToFourCC("ISFT");

是否需要 unsafe 和 fixed?

RIFF 文件是二进制文件。尝试一次读取一个字符的 RIFF 文件会导致严重的性能影响。文件中存储的数据结构设计为加载到内存中,然后使用固定大小的 C structs 来引用。例如,AVIMAINHEADER struct 定义如下:

typedef struct _avimainheader {
    FourCC fcc;
    DWORD  cb;
    DWORD  dwMicroSecPerFrame;
    DWORD  dwMaxBytesPerSec;
    DWORD  dwPaddingGranularity;
    DWORD  dwFlags;
    DWORD  dwTotalFrames;
    DWORD  dwInitialFrames;
    DWORD  dwStreams;
    DWORD  dwSuggestedBufferSize;
    DWORD  dwWidth;
    DWORD  dwHeight;
    DWORD  dwReserved[4];
} AVIMAINHEADER;

在 C++(或 C)中,您会这样做:

Private void DecodeAVIHeader(IOStream& stream)
{
    char[] data = new char[sizeof(AVIMAINHEADER)];

    stream.Read(data, sizeof(AVIMAINHEADER));

    AVIMAINHEADER* avi = (AVIMAINHEADER*)data;
    // Reference the struct members directly
    int totalFrames = avi->dwTotalFrames;
    …
}

但在 C# 中,在托管代码中 – 我们无法做到这一点。我们是否只能一次读取一个字节,然后进行大量工作来解码数据?

这就是 fixed/unsafe 的用武之地。fixed 关键字允许我们“固定”一段托管内存中的数据,从而保证数据不会被内存管理器移动或收集。一旦数据在内存中被 fixed,就可以(相对安全地)操作指向数据的指针并直接访问数据。fixed 就像 Unix 的 pinunpin,并被包装在 using 指令中。使用 fixed 需要使用 /unsafe 开关进行编译(或在 Visual Studio 项目配置属性页面中将“允许不安全的代码块”设置为 true)。

private unsafe void DecodeAVIHeader(RiffParser rp, int unpaddedLength, int length)
{
byte[] ba = new byte[length];

    rp.ReadData(ba, 0, length);

    fixed (Byte* bp = &ba[0]) 
    {
        AVIMAINHEADER* avi = (AVIMAINHEADER*)bp;
        m_frameRate = avi->dwMicroSecPerFrame;
    …
    }
}

托管数据结构在内存中的位置保持不变,并且在 fixed 块中是安全的,不会被收集。一旦离开 fixed 块,就无法保证任何事情,因此请不要保留指向可能不再存在的指针或数据的引用!在 fixed 块外复制所需数据并使用副本。

读取 RIFF 数据(文件访问)

读取 RIFF 头部

// Read the RIFF header
m_stream = new FileStream(m_filename, FileMode.Open, 
     FileAccess.Read, FileShare.Read);
int FourCC;
int datasize;
int fileType;

ReadTwoInts(out FourCC, out datasize);
ReadOneInt(out fileType);

读取 RIFF 元素。

int FourCC;
int size;

ReadTwoInts(out FourCC, out size);

...

// Examine the element, is it a list or a chunk
string type = FromFourCC(FourCC);
if (0 == String.Compare(type, LIST4CC))
{
    // We have a list
    ReadOneInt(out FourCC);

    if (null == list)
    {
        SkipData(size - 4);
    }
    else
    {
         // Invoke the list method
         list(this, FourCC, size - 4);
    }

    // Adjust size
    bytesleft -= size;
}
else
{
    // Calculated padded size - padded to WORD boundary
    int paddedSize = size;
    if (0 != (size & 1)) ++paddedSize;

    if (null == chunk)
    {
        SkipData(paddedSize);
    }
    else
    {
        chunk(this, FourCC, size, paddedSize);
    }

    // Adjust size
    bytesleft -= paddedSize;
}

读取两个 ints(注意使用 unsafefixed 关键字)。

public unsafe void ReadTwoInts(out int FourCC, out int size)
{
  try {
    int readsize = m_stream.Read(m_eightBytes, 0, TWODWORDSSIZE);

    if (TWODWORDSSIZE != readsize) {
      throw new RiffParserException("Unable to read. Corrupt RIFF file " + 
         FileName);
    }

    fixed (byte* bp = &m_eightBytes[0]) {
      FourCC = *((int*)bp);
      size = *((int*)(bp + DWORDSIZE));
    }
  }
  catch (Exception ex)
  {
    throw new RiffParserException("Problem accessing RIFF file " + FileName, ex);
  }
}

一个基本的 RIFF 解析器

以下是一个简单的解析器,用于显示 RIFF 文件中所有元素的完整源代码。

using System;
using System.Text;

namespace RiffParserDemo
{
    class RiffParserDemo
    {
        // Parse a RIFF file
        static void Main(string[] args)
        {
            // Create a parser instance
            RiffParser rp = new RiffParser();
            try 
            {
                string filename = @"C:\Program Files\Microsoft" +
                   " Visual Studio .NET 2003\Common7\Graphics\videos\BLUR24.avi";
                //string filename = @"C:\WINNT\Media\Chimes.wav"
                if (0 != args.Length)  
                {
                    filename = args[0];
                }
                    
                // Specify a file to open
                rp.OpenFile(filename);

                // If we got here - the file is valid. 
                //Output information about the file
                Console.WriteLine("File " + rp.ShortName + 
                    " is a \"" + RiffParser.FromFourCC(rp.FileRIFF)+ 
                    "\" with a specific type of \"" + 
                    RiffParser.FromFourCC(rp.FileType) + "\"");

                // Store the size to loop on the elements
                int size = rp.DataSize;

                // Define the processing delegates
                RiffParser.ProcessChunkElement pc = 
                     new RiffParser.ProcessChunkElement(ProcessChunk);
                RiffParser.ProcessListElement pl = 
                     new RiffParser.ProcessListElement(ProcessList);

                // Read all top level elements and chunks
                while (size > 0)
                {
                    // Prefix the line with the current top level type
                    Console.Write(RiffParser.FromFourCC(rp.FileType) + 
                          " (" + size.ToString() + "): ");
                    // Get the next element (if there is one)
                    if (false == rp.ReadElement(ref size, pc, pl)) break;
                }
                // Close the stream
                rp.CloseFile();
                Console.WriteLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("-----------------");
                Console.WriteLine("Problem: " + ex.ToString());
            }
            Console.WriteLine("\n\rDone. Press 'Enter' to exit.");
            Console.ReadLine();
        }

        // Process a RIFF list element (list sub elements)
        public static void ProcessList(RiffParser rp, int FourCC, int length)
        {
            string type = RiffParser.FromFourCC(FourCC);
            Console.WriteLine("Found list element of type \"" + 
                  type + "\" and length " + length.ToString());

            // Define the processing delegates
            RiffParser.ProcessChunkElement pc = 
                new RiffParser.ProcessChunkElement(ProcessChunk);
            RiffParser.ProcessListElement pl = 
                new RiffParser.ProcessListElement(ProcessList);

            // Read all the elements in the current list
            try {
                while (length > 0) {
                    // Prefix each line with the type of the current list
                    Console.Write(type + " (" + length.ToString() + "): ");
                    // Get the next element (if there is one)
                    if (false == rp.ReadElement(ref length, pc, pl)) break;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Problem: " + ex.ToString());
            }
        }

        // Process a RIFF chunk element (skip the data)
        public static void ProcessChunk(RiffParser rp, 
              int FourCC, int length, int paddedLength)
        {
            string type = RiffParser.FromFourCC(FourCC);
            Console.WriteLine("Found chunk element of type \"" + 
                type + "\" and length " + length.ToString());

            // Skip data and update bytesleft
            rp.SkipData(paddedLength);
        }
    }
}

附加功能

文件 AviRiffData.cs 包含与 C# 兼容的许多 AVIWAV 数据结构的定义。该文件还包含 AVIWAV 文件中使用的许多 FourCC 常量。

历史

  • 2005 年 6 月 6 日

    版本 1.0 - 初始发布。

© . All rights reserved.