playlist/playlist.mjs

343 lines
7.1 KiB
JavaScript
Raw Normal View History

let ctx = new AudioContext()
2019-03-16 10:34:15 -06:00
const Millisecond = 1
const Second = 1000 * Millisecond
const Minute = 60 * Second
2018-03-18 12:09:05 -06:00
class Track {
constructor() {
this.startedAt = 0
this.pausedAt = 0
window.track = this
}
2023-03-12 10:40:47 -06:00
async Load(url) {
this.filename = url.split("/").pop()
let resp = await fetch(url)
2023-03-12 10:40:47 -06:00
if (resp.ok) {
let buf = await resp.arrayBuffer()
this.abuf = await ctx.decodeAudioData(buf)
} else {
let options = {
length: 1,
sampleRate: 3000,
}
this.abuf = new AudioBuffer(options)
}
}
Duration() {
if (this.abuf) {
2023-03-12 10:40:47 -06:00
return this.abuf.duration
}
return 0
}
}
class Playlist {
constructor(base="./music") {
this.base = base
this.list = {}
this.current = null
this.startedAt = 0
this.pausedAt = 0
}
2023-03-12 10:40:47 -06:00
/**
* Preload a track
*
* @param {String} filename
* @returns {Track}
*/
async Add(filename) {
let track = new Track()
this.list[filename] = track
2023-03-12 10:40:47 -06:00
await track.Load(`${this.base}/${filename}`)
return track
}
2023-03-12 10:40:47 -06:00
/**
* Load a track by filename
*
* @param {String} filename
*/
async Load(filename) {
this.Stop()
this.current = this.list[filename]
if (!this.current) {
this.current = await this.add(filename)
}
}
2023-03-12 10:40:47 -06:00
/**
* Returns current position as a percentage (0.0-1.0)
*/
Position() {
let duration = this.Duration()
let pos = 0
if (!duration) {
return 0
}
if (this.startedAt) {
2023-03-12 10:40:47 -06:00
pos = ctx.currentTime - this.startedAt
pos = Math.min(pos, duration)
}
return pos / duration
}
Play(pos=null) {
let offset = this.pausedAt
if (pos) {
offset = this.current.abuf.duration * pos
}
2023-03-12 10:40:47 -06:00
this.Stop()
this.source = new AudioBufferSourceNode(ctx)
this.source.buffer = this.current.abuf
this.source.connect(ctx.destination)
this.source.start(0, offset)
2023-03-12 10:40:47 -06:00
this.startedAt = ctx.currentTime - offset
this.pausedAt = 0
}
2023-03-12 10:40:47 -06:00
Pause() {
let pos = this.CurrentTime()
2023-03-12 10:40:47 -06:00
this.Stop()
this.pausedAt = pos
}
2023-03-12 10:40:47 -06:00
Stop() {
if (this.source) {
this.source.disconnect()
this.source.stop()
}
this.pausedAt = 0
this.startedAt = 0
}
PlayPause() {
if (this.startedAt) {
2023-03-12 10:40:47 -06:00
this.Pause()
} else {
2023-03-12 10:40:47 -06:00
this.Play()
}
}
Seek(pos) {
if (this.startedAt) {
this.play(pos)
} else {
this.pausedAt = this.Duration() * pos
}
}
CurrentTime() {
if (this.startedAt) {
2023-03-12 10:40:47 -06:00
return ctx.currentTime - this.startedAt
}
if (this.pausedAt) {
return this.pausedAt
}
return 0
}
2019-03-16 10:34:15 -06:00
Duration() {
return this.current.Duration()
}
}
let playlist = new Playlist()
window.playlist = playlist
2018-03-18 12:07:08 -06:00
async function loadTrack(e) {
2019-03-16 10:34:15 -06:00
let li = e.target
2023-03-12 10:40:47 -06:00
playlist.Load(li.textContent)
2018-03-18 12:07:08 -06:00
// Update "current"
for (let cur of document.querySelectorAll(".current")) {
2019-03-16 10:34:15 -06:00
cur.classList.remove("current")
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
li.classList.add("current")
2018-03-18 12:07:08 -06:00
}
function clickOn(element) {
let e = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true
2019-03-16 10:34:15 -06:00
})
element.dispatchEvent(e)
2018-03-18 12:07:08 -06:00
}
function prev() {
2019-03-16 10:34:15 -06:00
let cur = document.querySelector(".current")
let prev = cur.previousElementSibling
2018-03-18 12:07:08 -06:00
if (prev) {
2019-03-16 10:34:15 -06:00
cur = prev
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
clickOn(cur)
2018-03-18 12:07:08 -06:00
}
function next() {
2019-03-16 10:34:15 -06:00
let cur = document.querySelector(".current")
let next = cur.nextElementSibling
2018-03-18 12:07:08 -06:00
if (next) {
2019-03-16 10:34:15 -06:00
cur = next
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
clickOn(cur)
2018-03-18 12:07:08 -06:00
}
function ended() {
2019-03-16 10:34:15 -06:00
next()
2018-03-18 12:07:08 -06:00
}
function mmss(duration) {
let mm = Math.floor(duration / Minute)
let ss = Math.floor((duration / Second) % 60)
2018-03-18 12:07:08 -06:00
if (ss < 10) {
2019-03-16 10:34:15 -06:00
ss = "0" + ss
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
return mm + ":" + ss
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
function volumechange(e) {
document.querySelector("#vol").value = e.target.volume
2018-03-18 16:25:32 -06:00
}
function timeupdate() {
2023-03-12 10:40:47 -06:00
let currentTime = playlist.CurrentTime() * Second
let duration = playlist.Duration() * Second
2019-03-16 10:34:15 -06:00
let tgt = document.querySelector("#currentTime")
let pos = document.querySelector("#pos")
pos.value = currentTime / duration
2018-03-18 16:25:32 -06:00
2019-03-16 10:34:15 -06:00
tgt.textContent = mmss(currentTime)
if (duration - currentTime < 20 * Second) {
2019-03-16 10:34:15 -06:00
tgt.classList.add("fin")
2018-03-18 12:07:08 -06:00
} else {
2019-03-16 10:34:15 -06:00
tgt.classList.remove("fin")
2018-03-18 12:07:08 -06:00
}
}
2019-03-16 10:34:15 -06:00
function setPos(e) {
let val = e.target.value
playlist.Seek(val)
2019-03-16 10:34:15 -06:00
}
function setGain(e) {
let val = e.target.value
let audio = document.querySelector("#audio")
audio.volume = val
}
2018-03-18 12:07:08 -06:00
function keydown(e) {
2019-03-16 10:34:15 -06:00
let audio = document.querySelector("#audio")
2018-03-18 12:07:08 -06:00
switch (e.key) {
2018-03-18 12:07:08 -06:00
case " ": // space bar
playlist.PlayPause()
2019-03-16 10:34:15 -06:00
break
2018-03-18 12:07:08 -06:00
case "ArrowDown": // Next track
2019-03-16 10:34:15 -06:00
next()
break
2018-03-18 12:07:08 -06:00
case "ArrowUp": // Previous track
2019-03-16 10:34:15 -06:00
prev()
break
2018-03-18 12:07:08 -06:00
}
}
function midiMessage(e) {
2019-03-16 10:34:15 -06:00
let audio = document.querySelector("#audio")
let data = e.data
let ctrl = data[1]
let val = data[2]
2018-03-18 16:25:32 -06:00
if ((data[0] == 0xb0) || (data[0] == 0xbf)) {
2018-03-18 12:07:08 -06:00
switch (ctrl) {
case 0: // master volume slider
2019-03-16 10:34:15 -06:00
audio.volume = val / 127
document.querySelector("#vol").value = audio.volume
break
2018-03-18 12:07:08 -06:00
case 41: // play button
if (val == 127) {
2019-03-16 10:47:59 -06:00
// The first time, the browser will reject this,
// because it doesn't consider MIDI input user interaction,
// so it looks like an autoplaying video.
2023-03-12 10:40:47 -06:00
playlist.Play()
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
break
2018-03-18 12:07:08 -06:00
case 42: // stop button
if (val == 127) {
2023-03-12 10:40:47 -06:00
playlist.Pause()
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
break
2018-03-18 12:07:08 -06:00
case 58: // prev button
if (val == 127) {
2019-03-16 10:34:15 -06:00
prev()
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
break
2018-03-18 12:07:08 -06:00
case 59: // next button
if (val == 127) {
2019-03-16 10:34:15 -06:00
next()
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
break
2018-03-18 12:07:08 -06:00
}
}
}
function handleMidiAccess(access) {
for (let input of access.inputs.values()) {
2019-03-16 10:34:15 -06:00
input.addEventListener("midimessage", midiMessage)
2018-03-18 12:07:08 -06:00
}
2018-03-18 16:25:32 -06:00
for (let output of access.outputs.values()) {
if (output.name == "nanoKONTROL2 MIDI 1") {
2019-03-16 10:34:15 -06:00
controller = output
2018-03-18 16:25:32 -06:00
output.send([0xf0, 0x42, 0x40, 0x00, 0x01, 0x13, 0x00, 0x00, 0x00, 0x01, 0xf7]); // Native Mode (lets us control LEDs, requires sysex privilege)
output.send([0xbf, 0x2a, 0x7f]); // Stop
output.send([0xbf, 0x29, 0x7f]); // Play
}
}
2018-03-18 12:07:08 -06:00
}
function run() {
2019-03-16 10:34:15 -06:00
let audio = document.querySelector("#audio")
2018-03-18 12:07:08 -06:00
// Set up events:
// - Prev/Next buttons
// - ended / timeupdate events on audio
// - Track items
2019-03-16 10:34:15 -06:00
document.querySelector("#prev").addEventListener("click", prev)
document.querySelector("#next").addEventListener("click", next)
document.querySelector("#pos").addEventListener("input", setPos)
document.querySelector("#vol").addEventListener("input", setGain)
audio.addEventListener("ended", ended)
audio.addEventListener("volumechange", volumechange)
2018-03-18 12:07:08 -06:00
for (let li of document.querySelectorAll("#playlist li")) {
2023-03-12 10:40:47 -06:00
playlist.Add(li.textContent)
2019-03-16 10:34:15 -06:00
li.addEventListener("click", loadTrack)
2018-03-18 12:07:08 -06:00
}
setInterval(() => timeupdate(), 250 * Millisecond)
2018-03-18 12:07:08 -06:00
2019-03-16 10:34:15 -06:00
document.querySelector("#vol").value = audio.volume
2018-03-18 12:07:08 -06:00
// Bind keypress events
// - space: play/pause
//
2019-03-16 10:34:15 -06:00
document.addEventListener("keydown", keydown)
2018-03-18 12:07:08 -06:00
// Load up first track
2019-03-16 10:34:15 -06:00
document.querySelector("#playlist li").classList.add("current")
prev()
2018-03-18 12:07:08 -06:00
2019-03-16 10:34:15 -06:00
navigator.requestMIDIAccess({sysex: true}).then(handleMidiAccess)
2018-03-18 12:07:08 -06:00
}
2019-03-16 10:34:15 -06:00
window.addEventListener("load", run)