mirror of
https://github.com/narrrl/pl8list-rewind.git
synced 2026-11-03 16:41:26 +01:00
added volume control to desktop
This commit is contained in:
1 parent
2ddc1b5629
commit
482cfc299b
4 files changed
+80
-4
No files matched your search
+7
-1
@@ -26,6 +26,8 @@
|
|||||||
<symbol id="i-open" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" d="M7 17 17 7M9 7h8v8"/></symbol>
|
<symbol id="i-open" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" d="M7 17 17 7M9 7h8v8"/></symbol>
|
||||||
<symbol id="i-down" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" d="M12 4v16M6 14l6 6 6-6"/></symbol>
|
<symbol id="i-down" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" d="M12 4v16M6 14l6 6 6-6"/></symbol>
|
||||||
<symbol id="i-play" viewBox="0 0 24 24"><path fill="currentColor" d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></symbol>
|
<symbol id="i-play" viewBox="0 0 24 24"><path fill="currentColor" d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></symbol>
|
||||||
|
<symbol id="i-vol" viewBox="0 0 24 24"><path fill="currentColor" d="M4 9h4l5-4.5v15L8 15H4z"/><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M16.5 8.5a5 5 0 0 1 0 7M19 6a8.5 8.5 0 0 1 0 12"/></symbol>
|
||||||
|
<symbol id="i-mute" viewBox="0 0 24 24"><path fill="currentColor" d="M4 9h4l5-4.5v15L8 15H4z"/><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M16.5 9.5l5 5M21.5 9.5l-5 5"/></symbol>
|
||||||
<symbol id="i-pause" viewBox="0 0 24 24"><path fill="currentColor" d="M7 4.5h3.5v15H7zM13.5 4.5H17v15h-3.5z"/></symbol>
|
<symbol id="i-pause" viewBox="0 0 24 24"><path fill="currentColor" d="M7 4.5h3.5v15H7zM13.5 4.5H17v15h-3.5z"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
@@ -37,6 +39,10 @@
|
|||||||
<span id="stats"></span>
|
<span id="stats"></span>
|
||||||
<button id="filters-btn" type="button" aria-controls="filters" aria-expanded="false">filters</button>
|
<button id="filters-btn" type="button" aria-controls="filters" aria-expanded="false">filters</button>
|
||||||
<button id="likes-btn" type="button" aria-controls="likes" aria-expanded="false">likes</button>
|
<button id="likes-btn" type="button" aria-controls="likes" aria-expanded="false">likes</button>
|
||||||
|
<div id="volume" hidden>
|
||||||
|
<button id="mute-btn" type="button" title="mute (m)" aria-label="mute" aria-pressed="false"><svg><use href="#i-vol"/></svg></button>
|
||||||
|
<input id="volume-slider" type="range" min="0" max="1" step="0.01" aria-label="volume">
|
||||||
|
</div>
|
||||||
<button id="help-btn" type="button" title="shortcuts (?)" aria-label="help">?</button>
|
<button id="help-btn" type="button" title="shortcuts (?)" aria-label="help">?</button>
|
||||||
<button id="account-btn" type="button" aria-controls="account" aria-expanded="false" title="account"></button>
|
<button id="account-btn" type="button" aria-controls="account" aria-expanded="false" title="account"></button>
|
||||||
</header>
|
</header>
|
||||||
@@ -183,7 +189,7 @@
|
|||||||
<dt>x</dt><dd>not for me</dd>
|
<dt>x</dt><dd>not for me</dd>
|
||||||
<dt>s</dt><dd>share</dd>
|
<dt>s</dt><dd>share</dd>
|
||||||
<dt>v</dt><dd>liquid / simple visuals</dd>
|
<dt>v</dt><dd>liquid / simple visuals</dd>
|
||||||
<dt>m</dt><dd>mute</dd>
|
<dt>m · + −</dt><dd>mute · volume</dd>
|
||||||
<dt>?</dt><dd>this help</dd>
|
<dt>?</dt><dd>this help</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<p>Drag the liquid artwork sideways to spin it. Lock-screen and headphone buttons work too.</p>
|
<p>Drag the liquid artwork sideways to spin it. Lock-screen and headphone buttons work too.</p>
|
||||||
|
|||||||
+41
-1
@@ -395,6 +395,44 @@ document.addEventListener("pointerdown", (e) => {
|
|||||||
setTimeout(() => { swallowClick = false; }, 500);
|
setTimeout(() => { swallowClick = false; }, 500);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
// Volume ------------------------------------------------------------------
|
||||||
|
|
||||||
|
const volumeEl = $("volume");
|
||||||
|
const muteBtn = $("mute-btn");
|
||||||
|
const volumeSlider = $<HTMLInputElement>("volume-slider");
|
||||||
|
|
||||||
|
function showVolume(): void {
|
||||||
|
const silent = player.muted || player.volume === 0;
|
||||||
|
muteBtn.querySelector("use")!.setAttribute("href", silent ? "#i-mute" : "#i-vol");
|
||||||
|
muteBtn.setAttribute("aria-pressed", String(player.muted));
|
||||||
|
volumeSlider.value = String(player.muted ? 0 : player.volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMuted(m: boolean): void {
|
||||||
|
player.muted = m;
|
||||||
|
// Unmuting at zero would stay silent.
|
||||||
|
if (!m && player.volume === 0) player.volume = 0.5;
|
||||||
|
showVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume(v: number): void {
|
||||||
|
player.volume = v;
|
||||||
|
player.muted = false;
|
||||||
|
showVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS ignores the volume, see player.ts.
|
||||||
|
volumeEl.hidden = !player.canMix;
|
||||||
|
showVolume();
|
||||||
|
muteBtn.addEventListener("click", () => setMuted(!player.muted));
|
||||||
|
volumeSlider.addEventListener("input", () => setVolume(Number(volumeSlider.value)));
|
||||||
|
// Hand the keys back to the feed, or space and the arrows would move the slider.
|
||||||
|
volumeSlider.addEventListener("change", () => volumeSlider.blur());
|
||||||
|
volumeSlider.addEventListener("wheel", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setVolume(player.volume - Math.sign(e.deltaY) * 0.05);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
const showHelp = () => { helpEl.hidden = false; };
|
const showHelp = () => { helpEl.hidden = false; };
|
||||||
const hideHelp = () => { helpEl.hidden = true; };
|
const hideHelp = () => { helpEl.hidden = true; };
|
||||||
$("help-btn").addEventListener("click", showHelp);
|
$("help-btn").addEventListener("click", showHelp);
|
||||||
@@ -620,7 +658,9 @@ document.addEventListener("keydown", (e) => {
|
|||||||
else if (k === "x" && card) dislike(card);
|
else if (k === "x" && card) dislike(card);
|
||||||
else if (k === "s" && card) share(card);
|
else if (k === "s" && card) share(card);
|
||||||
else if (k === "v") { setVisuals(visual.visuals === "liquid" ? "simple" : "liquid"); toast(`${visual.visuals} visuals`); }
|
else if (k === "v") { setVisuals(visual.visuals === "liquid" ? "simple" : "liquid"); toast(`${visual.visuals} visuals`); }
|
||||||
else if (k === "m") { player.muted = !player.muted; toast(player.muted ? "muted" : "sound on"); }
|
else if (k === "m") { setMuted(!player.muted); toast(player.muted ? "muted" : "sound on"); }
|
||||||
|
else if (k === "+" || k === "=") { setVolume(player.volume + 0.1); toast(`volume ${Math.round(player.volume * 100)}%`); }
|
||||||
|
else if (k === "-") { setVolume(player.volume - 0.1); toast(`volume ${Math.round(player.volume * 100)}%`); }
|
||||||
else if (k === "?") { helpEl.hidden ? showHelp() : hideHelp(); }
|
else if (k === "?") { helpEl.hidden ? showHelp() : hideHelp(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -2,7 +2,8 @@
|
|||||||
// crossfade into each other.
|
// crossfade into each other.
|
||||||
//
|
//
|
||||||
// Where the browser allows it, both <audio> elements run through a Web Audio graph:
|
// Where the browser allows it, both <audio> elements run through a Web Audio graph:
|
||||||
// a gain per deck (for the crossfade) into one analyser (for the visuals). iOS is
|
// a gain per deck (for the crossfade) into one analyser (for the visuals), then a
|
||||||
|
// master gain for the volume, so the visuals look the same at any volume. iOS is
|
||||||
// left out: its element volume is read-only and Web Audio stops when the screen
|
// left out: its element volume is read-only and Web Audio stops when the screen
|
||||||
// locks, which would break lock-screen playback. There tracks switch with a cut.
|
// locks, which would break lock-screen playback. There tracks switch with a cut.
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import { type Track, streamUrl } from "./api";
|
|||||||
const CROSSFADE = 0.9; // seconds
|
const CROSSFADE = 0.9; // seconds
|
||||||
const PRELOAD_BUFFER = 6; // seconds buffered of the track after the current one
|
const PRELOAD_BUFFER = 6; // seconds buffered of the track after the current one
|
||||||
const PLAY_BUFFER = 30;
|
const PLAY_BUFFER = 30;
|
||||||
|
const VOLUME_KEY = "pl8inf.volume";
|
||||||
|
|
||||||
type Hls = HlsType;
|
type Hls = HlsType;
|
||||||
type HlsClass = typeof HlsType;
|
type HlsClass = typeof HlsType;
|
||||||
@@ -68,6 +70,8 @@ export class Player {
|
|||||||
private ctx: AudioContext | null = null;
|
private ctx: AudioContext | null = null;
|
||||||
private unlocking: Promise<void> | null = null;
|
private unlocking: Promise<void> | null = null;
|
||||||
private fadeEnd = 0;
|
private fadeEnd = 0;
|
||||||
|
private master: GainNode | null = null;
|
||||||
|
private vol = Number(localStorage.getItem(VOLUME_KEY) ?? 1);
|
||||||
private listeners = new Map<PlayerEvent, Set<() => void>>();
|
private listeners = new Map<PlayerEvent, Set<() => void>>();
|
||||||
analyser: AnalyserNode | null = null;
|
analyser: AnalyserNode | null = null;
|
||||||
/// False on iOS, see the top of this file.
|
/// False on iOS, see the top of this file.
|
||||||
@@ -84,6 +88,8 @@ export class Player {
|
|||||||
return { el, hls: null, gain: null, track: null, wantPlay: false, gen: 0 };
|
return { el, hls: null, gain: null, track: null, wantPlay: false, gen: 0 };
|
||||||
};
|
};
|
||||||
this.decks = [deck(), deck()];
|
this.decks = [deck(), deck()];
|
||||||
|
if (!(this.vol >= 0 && this.vol <= 1)) this.vol = 1;
|
||||||
|
this.volume = this.vol;
|
||||||
for (const d of this.decks) {
|
for (const d of this.decks) {
|
||||||
for (const ev of FORWARDED) {
|
for (const ev of FORWARDED) {
|
||||||
d.el.addEventListener(ev, () => {
|
d.el.addEventListener(ev, () => {
|
||||||
@@ -150,8 +156,10 @@ export class Player {
|
|||||||
const analyser = ctx.createAnalyser();
|
const analyser = ctx.createAnalyser();
|
||||||
analyser.fftSize = 2048;
|
analyser.fftSize = 2048;
|
||||||
analyser.smoothingTimeConstant = 0.2; // features.ts smooths per feature; this only takes the edge off the FFT
|
analyser.smoothingTimeConstant = 0.2; // features.ts smooths per feature; this only takes the edge off the FFT
|
||||||
analyser.connect(ctx.destination);
|
const master = ctx.createGain();
|
||||||
|
analyser.connect(master).connect(ctx.destination);
|
||||||
for (const d of this.decks) {
|
for (const d of this.decks) {
|
||||||
|
d.el.volume = 1; // the master gain takes over
|
||||||
const gain = ctx.createGain();
|
const gain = ctx.createGain();
|
||||||
gain.gain.value = d === this.decks[this.cur] ? 1 : 0;
|
gain.gain.value = d === this.decks[this.cur] ? 1 : 0;
|
||||||
ctx.createMediaElementSource(d.el).connect(gain).connect(analyser);
|
ctx.createMediaElementSource(d.el).connect(gain).connect(analyser);
|
||||||
@@ -159,6 +167,8 @@ export class Player {
|
|||||||
}
|
}
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.analyser = analyser;
|
this.analyser = analyser;
|
||||||
|
this.master = master;
|
||||||
|
this.volume = this.vol;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Play a track, crossfading from the current one when possible.
|
/// Play a track, crossfading from the current one when possible.
|
||||||
@@ -282,4 +292,18 @@ export class Player {
|
|||||||
set muted(m: boolean) {
|
set muted(m: boolean) {
|
||||||
for (const d of this.decks) d.el.muted = m;
|
for (const d of this.decks) d.el.muted = m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 0 to 1, as the slider shows it. Squared on the way out, so it sounds even.
|
||||||
|
/// Without a graph the element volumes carry it; on iOS they are read-only.
|
||||||
|
get volume(): number {
|
||||||
|
return this.vol;
|
||||||
|
}
|
||||||
|
|
||||||
|
set volume(v: number) {
|
||||||
|
this.vol = Math.max(0, Math.min(1, v));
|
||||||
|
localStorage.setItem(VOLUME_KEY, String(this.vol));
|
||||||
|
const out = this.vol * this.vol;
|
||||||
|
if (this.master) this.master.gain.value = out;
|
||||||
|
else for (const d of this.decks) d.el.volume = out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,10 @@ button {
|
|||||||
.logo b { color: var(--accent); }
|
.logo b { color: var(--accent); }
|
||||||
#stats { color: var(--muted); font-size: 0.8rem; }
|
#stats { color: var(--muted); font-size: 0.8rem; }
|
||||||
#help-btn { width: 2.2em; padding: 0.35em 0; }
|
#help-btn { width: 2.2em; padding: 0.35em 0; }
|
||||||
|
#volume { display: flex; align-items: center; gap: 0.3rem; }
|
||||||
|
#mute-btn { width: 2.2em; padding: 0.35em 0; display: grid; place-items: center; }
|
||||||
|
#mute-btn svg { width: 1.1em; height: 1.1em; }
|
||||||
|
#volume-slider { width: 5.5rem; accent-color: var(--accent); cursor: pointer; }
|
||||||
#account-btn { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
#account-btn { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
#filters-btn.active::after {
|
#filters-btn.active::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -699,6 +703,8 @@ button.primary svg { width: 1em; height: 1em; }
|
|||||||
#bar { gap: 0.4rem; padding-left: 0.7rem; padding-right: 0.7rem; }
|
#bar { gap: 0.4rem; padding-left: 0.7rem; padding-right: 0.7rem; }
|
||||||
#bar button { padding: 0.3em 0.7em; font-size: 0.85rem; }
|
#bar button { padding: 0.3em 0.7em; font-size: 0.85rem; }
|
||||||
#help-btn { display: none; }
|
#help-btn { display: none; }
|
||||||
|
/* Phones have volume buttons; the bar has no room. */
|
||||||
|
#volume { display: none; }
|
||||||
#account-btn { max-width: 6.5rem; }
|
#account-btn { max-width: 6.5rem; }
|
||||||
.panel { left: 0.7rem; right: 0.7rem; }
|
.panel { left: 0.7rem; right: 0.7rem; }
|
||||||
#station { place-items: end stretch; padding: 0; }
|
#station { place-items: end stretch; padding: 0; }
|
||||||
|
|||||||
Reference in new issue
Block a user