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
Joyber 发布的文章 - Joyber 的博客

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,存入数据库

MIDI 文件基础:记录什么 + 编码原理

MIDI不是音频!不存声音波形,只存「演奏指令」,这是最核心区别。

一、MIDI文件里面记录哪些信息

1. 核心事件(MIDI Event)

  1. 音符事件(Note On / Note Off)

    • Note On:按下琴键/鼓垫

      • 参数:通道(0~15)、音号(0-127,代表音高)、力度(0-127)
      • 注:力度=0的Note On等价于Note Off
    • Note Off:松开按键

      • 参数:通道、音号、释放力度
  2. 控制器事件 CC(Control Change)

    • 通道音量、声像、踩镲控制、弯音轮、表情、延音踏板等。
    • 例如CC64=延音踏板,CC10=声像,CC7=通道音量
  3. 程序改变 Program Change

    • 切换音色:比如通道1改成钢琴,通道10默认是打击乐通道(鼓组,这就是电鼓DTX用到的)
  4. 弯音 Pitch Wheel:滑音、音高偏移
  5. 通道触后、音符触后:键盘压力感应(手卷鼓一般不用)

2. 元事件(Meta Event,只存在MIDI文件,不发给硬件)

  • 节拍、BPM(速度)、拍号(4/4)、调号
  • 音轨名称、歌曲标题、版权、歌词文本
  • 时间基准(TPQN,每四分音符多少tick):决定时间分辨率,常见480tick/四分音符

3. 时间信息:Tick + Delta-time(增量时间)

MIDI不是记录绝对时间,用Delta-Time(增量tick):

两个事件之间间隔多少tick,而不是“第2秒按下音符”
tick → 根据BPM换算成真实秒数。

4. 文件结构信息

  • 音轨数量(Track):多轨MIDI可以鼓一轨、钢琴一轨分开。
  • 格式:MIDI0(单轨) / MIDI1(多轨同步,最常用) / MIDI2(多轨异步很少用)

二、MIDI文件编码(标准:MIDI 1.0,.mid二进制文件)

MIDI文件由文件头块 + 多个音轨块组成,全部是大端(Big-Endian)二进制存储。

1. 文件头块(Header Chunk)

固定结构:

4字节块标识:MThd
4字节块长度:固定00 00 00 06
2字节:MIDI格式(0/1/2)
2字节:音轨数量
2字节:时间基准 TPQN

示例十六进制:4D 54 68 64 00 00 00 06 00 01 00 02 01 E0

  • 4D546864 = ASCII MThd
  • 后面6字节:格式=1,2条音轨,TPQN=480

2. 音轨块(Track Chunk,每条音轨一个MTrk)

4字节标识:MTrk
4字节:本音轨所有事件的总字节长度
然后:一连串【Delta-Time + MIDI事件】

4D 54 72 6B = ASCII MTrk

重点:Delta-Time 编码 → 可变长度变长整数(VLQ,Variable Length Quantity)

这是MIDI最有特点的编码!

  • 1~4字节,最高位bit7作为标记
  • bit7=1:后面还有下一个字节;bit7=0:这是最后一个字节
  • 每个字节只用低7bit存数值。

举例子:
数字 0x80 (128)
拆成7bit分段:01 0000000
编码:81 00

0x81:最高位=1,低7bit=1;0x00最高位0,低7bit=0

规则:从高位往低位,每7bit一组,除最后一组外,所有字节最高位置1。

MIDI事件编码(状态字节 + 数据字节)

状态字节高4bit=事件类型,低4bit=MIDI通道(0~15)

  • 9x = Note On(x=通道),后面2字节:音号、力度
  • 8x = Note Off,后面2字节:音号、释放力度
  • Ax = 音符触后
  • Bx = CC控制器
  • Cx = Program Change(只1个数据字节,音色号)
  • Dx = 通道触后
  • Ex = 弯音(2字节)
运行状态(Running Status)优化:连续同类型同通道事件,可以省略状态字节,只写数据,减少文件体积,MIDI文件常用。

