/* TODO: */ const audioCtx = new (AudioContext || webkitAudioContext)(); function mod(n, m) { return ((n % m) + m) % m; } const WHITE = 0; const BLACK = 1; let pianoLayout = [ WHITE, BLACK, WHITE, BLACK, WHITE, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE ]; let blackKeyOffset = [1, 3, 0, 6, 8, 10, 0]; let whiteKeyOffset = [0, 2, 4, 5, 7, 9, 11]; let keybindValues = [0, 1, 2, 3, 4, -1, 5, 6, 7, 8, 9, 10, 11, -1]; let keyMap = { "IntlBackslash": -16, "KeyA": -15, "KeyZ": -14, "KeyS": -13, "KeyX": -12, "KeyD": -11, "KeyC": -10, "KeyF": -9, "KeyV": -8, "KeyG": -7, "KeyB": -6, "KeyH": -5, "KeyN": -4, "KeyJ": -3, "KeyM": -2, "KeyK": -1, "Comma": 0, "KeyL": 1, "Period": 2, "Semicolon": 3, "Slash": 4, "Quote": 5, "Digit1": -1, "KeyQ": 0, "Digit2": 1, "KeyW": 2, "Digit3": 3, "KeyE": 4, "Digit4": 5, "KeyR": 6, "Digit5": 7, "KeyT": 8, "Digit6": 9, "KeyY": 10, "Digit7": 11, "KeyU": 12, "Digit8": 13, "KeyI": 14, "Digit9": 15, "KeyO": 16, "Digit0": 17, "KeyP": 18, "Minus": 19, "BracketLeft": 20, "Equal": 21, "BracketRight": 22 }; let keysDown = new Set(); let piano; let audioEngine; let net; let chat; let focus = true; let userMenuUser; let soundBank = []; let loading = 0; function loadMppBank(url, format) { for (let i=21; i<109; i++) { piano.keys[i - 21].loaded = false; let keyName = ["c", "cs", "d", "ds", "e", "f", "fs", "g", "gs", "a", "as", "b"][i % 12] + (Math.floor(i / 12) - 2); let xhttp = new XMLHttpRequest(); xhttp.responseType = "arraybuffer"; xhttp.addEventListener("load", (function(index) { return function() { audioCtx.decodeAudioData(xhttp.response, function(buffer) { soundBank[index] = buffer; loading++; piano.keys[i - 21].loaded = true; }); } })(i)); xhttp.open("GET", url + keyName + "." + format); xhttp.send(); } } class Key { constructor(index) { this.index = index; this.value = index + 21; this.type = pianoLayout[this.value % 12]; this.pressed = false; this.presser = null; this.loaded = false; this.y = 1; this.color = null; } animate() { if (this.pressed) { this.y = 0; if (!(this.presser in net.users)) { this.pressed = false; return; } if (Date.now() - this.pressed > 5000) { this.pressed = false; return; } this.color = net.users[this.presser].color; } else { this.y = 1; this.color = null; } } } class Piano { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.keys = []; for (let i=0; i<88; i++) { this.keys.push(new Key(i)); } this.sustain = false; this.performanceMode = false; this.margin = 8; this.scale = 1; this.setScale(3); } // Get key at position hit(x, y) { let whiteWidth = (this.canvas.width - 155 - this.margin * 2) / 52 + 2; let blackWidth = Math.ceil(whiteWidth * 0.5 / 2) * 2 + 1; let blackHeight = Math.floor((this.canvas.height - this.margin * 2) / 2); // Offset to octave 0 x += (whiteWidth + 1) * 5 - this.margin; y -= this.margin; if (y <= blackHeight) { let blackX = x - (blackWidth - 1) / 2; let blackKey = Math.floor(blackX / (whiteWidth + 1)); if (!(blackKey % 7 === 2 || blackKey % 7 === 6 || blackKey == 4 || blackKey == 56)) { blackX = blackX % (whiteWidth + 1); if (blackX >= whiteWidth - (blackWidth - 1)) { let index = Math.floor(blackKey / 7) * 12 + blackKeyOffset[blackKey % 7]; return this.keys[index - 9]; } } } if (x % (whiteWidth + 1) == whiteWidth) { return null; } let whiteKey = Math.floor(x / (whiteWidth + 1)); let index = Math.floor(whiteKey / 7) * 12 + whiteKeyOffset[whiteKey % 7]; return this.keys[index - 9]; } setScale(scale) { this.scale = scale; this.canvas.style.transform = "scale(" + scale + ")"; this.resize(); } resize() { // border + margin pixels: 155 let size = Math.floor((window.innerWidth / this.scale - 155) / 52); this.canvas.width = size * 52 + 155 + this.margin * 2; this.canvas.height = Math.floor(this.canvas.width * 0.18); } animateKeys() { this.keys.forEach(function(key) { key.animate(); }); } render() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Render casing this.ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Render keys let keyRegionWidth = this.canvas.width - this.margin * 2; let keyRegionHeight = this.canvas.height - this.margin * 2; let whiteWidth = (keyRegionWidth - 155) / 52 + 2; let blackWidth = Math.ceil(whiteWidth * 0.5 / 2) * 2 + 1; let blackHeight = Math.floor(keyRegionHeight / 2); let keyDepth = Math.ceil(keyRegionHeight / 20); this.ctx.save(); this.ctx.translate(this.margin, this.margin); // Draw white keys this.ctx.strokeStyle = "#ccc"; for (let i=0, x=0; i { this.remove(); }; this.source.addEventListener("ended", this.ended); this.gain = audioCtx.createGain(); this.gain.gain.value = velocity; this.source.connect(this.gain); this.gain.connect(audioEngine.destination); this.source.start(); this.user = user; audioEngine.playing.push(this); } stop() { let fade = 0.15; this.gain.gain.setTargetAtTime(0, audioCtx.currentTime, fade / 3); //this.gain.gain.linearRampToValueAtTime(this.gain.gain.value * 0.1, time + 0.2); //this.gain.gain.linearRampToValueAtTime(0, time + 0.5); this.source.removeEventListener("ended", this.ended); this.source.stop(audioCtx.currentTime + fade); this.remove(); } purge() { this.source.removeEventListener("ended", this.ended); this.source.stop(); this.gain.disconnect(audioEngine.destination); this.remove(); } remove() { let index = audioEngine.playing.indexOf(this); if (index === -1) { // This should never happen, but shit will fuck up if it happens. return; } audioEngine.playing.splice(index, 1); } } class AudioEngine { constructor() { this.active = false; // autoplay policy this.playing = []; this.masterGain = audioCtx.createGain(); this.masterGain.gain.value = 0.5; this.masterGain.connect(audioCtx.destination); this.compressor = audioCtx.createDynamicsCompressor(); // These should probably be fine tuned this.compressor.threshold.value = -10; this.compressor.knee.value = 0; this.compressor.ratio.value = 20; this.compressor.attack.value = 0; this.compressor.release.value = 0.1; this.compressor.connect(this.masterGain); this.destination = this.compressor; } get volume() { return this.masterGain.gain.value; } set volume(value) { this.masterGain.gain.value = value; } noteOn(key, velocity, user) { if (!this.active) return; /*if (this.playing.length >= 500) { // Prevent lag return; }*/ for (let i=0; i=0; i--) { if (this.playing[i].key == key && this.playing[i].user == user) { this.playing[i].stop(); } } } releaseSustain(user) { for (let i=this.playing.length - 1; i>=0; i--) { if (this.playing[i].user == user) { this.playing[i].stop(); } } } } function render() { piano.animateKeys(); piano.render(); window.requestAnimationFrame(render); } function setVolume(value, modifySlider) { audioEngine.volume = value; if (value === 0) { document.getElementById("volume-indicator").className = "mute"; } else if (value <= 0.5) { document.getElementById("volume-indicator").className = "low"; } else { document.getElementById("volume-indicator").className = ""; } if (modifySlider) { document.getElementById("volume").value = value; } } function openModal(element, dimmer, callback) { if (dimmer) { document.getElementById("dimmer").classList.add("active"); } element.classList.add("active"); function clickListener(event) { if (!element.contains(event.target)) { element.classList.remove("active"); document.getElementById("dimmer").classList.remove("active"); if (callback) { callback(); } document.removeEventListener("click", clickListener, true); } } document.addEventListener("click", clickListener, true); } class User { constructor(id, uid, nick, color) { this.id = id; this.uid = uid; this.nick = nick; this.color = color; this.local = this.id === net.id; this.muteNotes = false; this.element = document.createElement("li"); if (this.local) { this.element.classList.add("local"); } this.element.addEventListener("click", () => { this.updateUserMenu(); openModal(document.getElementById("user-menu"), false, () => { if (!this.local) return; let nick = document.getElementById("user-nick").value; if (nick != this.nick) { net.setNick(nick); } }); event.stopImmediatePropagation(); }); this.element.innerText = this.nick ? this.nick : this.id; this.element.style.backgroundColor = this.getColor(); document.getElementById("users").appendChild(this.element); } getColor() { if (this.muteNotes) { return "rgb(140, 140, 140)"; } else { return `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`; } } updateUserMenu() { userMenuUser = this.id; let userMenu = document.getElementById("user-menu"); if (this.id == net.id) { userMenu.classList.add("local"); } else { userMenu.classList.remove("local"); } userMenu.style.backgroundColor = this.getColor(); document.getElementById("user-uid").textContent = this.uid; document.getElementById("user-nick").value = this.nick; document.getElementById("user-mute-notes").checked = this.muteNotes; let box = this.element.getBoundingClientRect(); userMenu.style.top = (box.bottom + 8) + "px"; userMenu.style.left = Math.max(box.left, 8) + "px"; } update() { this.element.style.backgroundColor = this.getColor(); } setMuteNotes(value) { this.muteNotes = value; if (this.muteNotes) { piano.releaseSustain(this.id); } } changeNick(nick) { this.nick = nick; this.element.innerText = this.nick; } leave() { document.getElementById("users").removeChild(this.element); delete net.users[this.id]; } } class Networker { constructor(url = location.origin.replace("http", "ws")) { if (localStorage.nick) url += "?nick=" + encodeURIComponent(localStorage.nick); this.ws = new WebSocket(url); this.ws.binaryType = "arraybuffer"; this.ws.addEventListener("open", function() { console.log("open"); }); this.ws.addEventListener("message", (message) => { this.message(message.data); }); this.ws.addEventListener("close", function() { console.log("close"); chat.receive({ type: "disconnected" }); }); this.users = {}; } message(message) { if (typeof message == "string") { message = JSON.parse(message); console.log(message); switch(message.type) { case "load": this.id = message.id; this.users = {}; for (let i=0; i { if (event.code == "Enter") { if (document.activeElement == this.inputElement) { let message = this.inputElement.value; message = message.trim(); if (message) { this.send(message); } this.inputElement.value = ""; this.inputElement.blur(); } else { this.inputElement.focus(); } } }); this.inputElement.addEventListener("focus", () => { this.chatElement.className = "focus"; }); this.inputElement.addEventListener("blur", () => { this.chatElement.className = ""; }); } chatLog(chatLog) { for (let i=0; i { if (this.prevNotif) { this.prevNotif.close(); } }, 2000); return true; } Notification.requestPermission().then(function(permission) { if (permission === "granted") { new Notification("Notifications enabled!"); } }); } receive(message, chatLog) { let element = document.createElement("li"); switch(message.type) { case "message": element.className = "message"; let user = message.user; let nickSpan = document.createElement("span"); nickSpan.className = "nick"; nickSpan.style.color = `rgb(${user.color[0]}, ${user.color[1]}, ${user.color[2]})`; nickSpan.textContent = user.nick; element.appendChild(nickSpan); let text = document.createElement("span"); text.className = "content"; text.textContent = message.content; element.appendChild(text); if (!chatLog) { this.notification(user.nick, message.content); } break; case "join": element.className = "join"; element.textContent = message.nick + " joined"; break; case "leave": element.className = "leave"; element.textContent = message.nick + " left"; break; case "nick": element.className = "nick"; element.textContent = message.id + " changed nick to " + message.nick; break; case "disconnected": element.className = "disconnected"; element.textContent = "You were disconnected from the server!"; break; } let shouldScroll = false; if (this.messagesElement.scrollTop >= this.messagesElement.scrollHeight - this.messagesElement.clientHeight) { shouldScroll = true; } this.messagesElement.appendChild(element); if (shouldScroll) { this.messagesElement.scrollTop = this.messagesElement.scrollHeight - this.messagesElement.clientHeight; } } send(message) { net.chat(message); } } window.addEventListener("load", function() { piano = new Piano(document.getElementById("piano")); audioEngine = new AudioEngine(); //loadMppBank("https://ledlamp.github.io/piano-sounds/Emotional/", "mp3"); loadMppBank("https://ledlamp.github.io/piano-sounds/Untitled/", "mp3"); window.addEventListener("resize", function() { piano.resize(); }); function activate() { audioEngine.active = true; document.getElementById("autoplay-notice").classList.add("hidden"); window.removeEventListener("click", activate); } window.addEventListener("click", activate); chat = new Chat( document.getElementById("chat"), document.getElementById("chat-messages"), document.getElementById("chat-input") ); // User menu document.getElementById("user-mute-notes").addEventListener("input", function() { net.users[userMenuUser].setMuteNotes(this.checked); net.users[userMenuUser].updateUserMenu(); net.users[userMenuUser].update(); }); // Focus window.addEventListener("focus", function() { focus = true; }); window.addEventListener("blur", function() { focus = false; }); // Volume slider document.getElementById("volume").addEventListener("input", function(event) { let value = parseFloat(event.target.value); setVolume(value); }); document.getElementById("volume").addEventListener("focus", function(event) { this.blur(); }); // Footer menus document.getElementById("midi-player-button").addEventListener("click", function(event) { openModal(document.getElementById("midi-player-menu"), false); event.stopImmediatePropagation(); }); document.getElementById("settings-button").addEventListener("click", function(event) { openModal(document.getElementById("settings-menu"), false); event.stopImmediatePropagation(); }); // Midi player document.getElementById("midi-upload").addEventListener("change", function() { let reader = new FileReader(); reader.addEventListener("load", function() { let midiFile = new MidiFile(reader.result); midiPlayer.loadMidi(midiFile); console.log(midiFile); }); reader.readAsArrayBuffer(this.files[0]); }); document.getElementById("midi-play").addEventListener("click", function() { midiPlayer.play(); }); document.getElementById("midi-pause").addEventListener("click", function() { midiPlayer.pause(); }); // Settings menu let tabs = ["general", "midi", "input", "audio"]; tabs.forEach(function(tab) { document.getElementById("tab-" + tab + "-button").addEventListener("click", function() { for (let i=0; i { if (keyDown) { piano.localKeyUp(keyDown); keyDown = null; } let key = piano.hit(event.offsetX, event.offsetY); if (key) { piano.localKeyDown(key.value, 1); keyDown = key.value; } }); window.addEventListener("mouseup", (event) => { if (keyDown) { piano.localKeyUp(keyDown); keyDown = null; } }); // Keyboard Input window.addEventListener("keydown", (event) => { if (document.activeElement != document.body) return; let key = event.code; if (key == "Backspace") { piano.sustain = !piano.sustain; if (!piano.sustain) { piano.localReleaseSustain(); } return; } if (keysDown.has(key)) return; keysDown.add(key); if (!(key in keyMap)) return; let rawValue = keyMap[key]; if (keybindValues[mod(rawValue, 14)] === -1) return; let value = Math.floor(rawValue / 14) * 12 + keybindValues[mod(rawValue, 14)] + 60; piano.localKeyDown(value, 1); }); window.addEventListener("keyup", (event) => { if (document.activeElement != document.body) return; let key = event.code; keysDown.delete(key); let rawValue = keyMap[key]; if (!(key in keyMap)) return; if (keybindValues[mod(rawValue, 14)] === -1) return; let value = Math.floor(rawValue / 14) * 12 + keybindValues[mod(rawValue, 14)] + 60; piano.localKeyUp(value); }); // MIDI Input if (navigator.requestMIDIAccess) { navigator.requestMIDIAccess().then(function(midi) { console.log(midi); function messageHandler(event) { let cmd = event.data[0] >> 4; let channel = event.data[0] & 0xF; let note = event.data[1]; let vel = event.data[2]; if (cmd == 8 || (cmd == 9 && vel === 0)) { // Note off piano.localKeyUp(note); } else if (cmd == 9) { // Note on piano.localKeyDown(note, vel / 127); } else if (cmd == 11) { // Control change if (note == 64) { if (vel >= 64) { // Sustain piano.sustain = true; } else { // Sustain off piano.sustain = false; piano.localReleaseSustain(); } } else if (note == 7) { // Volume let volume = vel / 127; setVolume(volume, true); } } } function port() { midi.inputs.forEach(function(input) { input.addEventListener("midimessage", messageHandler); }); } port(); // Get lists of available MIDI controllers const inputs = midi.inputs.values(); const outputs = midi.outputs.values(); midi.addEventListener("statechange", function(e) { // Print information about the (dis)connected MIDI controller console.log(e.port.name, e.port.manufacturer, e.port.state); }); }); } // Net net = new Networker(); window.requestAnimationFrame(render); });