V1.3.4: 新增音频均衡器(参考 OpenEQ) + 随机续播改为伪随机公放清单
1) 音频均衡器
- 新增 KtvEqualizer:参考开源项目 OpenEQ(Turbofan3360/OpenEQ, GPL-3.0) 的 EQUtils,
直接用系统 android.media.audiofx.Equalizer, 按设备真实频段数与增益范围自适应
(频段动态生成, 频段标签规则同 OpenEQ: 60/230/910/3.6K ...)。
- 挂载方式改为按"实际播放会话": player.audioSessionId() /
vocalPlayer.audioSessionId / embeddedVocalEngine.audioSessionId()(IJK 原生
IjkMediaPlayer_getAudioSessionId 即真实 AudioTrack 会话)。会话随歌曲变化,
由 500ms lyricTicker -> syncEqualizerSessions() 周期对齐, 消失会话延迟 3s 释放;
设备不支持按会话挂载时可显式开启 globalMix(会话 0, 默认关闭)。
- UI(showEqualizerDialog)与调音弹窗同风格: 深色圆角面板/紫色频段卡/橙色焦点反馈;
滑杆固定 30 档、步进由设备增益范围均分(保证 0 dB 可选中), 5 个预设
标准/流行/抒情/摇滚/唱将, 手动调整后转"自定义"。
- 四个入口: 主页顶栏「调音」弹窗、播放界面「调音」面板、声音设置「均衡器入口」、
远程点歌页面「均衡器」卡片(PATCH /api/v1/player/equalizer, 状态随
GET /api/v1/state 的 equalizer 下发)。
- 设置持久化: KtvStore.eqEnabled/eqPreset/eqLevels/eqGlobalMix。
2) 随机续播改为伪随机公放清单
- 新增 RandomPlayQueue(纯内存): 随机续播一次性生成打乱顺序的临时清单, 一轮内不重复;
播完一首 markFinished +1; next() 每次重新比对曲库, 自动吸纳新增歌曲、剔除已删除歌曲;
一轮播完自动开新一轮(尽量不把刚播完的放开头)。
- 不显示(界面不展示)、不关联已点清单(orderQueue 完全独立, 随机歌曲不入队)、
App 退出即清理(onDestroy 里 isFinishing 时 clear(), 不落盘)。
版本号 1.3.3 -> 1.3.4(versionCode 9 -> 10)。按约定本次不发布 Release, 仅本地打包测试。
This commit is contained in:
@@ -3,7 +3,7 @@ const $$ = (selector) => document.querySelectorAll(selector);
|
||||
const uiState = {
|
||||
page: 1, query: "", searchMode: "singer", busy: false,
|
||||
hotPage: 1, hotPageSize: 10, hotBusy: false,
|
||||
visible: true, connected: false
|
||||
visible: true, connected: false, eqEnabled: false
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
@@ -81,11 +81,75 @@ async function refresh() {
|
||||
$("#original").classList.toggle("active", data.vocalMode === "original");
|
||||
$("#accompany").classList.toggle("active", data.vocalMode !== "original");
|
||||
$("#nextSong").textContent = data.next?.title || "队列为空";
|
||||
renderEqualizer(data.equalizer);
|
||||
} catch (_) {
|
||||
setConnected(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 均衡器(与设备端 KtvEqualizer 同步,频段由设备真实能力决定)───
|
||||
function renderEqualizer(eq) {
|
||||
if (!eq) return;
|
||||
uiState.eqEnabled = !!eq.enabled;
|
||||
const state = $("#eqState");
|
||||
if (state) state.textContent = eq.enabled ? (eq.preset || "自定义") : "已关闭";
|
||||
const toggle = $("#eqToggle");
|
||||
if (toggle) {
|
||||
toggle.textContent = eq.enabled ? "关闭均衡器" : "开启均衡器";
|
||||
toggle.classList.toggle("active", !!eq.enabled);
|
||||
}
|
||||
const hint = $("#eqHint");
|
||||
if (hint) {
|
||||
hint.textContent = eq.globalMix
|
||||
? "当前作用于全局混音(会影响本机所有声音)"
|
||||
: "均衡器作用于本机播放输出,不改动系统音量";
|
||||
}
|
||||
const bands = Array.isArray(eq.bands) ? eq.bands : [];
|
||||
const container = $("#eqBands");
|
||||
if (container && container.childElementCount !== bands.length) {
|
||||
container.innerHTML = bands.map((band, index) => `<label class="eq-band"><span>${escapeHtml(band.label)}Hz</span><b><em id="eqValue${index}">${Number(band.level).toFixed(1)}</em>dB</b><input id="eqBand${index}" type="range" min="${eq.minDb}" max="${eq.maxDb}" step="1" value="${band.level}"></label>`).join("");
|
||||
bands.forEach((band, index) => {
|
||||
const input = $("#eqBand" + index);
|
||||
if (!input) return;
|
||||
input.addEventListener("input", () => {
|
||||
const value = $("#eqValue" + index);
|
||||
if (value) value.textContent = Number(input.value).toFixed(1);
|
||||
});
|
||||
input.addEventListener("change", () => updateEqualizer({ levels: currentEqLevels() }));
|
||||
});
|
||||
} else if (container) {
|
||||
bands.forEach((band, index) => {
|
||||
const input = $("#eqBand" + index);
|
||||
// 正在拖动的那根滑杆不要被轮询覆盖。
|
||||
if (input && document.activeElement !== input) input.value = band.level;
|
||||
const value = $("#eqValue" + index);
|
||||
if (value) value.textContent = Number(band.level).toFixed(1);
|
||||
});
|
||||
}
|
||||
const presets = Array.isArray(eq.presets) ? eq.presets : [];
|
||||
const presetBox = $("#eqPresets");
|
||||
if (presetBox && presetBox.childElementCount !== presets.length) {
|
||||
presetBox.innerHTML = presets.map((name) => `<button type="button" class="eq-preset" data-preset="${escapeHtml(name)}">${escapeHtml(name)}</button>`).join("");
|
||||
presetBox.querySelectorAll(".eq-preset").forEach((button) => {
|
||||
button.addEventListener("click", () => updateEqualizer({ preset: button.dataset.preset }));
|
||||
});
|
||||
}
|
||||
presetBox?.querySelectorAll(".eq-preset").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.preset === eq.preset);
|
||||
});
|
||||
}
|
||||
|
||||
function currentEqLevels() {
|
||||
return Array.from($$("#eqBands input[type=range]")).map((input) => Number(input.value));
|
||||
}
|
||||
|
||||
async function updateEqualizer(values) {
|
||||
try {
|
||||
await api("/api/v1/player/equalizer", { method: "PATCH", body: JSON.stringify(values) });
|
||||
await refresh();
|
||||
} catch (error) { toast(error.message); }
|
||||
}
|
||||
|
||||
async function playerAction(action) {
|
||||
try {
|
||||
await api("/api/v1/player/actions", { method: "POST", body: JSON.stringify({ action }) });
|
||||
@@ -208,6 +272,9 @@ $("#hotNext").addEventListener("click", () => loadHot(uiState.hotPage + 1));
|
||||
// 音乐音量可调; 麦克风音量由功放硬件控制, 不再向设备发送无效果请求。
|
||||
$("#musicVolume").addEventListener("input", (event) => { $("#musicValue").textContent = event.target.value; });
|
||||
$("#musicVolume").addEventListener("change", (event) => updateAudio({ musicVolume: Number(event.target.value) }));
|
||||
// 均衡器入口(与设备端弹窗等价): 开关 / 恢复标准
|
||||
$("#eqToggle")?.addEventListener("click", () => updateEqualizer({ enabled: !uiState.eqEnabled }));
|
||||
$("#eqReset")?.addEventListener("click", () => updateEqualizer({ preset: "标准" }));
|
||||
document.addEventListener("visibilitychange", () => { uiState.visible = !document.hidden; if (uiState.visible) refresh(); });
|
||||
window.setInterval(() => { if (uiState.visible) refresh(); }, 1000);
|
||||
refresh();
|
||||
|
||||
Reference in New Issue
Block a user