useAudio

2020-10-15  本文已影响0人  skoll

音频可视化

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>
        
    </>
  );
}
上一篇下一篇

猜你喜欢

热点阅读