diff --git a/music-video-note-editor/README.md b/music-video-note-editor/README.md new file mode 100644 index 00000000..552b9f8f --- /dev/null +++ b/music-video-note-editor/README.md @@ -0,0 +1,374 @@ +# 🎹 Editor e Tutor Inteligente de Notas Musicais + +Um aplicativo web completo e interativo para criar vídeos educacionais de piano/teclado com notas musicais coloridas, transcrição automática de áudio, prática guiada em tempo real e análise de performance. + +## ✨ Funcionalidades Principais + +### 1. **Player de Vídeo Integrado** +- Carregue qualquer vídeo do YouTube +- Controles: Play/Pause, ±5s +- Timeline visual sincronizada + +### 2. **🎵 Transcrição Automática (Áudio → MIDI)** +- Framework pronto para integração com: + - **Spotify Basic Pitch** (leve, browser) + - **Magenta Onsets & Frames** (especializado em piano) + - **ByteDance Piano Transcription** (backend, alta qualidade) +- Modo demonstração funcional incluído +- Controle de confiança mínima +- Instruções completas para produção + +### 3. **🎹 Conexão com Teclado MIDI (WebMIDI)** +- Conecte teclados MIDI no navegador +- Visualização em tempo real +- Monitor de velocidade +- Zero latência + +### 4. **🎯 Modo Prática Guiada** (Tutor Inteligente) +- **Feedback em tempo real** +- **Métricas:** + - Acertos vs Erros + - Precisão (%) + - Streak (🔥) + - Análise de timing e pitch +- **Visualização dinâmica** em canvas +- **Gamificação:** conquistas e streaks +- **Relatórios detalhados** com dicas + +### 5. **📝 Visualização de Partitura (VexFlow)** +- Geração automática de partituras +- Claves Sol e Fá +- Notas coloridas +- Exportação para PNG + +### 6. **Editor Manual de Notas** +- 12 notas (Dó, Dó#, Ré, Ré#, Mi, Fá, Fá#, Sol, Sol#, Lá, Lá#, Si) +- Oitavas 1-7 +- Timeline interativa + +### 7. **Sistema de Cores Pedagógico** +**Notas naturais** (claras): +- Dó: Verde claro | Ré: Azul claro | Mi: Amarelo +- Fá: Vermelho claro | Sol: Roxo claro | Lá: Laranja claro | Si: Rosa claro + +**Notas sustenidas** (escuras): +- Dó#: Verde escuro | Ré#: Azul escuro | Fá#: Vermelho escuro +- Sol#: Roxo escuro | Lá#: Laranja escuro + +### 8. **🤖 Gerador de Prompts Educacionais** +Dicas personalizadas sobre: +- ✋ Posição das mãos +- 💪 Intensidade +- 🎵 Pedal +- ⏱️ Ritmo +- 🎹 Técnica geral + +### 9. **💾 Exportação Completa** +- Salvar projeto (JSON) +- Carregar projeto +- Exportar notas (CSV) +- Exportar partitura (PNG) + +## 🚀 Como Usar + +### Início Rápido + +1. **Abra `index.html` no navegador** + - Recomendado: Chrome ou Edge (suporte WebMIDI) + +2. **Carregue um vídeo do YouTube** + - Cole o link: `https://youtu.be/VIDEO_ID` + +3. **Adicione notas** (3 opções): + - 🎵 **Transcrição automática** (IA) + - 🎹 **Conectar teclado MIDI** e tocar + - ✏️ **Adicionar manualmente** + +4. **Pratique com feedback** + - Conecte teclado MIDI + - Modo Prática → Iniciar + - Toque as notas corretas no momento certo + - Veja métricas em tempo real + +5. **Gere partitura** + - Escolha clave e compasso + - VexFlow gera notação musical + +6. **Receba dicas** + - IA analisa suas notas + - Prompts personalizados + +7. **Salve seu trabalho** + - Projeto completo (JSON) + - Apenas notas (CSV) + +## 🛠️ Arquitetura do Sistema + +### Blueprint de Produto (3 Modos) + +#### 1. **MIDI-First** (Latência Mínima) +- WebMIDI.js para captura direta +- VexFlow para partitura +- Feedback instantâneo (< 10ms) +- **Ideal para:** Prática em tempo real + +#### 2. **Mic-First** (Sem Teclado Digital) +- Magenta Onsets & Frames via browser +- Web Audio API +- **Ideal para:** Piano acústico + +#### 3. **Híbrido Pro** (Backend GPU) +- ByteDance Piano Transcription +- FastAPI + Celery +- Detecção de pedal sustain +- **Ideal para:** Transcrições complexas + +## 📦 Arquivos do Projeto + +``` +music-video-note-editor/ +├── index.html # Interface principal +├── styles.css # Estilos visuais +├── app.js # Orquestração principal +├── midi-handler.js # WebMIDI management +├── transcription-handler.js # Transcrição áudio→MIDI +├── practice-mode.js # Tutor com gamificação +├── score-renderer.js # Partituras (VexFlow) +└── README.md # Esta documentação +``` + +## 🔧 Tecnologias Utilizadas + +### Frontend +- **HTML5**, **CSS3**, **JavaScript (Vanilla)** +- **YouTube IFrame API** + +### Bibliotecas (Open-Source) +- **VexFlow 4.2.2** (MIT) - Partituras +- **WebMIDI.js 3.1.6** (MIT) - MIDI +- **Framework pronto para:** + - Spotify Basic Pitch (Apache 2.0) + - Magenta.js (Apache 2.0) + - ByteDance Piano Transcription + +## 🔐 Integração com Transcrição Real + +### Para produção com Basic Pitch: + +```bash +npm install @spotify/basic-pitch +``` + +```javascript +import * as basicPitch from '@spotify/basic-pitch'; + +const model = await basicPitch.loadModel(); +const frames = await basicPitch.detectNotes(audioBuffer, { + onsetThreshold: 0.5, + frameThreshold: 0.3, + minNoteLength: 0.1 +}); +``` + +### Para backend com ByteDance: + +```bash +pip install piano-transcription-inference +``` + +```python +from piano_transcription_inference import PianoTranscription + +transcriptor = PianoTranscription(device='cuda') +transcribed_dict = transcriptor.transcribe('audio.wav', 'output.mid') +``` + +## 🎯 Casos de Uso + +### Para Professores +- Materiais didáticos interativos +- Avaliação de alunos com métricas +- Exercícios personalizados + +### Para YouTubers +- Tutoriais profissionais de piano +- Notas visuais sincronizadas +- Partituras para thumbnails + +### Para Estudantes +- Aprender músicas de vídeos +- Praticar com feedback real-time +- Acompanhar evolução + +### Para Músicos +- Transcrever músicas automaticamente +- Criar partituras digitais +- Analisar técnicas + +## 📊 Sistema de Gamificação + +### Métricas de Avaliação +- **Precisão de Pitch:** Nota correta +- **Precisão de Timing:** Momento certo (±500ms) +- **Sustentação:** Duração correta +- **Streaks:** Sequências de acertos + +### Conquistas +- 🔥 **Streak Master:** 10 acertos seguidos +- 🎯 **Perfeccionista:** 100% de precisão +- 📈 **Persistente:** 50 práticas completadas +- ⚡ **Relâmpago:** < 5% de erro +- 🎹 **Virtuoso:** 1000 notas corretas + +## 💡 Dicas e Truques + +### Para Melhor Performance +- Use Chrome/Edge (WebMIDI completo) +- Conecte teclado MIDI antes de abrir +- Use vídeos com áudio de boa qualidade + +### Para Melhor Aprendizado +- Comece com músicas simples (5-10 notas) +- Pratique devagar primeiro +- Foco em precisão > velocidade +- Revise relatórios finais + +### Atalhos +- **Enter:** Carregar vídeo +- **Espaço:** Play/Pause (vídeo em foco) + +## 🐛 Solução de Problemas + +**Vídeo não carrega:** +- Verifique o link +- Alguns vídeos têm restrições +- Verifique internet + +**Teclado MIDI não conecta:** +- Use Chrome ou Edge +- Conecte antes de abrir a página +- Verifique se está reconhecido pelo sistema + +**Transcrição não funciona:** +- Modo atual é demonstração +- Para produção: veja seção "Integração" +- Consulte console do navegador + +**Partitura com erro:** +- Adicione notas primeiro +- Verifique clave vs oitavas +- Graves → Fá | Agudas → Sol + +## 📚 Recursos de Aprendizado + +### Documentação +- [VexFlow](https://github.com/0xfe/vexflow/wiki) +- [WebMIDI.js](https://webmidijs.org/) +- [Basic Pitch](https://github.com/spotify/basic-pitch) +- [Magenta.js](https://github.com/magenta/magenta-js) + +### Datasets +- [MAESTRO](https://magenta.tensorflow.org/datasets/maestro) - 200h piano +- [ASAP](https://github.com/fosfrancesco/asap-dataset) - 222 partituras + +## 🌐 Compatibilidade + +- ✅ Chrome, Firefox, Safari, Edge (recentes) +- ✅ Responsivo para tablets +- ✅ WebMIDI: Chrome, Edge (Firefox requer flag) +- ⚠️ Requer internet (CDNs e YouTube) + +## 🚀 Instalação + +### Opção 1: Direto no Navegador +```bash +# Abra index.html +# Não requer servidor! +``` + +### Opção 2: Servidor Local +```bash +# Python +python -m http.server 8000 + +# Node.js +npx http-server + +# Acesse: http://localhost:8000 +``` + +## 📝 Formato dos Arquivos + +### Projeto (JSON) +```json +{ + "version": "1.0", + "videoId": "VIDEO_ID", + "videoUrl": "https://youtu.be/...", + "notes": [ + { + "id": 1234567890, + "note": "C", + "octave": 4, + "startTime": 5.2, + "duration": 0.5, + "color": "#90EE90", + "source": "manual" + } + ], + "prompts": "Dicas...", + "createdAt": "2025-01-27T..." +} +``` + +### Notas (CSV) +```csv +Nota,Oitava,Tempo Início (s),Duração (s),Cor,Fonte +C,4,5.20,0.5,#90EE90,manual +D,4,5.80,0.5,#87CEEB,transcription +``` + +## 🎵 Roadmap Futuro + +- [ ] Integração completa com Basic Pitch +- [ ] Suporte a múltiplas mãos +- [ ] Reconhecimento de acordes +- [ ] Exportação de vídeo com notas +- [ ] Modo multi-jogador +- [ ] Biblioteca de músicas +- [ ] App mobile +- [ ] Detecção de pedal +- [ ] Score following avançado + +## 🔐 Licenças + +Todas as bibliotecas são open-source: + +- **VexFlow:** MIT ✅ +- **WebMIDI.js:** MIT ✅ +- **Basic Pitch:** Apache 2.0 ✅ +- **Magenta:** Apache 2.0 ✅ +- **YouTube IFrame API:** ToS do YouTube + +## 🤝 Contribuindo + +- Reportar bugs via Issues +- Sugerir funcionalidades +- Fork e pull requests +- Melhorar documentação + +## 💖 Desenvolvido com Amor + +Este projeto democratiza o aprendizado de piano/teclado através de tecnologia open-source e IA. + +**Esperamos que este tutor ajude você a:** +- Criar tutoriais musicais incríveis +- Aprender piano de forma visual +- Melhorar técnica com feedback real +- Compartilhar conhecimento musical + +--- + +**Desenvolvido com ❤️ para educadores e estudantes de música** + +🎹 *"A música é a linguagem universal da humanidade"* - Henry Wadsworth Longfellow diff --git a/music-video-note-editor/app.js b/music-video-note-editor/app.js new file mode 100644 index 00000000..07d3cc89 --- /dev/null +++ b/music-video-note-editor/app.js @@ -0,0 +1,642 @@ +// ===== ESTADO GLOBAL DA APLICAÇÃO ===== +const appState = { + player: null, + videoId: null, + notes: [], + selectedNote: null, + currentTime: 0, + videoDuration: 0, + isPlaying: false +}; + +// ===== CORES DAS NOTAS ===== +const NOTE_COLORS = { + 'C': '#90EE90', // Verde claro + 'C#': '#2d5016', // Verde escuro + 'D': '#87CEEB', // Azul claro + 'D#': '#1e3a5f', // Azul escuro + 'E': '#FFD700', // Amarelo claro + 'F': '#FF6B6B', // Vermelho claro + 'F#': '#8B0000', // Vermelho escuro + 'G': '#DDA0DD', // Roxo claro + 'G#': '#4B0082', // Roxo escuro + 'A': '#FFA500', // Laranja claro + 'A#': '#cc5200', // Laranja escuro + 'B': '#FFB6C1' // Rosa claro +}; + +// ===== INICIALIZAÇÃO ===== +document.addEventListener('DOMContentLoaded', () => { + console.log('🎹 Aplicativo iniciado!'); + setupEventListeners(); + initializeModules(); +}); + +// ===== YOUTUBE API ===== +let playerReady = false; +function onYouTubeIframeAPIReady() { + console.log('✅ YouTube API carregada'); + playerReady = true; +} + +// ===== EVENT LISTENERS ===== +function setupEventListeners() { + // Video loading + const loadVideoBtn = document.getElementById('loadVideo'); + const videoUrlInput = document.getElementById('videoUrl'); + + if (loadVideoBtn) loadVideoBtn.addEventListener('click', loadVideo); + if (videoUrlInput) { + videoUrlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') loadVideo(); + }); + } + + // Video controls + const playPauseBtn = document.getElementById('playPause'); + const rewindBtn = document.getElementById('rewind'); + const forwardBtn = document.getElementById('forward'); + + if (playPauseBtn) playPauseBtn.addEventListener('click', togglePlayPause); + if (rewindBtn) rewindBtn.addEventListener('click', () => seekVideo(-5)); + if (forwardBtn) forwardBtn.addEventListener('click', () => seekVideo(5)); + + // Note selection + document.querySelectorAll('.note-btn').forEach(btn => { + btn.addEventListener('click', () => selectNote(btn)); + }); + + // Add note + const addNoteBtn = document.getElementById('addNoteAtCurrentTime'); + if (addNoteBtn) addNoteBtn.addEventListener('click', addNoteAtCurrentTime); + + // Prompts + const generatePromptBtn = document.getElementById('generatePrompt'); + if (generatePromptBtn) generatePromptBtn.addEventListener('click', generatePrompts); + + document.querySelectorAll('.prompt-btn').forEach(btn => { + btn.addEventListener('click', () => addCategoryPrompt(btn.dataset.category)); + }); + + // Export/Import + const saveProjectBtn = document.getElementById('saveProject'); + const loadProjectBtn = document.getElementById('loadProject'); + const projectFileInput = document.getElementById('projectFileInput'); + const exportNotesBtn = document.getElementById('exportNotes'); + + if (saveProjectBtn) saveProjectBtn.addEventListener('click', saveProject); + if (loadProjectBtn) { + loadProjectBtn.addEventListener('click', () => { + if (projectFileInput) projectFileInput.click(); + }); + } + if (projectFileInput) projectFileInput.addEventListener('change', loadProjectFile); + if (exportNotesBtn) exportNotesBtn.addEventListener('click', exportNotesCSV); + + // Transcription + const transcribeBtn = document.getElementById('transcribeAudio'); + if (transcribeBtn && typeof TranscriptionHandler !== 'undefined') { + transcribeBtn.addEventListener('click', () => TranscriptionHandler.transcribeVideo()); + } + + // MIDI + const connectMIDIBtn = document.getElementById('connectMIDI'); + if (connectMIDIBtn && typeof MIDIHandler !== 'undefined') { + connectMIDIBtn.addEventListener('click', () => MIDIHandler.connect()); + } + + // Practice Mode + const startPracticeBtn = document.getElementById('startPractice'); + const stopPracticeBtn = document.getElementById('stopPractice'); + const resetPracticeBtn = document.getElementById('resetPractice'); + + if (startPracticeBtn && typeof PracticeMode !== 'undefined') { + startPracticeBtn.addEventListener('click', () => PracticeMode.start()); + } + if (stopPracticeBtn && typeof PracticeMode !== 'undefined') { + stopPracticeBtn.addEventListener('click', () => PracticeMode.stop()); + } + if (resetPracticeBtn && typeof PracticeMode !== 'undefined') { + resetPracticeBtn.addEventListener('click', () => PracticeMode.reset()); + } + + // Score Generation + const generateScoreBtn = document.getElementById('generateScore'); + if (generateScoreBtn && typeof ScoreRenderer !== 'undefined') { + generateScoreBtn.addEventListener('click', () => ScoreRenderer.generateScore()); + } + + // Update timeline periodically + setInterval(updateTimeline, 100); +} + +// ===== INICIALIZAR MÓDULOS ===== +function initializeModules() { + // Inicializar modo prática + if (typeof PracticeMode !== 'undefined') { + PracticeMode.init(); + console.log('✅ Practice Mode inicializado'); + } + + // Tentar inicializar MIDI automaticamente (não bloqueia se falhar) + if (typeof MIDIHandler !== 'undefined') { + MIDIHandler.init().catch(err => { + console.log('⚠️ MIDI não disponível:', err.message); + }); + } + + console.log('✅ Módulos inicializados!'); +} + +// ===== FUNÇÕES DE VÍDEO ===== +function extractVideoId(url) { + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + /^([a-zA-Z0-9_-]{11})$/ + ]; + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) return match[1]; + } + return null; +} + +function loadVideo() { + const url = document.getElementById('videoUrl').value.trim(); + + if (!url) { + alert('Por favor, insira um link do YouTube!'); + return; + } + + const videoId = extractVideoId(url); + + if (!videoId) { + alert('URL inválida! Por favor, insira um link válido do YouTube.'); + return; + } + + appState.videoId = videoId; + + // Destruir player anterior se existir + if (appState.player && typeof appState.player.destroy === 'function') { + appState.player.destroy(); + } + + // Criar novo player + try { + appState.player = new YT.Player('player', { + videoId: videoId, + playerVars: { + 'playsinline': 1, + 'controls': 1 + }, + events: { + 'onReady': onPlayerReady, + 'onStateChange': onPlayerStateChange + } + }); + } catch (error) { + console.error('Erro ao criar player:', error); + alert('Erro ao carregar vídeo. Verifique se o YouTube API está carregado.'); + } +} + +function onPlayerReady(event) { + console.log('✅ Player pronto!'); + try { + appState.videoDuration = appState.player.getDuration(); + updateDurationDisplay(); + createTimelineRuler(); + } catch (error) { + console.error('Erro ao configurar player:', error); + } +} + +function onPlayerStateChange(event) { + appState.isPlaying = (event.data === YT.PlayerState.PLAYING); +} + +function togglePlayPause() { + if (!appState.player) { + alert('Carregue um vídeo primeiro!'); + return; + } + + try { + if (appState.isPlaying) { + appState.player.pauseVideo(); + } else { + appState.player.playVideo(); + } + } catch (error) { + console.error('Erro ao controlar reprodução:', error); + } +} + +function seekVideo(seconds) { + if (!appState.player) return; + + try { + const currentTime = appState.player.getCurrentTime(); + const newTime = Math.max(0, Math.min(appState.videoDuration, currentTime + seconds)); + appState.player.seekTo(newTime, true); + } catch (error) { + console.error('Erro ao buscar tempo:', error); + } +} + +// ===== TIMELINE ===== +function updateTimeline() { + if (!appState.player || typeof appState.player.getCurrentTime !== 'function') return; + + try { + appState.currentTime = appState.player.getCurrentTime(); + const currentTimeEl = document.getElementById('currentTime'); + if (currentTimeEl) { + currentTimeEl.textContent = formatTime(appState.currentTime); + } + + // Atualizar marcador de tempo + const timeline = document.getElementById('timeline'); + if (!timeline) return; + + let marker = timeline.querySelector('.time-marker'); + + if (!marker) { + marker = document.createElement('div'); + marker.className = 'time-marker'; + timeline.appendChild(marker); + } + + if (appState.videoDuration > 0) { + const percent = (appState.currentTime / appState.videoDuration) * 100; + marker.style.left = percent + '%'; + } + } catch (error) { + // Silencioso - erro esperado antes do vídeo carregar + } +} + +function updateDurationDisplay() { + const durationEl = document.getElementById('duration'); + if (durationEl) { + durationEl.textContent = formatTime(appState.videoDuration); + } +} + +function formatTime(seconds) { + if (isNaN(seconds) || seconds < 0) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function createTimelineRuler() { + const ruler = document.getElementById('timelineRuler'); + if (!ruler) return; + + ruler.innerHTML = ''; + + const duration = appState.videoDuration; + if (!duration || duration <= 0) return; + + const interval = duration > 120 ? 10 : 5; // Intervalos de 10s ou 5s + + for (let i = 0; i <= duration; i += interval) { + const marker = document.createElement('div'); + marker.style.position = 'absolute'; + marker.style.left = ((i / duration) * 100) + '%'; + marker.style.height = '100%'; + marker.style.borderLeft = '1px solid #999'; + marker.style.fontSize = '0.8em'; + marker.style.paddingLeft = '3px'; + marker.textContent = formatTime(i); + ruler.appendChild(marker); + } +} + +// ===== NOTAS ===== +function selectNote(button) { + // Remover seleção anterior + document.querySelectorAll('.note-btn').forEach(btn => { + btn.classList.remove('selected'); + }); + + // Adicionar nova seleção + button.classList.add('selected'); + appState.selectedNote = button.dataset.note; +} + +function addNoteAtCurrentTime() { + if (!appState.selectedNote) { + alert('Selecione uma nota primeiro!'); + return; + } + + if (!appState.player) { + alert('Carregue um vídeo primeiro!'); + return; + } + + const octaveInput = document.getElementById('octave'); + const durationInput = document.getElementById('noteDuration'); + + const octave = octaveInput ? parseInt(octaveInput.value) : 4; + const duration = durationInput ? parseFloat(durationInput.value) : 0.5; + const startTime = appState.currentTime; + + const note = { + id: Date.now() + Math.random(), + note: appState.selectedNote, + octave: octave, + startTime: startTime, + duration: duration, + color: NOTE_COLORS[appState.selectedNote] || '#ccc', + source: 'manual' + }; + + appState.notes.push(note); + renderNotes(); + updateNotesList(); +} + +function renderNotes() { + const timeline = document.getElementById('timeline'); + if (!timeline) return; + + // Remover blocos de notas existentes + timeline.querySelectorAll('.note-block').forEach(block => block.remove()); + + // Adicionar novos blocos + appState.notes.forEach(note => { + const block = createNoteBlock(note); + if (block) timeline.appendChild(block); + }); +} + +function createNoteBlock(note) { + if (!appState.videoDuration || appState.videoDuration <= 0) return null; + + const block = document.createElement('div'); + block.className = 'note-block'; + block.dataset.id = note.id; + + const startPercent = (note.startTime / appState.videoDuration) * 100; + const widthPercent = (note.duration / appState.videoDuration) * 100; + + block.style.left = startPercent + '%'; + block.style.width = Math.max(widthPercent, 1) + '%'; + block.style.backgroundColor = note.color || '#ccc'; + block.style.top = '50%'; + block.style.transform = 'translateY(-50%)'; + + // Texto da nota + const label = document.createElement('span'); + label.textContent = `${note.note}${note.octave}`; + block.appendChild(label); + + // Botão de deletar + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'delete-btn'; + deleteBtn.textContent = '×'; + deleteBtn.onclick = (e) => { + e.stopPropagation(); + deleteNote(note.id); + }; + block.appendChild(deleteBtn); + + // Click para ir ao tempo da nota + block.onclick = () => { + if (appState.player && typeof appState.player.seekTo === 'function') { + appState.player.seekTo(note.startTime, true); + } + }; + + return block; +} + +function updateNotesList() { + const list = document.getElementById('notesList'); + if (!list) return; + + list.innerHTML = ''; + + if (appState.notes.length === 0) { + list.innerHTML = '

