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语言版本2 - Joyber 的博客

<?php
class MidiDecoder
{
    public string $raw;
    public int $ptr = 0;
    public array $header = [];
    public array $tracks = [];

    // 新增:全局时间参数
    public int $tpqn;
    public float $microsecondsPerQuarter = 500000; // 默认 120BPM: 500000微秒/四分音符
    public float $bpm = 120.0;
    public array $tempoEvents = []; // 保存所有tempo变更点 delta => microseconds
    public array $allTrackEvents = []; // 合并全部事件,用于时间计算

    public function loadFile(string $filePath): bool
    {
        if (!is_file($filePath)) return false;
        $this->raw = file_get_contents($filePath);
        $this->ptr = 0;
        $this->tempoEvents = [];
        $this->allTrackEvents = [];
        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变长整数解码
    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;
    }

    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
        ];
        $this->tpqn = $tpqn;

        for ($t = 0; $t < $trackCount; $t++) {
            $this->parseTrack($t);
        }
        return true;
    }

    protected function parseTrack(int $trackIndex)
    {
        $id = substr($this->raw, $this->ptr, 4);
        $this->ptr +=4;
        $trackLen = $this->readBig(4);
        $trackEnd = $this->ptr + $trackLen;

        $events = [];
        $runningStatus = 0;
        $tickTotal = 0;

        while ($this->ptr < $trackEnd) {
            $delta = $this->readVLQ();
            $tickTotal += $delta;

            $byte = ord($this->raw[$this->ptr]);
            if (($byte & 0x80) === 0) {
                $status = $runningStatus;
            } else {
                $status = $byte;
                $runningStatus = $byte;
                $this->ptr++;
            }

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

            $ev = [
                'delta' => $delta,
                'tick' => $tickTotal,
                'track' => $trackIndex
            ];

            if ($status === 0xFF) {
                $metaType = ord($this->raw[$this->ptr++]);
                $metaLen = $this->readVLQ();
                $metaData = substr($this->raw, $this->ptr, $metaLen);
                $this->ptr += $metaLen;
                $ev['type'] = 'meta';
                $ev['meta_type'] = $metaType;
                $ev['data'] = $metaData;

                // BPM Tempo元事件 FF 51 03
                if ($metaType === 0x51 && $metaLen === 3) {
                    $ms = unpack('N', chr(0) . $metaData);
                    $this->tempoEvents[$tickTotal] = $ms[1];
                }
            } elseif ($status === 0xF0 || $status === 0xF7) {
                $sysexLen = $this->readVLQ();
                $data = substr($this->raw, $this->ptr, $sysexLen);
                $this->ptr += $sysexLen;
                $ev['type'] = 'sysex';
                $ev['status'] = $status;
                $ev['data'] = $data;
            } else {
                switch ($type) {
                    case 0x9:
                        $note = ord($this->raw[$this->ptr++]);
                        $vel = ord($this->raw[$this->ptr++]);
                        $ev['type'] = 'note_on';
                        $ev['ch'] = $ch;
                        $ev['note'] = $note;
                        $ev['vel'] = $vel;
                        break;
                    case 0x8:
                        $note = ord($this->raw[$this->ptr++]);
                        $vel = ord($this->raw[$this->ptr++]);
                        $ev['type'] = 'note_off';
                        $ev['ch'] = $ch;
                        $ev['note'] = $note;
                        $ev['vel'] = $vel;
                        break;
                    case 0xB:
                        $ccNum = ord($this->raw[$this->ptr++]);
                        $val = ord($this->raw[$this->ptr++]);
                        $ev['type'] = 'cc';
                        $ev['ch'] = $ch;
                        $ev['cc'] = $ccNum;
                        $ev['value'] = $val;
                        break;
                    case 0xC:
                        $prog = ord($this->raw[$this->ptr++]);
                        $ev['type'] = 'program';
                        $ev['ch'] = $ch;
                        $ev['program'] = $prog;
                        break;
                    case 0xE:
                        $b1 = ord($this->raw[$this->ptr++]);
                        $b2 = ord($this->raw[$this->ptr++]);
                        $pitch = ($b2 <<7) | $b1;
                        $ev['type'] = 'pitch';
                        $ev['ch'] = $ch;
                        $ev['value'] = $pitch;
                        break;
                    default:
                        $ev['type'] = 'unknown';
                        $ev['status'] = $status;
                        break;
                }
            }
            $events[] = $ev;
            $this->allTrackEvents[] = $ev;
        }
        $this->tracks[] = $events;
    }

    /**
     * 根据tick计算真实时间(秒),支持多段BPM变速
     */
    public function tickToSeconds(int $targetTick): float
    {
        ksort($this->tempoEvents);
        $lastTick = 0;
        $lastMicro = 500000;
        $totalSec = 0.0;

        foreach ($this->tempoEvents as $tick => $micro) {
            if ($targetTick <= $lastTick) break;
            $useTick = min($tick, $targetTick);
            $deltaTick = $useTick - $lastTick;
            $totalSec += $deltaTick * $lastMicro / $this->tpqn / 1000000;
            $lastTick = $tick;
            $lastMicro = $micro;
        }
        // 剩余tick
        if ($targetTick > $lastTick) {
            $deltaTick = $targetTick - $lastTick;
            $totalSec += $deltaTick * $lastMicro / $this->tpqn / 1000000;
        }
        return $totalSec;
    }

    // 打印全部事件,附带时间,增加过滤打击乐通道ch=9
    public function dumpWithTime(bool $onlyDrumChannel = false)
    {
        echo "==== MIDI HEADER ====\n";
        print_r($this->header);
        echo "\n==== TPQN: {$this->tpqn} ====\n";

        foreach ($this->tracks as $tkIdx => $events) {
            echo "\n--- Track #$tkIdx , total events: ".count($events)."\n";
            foreach ($events as $e) {
                $sec = $this->tickToSeconds($e['tick']);
                if ($onlyDrumChannel) {
                    if (!isset($e['ch']) || $e['ch'] !== 9) continue;
                }
                echo sprintf("tick:%6d | time:%.3fs | ", $e['tick'], $sec);
                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';
if ($decoder->loadFile($midFile)) {
    // 第二个参数true:只输出打击乐通道 ch=9(电鼓音符)
    $decoder->dumpWithTime(true);
} else {
    echo "解析失败,文件不存在或者不是合法MIDI";
}
?>

核心逻辑说明

  1. 收集所有FF 51 03 tempo事件,记录在哪个tick点变速
  2. tickToSeconds() 分段累加时间:
    [
    \text{秒数} = \frac{\text{tick数量} \times \text{每四分音符微秒数}}{\text{TPQN}} \div 1000000
    ]

    • microsecondsPerQuarter:一个四分音符占用多少微秒
    • BPM = 60000000 / microsecondsPerQuarter
    • 例:500000微秒/四分音符 → BPM=120
  3. 新增开关 $onlyDrumChannel = true,直接过滤只输出鼓通道ch=9,专门用于DTX/手卷鼓MIDI分析

用法

  • 只看鼓点并附带时间:$decoder->dumpWithTime(true);
  • 看全部音轨所有事件:$decoder->dumpWithTime(false);

扩展可选

如果你后续需要,我可以再加:

  1. 导出鼓点数组 [['time'=>0.5, 'note'=>36, 'vel'=>90],...] 直接存入数据库
  2. 过滤掉 vel=0 的NoteOn(MIDI标准里vel=0等价NoteOff,很多谱会这么写)
  3. 输出JSON

标签: none

添加新评论