本地音频响应式频谱播放器第二版(2月10日更新)
本帖最后由 马黑黑 于 2025-2-10 12:35 编辑 <br /><br /><style>/* 父元素 */
#papa { --bg1: #00ffff; --bg2: #fffafa; margin: 30px auto 0; width: 740px; height: 400px; font-size: 16px; color: var(--bg2); background: linear-gradient(to bottom right, #000, #ffc); box-shadow: 3px 6px 20px #000; border-radius: 6px; display: grid; place-items: center; position: relative; }
/* 顶部区域 */
#openFile { position: absolute; padding: 8px; top: 0; width: 100%; height: 40px; box-sizing: border-box; }
#mfile { display: none; }
#selectSong, #btnSet { position: absolute; border: 2px solid #ccc; border-radius: 6px; outline: none; color: var(--bg2); background: none; cursor: pointer; }
#btnSet { right: 10px; display: none; }
#openFile:hover #btnSet { display: inline-block; }
#selectSong:hover, #btnSet:hover { background: darkred; }
#curSong { position: absolute; left: 100px; color: var(--bg2); }
/* 设置栏 */
#setting { position: absolute; padding: 10px; width: 400px; right: 0; top: 0; color: var(--bg2); background: rgba(0,0,0,.5); display: none; }
/* 背景图地址栏 */
#bgurl { width: 300px; padding: 6px; }
/* 播放器 */
#mplayer { --ppLen: 4px; --prg: 0%; --ppCap: white; position: absolute; right: 180px; width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(to right, var(--bg2) var(--prg), var(--bg2) var(--prg), var(--bg1) 0) no-repeat 0 50% / 100% 2px; cursor: pointer; display: grid; place-items: center; }
#mplayer:hover { filter: hue-rotate(180deg) drop-shadow(0 0 26px black); }
#mplayer::before, #mplayer::after { position: absolute; }
#mplayer::before { content: attr(data-cu); top: 20%; }
#mplayer::after { content: attr(data-du); top: 56%; }
/* 频谱条 */
.pp { position: absolute; left: calc(50% - 2px); bottom: 50%; width: var(--ppLen); height: 20px; background: linear-gradient(to top, var(--bg1), var(--bg2)); transform-origin: 50% 100%; transform: rotate(var(--deg)) translate(-50px, 0); display: grid; place-items: center; }
.pp::after { position: absolute; content: ''; width: calc(var(--ppLen) + 4px); height: calc(var(--ppLen) + 4px); top: 0px; background: var(--bg2); border-radius: 50%; }
/* 音乐列表 */
#mlist { position: absolute; left: 20px; top: 60px; min-width: 40%; max-width: 50%; min-height: 20%; max-height: calc(100% - 80px); font-size: 14px; opacity: .7; overflow: hidden; scrollbar-width: thin; scrollbar-color: var(--bg1) rgba(255,255,255,.2); }
#mlist:hover { overflow: auto; opacity: 1; }
/* 音乐列表子项目 */
.list1, .list2, .list3 { display: inline-block; max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list1 { cursor: pointer; }
.list2 { color: var(--bg1); cursor:default; }
.list3 { width: fit-content; }
.list1:hover { color: var(--bg1); }
.tRight { text-align: right; }
</style>
<div id="papa">
<div id="openFile">
<input id="selectSong" type="button" value="选择音乐" />
<input type="file" id="mfile" accept=".mp3,.ogg,.wav,.acc,.webm,.flac" multiple />
<span id="curSong"></span>
<input id="btnSet" type="button" value="设置" />
</div>
<div id="setting">
<p>
<label for="bgurl">背景图:</label>
<input id="bgurl" type="text" placeholder="输入图片地址" value="" />
</p>
<p>
<label for="tColor">配色一 :</label>
<input type="color" id="tColor1" name="tc" value="#00ffff" />
</p>
<p>
<label for="tColor">配色二 :</label>
<input type="color" id="tColor2" name="tc" value="#ffffff" />
</p>
<p class="tRight">
<input id="btnQuit" type="button" value="退出" />
<input id="btnSave" type="button" value="保存" />
</p>
</div>
<div id="mplayer" class="mplayer"></div>
<div id="mlist"></div>
<audio id="aud"></audio>
</div>
<p style="text-align:center"><br>更新 :2025年2月10日 <br></p>
<script>
//选择的文件, 播放数组, 波形数据, 频谱
let files=[], playAr = [], output = [], pps = [];
//打开文件次数, 频谱条总数
let openIdx = 0, total = 30;
//获取波形数据
const getDatas = () => {
if(openIdx > 0) return;
openIdx ++;
Ac = new AudioContext;
source = Ac.createMediaElementSource(aud);
analyser = Ac.createAnalyser();
source.connect(analyser);
analyser.connect(Ac.destination);
output = new Uint8Array(total);
};
//生成频谱条
Array(total).fill(0).forEach((_, k) => {
let pp = document.createElement('span');
pp.className = 'pp';
pp.style.cssText += `--deg: ${360 / total * k}deg`;
mplayer.appendChild(pp);
pps.push(pp);
});
//波形数据刷新
(function update() {
if(aud.src) analyser.getByteFrequencyData(output);
for(let j = 0; j < total ; j++) {
pps.style.height = output / 2 + 'px';
}
window.requestAnimationFrame(update);
})();
//播放 :idx为空时随机播放
const mplay = (idx = null) => {
if(files.length === 0) return;
let isScrolling = false;
if(idx === null) {
if(playAr.length === 0) playAr = ranNum(files.length);
let tmpIdx = Math.floor(Math.random() * playAr.length);
idx = playAr;
playAr.splice(tmpIdx, 1);
isScrolling = true;
}
aud.src = URL.createObjectURL(files);
let name = files.name;
curSong.innerText = name.substring(0, name.lastIndexOf('.')) + `(${files.length}/${idx+1})`;
aud.play();
mlist.innerHTML = showList(files, idx);
if(isScrolling) mlist.scroll({left:0, top: idx*mlist.scrollHeight/files.length, behavior: 'smooth'});
};
//显示音乐列表
const showList = (ar, idx) => {
let res = '';
for(let j = 0; j < ar.length; j ++) {
let item = `<span class="list3">${j+1}. </span>`; //(j + 1) + '. ';
item += j === idx ?
`<span class="list2" title="${ar.name}">${ar.name}</span>` :
`<span class="list1" title="${ar.name}" onclick="mplay(${j})">${ar.name}</span>`;
res += item + '<br>';
}
return res;
};
//生成不重复随机数组
const ranNum = (total) => {
let ar = Array(total).fill().map((_,key) => key);
ar.sort(() => 0.5 - Math.random());
return ar;
};
//秒转分
const s2m = (seconds) => {
if (!seconds) return '00:00';
let min = parseInt(seconds / 60), sec = parseFloat(Math.floor(seconds) % 60);
if(min < 10) min = '0' + min;
if(sec < 10) sec = '0' + sec;
return min + ':' + sec;
};
//判断进度条区域
const innerH = (e, h) => e.offsetY > h / 2 - 5 && e.offsetY < h / 2 + 5;
//保存设置
const saveSetting = (url,color1,color2) => localStorage.setItem('player_url', `${url},${color1},${color2}`);
//读取设置
const getSetting = () => {
let ar = localStorage.getItem('player_url').split(',');
if(!ar) ar = '';
if(!ar) ar = '#00ffff';
if(!ar) ar = '#fffafa';
return ar;
};
//audio timeupdate监听事件
aud.addEventListener('timeupdate', () => {
mplayer.style.setProperty('--prg', aud.currentTime / aud.duration * 100 + '%');
mplayer.dataset.cu = s2m(aud.currentTime);
mplayer.dataset.du = s2m(aud.duration);
});
//单曲播放结束
aud.addEventListener('ended',() => mplay());
//选择歌曲
selectSong.onclick = () => mfile.click();
//文件选择器改变
mfile.onchange = () => {
let filelist = mfile.files;
if(filelist.length === 0) return;
files.length = 0;
for(let j = 0; j < filelist.length; j ++) {
files.push(filelist);
}
playAr = ranNum(files.length);
mplay();
getDatas();
}
//播放器点击
mplayer.onclick = (e) => {
if(files.length < 1) return;
if(innerH(e,mplayer.clientHeight)) {
aud.currentTime = aud.duration * e.offsetX / mplayer.offsetWidth;
}else{
aud.paused ? aud.play() : aud.pause();
}
};
//播放器鼠标移过
mplayer.onmousemove = (e) => {
mplayer.title = innerH(e,mplayer.clientHeight) ?
s2m(aud.duration * e.offsetX / mplayer.offsetWidth) :
(aud.paused ? '点击播放' : '点击暂停');
};
//启动设置窗口
btnSet.onclick = () => setting.style.display = 'block';
//保存设置窗口
btnSave.onclick = () => {
saveSetting(bgurl.value.trim(),tColor1.value,tColor2.value);
setting.style.display = 'none';
};
//背景图地址栏输入事件
bgurl.oninput = () => {
let img = new Image(), src = bgurl.value.trim();
img.src = src;
img.onload = () => {
papa.style.cssText += `background: url(${src}) no-repeat center/cover;`;
};
img.onerror = () => bgurl.value = '';
};
//背景图地址栏鼠标经过事件
bgurl.onmouseover = () => {
bgurl.focus();
bgurl.setSelectionRange(0,bgurl.value.length);
};
//颜色输入事件
tColor1.oninput = () => papa.style.setProperty('--bg1', tColor1.value);
tColor2.oninput = () => papa.style.setProperty('--bg2', tColor2.value);
//保存设置按钮
btnSave.onclick = () => {
saveSetting(bgurl.value.trim(),tColor1.value,tColor2.value);
tColor1.input;
tColor2.input;
setting.style.display = 'none';
}
//退出设置
btnQuit.onclick = () => setting.style.display = 'none';
//加载风格
const setVals = getSetting();
if(setVals) bgurl.value = setVals;
tColor1.value = setVals;
tColor2.value = setVals;
const bgStr = setVals ? `background: url(${setVals}) no-repeat center/cover` : '';
papa.style.cssText += `${bgStr};--bg1: ${setVals};--bg2: ${setVals};`;
</script> 本帖最后由 马黑黑 于 2025-2-10 12:36 编辑
代码(2月10日更新):
<style>
/* 父元素 */
#papa { --bg1: #00ffff; --bg2: #fffafa; margin: 30px auto 0; width: 800px; height: 400px; font-size: 16px; color: var(--bg2); background: linear-gradient(to bottom right, #000, #ffc); box-shadow: 3px 6px 20px #000; border-radius: 6px; display: grid; place-items: center; position: relative; }
/* 顶部区域 */
#openFile { position: absolute; padding: 8px; top: 0; width: 100%; height: 40px; box-sizing: border-box; }
#mfile { display: none; }
#selectSong, #btnSet { position: absolute; border: 2px solid #ccc; border-radius: 6px; outline: none; color: var(--bg2); background: none; cursor: pointer; }
#btnSet { right: 10px; display: none; }
#openFile:hover #btnSet { display: inline-block; }
#selectSong:hover, #btnSet:hover { background: darkred; }
#curSong { position: absolute; left: 100px; color: var(--bg2); }
/* 设置栏 */
#setting { position: absolute; padding: 10px; width: 400px; right: 0; top: 0; color: var(--bg2); background: rgba(0,0,0,.5); display: none; }
/* 背景图地址栏 */
#bgurl { width: 300px; padding: 6px; }
/* 播放器 */
#mplayer { --ppLen: 4px; --prg: 0%; --ppCap: white; position: absolute; right: 180px; width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(to right, var(--bg2) var(--prg), var(--bg2) var(--prg), var(--bg1) 0) no-repeat 0 50% / 100% 2px; cursor: pointer; display: grid; place-items: center; }
#mplayer:hover { filter: hue-rotate(180deg) drop-shadow(0 0 26px black); }
#mplayer::before, #mplayer::after { position: absolute; }
#mplayer::before { content: attr(data-cu); top: 20%; }
#mplayer::after { content: attr(data-du); top: 56%; }
/* 频谱条 */
.pp { position: absolute; left: calc(50% - 2px); bottom: 50%; width: var(--ppLen); height: 20px; background: linear-gradient(to top, var(--bg1), var(--bg2)); transform-origin: 50% 100%; transform: rotate(var(--deg)) translate(-50px, 0); display: grid; place-items: center; }
.pp::after { position: absolute; content: ''; width: calc(var(--ppLen) + 4px); height: calc(var(--ppLen) + 4px); top: 0px; background: var(--bg2); border-radius: 50%; }
/* 音乐列表 */
#mlist { position: absolute; left: 20px; top: 60px; min-width: 40%; max-width: 50%; min-height: 20%; max-height: calc(100% - 80px); font-size: 14px; opacity: .7; overflow: hidden; scrollbar-width: thin; scrollbar-color: var(--bg1) rgba(255,255,255,.2); }
#mlist:hover { overflow: auto; opacity: 1; }
/* 音乐列表子项目 */
.list1, .list2, .list3 { display: inline-block; max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list1 { cursor: pointer; }
.list2 { color: var(--bg1); cursor:default; }
.list3 { width: fit-content; }
.list1:hover { color: var(--bg1); }
.tRight { text-align: right; }
</style>
<div id="papa">
<div id="openFile">
<input id="selectSong" type="button" value="选择音乐" />
<input type="file" id="mfile" accept=".mp3,.ogg,.wav,.acc,.webm,.flac" multiple />
<span id="curSong"></span>
<input id="btnSet" type="button" value="设置" />
</div>
<div id="setting">
<p>
<label for="bgurl">背景图:</label>
<input id="bgurl" type="text" placeholder="输入图片地址" value="" />
</p>
<p>
<label for="tColor">配色一 :</label>
<input type="color" id="tColor1" name="tc" value="#00ffff" />
</p>
<p>
<label for="tColor">配色二 :</label>
<input type="color" id="tColor2" name="tc" value="#ffffff" />
</p>
<p class="tRight">
<input id="btnQuit" type="button" value="退出" />
<input id="btnSave" type="button" value="保存" />
</p>
</div>
<div id="mplayer" class="mplayer"></div>
<div id="mlist"></div>
<audio id="aud"></audio>
</div>
<p style="text-align:center"><br>更新 :2025年2月10日 <br></p>
<script>
//选择的文件, 播放数组, 波形数据, 频谱
let files=[], playAr = [], output = [], pps = [];
//打开文件次数, 频谱条总数
let openIdx = 0, total = 30;
//获取波形数据
const getDatas = () => {
if(openIdx > 0) return;
openIdx ++;
Ac = new AudioContext;
source = Ac.createMediaElementSource(aud);
analyser = Ac.createAnalyser();
source.connect(analyser);
analyser.connect(Ac.destination);
output = new Uint8Array(total);
};
//生成频谱条
Array(total).fill(0).forEach((_, k) => {
let pp = document.createElement('span');
pp.className = 'pp';
pp.style.cssText += `--deg: ${360 / total * k}deg`;
mplayer.appendChild(pp);
pps.push(pp);
});
//波形数据刷新
(function update() {
if(aud.src) analyser.getByteFrequencyData(output);
for(let j = 0; j < total ; j++) {
pps.style.height = output / 2 + 'px';
}
window.requestAnimationFrame(update);
})();
//播放 :idx为空时随机播放
const mplay = (idx = null) => {
if(files.length === 0) return;
let isScrolling = false;
if(idx === null) {
if(playAr.length === 0) playAr = ranNum(files.length);
let tmpIdx = Math.floor(Math.random() * playAr.length);
idx = playAr;
playAr.splice(tmpIdx, 1);
isScrolling = true;
}
aud.src = URL.createObjectURL(files);
let name = files.name;
curSong.innerText = name.substring(0, name.lastIndexOf('.')) + `(${files.length}/${idx+1})`;
aud.play();
mlist.innerHTML = showList(files, idx);
if(isScrolling) mlist.scroll({left:0, top: idx*mlist.scrollHeight/files.length, behavior: 'smooth'});
};
//显示音乐列表
const showList = (ar, idx) => {
let res = '';
for(let j = 0; j < ar.length; j ++) {
let item = `<span class="list3">${j+1}. </span>`; //(j + 1) + '. ';
item += j === idx ?
`<span class="list2" title="${ar.name}">${ar.name}</span>` :
`<span class="list1" title="${ar.name}" onclick="mplay(${j})">${ar.name}</span>`;
res += item + '<br>';
}
return res;
};
//生成不重复随机数组
const ranNum = (total) => {
let ar = Array(total).fill().map((_,key) => key);
ar.sort(() => 0.5 - Math.random());
return ar;
};
//秒转分
const s2m = (seconds) => {
if (!seconds) return '00:00';
let min = parseInt(seconds / 60), sec = parseFloat(Math.floor(seconds) % 60);
if(min < 10) min = '0' + min;
if(sec < 10) sec = '0' + sec;
return min + ':' + sec;
};
//判断进度条区域
const innerH = (e, h) => e.offsetY > h / 2 - 5 && e.offsetY < h / 2 + 5;
//保存设置
const saveSetting = (url,color1,color2) => localStorage.setItem('player_url', `${url},${color1},${color2}`);
//读取设置
const getSetting = () => {
let ar = localStorage.getItem('player_url').split(',');
if(!ar) ar = '';
if(!ar) ar = '#00ffff';
if(!ar) ar = '#fffafa';
return ar;
};
//audio timeupdate监听事件
aud.addEventListener('timeupdate', () => {
mplayer.style.setProperty('--prg', aud.currentTime / aud.duration * 100 + '%');
mplayer.dataset.cu = s2m(aud.currentTime);
mplayer.dataset.du = s2m(aud.duration);
});
//单曲播放结束
aud.addEventListener('ended',() => mplay());
//选择歌曲
selectSong.onclick = () => mfile.click();
//文件选择器改变
mfile.onchange = () => {
let filelist = mfile.files;
if(filelist.length === 0) return;
files.length = 0;
for(let j = 0; j < filelist.length; j ++) {
files.push(filelist);
}
playAr = ranNum(files.length);
mplay();
getDatas();
}
//播放器点击
mplayer.onclick = (e) => {
if(files.length < 1) return;
if(innerH(e,mplayer.clientHeight)) {
aud.currentTime = aud.duration * e.offsetX / mplayer.offsetWidth;
}else{
aud.paused ? aud.play() : aud.pause();
}
};
//播放器鼠标移过
mplayer.onmousemove = (e) => {
mplayer.title = innerH(e,mplayer.clientHeight) ?
s2m(aud.duration * e.offsetX / mplayer.offsetWidth) :
(aud.paused ? '点击播放' : '点击暂停');
};
//启动设置窗口
btnSet.onclick = () => setting.style.display = 'block';
//保存设置窗口
btnSave.onclick = () => {
saveSetting(bgurl.value.trim(),tColor1.value,tColor2.value);
setting.style.display = 'none';
};
//背景图地址栏输入事件
bgurl.oninput = () => {
let img = new Image(), src = bgurl.value.trim();
img.src = src;
img.onload = () => {
papa.style.cssText += `background: url(${src}) no-repeat center/cover;`;
};
img.onerror = () => bgurl.value = '';
};
//背景图地址栏鼠标经过事件
bgurl.onmouseover = () => {
bgurl.focus();
bgurl.setSelectionRange(0,bgurl.value.length);
};
//颜色输入事件
tColor1.oninput = () => papa.style.setProperty('--bg1', tColor1.value);
tColor2.oninput = () => papa.style.setProperty('--bg2', tColor2.value);
//保存设置按钮
btnSave.onclick = () => {
saveSetting(bgurl.value.trim(),tColor1.value,tColor2.value);
tColor1.input;
tColor2.input;
setting.style.display = 'none';
}
//退出设置
btnQuit.onclick = () => setting.style.display = 'none';
//加载风格
const setVals = getSetting();
if(setVals) bgurl.value = setVals;
tColor1.value = setVals;
tColor2.value = setVals;
const bgStr = setVals ? `background: url(${setVals}) no-repeat center/cover` : '';
papa.style.cssText += `${bgStr};--bg1: ${setVals};--bg2: ${setVals};`;
</script>
本帖最后由 马黑黑 于 2025-2-9 22:27 编辑
主要更新:
(一)增加了设置窗口
设置窗口中,可添加播放器的背景图片、配色风格。所有单项设置完成,播放器界面会立马渲染,但要保存下来,需要点击“保存”按钮。其中:
① 背景图片如果无效,文本框会被清空,这将继续使用先前的背景,保存后下回访问使用默认背景;
② 配色一用于设置频谱渐变颜色之一、进度条进度颜色以及频谱帽,也是播放器文本主色;
③ 配色二用于设置频谱渐变颜色之一、音乐列表滚动条滑块(若有)以及播放器进度条底色。
注意:chrome浏览器下,配色设置时,选择颜色后颜色窗口的关闭方法是点击它以外的任意地方(但不要去点击按钮)。
(二)音乐列表自动翻页机制加入平滑翻滚效果
当有滚动条出现时,音乐列表的自动翻页功能不像前一个版本那样直接到位,而是平滑翻滚。
此版本能继续使用上一个版本的图片背景(如有),但在这里设置了图片之后,前一个版本的背景图片会收到破坏。 黑黑又做了改进呢,辛苦了{:4_187:} 红影 发表于 2025-2-9 22:50
黑黑又做了改进呢,辛苦了
放上来测试一下 问好黑黑老师,这个测试很好用,如果有地址做音画就不用愁了。 俺这大门外汉也把一张图放上面,一首自己唱得的歌放上面,居然图片和声音都 出来了,真棒呵。{:4_176:} 能把中间那个播放器做小一点儿,放在下侧面一点儿就不挡图图了。另外,能做成一个集子,把好多首歌和图分别对应着放上面吗?{:4_189:} 2月10日更新内容:
(一)加入处理文件列表中较长文件名的显示问题:一行显示,用省略号表示文件名太长一行装不下,并用弹出警示菜单显示每一个曲目的完整文件名;
(二)改进自动翻页算法,解决了翻页不够精准的问题。 梦江南 发表于 2025-2-10 09:58
问好黑黑老师,这个测试很好用,如果有地址做音画就不用愁了。
这个不支持播放网络地址的音频,只支持播放自己电脑磁盘中的音乐 樵歌 发表于 2025-2-10 11:06
俺这大门外汉也把一张图放上面,一首自己唱得的歌放上面,居然图片和声音都 出来了,真棒呵。
操作层面尽量设计成零难度 樵歌 发表于 2025-2-10 11:09
能把中间那个播放器做小一点儿,放在下侧面一点儿就不挡图图了。另外,能做成一个集子,把好多首歌和图分别 ...
那个是播放器的灵魂,不能再小了。它能以自己的方式展现你唱歌的音波变化 马黑黑 发表于 2025-2-9 22:51
放上来测试一下
可以很好地听电脑上的音乐呢{:4_187:} 红影 发表于 2025-2-10 23:13
可以很好地听电脑上的音乐呢
是的,不用专门安装个播放器 马黑黑 发表于 2025-2-10 23:53
是的,不用专门安装个播放器
而且这个有自己的特色,那么漂亮{:4_187:} 红影 发表于 2025-2-11 15:46
而且这个有自己的特色,那么漂亮
漂亮得靠自己找件衣服{:4_170:} 马黑黑 发表于 2025-2-11 17:56
漂亮得靠自己找件衣服
找个喜欢的图图,听的都是自己喜欢的歌,这感觉真好{:4_187:} 红影 发表于 2025-2-11 20:48
找个喜欢的图图,听的都是自己喜欢的歌,这感觉真好
{:4_191:} 马黑黑 发表于 2025-2-11 21:21
干了{:4_191:} 红影 发表于 2025-2-11 22:42
干了
{:4_191:}