A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add audio engine

+368 -2
+1 -1
src/Javascript/Workers/brain.js
··· 5 5 // This worker is responsible for everything non-UI. 6 6 7 7 importScripts("/brain.js") 8 - importScripts("/indexed_db.js") 8 + importScripts("/indexed-db.js") 9 9 10 10 11 11 const app = Elm.Brain.init()
+316
src/Javascript/audio-engine.js
··· 1 + // 2 + // Audio engine 3 + // ♪(´ε` ) 4 + // 5 + // Creates audio elements and interacts with the Web Audio API. 6 + 7 + 8 + 9 + // Audio context 10 + // ------------- 11 + 12 + let context 13 + 14 + if (window.AudioContext) { 15 + context = new window.AudioContext() 16 + } else if (window.webkitAudioContext) { 17 + context = new window.webkitAudioContext() 18 + } 19 + 20 + 21 + 22 + // Container for <audio> elements 23 + // ------------------------------ 24 + 25 + const audioElementsContainer = (() => { 26 + let c 27 + let styles = 28 + [ "height: 0" 29 + , "width: 0" 30 + , "visibility: hidden" 31 + , "pointer-events: none" 32 + ] 33 + 34 + c = document.createElement("div") 35 + c.setAttribute("class", "absolute left-0 top-0") 36 + c.setAttribute("style", styles.join("; ")) 37 + 38 + return c 39 + })() 40 + 41 + 42 + document.body.appendChild(audioElementsContainer) 43 + 44 + 45 + 46 + // Audio nodes 47 + // ----------- 48 + // Flow: 49 + // {Input} -> Volume -> Low -> Mid -> High -> {Output} 50 + 51 + let volume, 52 + low, 53 + mid, 54 + high 55 + 56 + // volume 57 + volume = context.createGain() 58 + volume.gain.value = 1 59 + 60 + // biquad filters 61 + low = context.createBiquadFilter() 62 + mid = context.createBiquadFilter() 63 + high = context.createBiquadFilter() 64 + 65 + low.type = "lowshelf" 66 + mid.type = "peaking" 67 + high.type = "highshelf" 68 + 69 + low.frequency.value = 250 70 + mid.frequency.value = 2750 71 + mid.Q.value = 1 72 + high.frequency.value = 8000 73 + 74 + // connect them nodes 75 + volume.connect(low) 76 + low.connect(mid) 77 + mid.connect(high) 78 + high.connect(context.destination) 79 + 80 + 81 + function determineNodeGainValue(knobType, value) { 82 + switch (knobType) { 83 + case "Volume" : return value 84 + default : return value < 0 ? value * 50 : value * 15 85 + } 86 + } 87 + 88 + 89 + 90 + // Playback 91 + // -------- 92 + 93 + function insertTrack(orchestrion, queueItem) { 94 + if (!queueItem.url) console.error("insertTrack, missing `url`"); 95 + if (!queueItem.track && !queueItem.track.id) console.error("insertTrack, missing `track.id`"); 96 + 97 + // Resume audio context if it's suspended 98 + if (context.resume && context.state !== "running") { 99 + context.resume() 100 + } 101 + 102 + // Create audio node 103 + let audioNode 104 + 105 + audioNode = createAudioElement(orchestrion, queueItem) 106 + audioNode.context = context.createMediaElementSource(audioNode) 107 + audioNode.context.connect(volume) 108 + } 109 + 110 + 111 + function createAudioElement(orchestrion, queueItem) { 112 + let audio 113 + let timestampInMilliseconds = Date.now() 114 + 115 + const bind = fn => event => { 116 + const is = isActiveAudioElement(orchestrion, event.target) 117 + if (is) fn.call(orchestrion, event) 118 + } 119 + 120 + const timeUpdateFunc = bind(audioTimeUpdateEvent) 121 + 122 + audio = new window.Audio() 123 + audio.setAttribute("crossOrigin", "anonymous") 124 + audio.setAttribute("crossorigin", "anonymous") 125 + audio.setAttribute("preload", "none") 126 + audio.setAttribute("src", queueItem.url) 127 + audio.setAttribute("rel", queueItem.track.id) 128 + audio.setAttribute("data-timestamp", timestampInMilliseconds) 129 + 130 + audio.crossorigin = "anonymous" 131 + audio.volume = 1 132 + 133 + audio.addEventListener("error", bind(audioErrorEvent)) 134 + audio.addEventListener("stalled", bind(audioStalledEvent)) 135 + 136 + audio.addEventListener("canplay", bind(audioCanPlayEvent)) 137 + audio.addEventListener("ended", bind(audioEndEvent)) 138 + audio.addEventListener("loadstart", bind(audioLoading)) 139 + audio.addEventListener("loadeddata", bind(audioLoaded)) 140 + audio.addEventListener("pause", bind(audioPauseEvent)) 141 + audio.addEventListener("play", bind(audioPlayEvent)) 142 + audio.addEventListener("seeking", bind(audioLoading)) 143 + audio.addEventListener("seeked", bind(audioLoaded)) 144 + audio.addEventListener("timeupdate", timeUpdateFunc) 145 + 146 + audio.load() 147 + 148 + audioElementsContainer.appendChild(audio) 149 + orchestrion.audio = audio 150 + 151 + return audio 152 + } 153 + 154 + 155 + 156 + // Audio events 157 + // ------------ 158 + 159 + function audioErrorEvent(event) { 160 + console.error(`Audio error for '${ audioElementTrackId(event.target) }'`) 161 + 162 + switch (event.target.error.code) { 163 + case event.target.error.MEDIA_ERR_ABORTED: 164 + console.error("You aborted the audio playback.") 165 + break 166 + case event.target.error.MEDIA_ERR_NETWORK: 167 + console.error("A network error caused the audio download to fail.") 168 + break 169 + case event.target.error.MEDIA_ERR_DECODE: 170 + console.error("The audio playback was aborted due to a corruption problem or because the video used features your browser did not support.") 171 + 172 + // If this error happens at the end of the track, skip to the next. 173 + // NOTE: Weird issue with Chrome 174 + if (event.target.duration && (event.target.currentTime / event.target.duration) > 0.975) { 175 + console.log("Moving on to the next track.") 176 + // TODO 177 + // this.app.ports.activeQueueItemEnded.send(null) 178 + } 179 + break 180 + case event.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED: 181 + console.error("The audio not be loaded, either because the server or network failed or because the format is not supported.") 182 + break 183 + default: 184 + console.error("An unknown error occurred.") 185 + } 186 + } 187 + 188 + 189 + function audioStalledEvent(event) { 190 + this.stalledTimeoutId = setTimeout(() => { 191 + console.error(`Audio stalled for '${ audioElementTrackId(event.target) }'`) 192 + 193 + // TODO: 194 + // this.app.ports.setStalled.send(true) 195 + this.unstallTimeoutId = setTimeout(() => { 196 + // this.app.ports.setStalled.send(false) 197 + unstallAudio(event.target) 198 + }, 2500) 199 + }, 60000) 200 + } 201 + 202 + 203 + function audioTimeUpdateEvent(event) { 204 + clearTimeout(this.stalledTimeoutId) 205 + 206 + if (isNaN(event.target.duration) || isNaN(event.target.currentTime)) { 207 + setProgressBarWidth(0) 208 + } else if (event.target.duration > 0) { 209 + setProgressBarWidth(event.target.currentTime / event.target.duration) 210 + } 211 + } 212 + 213 + 214 + function audioEndEvent(event) { 215 + if (this.repeat) { 216 + event.target.play() 217 + } else { 218 + // TODO: this.app.ports.activeQueueItemEnded.send(null) 219 + } 220 + } 221 + 222 + 223 + function audioLoading() { 224 + this.loadingTimeoutId = setTimeout(() => { 225 + // TODO: this.app.ports.setIsLoading.send(true) 226 + }, 1750) 227 + } 228 + 229 + 230 + function audioLoaded() { 231 + clearTimeout(this.loadingTimeoutId) 232 + // TODO: this.app.ports.setIsLoading.send(false) 233 + } 234 + 235 + 236 + function audioPlayEvent(event) { 237 + // TODO: this.app.ports.setIsPlaying.send(true) 238 + } 239 + 240 + 241 + function audioPauseEvent(event) { 242 + // TODO: this.app.ports.setIsPlaying.send(false) 243 + } 244 + 245 + 246 + let lastSetDuration = 0 247 + 248 + 249 + function audioCanPlayEvent(event) { 250 + if (event.target.paused) event.target.play() 251 + if (event.target.duration != lastSetDuration) { 252 + // TODO: 253 + // this.app.ports.setDuration.send(event.target.duration || 0) 254 + lastSetDuration = event.target.duration 255 + } 256 + } 257 + 258 + 259 + 260 + // 🖍 Utensils 261 + // ----------- 262 + 263 + function audioElementTrackId(node) { 264 + return node ? node.getAttribute("rel") : undefined 265 + } 266 + 267 + 268 + function isActiveAudioElement(orchestrion, node) { 269 + if (!orchestrion.activeQueueItem || !node) return false; 270 + return orchestrion.activeQueueItem.track.id === audioElementTrackId(node) 271 + } 272 + 273 + 274 + function unstallAudio(node) { 275 + const time = node.currentTime 276 + 277 + node.load() 278 + node.currentTime = time 279 + } 280 + 281 + 282 + 283 + // Progress Bar 284 + // ------------ 285 + 286 + let progressBarNode 287 + 288 + function setProgressBarWidth(float) { 289 + if (!progressBarNode || !progressBarNode.offsetParent) { 290 + progressBarNode = document.querySelector(".progressBarValue") 291 + } 292 + 293 + if (progressBarNode) { 294 + progressBarNode.style.width = (float * 100).toString() + "%" 295 + } 296 + } 297 + 298 + 299 + 300 + // 💥 301 + // -- 302 + // Remove all the audio elements with a timestamp older than the given one. 303 + 304 + function removeOlderAudioElements(timestamp) { 305 + const nodes = audioElementsContainer.querySelectorAll("audio[data-timestamp]") 306 + 307 + nodes.forEach(node => { 308 + const t = parseInt(node.getAttribute("data-timestamp"), 10) 309 + if (t >= timestamp) return 310 + 311 + node.context.disconnect() 312 + node.context = null 313 + 314 + audioElementsContainer.removeChild(node) 315 + }) 316 + }
+50 -1
src/Javascript/index.js
··· 31 31 // Audio 32 32 // ----- 33 33 34 - // TODO 34 + const orchestrion = { 35 + activeQueueItem: null, 36 + app: app, 37 + repeat: false 38 + } 39 + 40 + 41 + // app.ports.activeQueueItemChanged.subscribe(item => { 42 + // const timestampInMilliseconds = Date.now() 43 + // 44 + // orchestrion.activeQueueItem = item 45 + // orchestrion.audio = null 46 + // 47 + // removeOlderAudioElements(timestampInMilliseconds) 48 + // 49 + // if (item) { 50 + // insertTrack(orchestrion, item) 51 + // } else { 52 + // app.ports.setIsPlaying.send(false) 53 + // setProgressBarWidth(0) 54 + // } 55 + // }) 56 + // 57 + // 58 + // app.ports.play.subscribe(_ => { 59 + // if (orchestrion.audio) orchestrion.audio.play() 60 + // }) 61 + // 62 + // 63 + // app.ports.pause.subscribe(_ => { 64 + // if (orchestrion.audio) orchestrion.audio.pause() 65 + // }) 66 + // 67 + // 68 + // app.ports.seek.subscribe(percentage => { 69 + // const audio = orchestrion.audio 70 + // 71 + // if (audio && !isNaN(audio.duration)) { 72 + // audio.currentTime = audio.duration * percentage 73 + // if (audio.paused) audio.pause() 74 + // } 75 + // }) 76 + // 77 + // 78 + // app.ports.unstall.subscribe(_ => { 79 + // if (orchestrion.audio) { 80 + // clearTimeout(orchestrion.unstallTimeoutId) 81 + // unstallAudio(orchestrion.audio) 82 + // } 83 + // })
src/Javascript/indexed_db.js src/Javascript/indexed-db.js
+1
src/Static/Html/Application.html
··· 65 65 66 66 <!-- Scripts --> 67 67 <script src="application.js"></script> 68 + <script src="audio-engine.js"></script> 68 69 69 70 <!-- Initialize Boot Procedure --> 70 71 <script src="index.js"></script>