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
-
-
+
{{ i18n.ts.sensitive }}
{{ i18n.ts.clickToShow }}
+
(), {
});
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 0021ee2bd..9c836b586 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
();
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 24cba944c..54e7e161f 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index a739eb909..1656c7cba 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 9a07826f1..8b0f981c3 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
({{ i18n.tsx.withNFiles({ n: note.files.length }) }})
-
+
{{ i18n.ts.poll }}
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index b4663d7c4..f6d677c70 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 623b6691b..35fde8c22 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
RN: ...
-
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae0914798..e48a6d700 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -884,6 +884,9 @@ importers:
vuedraggable:
specifier: next
version: 4.1.0(vue@3.5.13(typescript@5.7.2))
+ webgl-audiovisualizer:
+ specifier: github:tar-bin/webgl-audiovisualizer
+ version: https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc
devDependencies:
'@misskey-dev/eslint-plugin':
specifier: 1.0.0
@@ -969,6 +972,9 @@ importers:
'@types/sanitize-html':
specifier: 2.13.0
version: 2.13.0
+ '@types/three':
+ specifier: 0.170.0
+ version: 0.170.0
'@types/throttle-debounce':
specifier: 5.0.2
version: 5.0.2
@@ -3550,6 +3556,9 @@ packages:
resolution: {integrity: sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==}
engines: {node: '>=14.17'}
+ '@tweenjs/tween.js@23.1.3':
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
+
'@twemoji/parser@15.0.0':
resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
@@ -3835,9 +3844,15 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+ '@types/stats.js@0.17.3':
+ resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
+
'@types/statuses@2.0.5':
resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
+ '@types/three@0.170.0':
+ resolution: {integrity: sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==}
+
'@types/throttle-debounce@5.0.2':
resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==}
@@ -3862,6 +3877,9 @@ packages:
'@types/web-push@3.6.4':
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
+ '@types/webxr@0.5.20':
+ resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==}
+
'@types/ws@8.5.13':
resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
@@ -5633,6 +5651,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@@ -7018,6 +7039,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ meshoptimizer@0.18.1:
+ resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
+
methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
@@ -9737,6 +9761,10 @@ packages:
resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
engines: {node: '>= 8'}
+ webgl-audiovisualizer@https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc:
+ resolution: {tarball: https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc}
+ version: 0.0.0
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -12958,6 +12986,8 @@ snapshots:
'@tsd/typescript@5.4.5': {}
+ '@tweenjs/tween.js@23.1.3': {}
+
'@twemoji/parser@15.0.0': {}
'@twemoji/parser@15.1.0': {}
@@ -13253,8 +13283,19 @@ snapshots:
'@types/stack-utils@2.0.3': {}
+ '@types/stats.js@0.17.3': {}
+
'@types/statuses@2.0.5': {}
+ '@types/three@0.170.0':
+ dependencies:
+ '@tweenjs/tween.js': 23.1.3
+ '@types/stats.js': 0.17.3
+ '@types/webxr': 0.5.20
+ '@webgpu/types': 0.1.38
+ fflate: 0.8.2
+ meshoptimizer: 0.18.1
+
'@types/throttle-debounce@5.0.2': {}
'@types/tinycolor2@1.4.6': {}
@@ -13275,6 +13316,8 @@ snapshots:
dependencies:
'@types/node': 22.10.0
+ '@types/webxr@0.5.20': {}
+
'@types/ws@8.5.13':
dependencies:
'@types/node': 22.10.0
@@ -15540,6 +15583,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0
+ fflate@0.8.2: {}
+
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -17228,6 +17273,8 @@ snapshots:
merge2@1.4.1: {}
+ meshoptimizer@0.18.1: {}
+
methods@1.1.2: {}
mfm-js@0.24.0:
@@ -17735,7 +17782,7 @@ snapshots:
nodemon@3.1.7:
dependencies:
chokidar: 4.0.1
- debug: 4.3.7(supports-color@5.5.0)
+ debug: 4.3.7(supports-color@8.1.1)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
@@ -20152,6 +20199,8 @@ snapshots:
web-streams-polyfill@4.0.0: {}
+ webgl-audiovisualizer@https://codeload.github.com/tar-bin/webgl-audiovisualizer/tar.gz/345b9e82e25a1501749c622790b924658e4914cc: {}
+
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}