From f155d52b6d3281be8a5727b5425436e31fad3bf3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 18 Mar 2018 18:07:08 +0000 Subject: [PATCH] Used in 2018 ROF --- README.md | 26 +++++++++ playlist.css | 50 ++++++++++++++++ playlist.html | 40 +++++++++++++ playlist.js | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+) create mode 100644 README.md create mode 100644 playlist.css create mode 100644 playlist.html create mode 100644 playlist.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ee50e4 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +This is a pure HTML5 playlist, +specifically designed for theater work. + +You give it a play list, and it cues them up, +one at a time. +Press "play" to play the cued-up track, +use the arrow keys to select a different track. + +The normal browser media controls are available too. + +You can also plug a Korg NanoKontrol 2 in, +and use its play/stop/prev/next buttons, +as well as the first fader. + +This features a massive time display, +so everybody else in the control booth can see how far into the track you are, +for things like light cues, etc. + +Just drop these three files in the directory with all your tracks, +and edit the HTML to have the right list of tracks. +You can have the same track more than once, if you want. + +Author +------ + +Neale Pickett diff --git a/playlist.css b/playlist.css new file mode 100644 index 0000000..a7d0740 --- /dev/null +++ b/playlist.css @@ -0,0 +1,50 @@ +body { + background-color: #000; + color: #fa4; + font-family: sans-serif; +} + +audio { + min-width: 40em; +} + +#currentTime { + background-color: #001; + float: right; + font-size: 1500%; + font-family: "Roboto", "Droid Sans Mono", monospace; +} + +#controls { + padding: 1em; +} +#controls a { + font-size: 200%; + padding: 0.5em; +} + +#playlist { + width: auto; +} + +.current { + background-color: #224; +} + +@keyframes pulse { + 0% { + background-color: #430; + } + 25% { + background-color: inherit; + } + 75% { + background-color: inherit; + } + 100% { + background-color: #430; + } +} +.fin { + animation: pulse 1s infinite; +} \ No newline at end of file diff --git a/playlist.html b/playlist.html new file mode 100644 index 0000000..65d7837 --- /dev/null +++ b/playlist.html @@ -0,0 +1,40 @@ + + + + 2018 ROF + + + + + + + +
00:00
+ +
+ + +
+ + + + diff --git a/playlist.js b/playlist.js new file mode 100644 index 0000000..27cffc9 --- /dev/null +++ b/playlist.js @@ -0,0 +1,159 @@ +var base = "."; + +function loadTrack(e) { + let li = e.srcElement; + let audio = document.querySelector("#audio"); + audio.src = base + "/" + li.textContent; + audio.load(); + + // Update "current" + for (let cur of document.querySelectorAll(".current")) { + cur.classList.remove("current"); + } + li.classList.add("current"); +} + +function clickOn(element) { + let e = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true + }); + element.dispatchEvent(e); +} + +function prev() { + let cur = document.querySelector(".current"); + let prev = cur.previousElementSibling; + if (prev) { + cur = prev; + } + clickOn(cur); +} + +function next() { + let cur = document.querySelector(".current"); + let next = cur.nextElementSibling; + if (next) { + cur = next; + } + clickOn(cur); +} + +function ended() { + next(); +} + +function mmss(s) { + let mm = Math.floor(s / 60); + let ss = Math.floor(s % 60); + + if (ss < 10) { + ss = "0" + ss; + } + return mm + ":" + ss; +} + +function timeupdate(e) { + let currentTime = e.srcElement.currentTime; + let duration = e.srcElement.duration; + let tgt = document.querySelector("#currentTime"); + + document.querySelector("#currentTime").textContent = mmss(currentTime); + if (duration - currentTime < 20) { + tgt.classList.add("fin"); + } else { + tgt.classList.remove("fin"); + } +} + +function keydown(e) { + let audio = document.querySelector("#audio"); + + switch (event.key) { + case " ": // space bar + if (audio.paused) { + audio.play(); + } else { + audio.pause(); + } + break; + + case "ArrowDown": // Next track + next(); + break; + + case "ArrowUp": // Previous track + prev(); + break; + } +} + +function midiMessage(e) { + let audio = document.querySelector("#audio"); + let data = e.data; + let ctrl = data[1]; + let val = data[2]; + if (data[0] == 176) { + switch (ctrl) { + case 0: // master volume slider + audio.volume = val / 127; + break; + case 41: // play button + if (val == 127) { + audio.play(); + } + break; + case 42: // stop button + if (val == 127) { + audio.pause(); + } + break; + case 58: // prev button + if (val == 127) { + prev(); + } + break; + case 59: // next button + if (val == 127) { + next(); + } + break; + } + } +} + +function handleMidiAccess(access) { + for (let input of access.inputs.values()) { + input.addEventListener("midimessage", midiMessage); + } +} + +function run() { + let audio = document.querySelector("#audio"); + + // Set up events: + // - Prev/Next buttons + // - ended / timeupdate events on audio + // - Track items + document.querySelector("#prev").addEventListener("click", prev); + document.querySelector("#next").addEventListener("click", next); + audio.addEventListener("ended", ended); + audio.addEventListener("timeupdate", timeupdate); + for (let li of document.querySelectorAll("#playlist li")) { + li.addEventListener("click", loadTrack); + } + + // Bind keypress events + // - space: play/pause + // + document.addEventListener("keydown", keydown); + + // Load up first track + document.querySelector("#playlist li").classList.add("current"); + prev(); + + navigator.requestMIDIAccess().then(handleMidiAccess); +} + +window.addEventListener("load", run);