播放多个本地音频文件的响应式频谱播放器(2.5更新)
本帖最后由 马黑黑 于 2025-2-5 12:17 编辑 <br /><br /><style>/* 父元素 */
#papa {
margin: 30px auto 0;
padding: 10px;
width: 740px;
height: 400px;
font-size: 16px;
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;
left: 15px;
top: 15px;
width: calc(100% - 30px);
}
/* 选择音乐控件 */
#mfile { display: none; }
/* 选择音乐按钮 */
#selectSong {
margin-right: 8px;
border: 2px solid #ccc;
border-radius: 6px;
outline: none;
background: none;
color: snow;
cursor: pointer;
}
/* 背景图片地址栏 */
#bgurl {
position: absolute;
right: 0;
display: none;
}
#openFile:hover #bgurl {
display: inline;
}
/* 选择音乐按钮指针经过 */
#selectSong:hover { background: darkred; }
/* 当前播放曲目 */
#curSong { color: #eee; }
/* 播放器 */
#mplayer {
--bg1: teal;
--bg2: snow;
--ppLen: 4px;
--prog: white;
--track: silver;
--prg: 0%;
--ppCap: white;
position: absolute;
right: 180px;
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(
to right,
var(--prog) var(--prg),
var(--track) var(--prg),
var(--track) 0
) no-repeat 0 50% / 100% 2px;
cursor: pointer;
display: grid;
place-items: center;
}
/* 播放器指针经过 */
#mplayer:hover {
filter: hue-rotate(90deg) drop-shadow(0 0 26px black);
}
/* 播放器伪元素 :音频时间信息 */
#mplayer::before, #mplayer::after {
position: absolute;
color: snow;
}
/* 播放器伪元素一 :当前播放时间 */
#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);
color: silver;
font-size: 14px;
line-height: 20px;
opacity: .4;
/* 滚动条 */
overflow: hidden;
scrollbar-width: thin;
scrollbar-color: teal rgba(255,255,255,.2);
}
/* 音乐列表鼠标经过 */
#mlist:hover {
overflow: auto;
opacity: 1;
}
/* 音乐列表项目一 :待播放曲目 */
.list1 { cursor: pointer; }
/* 音乐列表项目二 :正在播放曲目 */
.list2 {
color: cyan;
cursor:default;
}
/* 音乐列表项目一指针经过 */
.list1:hover { color: white; }
</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="bgurl" type="text" placeholder="背景 : 网络图片地址" value="" />
</div>
<div id="mplayer" class="mplayer"></div>
<div id="mlist"></div>
<audio id="aud"></audio>
</div>
<p style="text-align:right"><br>更新 :2025年2月5日 <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.scrollTop = 20 * idx; //scrolling(20, idx);
};
//显示音乐列表
const showList = (ar, idx) => {
let res = '';
for(let j = 0; j < ar.length; j ++) {
let item = (j + 1) + '. ';
item += j === idx ?
`<span class="list2">${ar.name}</span>` :
`<span class="list1" 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;
//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 ? '点击播放' : '点击暂停');
};
//背景图地址栏输入事件
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;`;
localStorage.setItem('player_url', src)
};
};
//背景图地址栏鼠标经过事件
bgurl.onmouseover = () => {
bgurl.focus();
bgurl.setSelectionRange(0,bgurl.value.length);
};
//加载背景图
const player_url = bgurl.value = localStorage.getItem('player_url');
if(player_url) papa.style.cssText += `background: url(${player_url}) no-repeat center/cover;`;
</script> 本帖最后由 马黑黑 于 2025-1-27 08:47 编辑 <br /><br /><h2>代码(1.27更新版):欢迎使用、修改</h2>
<div id="hEdiv"><pre id="hEpre">
<style>
/* 父元素 */
#papa {
margin: 30px auto 0;
padding: 10px;
width: 740px;
height: 400px;
font-size: 16px;
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;
left: 15px;
top: 15px;
}
/* 选择音乐控件 */
#mfile { display: none; }
/* 选择音乐按钮 */
#selectSong {
margin-right: 8px;
border: 2px solid #ccc;
border-radius: 6px;
outline: none;
background: none;
color: snow;
cursor: pointer;
}
/* 选择音乐按钮指针经过 */
#selectSong:hover { background: darkred; }
/* 当前播放曲目 */
#curSong { color: #eee; }
/* 播放器 */
#mplayer {
--bg1: teal;
--bg2: snow;
--ppLen: 4px;
--prog: white;
--track: silver;
--prg: 0%;
--ppCap: white;
position: absolute;
right: 180px;
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(
to right,
var(--prog) var(--prg),
var(--track) var(--prg),
var(--track) 0
) no-repeat 0 50% / 100% 2px;
cursor: pointer;
display: grid;
place-items: center;
}
/* 播放器指针经过 */
#mplayer:hover {
filter: hue-rotate(90deg) drop-shadow(0 0 26px black);
}
/* 播放器伪元素 :音频时间信息 */
#mplayer::before, #mplayer::after {
position: absolute;
color: snow;
}
/* 播放器伪元素一 :当前播放时间 */
#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);
color: silver;
font-size: 14px;
line-height: 20px;
overflow: auto;
}
/* 音乐列表项目一 :待播放曲目 */
.list1 { cursor: pointer; }
/* 音乐列表项目二 :正在播放曲目 */
.list2 {
color: cyan;
cursor:default;
}
/* 音乐列表项目一指针经过 */
.list1:hover { color: white; }
</style>
<div id="papa">
<div id="openFile">
<input id="selectSong" type="button" value="选择音乐" />
<input type="file" id="mfile" accept=".mp3, .ogg, .wav, .acc, .webm" multiple />
<span id="curSong"></span>
</div>
<div id="mplayer" class="mplayer"></div>
<div id="mlist"></div>
<audio id="aud"></audio>
</div>
<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.scrollTop = 20 * idx; //scrolling(20, idx);
};
//显示音乐列表
const showList = (ar, idx) => {
let res = '';
for(let j = 0; j < ar.length; j ++) {
let item = (j + 1) + '. ';
item += j === idx ?
`<span class="list2">${ar.name}</span>` :
`<span class="list1" 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;
//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 ? '点击播放' : '点击暂停');
};
</script>
</pre></div>
<script type="module">
import hlight from 'https://638183.freep.cn/638183/web/mod/helight.js';
hlight.hl(hEdiv, hEpre);
</script> 这个好,而且这个频谱是跨域的,太好了{:4_199:} 红影 发表于 2025-1-24 21:41
这个好,而且这个频谱是跨域的,太好了
没有跨域的,是本域:就在自己的地儿上解析音波
放了几首歌曲进去,界面上显示歌曲名,正在播放的歌曲也有高亮显示{:4_187:} 红影 发表于 2025-1-24 21:43
放了几首歌曲进去,界面上显示歌曲名,正在播放的歌曲也有高亮显示
{:4_191:} 这个可以放在桌面,当自己的播放器了,真的太好了{:4_199:} 这个还能给它个自己喜欢的底图,放在桌面当自己的播放器。
谢谢黑黑带来的好东西{:4_199:} 这么好的内容,我又想置顶了,想起黑黑说的置顶的太多了,还是就这样吧,心里点赞无数{:4_187:} 放了几首本地歌曲,边听边去回帖去了。真好。{:4_205:} 马黑黑 发表于 2025-1-24 21:42
没有跨域的,是本域:就在自己的地儿上解析音波
哦,反正是真正的同步频谱呢,特别好{:4_199:} 马黑黑 发表于 2025-1-24 21:43
这个太好了,黑黑厉害{:4_178:} 红影 发表于 2025-1-24 22:07
哦,反正是真正的同步频谱呢,特别好
还行吧 红影 发表于 2025-1-24 22:07
这个太好了,黑黑厉害
这个之前其实早做过的:获取音波数据那个 马黑黑 发表于 2025-1-24 22:08
还行吧
现在没地方找到能跨域的上传地方了,否则同步频谱觉得特别好看{:4_187:} 马黑黑 发表于 2025-1-24 22:08
这个之前其实早做过的:获取音波数据那个
获取音波的仍然不及这个直接的{:4_204:} 红影 发表于 2025-1-24 21:48
放了几首本地歌曲,边听边去回帖去了。真好。
现在开放的音乐越来越收缩,资本运营的必然结果。所以,本地磁盘多保留一些音乐是应该的,也可以再淘宝天猫京东拼多多购买音乐U盘,成本极低还有一个大U盘用用 红影 发表于 2025-1-24 22:12
获取音波的仍然不及这个直接的
这个多加了一些东东而已,原理差不多的
红影 发表于 2025-1-24 22:11
现在没地方找到能跨域的上传地方了,否则同步频谱觉得特别好看
对的。音画帖其实也是越来越不好做了:音乐今天开放,明天可能变成收费的 马黑黑 发表于 2025-1-24 22:13
现在开放的音乐越来越收缩,资本运营的必然结果。所以,本地磁盘多保留一些音乐是应该的,也可以再淘宝天 ...
哦,那种大U盘我没买过,但是喜欢的歌曲倒是留了不少呢。