Meta元事件(FF开头)

状态字节固定0xFF,后面1字节元事件类型,1字节VLQ长度,再是内容。
例:FF 51 03 07 A1 20 → 设置BPM(微秒/四分音符)

三、简化编码流程(写一个MIDI文件的步骤)

  1. 写Header块:MThd + 长度 + 格式、轨道数、TPQN
  2. 对每一条音轨:

    1. 写入MTrk标识 + 预留长度(写完事件后回填)
    2. 遍历所有演奏事件,按时间先后排序
    3. 计算两个事件之间tick差值,用VLQ编码Delta-Time
    4. 写入事件状态+数据(使用running status压缩)
  3. 音轨结束事件:FF 2F 00(元事件:轨道结束)
  4. 回填MTrk块的长度字段

四、补充,和你电鼓相关

MIDI通道10(MIDI协议里是通道9,索引从0开始)固定打击乐通道:
每个音号对应不同鼓件:36底鼓、38军鼓、42闭踩镲、46开踩镲、49叮叮镲等。
所以DTX游戏、手卷鼓输出MIDI,就是输出Note On/Note Off到通道10。

五、举最小样例(极简MIDI示意)

四分音符TPQN=480,一个C4音符按下再松开。

  1. DeltaTime=0,NoteOn 通道1,音号60,力度100
  2. DeltaTime=480,NoteOff 通道1,音号60,力度80

Yii2 内置全部常用缓存依赖

1. FileDependency 文件依赖

yii\caching\FileDependency
原理:校验文件最后修改时间,文件改动则缓存失效。
适用:配置文件、静态模板、json元数据。

use yii\caching\FileDependency;

$query->cache(60, new FileDependency([
    'fileName' => '@config/menu.json'
]))

2. ExpressionDependency PHP表达式依赖

yii\caching\ExpressionDependency
原理:每次读取缓存,执行一段PHP表达式;返回值变化,缓存失效。
适用:自定义逻辑,比如读取Redis里的版本号、全局配置版本。

use yii\caching\ExpressionDependency;

$query->cache(0, new ExpressionDependency([
    'expression' => 'Yii::$app->redis->get("global_version")'
]));

3. TagDependency 标签依赖(业务开发最推荐)

yii\caching\TagDependency
原理:缓存打上标签;不需要自动检测,代码手动根据标签批量清除缓存。

你当前统计count场景非常适合这个,比DbDependency少一条额外SQL。

示例:

use yii\caching\TagDependency;

// 写入缓存,绑定标签 estate-{$merchantId}
$count = PropertyCommunity::find()
    ->where(['estate_id' => $merchantId])
    ->cache(0, new TagDependency([
        'tags' => ["estate:{$merchantId}"]
    ]))
    ->count();

// 新增/修改/删除数据后,一行代码清空该标签下所有缓存
TagDependency::invalidate(Yii::$app->cache, ["estate:{$merchantId}"]);

优点:读缓存时不会额外执行SQL;缺点:数据变更需要手动调用 invalidate。

4. ChainedDependency 组合依赖

yii\caching\ChainedDependency
原理:组合多个依赖,任意一个依赖触发变更 → 缓存失效。

use yii\caching\ChainedDependency;

new ChainedDependency([
    'dependencies' => [
        new FileDependency(['fileName'=>'@config/conf.json']),
        new TagDependency(['tags'=>['estate:1001']])
    ]
]);

5. DbQueryDependency(DbDependency升级版)

yii\caching\DbQueryDependency
接收 QueryInterface 对象,而不是原生SQL字符串,支持参数绑定,防SQL注入。

use yii\caching\DbQueryDependency;
use yii\db\Query;

new DbQueryDependency([
    'query' => (new Query())
        ->select('MAX(updated_at)')
        ->from('property_community')
        ->where(['estate_id' => $merchantId])
]);

选型对比(结合你的项目)

