feat: add music visualiser

This commit is contained in:
Harvey Lelliott 2023-05-16 17:45:44 +00:00 committed by Fernando Fernández
parent 6d2157f265
commit d0b4d5feef
6 changed files with 117 additions and 26 deletions

View File

@ -30,6 +30,7 @@
"@jellyfin/sdk": "0.8.2",
"@vueuse/components": "10.2.1",
"@vueuse/core": "10.2.1",
"audiomotion-analyzer": "4.0.0",
"axios": "1.4.0",
"blurhash": "2.0.5",
"comlink": "4.4.1",

View File

@ -1,7 +1,7 @@
<template>
<v-btn
:icon="isFavorite ? IMdiHeart : IMdiHeartOutline"
size="small"
:size="size"
:color="isFavorite ? 'primary' : undefined"
:loading="loading"
@click.stop.prevent="isFavorite = !isFavorite" />
@ -15,7 +15,12 @@ import IMdiHeart from 'virtual:icons/mdi/heart';
import IMdiHeartOutline from 'virtual:icons/mdi/heart-outline';
import { useRemote } from '@/composables';
const props = defineProps<{ item: BaseItemDto }>();
const props = withDefaults(
defineProps<{ item: BaseItemDto; size?: string }>(),
{
size: 'small'
}
);
const remote = useRemote();
const loading = ref(false);

View File

@ -0,0 +1,44 @@
<template>
<div ref="musicVisualizer" />
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import AudioMotionAnalyzer from 'audiomotion-analyzer';
import { mediaElementRef } from '@/store';
let visualizer: AudioMotionAnalyzer;
const musicVisualizer = ref(undefined);
onMounted(() => {
visualizer = new AudioMotionAnalyzer(musicVisualizer.value, {
source: mediaElementRef.value,
mode: 2,
gradient: 'prism',
reflexRatio: 0.025,
overlay: true,
showBgColor: true,
bgAlpha: 0,
fftSize: 16_384,
frequencyScale: 'bark',
showScaleX: false,
smoothing: 0.9
});
});
onBeforeUnmount(() => {
if (visualizer) {
visualizer.toggleAnalyzer();
visualizer.disconnectInput();
visualizer.disconnectOutput();
if (mediaElementRef.value) {
const audioCtx = new AudioContext();
audioCtx
.createMediaElementSource(mediaElementRef.value)
.connect(audioCtx.destination);
}
}
});
</script>

View File

