AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / user-30222843

user30222843's questions

Martin Hope
user30222843
Asked: 2025-04-09 22:22:42 +0800 CST

将音频与乐谱滚动同步

  • 6

我正在建立一个网页,供鼓手练习节奏型。

为了做到这一点,我必须:

  1. 可调节节拍器(功能性)
  2. 包含与使用 HTML 画布创建的节拍器同步的自动滚动的乐谱(功能不完整)

问题在于如何将乐谱滚动与节拍器同步。目前,我使用纯经验值,因为之前我尝试计算距离并使用 settimeout 滚动乐谱时,并没有得到令人满意的结果。

请注意:

  1. 注释之间的空间为30像素。
  2. 画布可能为页面宽度的 100%,如示例中所示。
  3. 画布中间的竖线是参考点。当音符到达此竖线时,会发出声音,以便用户听到应该播放哪个鼓元素以及如何播放(在本例中为小军鼓的最大音量或最小音量)。

这个脚本能完成任务,但效果比较粗糙。为了确保脚本可用,节拍器和到达参考小节的音符之间必须保持良好的同步。

我尝试计算每毫秒应偏移的距离,但 settimeout 不允许少于 15 到 17 毫秒,并且其变化不够准确。

以下是该项目的完整代码。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DrumChallenge.com</title>
    <style>
        #div_patterns {
            display: inline-block;
            margin: 20px;
        }
        #div_tempo {
            display: inline-block;
            margin: 20px;
        }
        #drumscore {
            position: relative;
            width: 100%;
            height: 200px;
        }
        #drumscore canvas {
            position: absolute;
            top: 0px;
            left: 0px;
        }
        canvas {
            border: 1px solid black;
            width: 100%;
            height: 200px;
        }
    </style>
