Compare commits
20 Commits
8b6f2e2588
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 840490b362 | |||
| 7c8741eb62 | |||
| e99fa4cbab | |||
| d2e24df6f4 | |||
| 3e5296ac25 | |||
| 8112a65204 | |||
| 3537634d17 | |||
| 42eb01257b | |||
| 761a33e540 | |||
| c13ba1839d | |||
| 6b8e0e2171 | |||
| cdd0552c78 | |||
| 171309f42c | |||
| 24e15b12b5 | |||
| 470e8e5937 | |||
| e2e3f1d35e | |||
| 239a65a241 | |||
| 33033e150c | |||
| a88ba01c6d | |||
| 75f07f2d82 |
@@ -1,17 +0,0 @@
|
|||||||
var express = require("express");
|
|
||||||
var {WebSocketServer} = require("ws");
|
|
||||||
|
|
||||||
var app = express();
|
|
||||||
var server = app.listen(924);
|
|
||||||
|
|
||||||
app.get('*', express.static("../frontend"));
|
|
||||||
|
|
||||||
|
|
||||||
var wss = new WebSocketServer({server});
|
|
||||||
|
|
||||||
wss.on("connection", (ws, req) => {
|
|
||||||
console.log(req);
|
|
||||||
ws.on("message", msg => {
|
|
||||||
console.log(msg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+1
-11
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Piano | DayDun</title>
|
<title>Piano</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
|
||||||
<link href="style.css" rel="stylesheet">
|
<link href="style.css" rel="stylesheet">
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
@@ -29,15 +29,6 @@
|
|||||||
<button id="user-kick">Kick</button>
|
<button id="user-kick">Kick</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="dimmer"></div>
|
<div id="dimmer"></div>
|
||||||
<div id="rooms-menu">
|
|
||||||
<ul id="rooms"></ul>
|
|
||||||
</div>
|
|
||||||
<div id="room-settings-menu">
|
|
||||||
<label title="Change the name of this room">Room name <input id="room-name" type="text"></input></label>
|
|
||||||
<label title="Change how fast people can mash their piano">Note quota <input id="room-quota-note" type="number"> notes per <input id="room-quota-time" type="number"> milliseconds</label>
|
|
||||||
<label title="Change the default piano sound that everyone in the room hears">Piano sound <select id="room-sound"></select></label>
|
|
||||||
<button id="room-settings-save">Save</button>
|
|
||||||
</div>
|
|
||||||
<div id="midi-player-menu">
|
<div id="midi-player-menu">
|
||||||
<input id="midi-upload" type="file" accept="audio/midi">
|
<input id="midi-upload" type="file" accept="audio/midi">
|
||||||
<button id="midi-play">Play</button>
|
<button id="midi-play">Play</button>
|
||||||
@@ -65,7 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<button id="rooms-button">Rooms</button>
|
|
||||||
<button id="room-settings-button">Room settings</button>
|
<button id="room-settings-button">Room settings</button>
|
||||||
<button id="midi-player-button">Midi player</button>
|
<button id="midi-player-button">Midi player</button>
|
||||||
|
|
||||||
|
|||||||
+5
-156
@@ -114,28 +114,6 @@ function loadMppBank(url, format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Bucket {
|
|
||||||
constructor(rate, time) {
|
|
||||||
this.lastCheck = Date.now();
|
|
||||||
this.allowance = rate;
|
|
||||||
this.rate = rate;
|
|
||||||
this.time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
spend(amount) {
|
|
||||||
this.allowance += (Date.now() - this.lastCheck) * (this.rate / this.time);
|
|
||||||
this.lastCheck = Date.now();
|
|
||||||
if (this.allowance > this.rate) {
|
|
||||||
this.allowance = this.rate;
|
|
||||||
}
|
|
||||||
if (this.allowance < amount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.allowance -= amount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Key {
|
class Key {
|
||||||
constructor(index) {
|
constructor(index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
@@ -180,7 +158,6 @@ class Piano {
|
|||||||
this.keys.push(new Key(i));
|
this.keys.push(new Key(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bucket = new Bucket(Infinity, 0);
|
|
||||||
this.sustain = false;
|
this.sustain = false;
|
||||||
this.performanceMode = false;
|
this.performanceMode = false;
|
||||||
|
|
||||||
@@ -319,8 +296,6 @@ class Piano {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localKeyDown(key, velocity) {
|
localKeyDown(key, velocity) {
|
||||||
if (!this.bucket.spend(1)) return;
|
|
||||||
|
|
||||||
net.keyDown(key, velocity);
|
net.keyDown(key, velocity);
|
||||||
this.keyDown(key, velocity, net.id);
|
this.keyDown(key, velocity, net.id);
|
||||||
}
|
}
|
||||||
@@ -535,27 +510,6 @@ function openModal(element, dimmer, callback) {
|
|||||||
document.addEventListener("click", clickListener, true);
|
document.addEventListener("click", clickListener, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Room {
|
|
||||||
constructor(room) {
|
|
||||||
this.name = room.name;
|
|
||||||
this.keyQuota = room.keyQuota;
|
|
||||||
this.sound = room.sound;
|
|
||||||
|
|
||||||
this.updateRoomSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRoomSettings() {
|
|
||||||
piano.bucket = new Bucket(this.keyQuota.rate, this.keyQuota.per);
|
|
||||||
|
|
||||||
document.getElementById("room-name").value = this.name;
|
|
||||||
document.getElementById("room-quota-note").value = this.keyQuota.rate;
|
|
||||||
document.getElementById("room-quota-time").value = this.keyQuota.per;
|
|
||||||
document.getElementById("room-sound").value = this.sound;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
constructor(id, uid, nick, color) {
|
constructor(id, uid, nick, color) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -638,11 +592,8 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Networker {
|
class Networker {
|
||||||
constructor(url = `ws://${location.host}?`) {
|
constructor(url = location.origin.replace("http", "ws")) {
|
||||||
url += "room=" + encodeURIComponent(decodeURIComponent(location.pathname.slice(7)));
|
if (localStorage.nick) url += "?nick=" + encodeURIComponent(localStorage.nick);
|
||||||
if (localStorage.nick) {
|
|
||||||
url += "&nick=" + encodeURIComponent(localStorage.nick);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws = new WebSocket(url);
|
this.ws = new WebSocket(url);
|
||||||
this.ws.binaryType = "arraybuffer";
|
this.ws.binaryType = "arraybuffer";
|
||||||
@@ -660,7 +611,6 @@ class Networker {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.rooms = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message(message) {
|
message(message) {
|
||||||
@@ -671,12 +621,8 @@ class Networker {
|
|||||||
|
|
||||||
switch(message.type) {
|
switch(message.type) {
|
||||||
case "load":
|
case "load":
|
||||||
if (this.room == message.room) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.id = message.id;
|
this.id = message.id;
|
||||||
this.room = new Room(message.room);
|
|
||||||
|
|
||||||
this.users = {};
|
this.users = {};
|
||||||
for (let i=0; i<message.users.length; i++) {
|
for (let i=0; i<message.users.length; i++) {
|
||||||
@@ -686,52 +632,10 @@ class Networker {
|
|||||||
|
|
||||||
chat.chatLog(message.chatlog);
|
chat.chatLog(message.chatlog);
|
||||||
|
|
||||||
this.rooms = message.rooms;
|
|
||||||
for (let i=0; i<this.rooms.length; i++) {
|
|
||||||
let element = document.createElement("li");
|
|
||||||
element.addEventListener("click", (function(room) {
|
|
||||||
return function() {
|
|
||||||
window.location.pathname = "/" + room;
|
|
||||||
};
|
|
||||||
})(this.rooms[i].name));
|
|
||||||
let roomName = document.createElement("span");
|
|
||||||
roomName.className = "name";
|
|
||||||
roomName.innerText = this.rooms[i].name;
|
|
||||||
element.appendChild(roomName);
|
|
||||||
|
|
||||||
let users = document.createElement("span");
|
|
||||||
users.className = "users";
|
|
||||||
users.innerText = this.rooms[i].users;
|
|
||||||
element.appendChild(users);
|
|
||||||
document.getElementById("rooms").appendChild(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorage.adminlogin) {
|
if (localStorage.adminlogin) {
|
||||||
net.chat("/adminlogin " + localStorage.adminlogin);
|
net.chat("/adminlogin " + localStorage.adminlogin);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "rooms":
|
|
||||||
this.rooms = message.rooms;
|
|
||||||
document.getElementById("rooms").innerHTML = "";
|
|
||||||
for (let i=0; i<this.rooms.length; i++) {
|
|
||||||
let element = document.createElement("li");
|
|
||||||
element.addEventListener("click", (function(room) {
|
|
||||||
return function() {
|
|
||||||
window.location.pathname = "/" + room;
|
|
||||||
};
|
|
||||||
})(this.rooms[i].name));
|
|
||||||
let roomName = document.createElement("span");
|
|
||||||
roomName.className = "name";
|
|
||||||
roomName.innerText = this.rooms[i].name;
|
|
||||||
element.appendChild(roomName);
|
|
||||||
|
|
||||||
let users = document.createElement("span");
|
|
||||||
users.className = "users";
|
|
||||||
users.innerText = this.rooms[i].users;
|
|
||||||
element.appendChild(users);
|
|
||||||
document.getElementById("rooms").appendChild(element);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "join":
|
case "join":
|
||||||
this.users[message.id] = new User(message.id, message.uid, message.nick, message.color);
|
this.users[message.id] = new User(message.id, message.uid, message.nick, message.color);
|
||||||
break;
|
break;
|
||||||
@@ -763,13 +667,6 @@ class Networker {
|
|||||||
document.body.className = "admin";
|
document.body.className = "admin";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "room-settings":
|
|
||||||
this.room.name = message.name;
|
|
||||||
this.room.keyQuota.rate = message.keyQuota.rate;
|
|
||||||
this.room.keyQuota.per = message.keyQuota.per;
|
|
||||||
this.room.sound = message.sound;
|
|
||||||
this.room.updateRoomSettings();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -781,25 +678,19 @@ class Networker {
|
|||||||
let key = dv.getUint8(1);
|
let key = dv.getUint8(1);
|
||||||
let velocity = dv.getUint8(2) / 255;
|
let velocity = dv.getUint8(2) / 255;
|
||||||
let id = dv.getUint8(3);
|
let id = dv.getUint8(3);
|
||||||
if (id != this.id) {
|
piano.keyDown(key, velocity, id);
|
||||||
piano.keyDown(key, velocity, id);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 1: { // Key up
|
case 1: { // Key up
|
||||||
let key = dv.getUint8(1);
|
let key = dv.getUint8(1);
|
||||||
let sustain = dv.getUint8(2);
|
let sustain = dv.getUint8(2);
|
||||||
let id = dv.getUint8(3);
|
let id = dv.getUint8(3);
|
||||||
if (id != this.id) {
|
piano.keyUp(key, sustain, id);
|
||||||
piano.keyUp(key, sustain, id);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: { // Sustain release
|
case 2: { // Sustain release
|
||||||
let id = dv.getUint8(1);
|
let id = dv.getUint8(1);
|
||||||
if (id != this.id) {
|
piano.releaseSustain(id);
|
||||||
piano.releaseSustain(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,18 +720,6 @@ class Networker {
|
|||||||
this.ws.send(buffer);
|
this.ws.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
roomSettings(data) {
|
|
||||||
this.ws.send(JSON.stringify({
|
|
||||||
type: "room-settings",
|
|
||||||
name: data.name,
|
|
||||||
keyQuota: {
|
|
||||||
rate: data.keyQuota.rate,
|
|
||||||
per: data.keyQuota.per
|
|
||||||
},
|
|
||||||
sound: data.sound
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
chat(message) {
|
chat(message) {
|
||||||
this.ws.send(JSON.stringify({
|
this.ws.send(JSON.stringify({
|
||||||
type: "chat",
|
type: "chat",
|
||||||
@@ -971,14 +850,6 @@ class Chat {
|
|||||||
element.className = "nick";
|
element.className = "nick";
|
||||||
element.textContent = message.id + " changed nick to " + message.nick;
|
element.textContent = message.id + " changed nick to " + message.nick;
|
||||||
break;
|
break;
|
||||||
case "rank":
|
|
||||||
element.className = "rank";
|
|
||||||
if (message.rank === 1) {
|
|
||||||
element.textContent = "You're now the owner of this room.";
|
|
||||||
} else if (message.rank === 2) {
|
|
||||||
element.textContent = "You're now logged in as admin.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "disconnected":
|
case "disconnected":
|
||||||
element.className = "disconnected";
|
element.className = "disconnected";
|
||||||
element.textContent = "You were disconnected from the server!";
|
element.textContent = "You were disconnected from the server!";
|
||||||
@@ -1053,14 +924,6 @@ window.addEventListener("load", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Footer menus
|
// Footer menus
|
||||||
document.getElementById("rooms-button").addEventListener("click", function(event) {
|
|
||||||
openModal(document.getElementById("rooms-menu"), true);
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
});
|
|
||||||
document.getElementById("room-settings-button").addEventListener("click", function(event) {
|
|
||||||
openModal(document.getElementById("room-settings-menu"), true);
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
});
|
|
||||||
document.getElementById("midi-player-button").addEventListener("click", function(event) {
|
document.getElementById("midi-player-button").addEventListener("click", function(event) {
|
||||||
openModal(document.getElementById("midi-player-menu"), false);
|
openModal(document.getElementById("midi-player-menu"), false);
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
@@ -1070,19 +933,6 @@ window.addEventListener("load", function() {
|
|||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Room settings
|
|
||||||
document.getElementById("room-settings-save").addEventListener("click", function() {
|
|
||||||
net.roomSettings({
|
|
||||||
name: document.getElementById("room-name").value,
|
|
||||||
keyQuota: {
|
|
||||||
rate: parseInt(document.getElementById("room-quota-note").value),
|
|
||||||
per: parseInt(document.getElementById("room-quota-time").value)
|
|
||||||
},
|
|
||||||
sound: document.getElementById("room-sound").value
|
|
||||||
});
|
|
||||||
document.getElementById("dimmer").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Midi player
|
// Midi player
|
||||||
document.getElementById("midi-upload").addEventListener("change", function() {
|
document.getElementById("midi-upload").addEventListener("change", function() {
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
@@ -1166,7 +1016,6 @@ window.addEventListener("load", function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("setting-sounds").appendChild(getOption());
|
document.getElementById("setting-sounds").appendChild(getOption());
|
||||||
document.getElementById("room-sound").appendChild(getOption());
|
|
||||||
});
|
});
|
||||||
document.getElementById("setting-sounds").addEventListener("input", function() {
|
document.getElementById("setting-sounds").addEventListener("input", function() {
|
||||||
let sound = this.value;
|
let sound = this.value;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
The frontend was stolen from [DayDun](https://daydun.com/piano/) and the backend re-implemented from scratch. Rooms and rate limit functionality was stripped out, each connection is a different user, and raw IP addresses are used for user ids.
|
||||||
|
|
||||||
|
The notable thing about this and DayDun's piano is that note events are simply, individually, immediately broadcasted in a minimal binary format, and clients _instantly_ play notes they receive; unlike Brandon Lockaby's Multiplayer Piano which buffers note events with timing data into JSON that's sent every 200ms and then played exactly _one second_ after they actually happened. (PianoRhythm does something similar.) Doing this preserves the exact note timing regardless of networking quality (unless it takes longer than a second for the data to get through), which is fine for listening to other players, but a problem when playing together. With this piano, notes go directly through as fast as possible, which is perfect for local networks, and reveals the true networking quality and latency over the internet.
|
||||||
|
|
||||||
|
It's also super simple to run if you do want to use it on your local network; just download, `npm install`, `node server.js` and connect to it (default port is 924).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
var express = require("express");
|
||||||
|
var qs = require("qs");
|
||||||
|
var proxyaddr = require("proxy-addr");
|
||||||
|
var {WebSocketServer} = require("ws");
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
app.set("trust proxy", "loopback");
|
||||||
|
app.use(express.static("frontend"));
|
||||||
|
var server = app.listen(process.env.PORT || 924, process.env.ADDRESS);
|
||||||
|
|
||||||
|
var wss = new WebSocketServer({server, clientTracking: true});
|
||||||
|
|
||||||
|
var chatlog = [];
|
||||||
|
|
||||||
|
wss.on("connection", (ws, req) => {
|
||||||
|
req.ip = proxyaddr(req, app.get("trust proxy"));
|
||||||
|
req.query = qs.parse(req.url.substr(req.url.indexOf('?')+1));
|
||||||
|
|
||||||
|
function broadcast(msg, excludeSelf) {
|
||||||
|
if (typeof msg == "object" && !(msg instanceof Buffer)) msg = JSON.stringify(msg);
|
||||||
|
for (let ows of wss.clients) if (!(ows == ws && excludeSelf)) ows.send(msg);
|
||||||
|
}
|
||||||
|
function broadcastChat(message) {
|
||||||
|
broadcast({type: "chat", message});
|
||||||
|
chatlog.push(message);
|
||||||
|
if (chatlog.length >= 100) chatlog.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.user = {
|
||||||
|
uid: req.ip,
|
||||||
|
nick: req.query.nick || req.ip,
|
||||||
|
color: [Math.floor(Math.random()*256),Math.floor(Math.random()*256),Math.floor(Math.random()*256)]
|
||||||
|
}
|
||||||
|
let t = Array.from(wss.clients).map(ws => ws.user.id);
|
||||||
|
for (let i = 0; i < 256; i++) if (!t.includes(i)) { ws.user.id = i; break; }
|
||||||
|
if (ws.user.id == null) return ws.close();
|
||||||
|
|
||||||
|
console.log("join", ws.user);
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: "load",
|
||||||
|
id: ws.user.id,
|
||||||
|
uid: ws.user.uid,
|
||||||
|
users: Array.from(wss.clients).map(x => x.user),
|
||||||
|
chatlog
|
||||||
|
}));
|
||||||
|
|
||||||
|
broadcast({type: "join", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick, color: ws.user.color}, true);
|
||||||
|
broadcastChat({type: "join", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick});
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("leave", ws.user);
|
||||||
|
broadcast({type: "leave", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick, color: ws.user.color}, true);
|
||||||
|
broadcastChat({type: "leave", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick})
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (msg, isBinary) => {
|
||||||
|
if (isBinary) {
|
||||||
|
broadcast(Buffer.concat([msg, Buffer.from([ws.user.id])]), true);
|
||||||
|
} else {
|
||||||
|
msg = msg.toString();
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(msg);
|
||||||
|
} catch (error) { return }
|
||||||
|
console.log(msg);
|
||||||
|
switch (msg.type) {
|
||||||
|
case "chat":
|
||||||
|
broadcastChat({type: "message", content: msg.message, user: ws.user});
|
||||||
|
break;
|
||||||
|
case "nick":
|
||||||
|
ws.user.nick = msg.nick;
|
||||||
|
broadcast({type: "nick", nick: ws.user.nick, id: ws.user.id, uid: ws.user.uid});
|
||||||
|
broadcastChat({type: "nick", nick: ws.user.nick, id: ws.user.id});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user