【C语言】Midi文件播放器(可跨平台)
来源:http://www.0xaa55.com/thread-1489-1-1.html转载请保留出处。
因为读写文件、发送Midi指令的函数都需要用户来亲自完成(实现接口)因此,可以实现跨平台。
不过目前我只写了个Windows平台的播放实例。DOS的以后会写出能在DOSBox播放Midi音乐的。
另外我这个的播放效果比mciSendCommand(或mciSendString)播放Midi文件的效果好很多。mciSendXXXX这玩意儿播放Midi存在跳音符的问题(比如某些鼓点没打上、节奏出现问题等),而我的这个Midi解析库则解决了这个问题。
midifile.h://=============================================================================
//作者:0xAA55
//论坛:http://www.0xaa55.com/
//版权所有(C) 2013-2015 技术宅的结界
//请保留原作者信息,否则视为侵权。
//-----------------------------------------------------------------------------
#ifndef _MIDI_Parser_
#define _MIDI_Parser_
#include<stdint.h>
//=============================================================================
//宏,可以用于调整调用约定、符号导入导出规则等
//-----------------------------------------------------------------------------
//MIDIFILE的函数调用约定
#ifndef Midi_c
#define Midi_c _cdecl
#endif // !Midi_c
//MIDIFILE的回调函数调用约定
#ifndef Midi_cb
#define Midi_cb _cdecl
#endif // !Midi_cb
//MIDIFILE的符号导出规则
#ifndef Midi_x
#ifdef __cplusplus
#define Midi_x extern"C"
#else // !__cplusplus
#define Midi_x extern
#endif // !__cplusplus
#endif // !Midi_x
//MIDIFILE的导出函数
#define Midi_fn(fn,rt) Midi_x rt Midi_c fn
//用户自定义数据的格式
#ifndef Midi_UserData_t
#define Midi_UserData_t void
#endif
#ifndef Midi_UserData_p
#define Midi_UserData_p Midi_UserData_t*
#endif
//为了防止出现fpos_t未定义的问题,这里定义一次。
typedef __int64 fpos_t;
//Midi时间刻度
#ifndef MidiTime_t
#define MidiTime_t double
#endif
#ifndef MidiTime_p
#define MidiTime_p MidiTime_t*
#endif
//-----------------------------------------------------------------------------
//=============================================================================
//错误代码
//-----------------------------------------------------------------------------
typedef enum
{
ME_OK = 0, //无错误
ME_OutOfMem, //内存不足
ME_TellFail, //取得文件指针失败
ME_SeekFail, //设置文件指针失败
ME_ReadFail, //读取失败
ME_BadFile //文件格式错误
}MidiErrCode_t, * MidiErrCode_p;
//=============================================================================
//用户回调函数,用户必须提供实现的函数
//-----------------------------------------------------------------------------
//读取文件:要求成功返回非零,失败返回0
typedef int(Midi_cb* ReadFile_f)
(
void* pBuf, //缓冲区
size_t cb, //要读取的字节数
Midi_UserData_p pUserData //用户数据
);
//-----------------------------------------------------------------------------
//取得文件指针:要求成功返回非零,失败返回0
typedef int(Midi_cb* GetFilePtr_f)
(
fpos_t* pPosition, //取得位置
Midi_UserData_p pUserData //用户数据
);
//-----------------------------------------------------------------------------
//设置文件指针:要求成功返回非零,失败返回0
typedef int(Midi_cb* SetFilePtr_f)
(
fpos_t Position, //新的位置
Midi_UserData_p pUserData //用户数据
);
//-----------------------------------------------------------------------------
//发送MIDI命令
typedef void(Midi_cb* SendMidiMsg_f)
(
uint32_t TickCount, //当前命令时钟
uint16_t TrackNo, //当前音轨号
uint8_t Command, //命令
uint8_t Param1, //参数1
uint8_t Param2, //参数2
Midi_UserData_p pUserData //用户数据
);
//-----------------------------------------------------------------------------
//错误处理
typedef void(Midi_cb* Error_f)(MidiErrCode_t);
//-----------------------------------------------------------------------------
//=============================================================================
//MIDI解析器的音轨数据
//-----------------------------------------------------------------------------
typedef struct
{
fpos_t StartPos; //音轨开始的文件偏移
fpos_t TrackSize; //音轨大小
fpos_t NextNotePos;//音轨下个音符的文件偏移
uint32_t LastNoteTick;//上个音符的时钟
uint32_t TotalTicks;//这个音轨的总时钟数
uint8_t EndOfTrack; //是否到达了当前轨道的末尾
uint8_t LastCommand;//上个命令
uint8_t LastParam1; //上个命令的参数1
uint8_t LastParam2; //上个命令的参数2
}TrackData_t, * TrackData_p;
//=============================================================================
//MIDI解析器结构体
//-----------------------------------------------------------------------------
typedef struct
{
//=========================================================================
//用户需要先填写如下的参数,才能使用MidiFile接口
ReadFile_f pfnReadFile; //读取文件回调
GetFilePtr_f pfnGetFilePtr; //设置文件指针回调
SetFilePtr_f pfnSetFilePtr; //设置文件指针回调
SendMidiMsg_f pfnSendMidiMsg; //发送Midi指令回调
Error_f pfnOnError; //错误处理回调
Midi_UserData_p pUserData; //用户自定义数据
//-------------------------------------------------------------------------
uint16_t wType; //Midi文件类型
uint16_t wNbTracks; //音轨数
uint16_t wTicksPerCrotchet;//每四分音符的Tick数
TrackData_p pTrackData; //音轨数据
uint32_t Velocity; //播放速度,一个四分音符的微秒数
uint32_t TotalTicks; //整个Midi文件的时钟数(长度)
uint8_t EndOfFile; //是否已经更新到了结尾
MidiErrCode_t LastErr; //错误代码
}MidiParser_t, * MidiParser_p;
//MIDI文件的类型
#define MIDIT_SingleTrack 0 /*单音轨*/
#define MIDIT_MultiSync 1 /*同步多音轨*/
#define MIDIT_MultiAsync 2 /*异步多音轨*/
//=============================================================================
//MidiParserStart:
//分析MIDI文件,为播放做准备。失败返回零,成功返回非零
//-----------------------------------------------------------------------------
Midi_fn(MidiParserStart, int)
(
MidiParser_p pParser
);
//=============================================================================
//MidiParserUpdate:
//更新播放状态到当前时间
//-----------------------------------------------------------------------------
Midi_fn(MidiParserUpdate, int)
(
MidiParser_p pParser,
MidiTime_t dbMilliSeconds,
uint8_t uSilent
);
//=============================================================================
//MidiParserReset:
//重置Midi解析器状态
//-----------------------------------------------------------------------------
Midi_fn(MidiParserReset, void)
(
MidiParser_p pParser
);
//=============================================================================
//MidiParserParseToEnd:
//分析整个文件
//-----------------------------------------------------------------------------
Midi_fn(MidiParserParseToEnd, int)
(
MidiParser_p pParser
);
//=============================================================================
//MidiParserShutdown:
//关闭Midi解析器
//-----------------------------------------------------------------------------
Midi_fn(MidiParserShutdown, void)
(
MidiParser_p pParser
);
/******************************************************************************
注释
1、播放
·用户需要自己编写读取文件、定位文件指针的函数、发送Midi字节的函数(按照本头文
件所描述)的规格进行编写。然后按照说明填写MidiParser_t结构体的成员。
·填写完成后,调用MidiParserStart,初始化播放器。
·随后,不断调用MidiParserUpdate(并提供播放时间)进行Midi文件的播放。播放时间
的数值只能向后增长。
·播放完最后一个音符后,MidiParser_t的成员EndOfFile将会被置1。如需重新播放,请
调用MidiParserReset,然后重新计时并重复上一个步骤。
·不需要播放的时候,请调用MidiParserShutdown完成最后的处理。
·请注意,如果你在Windows使用mciSendCommand播放Midi音乐,你会遇到跳音符的问题。
而本库所使用的算法比Windows的mciSendCommand好,因此没有这个问题。请使用本库。
2、定位到指定播放位置(慎用:可能导致播放乐曲的效果偏离作曲者的设定)
·调用MidiParserReset重置播放状态。
·随后,调用MidiParserUpdate并提供你要定位的时间值(如果设置Silent参数的话,
MidiParserUpdate将不会发送Midi字节)。
3、本程序也可用于Midi文件的解析器——获取每个Midi指令、时间、轨道等信息。
注:本程序对于格式有问题的Midi文件并不能做到很好的解析——要求:
·每个Midi音轨的最后一个音符必须是元数据——音轨结束标识
******************************************************************************/
#endif // !_MIDI_Parser_midifile.c://=============================================================================
//作者:0xAA55
//论坛:http://www.0xaa55.com/
//版权所有(C) 2013-2015 技术宅的结界
//请保留原作者信息,否则视为侵权。
//-----------------------------------------------------------------------------
#include"midifile.h"
#include<stdio.h>
#include<malloc.h>
#include<memory.h>
#pragma pack(push,1)
//MIDI文件头的结构体
typedef struct
{
uint32_t dwFlag; //MThd标识
uint32_t dwRestLen; //剩余部分长度
uint16_t wType; //类型
uint16_t wNbTracks; //音轨数
uint16_t wTicksPerCrotchet;//每四分音符的Tick数
}MIDIHeader, * MIDIHeaderP;
#pragma pack(pop,1)
//MIDI文件中出现过的标识
#define MIDI_MThd 0x6468544D
#define MIDI_MTrk 0x6B72544D
//各种长度的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))
//用于读取文件的宏。要使用它,函数必须要有MidiParser_p pParser参数
#define ErrC(e) {pParser->LastErr=(e);pParser->pfnOnError((e));goto BadRet;}
#define Tell(x) if(!pParser->pfnGetFilePtr((x),pParser->pUserData))\
ErrC(ME_TellFail)
#define Seek(x) if(!pParser->pfnSetFilePtr((x),pParser->pUserData))\
ErrC(ME_SeekFail)
#define Read(x) if(!pParser->pfnReadFile(&(x),sizeof((x)),pParser->pUserData))\
ErrC(ME_ReadFail)
#define Skip(x) {fpos_t __CurPos;Tell(&__CurPos);__CurPos+=(x);Seek(__CurPos);}
//=============================================================================
//ReadDynamicByte:
//读取动态字节,最多读取4字节。返回读取的字节数
//-----------------------------------------------------------------------------
static size_t ReadDynamicByte(MidiParser_p pParser, uint32_t* pOut)
{
size_t BytesRead;//已读取的字节数
uint32_t uVal = 0;
for (BytesRead = 1; BytesRead <= 4; BytesRead++)//最多读取4字节
{
uint8_t Value;
Read(Value);//读取一个字节
uVal = (uVal << 7) | (Value & 0x7F);//新读入的是低位
if (!(Value & 0x80))//如果没有后续字节
break;//就停止读取。
}
*pOut = uVal;
return BytesRead;//返回读取的字节数
BadRet:
*pOut = uVal;
return 0;
}
//=============================================================================
//GetCommandNbParams:
//取得MIDI命令的参数个数
//-----------------------------------------------------------------------------
static uint8_t GetCommandNbParams(uint8_t bEvent)
{
if (bEvent <= 0x7F)return 0;//上个命令的参数
else if (bEvent <= 0x8F)return 2;//两个参数
else if (bEvent <= 0x9F)return 2;//两个参数
else if (bEvent <= 0xAF)return 2;//两个参数
else if (bEvent <= 0xBF)return 2;//两个参数
else if (bEvent <= 0xCF)return 1;//一个参数
else if (bEvent <= 0xDF)return 1;//一个参数
else if (bEvent <= 0xEF)return 2;//两个参数
else if (bEvent <= 0xFF)return 0;//机器码
else return 2;//元数据
}
//=============================================================================
//DefOnError:
//默认的错误提示函数
//-----------------------------------------------------------------------------
static void Midi_cb DefOnError(MidiErrCode_t Err)
{
//什么也不做
}
//=============================================================================
//MidiParserStart:
//分析MIDI文件,为播放做准备。失败返回零,成功返回非零
//-----------------------------------------------------------------------------
Midi_fn(MidiParserStart, int)
(
MidiParser_p pParser
)
{
MIDIHeader midiHeader;
size_t i;
pParser->wType = 0;
pParser->wNbTracks = 0;
pParser->wTicksPerCrotchet = 0;
pParser->pTrackData = NULL;
pParser->Velocity = 0;
pParser->TotalTicks = 0;
pParser->EndOfFile = 0;
pParser->LastErr = ME_OK;
if (!pParser->pfnOnError)
pParser->pfnOnError = DefOnError;
//MIDI文件头就是一个MIDIHeader结构体。
//但是要注意其中的数值都是Big-Endian存储的
//需要进行转换
//读取MIDI文件头
Read(midiHeader);
//检查文件格式
if (midiHeader.dwFlag != MIDI_MThd)//标识必须是"MThd"
ErrC(ME_BadFile);
//转换为Little-Endian
midiHeader.dwRestLen = BSwapD(midiHeader.dwRestLen);
midiHeader.wType = BSwapW(midiHeader.wType);
midiHeader.wNbTracks = BSwapW(midiHeader.wNbTracks);
midiHeader.wTicksPerCrotchet = BSwapW(midiHeader.wTicksPerCrotchet);
//分析器数据
pParser->wType = midiHeader.wType;
pParser->wNbTracks = midiHeader.wNbTracks;
pParser->wTicksPerCrotchet = midiHeader.wTicksPerCrotchet;
//跳转到MIDI音轨的位置
Seek(8 + midiHeader.dwRestLen);
//轨道信息
i = sizeof(TrackData_t) * pParser->wNbTracks;//轨道信息总大小
pParser->pTrackData = (TrackData_p)malloc(i);//分配内存
if (!pParser->pTrackData)
ErrC(ME_OutOfMem);
memset(pParser->pTrackData, 0, i);//清零
//准备读取各个音轨
for (i = 0; i < midiHeader.wNbTracks; i++)
{
uint32_t dwTrackFlag;
uint32_t dwTrackLen;
uint8_t LastEvent = 0;//上一个事件
uint8_t EndOfTrack;//是否读取到了音轨的结束位置
//每个音轨的开头都是一个dwTrackFlag和一个dwTrackLen
//dwTrackFlag的值必须是MIDI_MTrk
//dwTrackLen标记了下一个音轨的位置
fpos_t TrackStartPos;
//轨道标记
Read(dwTrackFlag);
if (dwTrackFlag != MIDI_MTrk)
ErrC(ME_BadFile);
//轨道长度
Read(dwTrackLen);
dwTrackLen = BSwapD(dwTrackLen);//转换Big-Endian
Tell(&TrackStartPos);//记录当前位置
pParser->pTrackData.StartPos = TrackStartPos;
pParser->pTrackData.TrackSize = dwTrackLen;
pParser->pTrackData.NextNotePos = TrackStartPos;
pParser->pTrackData.LastNoteTick = 0;
pParser->pTrackData.EndOfTrack = 0;
//音轨的重要内容是事件数据
for (EndOfTrack = 0; !EndOfTrack;)//循环读取事件
{
uint32_t dwDelay;
uint8_t bEvent;
//每个事件的构成都是:
//延时,事件号,参数
//其中的延时是动态字节,参数大小随事件号而定
//读取延时
if (!ReadDynamicByte(pParser, &dwDelay))
ErrC(ME_BadFile);
//统计当前音轨总时钟数
pParser->pTrackData.TotalTicks += dwDelay;
//读取事件号
Read(bEvent);
//分析事件
if (bEvent <= 0x7F)
{//0到0x7F:和上一个事件的事件号相同,而读取的这个字节就是上一个事件
//号的参数的第一个字节
Skip(GetCommandNbParams(LastEvent) - 1);
bEvent = LastEvent;
}
else if (bEvent <= 0xEF)
{//基本的MIDI命令
Skip(GetCommandNbParams(bEvent));
}
else if (bEvent == 0xF0)
{//0xF0:系统码
uint8_t bSysCode = 0;
do
{
Read(bSysCode);
} while (bSysCode != 0xF7);
}
else if (bEvent == 0xFF)
{//元事件
uint8_t bType, Bytes;
fpos_t CurrentPos;
Read(bType);//元数据类型
Read(Bytes);//元数据字节数
Tell(&CurrentPos);//记录元数据读取的位置
switch (bType)
{
case 0x2F://音轨结束标识
EndOfTrack = 1;
break;
case 0x51://速度
{
uint8_t bVelocity1, bVelocity2, bVelocity3;
//速度是24位整数,一个四分音符的微秒数。
Read(bVelocity1);
Read(bVelocity2);
Read(bVelocity3);
if (!pParser->Velocity)
pParser->Velocity =
((uint32_t)bVelocity3 |
((uint32_t)bVelocity2 << 8) |
((uint32_t)bVelocity1 << 16));
}
break;
default:
break;
}
Seek(CurrentPos + Bytes);//读取完成后正确跳到下一个事件的位置。
}
else//其它事件,未知事件
{
ErrC(ME_BadFile);
}
LastEvent = bEvent;
}//回去继续读取事件。
//如果是异步多音轨的话,记录整个Midi文件的播放时长
if (pParser->wType == MIDIT_MultiAsync)
{
//异步多音轨,同级总时长
pParser->TotalTicks += pParser->pTrackData.TotalTicks;
}
else//否则(单音轨、同步多音轨)以时长最长的音轨为整个音乐的长度。
{
//同步多音轨或单音轨,找到时长最长的音轨。
if (pParser->pTrackData.TotalTicks > pParser->TotalTicks)
pParser->TotalTicks = pParser->pTrackData.TotalTicks;
}
Seek(TrackStartPos + dwTrackLen);//转到下一个音轨
}
return 1;
BadRet:
MidiParserShutdown(pParser);
return 0;
}
//=============================================================================
//MidiParserUpdate:
//更新播放状态到当前时间
//-----------------------------------------------------------------------------
Midi_fn(MidiParserUpdate, int)
(
MidiParser_p pParser,
MidiTime_t dbMilliSeconds,
uint8_t uSilent
)
{
uint16_t CurTrack;
uint32_t CurTick;
if (dbMilliSeconds < 0)
dbMilliSeconds = 0;
//当前时钟数
CurTick = (uint32_t)(dbMilliSeconds * 1000.0 * pParser->wTicksPerCrotchet / pParser->Velocity);
//检查是否播放到了结尾
if (CurTick >= pParser->TotalTicks)
{
pParser->EndOfFile = 1;
}
for (CurTrack = 0; CurTrack < pParser->wNbTracks; CurTrack++)
{
TrackData_p pData = &(pParser->pTrackData);
//每个事件的构成都是:
//延时,事件号,参数
//其中的延时是动态字节,参数大小随事件号而定
//异步多音轨单独处理
if (pParser->wType == MIDIT_MultiAsync)
{
if (CurTick >= pData->TotalTicks)
{
CurTick -= pData->TotalTicks;
continue;
}
}
for (; !pData->EndOfTrack; )
{
uint32_t dwDelay;
uint8_t bEvent;
//定位到当前轨道的下一个音符的位置
Seek(pData->NextNotePos);
//读取延时
if (!ReadDynamicByte(pParser, &dwDelay))
ErrC(ME_BadFile);
if (CurTick >= pData->LastNoteTick + dwDelay)
{
pData->LastNoteTick += dwDelay;
//读取事件号
Read(bEvent);
//分析事件
if (bEvent <= 0x7F)
{ //0到0x7F:和上一个事件的事件号相同,而读取的这个字节就是上一
//个事件号的参数的第一个字节
uint8_t NbParams = GetCommandNbParams(pData->LastCommand);
if (NbParams == 1)
{
pData->LastParam1 = bEvent;
if (!uSilent)
{
pParser->pfnSendMidiMsg(pData->LastNoteTick,
CurTrack, pData->LastCommand, bEvent, 0,
pParser->pUserData);
}
}
else if (NbParams == 2)
{
uint8_t bParam2;
Read(bParam2);
pData->LastParam1 = bEvent;
pData->LastParam2 = bParam2;
if (!uSilent)
{
pParser->pfnSendMidiMsg(pData->LastNoteTick,
CurTrack, pData->LastCommand, bEvent, bParam2,
pParser->pUserData);
}
}
}
else if (bEvent <= 0xEF)
{
uint8_t NbParams = GetCommandNbParams(bEvent);
pData->LastCommand = bEvent;
if (NbParams == 1)
{
uint8_t bParam1;
Read(bParam1);
pData->LastParam1 = bParam1;
if (!uSilent)
{
pParser->pfnSendMidiMsg(pData->LastNoteTick,
CurTrack, bEvent, bParam1, 0, pParser->pUserData);
}
}
else if (NbParams == 2)
{
uint8_t bParam1, bParam2;
Read(bParam1);
Read(bParam2);
pData->LastParam1 = bParam1;
pData->LastParam2 = bParam2;
if (!uSilent)
{
pParser->pfnSendMidiMsg(pData->LastNoteTick,
CurTrack, bEvent, bParam1, bParam2,
pParser->pUserData);
}
}
}
else if (bEvent == 0xF0)
{//0xF0:系统码
uint8_t bSysCode = 0;
do
{
Read(bSysCode);
} while (bSysCode != 0xF7);
}
else if (bEvent == 0xFF)
{//元事件
uint8_t bType, Bytes;
fpos_t CurrentPos;
Read(bType);//元数据类型
Read(Bytes);//元数据字节数
Tell(&CurrentPos);//记录元数据读取的位置
switch (bType)
{
case 0x2F://音轨结束标识
pData->EndOfTrack = 1;
break;
}
Seek(CurrentPos + Bytes);//读取完成后正确跳到下一个事件的位置
}
else//其它事件,未知事件
{
ErrC(ME_BadFile);
}
Tell(&(pData->NextNotePos));
if (pData->NextNotePos >= pData->StartPos + pData->TrackSize)
pData->EndOfTrack = 1;
}
else
break;
}
//异步多音轨单独处理
if (pParser->wType == MIDIT_MultiAsync)
{
//防止同步播放其它音轨
if (CurTick < pData->TotalTicks)
break;
}
}
return 1;
BadRet:
MidiParserShutdown(pParser);
return 0;
}
//=============================================================================
//MidiParserParseToEnd:
//分析整个文件
//-----------------------------------------------------------------------------
Midi_fn(MidiParserParseToEnd, int)
(
MidiParser_p pParser
)
{
return MidiParserUpdate(pParser, (double)(pParser->TotalTicks) * pParser->Velocity / pParser->wTicksPerCrotchet * 0.001, 0);
}
//=============================================================================
//MidiParserReset:
//重置Midi解析器状态
//-----------------------------------------------------------------------------
Midi_fn(MidiParserReset, void)
(
MidiParser_p pParser
)
{
uint16_t CurTrack;
for (CurTrack = 0; CurTrack < pParser->wNbTracks; CurTrack++)
{
pParser->pTrackData.NextNotePos =
pParser->pTrackData.StartPos;
pParser->pTrackData.LastNoteTick = 0;
pParser->pTrackData.EndOfTrack = 0;
}
pParser->EndOfFile = 0;
}
//=============================================================================
//MidiParserShutdown:
//关闭Midi解析器
//-----------------------------------------------------------------------------
Midi_fn(MidiParserShutdown, void)
(
MidiParser_p pParser
)
{
if (pParser)
{
free(pParser->pTrackData);
pParser->pTrackData = NULL;
pParser->wType = 0;
pParser->wNbTracks = 0;
pParser->wTicksPerCrotchet = 0;
pParser->Velocity = 0;
pParser->TotalTicks = 0;
pParser->EndOfFile = 0;
}
}
#undef ErrC
#undef Tell
#undef Seek
#undef Readentry.c:**** Hidden Message *****
有趣的是,这个CMD窗口显示的内容很有节奏感233(它会随着音乐跳动)
这东西除了可以做成Midi播放器以外,还可以做成解析器,用来分析一个Midi音乐(偷谱)
BIN:
SRC:**** Hidden Message *****
BIN的用法:将一个midi文件拖到它的图标上,松开鼠标就开始播放了。
已更新,修复了循环播放破坏内存的问题。 test~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 这是调用哪个音色库播放的?可以改吗? 还是使用了midiOutShortMsg我以为不通过系统就能发声了呢
gcyun 发表于 2015-8-24 10:41
还是使用了midiOutShortMsg我以为不通过系统就能发声了呢
你现实点吧。做是可以做,但是需要翻看各种兼容声卡驱动的源码。我做这个东西的下一步就是为了实现DOS下驱动SoundBlaster16播放Midi。文件解析和Midi命令发送的代码是分开的。此外它还可以做成一个查看Midi文件曲谱的工具(需要自己解析Midi指令,论坛有Midi指令解析的实例,自己看) xp系统下窗口一闪而过。 W·Y 发表于 2015-8-24 22:22
xp系统下窗口一闪而过。
要么你没提供Midi文件,要么你提供的文件是错误的。 宇宙卐之王 发表于 2015-8-24 10:18
这是调用哪个音色库播放的?可以改吗?
微软波表,也就是MIDI_MAPPER 0xAA55 发表于 2015-8-24 13:22
你现实点吧。做是可以做,但是需要翻看各种兼容声卡驱动的源码。我做这个东西的下一步就是为了实现DOS下 ...
是你压缩包下自带的midi文件 W·Y 发表于 2015-8-28 05:36
是你压缩包下自带的midi文件
忘了设置xp兼容了。你自己用VS编译一下(平台工具集选XP)就可以运行了。 0xAA55 发表于 2015-8-28 17:55
忘了设置xp兼容了。你自己用VS编译一下(平台工具集选XP)就可以运行了。 ...
原来如此。。 good mass
了解一下Midi文件播放 谢谢分享,来一个试试。。。 新手学习,感恩 剛好想研究midi
參考一下,謝謝 马一个 期待一下自己写出一份midi文件自动转midi massage的程序 支持 !! 棒棒哒,0xAA55写的详细又好,人最好啦