</head>
<body>
    <div id="params">
        <div id="div_patterns"></div>
        <div id="div_tempo">
            <span>Tempo :</span>
            <input type="text" id="value_tempo" value="60" />
            
            <span>Delai :</span>
            <span id="value_delai"></span>
            
            <span>Timer :</span>
            <span id="value_timer"></span>
            
        </div>
        <button id="launch" class="launch" type="button">Lancer challenge</button>
    </div>
    <div id="drumscore">
        <canvas id="score"></canvas>
        <canvas id="scoreCanvas"></canvas>
    </div>
    <script src="metronome.js"></script>

    <script>
        
        
        
    document.addEventListener('DOMContentLoaded', () => {
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const metronome = new Metronome(audioContext);
        
        var Current_Audio = false;
        var Challenge_Launched = false;
        var Current_Animation = false;
        var Timer_Challenge = false;
        var Timer_General = false;
        var Tempo = 60;
        var Delai;
        var Distance_Between_Notes = 30;
        var General_Timer = 0;
        var NextNoteTime = 0;
        //
        //
        //
        const LaunchButton = document.getElementById('launch');
        LaunchButton.addEventListener('click', function(){
            if (Challenge_Launched == false){
                CreerChallenge();
                Challenge_Launched = true;
                
                const bpm = parseInt(InputTempo.value, 10);
                metronome.setTempo(bpm);
                metronome.start();
                
                Timer_General = setInterval(SetGeneralTimer, 1000);
                NextNoteTime = performance.now();
                //drawNotes();
                requestAnimationFrame(drawNotes);
            } else {
                Challenge_Launched = false;
                clearTimeout(Timer_Challenge);
                clearInterval(Timer_General);
                metronome.stop();
                //cancelAnimationFrame(Current_Animation);
            }
        });
        //
        //
        //
        function SetTempo(){
            Tempo = InputTempo.value;
            Delai = (60000 / Tempo).toFixed(2);
            document.getElementById('value_tempo').innerHTML = Tempo;
            document.getElementById('value_delai').innerHTML = Delai + 'ms';
            metronome.setTempo(Tempo);
            /*if (Challenge_Launched){
                clearTimeout(Timer_Challenge);
                //Timer_Challenge = setTimeout(drawNotes, Delai);
            }*/
        }
        //
        //
        //
        const InputTempo = document.getElementById('value_tempo');
        InputTempo.addEventListener('blur', function(){
            SetTempo()
        });
        SetTempo()
        //
        //
        //
        const drumscore = document.getElementById('drumscore');
        //
        // Canvas et contexte de la partition
        //
        const score = document.getElementById('score');
        const scorectx = score.getContext('2d');
        scorectx.canvas.width  = drumscore.offsetWidth;
        scorectx.canvas.height = drumscore.offsetHeight;
        //
        // Canvas et contexte des notes
        //
        const canvas = document.getElementById('scoreCanvas');
        const ctx = canvas.getContext('2d');
        ctx.canvas.width  = drumscore.offsetWidth;
        ctx.canvas.height = drumscore.offsetHeight;
        //
        // Lignes de la partition
        //
        const ScoreLines = [60,80,100,120,140];
        //
        //
        //
        const Elements = [
             {Name: 'Snare', Line: ScoreLines[2]}
        ];
        //
        // Patterns
        //
        const Patterns = [
            {
                Name:       'Rll',
                Element:    'Snare',
                Notes:      ['R', 'l', 'l'],
                Checkbox:   false,
                Label:      false,
                Checked:    false,
            },
            {
                Name:       'rrL',
                Element:    'Snare',
                Notes:      ['r', 'r', 'L'],
                Checkbox:   false,
                Label:      false,
                Checked:    false,
            }
        ];
        //
        // Affichage patterns
        //
        const DivPatterns = document.getElementById('div_patterns');
        Patterns.forEach(pattern => {
            pattern.Checkbox = document.createElement('input');
            pattern.Checkbox.type = "checkbox";

            pattern.Label = document.createElement('label')
            pattern.Label.htmlFor = pattern.Name;
            pattern.Label.appendChild(document.createTextNode(pattern.Name));
            
            DivPatterns.appendChild(pattern.Checkbox);
            DivPatterns.appendChild(pattern.Label);
        });
        //
        // Sounds
        //
        const Sounds = [
            {
                Element:    'Snare',
                Type:       'Normal',
                URL:        '',
                Audio:      new Audio('snare_normal.wav')
            },
            {
                Element:    'Snare',
                Type:       'Ghost',
                Audio:      new Audio('snare_ghost.wav')
            }
            
        ];
        //
        // Notes à afficher
        //
        
        const measures = 20;
        const noteWidth = 10;
        const noteHeight = 10;
        const scrollSpeed = 3;
        //
        // Main Droite ou Gauche
        //
        const isAccented = str => str === str.toUpperCase();
        const isRightHand = str => str.toUpperCase() === 'R';
        
        //
        // Créer le challenge
        //
        var notes = [];
        var current_pattern;
        //
        // Dessiner la partition
        //
        function CreerChallenge() {
            notes = [];
            for (var i=0 ; i<measures ; i++){
                current_pattern = Patterns[Math.floor(Math.random() * 2)];
                
                for (var j=0 ; j<current_pattern.Notes.length ; j++){
                    notes.push({
                        x:      canvas.width / 2 + 180 + (notes.length * Distance_Between_Notes) + 1,
                        y:      isRightHand(current_pattern.Notes[j]) ? ScoreLines[2] - 5 : ScoreLines[2] + 5,
                        w:      isAccented(current_pattern.Notes[j]) ? 7 : 4,
                        h:      isAccented(current_pattern.Notes[j]) ? 5 : 3,
                        Audio:  isAccented(current_pattern.Notes[j]) ? new Audio('snare_normal.wav') : new Audio('snare_ghost.wav')
                    })
                }
            }
            console.log(notes);
        }
        //
        // Dessiner la partition
        //
        function drawScore() {
            scorectx.clearRect(0, 0, canvas.width, canvas.height);
            scorectx.strokeStyle = "#A0A0A0";
            
            ScoreLines.forEach(Line => {
                scorectx.beginPath();
                scorectx.moveTo(0, Line);
                scorectx.lineTo(canvas.width, Line);
                scorectx.stroke();
            });
            
            scorectx.beginPath();
            scorectx.moveTo(canvas.width / 2, ScoreLines[0]);
            scorectx.lineTo(canvas.width / 2, ScoreLines[ScoreLines.length-1]);
            scorectx.stroke();
        }
        //
        //
        //
        function nextNote() {
            const secondsPerBeat = 60.0 / Tempo;
            NextNoteTime += 1000 / Distance_Between_Notes;
        }

        //
        // Dessiner et animer les notes
        //
        function drawNotes() {
            
            NextNoteTime = performance.now();
            
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            notes.forEach(note => {
            
                //ctx.fillRect(note.x, note.y, note.w, note.w);
                ctx.beginPath();
                ctx.ellipse(note.x, note.y, note.w, note.h, Math.PI, 0, 2 * Math.PI);
                ctx.fill();
                
                if (note.x > canvas.width / 2 - 5 && note.x <= canvas.width / 2){
                    Current_Audio = note.Audio;
                    note.Audio.play();
                }
                
                //note.x -= scrollSpeed;
                note.x -= Tempo / 15;
            });

            
            //const endTime = performance.now()

            //console.log(`Call to doSomething took ${endTime - startTime} milliseconds ` + endTime)
            //Current_Animation = requestAnimationFrame(drawNotes);
            if (Challenge_Launched){
                //Timer_Challenge = setTimeout(drawNotes, 1);
                Timer_Challenge = setTimeout(() => requestAnimationFrame(drawNotes), 1);
            }
        }
        
        function SetGeneralTimer(){
            const startTime = performance.now()
            General_Timer++;
            document.getElementById('value_timer').innerHTML = General_Timer
            const endTime = performance.now()
            //console.log(`Started ` + startTime + ' | ' + (endTime - startTime) + ' | ' + General_Timer)
        }
        
        
        drawScore();
    });
    </script>
