Compare commits

..

5 commits

Author SHA1 Message Date
Daniel Bulant
f50a33af78
add osu folder, update 2024-06-25 12:01:57 +02:00
danbulant
decc493769 Fix missing repository 2020-09-19 19:33:55 +02:00
danbulant
3770c6e7af Fix github publisher version 2020-09-19 18:55:55 +02:00
danbulant
98c9cff4b5 Settings and video backgrounds, bug fixes 2020-09-19 18:33:29 +02:00
danbulant
86a45a90ae Install github publisher 2020-09-19 11:00:16 +02:00
15 changed files with 2511 additions and 5690 deletions

5
.gitignore vendored
View file

@ -9,9 +9,14 @@ lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pnpm-lock.yaml
package-lock.json
.envrc
# build
public/build
public/build/*
token.txt
# Runtime data
pids

5191
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,10 @@
{
"name": "osu",
"productName": "osu",
"version": "1.0.0",
"version": "0.2.0",
"description": "Osu! visualizer",
"main": "src/index.js",
"repository": "github:danbulant/osuVisualizer",
"scripts": {
"start": "concurrently \"npm:svelte-dev\" \"electron-forge start\"",
"package": "electron-forge package",
@ -48,27 +49,30 @@
},
"dependencies": {
"concurrently": "^5.3.0",
"discord-rpc": "^3.1.4",
"electron-is-dev": "^1.2.0",
"electron-reload": "^1.5.0",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^6.0.0",
"osu-db-parser": "^1.0.35",
"osu-parser": "^0.3.3",
"sirv-cli": "^1.0.0",
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^14.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"rollup": "^2.3.4",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^6.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0",
"@electron-forge/cli": "^6.0.0-beta.53",
"@electron-forge/maker-deb": "^6.0.0-beta.53",
"@electron-forge/maker-rpm": "^6.0.0-beta.53",
"@electron-forge/maker-squirrel": "^6.0.0-beta.53",
"@electron-forge/maker-zip": "^6.0.0-beta.53",
"electron": "10.1.2"
"@electron-forge/publisher-github": "^6.0.0-beta.52",
"@rollup/plugin-commonjs": "^14.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"electron": "10.1.2",
"rollup": "^2.3.4",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^6.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0"
}
}

View file

@ -1,5 +1,6 @@
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}
.info.svelte-1qt5obi.svelte-1qt5obi{opacity:1;position:relative;top:0;left:0;width:100vw;height:80px;transition:opacity 0.6s;z-index:2}.volume.svelte-1qt5obi.svelte-1qt5obi{opacity:1;position:fixed;z-index:5;right:0;bottom:0;border-radius:50%;font-size:30px;color:white;background-color:black;width:100px;height:100px}.volume.svelte-1qt5obi .slider.svelte-1qt5obi{position:relative;top:0;left:0;width:100%;height:100%}.percent.svelte-1qt5obi.svelte-1qt5obi{position:absolute;top:25px;left:0;width:100%;height:100%;text-align:center}.progress-ring.svelte-1qt5obi.svelte-1qt5obi{position:absolute;top:0;left:0;width:100%;height:100%}.progress-ring.svelte-1qt5obi circle.svelte-1qt5obi{transition:stroke-dashoffset 0.32s;transform:rotate(-90deg);transform-origin:50% 50%;position:absolute;top:1px;left:1px;width:100%;height:100%}.hidden.svelte-1qt5obi.svelte-1qt5obi{opacity:0;transition:opacity 1s}.info.svelte-1qt5obi .song.svelte-1qt5obi{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-1qt5obi .song h2.svelte-1qt5obi{margin:0}.info.svelte-1qt5obi .controls.svelte-1qt5obi{height:50px;display:flex}.info.svelte-1qt5obi .controls div.svelte-1qt5obi{height:100%}.info.svelte-1qt5obi .controls img.svelte-1qt5obi{height:100%;filter:invert(100%)}.info.svelte-1qt5obi .controls .settings img.svelte-1qt5obi{height:65%;padding-top:25%}
.main.svelte-18bmol8.svelte-18bmol8{width:100%;height:100%;background-size:cover;background-repeat:no-repeat}@keyframes svelte-18bmol8-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-18bmol8-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)}}video.svelte-18bmol8.svelte-18bmol8{position:fixed;z-index:0;top:0;left:0;width:100vw;height:100vh}.main.svelte-18bmol8 img.svelte-18bmol8{position:fixed;width:500px;height:500px;top:calc(50vh - 250px);left:calc(50vw - 250px);z-index:1}.main.svelte-18bmol8 .logo.svelte-18bmol8{animation-name:svelte-18bmol8-bpm;animation-direction:alternate}.main.svelte-18bmol8 .shadow.svelte-18bmol8{opacity:0.2;animation-name:svelte-18bmol8-bpmShadow;animation-delay:50ms}.main.svelte-18bmol8 .repeat.svelte-18bmol8{animation-iteration-count:infinite}
.bg.svelte-1h7gt2z{position:fixed;display:none;width:100vw;height:100vh;top:0;left:0;z-index:3}.bg.visible.svelte-1h7gt2z{display:block}nav.svelte-1h7gt2z{position:fixed;height:100vh;width:400px;top:0;left:-400px;opacity:0;background:rgba(0,0,0,0.4);color:white;z-index:4;padding-left:1rem;transition:opacity 0.3s, left 0.3s}nav.visible.svelte-1h7gt2z{left:0;opacity:1}
/*# 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

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="25px" height="25.001px" viewBox="0 0 25 25.001" style="enable-background:new 0 0 25 25.001;" xml:space="preserve">
<g>
<path d="M24.38,10.175l-2.231-0.268c-0.228-0.851-0.562-1.655-0.992-2.401l1.387-1.763c0.212-0.271,0.188-0.69-0.057-0.934
l-2.299-2.3c-0.242-0.243-0.662-0.269-0.934-0.057l-1.766,1.389c-0.743-0.43-1.547-0.764-2.396-0.99L14.825,0.62
C14.784,0.279,14.469,0,14.125,0h-3.252c-0.344,0-0.659,0.279-0.699,0.62L9.906,2.851c-0.85,0.227-1.655,0.562-2.398,0.991
L5.743,2.455c-0.27-0.212-0.69-0.187-0.933,0.056L2.51,4.812C2.268,5.054,2.243,5.474,2.456,5.746L3.842,7.51
c-0.43,0.744-0.764,1.549-0.991,2.4l-2.23,0.267C0.28,10.217,0,10.532,0,10.877v3.252c0,0.344,0.279,0.657,0.621,0.699l2.231,0.268
c0.228,0.848,0.561,1.652,0.991,2.396l-1.386,1.766c-0.211,0.271-0.187,0.69,0.057,0.934l2.296,2.301
c0.243,0.242,0.663,0.269,0.933,0.057l1.766-1.39c0.744,0.43,1.548,0.765,2.398,0.991l0.268,2.23
c0.041,0.342,0.355,0.62,0.699,0.62h3.252c0.345,0,0.659-0.278,0.699-0.62l0.268-2.23c0.851-0.228,1.655-0.562,2.398-0.991
l1.766,1.387c0.271,0.212,0.69,0.187,0.933-0.056l2.299-2.301c0.244-0.242,0.269-0.662,0.056-0.935l-1.388-1.764
c0.431-0.744,0.764-1.548,0.992-2.397l2.23-0.268C24.721,14.785,25,14.473,25,14.127v-3.252
C25.001,10.529,24.723,10.216,24.38,10.175z M12.501,18.75c-3.452,0-6.25-2.798-6.25-6.25s2.798-6.25,6.25-6.25
s6.25,2.798,6.25,6.25S15.954,18.75,12.501,18.75z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

515
public/lib/osu-parser.js Normal file
View file

@ -0,0 +1,515 @@
var fs = require('fs');
var slidercalc = require('osu-parser/lib/slidercalc.js');
function beatmapParser() {
var beatmap = {
nbCircles: 0,
nbSliders: 0,
nbSpinners: 0,
timingPoints: [],
breakTimes: [],
hitObjects: []
};
var osuSection;
var bpmMin;
var bpmMax;
var members;
var timingLines = [];
var objectLines = [];
var eventsLines = [];
var sectionReg = /^\[([a-zA-Z0-9]+)\]$/;
var keyValReg = /^([a-zA-Z0-9]+)[ ]*:[ ]*(.+)$/;
var curveTypes = {
C: "catmull",
B: "bezier",
L: "linear",
P: "pass-through"
};
/**
* Get the timing point affecting a specific offset
* @param {Integer} offset
* @return {Object} timingPoint
*/
var getTimingPoint = function (offset) {
for (var i = beatmap.timingPoints.length - 1; i >= 0; i--) {
if (beatmap.timingPoints[i].offset <= offset) { return beatmap.timingPoints[i]; }
}
return beatmap.timingPoints[0];
};
/**
* Parse additions member
* @param {String} str additions member (sample:add:customSampleIndex:Volume:hitsound)
* @return {Object} additions a list of additions
*/
var parseAdditions = function (str) {
if (!str) return {};
var additions = {};
var adds = str.split(':');
if (adds[0] && adds[0] !== '0') {
var sample;
switch (adds[0]) {
case '1':
sample = 'normal';
break;
case '2':
sample = 'soft';
break;
case '3':
sample = 'drum';
break;
}
additions.sample = sample;
}
if (adds[1] && adds[1] !== '0') {
var addSample;
switch (adds[1]) {
case '1':
addSample = 'normal';
break;
case '2':
addSample = 'soft';
break;
case '3':
addSample = 'drum';
break;
}
additions.additionalSample = addSample;
}
if (adds[2] && adds[2] !== '0') { additions.customSampleIndex = parseInt(adds[2]); }
if (adds[3] && adds[3] !== '0') { additions.hitsoundVolume = parseInt(adds[3]); }
if (adds[4]) { additions.hitsound = adds[4]; }
return additions;
};
/**
* Parse a timing line
* @param {String} line
*/
var parseTimingPoint = function (line) {
members = line.split(',');
var timingPoint = {
offset: parseInt(members[0]),
beatLength: parseFloat(members[1]),
velocity: 1,
timingSignature: parseInt(members[2]),
sampleSetId: parseInt(members[3]),
customSampleIndex: parseInt(members[4]),
sampleVolume: parseInt(members[5]),
timingChange: (members[6] == 1),
kiaiTimeActive: (members[7] == 1)
};
if (!isNaN(timingPoint.beatLength) && timingPoint.beatLength !== 0) {
if (timingPoint.beatLength > 0) {
// If positive, beatLength is the length of a beat in milliseconds
var bpm = Math.round(60000 / timingPoint.beatLength);
beatmap.bpmMin = beatmap.bpmMin ? Math.min(beatmap.bpmMin, bpm) : bpm;
beatmap.bpmMax = beatmap.bpmMax ? Math.max(beatmap.bpmMax, bpm) : bpm;
timingPoint.bpm = bpm;
} else {
// If negative, beatLength is a velocity factor
timingPoint.velocity = Math.abs(100 / timingPoint.beatLength);
}
}
beatmap.timingPoints.push(timingPoint);
};
/**
* Parse an object line
* @param {String} line
*/
var parseHitObject = function (line) {
members = line.split(',');
var soundType = members[4];
var objectType = members[3];
var hitObject = {
startTime: parseInt(members[2]),
newCombo: ((objectType & 4) == 4),
soundTypes: [],
position: [
parseInt(members[0]),
parseInt(members[1])
]
};
/**
* sound type is a bitwise flag enum
* 0 : normal
* 2 : whistle
* 4 : finish
* 8 : clap
*/
if ((soundType & 2) == 2) { hitObject.soundTypes.push('whistle'); }
if ((soundType & 4) == 4) { hitObject.soundTypes.push('finish'); }
if ((soundType & 8) == 8) { hitObject.soundTypes.push('clap'); }
if (hitObject.soundTypes.length === 0) { hitObject.soundTypes.push('normal'); }
/**
* object type is a bitwise flag enum
* 1: circle
* 2: slider
* 8: spinner
*/
if ((objectType & 1) == 1) {
// Circle
beatmap.nbCircles++;
hitObject.objectName = 'circle';
hitObject.additions = parseAdditions(members[5]);
} else if ((objectType & 8) == 8) {
// Spinner
beatmap.nbSpinners++;
hitObject.objectName = 'spinner';
hitObject.endTime = parseInt(members[5]);
hitObject.additions = parseAdditions(members[6]);
} else if ((objectType & 2) == 2) {
// Slider
beatmap.nbSliders++;
hitObject.objectName = 'slider';
hitObject.repeatCount = parseInt(members[6]);
hitObject.pixelLength = parseInt(members[7]);
hitObject.additions = parseAdditions(members[10]);
hitObject.edges = [];
hitObject.points = [
[hitObject.position[0], hitObject.position[1]]
];
/**
* Calculate slider duration
*/
var timing = getTimingPoint(hitObject.startTime);
if (timing) {
var pxPerBeat = beatmap.SliderMultiplier * 100 * timing.velocity;
var beatsNumber = (hitObject.pixelLength * hitObject.repeatCount) / pxPerBeat;
hitObject.duration = Math.ceil(beatsNumber * timing.beatLength);
hitObject.endTime = hitObject.startTime + hitObject.duration;
}
/**
* Parse slider points
*/
var points = (members[5] || '').split('|');
if (points.length) {
hitObject.curveType = curveTypes[points[0]] || 'unknown';
for (var i = 1, l = points.length; i < l; i++) {
var coordinates = points[i].split(':');
hitObject.points.push([
parseInt(coordinates[0]),
parseInt(coordinates[1])
]);
}
}
var edgeSounds = [];
var edgeAdditions = [];
if (members[8]) { edgeSounds = members[8].split('|'); }
if (members[9]) { edgeAdditions = members[9].split('|'); }
/**
* Get soundTypes and additions for each slider edge
*/
for (var j = 0, lgt = hitObject.repeatCount + 1; j < lgt; j++) {
var edge = {
soundTypes: [],
additions: parseAdditions(edgeAdditions[j])
};
if (edgeSounds[j]) {
var sound = edgeSounds[j];
if ((sound & 2) == 2) { edge.soundTypes.push('whistle'); }
if ((sound & 4) == 4) { edge.soundTypes.push('finish'); }
if ((sound & 8) == 8) { edge.soundTypes.push('clap'); }
if (edge.soundTypes.length === 0) { edge.soundTypes.push('normal'); }
} else {
edge.soundTypes.push('normal');
}
hitObject.edges.push(edge);
}
// get coordinates of the slider endpoint
var endPoint = slidercalc.getEndPoint(hitObject.curveType, hitObject.pixelLength, hitObject.points);
if (endPoint && endPoint[0] && endPoint[1]) {
hitObject.endPosition = [
Math.round(endPoint[0]),
Math.round(endPoint[1])
];
} else {
// If endPosition could not be calculated, approximate it by setting it to the last point
hitObject.endPosition = hitObject.points[hitObject.points.length - 1];
}
} else {
// Unknown
hitObject.objectName = 'unknown';
}
beatmap.hitObjects.push(hitObject);
};
/**
* Parse an event line
* @param {String} line
*/
var parseEvent = function (line) {
/**
* Background line : 0,0,"bg.jpg"
* TODO: confirm that the second member is always zero
*
* Breaktimes lines : 2,1000,2000
* second integer is start offset
* third integer is end offset
*/
members = line.split(',');
if (members[0] == '0' && members[1] == '0' && members[2]) {
var bgName = members[2].trim();
if (bgName.charAt(0) == '"' && bgName.charAt(bgName.length - 1) == '"') {
beatmap.bgFilename = bgName.substring(1, bgName.length - 1);
} else {
beatmap.bgFilename = bgName;
}
} else if (members[0] == 'Video' && members[2]) {
var bgName = members[2].trim();
if (bgName.charAt(0) == '"' && bgName.charAt(bgName.length - 1) == '"') {
beatmap.video = bgName.substring(1, bgName.length - 1);
} else {
beatmap.video = bgName;
}
} else if (members[0] == '2' && /^[0-9]+$/.test(members[1]) && /^[0-9]+$/.test(members[2])) {
beatmap.breakTimes.push({
startTime: parseInt(members[1]),
endTime: parseInt(members[2])
});
}
};
/**
* Compute the total time and the draining time of the beatmap
*/
var computeDuration = function () {
var firstObject = beatmap.hitObjects[0];
var lastObject = beatmap.hitObjects[beatmap.hitObjects.length - 1];
var totalBreakTime = 0;
beatmap.breakTimes.forEach(function (breakTime) {
totalBreakTime += (breakTime.endTime - breakTime.startTime);
});
if (firstObject && lastObject) {
beatmap.totalTime = Math.floor(lastObject.startTime / 1000);
beatmap.drainingTime = Math.floor((lastObject.startTime - firstObject.startTime - totalBreakTime) / 1000);
} else {
beatmap.totalTime = 0;
beatmap.drainingTime = 0;
}
};
/**
* Browse objects and compute max combo
*/
var computeMaxCombo = function () {
if (beatmap.timingPoints.length === 0) { return; }
var maxCombo = 0;
var sliderMultiplier = parseFloat(beatmap.SliderMultiplier);
var sliderTickRate = parseInt(beatmap.SliderTickRate, 10);
var timingPoints = beatmap.timingPoints;
var currentTiming = timingPoints[0];
var nextOffset = timingPoints[1] ? timingPoints[1].offset : Infinity;
var i = 1;
beatmap.hitObjects.forEach(function (hitObject) {
if (hitObject.startTime >= nextOffset) {
currentTiming = timingPoints[i++];
nextOffset = timingPoints[i] ? timingPoints[i].offset : Infinity;
}
var osupxPerBeat = sliderMultiplier * 100 * currentTiming.velocity;
var tickLength = osupxPerBeat / sliderTickRate;
switch (hitObject.objectName) {
case 'spinner':
case 'circle':
maxCombo++;
break;
case 'slider':
var tickPerSide = Math.ceil((Math.floor(hitObject.pixelLength / tickLength * 100) / 100) - 1);
maxCombo += (hitObject.edges.length - 1) * (tickPerSide + 1) + 1; // 1 combo for each tick and endpoint
}
});
beatmap.maxCombo = maxCombo;
};
/**
* Read a single line, parse when key/value, store when further parsing needed
* @param {String|Buffer} line
*/
var readLine = function (line) {
line = line.toString().trim();
if (!line) { return; }
var match = sectionReg.exec(line);
if (match) {
osuSection = match[1].toLowerCase();
return;
}
if(line.startsWith("//")) return;
switch (osuSection) {
case 'timingpoints':
timingLines.push(line);
break;
case 'hitobjects':
objectLines.push(line);
break;
case 'events':
eventsLines.push(line);
break;
default:
if (!osuSection) {
match = /^osu file format (v[0-9]+)$/.exec(line);
if (match) {
beatmap.fileFormat = match[1];
return;
}
}
/**
* Apart from events, timingpoints and hitobjects sections, lines are "key: value"
*/
match = keyValReg.exec(line);
if (match) { beatmap[match[1]] = match[2]; }
}
};
/**
* Compute everything that require the file to be completely parsed and return the beatmap
* @return {Object} beatmap
*/
var buildBeatmap = function () {
if (beatmap.Tags) {
beatmap.tagsArray = beatmap.Tags.split(' ');
}
eventsLines.forEach(parseEvent);
beatmap.breakTimes.sort(function (a, b) { return (a.startTime > b.startTime ? 1 : -1); });
timingLines.forEach(parseTimingPoint);
beatmap.timingPoints.sort(function (a, b) { return (a.offset > b.offset ? 1 : -1); });
var timingPoints = beatmap.timingPoints;
for (var i = 1, l = timingPoints.length; i < l; i++) {
if (!timingPoints[i].hasOwnProperty('bpm')) {
timingPoints[i].beatLength = timingPoints[i - 1].beatLength;
timingPoints[i].bpm = timingPoints[i - 1].bpm;
}
}
objectLines.forEach(parseHitObject);
beatmap.hitObjects.sort(function (a, b) { return (a.startTime > b.startTime ? 1 : -1); });
computeMaxCombo();
computeDuration();
return beatmap;
};
return {
readLine: readLine,
buildBeatmap: buildBeatmap
};
}
/**
* Parse a .osu file
* @param {String} file path to the file
* @param {Function} callback(err, beatmap)
*/
exports.parseFile = function (file, callback) {
if (!fs.existsSync(file)) {
callback(new Error('File does not exist'));
return;
}
var parser = beatmapParser();
var stream = fs.createReadStream(file);
var buffer = '';
stream.on('data', function (chunk) {
buffer += chunk;
var lines = buffer.split(/\r?\n/);
buffer = lines.pop() || '';
lines.forEach(parser.readLine);
});
stream.on('error', function (err) {
callback(err);
});
stream.on('end', function () {
buffer.split(/\r?\n/).forEach(parser.readLine);
callback(null, parser.buildBeatmap());
});
};
/**
* Parse a stream containing .osu content
* @param {Stream} stream
* @param {Function} callback(err, beatmap)
*/
exports.parseStream = function (stream, callback) {
var parser = beatmapParser();
var buffer = '';
stream.on('data', function (chunk) {
buffer += chunk.toString();
var lines = buffer.split(/\r?\n/);
buffer = lines.pop() || '';
lines.forEach(parser.readLine);
});
stream.on('error', function (err) {
callback(err);
});
stream.on('end', function () {
buffer.split(/\r?\n/).forEach(parser.readLine);
callback(null, parser.buildBeatmap());
});
};
/**
* Parse the content of a .osu
* @param {String|Buffer} content
* @return {Object} beatmap
*/
exports.parseContent = function (content) {
var parser = beatmapParser();
content.toString().split(/[\n\r]+/).forEach(function (line) {
parser.readLine(line);
});
return parser.buildBeatmap();
};

