diff --git a/package.json b/package.json
index 3f7796aa..974381bf 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "chorus",
"private": true,
"type": "module",
- "version": "1.16.2",
+ "version": "1.17.0",
"scripts": {
"build": "rollup -c",
"watch": "rollup -c -w"
diff --git a/src/actions/init.js b/src/actions/init.js
index 872b2ab7..0247a39d 100644
--- a/src/actions/init.js
+++ b/src/actions/init.js
@@ -5,10 +5,11 @@ import App from '../models/app.js'
async function load() {
await store.populate()
- const app = new App(spotifyVideo.element)
+
+ const app = new App({ video: spotifyVideo.element, reverb: spotifyVideo.reverb })
const enabled = JSON.parse(sessionStorage.getItem('enabled') ?? 'true')
- enabled ? await app.connect() : app.disconnect()
+ enabled ? app.connect() : app.disconnect()
spotifyVideo.element.active = enabled
document.addEventListener('app.enabled', async e => {
@@ -19,7 +20,7 @@ async function load() {
sessionStorage.setItem('enabled', enabled.newValue)
spotifyVideo.element.active = enabled.newValue
- enabled.newValue ? await app.connect() : app.disconnect()
+ enabled.newValue ? app.connect() : app.disconnect()
})
document.addEventListener('app.device_id', async e => {
diff --git a/src/actions/overload.js b/src/actions/overload.js
index 3e099477..f97e4358 100644
--- a/src/actions/overload.js
+++ b/src/actions/overload.js
@@ -1,53 +1,32 @@
+import Reverb from '../models/reverb/reverb.js'
import VideoElement from '../models/video/video.js'
class SpotifyVideo {
- #video
- #tries = 0
- #originalCreateElement = document.createElement
-
constructor() {
- this.#overloadCreateElement()
+ this._video
+ this._reverb
+ this._originalCreateElement = document.createElement
this.#init()
}
- #overloadCreateElement() {
+ #init() {
const self = this
document.createElement = function (tagName) {
- const element = self.#originalCreateElement.apply(this, arguments)
+ const element = self._originalCreateElement.apply(this, arguments)
if (tagName === 'video') {
- self.#video = new VideoElement(element)
-
- document.createElement = self.#originalCreateElement
+ self._reverb = new Reverb(element)
+ self._video = new VideoElement({ video: element, reverb: self._reverb })
+ document.createElement = self._originalCreateElement
}
return element
}
}
- #init = () => {
- try {
- this.#tries++
- this.#checkForMainEl()
- } catch {
- if (this.#tries <= 20) {
- setTimeout(this.#init, 500)
- return
- }
- }
- }
-
- #checkForMainEl() {
- const mainEl = document.getElementById('main')
+ get element() { return this._video }
- if (mainEl === null) {
- throw new Error('Main container element not found')
- }
- }
-
- get element() {
- return this.#video
- }
+ get reverb() { return this._reverb }
}
export const spotifyVideo = new SpotifyVideo()
diff --git a/src/background.js b/src/background.js
index 38f198f9..62182fc0 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,8 +1,8 @@
import { setState, getState } from './utils/state.js'
import { getActiveTab, sendMessage } from './utils/messaging.js'
-import { playSharedTrack } from './services/player.js'
import { createArtistDiscoPlaylist } from './services/artist-disco.js'
+import { playSharedTrack, seekTrackToPosition } from './services/player.js'
let ENABLED = true
let popupPort = null
@@ -79,20 +79,22 @@ chrome.webRequest.onBeforeSendHeaders.addListener(details => {
['requestHeaders']
)
-chrome.runtime.onMessage.addListener(({ key, data }, _, sendResponse) => {
- switch (key) {
- case 'artist.disco':
- createArtistDiscoPlaylist(data)
- .then(result => sendResponse({ state: 'completed', data: result }))
- .catch(error => sendResponse({ state: 'error', error: error.message }))
- return true
- case 'play.shared':
- playSharedTrack(data)
- .then(result => sendResponse({ state: 'completed', data: result }))
- .catch(error => sendResponse({ state: 'error', error: error.message }))
-
- return true
+function promiseHandler(promise, sendResponse) {
+ promise.then(result => sendResponse({ state: 'completed', data: result }))
+ .catch(error => sendResponse({ state: 'error', error: error.message }))
+}
+
+chrome.runtime.onMessage.addListener(({ key, values }, _, sendResponse) => {
+ const messageHandler = {
+ 'play.shared': playSharedTrack,
+ 'play.seek': seekTrackToPosition,
+ 'artist-disco': createArtistDiscoPlaylist,
}
+ const handlerFn = messageHandler[key]
+ if (!handlerFn) return
+
+ promiseHandler(handlerFn(values), sendResponse)
+ return true
})
chrome.commands.onCommand.addListener(async command => {
diff --git a/src/components/effects/effects-button.js b/src/components/effects/effects-button.js
new file mode 100644
index 00000000..782750d3
--- /dev/null
+++ b/src/components/effects/effects-button.js
@@ -0,0 +1,8 @@
+import { createTextButton } from '../text-button.js'
+
+export const createEffectsButtons = () => `
+
+ ${createTextButton({ text: 'reset', id: 'effects-reset' })}
+ ${createTextButton({ text: 'save', id: 'effects-save' })}
+
+`
diff --git a/src/components/effects/effects-controls.js b/src/components/effects/effects-controls.js
new file mode 100644
index 00000000..7ad8649a
--- /dev/null
+++ b/src/components/effects/effects-controls.js
@@ -0,0 +1,20 @@
+import { createEffectsButtons } from './effects-button.js'
+import { createEffectsSelector } from './effects-selector.js'
+import { convolverPresets, drinkPresets } from '../../lib/reverb/presets.js'
+
+export const createEffectsControls = () => `
+
${createHeaderButton({ role: 'snip', ariaLabel: 'Snip Controls', additionalStyles: 'background-color:green;' })}
${createHeaderButton({ role: 'speed', ariaLabel: 'Speed Controls', additionalStyles: 'margin-left:.5rem;' })}
+ ${createHeaderButton({ role: 'fx', ariaLabel: 'FX Controls', additionalStyles: 'margin-left:.5rem;' })}
${createHeaderButton({ role: 'seek', ariaLabel: 'Seek Controls', additionalStyles: 'margin:0 .5rem;' })}
diff --git a/src/components/text-button.js b/src/components/text-button.js
index f29618fa..ade9bd1c 100644
--- a/src/components/text-button.js
+++ b/src/components/text-button.js
@@ -1,10 +1,14 @@
-export const createTextButton = ({ id, text, style }) => `
-
-
-
+
`
diff --git a/src/content.js b/src/content.js
index b09f81de..07e79e15 100644
--- a/src/content.js
+++ b/src/content.js
@@ -5,17 +5,16 @@ const loadScript = filePath => {
const script = document.createElement('script')
script.src = chrome.runtime.getURL(filePath)
script.type = 'module'
- document.body.appendChild(script)
+ document.head.appendChild(script)
+ script.onload = () => { script.remove() }
}
loadScript('actions/init.js')
+sessionStorage.setItem('soundsDir', chrome.runtime.getURL('/lib/sounds/'))
+sessionStorage.setItem('reverbPath', chrome.runtime.getURL('/lib/reverb/reverb.js'))
const sendEventToPage = ({ eventType, detail }) => {
- window.postMessage({
- type: 'FROM_CONTENT_SCRIPT',
- requestType: eventType,
- payload: detail
- }, window.location.origin)
+ window.postMessage({ type: 'FROM_CONTENT_SCRIPT', requestType: eventType, payload: detail }, window.location.origin)
}
window.addEventListener('message', async (event) => {
@@ -23,37 +22,21 @@ window.addEventListener('message', async (event) => {
if (event.data.type !== 'FROM_PAGE_SCRIPT') return
const { requestType, payload } = event.data
- let response
-
- switch (requestType) {
- case 'artist.disco':
- response = await sendBackgroundMessage({ key: payload.key, data: payload.values })
- sendEventToPage({ eventType: 'artist.disco.response', detail: response })
- break
- case 'play.shared':
- response = await sendBackgroundMessage({ key: payload.key, data: payload.values })
- sendEventToPage({ eventType: 'play.shared.response', detail: response })
- break
- case 'storage.set':
- const { key, values } = payload
- response = await setState({ key, values })
- sendEventToPage({ eventType: 'storage.set.response', detail: response })
- break
-
- case 'storage.get':
- response = await getState(payload?.key)
- sendEventToPage({ eventType: 'storage.get.response', detail: response })
- break
-
- case 'storage.delete':
- await removeState(payload.key)
- break
-
- case 'storage.populate':
- response = await getState(null)
- sendEventToPage({ eventType: 'storage.populate.response', detail: response })
- break
- }
+ const messageHandlers = {
+ 'play.seek': sendBackgroundMessage,
+ 'play.shared': sendBackgroundMessage,
+ 'artist.disco': sendBackgroundMessage,
+ 'storage.populate': () => getState(null),
+ 'storage.get': ({ key }) => getState(key),
+ 'storage.delete': ({ key }) => removeState(key),
+ 'storage.set': ({ key, values }) => setState({ key, values }),
+ };
+
+ const handlerFn = messageHandlers[requestType]
+ if (!handlerFn) return
+
+ const response = await handlerFn(payload)
+ sendEventToPage({ eventType: `${requestType}.response`, detail: response })
})
chrome.runtime.onMessage.addListener(message => {
diff --git a/src/events/listeners/action-listeners.js b/src/events/listeners/action-listeners.js
index f1d04dfb..8ee5995d 100644
--- a/src/events/listeners/action-listeners.js
+++ b/src/events/listeners/action-listeners.js
@@ -9,63 +9,66 @@ export default class ActionListeners extends Listeners {
init() {
if (this._setup) return
+ this.#resetSeekListener()
this.#saveSeekListener()
- this.#saveTrackListener()
- this.#saveSpeedListener()
this.#shareTrackListener()
this.#deleteTrackListener()
+ this.#saveTrackListener()
+
this.#resetSpeedListener()
+ this.#saveSpeedListener()
+
+ this.#resetReverbListener()
+ this.#saveReverbListener()
this._setup = true
}
#resetSpeedListener() {
const resetButton = document.getElementById('chorus-speed-reset-button')
- resetButton?.addEventListener('click', async () => {
- await this._speed.reset()
- })
+ resetButton?.addEventListener('click', async () => await this._speed.reset())
}
#deleteTrackListener() {
const deleteButton = document.getElementById('chorus-snip-remove-button')
deleteButton?.addEventListener('click', async () => {
- await this._snip.delete()
- this._hide()
+ await this._snip.delete(); this._hide()
})
}
#saveTrackListener() {
const saveButton = document.getElementById('chorus-snip-save-button')
- saveButton?.addEventListener('click', async () => {
- await this._snip.save()
- this._hide()
- })
+ saveButton?.addEventListener('click', async () => await this._snip.save())
}
#saveSpeedListener() {
const speedSaveButton = document.getElementById('chorus-speed-save-button')
- speedSaveButton?.addEventListener('click', async () => {
- await this._speed.save()
- this._hide()
- })
+ speedSaveButton?.addEventListener('click', async () => await this._speed.save())
+ }
+
+ #saveReverbListener() {
+ const reverbSaveButton = document.getElementById('chorus-effects-save-button')
+ reverbSaveButton?.addEventListener('click', async () => await this._reverb.saveSelection())
+ }
+
+ #resetReverbListener() {
+ const reverbResetButton = document.getElementById('chorus-effects-reset-button')
+ reverbResetButton?.addEventListener('click', async () => await this._reverb.clearReverb())
}
#saveSeekListener() {
const seekSaveButton = document.getElementById('chorus-seek-save-button')
- seekSaveButton?.addEventListener('click', async () => {
- await this._seek.save()
- this._hide()
- })
+ seekSaveButton?.addEventListener('click', async () => await this._seek.save())
}
- #handleShare() {
- this._snip.share()
- this._hide()
+ #resetSeekListener() {
+ const seekResetButton = document.getElementById('chorus-seek-reset-button')
+ seekResetButton?.addEventListener('click', async () => await this._seek.reset())
}
#shareTrackListener() {
const shareButton = document.getElementById('chorus-snip-share-button')
- shareButton?.addEventListener('click', () => this.#handleShare())
+ shareButton?.addEventListener('click', () => this._snip.share())
}
}
diff --git a/src/events/listeners/header-listeners.js b/src/events/listeners/header-listeners.js
index b62514c0..d91dfae2 100644
--- a/src/events/listeners/header-listeners.js
+++ b/src/events/listeners/header-listeners.js
@@ -6,7 +6,7 @@ export default class HeaderListeners extends Listeners {
this._setup = false
this._viewInFocus = null
- this._VIEWS = ['snip', 'speed', 'seek']
+ this._VIEWS = ['snip', 'speed', 'fx', 'seek']
}
init() {
@@ -15,6 +15,7 @@ export default class HeaderListeners extends Listeners {
this.#snipViewToggle()
this.#seekViewToggle()
this.#speedViewToggle()
+ this.#effectsViewToggle()
this.#closeModalListener()
this._currentView = 'snip'
@@ -22,18 +23,12 @@ export default class HeaderListeners extends Listeners {
}
async hide() {
- if (this._viewInFocus == 'speed') {
- this._speed.clearCurrentSpeed()
- await this._speed.reset()
- }
-
- this._currentView = 'snip'
+ if (this._viewInFocus == 'speed') this._speed.clearCurrentSpeed()
this._hide()
}
#seekViewToggle() {
const seekButton = document.getElementById('chorus-seek-button')
-
seekButton?.addEventListener('click', async () => {
this._currentView = 'seek'
await this._seek.init()
@@ -42,7 +37,6 @@ export default class HeaderListeners extends Listeners {
set _currentView(selectedView) {
this._viewInFocus = selectedView
-
this._VIEWS.forEach(view => {
const viewButton = document.getElementById(`chorus-${view}-button`)
const viewInFocusContainer = document.getElementById(`chorus-${view}-controls`)
@@ -66,6 +60,14 @@ export default class HeaderListeners extends Listeners {
})
}
+ #effectsViewToggle() {
+ const effectsButton = document.getElementById('chorus-fx-button')
+ effectsButton?.addEventListener('click', () => {
+ this._currentView = 'fx'
+ this._reverb.init()
+ })
+ }
+
#closeModalListener() {
const closeButton = document.getElementById('chorus-modal-close-button')
closeButton?.addEventListener('click', async () => { await this.hide() })
diff --git a/src/events/listeners/listeners.js b/src/events/listeners/listeners.js
index 71b9925f..94d87d9a 100644
--- a/src/events/listeners/listeners.js
+++ b/src/events/listeners/listeners.js
@@ -1,6 +1,7 @@
import Seek from '../../models/seek/seek.js'
import Speed from '../../models/speed/speed.js'
import CurrentSnip from '../../models/snip/current-snip.js'
+import ReverbController from '../../models/reverb/reverb-controller.js'
import { spotifyVideo } from '../../actions/overload.js'
export default class Listeners {
@@ -9,6 +10,7 @@ export default class Listeners {
this._seek = new Seek()
this._speed = new Speed()
this._snip = new CurrentSnip(songTracker)
+ this._reverb = new ReverbController()
}
_hide() {
diff --git a/src/lib/reverb/presets.js b/src/lib/reverb/presets.js
new file mode 100644
index 00000000..c8c0bda3
--- /dev/null
+++ b/src/lib/reverb/presets.js
@@ -0,0 +1,23 @@
+const PARAMS = ['preDelay','bandwidth','diffuse','decay','damping','excursion','wet','dry']
+
+const PRESETS = {
+ // preDelay bandwidth diffuse decay damping excursion wet dry
+ demi: [ 1525 , 0.5683 , 0.5 , 0.3226 , 0.6446 , 0 , 0.2921 , 0.4361 ],
+ short: [ 596 , 0.983 , 0.9 , 0.8271 , 0.2975 , 2.8 , 0.1402 , 0.9000 ],
+ tall: [ 0 , 0.9999 , 1 , 0.5 , 0.005 , 16 , 0.6 , 0.3 ],
+ grande: [ 0 , 0.999 , 0.6 , 0.78 , 0.37 , 12 , 0.5015 , 0.5012 ],
+ venti: [ 0 , 0.999 , 0.6 , 0.78 , 0.37 , 12 , 0.715 , 0.394 ],
+ trenta: [ 0 , 0.7011 , 0.7331 , 0.88 , 0.35 , 12 , 0.7015 , 0.3012 ],
+ quaranta: [ 0 , 0.999 , 1 , 0.88 , 0.35 , 12 , 0.915 , 0.194 ],
+}
+
+const drinkPresets = ['demi','short','tall','grande','venti','trenta','quaranta' ]
+const convolverPresets = ['diffusor','kick','muffler','telephone']
+
+const getParamsListForEffect = effect => {
+ const effectValues = PRESETS[effect]
+ const paramsList = effectValues.map((value, idx) => ({ name: PARAMS[idx], value }))
+ return paramsList
+}
+
+export { drinkPresets, convolverPresets, getParamsListForEffect }
diff --git a/src/lib/reverb/reverb.js b/src/lib/reverb/reverb.js
new file mode 100644
index 00000000..88492c79
--- /dev/null
+++ b/src/lib/reverb/reverb.js
@@ -0,0 +1,230 @@
+class UXFDReverb extends AudioWorkletProcessor {
+ static get parameterDescriptors() {
+ return [{
+ name: 'preDelay',
+ defaultValue: 0,
+ minValue: 0,
+ maxValue: sampleRate/4-1,
+ automationRate: "k-rate"
+ },{
+ name: 'bandwidth',
+ defaultValue: 0.9999,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ },{
+ name: 'diffuse',
+ defaultValue: 1,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ },{
+ name: 'decay',
+ defaultValue: 0.5,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ },{
+ name: 'damping',
+ defaultValue: 0.005,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ },{
+ name: 'excursion',
+ defaultValue: 16,
+ minValue: 0,
+ maxValue: 32,
+ automationRate: "k-rate"
+ },{
+ name: 'wet',
+ defaultValue: 0.3,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ },{
+ name: 'dry',
+ defaultValue: 0.6,
+ minValue: 0,
+ maxValue: 1,
+ automationRate: "k-rate"
+ }]
+ }
+
+ constructor(options) {
+ super(options);
+
+ this._Delays = [];
+ this._pDLength = sampleRate + (128 - sampleRate%128)
+ this._preDelay = new Float32Array(this._pDLength);
+ this._pDWrite = 0;
+ this._lp1 = 0.0;
+ this._lp2 = 0.0;
+ this._lp3 = 0.0;
+
+ [
+ 0.004771345, 0.003595309, 0.012734787, 0.009307483,
+ 0.022579886, 0.149625349, 0.060481839, 0.1249958 ,
+ 0.030509727, 0.141695508, 0.089244313, 0.106280031
+ ].forEach(x => this.makeDelay(x));
+
+ this._taps = Int16Array.from([
+ 0.008937872, 0.099929438, 0.064278754, 0.067067639, 0.066866033, 0.006283391, 0.035818689,
+ 0.011861161, 0.121870905, 0.041262054, 0.08981553 , 0.070931756, 0.011256342, 0.004065724
+ ], x => Math.round(x * sampleRate));
+ }
+
+ makeDelay(length) {
+ // len, array, write, read
+ let len = Math.round(length * sampleRate);
+ this._Delays.push([ len, new Float32Array(len), len - 1, 0 ]);
+ }
+
+ writeDelay(index, data) {
+ this._Delays[index][1][this._Delays[index][2]] = data;
+ }
+
+ readDelay(index) {
+ return this._Delays[index][1][this._Delays[index][3]];
+ }
+
+ readDelayAt(index, i) {
+ return this._Delays[index][1][(this._Delays[index][3] + i)%this._Delays[index][0]];
+ }
+
+ // readDelayAt Linear Interpolated
+ readDelayLAt(index, i) {
+ let d = this._Delays[index];
+ let curr = d[1][(d[3] + ~~i)%d[0]];
+ return curr + (i-~~i++) * (d[1][(d[3] + ~~i)%d[0]] - curr);
+ }
+
+ readPreDelay(index) {
+ return this._Delays[index][1][this._Delays[index][2]];
+ }
+
+ // Only accepts one input, two channels.
+ // Spits one output, two channels.
+ process(inputs, outputs, parameters) {
+ let pd = ~~parameters.preDelay[0] ,
+ bw = parameters.bandwidth[0] ,
+ fi = parameters.diffuse[0] * 0.75 ,
+ si = parameters.diffuse[0] * 0.625 ,
+ dc = parameters.decay[0] ,
+ ft = parameters.diffuse[0] * 0.76 ,
+ st = Math.min(Math.max(dc + 0.15, 0.25), 0.5),
+ dp = parameters.damping[0] ,
+ ex = parameters.excursion[0] ,
+ we = parameters.wet[0] * 0.6 , // lo and ro are both multiplied by 0.6 anyways
+ dr = parameters.dry[0] ;
+
+ let lIn = inputs[0][0],
+ rIn = inputs[0][1],
+ lOut = outputs[0][0],
+ rOut = outputs[0][1];
+
+ // write to predelay and dry output
+ if (inputs[0].length == 2) {
+ for (let i = 127; i >= 0; i--) {
+ this._preDelay[this._pDWrite + i] =
+ (inputs[0][0][i] + inputs[0][1][i]) * 0.5;
+
+ outputs[0][0][i] = inputs[0][0][i] * dr;
+ outputs[0][1][i] = inputs[0][1][i] * dr;
+ }
+ } else if (inputs[0].length > 0) {
+ this._preDelay.set(inputs[0][0], this._pDWrite);
+ for (let i = 127; i >= 0; i--)
+ outputs[0][0][i] = outputs[0][1][i] = inputs[0][0][i] * dr;
+ } else {
+ this._preDelay.set(new Float32Array(128), this._pDWrite);
+ }
+
+
+ // write to predelay
+ if (inputs[0].length != 0) {
+ this._preDelay.set(inputs[0][0], this._pDWrite);
+ } else {
+ this._preDelay.fill(0, this._pDWrite, this._pDWrite + 128);
+ }
+ // this._preDelay.set(Float32Array.from(inputs[0][0], (n, i) => (n + inputs[0][1][i]) * 0.5), this._pDWrite);
+
+ let i = 0;
+ while (i < 128) {
+ let lo = 0.0,
+ ro = 0.0;
+
+ this._lp1 = this._preDelay[(this._pDLength + this._pDWrite - pd + i)%this._pDLength] * bw + (1 - bw) * this._lp1;
+
+ // Please note: The groupings and formatting below does not bear any useful information about
+ // the topology of the network. I just want orderly looking text.
+
+ // pre
+ this.writeDelay(0, this._lp1 - fi * this.readDelay(0) );
+ this.writeDelay(1, fi * (this.readPreDelay(0) - this.readDelay(1)) + this.readDelay(0) );
+ this.writeDelay(2, fi * this.readPreDelay(1) + this.readDelay(1) - si * this.readDelay(2) );
+ this.writeDelay(3, si * (this.readPreDelay(2) - this.readDelay(3)) + this.readDelay(2) );
+
+ let split = si * this.readPreDelay(3) + this.readDelay(3);
+
+ // 1Hz (footnote 14, pp. 665)
+ let excursion = ex * (1 + Math.cos(currentTime*6.28));
+
+ // left
+ this.writeDelay( 4, split + dc * this.readDelay(11) + ft * this.readDelayLAt(4, excursion) ); // tank diffuse 1
+ this.writeDelay( 5, this.readDelayLAt(4, excursion)- ft * this.readPreDelay(4) ); // long delay 1
+ this._lp2 = (1 - dp) * this.readDelay(5) + dp * this._lp2 ; // damp 1
+ this.writeDelay( 6, dc * this._lp2 - st * this.readDelay(6) ); // tank diffuse 2
+ this.writeDelay( 7, this.readDelay(6) + st * this.readPreDelay(6) ); // long delay 2
+
+ // right
+ this.writeDelay( 8, split + dc * this.readDelay(7) + ft * this.readDelayLAt(8, excursion) ); // tank diffuse 3
+ this.writeDelay( 9, this.readDelayLAt(8, excursion)- ft * this.readPreDelay(8) ); // long delay 3
+ this._lp3 = (1 - dp) * this.readDelay(9) + dp * this._lp3 ; // damper 2
+ this.writeDelay(10, dc * this._lp3 - st * this.readDelay(10) ); // tank diffuse 4
+ this.writeDelay(11, this.readDelay(10) + st * this.readPreDelay(10) ); // long delay 4
+
+ lo = this.readDelayAt( 9, this._taps[0])
+ + this.readDelayAt( 9, this._taps[1])
+ - this.readDelayAt(10, this._taps[2])
+ + this.readDelayAt(11, this._taps[3])
+ - this.readDelayAt( 5, this._taps[4])
+ - this.readDelayAt( 6, this._taps[5])
+ - this.readDelayAt( 7, this._taps[6]);
+
+ ro = this.readDelayAt( 5, this._taps[7])
+ + this.readDelayAt( 5, this._taps[8])
+ - this.readDelayAt( 6, this._taps[9])
+ + this.readDelayAt( 7, this._taps[10])
+ - this.readDelayAt( 9, this._taps[11])
+ - this.readDelayAt(10, this._taps[12])
+ - this.readDelayAt(11, this._taps[13]);
+
+ // write
+ lOut[i] = lo * we;
+ rOut[i] = ro * we;
+
+ if (lIn) {
+ lOut[i] += lIn[i] * dr;
+ rOut[i] += lIn[i] * dr;
+ }
+ // lOut[i] = lIn[i] * dr + lo * we;
+ // rOut[i] = rIn[i] * dr + ro * we;
+
+ i++;
+
+ for (let j = 0; j < this._Delays.length; j++) {
+ let d = this._Delays[j];
+ d[2] = (d[2] + 1) % d[0];
+ d[3] = (d[3] + 1) % d[0];
+ }
+ }
+
+ // Update preDelay index
+ this._pDWrite = (this._pDWrite + 128) % this._pDLength;
+
+ return true;
+ }
+}
+
+registerProcessor('UXFDReverb', UXFDReverb);
diff --git a/src/lib/sounds/diffusor.wav b/src/lib/sounds/diffusor.wav
new file mode 100644
index 00000000..8cc71bc7
Binary files /dev/null and b/src/lib/sounds/diffusor.wav differ
diff --git a/src/lib/sounds/kick.wav b/src/lib/sounds/kick.wav
new file mode 100644
index 00000000..39dc0724
Binary files /dev/null and b/src/lib/sounds/kick.wav differ
diff --git a/src/lib/sounds/muffler.wav b/src/lib/sounds/muffler.wav
new file mode 100644
index 00000000..8c93aac1
Binary files /dev/null and b/src/lib/sounds/muffler.wav differ
diff --git a/src/lib/sounds/telephone.wav b/src/lib/sounds/telephone.wav
new file mode 100644
index 00000000..47c50e7e
Binary files /dev/null and b/src/lib/sounds/telephone.wav differ
diff --git a/src/manifest.chrome.json b/src/manifest.chrome.json
index c972fa5c..771aceca 100644
--- a/src/manifest.chrome.json
+++ b/src/manifest.chrome.json
@@ -2,7 +2,7 @@
"short_name": "Chorus",
"name": "Chorus - Spotify Enhancer",
"description": "Enhance Spotify with controls to save favourite snips, auto-skip tracks, and set global and custom speed. More to come!",
- "version": "1.16.2",
+ "version": "1.17.0",
"manifest_version": 3,
"author": "cdrani",
"action": {
@@ -39,7 +39,9 @@
"stores/*.js",
"actions/*.js",
"data/*.js",
- "services/*.js"
+ "services/*.js",
+ "lib/**/*.js",
+ "lib/sounds/*.wav"
]
}
],
diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json
index 53340d69..3dfc7c0e 100644
--- a/src/manifest.firefox.json
+++ b/src/manifest.firefox.json
@@ -2,7 +2,7 @@
"short_name": "Chorus",
"name": "Chorus - Spotify Enhancer",
"description": "Enhance Spotify with controls to save favourite snips, auto-skip tracks, and set global and custom speed. More to come!",
- "version": "1.16.2",
+ "version": "1.17.0",
"manifest_version": 3,
"author": "cdrani",
"action": {
@@ -39,7 +39,9 @@
"stores/*.js",
"actions/*.js",
"data/*.js",
- "services/*.js"
+ "services/*.js",
+ "lib/**/*.js",
+ "lib/sounds/*.wav"
]
}
],
@@ -90,7 +92,7 @@
"browser_specific_settings": {
"gecko": {
"id": "chorus@cdrani.dev",
- "strict_min_version": "109.0"
+ "strict_min_version": "112.0"
}
}
}
diff --git a/src/models/app.js b/src/models/app.js
index fe7bde43..39139de7 100644
--- a/src/models/app.js
+++ b/src/models/app.js
@@ -10,9 +10,10 @@ import NowPlayingObserver from '../observers/now-playing.js'
import ArtistDiscoObserver from '../observers/artist-disco.js'
export default class App {
- constructor(video) {
+ constructor({ video, reverb }) {
this._store = store
this._video = video
+ this._reverb = reverb
this._active = true
this._intervalId = null
@@ -55,10 +56,13 @@ export default class App {
this._artistDiscoObserver.disconnect()
this.#resetInterval()
+
+ navigator.userAgent.includes('Firefox') && this._reverb.setReverbEffect('none')
}
- async connect() {
+ connect() {
this._active = true
+ this._chorus.init()
this._nowPlayingIcons.placeIcons()
this._trackListObserver.observe()
@@ -75,10 +79,7 @@ export default class App {
if (!this._intervalId) return
const chorus = document.getElementById('chorus')
-
- if (!chorus) {
- this._nowPlayingIcons.placeIcons()
- }
+ if (!chorus) this._nowPlayingIcons.placeIcons()
}, 3000)
}
}
diff --git a/src/models/chorus.js b/src/models/chorus.js
index 5a3a6a9e..ffd56ffe 100644
--- a/src/models/chorus.js
+++ b/src/models/chorus.js
@@ -1,6 +1,7 @@
import { createSnipControls } from '../components/snip/snip-controls.js'
import { createSpeedControls } from '../components/speed/speed-controls.js'
import { createSeekControls } from '../components/seek/seek-controls.js'
+import { createEffectsControls } from '../components/effects/effects-controls.js'
import HeaderListeners from '../events/listeners/header-listeners.js'
import ActionListeners from '../events/listeners/action-listeners.js'
@@ -10,9 +11,13 @@ import { spotifyVideo } from '../actions/overload.js'
export default class Chorus {
constructor(songTracker) {
+ this._songTracker = songTracker
this._video = spotifyVideo.element
- this.headerListeners = new HeaderListeners(songTracker)
- this.actionListeners = new ActionListeners(songTracker)
+ }
+
+ init() {
+ this.headerListeners = new HeaderListeners(this._songTracker)
+ this.actionListeners = new ActionListeners(this._songTracker)
}
get isShowing() {
@@ -43,9 +48,11 @@ export default class Chorus {
const snipControlsEl = parseNodeString(createSnipControls())
const speedControlsEl = parseNodeString(createSpeedControls())
const seekControlsEl = parseNodeString(createSeekControls())
+ const effectsControlsEl = parseNodeString(createEffectsControls())
this.chorusControls.appendChild(snipControlsEl)
this.chorusControls.appendChild(speedControlsEl)
+ this.chorusControls.appendChild(effectsControlsEl)
this.chorusControls.appendChild(seekControlsEl)
}
diff --git a/src/models/reverb/reverb-controller.js b/src/models/reverb/reverb-controller.js
new file mode 100644
index 00000000..bfba3d3e
--- /dev/null
+++ b/src/models/reverb/reverb-controller.js
@@ -0,0 +1,66 @@
+import { store } from '../../stores/data.js'
+import { spotifyVideo } from '../../actions/overload.js'
+import { drinkPresets } from '../../lib/reverb/presets.js'
+
+export default class ReverbController {
+ constructor() {
+ this._store = store
+ this._reverb = spotifyVideo.reverb
+ }
+
+ init() {
+ const effect = this._store.getReverb() ?? 'none'
+ this.#setupEvents(effect)
+ }
+
+ #setupEvents(effect) {
+ const { drinkEffectSelect, convolverEffectSelect, presetSelection } = this.elements
+
+ if (effect == 'none') {
+ this.setValuesToNone()
+ } else {
+ presetSelection.textContent = effect
+ const selectedElement = drinkPresets.includes(effect) ? drinkEffectSelect : convolverEffectSelect
+ selectedElement.value = effect
+ }
+
+ drinkEffectSelect.onchange = async (e) => { await this.handleSelection(e) }
+ convolverEffectSelect.onchange = async (e) => { await this.handleSelection(e) }
+ }
+
+ get elements() {
+ return {
+ presetSelection: document.getElementById('preset-selection'),
+ drinkEffectSelect: document.getElementById('drink-effect-presets'),
+ convolverEffectSelect: document.getElementById('convolver-effect-presets')
+ }
+ }
+
+ async handleSelection(e) {
+ const { target: { value, id } } = e
+ await this._reverb.setReverbEffect(value)
+
+ const { convolverEffectSelect, drinkEffectSelect } = this.elements
+ const nonSelectedElement = id?.startsWith('drink') ? convolverEffectSelect : drinkEffectSelect
+ nonSelectedElement.value = 'none'
+
+ this.elements.presetSelection.textContent = value
+ }
+
+ async saveSelection() {
+ await this._store.saveReverb(this.elements.presetSelection.textContent)
+ }
+
+ setValuesToNone() {
+ const { presetSelection, drinkEffectSelect, convolverEffectSelect } = this.elements
+ presetSelection.textContent = 'none'
+ drinkEffectSelect.value = 'none'
+ convolverEffectSelect.value = 'none'
+ }
+
+ async clearReverb() {
+ this.setValuesToNone()
+ await this.saveSelection()
+ await this._reverb.setReverbEffect('none')
+ }
+}
diff --git a/src/models/reverb/reverb.js b/src/models/reverb/reverb.js
new file mode 100644
index 00000000..44a33f21
--- /dev/null
+++ b/src/models/reverb/reverb.js
@@ -0,0 +1,75 @@
+import { drinkPresets, getParamsListForEffect } from '../../lib/reverb/presets.js'
+
+export default class Reverb {
+ constructor(video) {
+ this._video = video
+ }
+
+ #isDigital(effect) {
+ return drinkPresets.includes(effect)
+ }
+
+ async setReverbEffect(effect) {
+ this.#setup()
+ if (effect == 'none') return this.#disconnect()
+
+ const isDigital = this.#isDigital(effect)
+ await (isDigital ? this.#createDigitalReverb(effect) : this.#createImpulseReverb(effect))
+ if (!isDigital) return
+
+ this.#connect(); this.#applyReverbEffect(effect)
+ }
+
+ #setup() {
+ this._audioContext = this._audioContext ?? new AudioContext({ latencyHint: 'playback' })
+ this._source = this._source ?? this._audioContext.createMediaElementSource(this._video)
+ this._gain = this._gain ?? this._audioContext.createGain()
+ }
+
+ #connect() {
+ this._source.disconnect()
+ this._source.connect(this._gain)
+ this._gain.connect(this._reverb)
+ this._reverb.connect(this._audioContext.destination)
+ }
+
+ async #createDigitalReverb() {
+ const modulePath = sessionStorage.getItem('reverbPath')
+ await this._audioContext.audioWorklet.addModule(modulePath)
+ this._reverb = this._reverb ?? new AudioWorkletNode(
+ this._audioContext, 'UXFDReverb', { channelCountMode: 'explicit', channelCount: 1, outputChannelCount: [2] }
+ )
+ }
+
+ async #createImpulseReverb(effect) {
+ this._convolverNode = this._convolverNode ?? this._audioContext.createConvolver()
+ const soundsDir = sessionStorage.getItem('soundsDir')
+
+ const response = await fetch(`${soundsDir}${effect}.wav`)
+ const arraybuffer = await response.arrayBuffer()
+ this._convolverNode.buffer = await this._audioContext.decodeAudioData(arraybuffer)
+
+ this._source.connect(this._convolverNode)
+ this._convolverNode.connect(this._gain)
+ this._gain.connect(this._audioContext.destination)
+ }
+
+ #disconnect() {
+ this._source?.disconnect()
+ this._source?.connect(this._audioContext.destination)
+ }
+
+ async #applyReverbEffect(effect) {
+ if (effect == 'none') return this.#disconnect()
+
+ try { this.#applyReverbEffectParams(effect) }
+ catch (error) { console.error({ ERROR: error })}
+ }
+
+ #applyReverbEffectParams(effect) {
+ const paramsList = getParamsListForEffect(effect)
+ paramsList.forEach(({ name, value }) => {
+ this._reverb.parameters.get(name).linearRampToValueAtTime(value, this._audioContext.currentTime + 0.195)
+ })
+ }
+}
diff --git a/src/models/seek/seek-controller.js b/src/models/seek/seek-controller.js
index c5f621a9..57e237dd 100644
--- a/src/models/seek/seek-controller.js
+++ b/src/models/seek/seek-controller.js
@@ -1,8 +1,8 @@
export default class SeekController {
- #data
+ constructor() { this._data = null }
init(data) {
- if (!this.#data) {
+ if (!this._data) {
this.#setupEvents()
}
@@ -16,7 +16,7 @@ export default class SeekController {
this.#setCheckedUI(seekChecked)
this.#highlightSeekValue(seekChecked)
- this.#data = data
+ this._data = data
}
#setupEvents() {
@@ -72,7 +72,7 @@ export default class SeekController {
const { checked } = seekCheckbox
this.#setCheckedUI(checked)
- const { shows, global } = this.#data
+ const { shows, global } = this._data
rwInput.value = checked ? shows.rw : global.rw
ffInput.value = checked ? shows.ff : global.ff
diff --git a/src/models/seek/seek-icon.js b/src/models/seek/seek-icon.js
index 9a4ca4a0..3f74a71f 100644
--- a/src/models/seek/seek-icon.js
+++ b/src/models/seek/seek-icon.js
@@ -4,6 +4,8 @@ import { currentData } from '../../data/current.js'
import { songState } from '../../data/song-state.js'
import { parseNodeString } from '../../utils/parser.js'
import { spotifyVideo } from '../../actions/overload.js'
+import { playback } from '../../utils/playback.js'
+import { secondsToTime } from '../../utils/time.js'
export default class SeekIcons {
constructor() {
@@ -49,6 +51,7 @@ export default class SeekIcons {
removeIcons() {
const seekBack = document.getElementById('seek-player-rw-button')
const seekForward = document.getElementById('seek-player-ff-button')
+ if (!seekBack) return
seekBack.style.display = 'none'
seekForward.style.display = 'none'
@@ -123,10 +126,12 @@ export default class SeekIcons {
async #calculateCurrentTime({ role, seekTime }) {
const { startTime, endTime } = await songState()
- const currentTime = this._video.currentTime
+ let currentTime = parseInt(this._video.currentTime, 10)
+ if (currentTime !== playback.current()) currentTime = playback.current()
+
const newTimeFF = Math.min(parseInt(currentTime + seekTime, 10), parseInt(endTime, 10) - 0.5)
const newStartTime = currentTime < parseInt(startTime) ? 0 : startTime
- const newTimeRW = Math.max(parseInt(currentTime - seekTime, 10), parseInt(newStartTime, 10) - 0.5)
+ const newTimeRW = Math.max(parseInt(currentTime - seekTime, 10), parseInt(newStartTime, 10), 0)
return role == 'ff' ? newTimeFF : newTimeRW
}
@@ -138,6 +143,7 @@ export default class SeekIcons {
const newTime = await this.#calculateCurrentTime({ role, seekTime })
this._video.currentTime = newTime
+ document.querySelector('[data-testid="playback-position"]').textContent = secondsToTime(newTime)
}
#setupListeners() {
diff --git a/src/models/seek/seek.js b/src/models/seek/seek.js
index 494e8fdc..940be328 100644
--- a/src/models/seek/seek.js
+++ b/src/models/seek/seek.js
@@ -5,29 +5,20 @@ import { store } from '../../stores/data.js'
import { currentData } from '../../data/current.js'
export default class Seek {
- #store
- #controls
- #seekIcons
-
constructor() {
- this.#store = store
- this.#seekIcons = new SeekIcons()
- this.#controls = new SeekController()
+ this._store = store
+ this._seekIcons = new SeekIcons()
+ this._controls = new SeekController()
}
async init() {
const data = await currentData.getSeekValues()
- this.#controls.init(data)
+ this._controls.init(data)
}
get #inputValues() {
- const { rwInput, ffInput, seekCheckbox } = this.#controls.elements
-
- return {
- rw: rwInput.value,
- ff: ffInput.value,
- updateShows: seekCheckbox.checked
- }
+ const { rwInput, ffInput, seekCheckbox } = this._controls.elements
+ return { rw: rwInput.value, ff: ffInput.value, updateShows: seekCheckbox.checked }
}
async save() {
@@ -35,23 +26,22 @@ export default class Seek {
await this.#saveSeeking({ rw, ff, updateShows })
}
+ async reset() {
+ const { updateShows } = this.#inputValues
+ const seekTimeValue = updateShows ? 15 : 10
+ await this.#saveSeeking({ rw: seekTimeValue, ff: seekTimeValue, updateShows })
+ await this.init()
+ }
+
async #saveSeeking({ rw, ff, updateShows }) {
const { shows, global } = await currentData.getSeekValues()
-
- await this.#store.saveTrack({
+ await this._store.saveTrack({
id: 'chorus-seek',
value: {
- shows: {
- ...shows,
- ...updateShows && { rw, ff }
- },
- global: {
- ...global,
- ...!updateShows && { rw, ff }
- }
+ shows: { ...shows, ...updateShows && { rw, ff } },
+ global: { ...global, ...!updateShows && { rw, ff } }
},
})
-
- await this.#seekIcons.setSeekLabels()
+ await this._seekIcons.setSeekLabels()
}
}
diff --git a/src/models/snip/current-snip.js b/src/models/snip/current-snip.js
index 13b9e12a..4507b9e7 100644
--- a/src/models/snip/current-snip.js
+++ b/src/models/snip/current-snip.js
@@ -1,8 +1,9 @@
import Snip from './snip.js'
+import { currentData } from '../../data/current.js'
+import { spotifyVideo } from '../../actions/overload.js'
+import { playback } from '../../utils/playback.js'
import { currentSongInfo } from '../../utils/song.js'
-import { spotifyVideo } from '../../actions/overload.js'
-import { currentData } from '../../data/current.js'
export default class CurrentSnip extends Snip {
constructor(songTracker) {
@@ -58,8 +59,11 @@ export default class CurrentSnip extends Snip {
}
async delete() {
- await super._delete()
- const updatedValues = await this.read()
+ const track = await this.read()
+ const updatedValues = await this._store.saveTrack({
+ id: currentSongInfo().id,
+ value: { ...track, isSnip: false, startTime: 0, endTime: playback.duration() }
+ })
await this._songTracker.updateCurrentSongData(updatedValues)
}
diff --git a/src/models/snip/snip.js b/src/models/snip/snip.js
index 06c3bb35..e807fe7a 100644
--- a/src/models/snip/snip.js
+++ b/src/models/snip/snip.js
@@ -6,6 +6,7 @@ import SliderControls from '../slider/slider-controls.js'
import { timeToSeconds } from '../../utils/time.js'
import { currentData } from '../../data/current.js'
import { copyToClipBoard } from '../../utils/clipboard.js'
+import { setTrackInfo } from '../../utils/track-info.js'
export default class Snip {
constructor() {
@@ -88,10 +89,6 @@ export default class Snip {
}
_setTrackInfo({ title, artists }) {
- const titleElement = document.getElementById('track-title')
- const artistsElement = document.getElementById('track-artists')
-
- titleElement.textContent = title
- artistsElement.textContent = artists
+ setTrackInfo({ title, artists, chorusView: true })
}
}
diff --git a/src/models/speed/speed.js b/src/models/speed/speed.js
index 8bf0373c..18bab031 100644
--- a/src/models/speed/speed.js
+++ b/src/models/speed/speed.js
@@ -6,28 +6,21 @@ import { currentData } from '../../data/current.js'
import { spotifyVideo } from '../../actions/overload.js'
export default class Speed {
- #store
- #video
- #controls
-
constructor() {
- this.#store = store
- this.#video = spotifyVideo.element
- this.#controls = new RangeSlider(this.#video)
+ this._store = store
+ this._video = spotifyVideo.element
+ this._controls = new RangeSlider(this._video)
}
async init() {
const data = await currentData.getPlaybackValues()
- this.#controls.init(data)
+ this._controls.init(data)
}
- clearCurrentSpeed() {
- this.#video.clearCurrentSpeed()
- }
+ clearCurrentSpeed() { this._video.clearCurrentSpeed() }
get #inputValues() {
- const { input, speedCheckbox, pitchCheckbox } = this.#controls.elements
-
+ const { input, speedCheckbox, pitchCheckbox } = this._controls.elements
return {
playbackRate: input.value,
preservesPitch: pitchCheckbox?.checked,
@@ -36,53 +29,37 @@ export default class Speed {
}
async save() {
- const { playbackRate, settingGlobalSpeed, preservesPitch } = this.#inputValues
- this.#video.currentSpeed = playbackRate
-
- if (settingGlobalSpeed) {
- await this.saveGlobalSpeed({ playbackRate, preservesPitch })
- return
- }
-
- await this.saveTrackSpeed({ playbackRate, preservesPitch })
+ const { playbackRate, preservesPitch } = this.#inputValues
+ await this.#saveSelectedSpeed({ playbackRate, preservesPitch })
}
async saveTrackSpeed({ playbackRate, preservesPitch }) {
const trackInfo = await currentData.readTrack()
+ await this._store.saveTrack({ id: currentData.songId, value: { ...trackInfo, playbackRate, preservesPitch } })
- await this.#store.saveTrack({
- id: currentData.songId,
- value: {
- ...trackInfo,
- playbackRate,
- preservesPitch,
- },
- })
+ this.#updateVideoSpeed({ playbackRate, preservesPitch })
+ }
- this.#video.playbackRate = playbackRate
- this.#video.preservesPitch = preservesPitch
+ #updateVideoSpeed({ playbackRate, preservesPitch }) {
+ this._video.currentSpeed = playbackRate
+ this._video.playbackRate = playbackRate
+ this._video.preservesPitch = preservesPitch
}
async saveGlobalSpeed({ playbackRate, preservesPitch }) {
const globalsInfo = await currentData.readGlobals()
const trackInfo = await currentData.readTrack()
- await this.#store.saveTrack({
- id: 'globals',
- value: {
- ...globalsInfo,
- playbackRate,
- preservesPitch,
- }
- })
+ await this._store.saveTrack({ id: 'globals', value: { ...globalsInfo, playbackRate, preservesPitch } })
+ if (!trackInfo?.playbackRate) this.#updateVideoSpeed({ playbackRate, preservesPitch })
+ }
- if (!trackInfo?.playbackRate) {
- this.#video.playbackRate = playbackRate
- this.#video.preservesPitch = preservesPitch
- }
+ async #saveSelectedSpeed(speedValues) {
+ await (this.#inputValues.settingGlobalSpeed ? this.saveGlobalSpeed(speedValues) : this.saveTrackSpeed(speedValues))
}
async reset() {
+ await this.#saveSelectedSpeed({ playbackRate: 1, preservesPitch: true })
await this.init()
}
}
diff --git a/src/models/video/video.js b/src/models/video/video.js
index 53406784..515deab3 100644
--- a/src/models/video/video.js
+++ b/src/models/video/video.js
@@ -1,30 +1,27 @@
import VideoOverride from './video-override.js'
export default class VideoElement {
- constructor(video) {
+ constructor({ video, reverb }) {
this._video = video
- this._active = sessionStorage.getItem('enabled') == 'true'
+ this._reverb = reverb
this._isEditing = false
- this._video.autoplay = false
-
+ this._video.crossOrigin = 'anonymous'
this._videoOverride = new VideoOverride(this)
}
set active(value) {
this._active = value
- }
+ if (navigator.userAgent.includes('Firefox')) return
- get active() {
- return this._active
+ const effect = sessionStorage.getItem('reverb') ?? 'none'
+ this._reverb.setReverbEffect(value ? effect : 'none')
}
- get isEditing() {
- return this._isEditing
- }
+ get active() { return this._active }
- set isEditing(editing) {
- this._isEditing = editing
- }
+ get isEditing() { return this._isEditing }
+
+ set isEditing(editing) { this._isEditing = editing }
reset() {
this.clearCurrentSpeed()
@@ -32,67 +29,23 @@ export default class VideoElement {
this.preservesPitch = true
}
- get element() {
- return this._video
- }
+ get element() { return this._video }
- set currentTime(value) {
- if (this._video) {
- this._video.currentTime = value
- }
- }
+ set currentTime(value) { if (this._video) this._video.currentTime = value }
- play() {
- this._video.play()
- }
+ get currentTime() { return this._video?.currentTime }
- pause() {
- this._video.pause()
- }
+ clearCurrentSpeed() { this._video.removeAttribute('currentSpeed') }
- get currentTime() {
- return this._video?.currentTime
- }
+ set preservesPitch(value) { if (this._video) this._video.preservesPitch = value }
- clearCurrentSpeed() {
- this._video.removeAttribute('currentSpeed')
- }
+ get currentSpeed() { return this._video.getAttribute('currentSpeed') }
- set preservesPitch(value) {
- if (this._video) {
- this._video.preservesPitch = value
- }
- }
+ set currentSpeed(value) { this._video.setAttribute('currentSpeed', value) }
- get currentSpeed() {
- return this._video.getAttribute('currentSpeed')
- }
-
- set currentSpeed(value) {
- this._video.setAttribute('currentSpeed', value)
- }
+ get preservesPitch() { return this._video ? this._video.preservesPitch : true }
- get preservesPitch() {
- return this._video ? this._video.preservesPitch : true
- }
-
- set playbackRate(value) {
- if (this._active) {
- this._video.playbackRate = { source: 'chorus', value: value }
- } else {
- this._video.playbackRate = value
- }
- }
+ set playbackRate(value) { this._video.playbackRate = this._active ? { source: 'chorus', value } : value }
- get playbackRate() {
- return this._video ? this._video.playbackRate : 1
- }
-
- get volume() {
- return this._video.volume
- }
-
- set volume(value) {
- this._video.volume = value
- }
+ get playbackRate() { return this._video ? this._video.playbackRate : 1 }
}
diff --git a/src/observers/song-tracker.js b/src/observers/song-tracker.js
index fbe8adc3..6c7b2ee8 100644
--- a/src/observers/song-tracker.js
+++ b/src/observers/song-tracker.js
@@ -12,7 +12,7 @@ import Dispatcher from '../events/dispatcher.js'
export default class SongTracker {
constructor() {
this._init = true
- this._playedSnip = false
+ this._reverbSet = false
this._currentSongState = null
this._video = spotifyVideo.element
this._dispatcher = new Dispatcher()
@@ -33,6 +33,13 @@ export default class SongTracker {
})
}
+ async #dispatchSeekToPosition(position) {
+ await this._dispatcher.sendEvent({
+ eventType: 'play.seek',
+ detail: { key: 'play.seek', values: { position } },
+ })
+ }
+
async handleShared(songStateData) {
await this.#applyEffects(songStateData)
const { startTime: position } = songStateData
@@ -51,28 +58,40 @@ export default class SongTracker {
await this.#applyEffects(this._currentSongState)
}
+ get #isPlaying() {
+ const playButton = document.querySelector('[data-testid="control-button-playpause"]')
+ if (!playButton) return false
+
+ return playButton.getAttribute('aria-label') == 'Pause'
+ }
+
get #isLooping() {
const repeatButton = document.querySelector('[data-testid="control-button-repeat"]')
return repeatButton?.getAttribute('aria-label') === 'Disable repeat'
}
- get #muteButton() {
- return document.querySelector('[data-testid="volume-bar-toggle-mute-button"]')
- }
+ get #muteButton() { return document.querySelector('[data-testid="volume-bar-toggle-mute-button"]') }
- get #isMute() {
- return this.#muteButton?.getAttribute('aria-label') == 'Unmute'
- }
+ get #isMute() { return this.#muteButton?.getAttribute('aria-label') == 'Unmute' }
#mute() { if (!this.#isMute) this.#muteButton?.click() }
#unMute() { if (this.#isMute) this.#muteButton?.click() }
- #setupListeners() {
- this._video.element.addEventListener('timeupdate', this.#handleTimeUpdate)
+ get #isFirefox() { return navigator.userAgent.includes('Firefox') }
+
+ async #setReverb () {
+ if (this._reverbSet) return
+
+ const effect = sessionStorage.getItem('reverb') ?? 'none'
+ await spotifyVideo.reverb.setReverbEffect(effect)
+ this._reverbSet = true
}
+ #setupListeners() { this._video.element.addEventListener('timeupdate', this.#handleTimeUpdate) }
+
clearListeners() {
this._video.element.removeEventListener('timeupdate', this.#handleTimeUpdate)
+ this._reverbSet = false
}
async #applyEffects({ isShared, isSnip, playbackRate, preservesPitch }) {
@@ -109,7 +128,7 @@ export default class SongTracker {
const currentPositionTime = parseInt(currentPosition, 10) * 1000
if (parsedStartTime != 0 && currentPositionTime < parsedStartTime) {
- this._video.currentTime = startTime
+ await this.#dispatchSeekToPosition(parsedStartTime)
}
}
@@ -117,15 +136,13 @@ export default class SongTracker {
this._init = false
}
- get #nextButton() {
- return document.querySelector('[data-testid="control-button-skip-forward"]')
- }
+ get #nextButton() { return document.querySelector('[data-testid="control-button-skip-forward"]') }
- get #playbackPosition() {
- return document.querySelector('[data-testid="playback-position"]')
- }
+ get #playbackPosition() { return document.querySelector('[data-testid="playback-position"]') }
+
+ #handleTimeUpdate = async () => {
+ if (this.#isPlaying && this.#isFirefox && !this._reverbSet) await this.#setReverb()
- #handleTimeUpdate = () => {
if (this._video.isEditing) return
if (!this._currentSongState) return
diff --git a/src/popup/index.html b/src/popup/index.html
index fbf46cb6..321d43d2 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -3,6 +3,7 @@
+
diff --git a/src/popup/index.js b/src/popup/index.js
index e89c9e3a..22a04f55 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -1,6 +1,7 @@
import { extToggle } from './toggle.js'
import { createRootContainer } from './ui.js'
+import { setTrackInfo } from '../utils/track-info.js'
import { parseNodeString } from '../utils/parser.js'
import { getState, setState } from '../utils/state.js'
import { getImageBackgroundAndTextColours } from '../utils/image-colours.js'
@@ -20,36 +21,6 @@ PORT.onMessage.addListener(async ({ type, data }) => {
await setCoverImage(data)
})
-function getTextNode({ text, isShortText }) {
- const shortTextHtml = `
${text}
`
- const displayText = isShortText
- ? shortTextHtml
- : `
${shortTextHtml} • ${shortTextHtml} ·
`
-
- return parseNodeString(displayText)
-}
-
-function setNowPlayingTextElement({ element, text, textColour }) {
- const isShortText = text.length < 28
- const textNode = getTextNode({ isShortText, text })
-
- element.replaceChildren(textNode)
- element.style.color = textColour
-
- if (isShortText) return element.classList.remove('marquee')
-
- element.classList.add('marquee')
-}
-
-function setTrackInfo({ title, artists, textColour = '#000' }) {
- if (!title || !artists) return
-
- const { titleElement, artistsElement } = getElements()
-
- setNowPlayingTextElement({ element: titleElement, text: title, textColour })
- setNowPlayingTextElement({ element: artistsElement, text: artists, textColour })
-}
-
async function updatePopupUIState(popupState) {
await setState({ key: 'popup-ui', values: popupState })
}
@@ -91,9 +62,7 @@ function getElements() {
return {
cover: document.getElementById('cover'),
double: document.getElementById('double'),
- chorusPopup: document.getElementById('chorus'),
- titleElement: document.getElementById('track-title'),
- artistsElement: document.getElementById('track-artists'),
+ chorusPopup: document.getElementById('chorus')
}
}
diff --git a/src/popup/styles.css b/src/popup/styles.css
index adfdf2ac..9c298e11 100644
--- a/src/popup/styles.css
+++ b/src/popup/styles.css
@@ -54,34 +54,3 @@ label.chorus-common {
color: #b3b3b3;
align-items: center;
}
-
-.container {
- max-width: 200px;
- overflow: hidden;
- white-space: nowrap;
-}
-
-.marquee {
- white-space: nowrap;
- overflow: hidden;
- display: inline-block;
- animation: marquee 10s linear infinite;
-}
-
-.marquee p {
- display: inline-block;
-}
-
-.track-text {
- font-size: 14px;
- font-weight: 500;
-}
-
-@keyframes marquee {
- 0% {
- transform: translate3d(0, 0, 0);
- }
- 100% {
- transform: translate3d(-50%, 0, 0);
- }
-}
diff --git a/src/popup/ui.js b/src/popup/ui.js
index fbc4a24e..6312628b 100644
--- a/src/popup/ui.js
+++ b/src/popup/ui.js
@@ -6,14 +6,20 @@ const coverImage = () => `
`
+const trackText = id => `
+
+`
+
export const createRootContainer = () => `
diff --git a/src/services/player.js b/src/services/player.js
index f7fb2f51..aa597875 100644
--- a/src/services/player.js
+++ b/src/services/player.js
@@ -20,4 +20,16 @@ function playSharedTrack({ track_id, position }) {
})
}
-export { playSharedTrack }
+
+function seekTrackToPosition({ position }) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const options = await setOptions({ method: 'PUT' })
+ const device_id = await getState('device_id')
+ const url = `${API_URL}/seek?position_ms=${position}&device_id=${device_id}`
+ const response = await request({ url, options })
+ resolve(response)
+ } catch (error) { reject(error) }
+ })
+}
+export { playSharedTrack, seekTrackToPosition }
diff --git a/src/stores/cache.js b/src/stores/cache.js
index 4d3d0a28..3d975579 100644
--- a/src/stores/cache.js
+++ b/src/stores/cache.js
@@ -5,7 +5,9 @@ export default class CacheStore {
}
getKey(key) {
- return JSON.parse(this.#cache.getItem(key))
+ const result = this.#cache.getItem(key)
+ try { return JSON.parse(result) }
+ catch (error) { return result }
}
getValue({ key, value }) {
diff --git a/src/stores/data.js b/src/stores/data.js
index abb066e1..86917f54 100644
--- a/src/stores/data.js
+++ b/src/stores/data.js
@@ -3,7 +3,7 @@ import Dispatcher from '../events/dispatcher.js'
import { currentSongInfo } from '../utils/song.js'
import { playback } from '../utils/playback.js'
-const DO_NOT_INCLUDE = [ 'now-playing', 'device_id', 'auth_token', 'enabled', 'globals', 'chorus-seek']
+const DO_NOT_INCLUDE = ['reverb', 'now-playing', 'device_id', 'auth_token', 'enabled', 'globals', 'chorus-seek']
class DataStore {
#cache
@@ -28,14 +28,10 @@ class DataStore {
value.isSkipped = endTime == 0
}
- this.#cache.update({ key, value: JSON.stringify(value) })
+ this.#cache.update({ key, value: typeof value !== 'string' ? JSON.stringify(value) : value })
})
}
- removeTrack(id) {
- this.#cache.removeKey(id)
- }
-
getTrack({ id, value = {}}) {
const cacheValue = this.#cache.getValue({ key: id, value })
return cacheValue
@@ -55,42 +51,47 @@ class DataStore {
return this.#cache.getKey(id)
}
- async removeTrack(id) {
- await this.#dispatcher.sendEvent({
- eventType: 'storage.delete',
- detail: { key: id },
- })
- }
-
- shouldRemoveTrack({ isSkipped, playbackRate = '1', isSnip }) {
+ #shouldDeleteTrack({ isSkipped, playbackRate = '1', isSnip }) {
if (isSkipped || isSnip || playbackRate != '1') return false
return true
}
+ getReverb() {
+ const cacheValue = this.#cache.getValue({ key: 'reverb', value: 'none' })
+ return cacheValue
+ }
+
+ async saveReverb(effect) {
+ await this.#dispatcher.sendEvent({
+ eventType: 'storage.set',
+ detail: { key: 'reverb', values: effect },
+ })
+
+ this.#cache.update({ key: 'reverb', value: effect })
+ return this.#cache.getKey(id)
+ }
+
async saveTrack({ id, value }) {
let response
- if (this.shouldRemoveTrack(value)) {
- this.removeTrack(id)
+ if (this.#shouldDeleteTrack(value)) {
+ await this.deleteTrack(id)
} else {
response = await this.#dispatcher.sendEvent({
eventType: 'storage.set',
detail: { key: id, values: value },
})
- }
+ }
this.#cache.update({ key: id, value: response ?? value })
return this.#cache.getKey(id)
}
- async deleteTrack({ id, value }) {
+ async deleteTrack(id) {
await this.#dispatcher.sendEvent({
eventType: 'storage.delete',
detail: { key: id },
})
-
- this.#cache.update({ key: id, value: { ...value, isSnip: false } })
- return this.#cache.getKey(id)
}
}
diff --git a/src/styles.css b/src/styles.css
index d9fd7240..1cfd282a 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -56,36 +56,47 @@
width: 320px;
}
-#chorus-settings {
- width: 240px;
- padding: .5rem .75rem;
- background-color: #171717;
+#chorus button,
+#chorus input {
+ border-width: 0 !important;
+ border-style: solid !important;
+ border-color: #e5e7eb;
}
-#chorus-settings input {
- margin-left: .5rem;
- width: 56px;
- border-radius: .125rem;
- padding-left: .125rem;
- padding-right: .125rem;
- text-align: center;
- font-size: .875rem
-}
+.success {
+ background-color: green;
+
+ &:hover {
+ background-color: darkgreen;
+ }
-.chorus-text-button:hover {
- background-color: #9B9B9B;
- cursor: pointer;
+ &:active {
+ background-color: darkolivegreen;
+ }
}
-#chorus button:hover {
- cursor: pointer;
+.danger {
+ background-color: crimson;
+
+ &:hover {
+ background-color: firebrick;
+ }
+
+ &:active {
+ background-color: maroon;
+ }
}
-#chorus button,
-#chorus input {
- border-width: 0 !important;
- border-style: solid !important;
- border-color: #e5e7eb;
+.share {
+ background-color: darkviolet;
+
+ &:hover {
+ background-color: blueviolet;
+ }
+
+ &:active {
+ background-color: rebeccapurple;
+ }
}
.chorus-close-button,
@@ -97,10 +108,6 @@
background-color: unset;
}
-#chorus button:disabled {
- cursor: not-allowed;
-}
-
.chorus-icon-active,
.chorus-icon-active:hover {
color: #2edb64;
@@ -125,7 +132,6 @@
.chorus-text-button {
display: flex;
align-items: center;
- background-color: #535353;
color: #fff;
font-weight: 600;
font-size: .75rem;
@@ -139,6 +145,7 @@ input[type=number]::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #fff;
-moz-appearance: textfield;
-webkit-appearance: textfield;
+ appearance: textfield;
opacity: 1; /* Firefox */
}
@@ -339,3 +346,45 @@ input[type=number]::-webkit-outer-spin-button {
[role=artist-disco][aria-label]:hover:after {
display: inline-block;
}
+
+hr {
+ margin: 0;
+ width: 100%;
+}
+
+.select {
+ background-color: transparent;
+ color: #fff;
+ border: none;
+}
+
+.container {
+ max-width: 282px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.marquee {
+ white-space: nowrap;
+ overflow: hidden;
+ display: inline-block;
+ animation: marquee 10s linear infinite !important;
+}
+
+.marquee p {
+ display: inline-block;
+}
+
+.track-text {
+ font-size: 16px;
+ font-weight: 500;
+}
+
+@keyframes marquee {
+ 0% {
+ transform: translate3d(0, 0, 0);
+ }
+ 100% {
+ transform: translate3d(-50%, 0, 0);
+ }
+}
diff --git a/src/utils/song.js b/src/utils/song.js
index ab2f552a..5ba72d83 100644
--- a/src/utils/song.js
+++ b/src/utils/song.js
@@ -60,15 +60,9 @@ const getTrackId = row => {
}
const getArtists = row => {
- const artistsList = row.querySelectorAll('span > a')
+ const artistsList = row.querySelectorAll('span > div > a')
+ // Here means we are at artist page and can get name from h1
+ if (!artistsList.length) return document.querySelector('span[data-testid="entityTitle"] > h1').textContent
- if (!artistsList.length) {
- // Here means we are at artist page and can get name from h1
- return document.querySelector('span[data-testid="entityTitle"] > h1').textContent
- }
-
- return Array.from(artistsList)
- .filter(artist => artist.href.includes('artist'))
- .map(artist => artist.textContent)
- .join(', ')
+ return Array.from(artistsList).filter(artist => artist.href.includes('artist')).map(artist => artist.textContent).join(', ')
}
diff --git a/src/utils/track-info.js b/src/utils/track-info.js
new file mode 100644
index 00000000..00404cb9
--- /dev/null
+++ b/src/utils/track-info.js
@@ -0,0 +1,34 @@
+import { parseNodeString } from './parser.js'
+
+function getTextNode({ text, isShortText }) {
+ const shortTextHtml = `
${text}
`
+ const displayText = isShortText
+ ? shortTextHtml
+ : `
${shortTextHtml} • ${shortTextHtml} ·
`
+
+ return parseNodeString(displayText)
+}
+
+function setNowPlayingTextElement({ element, text, textColour, chorusView }) {
+ const isShortText = text.length < (chorusView ? 43 : 28)
+ const textNode = getTextNode({ isShortText, text })
+
+ element.replaceChildren(textNode)
+ element.style.color = textColour
+
+ isShortText ? element.classList.remove('marquee') : element.classList.add('marquee')
+}
+
+export function setTrackInfo({ title, artists, textColour = '#000', chorusView = false }) {
+ if (!title || !artists) return
+
+ const titleElement = document.getElementById('track-title')
+ const artistsElement = document.getElementById('track-artists')
+
+ setNowPlayingTextElement({
+ element: titleElement, text: title, textColour: chorusView ? '#fff' : textColour, chorusView
+ })
+ setNowPlayingTextElement({
+ element: artistsElement, text: artists, textColour: chorusView ? '#b3b3b3' : textColour, chorusView
+ })
+}