</body>
</html>

节拍器类:

class Metronome {
    constructor(context) {
        this.context = context;
        this.isPlaying = false;
        this.current16thNote = 0;
        this.tempo = 60;
        this.lookahead = 25.0;
        this.scheduleAheadTime = 0.1;
        this.nextNoteTime = 0.0;
        this.timerID = null;
    }

    nextNote() {
        const secondsPerBeat = 60.0 / this.tempo;
        this.nextNoteTime += 0.25 * secondsPerBeat;
        this.current16thNote++;
        if (this.current16thNote === 16) {
            this.current16thNote = 0;
        }
    }

    scheduleNote(beatNumber, time) {
        const osc = this.context.createOscillator();
        const envelope = this.context.createGain();
        osc.frequency.value = (beatNumber % 4 === 0) ? 1000 : 800;
        envelope.gain.value = 1;
        envelope.gain.exponentialRampToValueAtTime(0.001, time + 0.2);
        osc.connect(envelope);
        envelope.connect(this.context.destination);
        osc.start(time);
        osc.stop(time + 0.2);
    }

    scheduler() {
        console.log(this.nextNoteTime);
        console.log(this.context.currentTime);
        console.log(this.scheduleAheadTime);
        while (this.nextNoteTime < this.context.currentTime + this.scheduleAheadTime) {
            this.scheduleNote(this.current16thNote, this.nextNoteTime);
            this.nextNote();
        }
        this.timerID = setTimeout(this.scheduler.bind(this), this.lookahead);
    }

    start() {
        if (!this.isPlaying) {
            this.isPlaying = true;
            this.current16thNote = 0;
            this.nextNoteTime = this.context.currentTime;
            this.scheduler();
        }
    }

    stop() {
        this.isPlaying = false;
        clearTimeout(this.timerID);
    }

    setTempo(newTempo) {
        this.tempo = newTempo;
    }
}
javascript
  • 1 个回答
  • 43 Views

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve