技术宅的压博体育下载

 找回密码
 压博体育下载

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 6664|回复: 15
打印 上一主题 下一主题
收起左侧

【文件结构】MIDI文件的结构和完整的读取方法

[复制链接]

1044

主题

2345

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
218
威望
294 点
宅币
18324 个
贡献
37482 次
宅之契约
0 份
在线时间
1748 小时
注册时间
2014-1-26
跳转到指定楼层
楼主
发表于 2014-7-19 06:15:11 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

欢迎访问技术宅的压博体育下载,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?压博体育下载

x
原帖:http://www.0xaa55.com/thread-717-1-1.html
转载请注明出处。

MIDI文件是一种多媒体音乐文件。不同于别的音乐文件,MIDI文件只记录曲谱。播放的时候,软件会读取MIDI文件的内容,然后把曲谱发送给声卡,由声卡模拟发音。相对于留声机原理的音乐文件(MP3、OGG、AMR、WAV、FLAC、AAC等),这种记录曲谱的文件更小,更容易创作。缺点是因为声音是软件模拟发出的,音质不能得到保证,而且不同的声卡有不同的发音效果,因此MIDI文件的播放效果不能得到保证。不过为此,微软提供了软波表合成器,使用微软的软波表可以实现MIDI文件效果的统一。

着名的游戏《恶魔之星III》(俗称“雷电3”)的背景音乐就是用MIDI音乐播放的效果。微软DirectX的组件DirectMusic就能用于播放MIDI音乐。

MIDI文件大体上分两个部分:文件头和音轨。
文件头由14个字节组成。开头是文件标识“MThd”,之后是一个Big-Endian存储的DWORD,值通常为6。这个值的意义是“文件头的信息所占字节数”,也就是文件头接下来6个字节的字节数(就是6了嘛)。之后的6个字节,其实是3个Big-Endian存储的WORD,分别是类型字,音轨数,每四分音符的Tick数。类型字有三个值:0表示这个文件只有一个音轨,1表示这个文件有多个音轨,播放的时候要同步播放所有音轨,2表示这个文件虽然有多个音轨,但是播放的时候必须一个音轨一个音轨的播放。
用一个结构体来描述MIDI文件头的结构:
[C] 纯文本查看 复制代码
typedef struct
{
    DWORD   dwFlag;             //MThd标识
    DWORD   dwRestLen;          //剩余部分长度
    WORD    wType;              //类型
    WORD    wNbTracks;          //音轨数
    WORD    wTicksPerCrotchet;  //每四分音符的Tick数
}MIDIHeader;
以上就是文件头了。然后就是文件的重要部分:文件内容了。
文件内容由多个音轨组成,每个音轨的开头都是这样的结构:4字节的音轨标识"MTrk",4字节以Big-Endian存储的音轨字节数。如下所示:
[C] 纯文本查看 复制代码
typedef struct
{
    DWORD   dwFlag;     //为0x6B72544D,即"MTrk"
    DWORD   dwTrackLen; //音轨长度(除去音轨头部以外的字节数)(Big-Endian)
}MIDITrack;
其中dwTrackLen的值,是整个音轨的字节数减去音轨头部的8个字节。
每个音轨,除去音轨头部以外,剩下的就是主要的文件内容了。在介绍文件内容以前,我觉得有必要科普一下MIDI文件所用的“动态字节”是怎么回事。
首先这个动态字节存在的意义是为了减少一个数字的存储空间。1个字节是8位,我们拿出其中的低7位存储数字,那么我们能存储的范围是0-127,如果我们要存储的数字在这个范围内的话,我们就把最高位设置为0。而如果我们要存储的数字超出了0-127这个范围,那么我们就把最高位设置为1,然后记录下高7位,剩下的留给下一个字节。假设我们要存储111这个数字,因为它的值在0-127范围内,我们可以只用一个字节存储:01101111b。而假设我要存储333这个数字,把它转换成二进制的时候是101001101b,超出了7位能存储的范围,那么我们先提取出它的高7位和低7位:0000010和1001101.然后我们用这样的两个字节存储:10000010b和01001101b.当我们读取动态字节的时候,我们先读取一个字节,记录它的低7位,然后判断它的最高位来判断是否需要继续读取下一个字节。
以下VB代码用于展示读取动态字节的方法。调用的时候,以文件号1打开文件,然后用Seek #1定位文件指针,最后调用它来读取动态字节。
[Visual Basic] 纯文本查看 复制代码
'读取动态字节
Function ReadDynamicBytes(ValueOut As Long) As Long '返回读取的字节数
Dim OneByte As Byte
ValueOut = 0
For ReadDynamicBytes = 1 To 4 '最多读取4个字节
    Get #1, , OneByte '读取一个字节
    ValueOut = (ValueOut * &H80&) Or (OneByte And &H7F) '记录这个字节的低7位,同时左移以读取的数据让出位置。
    If (OneByte And &H80) = 0 Then Exit For '这个字节的最高位是0,没有后续字节,停止读取。
