AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79564596
Accepted
user30222843
user30222843
Asked: 2025-04-09 22:22:42 +0800 CST2025-04-09 22:22:42 +0800 CST 2025-04-09 22:22:42 +0800 CST

Sincronizando áudio com rolagem de partitura

  • 772

Estou construindo uma página da web que será usada por bateristas para praticar padrões.

Para isso, preciso ter:

  1. Um metrônomo ajustável (funcional)
  2. Uma partitura incluindo rolagem automática sincronizada com o metrônomo criada com tela HTML (não totalmente funcional)

O problema está na sincronização da rolagem da partitura com o metrônomo. Por enquanto, estou usando valores puramente empíricos, porque quando tentei calcular as distâncias e usei settimeout para rolar a partitura, não consegui um resultado satisfatório.

Observe:

  1. o espaço entre as notas é de 30 pixels.
  2. A tela provavelmente terá 100% da largura da página, como no exemplo.
  3. A barra vertical no meio da tela é o ponto de referência. Quando uma nota atinge essa barra, um som é reproduzido para que o usuário possa ouvir qual elemento da bateria deve ser tocado e como (volume máximo da caixa ou volume mínimo neste exemplo).

O script funciona, mas é bastante aproximado. Para que o script seja utilizável, deve haver uma sincronização muito boa entre o metrônomo e as notas que chegam ao compasso de referência.

Tentei calcular a distância que deve ser compensada a cada milissegundo, mas settimeout não permite menos que 15 a 17 ms e sua variação não é precisa o suficiente.

Abaixo está o código completo do projeto.

<!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>

E a classe metrônomo:

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 1 respostas
  • 43 Views

1 respostas

  • Voted
  1. Best Answer
    Phaelax
    2025-04-09T22:43:00+08:002025-04-09T22:43:00+08:00

    O fato de o tempo de atualização variar não deve ser um problema se você interpolar o valor de deslocamento em vez de depender de um valor de passo estático. O que eu faria é considerar o tempo total decorrido desde o início em cada passo, em vez de depender de incrementos a cada atualização. O motivo é que, se o valor do passo estiver um pouco errado, ele se acumulará e o desfará ainda mais à medida que você avançar.

    Com um andamento de 60 bpm, a cada 1 segundo a partitura deve rolar 30 px para a próxima nota. Correto?

    let seconds = elapsedTimeMS / 1000;
    let offsetPixels = seconds * 30;
    // update score offset here
    

    Ao rastrear o índice de tempo quando a música começa e subtraí-lo do índice de tempo atual quando ela é atualizada, você pode obter o tempo decorrido. E agora, não importa a velocidade da atualização, a precisão permanecerá. Visualmente, contanto que você atualize a cada 60 ms ou menos, acho que deve parecer suave.

    • 0

relate perguntas

  • classificação de mesclagem não está funcionando - código Javascript: não é possível encontrar o erro mesmo após a depuração

  • método select.remove() funciona estranho [fechado]

  • Sempre um 401 res em useOpenWeather () - react-open-weather lib [duplicado]

  • O elemento de entrada não possui atributo somente leitura, mas os campos ainda não podem ser editados [fechado]

  • Como editar o raio do primeiro nó de um RadialTree D3.js?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

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

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve