Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 339

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 392

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 394
AI编写的MIDI解码程序,PHP语言版本1 - Joyber 的博客

PHP MIDI 文件解码器(读取 .mid,解析MThd/MTrk、VLQ、事件)

说明:PHP 7.4+ 可用,仅解析 MIDI 1.0 标准文件,支持读取文件头、多音轨、VLQ变长整数解码、MIDI事件、Meta事件。
不做音频渲染,只解析MIDI二进制指令,刚好适配你电鼓MIDI分析需求。
<?php
class MidiDecoder
{
    public string $raw;
    public int $ptr = 0;
    public array $header = [];
    public array $tracks = [];

    public function loadFile(string $filePath): bool
    {
        if (!is_file($filePath)) return false;
        $this->raw = file_get_contents($filePath);
        $this->ptr = 0;
        return $this->parseHeader();
    }

    // 读取N字节,大端
    protected function readBig(int $bytes): int
    {
        $bin = substr($this->raw, $this->ptr, $bytes);
        $this->ptr += $bytes;
        $unpack = unpack('N', str_pad($bin, 4, chr(0), STR_PAD_LEFT));
        return $unpack[1];
    }

    // VLQ 变长整数解码(MIDI核心编码)
    protected function readVLQ(): int
    {
        $val = 0;
        while (true) {
            $b = ord($this->raw[$this->ptr++]);
            $val = ($val << 7) | ($b & 0x7F);
            if (($b & 0x80) === 0) break;
        }
        return $val;
    }

    // 解析文件头 MThd
    protected function parseHeader(): bool
    {
        $id = substr($this->raw, $this->ptr, 4);
        $this->ptr += 4;
        if ($id !== 'MThd') return false;

        $len = $this->readBig(4);
        $fmt = $this->readBig(2);
        $trackCount = $this->readBig(2);
        $tpqn = $this->readBig(2);

        $this->header = [
            'format' => $fmt,
            'track_count' => $trackCount,
            'tpqn' => $tpqn
        ];

        // 解析全部音轨 MTrk
        for ($t = 0; $t < $trackCount; $t++) {
            $this->parseTrack();
        }
        return true;
    }

    // 解析单条MTrk音轨
    protected function parseTrack()
    {
        $id = substr($this->raw, $this->ptr, 4);
        $this->ptr +=4;
        $trackLen = $this->readBig(4);
        $trackEnd = $this->ptr + $trackLen;

        $events = [];
        $runningStatus = 0;

        while ($this->ptr < $trackEnd) {
            $delta = $this->readVLQ();
            $byte = ord($this->raw[$this->ptr]);

            // Running Status复用
            if (($byte & 0x80) === 0) {
                $status = $runningStatus;
            } else {
                $status = $byte;
                $runningStatus = $byte;
                $this->ptr++;
            }

            $type = ($status >> 4) & 0x0F;
            $ch = $status & 0x0F;

            if ($status === 0xFF) {
                // Meta事件 FF
                $metaType = ord($this->raw[$this->ptr++]);
                $metaLen = $this->readVLQ();
                $metaData = substr($this->raw, $this->ptr, $metaLen);
                $this->ptr += $metaLen;
                $events[] = [
                    'delta' => $delta,
                    'type' => 'meta',
                    'meta_type' => $metaType,
                    'data' => $metaData
                ];
            } elseif ($status === 0xF0 || $status === 0xF7) {
                // SysEx系统独占事件
                $sysexLen = $this->readVLQ();
                $data = substr($this->raw, $this->ptr, $sysexLen);
                $this->ptr += $sysexLen;
                $events[] = [
                    'delta' => $delta,
                    'type' => 'sysex',
                    'status' => $status,
                    'data' => $data
                ];
            } else {
                // MIDI通道事件 NoteOn / NoteOff / CC / ProgramChange
                switch ($type) {
                    case 0x9: // Note On
                        $note = ord($this->raw[$this->ptr++]);
                        $vel = ord($this->raw[$this->ptr++]);
                        $events[] = [
                            'delta' => $delta,
                            'type' => 'note_on',
                            'ch' => $ch,
                            'note' => $note,
                            'vel' => $vel
                        ];
                        break;
                    case 0x8: // Note Off
                        $note = ord($this->raw[$this->ptr++]);
                        $vel = ord($this->raw[$this->ptr++]);
                        $events[] = [
                            'delta' => $delta,
                            'type' => 'note_off',
                            'ch' => $ch,
                            'note' => $note,
                            'vel' => $vel
                        ];
                        break;
                    case 0xB: // CC控制器
                        $ccNum = ord($this->raw[$this->ptr++]);
                        $val = ord($this->raw[$this->ptr++]);
                        $events[] = [
                            'delta' => $delta,
                            'type' => 'cc',
                            'ch' => $ch,
                            'cc' => $ccNum,
                            'value' => $val
                        ];
                        break;
                    case 0xC: // Program Change 音色切换
                        $prog = ord($this->raw[$this->ptr++]);
                        $events[] = [
                            'delta' => $delta,
                            'type' => 'program',
                            'ch' => $ch,
                            'program' => $prog
                        ];
                        break;
                    case 0xE: // 弯音
                        $b1 = ord($this->raw[$this->ptr++]);
                        $b2 = ord($this->raw[$this->ptr++]);
                        $pitch = ($b2 <<7) | $b1;
                        $events[] = [
                            'delta' => $delta,
                            'type' => 'pitch',
                            'ch' => $ch,
                            'value' => $pitch
                        ];
                        break;
                    default:
                        // 其他未实现事件,跳过字节
                        $events[] = ['delta'=>$delta, 'type'=>'unknown','status'=>$status];
                        break;
                }
            }
        }
        $this->tracks[] = $events;
    }

    // 打印解析结果
    public function dump()
    {
        echo "==== MIDI HEADER ====\n";
        print_r($this->header);
        echo "\n==== TRACK COUNT: {$this->header['track_count']} ====\n";

        foreach ($this->tracks as $tkIdx => $events) {
            echo "\n--- Track #$tkIdx , total events: ".count($events)."\n";
            foreach ($events as $e) {
                echo "delta:{$e['delta']} | ";
                echo match($e['type']) {
                    'note_on' => "NOTE_ON ch{$e['ch']} note{$e['note']} vel{$e['vel']}",
                    'note_off' => "NOTE_OFF ch{$e['ch']} note{$e['note']} vel{$e['vel']}",
                    'cc' => "CC ch{$e['ch']} cc{$e['cc']} val{$e['value']}",
                    'program' => "PROGRAM ch{$e['ch']} prog{$e['program']}",
                    'meta' => "META type{$e['meta_type']} len=".strlen($e['data']),
                    'sysex' => "SYSEX",
                    default => "UNKNOWN"
                };
                echo "\n";
            }
        }
    }
}

// ========= 使用示例 =========
$decoder = new MidiDecoder();
$midFile = './test.mid'; // 替换成你的midi文件路径
if ($decoder->loadFile($midFile)) {
    $decoder->dump();
} else {
    echo "解析失败,文件不存在或者不是合法MIDI";
}
?>

功能说明

  1. VLQ 变长整数解码(MIDI最核心部分)
  2. 解析 MThd 文件头:MIDI格式、轨道数量、TPQN
  3. 解析 MTrk 音轨块,支持 Running Status 运行状态压缩
  4. 支持事件类型:

    • NoteOn / NoteOff(鼓音符,通道10打击乐)
    • CC控制器、ProgramChange音色切换、弯音
    • Meta元事件(BPM、拍号、文本、轨道结束标记)
    • SysEx系统消息
  5. 输出每条事件的 delta tick

针对你的电鼓MIDI补充

MIDI打击乐固定在 通道9(程序里ch=9,对应MIDI文档叫通道10)
鼓键映射参考:
36=底鼓Kick,38=军鼓Snare,42=闭踩镲HH Closed,46开踩镲,49叮叮镲Ride,51吊镲Crash

使用要点

  1. 保存为 midi_decode.php,把 test.mid 放到同目录
  2. 命令行执行:php midi_decode.php
  3. 大MIDI文件可以把dump打印关掉,改成存入数组做后续分析

可扩展方向(你可以按需加)

  • 把tick换算成真实时间(依靠meta事件里的BPM)
  • 过滤只输出打击乐通道ch=9事件(只提取鼓点,DTX谱分析)
  • 导出为JSON,存入数据库

标签: none

添加新评论