Next
End Function
理论上动态字节可用于存储大数,不过特别大的数在MIDI文件里用不着。我们顶多读取4字节(28位整数)

讲完了动态字节,接下来就应该讲文件内容了。文件的内容都是“事件”,这些“事件”是一个接一个存储的。一个事件都有固定的结构:延迟,事件号,事件参数。
其中“延迟”是动态字节,用于表示上一个事件到这一个事件之间的延迟量。这个延迟量的单位是Tick。一个Tick有多长时间取决于MIDI文件的曲速。
事件号的值在0x80到0xFF之间的时候表示的是具体的值,若读取到的这个值在0到0x7F之间,则表示这个事件的事件号和上一个事件相同,而读取到的值是它的参数。
有关事件号的资料在网络上查找也大同小异。但是都讲得不够清楚。我专门写了一个C语言程序用于解释事件号。看源码便知。
[C] 纯文本查看 复制代码
//=============================================================================
//作者:0xAA55
//论坛:http://www.0xaa55.com/
//版权所有(C) 2013-2014 技术宅的压博体育下载
//请保留原作者信息,否则视为侵权。
//-----------------------------------------------------------------------------
#include
#include

//统一类型长度
typedef signed int      MIDIInt,*MIDIIntP;
typedef signed char     MIDIInt8,*MIDIInt8P;
typedef signed short    MIDIInt16,*MIDIInt16P;
typedef signed long     MIDIInt32,*MIDIInt32P;
typedef unsigned int    MIDIUInt,*MIDIUIntP;
typedef unsigned char   MIDIUInt8,*MIDIUInt8P;
typedef unsigned short  MIDIUInt16,*MIDIUInt16P;
typedef unsigned long   MIDIUInt32,*MIDIUInt32P;

typedef MIDIUInt8       BYTE;
typedef MIDIUInt16      WORD;
typedef MIDIUInt32      DWORD;

//MIDI文件头的结构体
typedef struct
{
    DWORD   dwFlag;             //MThd标识
    DWORD   dwRestLen;          //剩余部分长度
    WORD    wType;              //类型
    WORD    wNbTracks;          //音轨数
    WORD    wTicksPerCrotchet;  //每四分音符的Tick数
}MIDIHeader,*MIDIHeaderP;

//MIDI文件中出现过的标识
#define MIDI_MThd   0x6468544D
#define MIDI_MTrk   0x6B72544D

//MIDI文件头的“类型”
#define MIDIT_SingleTrack   0   /*单音轨*/
#define MIDIT_MultiSync     1   /*同步多音轨*/
#define MIDIT_MultiAsync    2   /*异步多音轨*/

//各种长度的Big-Endian到Little-Endian的转换
#define BSwapW(x)   ((((x) & 0xFF)<<8)|(((x) & 0xFF00)>>8))
#define BSwap24(x)  ((((x) & 0xFF)<<16)|((x) & 0xFF00)|(((x) & 0xFF0000)>>16))
#define BSwapD(x)   ((((x) & 0xFF)<<24)|(((x) & 0xFF00)<<8)|(((x) & 0xFF0000)>>8)|(((x) & 0xFF000000)>>24))

//将音符字节转换成字符串的函数
char*NoteToString(BYTE bNote);