Nenhuma nota adicionada ainda.

'; + return; + } + + // Ordenar notas por tempo + const sortedNotes = [...appState.notes].sort((a, b) => a.startTime - b.startTime); + + sortedNotes.forEach(note => { + const item = document.createElement('div'); + item.className = 'note-item'; + + const colorDiv = document.createElement('div'); + colorDiv.className = 'note-color'; + colorDiv.style.backgroundColor = note.color || '#ccc'; + + const info = document.createElement('div'); + info.className = 'note-info'; + info.innerHTML = ` + ${note.note}${note.octave} - + Início: ${formatTime(note.startTime)} - + Duração: ${note.duration}s + `; + + const goBtn = document.createElement('button'); + goBtn.textContent = '▶️ Ir'; + goBtn.onclick = () => { + if (appState.player && typeof appState.player.seekTo === 'function') { + appState.player.seekTo(note.startTime, true); + } + }; + + const delBtn = document.createElement('button'); + delBtn.textContent = '🗑️'; + delBtn.style.background = '#dc3545'; + delBtn.onclick = () => deleteNote(note.id); + + item.appendChild(colorDiv); + item.appendChild(info); + item.appendChild(goBtn); + item.appendChild(delBtn); + + list.appendChild(item); + }); +} + +function deleteNote(id) { + appState.notes = appState.notes.filter(note => note.id !== id); + renderNotes(); + updateNotesList(); +} + +// ===== PROMPTS ===== +function generatePrompts() { + const prompts = []; + const noteCount = appState.notes.length; + + if (noteCount === 0) { + alert('Adicione algumas notas primeiro!'); + return; + } + + const uniqueNotes = new Set(appState.notes.map(n => n.note)); + const hasBlackKeys = appState.notes.some(n => n.note.includes('#')); + + prompts.push('=== DICAS PERSONALIZADAS PARA ESTE VÍDEO ===\n'); + prompts.push('📚 VISÃO GERAL:'); + prompts.push(`- Total de notas: ${noteCount}`); + prompts.push(`- Notas únicas: ${uniqueNotes.size}`); + prompts.push(`- Usa teclas pretas: ${hasBlackKeys ? 'Sim' : 'Não'}\n`); + + prompts.push('✋ POSIÇÃO DAS MÃOS:'); + prompts.push('- Mantenha os dedos curvados, como se segurasse uma bola'); + prompts.push('- Pulsos alinhados com os antebraços'); + prompts.push('- Use o peso natural do braço\n'); + + prompts.push('💪 INTENSIDADE:'); + prompts.push('- Comece em mezzoforte (volume médio)'); + prompts.push('- Pressione com firmeza, sem bater'); + prompts.push('- Varie a intensidade para expressão\n'); + + prompts.push('🎵 USO DO PEDAL:'); + if (noteCount > 10) { + prompts.push('- Use o pedal sustain para conectar notas'); + prompts.push('- Pressione DEPOIS de tocar a nota'); + } else { + prompts.push('- Para iniciantes: pratique sem pedal primeiro'); + } + prompts.push('- Mantenha o calcanhar no chão\n'); + + prompts.push('⏱️ RITMO:'); + prompts.push('- Use metrônomo'); + prompts.push('- Comece devagar e aumente gradualmente'); + prompts.push('- Conte em voz alta: 1-2-3-4\n'); + + prompts.push('🎹 TÉCNICA:'); + if (hasBlackKeys) { + prompts.push('- Para teclas pretas: mova a mão para frente'); + } + prompts.push('- Pratique cada mão separadamente'); + prompts.push('- Relaxe os ombros\n'); + + prompts.push('🎓 DICAS DE APRENDIZADO:'); + prompts.push('- Pratique 15-30 min por dia'); + prompts.push('- Grave-se para identificar melhorias'); + prompts.push('- Seja paciente e consistente'); + prompts.push('- Divirta-se! 🎶'); + + const textarea = document.getElementById('promptsText'); + if (textarea) { + textarea.value = prompts.join('\n'); + } +} + +function addCategoryPrompt(category) { + const textarea = document.getElementById('promptsText'); + if (!textarea) return; + + const prompts = { + posicao: '\n\n✋ POSIÇÃO DAS MÃOS:\n- Dedos curvados naturalmente\n- Pulsos nivelados\n- Polegares relaxados', + intensidade: '\n\n💪 INTENSIDADE:\n- Toque firme mas controlado\n- Varie entre piano e forte\n- Use peso do braço', + pedal: '\n\n🎵 PEDAL:\n- Pressione após tocar\n- Solte ao mudar harmonia\n- Calcanhar no chão', + ritmo: '\n\n⏱️ RITMO:\n- Use metrônomo\n- Comece lento (60-80 BPM)\n- Conte em voz alta', + tecnica: '\n\n🎹 TÉCNICA:\n- Pratique mãos separadas\n- Relaxe ombros\n- Respire naturalmente' + }; + + textarea.value += prompts[category] || ''; +} + +// ===== EXPORTAÇÃO ===== +function saveProject() { + const project = { + version: '1.0', + videoId: appState.videoId, + videoUrl: document.getElementById('videoUrl')?.value || '', + notes: appState.notes, + prompts: document.getElementById('promptsText')?.value || '', + createdAt: new Date().toISOString() + }; + + const blob = new Blob([JSON.stringify(project, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `music-notes-${appState.videoId || 'project'}-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); +} + +function loadProjectFile(event) { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const project = JSON.parse(e.target.result); + + const videoUrlInput = document.getElementById('videoUrl'); + const promptsTextarea = document.getElementById('promptsText'); + + if (videoUrlInput && project.videoUrl) { + videoUrlInput.value = project.videoUrl; + } + + appState.notes = project.notes || []; + + if (promptsTextarea && project.prompts) { + promptsTextarea.value = project.prompts; + } + + if (project.videoUrl) { + loadVideo(); + } + + renderNotes(); + updateNotesList(); + + alert('✅ Projeto carregado com sucesso!'); + } catch (error) { + console.error('Erro ao carregar projeto:', error); + alert('❌ Erro ao carregar projeto: ' + error.message); + } + }; + reader.readAsText(file); +} + +function exportNotesCSV() { + if (appState.notes.length === 0) { + alert('Adicione algumas notas primeiro!'); + return; + } + + let csv = 'Nota,Oitava,Tempo Início (s),Duração (s),Cor,Fonte\n'; + + appState.notes + .sort((a, b) => a.startTime - b.startTime) + .forEach(note => { + csv += `${note.note},${note.octave},${note.startTime.toFixed(2)},${note.duration},${note.color},${note.source || 'manual'}\n`; + }); + + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `music-notes-${appState.videoId || 'export'}-${Date.now()}.csv`; + a.click(); + URL.revokeObjectURL(url); +} + +// Exportar para uso global +if (typeof window !== 'undefined') { + window.appState = appState; + window.NOTE_COLORS = NOTE_COLORS; + window.renderNotes = renderNotes; + window.updateNotesList = updateNotesList; +} diff --git a/music-video-note-editor/index.html b/music-video-note-editor/index.html new file mode 100644 index 00000000..3c962438 --- /dev/null +++ b/music-video-note-editor/index.html @@ -0,0 +1,223 @@ + + + + + + Editor e Tutor de Notas Musicais + + + +
+
+

