mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
add new background animation
This commit is contained in:
@@ -43,10 +43,6 @@ app.get( '/showcase.js', ( req, res ) => {
|
|||||||
res.sendFile( path.join( __dirname + '/client/showcase.js' ) );
|
res.sendFile( path.join( __dirname + '/client/showcase.js' ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
app.get( '/colorPaletteExtractor.js', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/colorPaletteExtractor.js' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/showcase.css', ( req, res ) => {
|
app.get( '/showcase.css', ( req, res ) => {
|
||||||
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
|
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
|
||||||
} );
|
} );
|
||||||
|
|||||||
@@ -11,14 +11,17 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat {
|
.beat, .beat-manual {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
background-color: rgba( 0, 0, 0, 0.15 );
|
||||||
animation: beatAnim 0.6s infinite linear;
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.beat {
|
||||||
|
animation: beatAnim 0.6s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes beatAnim {
|
@keyframes beatAnim {
|
||||||
0% {
|
0% {
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
background-color: rgba( 0, 0, 0, 0.2 );
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
// https://github.com/zygisS22/color-palette-extraction/blob/master/index.js
|
|
||||||
|
|
||||||
|
|
||||||
const buildRgb = ( imageData ) => {
|
|
||||||
const rgbValues = [];
|
|
||||||
// note that we are loopin every 4!
|
|
||||||
// for every Red, Green, Blue and Alpha
|
|
||||||
for (let i = 0; i < imageData.length; i += 4) {
|
|
||||||
const rgb = {
|
|
||||||
r: imageData[ i ],
|
|
||||||
g: imageData[ i + 1 ],
|
|
||||||
b: imageData[ i + 2 ],
|
|
||||||
};
|
|
||||||
|
|
||||||
rgbValues.push(rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgbValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns what color channel has the biggest difference
|
|
||||||
const findBiggestColorRange = (rgbValues) => {
|
|
||||||
/**
|
|
||||||
* Min is initialized to the maximum value posible
|
|
||||||
* from there we procced to find the minimum value for that color channel
|
|
||||||
*
|
|
||||||
* Max is initialized to the minimum value posible
|
|
||||||
* from there we procced to fin the maximum value for that color channel
|
|
||||||
*/
|
|
||||||
let rMin = Number.MAX_VALUE;
|
|
||||||
let gMin = Number.MAX_VALUE;
|
|
||||||
let bMin = Number.MAX_VALUE;
|
|
||||||
|
|
||||||
let rMax = Number.MIN_VALUE;
|
|
||||||
let gMax = Number.MIN_VALUE;
|
|
||||||
let bMax = Number.MIN_VALUE;
|
|
||||||
|
|
||||||
rgbValues.forEach((pixel) => {
|
|
||||||
rMin = Math.min(rMin, pixel.r);
|
|
||||||
gMin = Math.min(gMin, pixel.g);
|
|
||||||
bMin = Math.min(bMin, pixel.b);
|
|
||||||
|
|
||||||
rMax = Math.max(rMax, pixel.r);
|
|
||||||
gMax = Math.max(gMax, pixel.g);
|
|
||||||
bMax = Math.max(bMax, pixel.b);
|
|
||||||
});
|
|
||||||
|
|
||||||
const rRange = rMax - rMin;
|
|
||||||
const gRange = gMax - gMin;
|
|
||||||
const bRange = bMax - bMin;
|
|
||||||
|
|
||||||
// determine which color has the biggest difference
|
|
||||||
const biggestRange = Math.max(rRange, gRange, bRange);
|
|
||||||
if (biggestRange === rRange) {
|
|
||||||
return 'r';
|
|
||||||
} else if (biggestRange === gRange) {
|
|
||||||
return 'g';
|
|
||||||
} else {
|
|
||||||
return 'b';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Median cut implementation
|
|
||||||
* can be found here -> https://en.wikipedia.org/wiki/Median_cut
|
|
||||||
*/
|
|
||||||
const quantization = ( rgbValues, depth ) => {
|
|
||||||
const MAX_DEPTH = 4;
|
|
||||||
|
|
||||||
// Base case
|
|
||||||
if ( depth === MAX_DEPTH || rgbValues.length === 0 ) {
|
|
||||||
const color = rgbValues.reduce(
|
|
||||||
( prev, curr ) => {
|
|
||||||
prev.r += curr.r;
|
|
||||||
prev.g += curr.g;
|
|
||||||
prev.b += curr.b;
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
r: 0,
|
|
||||||
g: 0,
|
|
||||||
b: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
color.r = Math.round( color.r / rgbValues.length );
|
|
||||||
color.g = Math.round( color.g / rgbValues.length );
|
|
||||||
color.b = Math.round( color.b / rgbValues.length );
|
|
||||||
|
|
||||||
return [color];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively do the following:
|
|
||||||
* 1. Find the pixel channel (red,green or blue) with biggest difference/range
|
|
||||||
* 2. Order by this channel
|
|
||||||
* 3. Divide in half the rgb colors list
|
|
||||||
* 4. Repeat process again, until desired depth or base case
|
|
||||||
*/
|
|
||||||
const componentToSortBy = findBiggestColorRange( rgbValues );
|
|
||||||
rgbValues.sort( ( p1, p2 ) => {
|
|
||||||
return p1[ componentToSortBy ] - p2[ componentToSortBy ];
|
|
||||||
} );
|
|
||||||
|
|
||||||
const mid = rgbValues.length / 2;
|
|
||||||
return [
|
|
||||||
...quantization( rgbValues.slice( 0, mid ), depth + 1 ),
|
|
||||||
...quantization( rgbValues.slice( mid + 1 ), depth + 1 ),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColourPalette = ( imageURL ) => {
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
|
|
||||||
// Set the canvas size to be the same as of the uploaded image
|
|
||||||
let image = new Image();
|
|
||||||
image.src = imageURL;
|
|
||||||
const canvas = document.getElementById( 'canvas' );
|
|
||||||
setTimeout( () => {
|
|
||||||
canvas.width = image.width ?? 500;
|
|
||||||
canvas.height = image.height ?? 500;
|
|
||||||
const ctx = canvas.getContext( '2d' );
|
|
||||||
try {
|
|
||||||
ctx.drawImage( image, 0, 0 );
|
|
||||||
} catch ( err ) {
|
|
||||||
reject( err );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getImageData returns an array full of RGBA values
|
|
||||||
* each pixel consists of four values: the red value of the colour, the green, the blue and the alpha
|
|
||||||
* (transparency). For array value consistency reasons,
|
|
||||||
* the alpha is not from 0 to 1 like it is in the RGBA of CSS, but from 0 to 255.
|
|
||||||
*/
|
|
||||||
const imageData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
|
|
||||||
|
|
||||||
// Convert the image data to RGB values so its much simpler
|
|
||||||
const rgbArray = buildRgb( imageData.data );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Color quantization
|
|
||||||
* A process that reduces the number of colors used in an image
|
|
||||||
* while trying to visually maintin the original image as much as possible
|
|
||||||
*/
|
|
||||||
resolve( quantization( rgbArray, 0 ) );
|
|
||||||
}, 1000 );
|
|
||||||
} );
|
|
||||||
};
|
|
||||||
@@ -170,3 +170,15 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper {
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
right: 0.5%;
|
||||||
|
top: 0.5%;
|
||||||
|
padding: 0.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
@@ -16,13 +16,20 @@
|
|||||||
<div v-if="hasLoaded" style="width: 100%">
|
<div v-if="hasLoaded" style="width: 100%">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
||||||
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art">
|
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
|
||||||
<div class="current-song">
|
<div class="current-song">
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
<progress max="1000" id="progress" :value="progressBar"></progress>
|
||||||
<h1>{{ playingSong.title }}</h1>
|
<h1>{{ playingSong.title }}</h1>
|
||||||
<p>{{ playingSong.artist }}</p>
|
<p>{{ playingSong.artist }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mode-selector-wrapper">
|
||||||
|
<select v-model="visualizationSettings" @change="setVisualization()">
|
||||||
|
<option value="mic">Microphone (Mic access required)</option>
|
||||||
|
<option value="bpm">BPM (might not be 100% accurate)</option>
|
||||||
|
<option value="off">No visualization except background</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="song-list-wrapper">
|
<div class="song-list-wrapper">
|
||||||
<div v-for="song in songQueue" class="song-list">
|
<div v-for="song in songQueue" class="song-list">
|
||||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
||||||
@@ -51,12 +58,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="background" id="background">
|
<div class="background" id="background">
|
||||||
<div class="beat"></div>
|
<div class="beat"></div>
|
||||||
|
<div class="beat-manual"></div>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<script src="/colorPaletteExtractor.js"></script>
|
<!-- <script src="/colorPaletteExtractor.js"></script> -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
|
||||||
<script src="/showcase.js"></script>
|
<script src="/showcase.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -16,6 +16,10 @@ createApp( {
|
|||||||
progressBar: 0,
|
progressBar: 0,
|
||||||
timeTracker: null,
|
timeTracker: null,
|
||||||
timeCorrector: null,
|
timeCorrector: null,
|
||||||
|
visualizationSettings: 'mic',
|
||||||
|
micAnalyzer: null,
|
||||||
|
beatDetected: false,
|
||||||
|
colorThief: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -63,21 +67,26 @@ createApp( {
|
|||||||
clearInterval( this.timeCorrector );
|
clearInterval( this.timeCorrector );
|
||||||
this.oldPos = this.pos;
|
this.oldPos = this.pos;
|
||||||
},
|
},
|
||||||
// getTimeUntil( song ) {
|
getImageData() {
|
||||||
// let timeRemaining = 0;
|
return new Promise( ( resolve, reject ) => {
|
||||||
// for ( let i = this.queuePos; i < this.songs.length; i++ ) {
|
if ( this.playingSong.hasCoverArt ) {
|
||||||
// if ( this.songs[ i ] == song ) {
|
setTimeout( () => {
|
||||||
// break;
|
const img = document.getElementById( 'current-image' );
|
||||||
// }
|
if ( img.complete ) {
|
||||||
// timeRemaining += parseInt( this.songs[ i ].duration );
|
resolve( this.colorThief.getPalette( img ) );
|
||||||
// }
|
} else {
|
||||||
// if ( timeRemaining === 0 ) {
|
img.addEventListener( 'load', () => {
|
||||||
// return 'Currently playing';
|
resolve( this.colorThief.getPalette( img ) );
|
||||||
// } else {
|
} );
|
||||||
// return 'Playing in about ' + Math.ceil( timeRemaining / 60 ) + 'min';
|
}
|
||||||
// }
|
}, 500 );
|
||||||
// },
|
} else {
|
||||||
|
reject( 'no image' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
connect() {
|
connect() {
|
||||||
|
this.colorThief = new ColorThief();
|
||||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
||||||
source.onmessage = ( e ) => {
|
source.onmessage = ( e ) => {
|
||||||
let data;
|
let data;
|
||||||
@@ -95,7 +104,7 @@ createApp( {
|
|||||||
this.startTime = new Date().getTime();
|
this.startTime = new Date().getTime();
|
||||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
||||||
this.queuePos = data.data.queuePos ?? 0;
|
this.queuePos = data.data.queuePos ?? 0;
|
||||||
getColourPalette( '/getSongCover?filename=' + data.data.playingSong.filename ).then( palette => {
|
this.getImageData().then( palette => {
|
||||||
this.colourPalette = palette;
|
this.colourPalette = palette;
|
||||||
this.handleBackground();
|
this.handleBackground();
|
||||||
} ).catch( () => {
|
} ).catch( () => {
|
||||||
@@ -114,11 +123,11 @@ createApp( {
|
|||||||
this.songs = data.data;
|
this.songs = data.data;
|
||||||
} else if ( data.type === 'playingSong' ) {
|
} else if ( data.type === 'playingSong' ) {
|
||||||
this.playingSong = data.data;
|
this.playingSong = data.data;
|
||||||
getColourPalette( '/getSongCover?filename=' + data.data.filename ).then( palette => {
|
this.getImageData().then( palette => {
|
||||||
this.colourPalette = palette;
|
this.colourPalette = palette;
|
||||||
this.handleBackground();
|
this.handleBackground();
|
||||||
} ).catch( () => {
|
} ).catch( () => {
|
||||||
this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
|
this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
||||||
this.handleBackground();
|
this.handleBackground();
|
||||||
} );
|
} );
|
||||||
} else if ( data.type === 'queuePos' ) {
|
} else if ( data.type === 'queuePos' ) {
|
||||||
@@ -140,25 +149,142 @@ createApp( {
|
|||||||
},
|
},
|
||||||
handleBackground() {
|
handleBackground() {
|
||||||
// TODO: Add hotkeys
|
// TODO: Add hotkeys
|
||||||
// TODO: Check that colours are not too similar
|
let colourDetails = [];
|
||||||
let colours = {};
|
let colours = [];
|
||||||
|
let differentEnough = true;
|
||||||
if ( this.colourPalette[ 0 ] ) {
|
if ( this.colourPalette[ 0 ] ) {
|
||||||
for ( let i = 0; i < 3; i++ ) {
|
for ( let i in this.colourPalette ) {
|
||||||
colours[ i ] = 'rgb(' + this.colourPalette[ i ].r + ',' + this.colourPalette[ i ].g + ',' + this.colourPalette[ i ].b + ')';
|
for ( let colour in colourDetails ) {
|
||||||
|
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
|
||||||
|
+ Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
|
||||||
|
+ Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||||
|
if ( colourDiff > 15 ) {
|
||||||
|
differentEnough = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( differentEnough ) {
|
||||||
|
colourDetails.push( this.colourPalette[ i ] );
|
||||||
|
colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
|
||||||
|
}
|
||||||
|
differentEnough = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$( '#background' ).css( 'background', `conic-gradient( ${ colours[ 0 ] }, ${ colours[ 1 ] }, ${ colours[ 2 ] }, ${ colours[ 0 ] } )` );
|
let outColours = 'conic-gradient(';
|
||||||
if ( this.playingSong.bpm && this.isPlaying ) {
|
if ( colours.length < 3 ) {
|
||||||
$( '.beat' ).show();
|
for ( let i = 0; i < 3; i++ ) {
|
||||||
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
if ( colours[ i ] ) {
|
||||||
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
outColours += colours[ i ] + ',';
|
||||||
|
} else {
|
||||||
|
if ( i === 0 ) {
|
||||||
|
outColours += 'blue,';
|
||||||
|
} else if ( i === 1 ) {
|
||||||
|
outColours += 'green,';
|
||||||
|
} else if ( i === 2 ) {
|
||||||
|
outColours += 'red,';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( colours.length < 11 ) {
|
||||||
|
for ( let i in colours ) {
|
||||||
|
outColours += colours[ i ] + ',';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$( '.beat' ).hide();
|
for ( let i = 0; i < 10; i++ ) {
|
||||||
|
outColours += colours[ i ] + ',';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||||
|
|
||||||
|
$( '#background' ).css( 'background', outColours );
|
||||||
|
this.setVisualization();
|
||||||
|
},
|
||||||
|
setVisualization () {
|
||||||
|
if ( this.visualizationSettings === 'bpm' ) {
|
||||||
|
if ( this.playingSong.bpm && this.isPlaying ) {
|
||||||
|
$( '.beat' ).show();
|
||||||
|
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
||||||
|
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
||||||
|
} else {
|
||||||
|
$( '.beat' ).hide();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
clearInterval( this.micAnalyzer );
|
||||||
|
} catch ( err ) {}
|
||||||
|
} else if ( this.visualizationSettings === 'off' ) {
|
||||||
|
$( '.beat' ).hide();
|
||||||
|
try {
|
||||||
|
clearInterval( this.micAnalyzer );
|
||||||
|
} catch ( err ) {}
|
||||||
|
} else if ( this.visualizationSettings === 'mic' ) {
|
||||||
|
$( '.beat' ).hide();
|
||||||
|
try {
|
||||||
|
clearInterval( this.micAnalyzer );
|
||||||
|
} catch ( err ) {}
|
||||||
|
this.micAudioHandler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
micAudioHandler () {
|
||||||
|
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.fftSize = 256;
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array( bufferLength );
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
||||||
|
const mic = audioContext.createMediaStreamSource( stream );
|
||||||
|
mic.connect( analyser );
|
||||||
|
analyser.connect( audioContext.destination );
|
||||||
|
analyser.getByteFrequencyData( dataArray );
|
||||||
|
let prevSpectrum = null;
|
||||||
|
let threshold = 10; // Adjust as needed
|
||||||
|
this.beatDetected = false;
|
||||||
|
this.micAnalyzer = setInterval( () => {
|
||||||
|
analyser.getByteFrequencyData( dataArray );
|
||||||
|
// Convert the frequency data to a numeric array
|
||||||
|
const currentSpectrum = Array.from( dataArray );
|
||||||
|
|
||||||
|
if ( prevSpectrum ) {
|
||||||
|
// Calculate the spectral flux
|
||||||
|
const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
||||||
|
|
||||||
|
if ( flux > threshold && !this.beatDetected ) {
|
||||||
|
// Beat detected
|
||||||
|
this.beatDetected = true;
|
||||||
|
this.animateBeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevSpectrum = currentSpectrum;
|
||||||
|
}, 20 );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
animateBeat () {
|
||||||
|
$( '.beat-manual' ).stop();
|
||||||
|
const duration = Math.ceil( 60 / this.playingSong.bpm * 500 ) - 50;
|
||||||
|
$( '.beat-manual' ).fadeIn( 50 );
|
||||||
|
setTimeout( () => {
|
||||||
|
$( '.beat-manual' ).fadeOut( duration );
|
||||||
|
setTimeout( () => {
|
||||||
|
$( '.beat-manual' ).stop();
|
||||||
|
this.beatDetected = false;
|
||||||
|
}, duration );
|
||||||
|
}, 50 );
|
||||||
|
},
|
||||||
|
calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
|
||||||
|
let flux = 0;
|
||||||
|
|
||||||
|
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||||
|
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||||
|
flux += Math.max( 0, diff );
|
||||||
|
}
|
||||||
|
|
||||||
|
return flux;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connect();
|
this.connect();
|
||||||
|
if ( this.visualizationSettings === 'mic' ) {
|
||||||
|
this.micAudioHandler();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isPlaying( value ) {
|
isPlaying( value ) {
|
||||||
|
|||||||
Reference in New Issue
Block a user