Files

643 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Live Search & PostgreSQL Correction Management
let searchTimeout = null;
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('search-input');
const searchDropdown = document.getElementById('search-results-dropdown');
const clearBtn = document.getElementById('search-clear-btn');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const val = e.target.value.trim();
clearBtn.style.display = val ? 'block' : 'none';
clearTimeout(searchTimeout);
if (!val) {
searchDropdown.style.display = 'none';
searchDropdown.innerHTML = '';
return;
}
searchTimeout = setTimeout(() => {
executeSearch(val);
}, 250);
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-box-container')) {
searchDropdown.style.display = 'none';
}
});
}
// Populate Key options in edit modal
initEditKeyOptions();
});
function clearSearch() {
const searchInput = document.getElementById('search-input');
const searchDropdown = document.getElementById('search-results-dropdown');
const clearBtn = document.getElementById('search-clear-btn');
searchInput.value = '';
clearBtn.style.display = 'none';
searchDropdown.style.display = 'none';
searchDropdown.innerHTML = '';
}
async function executeSearch(query) {
const searchDropdown = document.getElementById('search-results-dropdown');
searchDropdown.innerHTML = '<div style="padding:14px;color:var(--text-dim);font-size:12px;font-family:monospace;">Suche in PostgreSQL & Online...</div>';
searchDropdown.style.display = 'block';
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const json = await res.json();
if (json.status !== 'success' || !json.data || json.data.length === 0) {
searchDropdown.innerHTML = '<div style="padding:14px;color:var(--text-dim);font-size:12px;font-family:monospace;">Keine Tracks gefunden.</div>';
return;
}
searchDropdown.innerHTML = '';
json.data.forEach(track => {
const item = document.createElement('div');
item.className = 'search-result-item';
const coverHtml = track.cover_url
? `<img src="${track.cover_url}" alt="cover" loading="lazy">`
: `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-dim);font-size:16px;">♪</div>`;
let badgesHtml = '';
if (track.is_local && track.camelot_key) {
const color = window.colorMatrix ? window.colorMatrix[track.camelot_key] : '#4F9D69';
badgesHtml += `<span class="search-badge" style="background:${color};">${track.camelot_key}</span>`;
if (track.bpm) {
badgesHtml += `<span class="search-badge-bpm">${track.bpm} BPM</span>`;
}
badgesHtml += `<span class="search-badge search-badge-local">${track.is_corrected ? 'KORRIGIERT' : 'LOKAL'}</span>`;
} else {
badgesHtml += `<span class="search-badge" style="background:var(--surface-2);color:var(--text-dim);border:1px solid var(--border);">NEU</span>`;
}
item.innerHTML = `
<div class="search-result-cover">${coverHtml}</div>
<div class="search-result-info">
<div class="search-result-title">${escapeHtml(track.title)}</div>
<div class="search-result-artist">${escapeHtml(track.artist)}</div>
</div>
<div class="search-result-badges">${badgesHtml}</div>
`;
item.addEventListener('click', () => {
selectTrack(track);
searchDropdown.style.display = 'none';
clearSearch();
});
searchDropdown.appendChild(item);
});
} catch (err) {
console.error('[Search] Error:', err);
searchDropdown.innerHTML = '<div style="padding:14px;color:#D62828;font-size:12px;">Suchfehler aufgetreten.</div>';
}
}
function formatDuration(sec) {
if (!sec || isNaN(sec)) return '-:--';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
}
function getCleanSearchTerm(artist, title) {
const raw = `${artist || ''} ${title || ''}`;
return raw.replace(/[/\\?#%&"'\(\)\[\]]/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDeezerUrl(track) {
if (track && track.deezer_link) return track.deezer_link;
if (track && track.deezer_id) return `https://www.deezer.com/track/${track.deezer_id}`;
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://www.deezer.com/search/${encodeURIComponent(term)}/track`;
}
function getSpotifyUrl(track) {
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://open.spotify.com/search/${encodeURIComponent(term)}`;
}
function getYoutubeUrl(track) {
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://www.youtube.com/results?search_query=${encodeURIComponent(term)}`;
}
function formatSourceInfo(source, isCorrected) {
if (isCorrected) return { match: '100%', label: 'PostgreSQL (Fix)' };
switch (source) {
case 'spotify':
case 'seed':
return { match: '100%', label: 'Spotify (Verifiziert)' };
case 'songbpm':
return { match: '98%', label: 'SongBPM (Spotify API)' };
case 'audio_analysis':
return { match: '85%', label: 'Audio-Analyse (ffmpeg)' };
case 'getsongbpm':
return { match: '90%', label: 'GetSongBPM' };
case 'postgresql':
return { match: '100%', label: 'PostgreSQL (Lokal)' };
default:
return { match: 'Online', label: 'Deezer / Online' };
}
}
async function selectTrack(track) {
// Update Hero UI
const initialSource = formatSourceInfo(track.source || (track.is_local ? 'postgresql' : null), track.is_corrected);
document.getElementById('track-title').textContent = track.title;
document.getElementById('track-artist').textContent = track.artist;
document.getElementById('stat-match').textContent = initialSource.match;
document.getElementById('stat-source-sub').textContent = initialSource.label;
// Stop any ongoing preview playback
if (window.stopAllPreviews) window.stopAllPreviews();
// Update Cover
if (track.cover_url) {
const coverEl = document.getElementById('cover');
coverEl.innerHTML = `<img src="${track.cover_url}" alt="cover">
<button id="hero-preview-btn" class="cover-play-btn" onclick="event.stopPropagation(); window.toggleHeroPreview()" title="30s Audio-Vorschau abspielen">
<i class="fa-solid fa-play"></i>
</button>`;
} else {
window.fetchCover(`${track.artist} ${track.title}`, document.getElementById('cover'));
}
// Update External Streaming Links
const trackActions = document.getElementById('track-actions');
if (trackActions) {
trackActions.style.display = 'flex';
const spotEl = document.getElementById('action-spotify');
const deezEl = document.getElementById('action-deezer');
const ytEl = document.getElementById('action-youtube');
if (spotEl) spotEl.href = getSpotifyUrl(track);
if (deezEl) deezEl.href = getDeezerUrl(track);
if (ytEl) ytEl.href = getYoutubeUrl(track);
}
// Fetch or apply metadata
let key = track.camelot_key || track.key || track.c;
let bpm = track.bpm;
let tags = track.tags || ['dance'];
let previewUrl = track.preview_url || null;
let durationSec = track.duration_sec || 215;
let energy = track.energy !== undefined ? track.energy : null;
let danceability = track.danceability !== undefined ? track.danceability : null;
let happiness = track.happiness !== undefined ? track.happiness : null;
let deezerLink = track.deezer_link || (track.deezer_id ? `https://www.deezer.com/track/${track.deezer_id}` : null);
let deezerId = track.deezer_id || null;
if (!key || !bpm || !energy) {
// Query metadata route to get key, bpm, and persist into PostgreSQL
try {
const metaRes = await fetch(`/api/metadata?track=${encodeURIComponent(track.title)}&artist=${encodeURIComponent(track.artist)}`);
const metaJson = await metaRes.json();
if (metaJson.status === 'success') {
const d = metaJson.data;
key = d.key;
bpm = d.bpm;
tags = d.tags || tags;
previewUrl = d.preview_url || previewUrl;
durationSec = d.duration_sec || durationSec;
energy = d.energy;
danceability = d.danceability;
happiness = d.happiness;
if (d.deezer_link || d.deezer_id) {
deezerLink = d.deezer_link || deezerLink;
deezerId = d.deezer_id || deezerId;
track.deezer_link = deezerLink;
track.deezer_id = deezerId;
const deezEl = document.getElementById('action-deezer');
if (deezEl) deezEl.href = getDeezerUrl(track);
}
const metaSource = formatSourceInfo(d.source, d.is_corrected);
document.getElementById('stat-match').textContent = metaSource.match;
document.getElementById('stat-source-sub').textContent = metaSource.label;
}
} catch (e) {
console.warn('[Search] Metadata fetch failed, using fallback', e);
key = '8B';
bpm = 120;
energy = 75;
danceability = 70;
happiness = 60;
}
}
window.currentTrack = {
id: track.id,
title: track.title,
artist: track.artist,
key: key,
bpm: bpm,
cover_url: track.cover_url,
preview_url: previewUrl,
duration_sec: durationSec,
energy: energy,
danceability: danceability,
happiness: happiness,
tags: tags
};
// Show hero preview button
const heroBtn = document.getElementById('hero-preview-btn');
if (heroBtn) heroBtn.style.display = 'flex';
updateHeroStats(key, bpm, tags, { durationSec, energy, danceability, happiness });
renderSuggestions(key, bpm, track.artist, track.title);
}
function updateHeroStats(key, bpm, tags = [], attrs = {}) {
document.getElementById('stat-key').textContent = key;
const musicKey = window.camelotToMusicKey ? window.camelotToMusicKey[key] : '';
document.getElementById('stat-key-sub').textContent = musicKey;
const keyColor = window.colorMatrix ? window.colorMatrix[key] : '#4F9D69';
document.getElementById('stat-key').style.color = keyColor;
document.documentElement.style.setProperty('--key-current', keyColor);
document.getElementById('stat-bpm').textContent = bpm;
document.getElementById('stat-bpm-sub').textContent = `± ${Math.round(bpm * 0.04)} BPM`;
// Render Tunebat Audio Attributes Grid
const attrGrid = document.getElementById('attributes-grid');
if (attrGrid) {
attrGrid.style.display = 'grid';
const durSec = attrs.durationSec || 215;
const nrg = attrs.energy || Math.min(95, Math.round(50 + ((bpm - 80) / 80) * 40));
const dnc = attrs.danceability || 75;
const mood = attrs.happiness || (key && key.endsWith('B') ? 68 : 45);
document.getElementById('attr-duration').textContent = formatDuration(durSec);
document.getElementById('attr-energy').textContent = `${nrg}%`;
document.getElementById('bar-energy').style.width = `${nrg}%`;
document.getElementById('attr-dance').textContent = `${dnc}%`;
document.getElementById('bar-dance').style.width = `${dnc}%`;
document.getElementById('attr-mood').textContent = `${mood}%`;
document.getElementById('bar-mood').style.width = `${mood}%`;
}
// Render Tags
const tagsRow = document.getElementById('tags-row');
tagsRow.innerHTML = '';
const keyTag = document.createElement('span');
keyTag.className = 'tag tag-key';
keyTag.textContent = `${key} · ${musicKey}`;
keyTag.style.backgroundColor = keyColor;
tagsRow.appendChild(keyTag);
tags.forEach(t => {
const el = document.createElement('span');
el.className = 'tag tag-genre';
el.textContent = t;
tagsRow.appendChild(el);
});
// Update Camelot Wheel
window.updateWheel(key);
}
// ----------------------------------------------------
// Suggestions Management & Interactive Filters (Tunebat Style)
// ----------------------------------------------------
let allSuggestionsData = [];
let currentHarmonyFilter = 'all'; // 'all', 'exact', 'quinte', 'relative', 'energy'
let currentBpmTolerance = 4; // 4, 8, 'all'
let currentRefBpm = 120;
function setHarmonyFilter(filter) {
currentHarmonyFilter = filter;
document.querySelectorAll('#suggestions-filters [data-filter]').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
});
applySuggestionsFilter();
}
function setBpmTolerance(tolerance) {
currentBpmTolerance = tolerance;
document.querySelectorAll('#suggestions-filters [data-bpm-tol]').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-bpm-tol') === String(tolerance));
});
applySuggestionsFilter();
}
function applySuggestionsFilter() {
const sugContainer = document.getElementById('suggestions');
const countBadge = document.getElementById('suggestions-count');
if (!sugContainer) return;
if (!allSuggestionsData || allSuggestionsData.length === 0) {
if (countBadge) countBadge.textContent = '0 Tracks';
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Keine passenden Tracks gefunden.</div>';
return;
}
// 1. Filter by Harmony Mix Type
let filtered = allSuggestionsData.filter(s => {
if (currentHarmonyFilter === 'all') return true;
if (currentHarmonyFilter === 'exact') return s.compat === 0; // Perfect (same key)
if (currentHarmonyFilter === 'quinte') return s.compat === 1 || s.compat === 2; // Quinte -1 / +1
if (currentHarmonyFilter === 'relative') return s.compat === 4; // Scale (A <-> B)
if (currentHarmonyFilter === 'energy') return s.compat === 3; // Energy Boost (+2)
return true;
});
let isFallback = false;
if (currentBpmTolerance !== 'all') {
const tolPercent = Number(currentBpmTolerance) / 100;
const minBpm = currentRefBpm * (1 - tolPercent);
const maxBpm = currentRefBpm * (1 + tolPercent);
const bpmFiltered = filtered.filter(s => s.bpm >= minBpm && s.bpm <= maxBpm);
if (bpmFiltered.length > 0) {
filtered = bpmFiltered;
} else if (filtered.length > 0) {
isFallback = true;
// Keep best matches sorted by compatibility
filtered = filtered.slice(0, 10);
}
}
if (countBadge) {
countBadge.textContent = isFallback ? `${filtered.length} Tracks (Erweitert)` : `${filtered.length} Tracks`;
}
if (filtered.length === 0) {
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Keine Tracks für die gewählten Filter vorhanden. Wähle eine größere BPM-Toleranz oder "Alle".</div>';
return;
}
sugContainer.innerHTML = '';
if (isFallback) {
const notice = document.createElement('div');
notice.className = 'sug-notice-banner';
notice.innerHTML = `<i class="fa-solid fa-circle-info"></i> Keine Tracks bei ±${currentBpmTolerance}% — zeige die harmonisch passendsten verfügbaren Tracks mit Pitch-Abweichung:`;
sugContainer.appendChild(notice);
}
filtered.forEach(s => {
const div = document.createElement('div');
div.className = 'sug';
const idSafe = `sug-${s.c}-${(s.title || '').replace(/[^A-Za-z0-9]/g, '')}`;
const pitchDiff = s.pitch_diff_pct !== undefined
? s.pitch_diff_pct
: (currentRefBpm ? Math.round(((s.bpm - currentRefBpm) / currentRefBpm) * 100) : 0);
const pitchSign = pitchDiff > 0 ? `+${pitchDiff}%` : `${pitchDiff}%`;
const pitchBadgeClass = Math.abs(pitchDiff) <= 4 ? 'pitch-dj' : (Math.abs(pitchDiff) <= 8 ? 'pitch-mid' : 'pitch-far');
const pitchBadgeHtml = (currentRefBpm && Math.abs(pitchDiff) > 0)
? `<span class="sug-pitch-badge ${pitchBadgeClass}" title="Pitch-Anpassung: ${pitchSign}">${pitchSign}</span>`
: '';
div.innerHTML = `
<div class="sug-cover" id="${idSafe}">
${s.cover_url ? `<img src="${s.cover_url}" alt="cover" loading="lazy">` : '<div class="sug-cover-placeholder">♪</div>'}
<button class="sug-play-btn" title="30s Vorschau abspielen">
<i class="fa-solid fa-play"></i>
</button>
</div>
<div class="sug-meta">
<div class="sug-key" style="background:${s.color}">${s.c}</div>
<div class="sug-bpm">${s.bpm} BPM ${pitchBadgeHtml}</div>
</div>
<div class="sug-sub-stats">
<span>⏱️ ${formatDuration(s.duration_sec)}</span>
<span class="sug-attr-pill sug-attr-energy">⚡ ${s.energy || 75}%</span>
</div>
<div>
<div class="sug-title" title="${escapeHtml(s.title)}">${escapeHtml(s.title)}</div>
<div class="sug-artist">${escapeHtml(s.artist)} · ${s.m}</div>
</div>
<div class="sug-bottom-row">
<div class="sug-compat">${s.compatLabel || 'KOMPATIBEL'}</div>
<div class="sug-links">
<a href="${getSpotifyUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-spotify" onclick="event.stopPropagation()" title="Auf Spotify öffnen">
<i class="fa-brands fa-spotify"></i>
</a>
<a href="${getDeezerUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-deezer" onclick="event.stopPropagation()" title="Auf Deezer öffnen">
<i class="fa-brands fa-deezer"></i>
</a>
<a href="${getYoutubeUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-youtube" onclick="event.stopPropagation()" title="Auf YouTube suchen">
<i class="fa-brands fa-youtube"></i>
</a>
</div>
</div>
`;
// Direct event listener for play preview button
const playBtn = div.querySelector('.sug-play-btn');
if (playBtn) {
playBtn.addEventListener('click', (e) => {
e.stopPropagation();
window.togglePreviewTrack(s, playBtn);
});
}
// Click to transition to this song as next current track!
div.addEventListener('click', () => {
selectTrack(s);
});
sugContainer.appendChild(div);
// Lazy load cover if not already available
if (!s.cover_url) {
window.fetchCover(`${s.artist} ${s.title}`, div.querySelector('.sug-cover'));
}
});
}
// Render real harmonic suggestions from PostgreSQL & Online
async function renderSuggestions(currentKey, currentBpm = 120, artist = '', title = '') {
const sugContainer = document.getElementById('suggestions');
if (!sugContainer) return;
currentRefBpm = currentBpm;
if (!currentKey) {
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Wähle einen Track für Vorschläge.</div>';
return;
}
sugContainer.innerHTML = '<div style="color:var(--text-dim);font-size:12px;font-family:monospace;">Berechne harmonische Übergänge & Tunebat-Metriken...</div>';
try {
const url = `/api/suggestions?key=${encodeURIComponent(currentKey)}&bpm=${currentBpm}&artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`;
const res = await fetch(url);
const json = await res.json();
if (json.status !== 'success') {
sugContainer.innerHTML = `<div style="color:#D62828;font-size:12px;font-family:monospace;">Fehler beim Laden der Vorschläge: ${escapeHtml(json.error || 'Serverfehler')}</div>`;
return;
}
allSuggestionsData = json.data || [];
applySuggestionsFilter();
} catch (e) {
console.error('[Suggestions] Error loading suggestions:', e);
sugContainer.innerHTML = '<div style="color:#D62828;font-size:12px;">Fehler beim Laden der Vorschläge.</div>';
}
}
// Key & BPM Correction Modal
function initEditKeyOptions() {
const select = document.getElementById('edit-key-select');
if (!select) return;
const camelotKeys = [
['12B', 'E'], ['12A', 'Dbm (C#m)'],
['1B', 'B'], ['1A', 'Abm (G#m)'],
['2B', 'F#'], ['2A', 'Ebm (D#m)'],
['3B', 'Db'], ['3A', 'Bbm (A#m)'],
['4B', 'Ab'], ['4A', 'Fm'],
['5B', 'Eb'], ['5A', 'Cm'],
['6B', 'Bb'], ['6A', 'Gm'],
['7B', 'F'], ['7A', 'Dm'],
['8B', 'C'], ['8A', 'Am'],
['9B', 'G'], ['9A', 'Em'],
['10B', 'D'], ['10A', 'Bm'],
['11B', 'A'], ['11A', 'F#m'],
];
select.innerHTML = '';
camelotKeys.forEach(([c, m]) => {
const opt = document.createElement('option');
opt.value = c;
opt.textContent = `${c} — ${m}`;
select.appendChild(opt);
});
}
function openKeyBpmModal() {
if (!window.currentTrack || !window.currentTrack.title) {
alert('Bitte wähle zuerst einen Track aus oder spiele einen Song ab.');
return;
}
const modal = document.getElementById('edit-modal');
const titleEl = document.getElementById('modal-track-title');
const keySelect = document.getElementById('edit-key-select');
const bpmInput = document.getElementById('edit-bpm-input');
titleEl.textContent = `Daten korrigieren: ${window.currentTrack.artist} – ${window.currentTrack.title}`;
if (window.currentTrack.key) keySelect.value = window.currentTrack.key;
if (window.currentTrack.bpm) bpmInput.value = window.currentTrack.bpm;
modal.style.display = 'flex';
}
function closeKeyBpmModal() {
const modal = document.getElementById('edit-modal');
if (modal) modal.style.display = 'none';
}
function closeModalOnOverlay(e) {
if (e.target.classList.contains('modal-overlay')) {
closeKeyBpmModal();
}
}
async function saveKeyBpmCorrection() {
if (!window.currentTrack) return;
const keySelect = document.getElementById('edit-key-select');
const bpmInput = document.getElementById('edit-bpm-input');
const newKey = keySelect.value;
const newBpm = parseInt(bpmInput.value, 10);
if (!newKey || isNaN(newBpm)) {
alert('Bitte gültigen Key und BPM eingeben.');
return;
}
try {
const res = await fetch('/api/tracks/correct', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
artist: window.currentTrack.artist,
title: window.currentTrack.title,
camelotKey: newKey,
bpm: newBpm
})
});
const json = await res.json();
if (json.status === 'success') {
window.currentTrack.key = newKey;
window.currentTrack.bpm = newBpm;
window.currentTrack.is_corrected = true;
document.getElementById('stat-source-sub').textContent = 'PostgreSQL (Korrigiert)';
updateHeroStats(newKey, newBpm, window.currentTrack.tags || ['dance']);
renderSuggestions(newKey, newBpm, window.currentTrack.artist, window.currentTrack.title);
closeKeyBpmModal();
} else {
alert('Speichern fehlgeschlagen: ' + (json.error || 'Serverfehler'));
}
} catch (err) {
console.error('Correction save error:', err);
alert('Verbindungsfehler beim Speichern in PostgreSQL.');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function selectKeyFromWheel(key, musicKey) {
if (!key) return;
const bpm = currentRefBpm || 120;
// Set hero title/artist if no active track
if (!window.currentTrack || !window.currentTrack.title) {
const titleEl = document.getElementById('track-title');
const artistEl = document.getElementById('track-artist');
if (titleEl) titleEl.textContent = `Harmonie für ${key}`;
if (artistEl) artistEl.textContent = musicKey ? `${musicKey} (Auswahl über Wheel)` : 'Auswahl über Wheel';
}
const keyColor = window.colorMatrix ? window.colorMatrix[key] : '#4F9D69';
const keyEl = document.getElementById('stat-key');
const keySubEl = document.getElementById('stat-key-sub');
if (keyEl) {
keyEl.textContent = key;
keyEl.style.color = keyColor;
}
if (keySubEl) {
keySubEl.textContent = musicKey || (window.camelotToMusicKey ? window.camelotToMusicKey[key] : '');
}
document.documentElement.style.setProperty('--key-current', keyColor);
window.updateWheel(key);
renderSuggestions(key, bpm, window.currentTrack ? window.currentTrack.artist : '', window.currentTrack ? window.currentTrack.title : '');
}
window.renderSuggestions = renderSuggestions;
window.selectTrack = selectTrack;
window.selectKeyFromWheel = selectKeyFromWheel;
window.openKeyBpmModal = openKeyBpmModal;
window.closeKeyBpmModal = closeKeyBpmModal;
window.saveKeyBpmCorrection = saveKeyBpmCorrection;
window.closeModalOnOverlay = closeModalOnOverlay;
window.setHarmonyFilter = setHarmonyFilter;
window.setBpmTolerance = setBpmTolerance;