🎹 Editor e Tutor Inteligente de Notas Musicais

+

Aprenda piano/teclado com feedback em tempo real e IA

+
+ +
+ +
+

1. Adicionar Vídeo do YouTube

+
+ + +
+
+ + +
+

2. Visualizar Vídeo

+
+
+
+
+ + + + 0:00 / 0:00 +
+
+ + +
+

3. Transcrição Automática (Áudio → MIDI)

+
+

Extraia as notas automaticamente do áudio do vídeo usando IA

+ +
+
+ + +
+
+
+ + +
+

4. Conectar Teclado MIDI

+
+

Conecte seu teclado MIDI e pratique em tempo real

+ +
Status: Desconectado
+
+
+

Últimas Notas Tocadas:

+
+
+
+
+ + +
+

5. Modo Prática Guiada

+
+

Pratique com feedback em tempo real - toque as notas corretas no momento certo!

+
+ + + +
+
+
+

Acertos

+
0
+
+
+

Erros

+
0
+
+
+

Precisão

+
100%
+
+
+

Streak

+
0 🔥
+
+
+
+ + +
+
+
+ + +
+

6. Visualização de Partitura

+
+ +
+ + +
+
+
+
+ + +
+

7. Adicionar Notas Manualmente

+ +
+
+

Selecione a Nota:

