useAudio
音频可视化
1 .https://github.com/goldfire/howler.js 添加空间声音,做音频兼容
2 .测试音频地址https://m8.music.126.net/21180815163607/04976f67866d4b4d11575ab418904467/ymusic/515a/5508/520b/f0cf47930abbbb0562c9ea61707c4c0b.mp3?infoId=92001
3 .状态码是206,表示分段加载
4 .全部元素震动 https://okazari.github.io/Rythm.js/
5 .我之前做的是采集的不同部位的频率数据,现在这个是把全部的都合成了一个,然后给一个元素添加样式
6 .
rythm.min.js 根据声音频率为元素添加样式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rythm.js/2.2.5/rythm.min.js"></script>
</head>
<body>
<div class="a">
哈哈哈
</div>
<div class="a">
啦啦啦
</div>
<div class="a">
你你你
</div>
<div class="a">
好!好好
</div>
<button id="start">start</button>
<button id="stop">stop</button>
<ul>
<li>
好像不能给太激烈的音乐
</li>
<li>
有三种自带的内建模式rythm-bass,rythm-high,rythm-medium直接加在class里面
</li>
<li>
第三个,四个参数不知道具体是干啥的,有的是在1,10.有的是在0,200之间
</li>
<li>
需要不同的搭配起来才比较好看
</li>
</ul>
<script>
const start=document.querySelector('#start')
const stop=document.querySelector('#stop')
var rythm=new Rythm()
start.addEventListener('click',()=>{
rythm.setMusic('./1.mp3')
rythm.start()
// rythm.addRythm('a', 'pulse', 0,2,{min:0.9,max:1.1})
// 脉冲变化
// rythm.addRythm('a', 'fontSize', 0, 2, { min: 0.5, max: 1.5 })
// 字体大小变化
// rythm.addRythm('a', 'jump', 0, 2, { })
// 上下
// rythm.addRythm('a', 'shake', 0, 2, { })
// 左右小幅度变化
// rythm.addRythm('a', 'twist', 0, 2, { })
// 旋转
// rythm.addRythm('a', 'vanish', 0, 2, { })
// 透明度变化
// rythm.addRythm('a','fontColor', 0,2, {from:[0,0,255],
// to:[255,0,255]}
// )
// 字体颜色
// rythm.addRythm('a','color', 0,2, {from:[0,0,255],
// to:[255,0,255]}
// )
// 背景颜色
// rythm.addRythm('a','borderColor', 0,2, {from:[0,0,255],
// to:[255,0,255]}
// )
// 边框颜色
// rythm.addRythm('a','neon', 0,2,{from:[0,0,255],
// to:[255,0,255]})
// 元素阴影
// rythm.addRythm('a','tile', 0,10,)
// 倾斜
// rythm.addRythm('a','borderWidth', 0,2,{
// min:2,max:10
// })
// rythm.addRythm('a','kern', 0,10,{
// min:-5,
// max:5
// })
// 字间距
// rythm.addRythm('a','blur', 0,10,{
// min:-5,
// max:5
// })
// 高斯模糊
// rythm.addRythm('a','swing', 0,10,{
// curve:'up',
// direction:'left',
// radius:10,
// })
// 左右摇摆
// rythm.addRythm('a','radius', 0,10,{
// min:0,
// max:5
// })
rythm.addRythm('a','radius', 0,10,{
min:0,
max:5
})
})
stop.addEventListener('click',()=>{
rythm.stop()
})
</script>
</body>
</html>
额外功能
1 .除了视频的基本功能,在传出频率的数据,来让别的东西根据频率生成可视化数据
2 .audio和video标签有哪些不同,他们是否可以通用,或者说是否可以直接把audio的音频地址传到video,把他当成没有画面的视频来操作。
3 .这样组件复用就会省很多的力气,甚至不用修改直接使用,唯一的就是多加一点判断条件
4 .可以这样使用就是一定要区分video和audio的属性和方法是否公用。初步上来看,应该是格式可能不兼容,也就是地址那里。但是chrome尝试是可以的。以及返回值的差距
5 .之前为了兼容都是一个video里面加好几个source标签
6 ..ogg, .wav和.mp3,常见音频格式.也可以使用MP4视频文件,因为MP4视频也包含ACC编码音频,不过就是体积大了很多,不建议这么使用。
7 .audio 属性
1 .src 音频的地址链接
2 .autoplay
3 .loop
4 .mute
5 .preload :none:表示在点击播放按钮之前不加载任何信息。
metadata: 下载音频的meta信息,就是视频长度,类型,还有作者(如果有)等信息。
auto: 会尝试下载整个音频,如今5G都快来了,流量已经不值钱了,因此,我个人是更推荐使用auto的,体验更好一点。然后,通常浏览器自己也会优化加载策略,不会所有音频文件都加载下来,只是会加载一部分,保证点击播放按钮的时候,可以立即播放
6 .controls
7 .type:指定音频文件的mine type类型
8 .Gecko内核浏览器速率范围是0.25到5.0,超出这个范围就静音 playbackRate
9 .loadstart → durationchange → loadedmetadata → loadeddata → progress → canplay → canplaythrough 音频事件触发顺序
10 .播放完毕
11 .本来这里想要加一些事件触发函数,比如播放完毕,播放开始,但是发现可以根据state返回的数据在组件外部计算得知,那其实那可以算吧
12 .
import * as React from 'react'
import {useEffect,useRef,useState,useCallback} from 'react'
import parseTimeRanges from './parseTimeRanges'
import useFullScreen from './useFullscreen'
// 这个全屏的应该放在外面,因为hook是不能嵌套的
interface HTMLMediaProps extends React.AudioHTMLAttributes<any>,React.VideoHTMLAttributes<any>{
src:string,
}
// xgplayer
interface HTMLMediaState{
buffered:any[],
duration:number,
paused:boolean,
muted:boolean,
time:number,
volume:number,
isFullScreen:boolean,
audioArray?:any[],
}
interface MediaProps{
type:"audio|video",
// 多媒体类型
src:string,
// 媒体资源的地址
autoPlay:boolean,
// 是否自动播放
controls:boolean,
// 是否显示媒体资源的组件
loop:boolean,
// 是否循环
muted:boolean,
// 是否静音
preload:string,
// 预加载模式
poster?:string,
// 预览海报
fftsize?:number,
}
interface HTMLMediaControls{
play:()=>Promise<void>|void,
pause:()=>void,
mute:()=>void,
unmute:()=>void,
volume:(volume:number)=>void,
seek:(time:number)=>void,
pip:()=>void,
speed:(value:number)=>void,
setFull:()=>void,
}
type createHTMLMediaHookReturn=[
React.ReactElement<HTMLMediaProps>,
HTMLMediaState,
HTMLMediaControls,
{current:HTMLMediaElement|null}
]
function useHTMLMedia(mediaConfig:any):createHTMLMediaHookReturn{
let el:React.ReactElement<any>|undefined
let props:HTMLMediaProps
const [state,setState]=useState<HTMLMediaState>({
buffered:[],
time:0,
duration:0,
paused:true,
muted:false,
volume:1,
isFullScreen:false,
audioArray:[],
})
const [hasConnect,setConnect]=useState(false)
// 是否注册声音绑定
const ref=useRef<HTMLMediaElement|null>(null)
const [isFullScreen,{setFull,exitFull,toggleFull}]=useFullScreen(ref)
const audioAnimation=useRef<number>(0)
const analyserRef=useRef<AnalyserNode>()
var draw=function(){
var bufferLength = analyserRef.current!.frequencyBinCount
var dataArray = new Uint8Array(bufferLength)
audioAnimation.current=requestAnimationFrame(draw)
analyserRef.current!.getByteFrequencyData(dataArray)
// // getByteFrequencyData得到的归一化数组的值在0到255之间
// // Web音频api返回的bin数量是fftSize的一半。
setState(Object.assign({},state,{audioArray:dataArray}))
}
const playAudio=useCallback(()=>{
if(!hasConnect){
var context = new(window.AudioContext)()
var analyser = context.createAnalyser()
analyser.fftSize = mediaConfig.fftSize
var source = context.createMediaElementSource(ref.current!)
source.connect(analyser)
analyser.connect(context.destination)
analyserRef.current=analyser
draw()
// 为什么这个传传进去就不行了
}else{
draw()
}
},[ref.current,hasConnect])
function onPlay(){
setState(Object.assign({},state,{paused:false}))
// 计算返回数据
if(fftSize){
console.log('playing')
playAudio()
setConnect(true)
}
}
function onPause(){
setState(Object.assign({},state,{paused:true}))
// 对象的时候都必须这样写
// 数组则是array.slice()
// 关闭动画
if(fftSize&&audioAnimation.current){
window.cancelAnimationFrame(audioAnimation.current)
}
}
function onVolumeChange(){
const el=ref.current
if(!el)return
setState(Object.assign({},state,{muted:el.muted,volume:el.volume}))
}
function onDurationChange(){
const el=ref.current
if(!el)return
const {duration,buffered,seekable}=el
// 音频被缓冲的部分
// seekable:是否可以调到改媒体的部分,而不需要进一步缓冲
setState(Object.assign({},state,{
duration,
buffered:parseTimeRanges(buffered)
}))
}
// 这些只是用来更新状态的,具体的操作逻辑应该在她之前,这些只是作为钩子函数来记录变化的数值
function onTimeUpdate(){
const el=ref.current
if(!el)return
setState(Object.assign({},state,{time:el.currentTime}))
}
function onProgress(){
const el=ref.current
if(!el)return
// 下载的时候触发
setState(Object.assign({},state,{buffered:parseTimeRanges(el.buffered)}))
}
let type=mediaConfig.type
// delete mediaConfig['type']
// delete mediaConfig['fftsize']
// 这俩属性是别的地方用到的,并不需要在真实的video/audio里面传进去
// console.log(mediaConfig)
const {fftSize,...mediaProps}=mediaConfig
if(fftSize){
mediaProps["crossOrigin"]="anonymous"
}
el=React.createElement(type,{
ref:ref,
...mediaProps,
onPlay,
onPause,
onVolumeChange,
onDurationChange,
onTimeUpdate,
onProgress,
},'对不起,你的浏览器不支持播放 video !')
let loclPlay:boolean=false
const controls={
play(){
const el:any=ref.current
if(!el)return
if(!loclPlay){
// 都需要强制转换
const promise=el.play()
const isPromise=typeof promise==='object'
if(isPromise){
loclPlay=true
const resetLock=()=>{
loclPlay=false
}
promise.then(resetLock,resetLock)
}
return promise
}
return undefined
},
pause(){
const el=ref.current
if(el&&!loclPlay){
return el.pause()
}
},
seek(time:number){
const el=ref.current
if(!el||state.duration===undefined)return
time=Math.min(state.duration,Math.max(0,time))
el.currentTime=time
},
volume(volume:number){
const el=ref.current
if(!el)return
volume=Math.min(1,Math.max(0,volume))
el.volume=volume
setState(Object.assign({},state,{volume:volume}))
},
mute(){
const el=ref.current
if(!el)return
el.muted=true
},
unmute(){
const el=ref.current
if(!el)return
el.muted=false
},
pip(){
let el:any=ref.current
let doc:any=document
if(mediaConfig['type']==='aduio')return
// 音频不支持小窗口
// 或者说可不可以还是创建视频的窗口,实际上来播音频呢。
// 用any承接,不然会提示没有这个属性
if(el!==doc.pictureInPictureElement){
el.requestPictureInPicture()
.catch((error:any)=>{
console.log('视频无法进入画中画模式!')
})
}else{
doc.exitPictureInPicture()
.catch((error:any)=>{
console.log('视频无法退出画中画模式!')
})
}
},
speed(value:number){
const el=ref.current
if(!el)return
let speeds=[0.5,1,2,3]
if(speeds.includes(value)){
el.playbackRate=value
}else{
console.log('不能修改限定之外的速度')
}
},
setFull,
}
useEffect(()=>{
const el=ref.current!;
if(!el){
return
}
setState(Object.assign({},state,{
volume:el.volume,
muted:el.muted,
paused:el.paused,
}))
if(mediaConfig.autoPlay&&el.paused){
controls.play()
}
},[mediaConfig.src,])
// 这里提示的补全有错误吧,还是按照自己的想法来,明确自己想要的效果是什么
// console.log(state)
return [el,state,controls,ref]
}
export default useHTMLMedia;
import useHtmlMedia from './useHTMLMedia'
interface MediaProps{
type:"audio",
// 多媒体类型
src:string,
// 媒体资源的地址
autoPlay:boolean,
// 是否自动播放
controls:boolean,
// 是否显示媒体资源的组件
loop:boolean,
// 是否循环
muted:boolean,
// 是否静音
preload:string,
// 预加载模式
fftSize?:number,
}
const initialMediaProps={
controls:false,
autoPlay:false,
loop:false,
muted:false,
type:'audio',
fftSize:1024,
}
function useAudio(mediaConfig:any){
const mediaProps:MediaProps=Object.assign({},initialMediaProps,mediaConfig)
return useHtmlMedia(mediaProps)
}
export default useAudio;
//使用
import useAudio from '../useAudio'
export default function(){
const [audio,state,controls,ref]=useAudio({
src:'https://m8.music.126.net/21180815163607/04976f67866d4b4d11575ab418904467/ymusic/515a/5508/520b/f0cf47930abbbb0562c9ea61707c4c0b.mp3?infoId=92001',
autoPlay:false,
controls:true,
fftSize:1024,
// 这里应该加入一些回调函数
})
return (
<>
{audio}
<hr/>
<pre>{JSON.stringify(state,null,2)}</pre>
<button onClick={controls.pause}>Pause</button>
<button onClick={controls.play}>Play</button>
<button onClick={controls.mute}>Mute</button>
<button onClick={controls.unmute}>unMute</button>
<br/>
{/* 传值的函数需要这么写 */}
<button onClick={() => controls.volume(.1)}>Volume: 10%</button>
<button onClick={() => controls.volume(.5)}>Volume: 50%</button>
<button onClick={() => controls.volume(1)}>Volume: 100%</button>
<br/>
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button>
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button>
<br/>
<button onClick={controls.pip}>pip</button>
<br/>
<button onClick={()=>controls.speed(0.5)}>speed:0.5</button>
<button onClick={()=>controls.speed(1)}>speed:1</button>
<button onClick={()=>controls.speed(2)}>speed:2</button>
<button onClick={()=>controls.speed(3)}>speed:3</button>
<br/>
<button onClick={controls.setFull}>fullScreen</button>
</>
);
}