View file

@ -1,18 +1,60 @@
<script>
import Menu from "./Menu.svelte";
import Visualizer from "./Visualizer.svelte";
const Store = require('electron-store');
const store = new Store();
var songData = {};
var config = store.get("config");
var osuData = {};
const osuFolder = process.env.OSU_FOLDER || process.env.USERPROFILE + "/AppData/Local/osu!";
(() => {
const configTemplate = {
parallax: {
enabled: true,
treshold: 10
},
rpc: true,
backgrounds: 0,
mediaSession: true,
videoBackground: true,
autohide: {
info: 2000,
volume: 2000
}
};
function checkSettings(value, template) {
if(value === undefined) return template;
if(typeof value !== "object") return value;
var out = {};
for(var key in template) {
if(value[key] === undefined || typeof value[key] === "undefined") {
out[key] = template[key];
continue;
}
if(typeof value[key] === "object") out[key] = checkSettings(value[key], template[key]);
if(typeof value[key] !== "object") out[key] = value[key];
}
return out;
}
config = checkSettings(config, configTemplate);
})();
$: store.set("config", config);
</script>
<main>
<div class="background">
<Visualizer bind:songData bind:osuData/>
<Visualizer bind:songData bind:osuData {config} {osuFolder} />
</div>
<div class="menu">
<Menu bind:song={songData} bind:osuData/>
<Menu bind:song={songData} bind:osuData bind:config {osuFolder} />
</div>
</main>

View file

@ -1,17 +1,19 @@
<script>
import Options from "./components/options.svelte";
export var osuData;
export var song;
export var config;
export var osuFolder;
var last = Date.now();
var lastVolumeUpdate = Date.now() - 5000;
var dialogActive = false;
var settingsOpen = false;
var now = Date.now();
setInterval(() => {
now = Date.now();
}, 500);
var playing = true;
}, 800);
function resetPool() {
if(!osuData.songs) return false;
@ -21,6 +23,11 @@
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
osuData.songPool.forEach(v => {
delete v.audio;
delete v.video;
v.playing = true;
})
song = osuData.songPool.shift();
}
@ -40,85 +47,143 @@
$: 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();
(() => {
if(song && song.folder && !song.audio) {
// var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// song.context = audioCtx;
// song.analyser = audioCtx.createAnalyser();
song.audio = new Audio(`${osuFolder}/Songs/${song.folder}/${song.audioFile}`);
// song.source = audioCtx.createMediaElementSource(song.audio);
// song.source.connect(song.analyser);
// song.analyser.connect(audioCtx.destination);
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' },
]
});
song.audio.onended = () => {
playNext();
}
song.audio.onpause = () => {
song.playing = false;
if(song.video) song.video.pause();
}
song.audio.onplay = () => {
song.playing = true;
if(song.video) song.video.play();
}
if ('mediaSession' in navigator && config.mediaSession) {
navigator.mediaSession.metadata = new MediaMetadata({
title: song.song,
artist: song.artist,
album: "Osu! visualizer",
artwork: [
{ src: `${osuFolder}/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()});
navigator.mediaSession.setActionHandler('play', function() { song.playing = true; song.audio.play(); });
navigator.mediaSession.setActionHandler('pause', function() { song.playing = false; song.audio.pause(); });
navigator.mediaSession.setActionHandler('nexttrack', function() { playNext()});
}
}
})();
}
$: if(song && song.audio && config.rpc) {
if(song.playing) {
window.songActivity = {
state: "Listening to osu! beatmaps",
details: `${song.artist} - ${song.song}`,
startTimestamp: Date.now(),
endTimestamp: Date.now() + song.audio.duration * 1000,
instance: false,
largeImageKey: "logo",
largeImageText: "Osu!visualizer"
}
} else {
window.songActivity = {
state: "Paused",
details: `${song.artist} - ${song.song}`,
instance: false,
largeImageKey: "logo",
largeImageText: "Osu!visualizer"
}
}
}
function togglePlay() {
playing = !playing;
if(playing) {
song.playing = !song.playing;
if(!song.audio) return;
if(song.playing) {
song.audio.play();
} else {
song.audio.pause();
}
}
var volume = 1;
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));
volume = Math.min(1, Math.max(volume, 0));
}
$: if(song.audio) song.audio.volume = volume;
setTimeout(() => {
song = song;
playNext();
}, 200);
const volumeWidth = 100;
const volumeStroke = 4;
const volumeRadius = 50;
</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">
{#if now - last < config.autohide.info + 1000}
<div class="info" class:hidden={now - last > config.autohide.info}>
{#if song}
<div class="song">
<h2>{song.artist} - {song.song}</h2>
<div class="controls">
<div class="play" on:click={togglePlay}>
<img src="images/music_{song.playing ? "pause" : "play"}.svg" alt="{song.playing ? "Pause" : "Play"} music" title="{song.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 class="settings" on:click={() => settingsOpen = !settingsOpen}>
<img src="images/settings.svg" alt="Settings" title="Open settings">
</div>
</div>
</div>
</div>
{/if}
</div>
{#if now - lastVolumeUpdate < 4000 && song && song.audio}
<div class="volume" class:hidden={now - lastVolumeUpdate > 2000}>
{/if}
</div>
{/if}
{#if now - lastVolumeUpdate < config.autohide.volume + 1000 && song && song.audio}
<div class="volume" class:hidden={now - lastVolumeUpdate > config.autohide.volume}>
<div class="slider">
<div class="percent">
{Math.round(song.audio.volume * 100)}%
</div>
<svg class="progress-ring" width={volumeWidth} height={volumeWidth}>
<circle
stroke-width={volumeStroke}
fill="transparent"
stroke="blue"
stroke-dasharray={volumeRadius * 2 * Math.PI + " " + volumeRadius * 2 * Math.PI}
stroke-dashoffset={volumeRadius * 2 * Math.PI - song.audio.volume * volumeRadius * 2 * Math.PI}
r={volumeRadius - 1}
cx={volumeRadius - 1}
cy={volumeRadius + 1}
/>
</svg>
</div>
</div>
{/if}
<Options bind:config={config} bind:visible={settingsOpen} {osuFolder} />
</div>
<style>
@ -130,18 +195,53 @@
width: 100vw;
height: 80px;
transition: opacity 0.6s;
z-index: 1;
z-index: 2;
}
.volume {
opacity: 1;
position: fixed;
z-index: 2;
z-index: 5;
right: 0;
bottom: 0;
border-radius: 50%;
color: black;
font-size: 30px;
color: white;
background-color: black;
width: 100px;
height: 100px;
}
.volume .slider {
position: relative;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.percent {
position: absolute;
top: 25px;
left: 0;
width: 100%;
height: 100%;
text-align: center;
}
.progress-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.progress-ring circle {
transition: stroke-dashoffset 0.32s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
position: absolute;
top: 1px;
left: 1px;
width: 100%;
height: 100%;
}
.hidden {
@ -176,4 +276,8 @@
height: 100%;
filter: invert(100%);
}
.info .controls .settings img {
height: 65%;
padding-top: 25%;
}
</style>

View file

@ -1,21 +1,22 @@
<script>
import { createEventDispatcher } from 'svelte';
const fs = require("fs");
const OsuDBParser = require("osu-db-parser");
const osuParser = require("osu-parser");
const osuParser = require("./lib/osu-parser.js");
export var osuData;
export var songData;
export var config;
export var osuFolder;
var wallpapers = [];
try {
wallpapers = fs.readdirSync(process.env.USERPROFILE + "/AppData/Local/osu!/Data/bg");
wallpapers = fs.readdirSync(`${osuFolder}/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();
osuData = (new OsuDBParser(Buffer.from(fs.readFileSync(`${osuFolder}/osu!.db`)))).getOsuDBData();
console.log(osuData);
osuData.songs = osuData.beatmaps.map(v => ({
artist: v.artist_name,
@ -25,7 +26,8 @@
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`
dataFile: `${v.artist_name} - ${v.song_title} (${v.creator_name}) [${v.difficulty}].osu`.replace(/\/|\*|"|:|\?/g, ""),
playing: true
})).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);
@ -34,20 +36,41 @@
var wallpaper;
function shuffleWallpapers() {
wallpaper = wallpapers[Math.floor(Math.random() * wallpapers.length)];
switch(config.backgrounds) {
case 1:
if(songData.beatmap) {
wallpaper = `${osuFolder}/Songs/${songData.folder}/${songData.beatmap.bgFilename}`;
} else {
wallpaper = `${osuFolder}/Data/bg/${wallpapers[Math.floor(Math.random() * wallpapers.length)]}`;
}
break;
case 0:
default:
wallpaper = `${osuFolder}/Data/bg/${wallpapers[Math.floor(Math.random() * wallpapers.length)]}`;
}
}
shuffleWallpapers();
var lastSong = null;
var lastBackgroundOption = null;
$: {
if(songData !== lastSong) {
lastSong = songData;
shuffleWallpapers();
}
if(config.backgrounds !== lastBackgroundOption) {
lastBackgroundOption = config.backgrounds;
shuffleWallpapers();
}
}
function fetchBeatmap() {
let file = fs.readFileSync(process.env.USERPROFILE + "/AppData/Local/osu!/Songs/" + songData.folder + "/" + songData.dataFile);
let file = fs.readFileSync(`${osuFolder}/Songs/${songData.folder}/${songData.dataFile}`);
songData.beatmap = osuParser.parseContent(file);
if(config.backgrounds === 1) {
wallpaper = `${osuFolder}/Songs/${songData.folder}/${songData.beatmap.bgFilename}`;
}
}
$: if(songData && songData.dataFile && !songData.beatmap) fetchBeatmap();
@ -56,9 +79,11 @@
y: 0.5
};
const parallaxTreshold = 10;
var parallaxTreshold;
$: parallaxTreshold = config.parallax.treshold;
function updateMouse(e) {
if(!config.parallax.enabled) return;
mouse = {
x: -(e.clientX / window.innerWidth) * parallaxTreshold - parallaxTreshold/2,
y: -(e.clientY / window.innerHeight) * parallaxTreshold - parallaxTreshold/2
@ -78,7 +103,7 @@
setInterval(() => {
if(!songData) return;
if(!songData.beatmap && songData.dataFile) fetchBeatmap();
if(!songData.beatmap) return;
if(!songData.beatmap || !songData.audio) return;
var tp = null;
for(var t of songData.beatmap.timingPoints) {
@ -94,6 +119,25 @@
animDuration = tp.beatLength/2;
kiaiTime = tp.kiaiTimeActive;
}, 50);
$: {
if(!songData || !songData.beatmap || !songData.beatmap.video || !config.videoBackground) window.backgroundVideo = null;
}
$: console.log("Wallpaper", wallpaper);
$: console.log("Beatmap", songData.beatmap);
var backgroundVideo;
$: {
if(backgroundVideo) {
songData.video = backgroundVideo;
if(songData && songData.audio && songData.video) {
songData.video.currentTime = songData.audio.currentTime;
}
}
}
</script>
<svelte:window on:mousemove={updateMouse} on:resize={resize} />
@ -101,13 +145,24 @@
<div
class="main"
style="
background-image: url('{process.env.USERPROFILE.replace(/\\/g, "/")}/AppData/Local/osu!/Data/bg/{wallpaper}');
background-image: url('{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;">
{#if songData && songData.beatmap && songData.beatmap.video && config.videoBackground}
<!-- svelte-ignore a11y-media-has-caption -->
<video bind:this={backgroundVideo} style="
width: {isWidthSmaller ? "auto" : `calc(100% + ${parallaxTreshold * 1.5}px)`};
height: {!isWidthSmaller ? "auto" : `calc(100% + ${parallaxTreshold * 1.5}px)`};
top: {mouse.y}px;
left: {mouse.x}px;
">
<source src="file://{osuFolder}/Songs/{songData.folder}/{songData.beatmap.video}">
</video>
{/if}
<img src="images/logo.svg" alt="logo" class="logo" style="animation-duration: {animDuration}ms;" class:repeat={songData.playing}>
<img src="images/logo.svg" alt="" class="shadow" style="animation-duration: {animDuration * 2}ms;" class:repeat={songData.playing}>
</div>
<style>
@ -154,24 +209,36 @@
}
}
video {
position: fixed;
z-index: 0;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.main img {
position: fixed;
width: 500px;
height: 500px;
top: calc(50vh - 250px);
left: calc(50vw - 250px);
z-index: 1;
}
.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;
}
.main .repeat {
animation-iteration-count: infinite;
}
</style>

View file

@ -0,0 +1,92 @@
<script>
export var config;
export var visible;
export var osuFolder;
$: console.log("Config", config);
</script>
<div class="options">
<div class="bg" class:visible={visible} on:click={() => visible = false}></div>
<nav class:visible={visible}>
<h2>Options</h2>
<div class="group">
<h3>Parallax</h3>
<div class="row">
<span>Enable parallax</span>
<input type="checkbox" bind:checked={config.parallax.enabled}>
</div>
<div class="row" class:enabled={config.parallax.enabled}>
<span>Parallax treshold</span>
<input type="range" min="1" max="30" bind:value={config.parallax.treshold}>
</div>
</div>
<div class="group">
<h3>Integrations</h3>
<div class="row">
<span>Discord Rich Presence</span>
<input type="checkbox" bind:checked={config.rpc}>
</div>
<div class="row">
<span>MediaSession (system-wide controls)</span>
<input type="checkbox" bind:checked={config.mediaSession}>
</div>
</div>
<div class="group">
<h3>Backgrounds</h3>
<select bind:value={config.backgrounds}>
<option value={0}>Osu!wallpapers</option>
<option value={1}>Beatmap wallpapers</option>
</select>
<div class="row">
<span>Video backgrounds</span>
<input type="checkbox" bind:checked={config.videoBackground}>
</div>
</div>
<div class="group">
<h3>UI</h3>
<div class="row">
<span>Song info hide timeout</span>
<input type="range" min="1000" max="15000" step="500" bind:value={config.autohide.info}>
</div>
<div class="row">
<span>Volume hide timeout</span>
<input type="range" min="1000" max="15000" step="500" bind:value={config.autohide.volume}>
</div>
</div>
<span>Osu folder used: <code>{osuFolder}</code></span>
</nav>
</div>
<style>
.bg {
position: fixed;
display: none;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 3;
}
.bg.visible {
display: block;
}
nav {
position: fixed;
height: 100vh;
width: 400px;
top: 0;
left: -400px;
opacity: 0;
background: rgba(0,0,0,0.4);
color: white;
z-index: 4;
padding-left: 1rem;
transition: opacity 0.3s, left 0.3s;
}
nav.visible {
left: 0;
opacity: 1;
}
</style>

View file

@ -1,38 +1,48 @@
const { app, BrowserWindow } = require('electron');
const isDev = require('electron-is-dev');
const RPC = require("discord-rpc");
const rpc = new RPC.Client({ transport: "ipc" });
const path = require('path');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit();
app.quit();
}
if(!isDev) require('update-electron-app')()
if (!isDev) require('update-electron-app')()
console.log("isDev?", isDev);
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
var mainWindow;
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
},
autoHideMenuBar: true
});
mainWindow.setMenu(null);
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
},
autoHideMenuBar: true
});
mainWindow.setMenu(null);
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, '../public/index.html'));
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, '../public/index.html'));
// Open the DevTools.
mainWindow.webContents.openDevTools();
// Open the DevTools.
if(isDev) mainWindow.webContents.openDevTools({
activate: true,
mode: 'detach'
});
};
require('electron-reload')(__dirname, {
electron: path.join(__dirname, '../node_modules', '.bin', 'electron'),
awaitWriteFinish: true,
electron: path.join(__dirname, '../node_modules', '.bin', 'electron'),
awaitWriteFinish: true,
});
// This method will be called when Electron has finished
@ -44,18 +54,39 @@ app.on('ready', createWindow);
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
async function setActivity() {
if (!rpc || !mainWindow) {
return;
}
const activity = await mainWindow.webContents.executeJavaScript('window.songActivity');
rpc.setActivity(activity).catch((e) => { console.error(e); });
}
rpc.on('ready', () => {
setActivity();
// activity can only be set every 15 seconds
setInterval(() => {
setActivity();
}, 15e3);
});
rpc.login({ clientId: "756806736106618951" });