电子商务网站的建设和维护,晋城做网站的公司,同创企业网站源码,网址自动生成手机网站简介
记录关于自己使用 Web Audio API 的 AudioContext 播放音乐的知识点。
需求分析 1.列表展示音乐#xff1b; 2.上/下一首、播放/暂停/续播#xff1b; 3.播放模式切换#xff1a;循环播放、单曲循环、随机播放#xff1b; 4.播放状态显示#xff1a;当前播放的音乐…简介
记录关于自己使用 Web Audio API 的 AudioContext 播放音乐的知识点。
需求分析 1.列表展示音乐 2.上/下一首、播放/暂停/续播 3.播放模式切换循环播放、单曲循环、随机播放 4.播放状态显示当前播放的音乐名、播放时间、总时间、进度条效果 5.播放控制器显示在底部区域 6.支持音量调节 7.浏览器隐藏、显示的交互后也能正常有效播放播放、声音。
注意
安卓与IOS上有不同的兼容性所以采用了 Web Audio API 的 AudioContext 兼容性强大但是截止写文章前IOS17版本不支持没有声音。
稍微复杂点点的逻辑就是AudioContext与手机系统的关联可以看看 AudioContext: createMediaElementSource。 具体实现
test/music/musicPlayer/musics.ts test/music/musicPlayer/useMusicPlayer.ts test/music/index.vue
1.test/music/musicPlayer/musics.ts
interface musicItem {title: stringsrc: stringtime: stringmp3Name: string
}
const musicList: musicItem[] [{title: How to Love,src: ,time: 03:39,mp3Name: sx_music_HowtoLove_CashCash},{title: 空空如也,src: ,time: 03:34,mp3Name: sx_music_kongkongruye},{title: 2 Soon,src: ,time: 03:19,mp3Name: sx_music_Soon_JonYoung},{title: 孤勇者,src: ,time: 04:16,mp3Name: sx_music_guyongzhe},{ title: 秒针, src: , time: 02:58, mp3Name: sx_music_miaozhen },{title: 热爱105˚的你,src: ,time: 03:15,mp3Name: sx_music_reai105dudeni},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba},{title: 她会魔法吧,src: ,time: 03:01,mp3Name: sx_music_tahuimofaba}
] // 音乐列表信息export { type musicItem, musicList }2.test/music/musicPlayer/useMusicPlayer.ts
import { ref, nextTick } from vue
import { musicList } from ./musicsenum PlayMode {REPEAT, // 循环播放SINGLE_CYCLE, // 单曲循环RANDOM // 随机播放
}const musicPlayer refHTMLAudioElement | null()
const musicPlayingIndex ref(-1) // 播放的音乐的下标
const musicIsPlaying ref(false) // 是否播放中
const currentTime ref(0) // 正在播放的音乐时间点
const musicPlayMode ref(PlayMode.REPEAT) // 播放模式
const progressInterval 500 // 计时器触发的频率
let defaultVolume 1 // 音量 0-1
let timer: NodeJS.Timer | null null // 计时器 ---此处需要在 .eslintrc.js/.cjs 文件中配置 globals: { NodeJS: true }
let source: MediaElementAudioSourceNode | null null
let audioCtx: AudioContext | null null
let gainNode: GainNode | null null
let audioContextAttr: string | null null
if (AudioContext in window) {audioContextAttr AudioContext
} else if (webkitAudioContext in window) {audioContextAttr webkitAudioContext
}const useMusicPlayer () {const _getMusicFile (mp3Name: string) {// 此处需要相对路径// vite项目// return new URL(../../../assets/music/${mp3Name}.mp3, import.meta.url).href// webpack项目return require(../../../assets/music/${mp3Name}.mp3)}/** 设置音量百分比 0-100 变为 0-1* param v number 0-100*/const _saveDefaultVolume (v: number) {let num vif (v 0) {num 0} else if (v 100) {num 100}defaultVolume num / 100return defaultVolume}/*** 计时器回调-更新显示-MP3的播放时间*/const _intervalUpdatePlayTime () {const player musicPlayer.valueif (!player) returncurrentTime.value player.currentTime}/*** 计时器清除*/const _clearTimer () {if (!timer) returnclearInterval(timer)timer null}/*** 计时器绑定开始*/const _startTimer () {_clearTimer()timer setInterval(_intervalUpdatePlayTime, progressInterval)}/*** 方法取两个值之间的随机数*/const _random (min 0, max 100) Math.floor(Math.random() * (max - min 1)) min/*** 销毁断开audio与AudioContext之间的链接*/const _destroyConnect () {if (source) {source.disconnect()}if (gainNode) {gainNode.disconnect()}if (audioCtx) {audioCtx.close()}source nullgainNode nullaudioCtx nullmusicPlayer.value null}/*** 音乐初始化audio与AudioContext的绑定* 目的是为了 IOS 上能调整音量*/const _init () {if (!audioContextAttr) return// 先暂停已有的播放pause()// 对已创建的绑定关系进行解绑_destroyConnect()// 若在body中找得到对应的dom则进行移除const findDom document.getElementById(musicPlayerAudio) as HTMLAudioElementif (findDom) {findDom.remove()}// 创建audio加入body中const dom document.createElement(audio)dom.id musicPlayerAudiodocument.body.appendChild(dom)// 给audio绑定播放结束的回调函数dom.onended onAudioEnded// 创建AudioContext、source、gainNode进行关联便于IOS控制音量const UseAudioContext (window as any)[audioContextAttr]audioCtx new UseAudioContext()if (!audioCtx) returnsource audioCtx.createMediaElementSource(dom)gainNode audioCtx.createGain()source.connect(gainNode)gainNode.connect(audioCtx.destination)// 设置音量if (defaultVolume 0) {dom.muted true} else {dom.muted false}gainNode.gain.value defaultVolume// 存储dom便于后续访问audio对应的属性musicPlayer.value dom// 若播放控制器的状态未启动则启动if (audioCtx audioCtx.state suspended) {audioCtx.resume()}}/*** 音乐播放器-音量调整*/const setVolume (volume: number) {const v _saveDefaultVolume(volume)const player musicPlayer.valueif (!player) returnif (v 0) {player.muted true} else {player.muted false}if (!gainNode || !gainNode.gain) returngainNode.gain.value v}/*** 音乐播放器-暂停*/const pause () {const player musicPlayer.valueif (!musicIsPlaying.value || !player) {return}musicIsPlaying.value falseplayer.pause()_clearTimer()}/*** 音乐播放器-播放*/const playByLast () {const player musicPlayer.valueif (!player || !player.src) returnif (audioCtx audioCtx.state suspended) {audioCtx.resume()}nextTick(() {// play触发时会先自动加载资源player.play().then(() {musicIsPlaying.value true_startTimer()})})}/*** 音乐播放器-播放-通过下标*/const playByIndex (index: number) {if (index 0 || index 1 musicList.length) {return}musicIsPlaying.value false// 重新初始化便于释放上一个播放器所占用的内存_init()const player musicPlayer.valueif (!player) {return}// 重置当前播放了的时长currentTime.value 0// 更新要播放的下标musicPlayingIndex.value indexif (!musicList[index].src) {// 若资源路径不存在则进行对应的路径引入musicList[index].src _getMusicFile(musicList[index].mp3Name)}if (!musicList[index].src) {console.error(find music file failed)return}player.src musicList[index].srcplayByLast()}/*** 音乐随机播放*/const randomPlay () {const index _random(0, musicList.length - 1)playByIndex(index)}/*** 音乐播放器-下一首*/const playNext () {if (musicPlayMode.value PlayMode.RANDOM) {randomPlay()} else {const index: number musicPlayingIndex.value 1 musicList.length ? 0 : musicPlayingIndex.value 1playByIndex(index)}}/*** 音乐播放器-上一首*/const playPrev () {if (musicPlayMode.value PlayMode.RANDOM) {randomPlay()} else {const index: number musicPlayingIndex.value 1 ? musicList.length - 1 : musicPlayingIndex.value - 1playByIndex(index)}}/*** 回调播放结束后下一首播放什么*/const onAudioEnded () {switch (musicPlayMode.value) {case PlayMode.REPEAT:playNext()breakcase PlayMode.SINGLE_CYCLE:playByIndex(musicPlayingIndex.value)breakcase PlayMode.RANDOM:randomPlay()breakdefault:break}return true}/** 自动播放音乐 */const startPlayInRoom () {// 用户第一次点击时自动播放音乐const initMusicAutoPlayOnReload () {document.removeEventListener(click, initMusicAutoPlayOnReload, true)playByIndex(0)}document.addEventListener(click, initMusicAutoPlayOnReload, true)}return {musicList,musicPlayer,musicPlayingIndex,musicIsPlaying,currentTime,musicPlayMode,setVolume,pause,playByIndex,playByLast,playPrev,playNext,_clearTimer,startPlayInRoom}
}export { PlayMode, useMusicPlayer }3.test/music/index.vue
templatediv classmusic-box!-- 音乐列表 --div classmusic-listdivv-for(music, index) in musicList:keyindexclassmusic-item:class{ music-item-active: musicPlayer.musicPlayingIndex.value index }click.stopswitchAudio(index)div classitem-leftdiv classitem-left-title{{ music.title }}/divsvgv-ifmusicPlayer.musicPlayingIndex.value index musicPlayer.musicIsPlaying.valueidequalizerwidth13pxheight11pxviewBox0 0 10 7version1.1xmlnshttp://www.w3.org/2000/svgxmlns:xlinkhttp://www.w3.org/1999/xlinkg fill#3994f9rectidbar1transformtranslate(0.500000, 6.000000) rotate(180.000000) translate(-0.500000, -6.000000) x0y5width1height2px/rectrectidbar2transformtranslate(3.500000, 4.500000) rotate(180.000000) translate(-3.500000, -4.500000) x3y2width1height5/rectrectidbar3transformtranslate(6.500000, 3.500000) rotate(180.000000) translate(-6.500000, -3.500000) x6y0width1height7/rect/g/svgsvgv-else-ifmusicPlayer.musicPlayingIndex.value indexidequalizerwidth13pxheight11pxviewBox0 0 10 7version1.1xmlnshttp://www.w3.org/2000/svgxmlns:xlinkhttp://www.w3.org/1999/xlinkg fill#3994f9rect x0 y5 width1 height2px/rectrect x3 y2 width1 height5/rectrect x6 y0 width1 height7/rect/g/svg/divdiv classitem-right{{ music.time }}/div/div/div!-- 播放控制 --div classmusic-controldiv classcontrol-contentdiv classcontrol-content-leftdivclassmusic-btn prevtouchstart.passiveonTouchEventtouchend.passiveonTouchEventclickprev/div:class[music-btn, musicPlayer.musicIsPlaying.value ? pause : play]touchstart.passiveonTouchEventtouchend.passiveonTouchEventclicktogglePlayer/divclassmusic-btn nexttouchstart.passiveonTouchEventtouchend.passiveonTouchEventclicknext//divdiv classcontrol-content-centerdiv classcenter-title{{ currentMusicTitle || - }}/divdiv refaudioProgressWrap classcenter-progress-wrapdiv refaudioProgress classcenter-progress-wrap-active //divdiv classcenter-timediv classcenter-time-now{{ formatSecond(musicPlayer.currentTime.value) }}/divdiv classcenter-time-total{{ currentMusicTotalTimeStr }}/div/div/divdiv classcontrol-content-rightdivv-ifmusicPlayer.musicPlayMode.value PlayMode.REPEATclassmusic-btn playRepeattouchstart.passiveonTouchEventtouchend.passiveonTouchEventclicknextPlayMode/divv-ifmusicPlayer.musicPlayMode.value PlayMode.SINGLE_CYCLEclassmusic-btn singleCycletouchstart.passiveonTouchEventtouchend.passiveonTouchEventclicknextPlayMode/divv-ifmusicPlayer.musicPlayMode.value PlayMode.RANDOMclassmusic-btn playRandomtouchstart.passiveonTouchEventtouchend.passiveonTouchEventclicknextPlayMode//div/div/div/div
/templatescript setup langtsimport { ref, computed, watch } from vueimport { PlayMode, useMusicPlayer } from ./musicPlayer/useMusicPlayerimport { musicList } from ./musicPlayer/musicsconst systemSoundMode ref(true) // 该变量应该在store中便于设置页面控制全局声音的开启与否const musicPlayer useMusicPlayer()const audioProgressWrap ref()const audioProgress ref()/** 当前播放的音乐名 */const currentMusicTitle computed(() musicPlayer.musicPlayingIndex.value 1 0? musicList[musicPlayer.musicPlayingIndex.value].title: )/** 当前播放的音乐总时间 */const currentMusicTotalTimeStr computed(() musicPlayer.musicPlayingIndex.value 1 0? musicList[musicPlayer.musicPlayingIndex.value].time: 00:00)/** 操作切换播放模式 */const nextPlayMode () {musicPlayer.musicPlayMode.value (musicPlayer.musicPlayMode.value 1) % 3switch (musicPlayer.musicPlayMode.value) {case PlayMode.REPEAT:console.log(循环播放)breakcase PlayMode.RANDOM:console.log(随机播放)breakcase PlayMode.SINGLE_CYCLE:console.log(单曲循环)breakdefault:break}}/** 事件当点击按钮时的过渡效果 */const onTouchEvent (event: Event) {const tg event.currentTarget as HTMLElementif (!tg) returnif (event.type touchstart) {tg.classList.add(touch)}if (event.type touchend) {tg.classList.remove(touch)}}/** 格式化秒数ss:mm */const formatSecond (second: number) {let hourStr ${Math.floor(second / 60)}let secondStr ${Math.ceil(second % 60)}if (hourStr.length 1) {hourStr 0${hourStr}}if (secondStr.length 1) {secondStr 0${secondStr}}return ${hourStr}:${secondStr}}/** 操作播放所选音乐 */const switchAudio (index: number) {const player musicPlayer.musicPlayer.valueif (!systemSoundMode.value) {window.alert(所有声音已关闭)}if (player?.src player?.src.includes(musicList[index].mp3Name)) {if (musicPlayer.musicIsPlaying.value) {return}musicPlayer.playByLast()} else {musicPlayer.playByIndex(index)}}/** 操作上一首 */const prev () {if (!systemSoundMode.value) {window.alert(所有声音已关闭)}musicPlayer.playPrev()}/** 操作下一首 */const next () {if (!systemSoundMode.value) {window.alert(所有声音已关闭)}musicPlayer.playNext()}/** 操作播放/暂停 */const togglePlayer () {const player musicPlayer.musicPlayer.valueif (!systemSoundMode.value) {window.alert(所有声音已关闭)}if (musicPlayer.musicIsPlaying.value player?.src) {// 正在播放则暂停musicPlayer.pause()} else if (!player?.src) {// 未开始播放则播放第一首musicPlayer.playByIndex(0)} else {// 暂停了则继续播放刚才的musicPlayer.playByLast()}}/** 监听当前播放中的音乐的进度时间进度条变化 */watch(() musicPlayer.currentTime.value,() {const player musicPlayer.musicPlayer.valueif (!audioProgressWrap.value || !audioProgress.value || !player) {return}const offsetLeft (player.currentTime / player.duration) * audioProgressWrap.value.offsetWidthaudioProgress.value.style.width ${offsetLeft}px})
/scriptstyle langless scopedbottomHeight: 97px;controlHeight: 63px;controlBottom: 34px;.music-box {width: 100%;height: 100%;background-color: #141624;position: relative;display: flex;flex-direction: column;}.music-list {flex: 1;overflow-y: auto;scrollbar-width: none;-ms-overflow-style: none;::-webkit-scrollbar {display: none;}.music-item:nth-of-type(1) {margin-top: 7px;}.music-item {padding: 12px 20px 19px;display: flex;align-items: center;justify-content: space-between;font-size: 14px;font-weight: 500;line-height: 120%;color: #8f9095;.item-left {display: flex;align-items: center;.item-left-title {height: 17px;margin-right: 10px;}#equalizer {position: relative;}#bar1 {animation: bar1 1.2s infinite linear;}#bar2 {animation: bar2 0.8s infinite linear;}#bar3 {animation: bar3 1s infinite linear;}#bar4 {animation: bar4 0.7s infinite linear;}keyframes bar1 {0% {height: 2px;}50% {height: 7px;}100% {height: 2px;}}keyframes bar2 {0% {height: 5px;}40% {height: 1px;}80% {height: 7px;}100% {height: 5px;}}keyframes bar3 {0% {height: 7px;}50% {height: 0;}100% {height: 7px;}}keyframes bar4 {0% {height: 2px;}50% {height: 7px;}100% {height: 2px;}}}}.music-item-active {.item-left {.item-left-title {color: #3994f9;}}.item-right {color: #3994f9;}}}.music-control {height: bottomHeight;padding: 0 10px;background-color: #141624;.control-content {height: controlHeight;border-radius: 7px;background-color: #1b1d2a;display: flex;align-items: center;justify-content: space-between;.control-content-left {display: flex;align-items: center;.prev,.pause,.play {margin-right: 10px;}.next {margin-right: 17px;}}.control-content-center {margin-top: 1px;flex: 1;.center-title {margin-bottom: 5px;line-height: 120%;font-size: 13px;font-weight: 500;color: #fff;}.center-progress-wrap {width: 100%;height: 2px;background-color: #3e404e;.center-progress-wrap-active {width: 0;height: 100%;background-color: #3994f9;}}.center-time {height: 50%;margin-top: 10px;display: flex;justify-content: space-between;align-items: center;.center-time-now,.center-time-total {font-size: 10px;font-weight: 400;line-height: 12px;color: #3994f9;}.center-time-total {color: #8f9095;}}}.control-content-right {padding-left: 10px;}.music-btn {width: 33px;height: 33px;.prev {background: url(../../assets/images/music/music-prev.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-prev-touch.png);}}.play {background: url(../../assets/images/music/music-play.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-play-touch.png);}}.pause {background: url(../../assets/images/music/music-pause.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-pause-touch.png);}}.next {background: url(../../assets/images/music/music-next.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-next-touch.png);}}.playRepeat {background: url(../../assets/images/music/music-repeat.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-repeat-touch.png);}}.singleCycle {background: url(../../assets/images/music/music-single-cycle.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-single-cycle-touch.png);}}.playRandom {background: url(../../assets/images/music/music-random.png);background-size: 100%;background-repeat: no-repeat;.touch {background: url(../../assets/images/music/music-random-touch.png);}}}}}
/style最后
觉得有用的朋友请用你的金手指点一下赞或者评论留言一起探讨技术