花潮论坛

搜索
热搜: 活动 交友 discuz
查看: 9|回复: 2

天籁之音 - 本地音频播放器

[复制链接]
  • TA的每日心情
    奋斗
    2026-4-5 08:56
  • 签到天数: 1780 天

    [LV.Master]伴坛终老

    3175

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

    发表于 2026-4-5 22:36 | 显示全部楼层 |阅读模式

    请马上登录,朋友们都在花潮里等着你哦:)

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>天籁之音</title> </head> <body> <style> body { background: #666; } h1 { font-size: 2.5em; text-align: center; color: white; text-shadow: -2px -2px tan; } .papa { margin: auto; padding: 20px; width: clamp(600px, 60vw, 1000px); height: 80vh; font-size: 16px; background: linear-gradient(to bottom right, #000, #ffc); box-shadow: 3px 6px 20px #000; border-radius: 10px; display: flex; flex-direction: column; flex-wrap: wrap; gap: 10px; align-content: space-between; justify-content: space-between; position: relative; } .papa * { box-sizing: border-box; } .son { box-sizing: border-box; width: calc(50% - 5px); height: calc(50% - 5px); display: grid; place-items: center; position: relative; } #openFile { position: absolute; left: 15px; top: 10px; } #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; padding: 4px; cursor: pointer; } #mlist { position: absolute; padding: 12px 20px; left: 10px; top: 60px; width: 90%; height: calc(100% - 12px); color: silver; line-height: 2.5em; overflow: hidden; scrollbar-width: thin; scrollbar-color: tan transparent; z-index: 20; transition: all .5s; } #mlist:hover { overflow: auto; } .list1 { cursor: pointer; } .list2 { color: cyan; cursor:default; } .list1:hover { color: white; } #mplayer { --bg1: teal; --bg2: snow; --ppLen: 4px; --prog: white; --track: silver; --prg: 0%; --ppCap: white; position: absolute; 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; filter: drop-shadow(0 0 10px gray); 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%; } #playbtn { position: absolute; padding: 2px 20px; bottom: 20px; color: white; font-size: 1.2em; border: 3px solid white; border-radius: 12px; cursor: pointer; user-select: none; } #playbtn:hover { background: darkred; } .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%; } .hidden { overflow: hidden; } #mpic { position: absolute; width: 65%; border-radius: 8px; object-fit: cover; transition: .35s; } #mpic:hover { transform: scale(1.2); } #mMsg pre { font-family: monospace; line-height: 30px; white-space: pre-wrap; } </style> <h1>天籁之音</h1> <audio id="aud"></audio> <div class="papa"> <div class="son"> <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="mlist"></div> </div> <div class="son"> <div id="mplayer"></div> <div id="playbtn" title="随机选择">⏭ 下一曲</div> </div> <div class="son hidden"> <!-- 封面图片可以换成本地文件 --> <!--img id="mpic" src="./api/piano.svg" alt="" title="歌曲封面"--> <img id="mpic" src="https://638183.freep.cn/638183/web/svg/piano.svg" alt="" title="歌曲封面"> </div> <div id="mMsg" class="son"></div> </div> <!-- 可以下载 jsmediatags 文件到本地使用 --> <!--script src="./jsmediatags.min.js"></script--> <script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script> <script> let files = [], playAr = [], output = [], pps = [], total = 30, Ac = null, currentIdx, mData = {}; // 音频信息输出 const outputData = (data) => { if (!data.size) return; mMsg.innerHTML = [ `<pre>`, `文 件 名 :${data.name}`, ``, `专辑名称 :${data.album}`, `歌曲标题 :${data.title}`, `艺 术 家 :${data.artist}`, `文件大小 :${data.size}`, `音频时长 :${data.duration}`, `</pre>` ].join('\n'); }; // 获取音频元数据 const getTrackMsg = (file) => { jsmediatags.read(file, { onSuccess: (tag) => { if (tag.tags.picture) { mpic.src = URL.createObjectURL(new Blob([new Uint8Array(tag.tags.picture.data).buffer])); } mData.name = file.name; mData.album = tag.tags.album || '未知'; mData.artist = tag.tags.artist || '未知'; mData.size = formatFileSize(file.size); mData.title = tag.tags.title || file.name.replace(/\..+/, ''); outputData(mData); }, onError: console.error }); }; //获取波形数据 const getAcDatas = () => { if (Ac !== null) return; Ac = new AudioContext; source = Ac.createMediaElementSource(aud); analyser = Ac.createAnalyser(); source.connect(analyser); analyser.connect(Ac.destination); output = new Uint8Array(total); }; // 格式化文件大小 const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; //生成频谱条 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[j].style.height = output[j] / 2 + 'px'; } window.requestAnimationFrame(update); })(); //播放音频 :idx为空时随机播放 const mplay = (idx = null) => { if(files.length === 0) return; if(idx === null) { if(playAr.length === 0) playAr = ranNum(files.length); let tmpIdx = Math.floor(Math.random() * playAr.length); idx = currentIdx = playAr[tmpIdx]; playAr.splice(tmpIdx, 1); } else currentIdx = idx; aud.src = URL.createObjectURL(files[idx]); let name = files[idx].name; curSong.innerText = name.substring(0, name.lastIndexOf('.')) + `(${files.length}/${idx+1})`; aud.play(); mlist.innerHTML = showList(files, idx); scrollList(); getTrackMsg(files[idx]); aud.onloadedmetadata = () => { mData.duration = s2m(aud.duration); outputData(mData); }; }; //生成音乐列表 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[j].name}</span>` : `<span class="list1" onclick="mplay(${j})">${ar[j].name}</span>`; res += item + '<br>'; } return res; }; // 列表滚动 const scrollList = () => { const lists = mlist.querySelectorAll('span'); if(lists.length > 0 && isInViewport(mlist) && mlist.scrollHeight > mlist.clientHeight) { lists[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'center' }); console.log('Yes') } }; //生成不重复随机数组 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; // mlist进入视口时项目滚动 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { scrollList(); } }); }); observer.observe(mlist); // 元素是否进入视口 const isInViewport = (elm) => { const rect = elm.getBoundingClientRect(); const viewHeight = window.innerHeight || document.documentElement.clientHeight; const viewWidth = window.innerWidth || document.documentElement.clientWidth; return (rect.bottom > 0 && rect.right > 0 && rect.top < viewHeight && rect.left < viewWidth); }; //audio timeupdate监听事件 aud.ontimeupdate = () => { mplayer.style.setProperty('--prg', aud.currentTime / aud.duration * 100 + '%'); mplayer.dataset.cu = s2m(aud.currentTime); mplayer.dataset.du = s2m(aud.duration); }; //单曲播放结束 aud.onended = () => 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[j]); } playAr = ranNum(files.length); mplay(); getAcDatas(); } //播放器点击 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 ? '点击播放' : '点击暂停'); }; curSong.onclick = () => scrollList(); playbtn.onclick = () => mplay(); </script> </body> </html>
  • TA的每日心情
    奋斗
    2026-4-5 08:56
  • 签到天数: 1780 天

    [LV.Master]伴坛终老

    3175

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-4-5 22:42 | 显示全部楼层
    一楼的代码可以在线预览,如果觉得满意,可以考虑将代码存为本地 .html 文档使用。存为本地页面,建议修改两个地方,以避免网络依赖,详情查看源码的第 59 行 和 67 行注释。具体做法是将图片、JS文件保存在和 .html 文档相同的目录,然后分别解开上述注释下的代码行,并将其下的对应行删掉(建议)或者注释掉。

    jsmediatags.min.js 文件保存方法:复制 69 行代码中的JS地址到浏览器地址栏,回车,成功打开后全选、复制,然后用文本编辑器保存文档。
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2026-4-5 08:56
  • 签到天数: 1780 天

    [LV.Master]伴坛终老

    3175

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-4-5 22:51 | 显示全部楼层
    源码中的两个处理项目列表滚动的函数和方法,209行、220行分别是它们的标识注释,它们对于单独使用的页面来讲不是必须的,加入它们是考虑在长页面中使用的情形。就是说,这两个函数以及在源码中的应用可以剔除,因为单独应用时 mlist 元素总是在可视区域。保留也无法,一定要剔除的话需要通过函数名找到对应的应用语句进行准确修改。
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|手机版|Archiver|服务支持:DZ动力|huachaowang.com Inc. ( 蜀ICP备17032287号-1 )

    GMT+8, 2026-4-6 01:43 , Processed in 0.066378 second(s), 24 queries .

    Powered by Discuz! X3.4

    © 2001-2013 Comsenz Inc.

    快速回复 返回顶部 返回列表