diff --git a/packages/frontend/package.json b/packages/frontend/package.json index f1d5ec656..612e07826 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -76,7 +76,8 @@ "vite": "5.4.11", "vue": "3.5.13", "vue-gtag": "2.0.1", - "vuedraggable": "next" + "vuedraggable": "next", + "webgl-audiovisualizer": "github:tar-bin/webgl-audiovisualizer" }, "devDependencies": { "@misskey-dev/eslint-plugin": "1.0.0", @@ -107,6 +108,7 @@ "@types/node": "22.10.0", "@types/punycode": "2.1.4", "@types/sanitize-html": "2.13.0", + "@types/three": "0.170.0", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/ws": "8.5.13", diff --git a/packages/frontend/src/components/MkAudioVisualizer.vue b/packages/frontend/src/components/MkAudioVisualizer.vue new file mode 100644 index 000000000..35f2603b4 --- /dev/null +++ b/packages/frontend/src/components/MkAudioVisualizer.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index b23ef9b72..88a55f17c 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -35,40 +35,43 @@ SPDX-License-Identifier: AGPL-3.0-only -
- -
- -
-
- -
-
{{ hms(elapsedTimeMs) }}
-
- +
+ +
+ +
+ +
+
+ +
+
{{ hms(elapsedTimeMs) }}
+
+ + +
-
@@ -82,12 +85,14 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; +import MkAudioVisualizer from '@/components/MkAudioVisualizer.vue'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { pleaseLogin } from '@/scripts/please-login.js'; import { $i, iAmModerator } from '@/account.js'; const props = defineProps<{ audio: Misskey.entities.DriveFile; + user?: Misskey.entities.UserLite; }>(); const keymap = { @@ -126,6 +131,7 @@ function hasFocus() { const playerEl = shallowRef(); const audioEl = shallowRef(); +const audioVisualizer = ref>(); // eslint-disable-next-line vue/no-setup-props-destructure const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); @@ -240,6 +246,11 @@ const isPlaying = ref(false); const isActuallyPlaying = ref(false); const elapsedTimeMs = ref(0); const durationMs = ref(0); +const audioContext = ref(null); +const sourceNode = ref(null); +const gainNode = ref(null); +const analyserGainNode = ref(null); +const analyserNode = ref(null); const rangePercent = computed({ get: () => { return (elapsedTimeMs.value / durationMs.value) || 0; @@ -262,11 +273,33 @@ const bufferedDataRatio = computed(() => { function togglePlayPause() { if (!isReady.value || !audioEl.value) return; + if (!sourceNode.value) { + audioContext.value = new (window.AudioContext || window.webkitAudioContext)(); + sourceNode.value = audioContext.value.createMediaElementSource(audioEl.value); + + analyserGainNode.value = audioContext.value.createGain(); + gainNode.value = audioContext.value.createGain(); + analyserNode.value = audioContext.value.createAnalyser(); + + sourceNode.value.connect(analyserGainNode.value); + analyserGainNode.value.connect(analyserNode.value); + analyserNode.value.connect(gainNode.value); + gainNode.value.connect(audioContext.value.destination); + + analyserNode.value.fftSize = 2048; + + analyserGainNode.value.gain.setValueAtTime(0.8, audioContext.value.currentTime); + + gainNode.value.gain.setValueAtTime(volume.value, audioContext.value.currentTime); + } + if (isPlaying.value) { audioEl.value.pause(); + audioVisualizer.value?.pauseAnimation(); isPlaying.value = false; } else { audioEl.value.play(); + audioVisualizer.value?.resumeAnimation(); isPlaying.value = true; oncePlayed.value = true; } @@ -324,6 +357,7 @@ function init() { oncePlayed.value = false; isActuallyPlaying.value = false; isPlaying.value = false; + audioVisualizer.value?.pauseAnimation(); }); durationMs.value = audioEl.value.duration * 1000; @@ -332,8 +366,7 @@ function init() { durationMs.value = audioEl.value.duration * 1000; } }); - - audioEl.value.volume = volume.value; + gainNode.value?.gain.setValueAtTime(volume.value, audioContext.value?.currentTime); } }, { immediate: true, @@ -341,7 +374,7 @@ function init() { } watch(volume, (to) => { - if (audioEl.value) audioEl.value.volume = to; + if (audioEl.value) gainNode.value?.gain.setValueAtTime(to, audioContext.value?.currentTime); }); watch(speed, (to) => { diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 7a128fbad..7eeab073c 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only