依赖读缓存额外SQL触发方式适合场景
DbDependency✅ 每次读执行1条SQL自动检测SQL结果低并发,不想手动清缓存
DbQueryDependency✅ 同上自动检测Query结果同上,推荐替代DbDependency
TagDependency❌ 无额外SQL代码手动清除标签高并发统计count,优先选这个
FileDependency❌文件修改时间配置文件
ExpressionDependency❌php表达式返回值Redis版本号、自定义变量
ChainedDependency看子依赖任一子依赖变更多条件组合

重点建议

你现在 PropertyCommunity count统计:优先 TagDependency。
理由:
DbDependency每次访问count都会额外跑一条MAX(updated_at),并发上来会增加数据库压力;TagDependency在新增/删除数据时手动 invalidate 标签,读的时候没有额外开销。

2010年后出生群体(10后)成年进入社会的压力与出路

现在2010年出生的人,2026年是16岁,再过几年会陆续成年、走入社会。他们所处的时代,和80、90、00后完全不一样:增量时代基本结束,是存量竞争、高度数字化的时代,压力结构发生了很大变化。

一、他们将要面对的核心压力

1. 就业:岗位门槛抬升,内卷从大学前置到基础教育

10后面临的是高等教育普及化,本科、研究生的人数持续增多,学历不再是“保底通行证”。很多基础岗位不再只看文凭,更看重实操能力、项目经验、复合技能。
一方面传统稳定岗位竞争极度激烈;另一方面大量重复性岗位会被AI、自动化替代,简单流水线、基础文职、基础客服、初级图文处理这类岗位会持续缩减。
同时,行业迭代速度极快,可能上学时热门的专业,毕业时行业已经收缩,需要持续学习,不能靠“一技吃一辈子”。

2. 生活成本与资产压力

房价、大城市生活成本依然偏高,但收入增长速度放缓。
过去几代人可以靠买房实现资产增值的时代不再,房产不再是稳赚的投资。他们大概率很难依靠单一工资快速完成买房、结婚、育儿的人生任务。
并且社会默认的人生时间表(几岁结婚、生子)正在松动,但家庭和传统观念仍会带来隐性期待压力。

3. 精神与心理压力

10后是原生互联网世代,从小浸泡短视频、社交网络,习惯看到别人“高光生活”,更容易产生比较焦虑。
信息过载,真假信息混杂,辨别信息、管理注意力会变成一种刚需能力。同时,竞争从小就开始:课外培训、特长、升学竞争,长期高压,心理耗损会更明显。
另外,社会不确定性变强,经济、行业变化快,很难获得一份“一眼望到头”的稳定工作,不确定性焦虑是这一代人很突出的情绪。

4. 家庭支持的分化

家庭条件差距会被放大:能提供教育资源、试错资金、人脉支持的家庭,孩子试错空间更大;普通家庭的孩子,容错成本很高,一次选择失误代价更大。
养老压力也会传导到他们身上:老龄化加剧,未来赡养老人的负担会落在这一代人身上。

二、出路:不再是单一路线,而是多赛道选择

过去的主流路径:读书→好大学→进大厂/体制→买房成家,这套路径对10后不再是最优解,甚至不再是通用解。出路会分成几类:

1. 专业技术路线:深耕硬核能力,做AI难以替代的岗位

AI擅长标准化、重复性工作;但需要复杂人际判断、动手实操、复杂临场决策、深度创意的岗位更安全。

  • 硬核工程:医疗、精密制造、能源、土木、生物医药、先进设备维修;
  • 高端服务:临床医护、心理咨询、养老康复、高端教育;
  • 创意类:原创内容、剧本、产品设计,重点是原创审美与洞察,而不是简单排版生成。
    核心思路:不要只学知识,要学解决真实问题的能力。证书只是背书,解决问题的案例、作品集才是硬通货。

2. 稳定公共服务路线:体制、公共事业,但要理性看待

公务员、事业单位、基层公共岗位依然是抗风险选择,但竞争会持续激烈,收入不会暴富,优势是稳定性。
适合偏好低风险、追求安稳生活的人,但不要抱着“一劳永逸”的期待,体制内同样需要持续考核与学习。

3. 个体小商业/自由职业、技能个体户

