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
HTML播放MIDI解码后的演奏时序数据列子 - Joyber 的博客

两个关键点说明

  1. 刚才那个原生WebAudio版本不是真实采样音源,是实时算法合成声音(振荡器+白噪声模拟鼓,所以不用加载外部音频文件,离线也能用),音质偏简易,适合验证时序逻辑。
  2. Magenta那个midi-player没看到播放按钮:<midi-player> 自定义组件,部分浏览器需要加载成功后才渲染播放控件,而且它是完整MIDI播放器,和你「后端返回时序JSON,前端播放」的目标不完全匹配。

下面给你最终目标版本:
后端PHP输出 [{"time":秒, "note":音符号, "vel":力度},...] JSON时序
前端ajax请求后端拿到时序列表
WebAudio原生合成鼓(底鼓/军鼓/闭踩镲),无外部音源依赖,离线可用
带播放、停止按钮,日志输出
预留替换接口:以后可以把简易合成换成真实采样音源

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>后端MIDI时序 - 前端鼓播放器</title>
<style>
*{box-sizing:border-box;font-family:system-ui}
body{max-width:820px;margin:24px auto;padding:0 12px}
#log{height:320px;overflow:auto;border:1px solid #aaa;padding:10px;margin:12px 0;font-family:monospace;font-size:12px}
button{padding:9px 18px;margin:5px;cursor:pointer;font-size:14px}
#tip{color:#666;margin:8px 0}
</style>
</head>
<body>
<h2>后端获取MIDI鼓时序 · 前端播放</h2>
<div>
    <button id="btnFetch">【1】从后端加载鼓时序JSON</button>
    <button id="btnPlay" disabled>【2】播放</button>
    <button id="btnStop" disabled>停止</button>
</div>
<div id="tip">
说明:默认先模拟后端数据;把fetch里url改成你的php接口地址,即可读取PHP解码MIDI输出的鼓点。
</div>
<div id="log"></div>

<script>
const logEl = document.getElementById('log');
const btnFetch = document.getElementById('btnFetch');
const btnPlay = document.getElementById('btnPlay');
const btnStop = document.getElementById('btnStop');

let audioCtx = null;
let drumList = []; // 存放后端返回时序数据
let isLoaded = false;

function appendLog(text){
    logEl.innerHTML += text + "\n";
    logEl.scrollTop = logEl.scrollHeight;
}

// ======================
// WebAudio 合成鼓发生器
// note: GM鼓编号 36底鼓,38军鼓,42闭踩镲
// ======================
function playDrum(ctx, note, vel, startTime) {
    const gainVal = vel / 127;
    const mainGain = ctx.createGain();
    mainGain.gain.value = 0;

    if(note === 36) {
        // 底鼓 Kick
        const osc = ctx.createOscillator();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(55, startTime);
        osc.frequency.exponentialRampToValueAtTime(18, startTime + 0.32);
        mainGain.gain.setValueAtTime(gainVal * 0.9, startTime);
        mainGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.32);
        osc.connect(mainGain);
        mainGain.connect(ctx.destination);
        osc.start(startTime);
        osc.stop(startTime + 0.32);
    } else if(note === 38) {
        // 军鼓 Snare
        const osc = ctx.createOscillator();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(170, startTime);
        mainGain.gain.setValueAtTime(gainVal * 0.65, startTime);
        mainGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.22);
        osc.connect(mainGain);

        // 军鼓叠加白噪声
        const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.2, ctx.sampleRate);
        const data = noiseBuf.getChannelData(0);
        for(let i=0;i<data.length;i++) data[i] = Math.random()*2-1;
        const noiseSrc = ctx.createBufferSource();
        noiseSrc.buffer = noiseBuf;
        const filter = ctx.createBiquadFilter();
        filter.type = "highpass";
        filter.frequency.value = 4500;
        noiseSrc.connect(filter);
        filter.connect(mainGain);
        noiseSrc.start(startTime);

        mainGain.connect(ctx.destination);
        osc.start(startTime);
        osc.stop(startTime + 0.22);
    } else if(note === 42) {
        // 闭踩镲 HH Closed
        const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 0.14, ctx.sampleRate);
        const data = noiseBuf.getChannelData(0);
        for(let i=0;i<data.length;i++) data[i] = Math.random()*2-1;
        const noiseSrc = ctx.createBufferSource();
        noiseSrc.buffer = noiseBuf;
        const filter = ctx.createBiquadFilter();
        filter.type = "highpass";
        filter.frequency.value = 7500;
        noiseSrc.connect(filter);
        filter.connect(mainGain);
        mainGain.gain.setValueAtTime(gainVal * 0.45, startTime);
        mainGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.14);
        noiseSrc.start(startTime);
        mainGain.connect(ctx.destination);
    }
}

