Implement BPM visualizer

This commit is contained in:
danbulant 2020-09-18 21:50:21 +02:00
parent 5b1148733e
commit 8df5ffd202
17 changed files with 1541 additions and 70 deletions

4
.gitignore vendored
View file

@ -9,6 +9,10 @@ lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# build
public/build
public/build/*
# Runtime data
pids
*.pid

18
package-lock.json generated
View file

@ -3637,6 +3637,24 @@
"os-tmpdir": "^1.0.0"
}
},
"osu-buffer": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/osu-buffer/-/osu-buffer-1.3.5.tgz",
"integrity": "sha512-9I1JTXwOrkmurmvpUsnOc+StCb4w9nF1KLA52BIDAllXxIf6cw6/xlT+hdz8QIjG4D8DPWRcLyX/1MxNNo7XzA=="
},
"osu-db-parser": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/osu-db-parser/-/osu-db-parser-1.0.35.tgz",
"integrity": "sha512-oG4aSRyzXvVw4XLrghn1usbSxQhlTLJo08ZRbVTAp2+3xP0n1kyn4cG75aGWoOu6M0MU89QL/vhSgYBEyiIc1w==",
"requires": {
"osu-buffer": "^1.3.2"
}
},
"osu-parser": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/osu-parser/-/osu-parser-0.3.3.tgz",
"integrity": "sha1-b8V7qXPfD8pcVU6ligz3keHUUNg="
},
"p-cancelable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",

View file

@ -2,7 +2,7 @@
"name": "osu",
"productName": "osu",
"version": "1.0.0",
"description": "My Electron application description",
"description": "Osu! visualizer",
"main": "src/index.js",
"scripts": {
"start": "concurrently \"npm:svelte-dev\" \"electron-forge start\"",
@ -27,7 +27,8 @@
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "osu"
"name": "osu",
"authors": "Daniel Bulant"
}
},
{
@ -51,6 +52,8 @@
"concurrently": "^5.3.0",
"electron-reload": "^1.5.0",
"electron-squirrel-startup": "^1.0.0",
"osu-db-parser": "^1.0.35",
"osu-parser": "^0.3.3",
"sirv-cli": "^1.0.0"
},
"devDependencies": {

View file

@ -1,3 +1,5 @@
main.svelte-2x1evt{text-align:center;padding:1em;max-width:240px;margin:0 auto}h1.svelte-2x1evt{color:#ff3e00;text-transform:uppercase;font-size:4em;font-weight:100}@media(min-width: 640px){main.svelte-2x1evt{max-width:none}}
main.svelte-5atqf{position:relative;width:100vw;height:100vh}.background.svelte-5atqf{position:fixed;z-index:0;left:0;right:0;width:100vw;height:100vh}.menu.svelte-5atqf{position:absolute;z-index:1;left:0;right:0;width:100vw;height:100vh}
.info.svelte-1j9fr45.svelte-1j9fr45{opacity:1;position:relative;top:0;left:0;width:100vw;height:80px;transition:opacity 0.6s;z-index:1}.volume.svelte-1j9fr45.svelte-1j9fr45{opacity:1;position:fixed;z-index:2;right:0;bottom:0;border-radius:50%;color:black;font-size:30px}.hidden.svelte-1j9fr45.svelte-1j9fr45{opacity:0;transition:opacity 1s}.info.svelte-1j9fr45 .song.svelte-1j9fr45{color:white;position:absolute;padding:5px 5px 5px 25px;top:0;right:0;text-align:right;background:black;background:linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.5) 15%, rgba(0,0,0,0.5) 100%)}.info.svelte-1j9fr45 .song h2.svelte-1j9fr45{margin:0}.info.svelte-1j9fr45 .controls.svelte-1j9fr45{height:50px;display:flex}.info.svelte-1j9fr45 .controls div.svelte-1j9fr45{height:100%}.info.svelte-1j9fr45 .controls img.svelte-1j9fr45{height:100%;filter:invert(100%)}
.main.svelte-yh70k3.svelte-yh70k3{width:100%;height:100%;background-size:cover;background-repeat:no-repeat}@keyframes svelte-yh70k3-bpm{from{width:500px;height:500px;top:calc(50vh - 250px);left:calc(50vw - 250px)}to{width:525px;height:525px;top:calc(50vh - 262.5px);left:calc(50vw - 262.5px)}}@keyframes svelte-yh70k3-bpmShadow{0%{width:500px;height:500px;top:calc(50vh - 250px);left:calc(50vw - 250px)}70%{width:510px;height:510px;top:calc(50vh - 255px);left:calc(50vw - 255px)}100%{width:500px;height:500px;top:calc(50vh - 250px);left:calc(50vw - 250px)}}.main.svelte-yh70k3 img.svelte-yh70k3{position:fixed;width:500px;height:500px;top:calc(50vh - 250px);left:calc(50vw - 250px)}.main.svelte-yh70k3 .logo.svelte-yh70k3{animation-name:svelte-yh70k3-bpm;animation-iteration-count:infinite;animation-direction:alternate}.main.svelte-yh70k3 .shadow.svelte-yh70k3{opacity:0.2;animation-name:svelte-yh70k3-bpmShadow;animation-iteration-count:infinite;animation-delay:50ms}
/*# sourceMappingURL=bundle.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -7,7 +7,7 @@ html, body {
body {
color: #333;
margin: 0;
padding: 8px;
padding: 0px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}

41
public/images/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="blue_copy" style="enable-background:new 0 0 100 100;" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_8_copy_4_1_"><path d="M41.762,27.26v8.294c0,0.571,0.305,1.1,0.8,1.385l20.212,11.669c1.066,0.616,1.066,2.155,0,2.771L42.562,63.048 c-0.495,0.286-0.8,0.814-0.8,1.385v8.294c0,1.231,1.333,2.001,2.399,1.385l39.375-22.733c1.066-0.616,1.066-2.155,0-2.771 L44.161,25.875C43.095,25.259,41.762,26.029,41.762,27.26z"/><path d="M83.537,48.608L44.161,25.875c-0.193-0.112-0.395-0.164-0.597-0.19l37.972,21.923c1.066,0.616,1.066,2.155,0,2.771 L42.161,73.112c-0.1,0.058-0.205,0.092-0.308,0.126c0.308,0.914,1.401,1.398,2.308,0.874l39.375-22.733 C84.603,50.763,84.603,49.224,83.537,48.608z"/><path d="M41.762,27.26v8.294c0,0.571,0.305,1.1,0.8,1.385l20.212,11.669 c1.066,0.616,1.066,2.155,0,2.771L42.562,63.048c-0.495,0.286-0.8,0.814-0.8,1.385v8.294c0,1.231,1.333,2.001,2.399,1.385 l39.375-22.733c1.066-0.616,1.066-2.155,0-2.771L44.161,25.875C43.095,25.259,41.762,26.029,41.762,27.26z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/><path d="M14.664,27.273v8.294c0,0.571,0.305,1.1,0.8,1.385l20.212,11.669c1.066,0.616,1.066,2.155,0,2.771L15.464,63.061 c-0.495,0.286-0.8,0.814-0.8,1.385v8.294c0,1.231,1.333,2.001,2.399,1.385l39.375-22.733c1.066-0.616,1.066-2.155,0-2.771 L17.063,25.888C15.997,25.272,14.664,26.042,14.664,27.273z"/><path d="M56.438,48.621L17.063,25.888c-0.193-0.112-0.395-0.164-0.597-0.19l37.972,21.923c1.066,0.616,1.066,2.155,0,2.771 L15.063,73.125c-0.1,0.058-0.205,0.092-0.308,0.126c0.308,0.914,1.401,1.398,2.308,0.874l39.375-22.733 C57.505,50.776,57.505,49.237,56.438,48.621z"/><path d="M14.664,27.273v8.294c0,0.571,0.305,1.1,0.8,1.385l20.212,11.669 c1.066,0.616,1.066,2.155,0,2.771L15.464,63.061c-0.495,0.286-0.8,0.814-0.8,1.385v8.294c0,1.231,1.333,2.001,2.399,1.385 l39.375-22.733c1.066-0.616,1.066-2.155,0-2.771L17.063,25.888C15.997,25.272,14.664,26.042,14.664,27.273z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="blue_copy" style="enable-background:new 0 0 100 100;" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_7_copy"><path d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z"/><path d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z"/><path d="M40.489,27.248c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.119 C43.746,29.177,42.338,27.573,40.489,27.248z"/><path d="M68.792,27.211c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.082 C72.049,29.14,70.641,27.535,68.792,27.211z"/><path d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/><path d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="blue_copy" style="enable-background:new 0 0 100 100;" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_4_copy"><path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7 C27.852,25.902,29.798,24.778,31.356,25.677z"/><path d="M69.981,47.977l-38.625-22.3c-0.233-0.134-0.474-0.21-0.716-0.259l37.341,21.559c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-0.349,0.201-0.716,0.288-1.078,0.301c0.656,0.938,1.961,1.343,3.078,0.699l38.625-22.3 C71.538,51.124,71.538,48.876,69.981,47.977z"/><path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7C27.852,25.902,29.798,24.778,31.356,25.677z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/></g></svg>

After

Width:  |  Height:  |  Size: 916 B

View file

@ -4,7 +4,7 @@
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<title>Osu visualizer</title>
<link rel='icon' type='image/png' href='./favicon.png'>
<link rel='stylesheet' href='./global.css'>

View file

@ -1,28 +1,41 @@
<script>
let name = "world";
import Menu from "./Menu.svelte";
import Visualizer from "./Visualizer.svelte";
var songData = {};
var osuData = {};
</script>
<main>
<h1>Hello {name}!</h1>
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
<div class="background">
<Visualizer bind:songData bind:osuData/>
</div>
<div class="menu">
<Menu bind:song={songData} bind:osuData/>
</div>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
main {
position: relative;
width: 100vw;
height: 100vh;
}
.background {
position: fixed;
z-index: 0;
left: 0;
right: 0;
width: 100vw;
height: 100vh;
}
.menu {
position: absolute;
z-index: 1;
left: 0;
right: 0;
width: 100vw;
height: 100vh;
}
</style>

179
src/Menu.svelte Normal file
View file

@ -0,0 +1,179 @@
<script>
export var osuData;
export var song;
var last = Date.now();
var lastVolumeUpdate = Date.now() - 5000;
var dialogActive = false;
var now = Date.now();
setInterval(() => {
now = Date.now();
}, 500);
var playing = true;
function resetPool() {
if(!osuData.songs) return false;
osuData.songPool = osuData.songs.filter(v => true);
let a = osuData.songPool;
for (let i = a.length - 1; i > 0; i--) { // shuffle
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
song = osuData.songPool.shift();
}
function playNext() {
if(song.audio) {
song.audio.pause();
}
if(osuData.songPool.length) {
song = osuData.songPool.shift();
} else {
resetPool();
}
}
$: resetPool(osuData.songs);
$: console.log(song);
$: {
if(song && song.folder && !song.audio) {
song.audio = new Audio(process.env.USERPROFILE + "/AppData/Local/osu!/Songs/" + song.folder + "/" + song.audioFile);
song.audio.play();
song.audio.onended = () => {
playNext();
}
song.audio.onpause = () => {
playing = false;
}
song.audio.onplay = () => {
playing = true;
}
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: song.song,
artist: song.artist,
album: "Osu! visualizer",
artwork: [
{ src: process.env.USERPROFILE + "/AppData/Local/osu!/Data/bt/" + song.id + ".jpg", type: 'image/jpeg' },
]
});
navigator.mediaSession.setActionHandler('play', function() { playing = true; song.audio.play(); });
navigator.mediaSession.setActionHandler('pause', function() { playing = false; song.audio.pause(); });
navigator.mediaSession.setActionHandler('nexttrack', function() { playNext()});
}
}
}
function togglePlay() {
playing = !playing;
if(playing) {
song.audio.play();
} else {
song.audio.pause();
}
}
function updateVolume(e) {
if(!song || !song.audio || !e.altKey) return;
lastVolumeUpdate = Date.now();
var volume = song.audio.volume;
volume += e.deltaY * -0.0005;
song.audio.volume = Math.min(1, Math.max(volume, 0));
}
setTimeout(() => {
song = song;
}, 200);
</script>
<svelte:window on:mousemove={() => last = Date.now()} on:wheel={e => updateVolume(e)} />
<div class="menu">
<div class="info" class:hidden={now - last > 2000}>
{#if song}
<div class="song">
<h2>{song.artist} - {song.song}</h2>
<div class="controls">
<div class="play" on:click={togglePlay}>
<img src="images/music_{playing ? "pause" : "play"}.svg" alt="{playing ? "Pause" : "Play"} music" title="{playing ? "Pause" : "Play"} music">
</div>
<div class="forward" on:click={playNext}>
<img src="images/music_forward.svg" alt="Skip the song" title="Skip the song">
</div>
</div>
</div>
{/if}
</div>
{#if now - lastVolumeUpdate < 4000 && song && song.audio}
<div class="volume" class:hidden={now - lastVolumeUpdate > 2000}>
<div class="slider">
<div class="percent">
{Math.round(song.audio.volume * 100)}%
</div>
</div>
</div>
{/if}
</div>
<style>
.info {
opacity: 1;
position: relative;
top: 0;
left: 0;
width: 100vw;
height: 80px;
transition: opacity 0.6s;
z-index: 1;
}
.volume {
opacity: 1;
position: fixed;
z-index: 2;
right: 0;
bottom: 0;
border-radius: 50%;
color: black;
font-size: 30px;
}
.hidden {
opacity: 0;
transition: opacity 1s;
}
.info .song {
color: white;
position: absolute;
padding: 5px 5px 5px 25px;
top: 0;
right: 0;
text-align: right;
background: black;
background: linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.5) 15%, rgba(0,0,0,0.5) 100%);
}
.info .song h2 {
margin: 0;
}
.info .controls {
height: 50px;
display: flex;
}
.info .controls div {
height: 100%;
}
.info .controls img {
height: 100%;
filter: invert(100%);
}
</style>

177
src/Visualizer.svelte Normal file
View file

@ -0,0 +1,177 @@
<script>
import { createEventDispatcher } from 'svelte';
const fs = require("fs");
const OsuDBParser = require("osu-db-parser");
const osuParser = require("osu-parser");
export var osuData;
export var songData;
var wallpapers = [];
try {
wallpapers = fs.readdirSync(process.env.USERPROFILE + "/AppData/Local/osu!/Data/bg");
} catch(e) {
console.error("Osu backgrounds weren't found. You must have osu installed and started at least once.", e);
alert("Osu backgrounds not found!");
}
try {
osuData = (new OsuDBParser(Buffer.from(fs.readFileSync(process.env.USERPROFILE + "/AppData/Local/osu!/osu!.db")))).getOsuDBData();
console.log(osuData);
osuData.songs = osuData.beatmaps.map(v => ({
artist: v.artist_name,
artist_u: v.artist_name_unicode,
audioFile: v.audio_file_name,
folder: v.folder_name,
song: v.song_title,
song_u: v.song_title_unicode,
id: v.beatmapset_id,
dataFile: `${v.artist_name} - ${v.song_title} (${v.creator_name}) [${v.difficulty}].osu`
})).filter((v, i, a) => a.findIndex(x => x.id === v.id) === i);
} catch(e) {
console.error("Osu DB weren't found. You must have osu installed and started at least once.", e);
alert("Osu DB not found!");
}
var wallpaper;
function shuffleWallpapers() {
wallpaper = wallpapers[Math.floor(Math.random() * wallpapers.length)];
}
var lastSong = null;
$: {
if(songData !== lastSong) {
lastSong = songData;
shuffleWallpapers();
}
}
function fetchBeatmap() {
let file = fs.readFileSync(process.env.USERPROFILE + "/AppData/Local/osu!/Songs/" + songData.folder + "/" + songData.dataFile);
songData.beatmap = osuParser.parseContent(file);
}
$: if(songData && songData.dataFile && !songData.beatmap) fetchBeatmap();
var mouse = {
x: 0.5,
y: 0.5
};
const parallaxTreshold = 10;
function updateMouse(e) {
mouse = {
x: -(e.clientX / window.innerWidth) * parallaxTreshold - parallaxTreshold/2,
y: -(e.clientY / window.innerHeight) * parallaxTreshold - parallaxTreshold/2
}
}
var isWidthSmaller = false;
function resize() {
isWidthSmaller = window.innerWidth * 9 < window.innerHeight * 16;
}
resize();
var animDuration = 0;
var kiaiTime = false;
setInterval(() => {
if(!songData) return;
if(!songData.beatmap && songData.dataFile) fetchBeatmap();
if(!songData.beatmap) return;
var tp = null;
for(var t of songData.beatmap.timingPoints) {
if(t.offset > songData.audio.currentTime * 1000) break;
tp = t;
}
if(!tp) {
animDuration = 0;
kiaiTime = false;
return;
}
if(tp.beatLength/2 !== animDuration)
animDuration = tp.beatLength/2;
kiaiTime = tp.kiaiTimeActive;
}, 50);
</script>
<svelte:window on:mousemove={updateMouse} on:resize={resize} />
<div
class="main"
style="
background-image: url('{process.env.USERPROFILE.replace(/\\/g, "/")}/AppData/Local/osu!/Data/bg/{wallpaper}');
background-size: {!isWidthSmaller ? `calc(100% + ${parallaxTreshold * 1.5}px) auto` : `auto calc(100% + ${parallaxTreshold * 1.5}px)`};
background-position: {mouse.x}px {mouse.y}px;
"
>
<img src="images/logo.svg" alt="logo" class="logo" style="animation-duration: {animDuration}ms;">
<img src="images/logo.svg" alt="" class="shadow" style="animation-duration: {animDuration * 2}ms;">
</div>
<style>
.main {
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
}
@keyframes bpm {
from {
width: 500px;
height: 500px;
top: calc(50vh - 250px);
left: calc(50vw - 250px);
}
to {
width: 525px;
height: 525px;
top: calc(50vh - 262.5px);
left: calc(50vw - 262.5px);
}
}
@keyframes bpmShadow {
0% {
width: 500px;
height: 500px;
top: calc(50vh - 250px);
left: calc(50vw - 250px);
}
70% {
width: 510px;
height: 510px;
top: calc(50vh - 255px);
left: calc(50vw - 255px);
}
100% {
width: 500px;
height: 500px;
top: calc(50vh - 250px);
left: calc(50vw - 250px);
}
}
.main img {
position: fixed;
width: 500px;
height: 500px;
top: calc(50vh - 250px);
left: calc(50vw - 250px);
}
.main .logo {
animation-name: bpm;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.main .shadow {
opacity: 0.2;
animation-name: bpmShadow;
animation-iteration-count: infinite;
animation-delay: 50ms;
}
</style>

View file

@ -6,12 +6,19 @@ if (require('electron-squirrel-startup')) { // eslint-disable-line global-requir
app.quit();
}
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
},
autoHideMenuBar: true
});
mainWindow.setMenu(null);
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, '../public/index.html'));