@ -10,30 +10,33 @@
</app-bar-button-layout>
</v-app-bar>
<v-col class="px-0">
<swiper
v-if="playbackManager.queue"
class="d-flex justify-center align-center user-select-none"
:modules="modules"
:slides-per-view="4"
centered-slides
:autoplay="false"
effect="coverflow"
:coverflow-effect="coverflowEffect"
keyboard
a11y
virtual
@slide-change="onSlideChange"
@swiper="setControlledSwiper">
<swiper-slide
v-for="(item, index) in playbackManager.queue"
:key="`${item.Id}-${index}`"
:virtual-index="`${item.Id}-${index}`"
class="d-flex justify-center">
<div class="album-cover">
<blurhash-image :item="item" />
</div>
</swiper-slide>
</swiper>
<transition v-if="playbackManager.queue">
<swiper
v-if="!isVisualizing"
class="d-flex justify-center align-center user-select-none"
:modules="modules"
:slides-per-view="4"
centered-slides
:autoplay="false"
effect="coverflow"
:coverflow-effect="coverflowEffect"
keyboard
a11y
virtual
@slide-change="onSlideChange"
@swiper="setControlledSwiper">
<swiper-slide
v-for="(item, index) in playbackManager.queue"
:key="`${item.Id}-${index}`"
:virtual-index="`${item.Id}-${index}`"
class="d-flex justify-center">
<div class="album-cover">
<blurhash-image :item="item" />
</div>
</swiper-slide>
</swiper>
<music-visualizer v-else class="visualizer" />
</transition>
<v-row class="justify-center align-center mt-3">
<v-col cols="6">
<v-row class="justify-center align-center">
@ -50,11 +53,25 @@
<time-slider />
</v-row>
<v-row class="justify-center align-center">
<v-btn
icon
size="x-large"
:color="isVisualizing ? 'primary' : undefined"
@click.stop.prevent="isVisualizing = !isVisualizing">
<v-icon size="x-large">
<i-mdi-chart-bar />
</v-icon>
</v-btn>
<shuffle-button size="x-large" />
<previous-track-button size="x-large" />
<play-pause-button size="x-large" />
<next-track-button size="x-large" />
<repeat-button size="x-large" />
<like-button
v-if="playbackManager.currentItem"
:item="playbackManager?.currentItem"
size="x-large"
class="active-button" />
</v-row>
</v-col>
</v-row>
@ -92,6 +109,7 @@ const modules = [A11y, Keyboard, Virtual, EffectCoverflow];
const route = useRoute();
const playbackManager = playbackManagerStore();
const coverflowEffect = {
depth: 500,
slideShadows: false,
@ -99,6 +117,8 @@ const coverflowEffect = {
stretch: -400
};
const isVisualizing = ref(false);
const backdropHash = computed(() => {
return playbackManager.currentItem
? getBlurhash(playbackManager.currentItem, ImageType.Primary)
@ -161,4 +181,13 @@ function onSlideChange(): void {
min-width: 65vh;
width: 65vh;
}
.visualizer {
display: flex;
justify-content: center;
align-self: center;
user-select: none;
height: 65vh;
padding: 1vh;
}
</style>

View File

@ -41,6 +41,7 @@ declare module 'vue' {
IMdiBrightnessAuto: typeof import('~icons/mdi/brightness-auto')['default']
IMdiCalendarRange: typeof import('~icons/mdi/calendar-range')['default']
IMdiCast: typeof import('~icons/mdi/cast')['default']
IMdiChartBar: typeof import('~icons/mdi/chart-bar')['default']
IMdiCheck: typeof import('~icons/mdi/check')['default']
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IMdiChevronUp: typeof import('~icons/mdi/chevron-up')['default']
@ -100,6 +101,7 @@ declare module 'vue' {
MediaStreamSelector: typeof import('./../../src/components/Item/MediaStreamSelector.vue')['default']
MetadataEditor: typeof import('./../../src/components/Item/Metadata/MetadataEditor.vue')['default']
MetadataEditorDialog: typeof import('./../../src/components/Item/Metadata/MetadataEditorDialog.vue')['default']
MusicVisualizer: typeof import('./../../src/components/Playback/MusicVisualizer.vue')['default']
NavigationDrawer: typeof import('./../../src/components/Layout/Navigation/NavigationDrawer.vue')['default']
NextTrackButton: typeof import('./../../src/components/Buttons/Playback/NextTrackButton.vue')['default']
PeopleList: typeof import('./../../src/components/Item/PeopleList.vue')['default']

10
package-lock.json generated
View File

@ -25,6 +25,7 @@
"@jellyfin/sdk": "0.8.2",
"@vueuse/components": "10.2.1",
"@vueuse/core": "10.2.1",
"audiomotion-analyzer": "4.0.0",
"axios": "1.4.0",
"blurhash": "2.0.5",
"comlink": "4.4.1",
@ -2068,6 +2069,15 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/audiomotion-analyzer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.0.0.tgz",
"integrity": "sha512-TTMt+XYFhbm//z2G4gNw5g+eP58GW8E+ON8cv+NmpupOh1kbREgxsat8RLbh5bmQj7sSSB96AyFC9FLe5xrJFA==",
"funding": {
"type": "Ko-fi",
"url": "https://ko-fi.com/hvianna"
}
},
"node_modules/autoprefixer": {
"version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",