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
后端获取MIDI鼓时序 · 前端播放,仅参考 - Joyber 的博客

<!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:260px;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}
#progressWrap{width:100%;height:24px;border:1px solid #777;margin:10px 0;position:relative;overflow:hidden}
#progressBar{height:100%;width:0%;background:#4488dd;transition:none}
#visualWrap{width:100%;height:140px;border:1px solid #777;margin:10px 0;position:relative;background:#f7f7f7;overflow:hidden}
.drumMarker{position:absolute;width:8px;height:20px;background:#222;top:60px}
#playHead{position:absolute;width:2px;height:100%;background:#ff3333;top:0;left:0px}
</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="progressWrap">
    <div id="progressBar"></div>
</div>
<div id="visualWrap">
    <div id="playHead"></div>
</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');
const progressBar = document.getElementById('progressBar');
const visualWrap = document.getElementById('visualWrap');
const playHead = document.getElementById('playHead');

let audioCtx = null;
let drumList = [];
let isLoaded = false;
let totalDuration = 0;
let animationFrameId = null;
let playStartAudioTime = 0;
let isPlaying = false;

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

function clearVisualMarkers() {
    const markers = visualWrap.querySelectorAll('.drumMarker');
    markers.forEach(m=>m.remove());
}

function renderDrumMarkers() {
    clearVisualMarkers();
    if(totalDuration <= 0) return;
    const wrapWidth = visualWrap.clientWidth;
    drumList.forEach(ev=>{
        const marker = document.createElement('div');
        marker.className = 'drumMarker';
        const leftPercent = (ev.time / totalDuration) * 100;
        marker.style.left = leftPercent + "%";
        if(ev.note === 36) marker.style.background="#222222";
        else if(ev.note ===38) marker.style.background="#555555";
        else if(ev.note ===42) marker.style.background="#884422";
        visualWrap.appendChild(marker);
    })
}

function updateProgress() {
    if(!isPlaying || !audioCtx) return;
    const currentAudioTime = audioCtx.currentTime;
    const elapsed = currentAudioTime - playStartAudioTime;
    let percent = 0;
    if(totalDuration>0){
        percent = Math.min(100, (elapsed / totalDuration)*100);
    }
    progressBar.style.width = percent + "%";
    playHead.style.left = percent + "%";

    if(elapsed >= totalDuration){
        stopPlayback();
        return;
    }
    animationFrameId = requestAnimationFrame(updateProgress);
}

function playDrum(ctx, note, vel, startTime) {
    const gainVal = vel / 127;
    const mainGain = ctx.createGain();
    mainGain.gain.value = 0;

    if(note === 36) {
        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) {
        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) {
        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{
        // const res = await fetch("./midi_decode.php");
        // drumList = await res.json();

        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},
        ];

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

function stopPlayback(){
    isPlaying = false;
    if(animationFrameId){
        cancelAnimationFrame(animationFrameId);
        animationFrameId = null;
    }
    progressBar.style.width = "0%";
    playHead.style.left = "0%";
    appendLog("播放停止\n");
}

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

btnStop.onclick = ()=>{
    stopPlayback();
    if(audioCtx){
        audioCtx.close();
        audioCtx = null;
    }
}
</script>
</body>
</html>

标签: none

添加新评论