//读取字符串然后打印
size_t ReadStringAndPrint(FILE*,size_t);

//=============================================================================
//ReadDynamicBytes:
//读取动态字节,最多读取4字节。返回读取的字节数
//-----------------------------------------------------------------------------
size_t ReadDynamicBytes(FILE*fp,DWORD*pOut)
{
    size_t bBytesRead;//已读取的字节数
    *pOut=0;
    for(bBytesRead=1;bBytesRead<=4;bBytesRead++)//最多读取4字节
    {
        int iValue=fgetc(fp);
        if(iValue==EOF)
            return 0;
        *pOut=(*pOut<<7)|(iValue & 0x7F);//新读入的是低位
        if(!(iValue & 0x80))//如果没有后续字节
            break;//就停止读取。
    }
    return bBytesRead;//返回读取的字节数
}

//=============================================================================
//ParseMIDI:
//分析MIDI文件。失败返回零,成功返回非零
//-----------------------------------------------------------------------------
int ParseMIDI(char*pszFileName)
{
#   define READERR {fprintf(stderr,"读取文件%s遇到错误\n",pszFileName);goto BadReturn;}
#   define FMTERR {fprintf(stderr,"%s不是一个正常的MIDI文件\n",pszFileName);goto BadReturn;}
#   define READ(x) if(fread(&(x),1,sizeof(x),fp)!=sizeof(x))READERR

    FILE*fp;
    MIDIHeader midiHeader;
    size_t i;

    //打开文件
    fp=fopen(pszFileName,"rb");
    if(!fp)
        READERR;

    //MIDI文件头就是一个MIDIHeader结构体。
    //但是要注意其中的数值都是Big-Endian存储的
    //需要进行转换

    //读取MIDI文件头
    READ(midiHeader);

    //检查文件格式
    if(midiHeader.dwFlag!=MIDI_MThd)//标识必须是"MThd"
        FMTERR;

    //转换为Little-Endian
    midiHeader.dwRestLen=           BSwapD(midiHeader.dwRestLen);
    midiHeader.wType=               BSwapW(midiHeader.wType);
    midiHeader.wNbTracks=           BSwapW(midiHeader.wNbTracks);
    midiHeader.wTicksPerCrotchet=   BSwapW(midiHeader.wTicksPerCrotchet);

    //分析文件头
    switch(midiHeader.wType)
    {
    case MIDIT_SingleTrack:
        fputs("类型:单音轨\n",stdout);
        break;
    case MIDIT_MultiSync:
        fputs("类型:同步多音轨\n",stdout);
        break;
    case MIDIT_MultiAsync:
        fputs("类型:异步多音轨\n",stdout);
        break;
    default:
        fprintf(stdout,"类型:未知(0x%04X)\n",midiHeader.wType);
        break;
    }

    //打印音轨数等信息
    fprintf(stdout,
        "音轨数:%u\n"
        "每四分音符时钟数:%u\n",
        midiHeader.wNbTracks,
        midiHeader.wTicksPerCrotchet);

    //正确跳转到MIDI音轨的位置,体现midiHeader.dwRestLen的作用……
    fseek(fp,8+midiHeader.dwRestLen,SEEK_SET);

    //准备读取各个音轨
    for(i=0;i
BIN: MIDIFile.exe (48 KB, 下载次数: 55, 售价: 1 个宅币)
SRC: MIDIFile.7z (21.83 KB, 下载次数: 34, 售价: 10 个宅币)

0

主题

5

帖子

7

积分

用户组: 初·技术宅

UID
2271
精华
0
威望
1 点
宅币
0 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-2-21
沙发
发表于 2017-2-21 18:53:56 | 只看该作者
好东西,好好学习

0

主题

18

帖子

48

积分

用户组: 初·技术宅

UID
2268
精华
0
威望
1 点
宅币
28 个
贡献
0 次
宅之契约
0 份
在线时间
2 小时
注册时间
2017-2-21
板凳
发表于 2017-2-22 10:51:36 | 只看该作者
这个资料在十几年前应该很有用。。。

1044

主题

2345

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
218
威望
294 点
宅币
18324 个
贡献
37482 次
宅之契约
0 份
在线时间
1748 小时
注册时间
2014-1-26
地板
 楼主| 发表于 2017-2-22 12:55:42 | 只看该作者
tx7790 发表于 2017-2-22 10:51
这个资料在十几年前应该很有用。。。

现在依然有用,尤其是嵌入式开发,这个技术根本就没过时

0

主题

4

帖子

6

积分

用户组: 初·技术宅

UID
2655
精华
0
威望
0 点
宅币
2 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2017-7-6
5#
发表于 2017-7-6 18:15:27 | 只看该作者
谢谢楼主分享!!

0

主题

2

帖子

9

积分

用户组: 初·技术宅

UID
2798
精华
0
威望
1 点
宅币
5 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2017-8-21
6#
发表于 2017-8-21 00:24:55 | 只看该作者
学习了…感谢楼主!

2

主题

24

帖子

115

积分

用户组: 小·技术宅

UID
3003
精华
0
威望
2 点
宅币
87 个
贡献
0 次
宅之契约
0 份
在线时间
20 小时
注册时间
2017-10-24
7#
发表于 2017-10-25 01:52:21 | 只看该作者
谢谢楼主分享

2

主题

24

帖子

115

积分

用户组: 小·技术宅

UID
3003
精华
0
威望
2 点
宅币
87 个
贡献
0 次
宅之契约
0 份
在线时间
20 小时
注册时间
2017-10-24
8#
发表于 2017-10-25 02:27:32 | 只看该作者
正好在写这方面的程序

0

主题

1

帖子

9

积分

用户组: 初·技术宅

UID
3049
精华
0
威望
0 点
宅币
8 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2017-11-5
9#
发表于 2017-11-5 19:13:50 | 只看该作者
挺好的,这个文件挺有用的

1

主题

85

帖子

91

积分

用户组: 小·技术宅

UID
3026
精华
0
威望
1 点
宅币
3 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2017-10-31
10#
发表于 2017-11-7 07:50:28 | 只看该作者
感谢源代码。收藏

0

主题

5

帖子

11

积分

用户组: 初·技术宅

UID
3153
精华
0
威望
0 点
宅币
6 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-12-1
11#
发表于 2017-12-1 15:46:38 | 只看该作者
在做音乐方面的,希望可以用到

0

主题

5

帖子

11

积分

用户组: 初·技术宅

UID
3153
精华
0
威望
0 点
宅币
6 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-12-1
12#
发表于 2017-12-4 10:42:41 | 只看该作者
mac上跑的有问题,

0

主题

9

帖子

35

积分

用户组: 初·技术宅

UID
3290
精华
0
威望
0 点
宅币
26 个
贡献
0 次
宅之契约
0 份
在线时间
4 小时
注册时间
2018-1-3
13#
发表于 2018-1-3 14:59:16 | 只看该作者

挺好的,这个文件挺有用的

0

主题

2

帖子

30

积分

用户组: 初·技术宅

UID
3441
精华
0
威望
0 点
宅币
27 个
贡献
1 次
宅之契约
0 份
在线时间
9 小时
注册时间
2018-2-7
14#
发表于 2018-2-25 22:19:45 | 只看该作者
谢谢楼主分享

0

主题

1

帖子

8

积分

用户组: 初·技术宅

UID
3770
精华
0
威望
0 点
宅币
7 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2018-4-28
15#
发表于 2018-4-28 23:12:04 | 只看该作者
跑这个程序应该出什么结果?文件名应该在什么地方修改?

0

主题

5

帖子

91

积分

用户组: 小·技术宅

UID
3737
精华
0
威望
20 点
宅币
46 个
贡献
0 次
宅之契约
0 份
在线时间
7 小时
注册时间
2018-4-21
16#
发表于 2019-9-9 21:53:46 | 只看该作者
你这个程序有问题,元事件长度用ReadDynamicBytes函数读取的
您需要登录后才可以回帖 登录 | 压博体育下载

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的压博体育下载 ( 滇ICP备16008837号|网站地图

GMT+8, 2019-11-11 20:11 , Processed in 0.138747 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表