diff --git a/uniapp/src/components/dev-training-entry/index.vue b/uniapp/src/components/dev-training-entry/index.vue index 13df6145..487e52d1 100644 --- a/uniapp/src/components/dev-training-entry/index.vue +++ b/uniapp/src/components/dev-training-entry/index.vue @@ -1,8 +1,27 @@ @@ -18,49 +37,64 @@ const props = withDefaults( ) const visible = ref(isDevMode()) +const expanded = ref(false) -const goTraining = () => { - uni.navigateTo({ url: '/pages/training/index' }) +const toggle = () => { + expanded.value = !expanded.value +} + +const goto = (url: string) => { + expanded.value = false + uni.navigateTo({ url }) } diff --git a/uniapp/src/hooks/useMetronome.ts b/uniapp/src/hooks/useMetronome.ts index d128440f..29fdde19 100644 --- a/uniapp/src/hooks/useMetronome.ts +++ b/uniapp/src/hooks/useMetronome.ts @@ -5,6 +5,7 @@ export interface MetronomeOptions { bpmMin?: number bpmMax?: number clickSrc?: string + accentSrc?: string accentEvery?: number onBeat?: (beatIndex: number, isAccent: boolean) => void } @@ -13,9 +14,9 @@ export function useMetronome(options: MetronomeOptions = {}) { const { initialBpm = 80, bpmMin = 40, - bpmMax = 200, + bpmMax = 240, clickSrc = '/static/training/audio/click.mp3', - accentEvery = 4, + accentSrc, onBeat, } = options @@ -23,37 +24,47 @@ export function useMetronome(options: MetronomeOptions = {}) { const isPlaying = ref(false) const beatIndex = ref(0) const isAccent = ref(false) + const accentEveryRef = ref(options.accentEvery ?? 4) const intervalMs = computed(() => 60000 / bpm.value) - let audioCtx: UniApp.InnerAudioContext | null = null + let clickCtx: UniApp.InnerAudioContext | null = null + let accentCtx: UniApp.InnerAudioContext | null = null let timer: ReturnType | null = null let nextTickAt = 0 const ensureAudio = () => { - if (audioCtx) return - audioCtx = uni.createInnerAudioContext() - audioCtx.src = clickSrc - audioCtx.obeyMuteSwitch = false + if (!clickCtx) { + clickCtx = uni.createInnerAudioContext() + clickCtx.src = clickSrc + clickCtx.obeyMuteSwitch = false + } + if (accentSrc && !accentCtx) { + accentCtx = uni.createInnerAudioContext() + accentCtx.src = accentSrc + accentCtx.obeyMuteSwitch = false + } } - const playClick = () => { - if (!audioCtx) return + const playSound = (accent: boolean) => { + const ctx = accent && accentCtx ? accentCtx : clickCtx + if (!ctx) return try { - audioCtx.seek(0) - audioCtx.play() + ctx.seek(0) + ctx.play() } catch (_) { - audioCtx.play() + ctx.play() } } const tick = () => { if (!isPlaying.value) return - playClick() - - const accent = beatIndex.value % accentEvery === 0 + const every = Math.max(1, accentEveryRef.value) + const accent = beatIndex.value % every === 0 isAccent.value = accent + + playSound(accent) onBeat?.(beatIndex.value, accent) beatIndex.value++ @@ -85,10 +96,37 @@ export function useMetronome(options: MetronomeOptions = {}) { bpm.value = Math.max(bpmMin, Math.min(bpmMax, Math.round(val))) } + const setAccentEvery = (n: number) => { + accentEveryRef.value = Math.max(1, Math.min(16, Math.round(n))) + beatIndex.value = 0 + } + + let tapTimes: number[] = [] + const tapTempo = (): number | null => { + const now = Date.now() + if (tapTimes.length > 0 && now - tapTimes[tapTimes.length - 1] > 2000) { + tapTimes = [] + } + tapTimes.push(now) + if (tapTimes.length > 5) tapTimes.shift() + if (tapTimes.length < 2) return null + + const intervals: number[] = [] + for (let i = 1; i < tapTimes.length; i++) { + intervals.push(tapTimes[i] - tapTimes[i - 1]) + } + const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length + const detected = Math.round(60000 / avg) + setBpm(detected) + return detected + } + onUnmounted(() => { stop() - audioCtx?.destroy?.() - audioCtx = null + clickCtx?.destroy?.() + accentCtx?.destroy?.() + clickCtx = null + accentCtx = null }) return { @@ -97,8 +135,11 @@ export function useMetronome(options: MetronomeOptions = {}) { beatIndex, isAccent, intervalMs, + accentEvery: accentEveryRef, start, stop, setBpm, + setAccentEvery, + tapTempo, } } diff --git a/uniapp/src/pages.json b/uniapp/src/pages.json index d02860f2..5fecba5b 100644 --- a/uniapp/src/pages.json +++ b/uniapp/src/pages.json @@ -166,6 +166,14 @@ "style": { "navigationBarTitleText": "练一练" } + }, + { + "path": "pages/training/metronome", + "style": { + "navigationBarTitleText": "节拍器", + "navigationBarBackgroundColor": "#0f172a", + "navigationBarTextStyle": "white" + } } ], "subPackages": [{ diff --git a/uniapp/src/pages/training/index.vue b/uniapp/src/pages/training/index.vue index 4dda44b9..36577310 100644 --- a/uniapp/src/pages/training/index.vue +++ b/uniapp/src/pages/training/index.vue @@ -1,8 +1,13 @@