+
+ + + + + + + + + + + + +
+
+ + +
+
+ + +
+ + +
+

Timeline de Notas

+
+
+
+ + +
+

Notas Adicionadas

+
+
+
+ + +
+

8. Prompts e Dicas de Aprendizado

+ +
+

Dicas de Técnica:

+ +
+ + + + + +
+
+
+ + +
+

9. Salvar e Exportar

+
+ + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + diff --git a/music-video-note-editor/midi-handler.js b/music-video-note-editor/midi-handler.js new file mode 100644 index 00000000..9faafc63 --- /dev/null +++ b/music-video-note-editor/midi-handler.js @@ -0,0 +1,220 @@ +// ===== MIDI HANDLER ===== +// Gerenciamento de conexão e entrada de teclados MIDI usando WebMIDI.js + +const MIDIHandler = { + enabled: false, + connectedDevices: [], + currentInput: null, + noteCallbacks: [], + lastNotes: [], + + // Inicializar WebMIDI + async init() { + // Verificar se WebMIDI está disponível + if (typeof WebMidi === 'undefined') { + console.warn('WebMIDI.js não está carregado'); + return false; + } + + try { + await WebMidi.enable(); + this.enabled = true; + console.log('✅ WebMIDI habilitado!'); + this.updateStatus('WebMIDI pronto para conectar'); + return true; + } catch (err) { + console.error('Erro ao habilitar WebMIDI:', err); + this.updateStatus('Erro: ' + err.message); + return false; + } + }, + + // Conectar dispositivo MIDI + async connect() { + if (!this.enabled) { + const initialized = await this.init(); + if (!initialized) return false; + } + + const inputs = WebMidi.inputs; + + if (inputs.length === 0) { + this.updateStatus('Nenhum dispositivo MIDI encontrado. Conecte um teclado MIDI e tente novamente.'); + alert('⚠️ Nenhum dispositivo MIDI encontrado!\n\nVerifique se:\n- O teclado está ligado\n- O cabo USB está conectado\n- O dispositivo está reconhecido pelo sistema'); + return false; + } + + // Mostrar dispositivos disponíveis + this.displayDevices(inputs); + + // Conectar ao primeiro dispositivo + this.currentInput = inputs[0]; + this.setupInputListeners(this.currentInput); + + this.updateStatus(`✅ Conectado: ${this.currentInput.name}`); + console.log(`🎹 MIDI conectado: ${this.currentInput.name}`); + return true; + }, + + // Configurar listeners de entrada MIDI + setupInputListeners(input) { + // Note On + input.addListener('noteon', e => { + const noteData = { + note: e.note.name + (e.note.accidental || ''), + octave: e.note.octave, + number: e.note.number, + velocity: e.rawVelocity, + timestamp: Date.now() + }; + + this.handleNoteOn(noteData); + }); + + // Note Off + input.addListener('noteoff', e => { + const noteData = { + note: e.note.name + (e.note.accidental || ''), + octave: e.note.octave, + number: e.note.number, + timestamp: Date.now() + }; + + this.handleNoteOff(noteData); + }); + }, + + // Handle note on + handleNoteOn(noteData) { + // Adicionar às últimas notas + this.lastNotes.unshift(noteData); + if (this.lastNotes.length > 10) { + this.lastNotes.pop(); + } + + // Atualizar display + this.updateNoteDisplay(); + + // Chamar callbacks + this.noteCallbacks.forEach(callback => { + try { + callback('noteon', noteData); + } catch (error) { + console.error('Erro em callback MIDI:', error); + } + }); + }, + + // Handle note off + handleNoteOff(noteData) { + this.noteCallbacks.forEach(callback => { + try { + callback('noteoff', noteData); + } catch (error) { + console.error('Erro em callback MIDI:', error); + } + }); + }, + + // Registrar callback para eventos de nota + onNote(callback) { + if (typeof callback === 'function') { + this.noteCallbacks.push(callback); + } + }, + + // Remover callback + removeCallback(callback) { + const index = this.noteCallbacks.indexOf(callback); + if (index > -1) { + this.noteCallbacks.splice(index, 1); + } + }, + + // Atualizar status + updateStatus(message) { + const statusEl = document.getElementById('midiStatus'); + if (statusEl) { + statusEl.textContent = 'Status: ' + message; + statusEl.className = 'status-message ' + + (message.includes('Conectado') || message.includes('✅') ? 'success' : + message.includes('Erro') || message.includes('❌') ? 'error' : + message.includes('⚠️') ? 'warning' : 'info'); + } + }, + + // Mostrar dispositivos disponíveis + displayDevices(inputs) { + const devicesEl = document.getElementById('midiDevices'); + if (!devicesEl) return; + + devicesEl.innerHTML = '

