AI编写的MIDI解码程序,PHP语言版本2
<?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";
}
?>核心逻辑说明
- 收集所有
FF 51 03tempo事件,记录在哪个tick点变速 tickToSeconds()分段累加时间:
[
\text{秒数} = \frac{\text{tick数量} \times \text{每四分音符微秒数}}{\text{TPQN}} \div 1000000
]- microsecondsPerQuarter:一个四分音符占用多少微秒
- BPM =
60000000 / microsecondsPerQuarter - 例:500000微秒/四分音符 → BPM=120
- 新增开关
$onlyDrumChannel = true,直接过滤只输出鼓通道ch=9,专门用于DTX/手卷鼓MIDI分析
用法
- 只看鼓点并附带时间:
$decoder->dumpWithTime(true); - 看全部音轨所有事件:
$decoder->dumpWithTime(false);
扩展可选
如果你后续需要,我可以再加:
- 导出鼓点数组
[['time'=>0.5, 'note'=>36, 'vel'=>90],...]直接存入数据库 - 过滤掉 vel=0 的NoteOn(MIDI标准里vel=0等价NoteOff,很多谱会这么写)
- 输出JSON
版权属于:Joyber
本文链接:https://blog.qqvbc.com/default/AI-MIDI-PHP-2.html
转载时须注明出处及本声明