// ======================
// 【获取后端时序接口】
// ======================
btnFetch.onclick = async ()=>{
    appendLog("请求后端鼓时序数据...");
    try{
        // ========== 这里改成你的PHP接口地址 ==========
        // const res = await fetch("./midi_decode.php");
        // drumList = await res.json();

        // 模拟后端返回JSON(测试用,上线注释掉上面模拟,启用fetch)
        drumList = [
            {time:0.0, note:36, vel:100},
            {time:0.5, note:42, vel:80},
            {time:1.0, note:38, vel:95},
            {time:1.5, note:42, vel:80},
            {time:2.0, note:36, vel:100},
            {time:2.5, note:42, vel:80},
            {time:3.0, note:38, vel:95},
            {time:3.5, note:42, vel:80},
            {time:4.0, note:36, vel:100},
        ];
        // ===========================================

        isLoaded = true;
        btnPlay.disabled = false;
        appendLog(` 成功拿到时序,共${drumList.length}个鼓点`);
        drumList.forEach(e=>{
            appendLog(` > t:${e.time.toFixed(2)}s | note:${e.note} vel:${e.vel}`);
        })
    }catch(err){
        appendLog(" 获取后端数据失败:" + err.message);
        console.error(err);
    }
};

// 播放
btnPlay.onclick = ()=>{
    if(!isLoaded) return;
    if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const startTime = audioCtx.currentTime;
    appendLog("\n===== 开始播放 =====");
    drumList.forEach(ev=>{
        const playAt = startTime + ev.time;
        playDrum(audioCtx, ev.note, ev.vel, playAt);
    })
};

// 停止
btnStop.onclick = ()=>{
    if(audioCtx){
        audioCtx.close();
        audioCtx = null;
    }
    appendLog(" 播放停止\n");
}
</script>
</body>
</html>

PHP后端改造:输出JSON时序

修改你之前的midi_decode.php,增加一个接口函数,只输出鼓通道ch=9的时序数组:

// 在MidiDecoder类里面新增这个函数
public function getDrumJson():array
{
    $out = [];
    foreach ($this->tracks as $events) {
        foreach ($events as $e) {
            if ($e['type'] === 'note_on' && $e['ch'] === 9) {
                // 过滤 vel=0 的NoteOn(MIDI标准等价NoteOff,忽略)
                if($e['vel'] === 0) continue;
                $sec = $this->tickToSeconds($e['tick']);
                $out[] = [
                    'time' => $sec,
                    'note' => $e['note'],
                    'vel' => $e['vel']
                ];
            }
        }
    }
    return $out;
}

// 文件末尾,接口模式:直接输出JSON
// $decoder = new MidiDecoder();
// $midFile = './test.mid';
// if ($decoder->loadFile($midFile)) {
//     header("Content-Type: application/json");
//     echo json_encode($decoder->getDrumJson(), JSON_UNESCAPED_UNICODE);
//     exit;
// }
当访问 midi_decode.php,后端直接返回JSON数组,前端fetch拿到,完美对接。

扩展规划(后续可选)

  1. 增加更多鼓音色:吊镲、叮叮镲、踩镲CC控制(开踩镲)
  2. 增加播放进度条,可视化动态鼓谱(DTX风格)
  3. 替换简易算法合成 → 加载真实鼓采样wav,音质升级

使用步骤

  1. 保存html,浏览器打开
  2. 点【从后端加载鼓时序JSON】,加载模拟数据
  3. 点【播放】,听到鼓声
  4. 测试没问题后,把前端fetch注释掉模拟数据,换成请求php接口

标签: none

添加新评论