Dispositivos MIDI Disponíveis:

'; + + inputs.forEach((input, index) => { + const deviceDiv = document.createElement('div'); + deviceDiv.className = 'device-item'; + deviceDiv.innerHTML = ` + ${input.name} + ${input.manufacturer ? `(${input.manufacturer})` : ''} + ${index === 0 ? 'Conectado' : ''} + `; + devicesEl.appendChild(deviceDiv); + }); + }, + + // Atualizar display de notas + updateNoteDisplay() { + const displayEl = document.getElementById('midiNoteDisplay'); + if (!displayEl) return; + + displayEl.innerHTML = ''; + + this.lastNotes.slice(0, 5).forEach(note => { + const noteDiv = document.createElement('div'); + noteDiv.className = 'midi-note-item'; + + const noteName = this.convertToPortugueseName(note.note); + const color = (typeof NOTE_COLORS !== 'undefined' && NOTE_COLORS[note.note]) || '#ccc'; + + noteDiv.innerHTML = ` +
+ ${noteName}${note.octave} - Vel: ${note.velocity} + `; + displayEl.appendChild(noteDiv); + }); + + if (this.lastNotes.length === 0) { + displayEl.innerHTML = '

Toque algumas notas no seu teclado MIDI...

'; + } + }, + + // Converter nome de nota para português + convertToPortugueseName(note) { + const nameMap = { + 'C': 'Dó', 'C#': 'Dó#', 'Db': 'Réb', + 'D': 'Ré', 'D#': 'Ré#', 'Eb': 'Mib', + 'E': 'Mi', + 'F': 'Fá', 'F#': 'Fá#', 'Gb': 'Solb', + 'G': 'Sol', 'G#': 'Sol#', 'Ab': 'Láb', + 'A': 'Lá', 'A#': 'Lá#', 'Bb': 'Sib', + 'B': 'Si' + }; + return nameMap[note] || note; + }, + + // Desconectar + disconnect() { + if (this.currentInput && typeof this.currentInput.removeListener === 'function') { + this.currentInput.removeListener(); + this.currentInput = null; + } + this.lastNotes = []; + this.updateStatus('Desconectado'); + this.updateNoteDisplay(); + } +}; + +// Exportar para uso global +if (typeof window !== 'undefined') { + window.MIDIHandler = MIDIHandler; +} diff --git a/music-video-note-editor/practice-mode.js b/music-video-note-editor/practice-mode.js new file mode 100644 index 00000000..61a82d0d --- /dev/null +++ b/music-video-note-editor/practice-mode.js @@ -0,0 +1,424 @@ +// ===== PRACTICE MODE ===== +// Modo de prática guiada com feedback em tempo real, gamificação e métricas + +const PracticeMode = { + isActive: false, + startTime: null, + currentNoteIndex: 0, + expectedNotes: [], + playedNotes: [], + animationFrame: null, + + // Estatísticas + stats: { + correct: 0, + wrong: 0, + streak: 0, + maxStreak: 0, + totalNotes: 0, + accuracy: 100, + timingErrors: [], + pitchErrors: [] + }, + + // Configurações + config: { + timingTolerance: 500, // ms + enableVisualFeedback: true + }, + + // Canvas + canvas: null, + ctx: null, + + // Inicializar + init() { + this.canvas = document.getElementById('noteVisualization'); + if (this.canvas) { + this.ctx = this.canvas.getContext('2d'); + } + + // Registrar callback MIDI + if (typeof MIDIHandler !== 'undefined') { + MIDIHandler.onNote((type, noteData) => { + if (this.isActive && type === 'noteon') { + this.handleNotePlayed(noteData); + } + }); + } + }, + + // Iniciar prática + start() { + if (typeof appState === 'undefined' || appState.notes.length === 0) { + alert('❌ Adicione algumas notas primeiro!\n\nDica: Use a transcrição automática ou adicione manualmente.'); + return; + } + + // Verificar MIDI + if (typeof MIDIHandler === 'undefined' || !MIDIHandler.enabled || !MIDIHandler.currentInput) { + const tryConnect = confirm('❌ Nenhum teclado MIDI conectado!\n\nDeseja conectar agora?'); + if (tryConnect && typeof MIDIHandler !== 'undefined') { + MIDIHandler.connect(); + } + return; + } + + this.isActive = true; + this.startTime = Date.now(); + this.currentNoteIndex = 0; + + // Preparar notas + this.expectedNotes = [...appState.notes].sort((a, b) => a.startTime - b.startTime); + this.playedNotes = []; + + // Reset stats + this.resetStats(); + this.updateUI(); + this.updateButtons(true); + + // Iniciar vídeo + if (appState.player && typeof appState.player.seekTo === 'function') { + appState.player.seekTo(0, true); + appState.player.playVideo(); + } + + // Iniciar visualização + this.startVisualization(); + + this.showFeedback('🎵 Prática iniciada! Toque as notas no momento certo!', 'info'); + }, + + // Parar prática + stop() { + this.isActive = false; + + if (appState && appState.player && typeof appState.player.pauseVideo === 'function') { + appState.player.pauseVideo(); + } + + this.updateButtons(false); + this.stopVisualization(); + + if (this.stats.totalNotes > 0) { + this.showFinalReport(); + } + }, + + // Reset + reset() { + this.stop(); + this.resetStats(); + this.updateUI(); + this.showFeedback('🔄 Prática resetada. Clique em "Iniciar" quando estiver pronto!', 'info'); + }, + + // Reset stats + resetStats() { + this.stats = { + correct: 0, + wrong: 0, + streak: 0, + maxStreak: 0, + totalNotes: 0, + accuracy: 100, + timingErrors: [], + pitchErrors: [] + }; + }, + + // Handle nota tocada + handleNotePlayed(noteData) { + if (!this.isActive || this.currentNoteIndex >= this.expectedNotes.length) { + return; + } + + const expected = this.expectedNotes[this.currentNoteIndex]; + const currentTime = appState.currentTime; + + this.playedNotes.push({ ...noteData, timestamp: currentTime }); + + const isCorrectPitch = this.checkPitch(noteData, expected); + const isCorrectTiming = this.checkTiming(currentTime, expected); + + if (isCorrectPitch && isCorrectTiming) { + this.handleCorrectNote(noteData, expected); + } else { + this.handleWrongNote(noteData, expected, isCorrectPitch, isCorrectTiming); + } + + this.updateUI(); + }, + + // Verificar pitch + checkPitch(played, expected) { + const playedNote = played.note.replace(/[♯♭#b]/g, match => match === '♯' || match === '#' ? '#' : 'b'); + return playedNote === expected.note && played.octave === expected.octave; + }, + + // Verificar timing + checkTiming(playedTime, expectedNote) { + const timeDiff = Math.abs(playedTime - expectedNote.startTime); + return timeDiff <= (this.config.timingTolerance / 1000); + }, + + // Nota correta + handleCorrectNote(played, expected) { + this.stats.correct++; + this.stats.streak++; + this.stats.totalNotes++; + + if (this.stats.streak > this.stats.maxStreak) { + this.stats.maxStreak = this.stats.streak; + } + + this.calculateAccuracy(); + + this.showFeedback(`✅ Correto! ${expected.note}${expected.octave}`, 'success'); + this.flashNote(expected, 'green'); + + if (this.stats.streak > 0 && this.stats.streak % 5 === 0) { + this.showFeedback(`🔥 INCRÍVEL! Streak de ${this.stats.streak}!`, 'success'); + } + + this.currentNoteIndex++; + + if (this.currentNoteIndex >= this.expectedNotes.length) { + setTimeout(() => this.stop(), 1000); + } + }, + + // Nota errada + handleWrongNote(played, expected, correctPitch, correctTiming) { + this.stats.wrong++; + this.stats.streak = 0; + this.stats.totalNotes++; + + this.calculateAccuracy(); + + let errorMsg = '❌ '; + if (!correctPitch && !correctTiming) { + errorMsg += `Nota e timing errados! Esperava: ${expected.note}${expected.octave}`; + this.stats.pitchErrors.push(expected); + this.stats.timingErrors.push(expected); + } else if (!correctPitch) { + errorMsg += `Nota errada! Esperava: ${expected.note}${expected.octave}`; + this.stats.pitchErrors.push(expected); + } else { + errorMsg += `Timing errado! Toque no momento certo.`; + this.stats.timingErrors.push(expected); + } + + this.showFeedback(errorMsg, 'error'); + this.flashNote(expected, 'red'); + + this.currentNoteIndex++; + }, + + // Calcular precisão + calculateAccuracy() { + if (this.stats.totalNotes > 0) { + this.stats.accuracy = Math.round((this.stats.correct / this.stats.totalNotes) * 100); + } + }, + + // Visualização + startVisualization() { + if (!this.canvas || !this.ctx) return; + + const draw = () => { + if (!this.isActive) return; + + try { + this.clearCanvas(); + this.drawUpcomingNotes(); + this.drawCurrentNote(); + this.drawProgress(); + + this.animationFrame = requestAnimationFrame(draw); + } catch (error) { + console.error('Erro na visualização:', error); + } + }; + + draw(); + }, + + stopVisualization() { + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + if (this.canvas && this.ctx) { + this.clearCanvas(); + } + }, + + clearCanvas() { + this.ctx.fillStyle = '#f8f9fa'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + }, + + drawUpcomingNotes() { + if (!appState) return; + + const currentTime = appState.currentTime; + const lookAhead = 5; + + const upcomingNotes = this.expectedNotes.filter(note => + note.startTime >= currentTime && note.startTime <= currentTime + lookAhead + ); + + upcomingNotes.forEach(note => { + const x = ((note.startTime - currentTime) / lookAhead) * this.canvas.width; + const y = this.canvas.height / 2; + + this.ctx.fillStyle = note.color || '#667eea'; + this.ctx.fillRect(x - 5, y - 20, 10, 40); + + this.ctx.fillStyle = '#000'; + this.ctx.font = '12px Arial'; + this.ctx.textAlign = 'center'; + this.ctx.fillText(`${note.note}${note.octave}`, x, y + 55); + }); + }, + + drawCurrentNote() { + if (this.currentNoteIndex >= this.expectedNotes.length) return; + + const note = this.expectedNotes[this.currentNoteIndex]; + + this.ctx.strokeStyle = '#ff0000'; + this.ctx.lineWidth = 3; + this.ctx.strokeRect(50, this.canvas.height / 2 - 25, 20, 50); + + this.ctx.fillStyle = '#000'; + this.ctx.font = 'bold 16px Arial'; + this.ctx.textAlign = 'center'; + this.ctx.fillText(`AGORA: ${note.note}${note.octave}`, 60, this.canvas.height / 2 - 35); + }, + + drawProgress() { + const progress = (this.currentNoteIndex / this.expectedNotes.length) * 100; + + this.ctx.fillStyle = '#e0e0e0'; + this.ctx.fillRect(0, this.canvas.height - 20, this.canvas.width, 20); + + this.ctx.fillStyle = '#4caf50'; + this.ctx.fillRect(0, this.canvas.height - 20, (progress / 100) * this.canvas.width, 20); + + this.ctx.fillStyle = '#000'; + this.ctx.font = '12px Arial'; + this.ctx.textAlign = 'center'; + this.ctx.fillText(`${Math.round(progress)}%`, this.canvas.width / 2, this.canvas.height - 6); + }, + + flashNote(note, color) { + const noteBlocks = document.querySelectorAll('.note-block'); + noteBlocks.forEach(block => { + if (parseInt(block.dataset.id) === note.id) { + block.style.boxShadow = `0 0 20px ${color}`; + setTimeout(() => { block.style.boxShadow = ''; }, 500); + } + }); + }, + + showFeedback(message, type = 'info') { + const feedbackEl = document.getElementById('feedbackMessage'); + if (feedbackEl) { + feedbackEl.textContent = message; + feedbackEl.className = 'feedback-message ' + type; + + setTimeout(() => { + if (feedbackEl.textContent === message) { + feedbackEl.textContent = ''; + } + }, 3000); + } + }, + + updateUI() { + const elements = { + correctNotes: document.getElementById('correctNotes'), + wrongNotes: document.getElementById('wrongNotes'), + accuracy: document.getElementById('accuracy'), + streak: document.getElementById('streak') + }; + + if (elements.correctNotes) elements.correctNotes.textContent = this.stats.correct; + if (elements.wrongNotes) elements.wrongNotes.textContent = this.stats.wrong; + if (elements.accuracy) elements.accuracy.textContent = this.stats.accuracy + '%'; + if (elements.streak) elements.streak.textContent = this.stats.streak + ' 🔥'; + }, + + updateButtons(practicing) { + const startBtn = document.getElementById('startPractice'); + const stopBtn = document.getElementById('stopPractice'); + + if (startBtn) startBtn.disabled = practicing; + if (stopBtn) stopBtn.disabled = !practicing; + }, + + showFinalReport() { + const report = ` +🎉 PRÁTICA CONCLUÍDA! + +📊 Estatísticas: +• Acertos: ${this.stats.correct} +• Erros: ${this.stats.wrong} +• Precisão: ${this.stats.accuracy}% +• Melhor streak: ${this.stats.maxStreak} + +${this.getPerformanceAnalysis()} + +${this.getImprovementTips()} + `.trim(); + + this.showFeedback(report, 'success'); + console.log(report); + alert(report); + }, + + getPerformanceAnalysis() { + let analysis = '🎯 ANÁLISE:\n'; + + if (this.stats.accuracy >= 90) { + analysis += '• Excelente! Você está dominando esta música! 🌟'; + } else if (this.stats.accuracy >= 70) { + analysis += '• Bom trabalho! Continue praticando para melhorar.'; + } else { + analysis += '• Continue praticando! A repetição é a chave.'; + } + + if (this.stats.pitchErrors.length > this.stats.timingErrors.length) { + analysis += '\n• Foco nas notas corretas (pitch).'; + } else if (this.stats.timingErrors.length > this.stats.pitchErrors.length) { + analysis += '\n• Foco no timing (momento certo).'; + } + + return analysis; + }, + + getImprovementTips() { + let tips = '\n💡 DICAS:\n'; + + if (this.stats.timingErrors.length > 3) { + tips += '• Use metrônomo\n• Pratique mais devagar\n'; + } + + if (this.stats.pitchErrors.length > 3) { + tips += '• Revise as notas\n• Pratique seções isoladamente\n'; + } + + if (this.stats.accuracy < 80) { + tips += '• Divida em seções menores\n• Pratique 15-20 min/dia\n'; + } + + return tips; + } +}; + +// Exportar +if (typeof window !== 'undefined') { + window.PracticeMode = PracticeMode; +} diff --git a/music-video-note-editor/score-renderer.js b/music-video-note-editor/score-renderer.js new file mode 100644 index 00000000..d420ba49 --- /dev/null +++ b/music-video-note-editor/score-renderer.js @@ -0,0 +1,233 @@ +// ===== SCORE RENDERER ===== +// Renderização de partituras musicais usando VexFlow + +const ScoreRenderer = { + vf: null, + renderer: null, + context: null, + + // Inicializar VexFlow + init() { + if (typeof Vex === 'undefined') { + console.error('❌ VexFlow não está carregado!'); + return false; + } + + this.vf = Vex.Flow; + return true; + }, + + // Gerar partitura + generateScore() { + if (!this.init()) { + alert('❌ Erro ao carregar VexFlow.\n\nVerifique sua conexão com a internet e recarregue a página.'); + return; + } + + if (typeof appState === 'undefined' || appState.notes.length === 0) { + alert('❌ Adicione algumas notas primeiro!'); + return; + } + + const outputDiv = document.getElementById('scoreOutput'); + if (!outputDiv) return; + + outputDiv.innerHTML = ''; // Limpar + + try { + const width = Math.min(800, outputDiv.clientWidth || 800); + const height = 300; + + this.renderer = new this.vf.Renderer(outputDiv, this.vf.Renderer.Backends.SVG); + this.renderer.resize(width, height); + this.context = this.renderer.getContext(); + + const clefType = document.getElementById('clefType')?.value || 'treble'; + const timeSignature = document.getElementById('timeSignature')?.value || '4/4'; + + // Criar pauta + const stave = new this.vf.Stave(10, 40, width - 20); + stave.addClef(clefType); + stave.addTimeSignature(timeSignature); + stave.setContext(this.context).draw(); + + // Converter notas + const vexNotes = this.convertNotesToVexFlow(appState.notes, clefType); + + if (vexNotes.length > 0) { + const voice = new this.vf.Voice({ + num_beats: 4, + beat_value: 4 + }); + voice.addTickables(vexNotes); + + const formatter = new this.vf.Formatter(); + formatter.joinVoices([voice]).format([voice], width - 50); + voice.draw(this.context, stave); + + this.colorizeNotes(vexNotes, appState.notes); + } + + console.log('✅ Partitura gerada com sucesso!'); + + } catch (error) { + console.error('Erro ao gerar partitura:', error); + alert('❌ Erro ao gerar partitura:\n' + error.message); + } + }, + + // Converter notas para VexFlow + convertNotesToVexFlow(notes, clef) { + const sortedNotes = [...notes].sort((a, b) => a.startTime - b.startTime); + const vexNotes = []; + + sortedNotes.slice(0, 16).forEach(note => { + try { + const vexNote = this.noteToVexFormat(note, clef); + const staveNote = new this.vf.StaveNote({ + clef: clef, + keys: [vexNote], + duration: this.durationToVexFormat(note.duration) + }); + + if (note.note.includes('#')) { + staveNote.addModifier(new this.vf.Accidental('#'), 0); + } else if (note.note.includes('b')) { + staveNote.addModifier(new this.vf.Accidental('b'), 0); + } + + vexNotes.push(staveNote); + + } catch (error) { + console.warn('⚠️ Erro ao converter nota:', note, error); + } + }); + + if (vexNotes.length === 0) { + vexNotes.push(new this.vf.StaveNote({ + clef: clef, + keys: ['b/4'], + duration: 'qr' + })); + } + + return vexNotes; + }, + + // Converter nota para formato VexFlow + noteToVexFormat(note, clef) { + let noteName = note.note.toLowerCase().replace('#', ''); + let octave = note.octave; + + if (clef === 'bass') { + octave = Math.max(2, octave - 1); + } + + return `${noteName}/${octave}`; + }, + + // Converter duração para formato VexFlow + durationToVexFormat(durationSeconds) { + if (durationSeconds < 0.3) return '16'; + if (durationSeconds < 0.6) return '8'; + if (durationSeconds < 1.2) return 'q'; + if (durationSeconds < 2.4) return 'h'; + return 'w'; + }, + + // Colorir notas + colorizeNotes(vexNotes, originalNotes) { + vexNotes.forEach((vexNote, index) => { + if (originalNotes[index] && originalNotes[index].color) { + try { + vexNote.setStyle({ + fillStyle: originalNotes[index].color, + strokeStyle: originalNotes[index].color + }); + } catch (error) { + console.warn('⚠️ Erro ao colorir nota:', error); + } + } + }); + }, + + // Exportar como imagem + exportAsImage() { + const outputDiv = document.getElementById('scoreOutput'); + const svg = outputDiv?.querySelector('svg'); + + if (!svg) { + alert('❌ Gere a partitura primeiro!'); + return; + } + + const svgData = new XMLSerializer().serializeToString(svg); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + canvas.toBlob((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `partitura-${Date.now()}.png`; + a.click(); + URL.revokeObjectURL(url); + }); + }; + + img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); + }, + + // Gerar MIDI (stub) + generateMIDI() { + console.log('📝 Exportação MIDI:'); + console.log('Para implementação completa, use @tonejs/midi ou midi-writer-js'); + + if (typeof appState === 'undefined' || !appState.notes) return; + + const midiData = { + header: { + format: 0, + numTracks: 1, + ticksPerBeat: 480 + }, + tracks: [{ + name: 'Piano', + notes: appState.notes.map(note => ({ + midi: this.noteToMIDINumber(note), + time: note.startTime, + duration: note.duration, + velocity: 0.8 + })) + }] + }; + + console.log('MIDI Data:', JSON.stringify(midiData, null, 2)); + alert('✅ Dados MIDI gerados!\n\nVeja o console do navegador para detalhes.\n\nPara exportação real de MIDI, integre @tonejs/midi.'); + }, + + // Converter nota para número MIDI + noteToMIDINumber(note) { + const noteMap = { + 'C': 0, 'C#': 1, 'D': 2, 'D#': 3, + 'E': 4, 'F': 5, 'F#': 6, 'G': 7, + 'G#': 8, 'A': 9, 'A#': 10, 'B': 11 + }; + + const noteValue = noteMap[note.note]; + if (noteValue === undefined) return 60; + + return (note.octave + 1) * 12 + noteValue; + } +}; + +// Exportar +if (typeof window !== 'undefined') { + window.ScoreRenderer = ScoreRenderer; +} diff --git a/music-video-note-editor/styles.css b/music-video-note-editor/styles.css new file mode 100644 index 00000000..be16d759 --- /dev/null +++ b/music-video-note-editor/styles.css @@ -0,0 +1,680 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #333; + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; + background: white; + border-radius: 15px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + text-align: center; +} + +header h1 { + font-size: 2.5em; + margin-bottom: 10px; +} + +header p { + font-size: 1.1em; + opacity: 0.9; +} + +main { + padding: 30px; +} + +section { + margin-bottom: 40px; + padding: 25px; + background: #f8f9fa; + border-radius: 10px; + border: 2px solid #e9ecef; +} + +h2 { + color: #667eea; + margin-bottom: 20px; + font-size: 1.8em; +} + +h3 { + color: #764ba2; + margin-bottom: 15px; + font-size: 1.3em; +} + +/* Video Input Section */ +.input-group { + display: flex; + gap: 10px; +} + +#videoUrl { + flex: 1; + padding: 12px; + border: 2px solid #667eea; + border-radius: 8px; + font-size: 1em; +} + +button { + padding: 12px 24px; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1em; + cursor: pointer; + transition: all 0.3s ease; +} + +button:hover { + background: #5568d3; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +button:active { + transform: translateY(0); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* Video Section */ +#videoContainer { + position: relative; + width: 100%; + max-width: 800px; + margin: 0 auto 20px; + background: #000; + border-radius: 10px; + overflow: hidden; +} + +#player { + width: 100%; + aspect-ratio: 16/9; +} + +.video-controls { + display: flex; + gap: 10px; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +#currentTime, #duration { + font-weight: bold; + color: #667eea; +} + +/* Transcription Section */ +.transcription-section, +.midi-section, +.practice-section, +.score-section { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +.feature-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 15px 30px; + font-size: 1.1em; + margin-bottom: 15px; +} + +.feature-btn:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.status-message { + padding: 12px; + border-radius: 8px; + margin: 10px 0; + font-weight: 500; +} + +.status-message.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.status-message.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.status-message.warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} + +.status-message.info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.transcription-options { + margin-top: 15px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.transcription-options label { + display: flex; + align-items: center; + gap: 10px; +} + +/* MIDI Section */ +.midi-controls { + display: flex; + flex-direction: column; + gap: 15px; +} + +.device-list { + background: white; + padding: 15px; + border-radius: 8px; + margin-top: 10px; +} + +.device-item { + padding: 10px; + background: #f8f9fa; + border-left: 4px solid #667eea; + margin-bottom: 8px; + border-radius: 4px; +} + +.badge { + background: #28a745; + color: white; + padding: 3px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-left: 10px; +} + +.midi-monitor { + background: white; + padding: 15px; + border-radius: 8px; +} + +.note-display { + min-height: 100px; + margin-top: 10px; +} + +.midi-note-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: #f8f9fa; + border-radius: 5px; + margin-bottom: 5px; +} + +/* Practice Section */ +.practice-controls { + display: flex; + flex-direction: column; + gap: 20px; +} + +.practice-buttons { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.practice-btn { + flex: 1; + min-width: 150px; + padding: 12px 20px; +} + +.practice-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin: 20px 0; +} + +.stat-box { + background: white; + padding: 20px; + border-radius: 10px; + text-align: center; + border: 2px solid #667eea; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.stat-box h4 { + color: #667eea; + margin-bottom: 10px; + font-size: 0.9em; + text-transform: uppercase; +} + +.stat-value { + font-size: 2em; + font-weight: bold; + color: #333; +} + +.practice-feedback { + background: white; + padding: 20px; + border-radius: 10px; +} + +.feedback-message { + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; + font-weight: 500; + min-height: 50px; +} + +#noteVisualization { + border: 2px solid #667eea; + border-radius: 8px; + background: #f8f9fa; + width: 100%; + max-width: 800px; + display: block; + margin: 0 auto; +} + +/* Score Section */ +.score-controls { + margin-bottom: 20px; +} + +.score-options { + display: flex; + gap: 20px; + margin-top: 15px; + flex-wrap: wrap; +} + +.score-options label { + display: flex; + align-items: center; + gap: 10px; + font-weight: bold; +} + +.score-options select, +.score-options input { + padding: 8px; + border: 2px solid #667eea; + border-radius: 5px; +} + +.score-output { + background: white; + padding: 20px; + border-radius: 8px; + margin-top: 20px; + min-height: 200px; + overflow-x: auto; +} + +.score-output svg { + max-width: 100%; + height: auto; +} + +/* Notes Editor Section */ +.note-controls { + margin-bottom: 30px; +} + +.note-selector { + background: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.note-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 10px; + margin-bottom: 20px; +} + +.note-btn { + padding: 15px; + font-size: 1em; + font-weight: bold; + border: 3px solid transparent; + transition: all 0.2s ease; +} + +.note-btn.selected { + border-color: #000; + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Sistema de cores das notas - naturais (claras) e sustenidos (escuros) */ +.note-btn[data-note="C"] { background: #90EE90; } /* Verde claro */ +.note-btn[data-note="C#"] { background: #2d5016; color: white; } /* Verde escuro */ +.note-btn[data-note="D"] { background: #87CEEB; } /* Azul claro */ +.note-btn[data-note="D#"] { background: #1e3a5f; color: white; } /* Azul escuro */ +.note-btn[data-note="E"] { background: #FFD700; } /* Amarelo claro */ +.note-btn[data-note="F"] { background: #FF6B6B; } /* Vermelho claro */ +.note-btn[data-note="F#"] { background: #8B0000; color: white; } /* Vermelho escuro */ +.note-btn[data-note="G"] { background: #DDA0DD; } /* Roxo claro */ +.note-btn[data-note="G#"] { background: #4B0082; color: white; } /* Roxo escuro */ +.note-btn[data-note="A"] { background: #FFA500; } /* Laranja claro */ +.note-btn[data-note="A#"] { background: #cc5200; color: white; } /* Laranja escuro */ +.note-btn[data-note="B"] { background: #FFB6C1; } /* Rosa claro */ + +.note-properties { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.note-properties label { + display: flex; + align-items: center; + gap: 10px; + font-weight: bold; +} + +.note-properties input { + padding: 8px; + border: 2px solid #667eea; + border-radius: 5px; + width: 80px; +} + +.add-note-btn { + width: 100%; + padding: 15px; + font-size: 1.2em; + background: #28a745; +} + +.add-note-btn:hover { + background: #218838; +} + +/* Timeline */ +.timeline-container { + margin-top: 30px; + background: white; + padding: 20px; + border-radius: 8px; +} + +.timeline { + position: relative; + height: 150px; + background: #f0f0f0; + border: 2px solid #667eea; + border-radius: 8px; + overflow-x: auto; + overflow-y: hidden; + margin-bottom: 10px; +} + +.timeline-ruler { + height: 30px; + background: #e9ecef; + border: 1px solid #ccc; + border-radius: 5px; + position: relative; + overflow: hidden; +} + +.time-marker { + position: absolute; + bottom: 0; + width: 2px; + height: 100%; + background: red; + z-index: 100; +} + +.note-block { + position: absolute; + height: 40px; + border-radius: 5px; + border: 2px solid rgba(0, 0, 0, 0.3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.9em; + transition: all 0.2s ease; + color: rgba(0, 0, 0, 0.8); +} + +.note-block:hover { + transform: translateY(-3px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 10; +} + +.note-block .delete-btn { + position: absolute; + top: -8px; + right: -8px; + width: 20px; + height: 20px; + padding: 0; + background: red; + border-radius: 50%; + font-size: 0.7em; + display: none; +} + +.note-block:hover .delete-btn { + display: block; +} + +/* Lista de notas */ +.notes-list { + background: white; + padding: 20px; + border-radius: 8px; + margin-top: 20px; + max-height: 300px; + overflow-y: auto; +} + +#notesList { + display: flex; + flex-direction: column; + gap: 10px; +} + +.note-item { + display: flex; + align-items: center; + gap: 15px; + padding: 10px; + background: #f8f9fa; + border-radius: 5px; + border-left: 5px solid #667eea; +} + +.note-color { + width: 30px; + height: 30px; + border-radius: 5px; + border: 2px solid #333; + flex-shrink: 0; +} + +.note-info { + flex: 1; +} + +.note-item button { + padding: 5px 10px; + font-size: 0.9em; +} + +/* Prompts Section */ +.prompt-container { + background: white; + padding: 20px; + border-radius: 8px; + margin-top: 20px; +} + +#promptsText { + width: 100%; + padding: 15px; + border: 2px solid #667eea; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + margin-bottom: 15px; + resize: vertical; +} + +.prompt-categories { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.prompt-btn { + background: #764ba2; +} + +.prompt-btn:hover { + background: #5e3a82; +} + +#generatePrompt { + background: #28a745; + font-size: 1.1em; + margin-bottom: 20px; +} + +#generatePrompt:hover { + background: #218838; +} + +/* Export Section */ +.export-controls { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.export-controls button { + flex: 1; + min-width: 200px; +} + +#saveProject { + background: #17a2b8; +} + +#saveProject:hover { + background: #138496; +} + +#loadProject { + background: #ffc107; + color: #333; +} + +#loadProject:hover { + background: #e0a800; +} + +#exportNotes { + background: #6c757d; +} + +#exportNotes:hover { + background: #5a6268; +} + +/* Footer */ +footer { + background: #333; + color: white; + text-align: center; + padding: 20px; +} + +/* Responsive */ +@media (max-width: 768px) { + header h1 { + font-size: 1.8em; + } + + .note-buttons { + grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); + } + + .export-controls button { + min-width: 100%; + } + + .practice-stats { + grid-template-columns: 1fr 1fr; + } + + .practice-buttons { + flex-direction: column; + } + + .practice-btn { + min-width: 100%; + } + + #noteVisualization { + width: 100%; + } + + .score-options { + flex-direction: column; + } +} diff --git a/music-video-note-editor/transcription-handler.js b/music-video-note-editor/transcription-handler.js new file mode 100644 index 00000000..98a121e5 --- /dev/null +++ b/music-video-note-editor/transcription-handler.js @@ -0,0 +1,180 @@ +// ===== TRANSCRIPTION HANDLER ===== +// Framework de integração para transcrição de áudio para MIDI +// Spotify Basic Pitch / Magenta Onsets & Frames + +const TranscriptionHandler = { + isProcessing: false, + + // Transcrever áudio do vídeo atual + async transcribeVideo() { + if (this.isProcessing) { + alert('Já existe uma transcrição em andamento...'); + return; + } + + if (typeof appState === 'undefined' || !appState.player) { + alert('Carregue um vídeo primeiro!'); + return; + } + + this.updateStatus('⚠️ MODO DEMONSTRAÇÃO: Transcrição simulada', 'warning'); + this.isProcessing = true; + + try { + await this.simulateTranscription(); + } catch (error) { + console.error('Erro na transcrição:', error); + this.updateStatus('❌ Erro: ' + error.message, 'error'); + } finally { + this.isProcessing = false; + } + }, + + // Simulação de transcrição para demonstração + async simulateTranscription() { + this.updateStatus('Analisando áudio... (0%)', 'info'); + + // Simular progresso + for (let i = 0; i <= 100; i += 10) { + await this.sleep(200); + this.updateStatus(`Analisando áudio... (${i}%)`, 'info'); + } + + // Gerar notas de exemplo + const duration = appState.videoDuration || 60; + const sampleNotes = this.generateSampleNotes(duration); + + this.updateStatus(`✅ Transcrição concluída! ${sampleNotes.length} notas detectadas.`, 'success'); + + // Adicionar notas se opção estiver marcada + const autoAdd = document.getElementById('autoAddTranscribedNotes'); + if (autoAdd && autoAdd.checked) { + this.addTranscribedNotes(sampleNotes); + } + + // Mostrar instruções + this.showRealIntegrationInfo(); + }, + + // Gerar notas de exemplo (demonstração) + generateSampleNotes(duration) { + const notes = []; + const noteNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + const octaves = [3, 4, 5]; + + let currentTime = 0; + while (currentTime < Math.min(duration, 30)) { + const note = noteNames[Math.floor(Math.random() * noteNames.length)]; + const octave = octaves[Math.floor(Math.random() * octaves.length)]; + const noteDuration = 0.3 + Math.random() * 0.7; + + notes.push({ + note: note, + octave: octave, + startTime: currentTime, + duration: noteDuration, + confidence: 0.7 + Math.random() * 0.3 + }); + + currentTime += 2 + Math.random() * 2; + } + + return notes; + }, + + // Adicionar notas transcritas + addTranscribedNotes(transcribedNotes) { + const thresholdEl = document.getElementById('confidenceThreshold'); + const confidenceThreshold = thresholdEl ? (thresholdEl.value / 100) : 0.7; + + transcribedNotes.forEach(note => { + if (note.confidence >= confidenceThreshold) { + const newNote = { + id: Date.now() + Math.random(), + note: note.note, + octave: note.octave, + startTime: note.startTime, + duration: note.duration, + color: (typeof NOTE_COLORS !== 'undefined' && NOTE_COLORS[note.note]) || '#ccc', + source: 'transcription', + confidence: note.confidence + }; + + if (typeof appState !== 'undefined') { + appState.notes.push(newNote); + } + } + }); + + // Atualizar visualizações + if (typeof renderNotes === 'function') renderNotes(); + if (typeof updateNotesList === 'function') updateNotesList(); + + const addedCount = transcribedNotes.filter(n => n.confidence >= confidenceThreshold).length; + this.updateStatus(`✅ ${addedCount} notas adicionadas ao projeto!`, 'success'); + }, + + // Informações de integração real + showRealIntegrationInfo() { + const info = ` +═══════════════════════════════════════════════════ +📘 IMPLEMENTAÇÃO REAL - TRANSCRIÇÃO DE ÁUDIO +═══════════════════════════════════════════════════ + +Esta é uma DEMONSTRAÇÃO. Para implementação real: + +🎵 OPÇÃO 1: Spotify Basic Pitch (Recomendado) + npm install @spotify/basic-pitch + GitHub: spotify/basic-pitch + + import * as basicPitch from '@spotify/basic-pitch'; + const model = await basicPitch.loadModel(); + const frames = await basicPitch.detectNotes(audioData); + +🎹 OPÇÃO 2: Magenta Onsets & Frames + CDN: https://cdn.jsdelivr.net/npm/@magenta/music + Exemplo: Piano Scribe demo + +🔧 OPÇÃO 3: Backend ByteDance Piano Transcription + pip install piano-transcription-inference + Melhor qualidade (detecta até pedal!) + +═══════════════════════════════════════════════════ + `.trim(); + + console.log(info); + }, + + // Atualizar status + updateStatus(message, type = 'info') { + const statusEl = document.getElementById('transcriptionStatus'); + if (statusEl) { + statusEl.textContent = message; + statusEl.className = 'status-message ' + type; + } + }, + + // Sleep helper + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +}; + +// Event listener para slider de confiança +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', () => { + const slider = document.getElementById('confidenceThreshold'); + const valueDisplay = document.getElementById('confidenceValue'); + + if (slider && valueDisplay) { + slider.addEventListener('input', (e) => { + valueDisplay.textContent = e.target.value + '%'; + }); + } + }); +} + +// Exportar para uso global +if (typeof window !== 'undefined') { + window.TranscriptionHandler = TranscriptionHandler; +}