导读:本期聚焦于小伙伴创作的《C++如何读取和解析Midi格式音乐文件?变长数量VLQ字节解码怎么实现?》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C++如何读取和解析Midi格式音乐文件?变长数量VLQ字节解码怎么实现?》有用,将其分享出去将是对创作者最好的鼓励。

Midi文件采用二进制格式存储音乐相关信息,和常见的音频文件不同,它不直接存储声音波形,而是存储音符、音色、节奏等指令信息。Midi文件中大量使用变长数量VLQ编码来节省存储空间,这种编码方式可以用1到4个字节表示不同范围的数值,是解析Midi文件必须掌握的核心知识点。

C++如何读取和解析Midi格式音乐文件?变长数量VLQ字节解码怎么实现?

Midi文件基础结构

Midi文件整体由文件头块和若干个轨道块组成,每个块都有固定的格式:

  • 文件头块:固定以MThd开头,后面跟着4字节的长度值(固定为6)和3个16位数据,分别是文件格式、轨道数量、时间单位
  • 轨道块:固定以MTrk开头,后面跟着4字节的长度值表示轨道数据的总字节数,剩下的就是事件数据,事件数据中的时间偏移量全部采用VLQ编码

VLQ编码规则

VLQ(Variable Length Quantity)变长数量编码的核心规则如下:

  • 每个字节的最高位(第7位)是标志位,1表示后面还有后续字节,0表示这是最后一个字节
  • 每个字节的低7位(第0到6位)是有效数据位
  • 解码时需要将每个字节的低7位按顺序拼接,得到最终的数值

比如数值0x3F(十进制63)只需要1个字节:0x3F(最高位0,低7位是00111111)。数值0x80(十进制128)需要2个字节:第一个字节0x81(最高位1,低7位是0000001),第二个字节0x00(最高位0,低7位是0000000),拼接后得到0000001 0000000,即0x80。

C++实现VLQ解码

VLQ解码的步骤很清晰:从输入流中逐个读取字节,每次取低7位左移对应位数后累加,直到读取到最高位为0的字节为止。下面是完整的实现代码:

#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>

// 从文件流中读取一个VLQ编码的数值,返回读取的字节数,结果存到result中
// 如果读取失败(比如到文件末尾还没有结束)返回-1
int readVLQ(std::ifstream& file, uint32_t& result) {
    result = 0;
    int byteCount = 0;
    uint8_t byte;
    do {
        // 读取一个字节
        if (!file.read(reinterpret_cast<char*>(&byte), sizeof(byte))) {
            return -1;
        }
        byteCount++;
        // 取低7位,左移7*(byteCount-1)位后累加
        result = (result << 7) | (byte & 0x7F);
    } while (byte & 0x80); // 最高位为1则继续读取
    return byteCount;
}

结合Midi文件读取的完整示例

下面代码实现读取Midi文件头部,并解析第一个轨道中的第一个VLQ时间偏移量:

// 读取Midi文件头
bool readMidiHeader(std::ifstream& file, uint16_t& format, uint16_t& trackCount, uint16_t& division) {
    char headerTag[4];
    // 读取文件头标识MThd
    if (!file.read(headerTag, 4) || strncmp(headerTag, "MThd", 4) != 0) {
        std::cerr << "不是合法的Midi文件" << std::endl;
        return false;
    }
    // 读取头部长度,Midi文件采用大端字节序
    uint32_t headerLen;
    file.read(reinterpret_cast<char*>(&headerLen), 4);
    headerLen = __builtin_bswap32(headerLen); // 大端转小端,不同编译器可能有不同的大端转换函数
    if (headerLen != 6) {
        std::cerr << "Midi文件头长度异常" << std::endl;
        return false;
    }
    // 读取文件格式
    file.read(reinterpret_cast<char*>(&format), 2);
    format = __builtin_bswap16(format);
    // 读取轨道数量
    file.read(reinterpret_cast<char*>(&trackCount), 2);
    trackCount = __builtin_bswap16(trackCount);
    // 读取时间单位
    file.read(reinterpret_cast<char*>(&division), 2);
    division = __builtin_bswap16(division);
    return true;
}

int main() {
    std::ifstream midiFile("test.mid", std::ios::binary);
    if (!midiFile.is_open()) {
        std::cerr << "无法打开Midi文件" << std::endl;
        return 1;
    }
    uint16_t format, trackCount, division;
    if (!readMidiHeader(midiFile, format, trackCount, division)) {
        return 1;
    }
    std::cout << "Midi文件格式: " << format << std::endl;
    std::cout << "轨道数量: " << trackCount << std::endl;
    std::cout << "时间单位: " << division << std::endl;

    // 读取第一个轨道的标识和长度
    char trackTag[4];
    if (!midiFile.read(trackTag, 4) || strncmp(trackTag, "MTrk", 4) != 0) {
        std::cerr << "轨道格式异常" << std::endl;
        return 1;
    }
    uint32_t trackLen;
    midiFile.read(reinterpret_cast<char*>(&trackLen), 4);
    trackLen = __builtin_bswap32(trackLen);

    // 解析轨道中的第一个VLQ时间偏移量
    uint32_t deltaTime;
    int vlqBytes = readVLQ(midiFile, deltaTime);
    if (vlqBytes == -1) {
        std::cerr << "VLQ解码失败" << std::endl;
        return 1;
    }
    std::cout << "第一个事件的时间偏移量: " << deltaTime << std::endl;
    std::cout << "该VLQ编码占用的字节数: " << vlqBytes << std::endl;

    midiFile.close();
    return 0;
}

注意事项

  • Midi文件所有多字节数值都采用大端字节序,读取后需要转换成本地字节序再使用
  • VLQ编码最多使用4个字节,解码时可以增加校验逻辑,避免异常文件导致的无限循环
  • 轨道数据中的事件类型很多,读取VLQ时间偏移量后还需要根据后续字节判断事件类型,再做对应的解析
VLQ解码是Midi文件解析的基础,掌握这个逻辑后,再扩展其他事件类型的解析就会轻松很多,开发者可以在此基础上逐步完善整个Midi文件解析器的功能。

C++Midi文件解析VLQ解码变长数量字节修改时间:2026-06-30 12:06:38

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。