Tap function fix #7
This commit is contained in:
+63
-17
@@ -13,6 +13,12 @@ const STORAGE_KEY_PRESETS = "ClickPresets"
|
|||||||
const STORAGE_KEY_VIEW = "ClickView"
|
const STORAGE_KEY_VIEW = "ClickView"
|
||||||
const STORAGE_KEY_SORT = "ClickSort"
|
const STORAGE_KEY_SORT = "ClickSort"
|
||||||
|
|
||||||
|
// TAP
|
||||||
|
const TAP_RESET_MS = 2000; // nach dieser Pause beginnt eine neue Messung
|
||||||
|
const TAP_MAX_SAMPLES = 8; // so viele letzte Taps werden gemittelt
|
||||||
|
|
||||||
|
let tapTimes = [];
|
||||||
|
|
||||||
// Reaktiver Zustand (für die UI)
|
// Reaktiver Zustand (für die UI)
|
||||||
const bpm = ref(120);
|
const bpm = ref(120);
|
||||||
const isRunning = ref(false);
|
const isRunning = ref(false);
|
||||||
@@ -81,20 +87,7 @@ async function start() {
|
|||||||
if (isRunning.value) return;
|
if (isRunning.value) return;
|
||||||
|
|
||||||
clampBpm();
|
clampBpm();
|
||||||
|
if (!initAudioInterface()) {
|
||||||
try {
|
|
||||||
// AudioContext erst durch User-Interaktion starten
|
|
||||||
if (!audioContext) {
|
|
||||||
audioContext = new AudioContext();
|
|
||||||
}
|
|
||||||
if (!clickBuffer) {
|
|
||||||
await loadClickSound();
|
|
||||||
}
|
|
||||||
if (audioContext.state === "suspended") {
|
|
||||||
await audioContext.resume();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Metronom konnte nicht gestartet werden:", error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +105,25 @@ function stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initAudioInterface() {
|
||||||
|
try {
|
||||||
|
// AudioContext erst durch User-Interaktion starten
|
||||||
|
if (!audioContext) {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
}
|
||||||
|
if (!clickBuffer) {
|
||||||
|
loadClickSound();
|
||||||
|
}
|
||||||
|
if (audioContext.state === "suspended") {
|
||||||
|
audioContext.resume();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Metronom konnte nicht gestartet werden:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stop();
|
stop();
|
||||||
if (audioContext) {
|
if (audioContext) {
|
||||||
@@ -122,6 +134,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
//PRESETS
|
//PRESETS
|
||||||
function loadPresets() {
|
function loadPresets() {
|
||||||
|
|
||||||
|
initAudioInterface();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY_PRESETS);
|
const raw = localStorage.getItem(STORAGE_KEY_PRESETS);
|
||||||
return raw ? JSON.parse(raw) : [];
|
return raw ? JSON.parse(raw) : [];
|
||||||
@@ -204,6 +219,35 @@ function setSort(val) {
|
|||||||
localStorage.setItem(STORAGE_KEY_SORT, val);
|
localStorage.setItem(STORAGE_KEY_SORT, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TAP
|
||||||
|
|
||||||
|
function tap() {
|
||||||
|
stop();
|
||||||
|
selectedPreset.value = null;
|
||||||
|
playClick(0);
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
// Lange Pause seit dem letzten Tap: neu beginnen
|
||||||
|
if (tapTimes.length && now - tapTimes[tapTimes.length - 1] > TAP_RESET_MS) {
|
||||||
|
tapTimes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
tapTimes.push(now);
|
||||||
|
if (tapTimes.length > TAP_MAX_SAMPLES) {
|
||||||
|
tapTimes.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mindestens zwei Taps nötig, um einen Abstand zu haben
|
||||||
|
if (tapTimes.length < 2) return;
|
||||||
|
|
||||||
|
// Durchschnittlicher Abstand zwischen den Taps in ms
|
||||||
|
const intervals = tapTimes.slice(1).map((t, i) => t - tapTimes[i]);
|
||||||
|
const avg = intervals.reduce((sum, v) => sum + v, 0) / intervals.length;
|
||||||
|
|
||||||
|
bpm.value = Math.round(60000 / avg);
|
||||||
|
clampBpm();
|
||||||
|
}
|
||||||
|
|
||||||
// EXPORT / IMPORT
|
// EXPORT / IMPORT
|
||||||
function exportPresets() {
|
function exportPresets() {
|
||||||
const json = JSON.stringify(presets.value, null, 2);
|
const json = JSON.stringify(presets.value, null, 2);
|
||||||
@@ -318,7 +362,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<div class="left">
|
<div class="grow left">
|
||||||
<button @click="$refs.fileInput.click()">
|
<button @click="$refs.fileInput.click()">
|
||||||
<font-awesome-icon :icon="faUpload" size="xl" />
|
<font-awesome-icon :icon="faUpload" size="xl" />
|
||||||
</button>
|
</button>
|
||||||
@@ -326,8 +370,10 @@ watch(
|
|||||||
<font-awesome-icon :icon="faDownload" size="xl" />
|
<font-awesome-icon :icon="faDownload" size="xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow"></div>
|
<div class="grow center">
|
||||||
<div class="right">
|
<button id="tapButton" @click="tap">TAP</button>
|
||||||
|
</div>
|
||||||
|
<div class="grow right">
|
||||||
<button v-if="view == 'grid'" @click="setView('list')">
|
<button v-if="view == 'grid'" @click="setView('list')">
|
||||||
<font-awesome-icon :icon="faList" size="xl" />
|
<font-awesome-icon :icon="faList" size="xl" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -223,6 +223,36 @@ input[type="number"] {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.center,
|
||||||
|
.right {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tapButton {
|
||||||
|
border-radius: 32px;
|
||||||
|
height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
margin: -8px;
|
||||||
|
background: var(--secondary);
|
||||||
|
box-shadow: 0 0px 4px var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
/* DIALOG */
|
/* DIALOG */
|
||||||
#dialog {
|
#dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user