数字化降低了个人创业门槛,不需要开实体店,靠单项技能就能接单:比如定制设计、剪辑、技术运维、手工、本地生活服务、知识咨询。
特点:上限高、自由度高,但不稳定,需要懂营销、财务、风险控制。适合擅长自我管理、抗压力强的人。
未来的趋势:很多人不再隶属于某一家公司,而是以独立个体,对接多个项目。

4. 跨区域选择:不扎堆一线城市

不必全部挤在一线大城市。新一线、地级市、县域,机会结构不一样:大城市适合技术、创意、互联网;中小城市适合本地服务业、公共岗位、轻资产小生意。
可以灵活切换:年轻时去大城市积累经验,后期回压力更低的城市定居,拆分“搞事业”和“安家”两个目标。

5. 底层生存能力(最重要)

相比于学历,下面几项能力会决定他们长期能不能走下去:

  1. 终身学习能力:行业3-5年一变,学会快速自学新工具,学会使用AI作为效率工具,而不是害怕AI;
  2. 财务基础素养:懂得储蓄、控制负债,不超前消费,建立风险缓冲金。不再默认依靠资产增值,靠现金流管理生活;
  3. 情绪与心理韧性:接受人生不是线性上升,允许阶段性低谷,减少社交比较;
  4. 可迁移能力:沟通、项目统筹、谈判、问题排查,这类能力可以跨行业复用。

三、一个客观判断

10后面临的不是“没有机会”,而是没有轻松的机会。
增量时代,只要肯吃苦就能抓住时代红利;存量时代,单纯努力不够,需要选对方向、持续迭代、控制风险。
对他们来说最重要的转变:放弃“必须成功”的单一成功标准。成功不一定等于高薪、买房、结婚生子,拥有健康、稳定现金流、自主选择权,也是很好的人生。

四、风险提醒

  • 不要盲目追逐短期网红风口,很多风口生命周期很短;
  • 尽量避免高负债(消费贷、高杠杆买房),在不确定环境里,现金流安全优先级最高;
  • AI是工具,不是对手,最好的策略是学会驾驭AI,提升效率,而不是和AI比拼基础劳动。

✅ 可以,Win11右下角任务栏时间区域显示星期,有两种方案

方案一:系统原生(不用装软件,推荐)

原理:修改短日期格式,会全局影响系统所有地方的日期展示(文件资源管理器、Excel等日期都会带星期),但不需要第三方工具。
  1. Win+R,输入 intl.cpl 回车,直接打开【区域】面板
  2. 点右下角 其他设置 → 切换到【日期】标签页
  3. 修改【短日期】输入框(英文输入法!)

    • ddd:简写,如 周二
    • dddd:完整,如 星期二

示例:

  • 国内默认短日期 yyyy/M/d,改成:yyyy/M/d dddd → 显示 2026/10/6 星期二
  • 想要简写:yyyy/M/d ddd → 2026/10/6 周二
  • 点【应用】→【确定】。如果没立刻生效,重启资源管理器:任务管理器找到explorer.exe右键重启。
⚠️ 缺点:全局生效,所有软件里的短日期都会带上星期。如果不想改动全系统日期格式,用方案二。

方案二:第三方 ElevenClock(推荐开发人员,只改任务栏时钟,不影响系统)

开源免费 Github 工具,专门给Win11任务栏时钟自定义格式,只修改右下角时间显示,不改动系统全局日期格式,可以实现换行、自定义星期。

  1. 微软商店或者 Github 下载 ElevenClock
  2. 安装后右键右下角时钟 → ElevenClock 设置
  3. 开启自定义格式,示例模板:

    HH:mm
    dddd
    yyyy/M/d

    效果:

    14:30
    星期二
    2026/10/6

    这个方案不会干扰文件、Excel、脚本里的日期格式,适合你这种开发环境。

小提示

任务栏如果比较窄,太长的dddd(星期二)可能会被截断,优先试 ddd(周二)。
想恢复默认:回到intl.cpl日期设置页,点【重置】即可一键还原。