H5: 使用Web Audio API播放音乐

编程入门 行业动态 更新时间:2024-10-16 22:13:05

H5: 使用Web Audio API播放<a href=https://www.elefans.com/category/jswz/34/1768982.html style=音乐"/>

H5: 使用Web Audio API播放音乐

简介

记录关于自己使用 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 './musics'enum PlayMode {REPEAT, // 循环播放SINGLE_CYCLE, // 单曲循环RANDOM // 随机播放
}const musicPlayer = ref<HTMLAudioElement | 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 = 'musicPlayerAudio'document.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

<template><div class="music-box"><!-- 音乐列表 --><div class="music-list"><divv-for="(music, index) in musicList":key="index"class="music-item":class="{ 'music-item-active': musicPlayer.musicPlayingIndex.value === index }"@click.stop="switchAudio(index)"><div class="item-left"><div class="item-left-title">{{ music.title }}</div><svgv-if="musicPlayer.musicPlayingIndex.value === index && musicPlayer.musicIsPlaying.value"id="equalizer"width="13px"height="11px"viewBox="0 0 10 7"version="1.1"xmlns=""xmlns:xlink=""><g fill="#3994f9"><rectid="bar1"transform="translate(0.500000, 6.000000) rotate(180.000000) translate(-0.500000, -6.000000) "x="0"y="5"width="1"height="2px"></rect><rectid="bar2"transform="translate(3.500000, 4.500000) rotate(180.000000) translate(-3.500000, -4.500000) "x="3"y="2"width="1"height="5"></rect><rectid="bar3"transform="translate(6.500000, 3.500000) rotate(180.000000) translate(-6.500000, -3.500000) "x="6"y="0"width="1"height="7"></rect></g></svg><svgv-else-if="musicPlayer.musicPlayingIndex.value === index"id="equalizer"width="13px"height="11px"viewBox="0 0 10 7"version="1.1"xmlns=""xmlns:xlink=""><g fill="#3994f9"><rect x="0" y="5" width="1" height="2px"></rect><rect x="3" y="2" width="1" height="5"></rect><rect x="6" y="0" width="1" height="7"></rect></g></svg></div><div class="item-right">{{ music.time }}</div></div></div><!-- 播放控制 --><div class="music-control"><div class="control-content"><div class="control-content-left"><divclass="music-btn prev"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="prev"/><div:class="['music-btn', musicPlayer.musicIsPlaying.value ? 'pause' : 'play']"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="togglePlayer"/><divclass="music-btn next"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="next"/></div><div class="control-content-center"><div class="center-title">{{ currentMusicTitle || '-' }}</div><div ref="audioProgressWrap" class="center-progress-wrap"><div ref="audioProgress" class="center-progress-wrap-active" /></div><div class="center-time"><div class="center-time-now">{{ formatSecond(musicPlayer.currentTime.value) }}</div><div class="center-time-total">{{ currentMusicTotalTimeStr }}</div></div></div><div class="control-content-right"><divv-if="musicPlayer.musicPlayMode.value === PlayMode.REPEAT"class="music-btn playRepeat"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="nextPlayMode"/><divv-if="musicPlayer.musicPlayMode.value === PlayMode.SINGLE_CYCLE"class="music-btn singleCycle"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="nextPlayMode"/><divv-if="musicPlayer.musicPlayMode.value === PlayMode.RANDOM"class="music-btn playRandom"@touchstart.passive="onTouchEvent"@touchend.passive="onTouchEvent"@click="nextPlayMode"/></div></div></div></div>
</template><script setup lang="ts">import { ref, computed, watch } from 'vue'import { PlayMode, useMusicPlayer } from './musicPlayer/useMusicPlayer'import { musicList } from './musicPlayer/musics'const 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`})
</script><style lang="less" scoped>@bottomHeight: 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>

最后

觉得有用的朋友请用你的金手指点一下赞,或者评论留言一起探讨技术!

更多推荐

H5: 使用Web Audio API播放音乐

本文发布于:2023-11-17 04:24:28,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1639761.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:音乐   Web   API   Audio

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!