···55// This worker is responsible for everything non-UI.
6677importScripts("/brain.js")
88-importScripts("/indexed_db.js")
88+importScripts("/indexed-db.js")
9910101111const app = Elm.Brain.init()
+316
src/Javascript/audio-engine.js
···11+//
22+// Audio engine
33+// ♪(´ε` )
44+//
55+// Creates audio elements and interacts with the Web Audio API.
66+77+88+99+// Audio context
1010+// -------------
1111+1212+let context
1313+1414+if (window.AudioContext) {
1515+ context = new window.AudioContext()
1616+} else if (window.webkitAudioContext) {
1717+ context = new window.webkitAudioContext()
1818+}
1919+2020+2121+2222+// Container for <audio> elements
2323+// ------------------------------
2424+2525+const audioElementsContainer = (() => {
2626+ let c
2727+ let styles =
2828+ [ "height: 0"
2929+ , "width: 0"
3030+ , "visibility: hidden"
3131+ , "pointer-events: none"
3232+ ]
3333+3434+ c = document.createElement("div")
3535+ c.setAttribute("class", "absolute left-0 top-0")
3636+ c.setAttribute("style", styles.join("; "))
3737+3838+ return c
3939+})()
4040+4141+4242+document.body.appendChild(audioElementsContainer)
4343+4444+4545+4646+// Audio nodes
4747+// -----------
4848+// Flow:
4949+// {Input} -> Volume -> Low -> Mid -> High -> {Output}
5050+5151+let volume,
5252+ low,
5353+ mid,
5454+ high
5555+5656+// volume
5757+volume = context.createGain()
5858+volume.gain.value = 1
5959+6060+// biquad filters
6161+low = context.createBiquadFilter()
6262+mid = context.createBiquadFilter()
6363+high = context.createBiquadFilter()
6464+6565+low.type = "lowshelf"
6666+mid.type = "peaking"
6767+high.type = "highshelf"
6868+6969+low.frequency.value = 250
7070+mid.frequency.value = 2750
7171+mid.Q.value = 1
7272+high.frequency.value = 8000
7373+7474+// connect them nodes
7575+volume.connect(low)
7676+low.connect(mid)
7777+mid.connect(high)
7878+high.connect(context.destination)
7979+8080+8181+function determineNodeGainValue(knobType, value) {
8282+ switch (knobType) {
8383+ case "Volume" : return value
8484+ default : return value < 0 ? value * 50 : value * 15
8585+ }
8686+}
8787+8888+8989+9090+// Playback
9191+// --------
9292+9393+function insertTrack(orchestrion, queueItem) {
9494+ if (!queueItem.url) console.error("insertTrack, missing `url`");
9595+ if (!queueItem.track && !queueItem.track.id) console.error("insertTrack, missing `track.id`");
9696+9797+ // Resume audio context if it's suspended
9898+ if (context.resume && context.state !== "running") {
9999+ context.resume()
100100+ }
101101+102102+ // Create audio node
103103+ let audioNode
104104+105105+ audioNode = createAudioElement(orchestrion, queueItem)
106106+ audioNode.context = context.createMediaElementSource(audioNode)
107107+ audioNode.context.connect(volume)
108108+}
109109+110110+111111+function createAudioElement(orchestrion, queueItem) {
112112+ let audio
113113+ let timestampInMilliseconds = Date.now()
114114+115115+ const bind = fn => event => {
116116+ const is = isActiveAudioElement(orchestrion, event.target)
117117+ if (is) fn.call(orchestrion, event)
118118+ }
119119+120120+ const timeUpdateFunc = bind(audioTimeUpdateEvent)
121121+122122+ audio = new window.Audio()
123123+ audio.setAttribute("crossOrigin", "anonymous")
124124+ audio.setAttribute("crossorigin", "anonymous")
125125+ audio.setAttribute("preload", "none")
126126+ audio.setAttribute("src", queueItem.url)
127127+ audio.setAttribute("rel", queueItem.track.id)
128128+ audio.setAttribute("data-timestamp", timestampInMilliseconds)
129129+130130+ audio.crossorigin = "anonymous"
131131+ audio.volume = 1
132132+133133+ audio.addEventListener("error", bind(audioErrorEvent))
134134+ audio.addEventListener("stalled", bind(audioStalledEvent))
135135+136136+ audio.addEventListener("canplay", bind(audioCanPlayEvent))
137137+ audio.addEventListener("ended", bind(audioEndEvent))
138138+ audio.addEventListener("loadstart", bind(audioLoading))
139139+ audio.addEventListener("loadeddata", bind(audioLoaded))
140140+ audio.addEventListener("pause", bind(audioPauseEvent))
141141+ audio.addEventListener("play", bind(audioPlayEvent))
142142+ audio.addEventListener("seeking", bind(audioLoading))
143143+ audio.addEventListener("seeked", bind(audioLoaded))
144144+ audio.addEventListener("timeupdate", timeUpdateFunc)
145145+146146+ audio.load()
147147+148148+ audioElementsContainer.appendChild(audio)
149149+ orchestrion.audio = audio
150150+151151+ return audio
152152+}
153153+154154+155155+156156+// Audio events
157157+// ------------
158158+159159+function audioErrorEvent(event) {
160160+ console.error(`Audio error for '${ audioElementTrackId(event.target) }'`)
161161+162162+ switch (event.target.error.code) {
163163+ case event.target.error.MEDIA_ERR_ABORTED:
164164+ console.error("You aborted the audio playback.")
165165+ break
166166+ case event.target.error.MEDIA_ERR_NETWORK:
167167+ console.error("A network error caused the audio download to fail.")
168168+ break
169169+ case event.target.error.MEDIA_ERR_DECODE:
170170+ console.error("The audio playback was aborted due to a corruption problem or because the video used features your browser did not support.")
171171+172172+ // If this error happens at the end of the track, skip to the next.
173173+ // NOTE: Weird issue with Chrome
174174+ if (event.target.duration && (event.target.currentTime / event.target.duration) > 0.975) {
175175+ console.log("Moving on to the next track.")
176176+ // TODO
177177+ // this.app.ports.activeQueueItemEnded.send(null)
178178+ }
179179+ break
180180+ case event.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
181181+ console.error("The audio not be loaded, either because the server or network failed or because the format is not supported.")
182182+ break
183183+ default:
184184+ console.error("An unknown error occurred.")
185185+ }
186186+}
187187+188188+189189+function audioStalledEvent(event) {
190190+ this.stalledTimeoutId = setTimeout(() => {
191191+ console.error(`Audio stalled for '${ audioElementTrackId(event.target) }'`)
192192+193193+ // TODO:
194194+ // this.app.ports.setStalled.send(true)
195195+ this.unstallTimeoutId = setTimeout(() => {
196196+ // this.app.ports.setStalled.send(false)
197197+ unstallAudio(event.target)
198198+ }, 2500)
199199+ }, 60000)
200200+}
201201+202202+203203+function audioTimeUpdateEvent(event) {
204204+ clearTimeout(this.stalledTimeoutId)
205205+206206+ if (isNaN(event.target.duration) || isNaN(event.target.currentTime)) {
207207+ setProgressBarWidth(0)
208208+ } else if (event.target.duration > 0) {
209209+ setProgressBarWidth(event.target.currentTime / event.target.duration)
210210+ }
211211+}
212212+213213+214214+function audioEndEvent(event) {
215215+ if (this.repeat) {
216216+ event.target.play()
217217+ } else {
218218+ // TODO: this.app.ports.activeQueueItemEnded.send(null)
219219+ }
220220+}
221221+222222+223223+function audioLoading() {
224224+ this.loadingTimeoutId = setTimeout(() => {
225225+ // TODO: this.app.ports.setIsLoading.send(true)
226226+ }, 1750)
227227+}
228228+229229+230230+function audioLoaded() {
231231+ clearTimeout(this.loadingTimeoutId)
232232+ // TODO: this.app.ports.setIsLoading.send(false)
233233+}
234234+235235+236236+function audioPlayEvent(event) {
237237+ // TODO: this.app.ports.setIsPlaying.send(true)
238238+}
239239+240240+241241+function audioPauseEvent(event) {
242242+ // TODO: this.app.ports.setIsPlaying.send(false)
243243+}
244244+245245+246246+let lastSetDuration = 0
247247+248248+249249+function audioCanPlayEvent(event) {
250250+ if (event.target.paused) event.target.play()
251251+ if (event.target.duration != lastSetDuration) {
252252+ // TODO:
253253+ // this.app.ports.setDuration.send(event.target.duration || 0)
254254+ lastSetDuration = event.target.duration
255255+ }
256256+}
257257+258258+259259+260260+// 🖍 Utensils
261261+// -----------
262262+263263+function audioElementTrackId(node) {
264264+ return node ? node.getAttribute("rel") : undefined
265265+}
266266+267267+268268+function isActiveAudioElement(orchestrion, node) {
269269+ if (!orchestrion.activeQueueItem || !node) return false;
270270+ return orchestrion.activeQueueItem.track.id === audioElementTrackId(node)
271271+}
272272+273273+274274+function unstallAudio(node) {
275275+ const time = node.currentTime
276276+277277+ node.load()
278278+ node.currentTime = time
279279+}
280280+281281+282282+283283+// Progress Bar
284284+// ------------
285285+286286+let progressBarNode
287287+288288+function setProgressBarWidth(float) {
289289+ if (!progressBarNode || !progressBarNode.offsetParent) {
290290+ progressBarNode = document.querySelector(".progressBarValue")
291291+ }
292292+293293+ if (progressBarNode) {
294294+ progressBarNode.style.width = (float * 100).toString() + "%"
295295+ }
296296+}
297297+298298+299299+300300+// 💥
301301+// --
302302+// Remove all the audio elements with a timestamp older than the given one.
303303+304304+function removeOlderAudioElements(timestamp) {
305305+ const nodes = audioElementsContainer.querySelectorAll("audio[data-timestamp]")
306306+307307+ nodes.forEach(node => {
308308+ const t = parseInt(node.getAttribute("data-timestamp"), 10)
309309+ if (t >= timestamp) return
310310+311311+ node.context.disconnect()
312312+ node.context = null
313313+314314+ audioElementsContainer.removeChild(node)
315315+ })
316316+}