tools:intervalltrainer
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
| Nächste Überarbeitung | Vorherige Überarbeitung | ||
| tools:intervalltrainer [10/05/2025 14:32] – angelegt - Externe Bearbeitung 127.0.0.1 | tools:intervalltrainer [22/05/2025 16:49] (aktuell) – angelegt Eric Weber | ||
|---|---|---|---|
| Zeile 1: | Zeile 1: | ||
| + | < | ||
| + | < | ||
| + | <meta charset=" | ||
| + | <meta name=" | ||
| + | < | ||
| + | < | ||
| + | /* Reset und Container-Isolation */ | ||
| + | .interval-trainer-app { | ||
| + | all: initial; | ||
| + | font-family: | ||
| + | color: #1a1a1a; | ||
| + | background: #f8f9fa; | ||
| + | min-height: 100vh; | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | align-items: | ||
| + | padding: 20px; | ||
| + | box-sizing: border-box; | ||
| + | } | ||
| + | .interval-trainer-app * { | ||
| + | box-sizing: border-box; | ||
| + | } | ||
| + | |||
| + | .app-container { | ||
| + | width: 100%; | ||
| + | max-width: 800px; | ||
| + | background: white; | ||
| + | border-radius: | ||
| + | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | ||
| + | padding: 30px; | ||
| + | margin: 20px 0; | ||
| + | } | ||
| + | |||
| + | h1, h2 { | ||
| + | text-align: center; | ||
| + | color: #2d3748; | ||
| + | margin-bottom: | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | h1 { | ||
| + | font-size: 2.5rem; | ||
| + | background: linear-gradient(135deg, | ||
| + | -webkit-background-clip: | ||
| + | -webkit-text-fill-color: | ||
| + | background-clip: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | h2 { | ||
| + | font-size: 1.8rem; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .settings-section { | ||
| + | margin-bottom: | ||
| + | padding: 25px; | ||
| + | background: rgba(255, 255, 255, 0.8); | ||
| + | border-radius: | ||
| + | border: 1px solid rgba(102, 126, 234, 0.2); | ||
| + | transition: all 0.3s ease; | ||
| + | } | ||
| + | |||
| + | .settings-section: | ||
| + | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15); | ||
| + | transform: translateY(-2px); | ||
| + | } | ||
| + | |||
| + | label { | ||
| + | display: block; | ||
| + | margin-bottom: | ||
| + | font-weight: | ||
| + | color: #2d3748; | ||
| + | font-size: 1.1rem; | ||
| + | } | ||
| + | |||
| + | .interval-checkboxes { | ||
| + | display: grid; | ||
| + | grid-template-columns: | ||
| + | gap: 10px; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .interval-checkbox-item { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | padding: 8px 12px; | ||
| + | background: rgba(255, 255, 255, 0.7); | ||
| + | border-radius: | ||
| + | transition: all 0.2s ease; | ||
| + | cursor: pointer; | ||
| + | } | ||
| + | |||
| + | .interval-checkbox-item: | ||
| + | background: rgba(102, 126, 234, 0.1); | ||
| + | transform: translateX(5px); | ||
| + | } | ||
| + | |||
| + | .interval-checkbox-item label { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | margin: 0; | ||
| + | cursor: pointer; | ||
| + | font-weight: | ||
| + | font-size: 0.95rem; | ||
| + | width: 100%; | ||
| + | } | ||
| + | |||
| + | .interval-checkbox-item input[type=" | ||
| + | margin-right: | ||
| + | width: 18px; | ||
| + | height: 18px; | ||
| + | accent-color: | ||
| + | } | ||
| + | |||
| + | .interval-name { | ||
| + | flex: 1; | ||
| + | } | ||
| + | |||
| + | .play-interval-sample { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | border: none; | ||
| + | border-radius: | ||
| + | width: 30px; | ||
| + | height: 30px; | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | justify-content: | ||
| + | cursor: pointer; | ||
| + | font-size: 12px; | ||
| + | transition: all 0.2s ease; | ||
| + | margin-left: | ||
| + | } | ||
| + | |||
| + | .play-interval-sample: | ||
| + | transform: scale(1.1); | ||
| + | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); | ||
| + | } | ||
| + | |||
| + | .play-interval-sample: | ||
| + | background: #cbd5e0; | ||
| + | cursor: not-allowed; | ||
| + | transform: none; | ||
| + | box-shadow: none; | ||
| + | } | ||
| + | |||
| + | .radio-group { | ||
| + | display: flex; | ||
| + | flex-wrap: wrap; | ||
| + | gap: 15px; | ||
| + | } | ||
| + | |||
| + | .radio-group label { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | margin: 0; | ||
| + | font-weight: | ||
| + | font-size: 0.95rem; | ||
| + | cursor: pointer; | ||
| + | padding: 8px 15px; | ||
| + | background: rgba(255, 255, 255, 0.7); | ||
| + | border-radius: | ||
| + | transition: all 0.2s ease; | ||
| + | } | ||
| + | |||
| + | .radio-group label:hover { | ||
| + | background: rgba(102, 126, 234, 0.1); | ||
| + | } | ||
| + | |||
| + | .radio-group input[type=" | ||
| + | margin-right: | ||
| + | accent-color: | ||
| + | } | ||
| + | |||
| + | button { | ||
| + | padding: 12px 24px; | ||
| + | border: none; | ||
| + | border-radius: | ||
| + | cursor: pointer; | ||
| + | font-size: 1rem; | ||
| + | font-weight: | ||
| + | transition: all 0.3s ease; | ||
| + | margin: 5px; | ||
| + | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | ||
| + | } | ||
| + | |||
| + | button: | ||
| + | transform: translateY(-2px); | ||
| + | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); | ||
| + | } | ||
| + | |||
| + | button: | ||
| + | transform: translateY(0); | ||
| + | } | ||
| + | |||
| + | /* Primäre Buttons */ | ||
| + | .btn-primary { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | } | ||
| + | |||
| + | .btn-primary: | ||
| + | background: linear-gradient(135deg, | ||
| + | } | ||
| + | |||
| + | /* Sekundäre Buttons */ | ||
| + | .btn-secondary { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | } | ||
| + | |||
| + | .btn-secondary: | ||
| + | background: linear-gradient(135deg, | ||
| + | } | ||
| + | |||
| + | /* Disabled Buttons */ | ||
| + | button: | ||
| + | background: #e2e8f0 !important; | ||
| + | color: #a0aec0 !important; | ||
| + | cursor: not-allowed !important; | ||
| + | transform: none !important; | ||
| + | box-shadow: none !important; | ||
| + | } | ||
| + | |||
| + | /* Antwort-Buttons */ | ||
| + | .answer-button { | ||
| + | background: white; | ||
| + | color: #2d3748; | ||
| + | border: 2px solid #e2e8f0; | ||
| + | margin: 8px; | ||
| + | padding: 15px 20px; | ||
| + | font-weight: | ||
| + | min-width: 150px; | ||
| + | } | ||
| + | |||
| + | .answer-button: | ||
| + | border-color: | ||
| + | background: rgba(102, 126, 234, 0.05); | ||
| + | } | ||
| + | |||
| + | .answer-button.correct { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | .answer-button.incorrect { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | .answer-button.revealed-correct { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | .answer-options { | ||
| + | display: flex; | ||
| + | flex-wrap: wrap; | ||
| + | justify-content: | ||
| + | margin: 20px 0; | ||
| + | } | ||
| + | |||
| + | .test-area { | ||
| + | text-align: center; | ||
| + | padding: 20px; | ||
| + | } | ||
| + | |||
| + | .feedback { | ||
| + | margin: 20px 0; | ||
| + | padding: 15px 20px; | ||
| + | border-radius: | ||
| + | font-size: 1.1rem; | ||
| + | font-weight: | ||
| + | text-align: center; | ||
| + | } | ||
| + | |||
| + | .feedback.correct { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: #22543d; | ||
| + | border: 2px solid #68d391; | ||
| + | } | ||
| + | |||
| + | .feedback.incorrect { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: #742a2a; | ||
| + | border: 2px solid #fc8181; | ||
| + | } | ||
| + | |||
| + | .feedback.revealed { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: #2a4365; | ||
| + | border: 2px solid #63b3ed; | ||
| + | } | ||
| + | |||
| + | .progress-area { | ||
| + | background: rgba(255, 255, 255, 0.9); | ||
| + | border-radius: | ||
| + | padding: 20px; | ||
| + | margin: 20px 0; | ||
| + | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); | ||
| + | } | ||
| + | |||
| + | .progress-grid { | ||
| + | display: grid; | ||
| + | grid-template-columns: | ||
| + | gap: 15px; | ||
| + | margin-top: 15px; | ||
| + | } | ||
| + | |||
| + | .progress-item { | ||
| + | text-align: center; | ||
| + | padding: 15px; | ||
| + | background: rgba(102, 126, 234, 0.1); | ||
| + | border-radius: | ||
| + | } | ||
| + | |||
| + | .progress-value { | ||
| + | font-size: 1.5rem; | ||
| + | font-weight: | ||
| + | color: #667eea; | ||
| + | display: block; | ||
| + | } | ||
| + | |||
| + | .progress-label { | ||
| + | font-size: 0.9rem; | ||
| + | color: #4a5568; | ||
| + | margin-top: 5px; | ||
| + | } | ||
| + | |||
| + | .results-area { | ||
| + | text-align: center; | ||
| + | padding: 30px; | ||
| + | background: rgba(255, 255, 255, 0.9); | ||
| + | border-radius: | ||
| + | } | ||
| + | |||
| + | .results-score { | ||
| + | font-size: 3rem; | ||
| + | font-weight: | ||
| + | background: linear-gradient(135deg, | ||
| + | -webkit-background-clip: | ||
| + | -webkit-text-fill-color: | ||
| + | background-clip: | ||
| + | margin: 20px 0; | ||
| + | } | ||
| + | |||
| + | .hidden { | ||
| + | display: none; | ||
| + | } | ||
| + | |||
| + | .task-description { | ||
| + | font-size: 1.3rem; | ||
| + | margin: 20px 0; | ||
| + | color: #2d3748; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | .play-main-button { | ||
| + | background: linear-gradient(135deg, | ||
| + | color: white; | ||
| + | font-size: 1.2rem; | ||
| + | padding: 15px 30px; | ||
| + | margin: 20px 0; | ||
| + | } | ||
| + | |||
| + | .play-main-button: | ||
| + | background: linear-gradient(135deg, | ||
| + | } | ||
| + | |||
| + | .question-header { | ||
| + | background: rgba(102, 126, 234, 0.1); | ||
| + | padding: 15px; | ||
| + | border-radius: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .question-number { | ||
| + | font-size: 1.2rem; | ||
| + | font-weight: | ||
| + | color: #667eea; | ||
| + | } | ||
| + | |||
| + | @media (max-width: 768px) { | ||
| + | .interval-trainer-app { | ||
| + | padding: 10px; | ||
| + | } | ||
| + | |||
| + | .app-container { | ||
| + | padding: 20px; | ||
| + | margin: 10px 0; | ||
| + | } | ||
| + | |||
| + | h1 { | ||
| + | font-size: 2rem; | ||
| + | } | ||
| + | |||
| + | .interval-checkboxes { | ||
| + | grid-template-columns: | ||
| + | } | ||
| + | |||
| + | .radio-group { | ||
| + | flex-direction: | ||
| + | } | ||
| + | |||
| + | .answer-options { | ||
| + | flex-direction: | ||
| + | align-items: | ||
| + | } | ||
| + | |||
| + | .answer-button { | ||
| + | width: 100%; | ||
| + | max-width: 300px; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | < | ||
| + | |||
| + | <div id=" | ||
| + | < | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | <!-- Wird dynamisch gefüllt --> | ||
| + | </ | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <button id=" | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | | ||
| + | <p id=" | ||
| + | <button id=" | ||
| + | | ||
| + | <div id=" | ||
| + | <!-- Wird dynamisch gefüllt --> | ||
| + | </ | ||
| + | | ||
| + | <div id=" | ||
| + | | ||
| + | <div id=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | < | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <span class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <span class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <span class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <span class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | < | ||
| + | <p>Du hast die Prüfung abgeschlossen!</ | ||
| + | <div class=" | ||
| + | < | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | const intervalDefinitions = [ | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: "Reine Quarte", | ||
| + | { name: " | ||
| + | { name: "Reine Quinte", | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: " | ||
| + | { name: "Reine Oktave", | ||
| + | ]; | ||
| + | |||
| + | const noteNamesDE = [ | ||
| + | " | ||
| + | " | ||
| + | ]; | ||
| + | |||
| + | const baseMidiNotes = { | ||
| + | " | ||
| + | }; | ||
| + | | ||
| + | const midiToGermanNoteName = (midiNote) => { | ||
| + | const noteIndex = midiNote % 12; | ||
| + | const octaveVal = Math.floor(midiNote / 12); | ||
| + | let noteBase = noteNamesDE[noteIndex]; | ||
| + | | ||
| + | if (octaveVal === 4) return noteBase; | ||
| + | if (octaveVal === 5) return noteBase + "'"; | ||
| + | if (octaveVal === 6) return noteBase + "''"; | ||
| + | if (octaveVal === 7) return noteBase + "'''"; | ||
| + | | ||
| + | return noteBase + (octaveVal - 1); | ||
| + | }; | ||
| + | |||
| + | const A4_FREQ = 440; | ||
| + | const A4_MIDI = 69; | ||
| + | |||
| + | const midiToFreq = (midiNote) => { | ||
| + | return A4_FREQ * Math.pow(2, (midiNote - A4_MIDI) / 12); | ||
| + | }; | ||
| + | |||
| + | let audioContext; | ||
| + | let currentTask = {}; | ||
| + | let selectedIntervalsForTest = []; | ||
| + | let playbackSetting = ' | ||
| + | let difficultySetting = ' | ||
| + | let testModeSetting = ' | ||
| + | |||
| + | let currentQuestionNumber = 0; | ||
| + | const totalQuestions = 10; | ||
| + | let correctAnswersCount = 0; | ||
| + | let wrongAnswersCount = 0; | ||
| + | let questionAnswered = false; | ||
| + | |||
| + | // DOM Elements | ||
| + | const settingsContainer = document.getElementById(' | ||
| + | const testArea = document.getElementById(' | ||
| + | const progressArea = document.getElementById(' | ||
| + | const resultsArea = document.getElementById(' | ||
| + | |||
| + | const intervalCheckboxesContainer = document.getElementById(' | ||
| + | const selectAllButton = document.getElementById(' | ||
| + | const deselectAllButton = document.getElementById(' | ||
| + | const startTestButton = document.getElementById(' | ||
| + | |||
| + | const currentQuestionNumberDisplay = document.getElementById(' | ||
| + | const totalQuestionNumberDisplay = document.getElementById(' | ||
| + | const taskDescriptionDisplay = document.getElementById(' | ||
| + | const playIntervalButton = document.getElementById(' | ||
| + | const answerOptionsContainer = document.getElementById(' | ||
| + | const feedbackMessageDisplay = document.getElementById(' | ||
| + | const actionButtonsDiv = document.getElementById(' | ||
| + | const nextQuestionButton = document.getElementById(' | ||
| + | const repeatQuestionButton = document.getElementById(' | ||
| + | const showCorrectAnswerButton = document.getElementById(' | ||
| + | const newSettingsButton = document.getElementById(' | ||
| + | |||
| + | const answeredQuestionsDisplay = document.getElementById(' | ||
| + | const correctAnswersDisplay = document.getElementById(' | ||
| + | const wrongAnswersDisplay = document.getElementById(' | ||
| + | const successRateDisplay = document.getElementById(' | ||
| + | | ||
| + | const finalCorrectDisplay = document.getElementById(' | ||
| + | const finalTotalDisplay = document.getElementById(' | ||
| + | const finalPercentageDisplay = document.getElementById(' | ||
| + | const restartTestButton = document.getElementById(' | ||
| + | const changeSettingsButton = document.getElementById(' | ||
| + | |||
| + | function initializeAudio() { | ||
| + | if (!audioContext) { | ||
| + | try { | ||
| + | audioContext = new (window.AudioContext || window.webkitAudioContext)(); | ||
| + | } catch (e) { | ||
| + | alert(' | ||
| + | console.error(' | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function playNote(frequency, | ||
| + | if (!audioContext) return; | ||
| + | const oscillator = audioContext.createOscillator(); | ||
| + | const gainNode = audioContext.createGain(); | ||
| + | |||
| + | oscillator.type = waveType; | ||
| + | oscillator.frequency.setValueAtTime(frequency, | ||
| + | | ||
| + | gainNode.gain.setValueAtTime(0, | ||
| + | gainNode.gain.linearRampToValueAtTime(0.3, | ||
| + | gainNode.gain.setValueAtTime(0.3, | ||
| + | gainNode.gain.linearRampToValueAtTime(0, | ||
| + | |||
| + | oscillator.connect(gainNode); | ||
| + | gainNode.connect(audioContext.destination); | ||
| + | |||
| + | oscillator.start(audioContext.currentTime + startTime); | ||
| + | oscillator.stop(audioContext.currentTime + startTime + duration + 0.1); | ||
| + | } | ||
| + | |||
| + | function playIntervalSample(intervalDef, | ||
| + | initializeAudio(); | ||
| + | if (!audioContext || audioContext.state === ' | ||
| + | audioContext.resume().then(() => { | ||
| + | actuallyPlayIntervalSample(intervalDef, | ||
| + | }).catch(err => console.error(" | ||
| + | } else { | ||
| + | actuallyPlayIntervalSample(intervalDef, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function actuallyPlayIntervalSample(intervalDef, | ||
| + | const startMidi = baseMidiNotes[" | ||
| + | const endMidi = startMidi + intervalDef.semitones; | ||
| + | | ||
| + | const freq1 = midiToFreq(startMidi); | ||
| + | const freq2 = midiToFreq(endMidi); | ||
| + | const noteDuration = 0.7; | ||
| + | |||
| + | button.disabled = true; | ||
| + | button.textContent = ' | ||
| + | |||
| + | const currentPlayback = document.querySelector(' | ||
| + | let totalPlaybackTime = noteDuration; | ||
| + | |||
| + | if (currentPlayback === ' | ||
| + | playNote(freq1, | ||
| + | playNote(freq2, | ||
| + | totalPlaybackTime = noteDuration * 1.8; | ||
| + | } else if (currentPlayback === ' | ||
| + | playNote(freq2, | ||
| + | playNote(freq1, | ||
| + | totalPlaybackTime = noteDuration * 1.8; | ||
| + | } else { | ||
| + | playNote(freq1, | ||
| + | playNote(freq2, | ||
| + | totalPlaybackTime = noteDuration + 0.3; | ||
| + | } | ||
| + | |||
| + | setTimeout(() => { | ||
| + | button.disabled = false; | ||
| + | button.textContent = ' | ||
| + | }, totalPlaybackTime * 1000 + 200); | ||
| + | } | ||
| + | |||
| + | function playCurrentInterval() { | ||
| + | initializeAudio(); | ||
| + | if (!audioContext || audioContext.state === ' | ||
| + | audioContext.resume().then(() => { | ||
| + | actuallyPlayInterval(); | ||
| + | }).catch(err => console.error(" | ||
| + | } else { | ||
| + | actuallyPlayInterval(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function actuallyPlayInterval() { | ||
| + | if (!currentTask.startNoteMidi || typeof currentTask.endNoteMidi === ' | ||
| + | |||
| + | const freq1 = midiToFreq(currentTask.startNoteMidi); | ||
| + | const freq2 = midiToFreq(currentTask.endNoteMidi); | ||
| + | const noteDuration = 0.7; | ||
| + | |||
| + | playIntervalButton.disabled = true; | ||
| + | playIntervalButton.textContent = '⏸ Spielt ab...'; | ||
| + | let totalPlaybackTime = noteDuration; | ||
| + | |||
| + | if (playbackSetting === ' | ||
| + | playNote(freq1, | ||
| + | playNote(freq2, | ||
| + | totalPlaybackTime = noteDuration * 1.8; | ||
| + | } else if (playbackSetting === ' | ||
| + | playNote(freq1, | ||
| + | playNote(freq2, | ||
| + | totalPlaybackTime = noteDuration * 1.8; | ||
| + | } else { | ||
| + | playNote(freq1, | ||
| + | playNote(freq2, | ||
| + | totalPlaybackTime = noteDuration + 0.3; | ||
| + | } | ||
| + | setTimeout(() => { | ||
| + | playIntervalButton.disabled = false; | ||
| + | playIntervalButton.textContent = '🔊 Intervall abspielen'; | ||
| + | }, totalPlaybackTime * 1000 + 200); | ||
| + | } | ||
| + | |||
| + | function populateIntervalCheckboxes() { | ||
| + | intervalDefinitions.forEach(interval => { | ||
| + | const div = document.createElement(' | ||
| + | div.className = ' | ||
| + | | ||
| + | const label = document.createElement(' | ||
| + | const checkbox = document.createElement(' | ||
| + | checkbox.type = ' | ||
| + | checkbox.value = interval.id; | ||
| + | checkbox.id = `interval-${interval.id}`; | ||
| + | checkbox.dataset.semitones = interval.semitones; | ||
| + | checkbox.checked = true; | ||
| + | | ||
| + | const nameSpan = document.createElement(' | ||
| + | nameSpan.className = ' | ||
| + | nameSpan.textContent = interval.name; | ||
| + | | ||
| + | const playButton = document.createElement(' | ||
| + | playButton.className = ' | ||
| + | playButton.textContent = ' | ||
| + | playButton.title = ' | ||
| + | playButton.onclick = (e) => { | ||
| + | e.preventDefault(); | ||
| + | playIntervalSample(interval, | ||
| + | }; | ||
| + | | ||
| + | label.appendChild(checkbox); | ||
| + | label.appendChild(nameSpan); | ||
| + | div.appendChild(label); | ||
| + | div.appendChild(playButton); | ||
| + | intervalCheckboxesContainer.appendChild(div); | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | function toggleAllIntervals(select) { | ||
| + | intervalCheckboxesContainer.querySelectorAll(' | ||
| + | cb.checked = select; | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | function collectSettings() { | ||
| + | selectedIntervalsForTest = Array.from(intervalCheckboxesContainer.querySelectorAll(' | ||
| + | .map(cb => intervalDefinitions.find(i => i.id === cb.value)) | ||
| + | .sort((a, b) => a.semitones - b.semitones); | ||
| + | |||
| + | playbackSetting = document.querySelector(' | ||
| + | difficultySetting = document.querySelector(' | ||
| + | testModeSetting = document.querySelector(' | ||
| + | |||
| + | if (selectedIntervalsForTest.length === 0) { | ||
| + | alert(" | ||
| + | return false; | ||
| + | } | ||
| + | return true; | ||
| + | } | ||
| + | | ||
| + | function generateRandomMidiNote(minMidi, | ||
| + | return Math.floor(Math.random() * (maxMidi - minMidi + 1)) + minMidi; | ||
| + | } | ||
| + | |||
| + | function generateQuestion() { | ||
| + | questionAnswered = false; | ||
| + | currentQuestionNumber++; | ||
| + | updateProgress(); | ||
| + | |||
| + | const randomIntervalDef = selectedIntervalsForTest[Math.floor(Math.random() * selectedIntervalsForTest.length)]; | ||
| + | currentTask.interval = randomIntervalDef; | ||
| + | let startNoteMidi; | ||
| + | |||
| + | const c3Midi = baseMidiNotes[" | ||
| + | const c4Midi = baseMidiNotes[" | ||
| + | const c5Midi = baseMidiNotes[" | ||
| + | const c6Midi = baseMidiNotes[" | ||
| + | |||
| + | if (difficultySetting === ' | ||
| + | if (playbackSetting === ' | ||
| + | startNoteMidi = c5Midi; | ||
| + | } else { | ||
| + | startNoteMidi = c4Midi; | ||
| + | } | ||
| + | } else if (difficultySetting === ' | ||
| + | const ascStarts = [c3Midi, c4Midi, c5Midi]; | ||
| + | const descStarts = [c4Midi, c5Midi, c6Midi]; | ||
| + | if (playbackSetting === ' | ||
| + | startNoteMidi = descStarts[Math.floor(Math.random() * descStarts.length)]; | ||
| + | } else { | ||
| + | startNoteMidi = ascStarts[Math.floor(Math.random() * ascStarts.length)]; | ||
| + | } | ||
| + | } else { | ||
| + | const minPlayableMidi = c3Midi; | ||
| + | const maxPlayableMidi = c6Midi + 11; | ||
| + | |||
| + | if (playbackSetting === ' | ||
| + | const minStart = minPlayableMidi + randomIntervalDef.semitones; | ||
| + | startNoteMidi = generateRandomMidiNote(Math.max(minStart, | ||
| + | } else { | ||
| + | const maxStart = maxPlayableMidi - randomIntervalDef.semitones; | ||
| + | startNoteMidi = generateRandomMidiNote(c3Midi, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | currentTask.startNoteMidi = startNoteMidi; | ||
| + | if (playbackSetting === ' | ||
| + | currentTask.endNoteMidi = startNoteMidi - randomIntervalDef.semitones; | ||
| + | } else { | ||
| + | currentTask.endNoteMidi = startNoteMidi + randomIntervalDef.semitones; | ||
| + | } | ||
| + | | ||
| + | if (playbackSetting === ' | ||
| + | if (currentTask.startNoteMidi > currentTask.endNoteMidi) { | ||
| + | [currentTask.startNoteMidi, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | currentTask.startNoteName = midiToGermanNoteName(currentTask.startNoteMidi); | ||
| + | currentTask.endNoteName = midiToGermanNoteName(currentTask.endNoteMidi); | ||
| + | | ||
| + | displayQuestion(); | ||
| + | } | ||
| + | |||
| + | function displayQuestion() { | ||
| + | currentQuestionNumberDisplay.textContent = currentQuestionNumber; | ||
| + | totalQuestionNumberDisplay.textContent = totalQuestions; | ||
| + | answerOptionsContainer.innerHTML = ''; | ||
| + | feedbackMessageDisplay.textContent = ''; | ||
| + | feedbackMessageDisplay.className = ' | ||
| + | actionButtonsDiv.classList.add(' | ||
| + | nextQuestionButton.classList.add(' | ||
| + | repeatQuestionButton.classList.add(' | ||
| + | showCorrectAnswerButton.classList.add(' | ||
| + | newSettingsButton.classList.add(' | ||
| + | playIntervalButton.disabled = false; | ||
| + | playIntervalButton.textContent = '🔊 Intervall abspielen'; | ||
| + | |||
| + | if (testModeSetting === ' | ||
| + | taskDescriptionDisplay.textContent = " | ||
| + | const options = [...selectedIntervalsForTest]; | ||
| + | |||
| + | options.forEach(interval => { | ||
| + | const button = document.createElement(' | ||
| + | button.textContent = interval.name; | ||
| + | button.classList.add(' | ||
| + | button.dataset.intervalId = interval.id; | ||
| + | button.onclick = () => checkAnswer(interval.id); | ||
| + | answerOptionsContainer.appendChild(button); | ||
| + | }); | ||
| + | currentTask.correctAnswer = currentTask.interval.id; | ||
| + | |||
| + | } else { | ||
| + | let displayStartNote = currentTask.startNoteName; | ||
| + | if (playbackSetting === ' | ||
| + | | ||
| + | } else if (playbackSetting === ' | ||
| + | // displayStartNote remains currentTask.startNoteName (which is the higher note) | ||
| + | } | ||
| + | |||
| + | taskDescriptionDisplay.textContent = `Der Startton ist ${displayStartNote}. Welches ist der Zielton?`; | ||
| + | currentTask.correctAnswer = currentTask.endNoteName; | ||
| + | |||
| + | let noteOptionsMidi = [currentTask.endNoteMidi]; | ||
| + | const numTotalOptions = Math.min(5, Math.max(3, selectedIntervalsForTest.length > 0 ? selectedIntervalsForTest.length : 3)); | ||
| + | |||
| + | let attempts = 0; | ||
| + | const maxAttempts = 30; | ||
| + | const deviationRange = 4; | ||
| + | |||
| + | while (noteOptionsMidi.length < numTotalOptions && attempts < maxAttempts) { | ||
| + | const randomDeviation = Math.floor(Math.random() * (2 * deviationRange + 1)) - deviationRange; | ||
| + | if (randomDeviation === 0 && noteOptionsMidi.includes(currentTask.endNoteMidi + randomDeviation) ) { | ||
| + | attempts++; | ||
| + | continue; | ||
| + | } | ||
| + | const potentialWrongNoteMidi = currentTask.endNoteMidi + randomDeviation; | ||
| + | | ||
| + | if (potentialWrongNoteMidi >= baseMidiNotes[" | ||
| + | noteOptionsMidi.push(potentialWrongNoteMidi); | ||
| + | } | ||
| + | attempts++; | ||
| + | } | ||
| + | | ||
| + | if (noteOptionsMidi.length < numTotalOptions) { | ||
| + | let otherIntervalChoices = [...selectedIntervalsForTest].filter(i => i.id !== currentTask.interval.id); | ||
| + | otherIntervalChoices.sort(() => 0.5 - Math.random()); | ||
| + | |||
| + | for (let i = 0; i < otherIntervalChoices.length && noteOptionsMidi.length < numTotalOptions; | ||
| + | let otherTargetMidi; | ||
| + | const referenceStartMidi = (playbackSetting === ' | ||
| + | |||
| + | if (playbackSetting === ' | ||
| + | otherTargetMidi = referenceStartMidi - otherIntervalChoices[i].semitones; | ||
| + | } else { | ||
| + | otherTargetMidi = referenceStartMidi + otherIntervalChoices[i].semitones; | ||
| + | } | ||
| + | if (!noteOptionsMidi.includes(otherTargetMidi) && otherTargetMidi >= baseMidiNotes[" | ||
| + | noteOptionsMidi.push(otherTargetMidi); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | if (!noteOptionsMidi.includes(currentTask.endNoteMidi)) { | ||
| + | if (noteOptionsMidi.length >= numTotalOptions) noteOptionsMidi.pop(); | ||
| + | noteOptionsMidi.push(currentTask.endNoteMidi); | ||
| + | } | ||
| + | |||
| + | noteOptionsMidi.sort((a, | ||
| + | |||
| + | noteOptionsMidi.forEach(noteMidi => { | ||
| + | const button = document.createElement(' | ||
| + | const noteName = midiToGermanNoteName(noteMidi); | ||
| + | button.textContent = noteName; | ||
| + | button.classList.add(' | ||
| + | button.dataset.noteName = noteName; | ||
| + | button.onclick = () => checkAnswer(noteName); | ||
| + | answerOptionsContainer.appendChild(button); | ||
| + | }); | ||
| + | } | ||
| + | if (currentQuestionNumber > 0) setTimeout(playCurrentInterval, | ||
| + | } | ||
| + | |||
| + | function checkAnswer(selectedValue) { | ||
| + | if (questionAnswered) return; | ||
| + | questionAnswered = true; | ||
| + | |||
| + | const isCorrect = (selectedValue === currentTask.correctAnswer); | ||
| + | const clickedButton = Array.from(answerOptionsContainer.querySelectorAll(' | ||
| + | return testModeSetting === ' | ||
| + | }); | ||
| + | |||
| + | playIntervalButton.disabled = true; | ||
| + | |||
| + | if (isCorrect) { | ||
| + | correctAnswersCount++; | ||
| + | feedbackMessageDisplay.textContent = "🎉 Richtig!"; | ||
| + | feedbackMessageDisplay.className = ' | ||
| + | if(clickedButton) clickedButton.classList.add(' | ||
| + | | ||
| + | actionButtonsDiv.classList.remove(' | ||
| + | nextQuestionButton.classList.remove(' | ||
| + | repeatQuestionButton.classList.add(' | ||
| + | showCorrectAnswerButton.classList.add(' | ||
| + | newSettingsButton.classList.remove(' | ||
| + | |||
| + | } else { | ||
| + | wrongAnswersCount++; | ||
| + | feedbackMessageDisplay.textContent = "❌ Leider falsch."; | ||
| + | feedbackMessageDisplay.className = ' | ||
| + | if(clickedButton) clickedButton.classList.add(' | ||
| + | |||
| + | actionButtonsDiv.classList.remove(' | ||
| + | repeatQuestionButton.classList.remove(' | ||
| + | showCorrectAnswerButton.classList.remove(' | ||
| + | nextQuestionButton.classList.add(' | ||
| + | newSettingsButton.classList.remove(' | ||
| + | } | ||
| + | updateProgress(); | ||
| + | disableAnswerButtons(); | ||
| + | |||
| + | if (currentQuestionNumber >= totalQuestions) { | ||
| + | | ||
| + | if (!isCorrect) { | ||
| + | showCorrectAnswerButton.onclick = () => { | ||
| + | revealCorrectAnswer(true); | ||
| + | }; | ||
| + | } else { | ||
| + | actionButtonsDiv.classList.remove(' | ||
| + | nextQuestionButton.classList.remove(' | ||
| + | newSettingsButton.classList.remove(' | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | function disableAnswerButtons() { | ||
| + | answerOptionsContainer.querySelectorAll(' | ||
| + | } | ||
| + | | ||
| + | function enableAnswerButtons() { | ||
| + | answerOptionsContainer.querySelectorAll(' | ||
| + | } | ||
| + | |||
| + | function handleRepeatQuestion() { | ||
| + | feedbackMessageDisplay.textContent = ''; | ||
| + | feedbackMessageDisplay.className = ' | ||
| + | enableAnswerButtons(); | ||
| + | questionAnswered = false; | ||
| + | playIntervalButton.disabled = false; | ||
| + | playIntervalButton.textContent = '🔊 Intervall abspielen'; | ||
| + | actionButtonsDiv.classList.add(' | ||
| + | newSettingsButton.classList.add(' | ||
| + | |||
| + | answerOptionsContainer.querySelectorAll(' | ||
| + | btn.classList.remove(' | ||
| + | }); | ||
| + | playCurrentInterval(); | ||
| + | } | ||
| + | |||
| + | function revealCorrectAnswer(isEndOfTestPath = false) { | ||
| + | const correctButton = Array.from(answerOptionsContainer.querySelectorAll(' | ||
| + | | ||
| + | }); | ||
| + | if (correctButton && !correctButton.classList.contains(' | ||
| + | correctButton.classList.add(' | ||
| + | } | ||
| + | | ||
| + | const correctAnswerText = testModeSetting === ' | ||
| + | feedbackMessageDisplay.textContent = `💡 Die richtige Antwort war: ${correctAnswerText}`; | ||
| + | feedbackMessageDisplay.className = ' | ||
| + | | ||
| + | actionButtonsDiv.classList.remove(' | ||
| + | nextQuestionButton.classList.remove(' | ||
| + | if (currentQuestionNumber >= totalQuestions || isEndOfTestPath) { | ||
| + | nextQuestionButton.textContent = " | ||
| + | } | ||
| + | repeatQuestionButton.classList.add(' | ||
| + | showCorrectAnswerButton.classList.add(' | ||
| + | newSettingsButton.classList.add(' | ||
| + | } | ||
| + | |||
| + | function handleNextQuestion() { | ||
| + | if (currentQuestionNumber >= totalQuestions) { | ||
| + | showResults(); | ||
| + | } else { | ||
| + | playIntervalButton.disabled = false; | ||
| + | playIntervalButton.textContent = '🔊 Intervall abspielen'; | ||
| + | enableAnswerButtons(); | ||
| + | answerOptionsContainer.querySelectorAll(' | ||
| + | btn.classList.remove(' | ||
| + | }); | ||
| + | generateQuestion(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function updateProgress() { | ||
| + | const answeredTotal = correctAnswersCount + wrongAnswersCount; | ||
| + | answeredQuestionsDisplay.textContent = answeredTotal; | ||
| + | correctAnswersDisplay.textContent = correctAnswersCount; | ||
| + | wrongAnswersDisplay.textContent = wrongAnswersCount; | ||
| + | const rate = answeredTotal > 0 ? Math.round((correctAnswersCount / answeredTotal) * 100) : 0; | ||
| + | successRateDisplay.textContent = rate; | ||
| + | } | ||
| + | | ||
| + | function showResults() { | ||
| + | testArea.classList.add(' | ||
| + | progressArea.classList.add(' | ||
| + | resultsArea.classList.remove(' | ||
| + | |||
| + | finalCorrectDisplay.textContent = correctAnswersCount; | ||
| + | finalTotalDisplay.textContent = totalQuestions; | ||
| + | const percentage = totalQuestions > 0 ? Math.round((correctAnswersCount / totalQuestions) * 100) : 0; | ||
| + | finalPercentageDisplay.textContent = percentage; | ||
| + | } | ||
| + | |||
| + | function goToSettings() { | ||
| + | testArea.classList.add(' | ||
| + | progressArea.classList.add(' | ||
| + | resultsArea.classList.add(' | ||
| + | settingsContainer.classList.remove(' | ||
| + | } | ||
| + | |||
| + | function resetAndRestart() { | ||
| + | currentQuestionNumber = 0; | ||
| + | correctAnswersCount = 0; | ||
| + | wrongAnswersCount = 0; | ||
| + | questionAnswered = false; | ||
| + | currentTask = {}; | ||
| + | |||
| + | resultsArea.classList.add(' | ||
| + | settingsContainer.classList.remove(' | ||
| + | progressArea.classList.add(' | ||
| + | testArea.classList.add(' | ||
| + | |||
| + | nextQuestionButton.textContent = " | ||
| + | showCorrectAnswerButton.onclick = () => revealCorrectAnswer(false); | ||
| + | newSettingsButton.classList.add(' | ||
| + | | ||
| + | answeredQuestionsDisplay.textContent = 0; | ||
| + | correctAnswersDisplay.textContent = 0; | ||
| + | wrongAnswersDisplay.textContent = 0; | ||
| + | successRateDisplay.textContent = 0; | ||
| + | } | ||
| + | |||
| + | document.addEventListener(' | ||
| + | populateIntervalCheckboxes(); | ||
| + | totalQuestionNumberDisplay.textContent = totalQuestions; | ||
| + | |||
| + | selectAllButton.addEventListener(' | ||
| + | deselectAllButton.addEventListener(' | ||
| + | | ||
| + | startTestButton.addEventListener(' | ||
| + | initializeAudio(); | ||
| + | if (audioContext && audioContext.state === ' | ||
| + | audioContext.resume().catch(err => console.error(" | ||
| + | } | ||
| + | |||
| + | if (collectSettings()) { | ||
| + | settingsContainer.classList.add(' | ||
| + | testArea.classList.remove(' | ||
| + | progressArea.classList.remove(' | ||
| + | resultsArea.classList.add(' | ||
| + | currentQuestionNumber = 0; | ||
| + | correctAnswersCount = 0; | ||
| + | wrongAnswersCount = 0; | ||
| + | | ||
| + | answeredQuestionsDisplay.textContent = 0; | ||
| + | correctAnswersDisplay.textContent = 0; | ||
| + | wrongAnswersDisplay.textContent = 0; | ||
| + | successRateDisplay.textContent = 0; | ||
| + | generateQuestion(); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | playIntervalButton.addEventListener(' | ||
| + | nextQuestionButton.addEventListener(' | ||
| + | repeatQuestionButton.addEventListener(' | ||
| + | showCorrectAnswerButton.addEventListener(' | ||
| + | newSettingsButton.addEventListener(' | ||
| + | restartTestButton.addEventListener(' | ||
| + | changeSettingsButton.addEventListener(' | ||
| + | }); | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
