Javascript[31天] 再探Audio 同步歌词,频谱,进度条控制

0

[31天] 再探Audio 同步歌词,频谱,进度条控制

阅读:82 时间:2022年06月16日

使用JavaScript AudioContext相关API实现的音乐播放,同步显示歌词(LRC),频谱以及进度显示和改变播放进度等。 其中进度相关的实现感觉还是不太对路,暂时也没有查到对路的方法。不清楚为什么context.currentTime不是buffer实时...

使用JavaScript AudioContext相关API实现的音乐播放,同步显示歌词(LRC),频谱以及进度显示和改变播放进度等。

其中进度相关的实现感觉还是不太对路,暂时也没有查到对路的方法。不清楚为什么context.currentTime不是buffer实时的currentTime。

对于这个问题,后面BufferSource换用HTML5 Audio标签(mediaElement)做为AudioContext就简单多了。。。应该是两者的应用场景不同,我还没搞清楚吧。

注意:以下内容是 createBufferSource() 的DEMO,最后面附上了Audio标签的实现

 

Demo的在线尝试: 再探Audio 同步歌词,频谱,进度条控制

没有教程,没得力气写了,只有实例,DEMO效果长这样:

微信截图_20220616154103.jpg

完整代码结构:

微信截图_20220616154009.jpg

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Life & Music</title>
  <link rel="stylesheet" href="./style/index.css">
</head>
<body>
<div class="screen">
  <h1>别错过</h1> 
  <article>
    <div class="play load"></div>
    <div class="lrc">
      <p class="before"></p>
      <p class="current"></p>
      <p class="next"></p>
    </div>
  </article> 
  <div class="processbar">
    <div class="processbar-side"></div>
  </div>
</div>
<canvas></canvas>
<script src="./script/music.js"></script>
</body>
</html>

CSS:

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background-color: #1d283d;
  background-image: linear-gradient(45deg, #1d283d, #15443e);
}

* {
  margin: 0;
  padding: 0;
  color: #e3f9ff;
}

body {
  position: relative;
}

.screen {
  position: relative;
  height: 100%;
  z-index: 2;
  display: flex;
  flex-direction: column;
  position: relative;
}

h1 {
  text-align: center;
  line-height: 100px;
}

@keyframes heart {
  0% {
    transform: scale(1, 1) rotate(-180deg);
  }
  40% {
    transform: scale(3, 1) rotate(180deg);
  }
  100% {
    transform: scale(1, 1) rotate(-180deg);
  }
}
article {
  flex: 1;
  text-align: center;
  overflow: hidden;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}
article .play {
  position: absolute;
  width: 100px;
  aspect-ratio: 1/1;
  border-radius: 50%;
  background-color: rgba(255, 255, 255, 0.048);
  left: 50%;
  top: 50%;
  transform: translate3d(-50%, -50%, 0);
  z-index: 2;
  overflow: hidden;
}
article .play.load::before {
  content: "";
  left: -20px;
  right: -20px;
  height: 100%;
  top: 45%;
  background-color: rgba(37, 128, 131, 0.3);
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  animation: heart 3s infinite;
}
article .play.loaded {
  cursor: pointer;
}
article .play.loaded:hover {
  box-shadow: 0 4px 8px rgba(18, 52, 56, 0.3);
}
article .play.loaded::before {
  content: "";
  position: absolute;
  height: 1px;
  width: 1px;
  border: 30px solid #fff;
  border-width: 20px 30px 20px 30px;
  border-color: transparent transparent transparent rgba(187, 245, 255, 0.5);
  top: 28px;
  left: 41px;
}
article p {
  line-height: 60px;
}
article .lrc .before, article .lrc .next {
  opacity: 0.5;
}
article .lrc .current {
  font-size: 20px;
}

.processbar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 10px;
  background-color: rgba(255, 255, 255, 0.1);
}
.processbar:hover {
  background-color: rgba(255, 255, 255, 0.2);
}
.processbar .processbar-side {
  height: 100%;
  width: 0%;
  background-color: rgba(0, 217, 255, 0.1);
  border-radius: 0 20px 20px 0;
  transition: 0.2s all linear;
}
.processbar .processbar-side:hover {
  background-color: rgba(0, 217, 255, 0.8);
}

canvas {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 0;
}

JavaScript:

const LRC = `
[00:00.00]程jiajia - 别错过
[00:00.15]作词:程jiajia
[00:00.30]作曲:程jiajia
[00:00.45]编曲:余威
[00:00.55]制作人:颜小健 @Jyken
[00:00.80]配唱制作人:吕宏斌
[00:01.10]和声:赫拉Hera
[00:01.26]吉他:LZY
[00:01.41]录音师:李妙心、候天宇
[00:01.71]混音师:裴东文
[00:01.91]母带后期工程师:裴东文
[00:02.27]混音、母带后期录音室:18 Music Studio
[00:02.67]推广:张作全@鲸鱼向海
[00:03.03]统筹:锦书
[00:03.18]监制:潇喆Sean
[00:03.38]录音棚:Hikoon Music、莫非录音棚(成都)
[00:03.78]OP、发行:鲸鱼向海(北京)文化有限公司
[00:04.34]「未经许可 不得翻唱翻录或使用」
[00:18.57]把你的心给我
[00:20.85]把你的爱给我
[00:23.28]这样我才能大胆尝试
[00:25.91]有更多的把握
[00:27.99]我要的也不多
[00:30.41]你不要嫌我啰嗦
[00:32.84]我只是十分害怕
[00:34.91]不小心与你错过
[00:37.94]我们辗转几何
[00:39.96]可结果又是如何
[00:42.65]没有任何意义
[00:44.83]其实你根本没爱过我
[00:47.20]脑袋空白在此刻
[00:50.24]我写了这首歌
[00:52.21]其实没什么舍不得
[00:54.64]只是眼睛酸涩全是红色
[01:16.09]把你的心给我
[01:18.42]把你的爱给我
[01:20.79]这样我才能大胆尝试
[01:23.52]有更多的把握
[01:25.59]我要的也不多
[01:27.98]你不要嫌我啰嗦
[01:30.40]我只是十分害怕
[01:32.47]不小心与你错过
[01:35.21]无处不在的难过
[01:37.89]又是谁的过错
[01:40.27]就此和你别过
[01:42.54]你会不会快乐
[01:44.87]这次我终于解脱
[01:47.75]等到了这一刻
[01:49.83]其实没什么舍不得
[01:52.21]好好学会得过且过
[02:04.34]我们辗转几何
[02:06.42]可结果又是如何
[02:09.10]没有任何意义
[02:11.17]其实你根本没爱过我
[02:13.60]脑袋空白在此刻
[02:16.63]我写了这首歌
[02:18.61]其实没什么舍不得
[02:21.03]只是眼睛酸涩全是红色
`
const LRCARR = LRC.split('\n')
LRCARR.pop()
LRCARR.shift() // header

class Music{
  constructor(canvas) {
    this.initCanvas(canvas)
    this.initMusic()
    this.start = 0
    this.startCurrent = 0
  }

  initCanvas(canvas) {
    this.canvas = canvas
    this.canvas.width = document.body.clientWidth
    this.canvas.height = document.body.clientHeight
    this.ctx = canvas.getContext('2d')
    this.ctx.fillStyle = '#1f5c55'
  }

  initMusic() {
    const musicSrc = '../music/bcg.mp3'
    const request = new XMLHttpRequest()
    request.responseType = 'arraybuffer'
    request.open('GET', musicSrc, true)
    request.onload = () => {
      document.querySelector('.play').className = 'play loaded'
      this.data = request.response
      this.playMusic(request.response)
    }
    request.send()
  }

  playMusic(data, duration = 0) {
    this.context = new AudioContext()
    this.analyser = this.context.createAnalyser()
    this.analyser.fftSize = 1024
    this.source = this.context.createBufferSource()
    this.source.connect(this.analyser)
    this.analyser.connect(this.context.destination)  
    this.context.decodeAudioData(data, buffer => {
      this.buffer = buffer
      this.source.buffer = buffer
      this.source.loop = true
      const playDom = document.querySelector('.play')
      playDom.addEventListener('click', () => {
        playDom.style.display = 'none'
        this.source.start(0, duration)
        this.animate()
      })
    })
   
    document.querySelector('.processbar').addEventListener('click', event => {
      this.setDuration(event.clientX / this.canvas.width)
    })
  }

  setDuration(duration = 0.0) {
    if (this.source) {
      this.source.stop()
    }
    this.source = this.context.createBufferSource()
    this.source.connect(this.analyser)
    this.source.buffer = this.buffer
    this.source.loop = true
    const currentTime = this.source.buffer.duration * duration
    this.source.start(0, currentTime)
    this.start = this.context.currentTime
    this.startCurrent = currentTime
  }

  setLrc(currentTime) {
    currentTime = currentTime % this.source.buffer.duration
    let text = ['', '', '']
    LRCARR.forEach((line, index) => {
      const time = line.substring(1,9).split(':')
      const durtion = parseInt(time[0]) * 60 + Number(time[1])
      if (currentTime > durtion) {
        text[0] = (LRCARR[index - 1] ?? '').substring(10)
        text[1] = line.substring(10)
        text[2] = (LRCARR[index + 1] ?? '').substring(10)
      }
    })
    document.querySelector('.before').innerHTML = text[0]
    document.querySelector('.current').innerHTML = text[1]
    document.querySelector('.next').innerHTML = text[2]
  }

  animate() {
    this.render()
    this.interval = requestAnimationFrame(() => {
      this.animate()
    })
  }

  render() {
    const width = this.canvas.width
    const height = this.canvas.height
    const length = this.analyser.frequencyBinCount*44100 / this.context.sampleRate|0
    const arrBuffer = new Uint8Array(length)
    this.analyser.getByteFrequencyData(arrBuffer)

    const BAR_WIDTH = 1
    const BAR_SPACE = 50
    const BAR_COUNT = Math.floor((width + BAR_SPACE) / (BAR_WIDTH + BAR_SPACE))

    this.ctx.clearRect(0, 0, width, height)
    for(let i = 0; i < BAR_COUNT; i++) {
      const index = Math.floor(arrBuffer.length / BAR_COUNT * i * 0.5)
      const barHeight = arrBuffer[index] / 255 * height
      this.ctx.beginPath()
      this.ctx.rect(i * (BAR_WIDTH + BAR_SPACE), 0, BAR_WIDTH, barHeight * 0.5)
      this.ctx.closePath()
      this.ctx.fill()
    }
    const currentTime = this.context.currentTime - this.start + this.startCurrent
    const duration = currentTime / this.source.buffer.duration * 100 % 100
    document.querySelector('.processbar-side').style.width = duration + '%'
    // document.querySelector('.processbar-side').innerHTML = currentTime
    this.setLrc(currentTime)
  }  

}

const music = new Music(document.querySelector('canvas'))

 

附:后面又尝试用Audio,其它部分不变,JS部分为:

const LRC = `
[00:00.00]程jiajia - 别错过
[00:00.15]作词:程jiajia
[00:00.30]作曲:程jiajia
[00:00.45]编曲:余威
[00:00.55]制作人:颜小健 @Jyken
[00:00.80]配唱制作人:吕宏斌
[00:01.10]和声:赫拉Hera
[00:01.26]吉他:LZY
[00:01.41]录音师:李妙心、候天宇
[00:01.71]混音师:裴东文
[00:01.91]母带后期工程师:裴东文
[00:02.27]混音、母带后期录音室:18 Music Studio
[00:02.67]推广:张作全@鲸鱼向海
[00:03.03]统筹:锦书
[00:03.18]监制:潇喆Sean
[00:03.38]录音棚:Hikoon Music、莫非录音棚(成都)
[00:03.78]OP、发行:鲸鱼向海(北京)文化有限公司
[00:04.34]「未经许可 不得翻唱翻录或使用」
[00:18.57]把你的心给我
[00:20.85]把你的爱给我
[00:23.28]这样我才能大胆尝试
[00:25.91]有更多的把握
[00:27.99]我要的也不多
[00:30.41]你不要嫌我啰嗦
[00:32.84]我只是十分害怕
[00:34.91]不小心与你错过
[00:37.94]我们辗转几何
[00:39.96]可结果又是如何
[00:42.65]没有任何意义
[00:44.83]其实你根本没爱过我
[00:47.20]脑袋空白在此刻
[00:50.24]我写了这首歌
[00:52.21]其实没什么舍不得
[00:54.64]只是眼睛酸涩全是红色
[01:16.09]把你的心给我
[01:18.42]把你的爱给我
[01:20.79]这样我才能大胆尝试
[01:23.52]有更多的把握
[01:25.59]我要的也不多
[01:27.98]你不要嫌我啰嗦
[01:30.40]我只是十分害怕
[01:32.47]不小心与你错过
[01:35.21]无处不在的难过
[01:37.89]又是谁的过错
[01:40.27]就此和你别过
[01:42.54]你会不会快乐
[01:44.87]这次我终于解脱
[01:47.75]等到了这一刻
[01:49.83]其实没什么舍不得
[01:52.21]好好学会得过且过
[02:04.34]我们辗转几何
[02:06.42]可结果又是如何
[02:09.10]没有任何意义
[02:11.17]其实你根本没爱过我
[02:13.60]脑袋空白在此刻
[02:16.63]我写了这首歌
[02:18.61]其实没什么舍不得
[02:21.03]只是眼睛酸涩全是红色
`
const LRCARR = LRC.split('\n')
LRCARR.pop()
LRCARR.shift() // header

class Music{
  constructor(canvas) {
    this.audio = new Audio()
    this.initCanvas(canvas)
    this.initMusic()
  }

  initCanvas(canvas) {
    this.canvas = canvas
    this.canvas.width = document.body.clientWidth
    this.canvas.height = document.body.clientHeight
    this.ctx = canvas.getContext('2d')
    this.ctx.fillStyle = '#1f5c55'
  }

  initMusic() {
    const musicSrc = '../music/bcg.mp3'
    this.audio.oncanplay = () => {
      document.querySelector('.play').className = 'play loaded'
      const playDom = document.querySelector('.play')
      playDom.addEventListener('click', () => {
        playDom.style.display = 'none'
        this.playMusic()
        this.animate()
      })      
    }
    this.audio.src = musicSrc
  }

  playMusic(duration = 0) {
    this.context = new AudioContext()
    this.analyser = this.context.createAnalyser()
    this.analyser.fftSize = 1024
    this.source = this.context.createMediaElementSource(this.audio)
    this.source.connect(this.analyser)
    this.analyser.connect(this.context.destination)  
    document.querySelector('.processbar').addEventListener('click', event => {
      this.setDuration(event.clientX / this.canvas.width)
    })
    this.source.mediaElement.play(duration)
  }

  setDuration(duration = 0.0) {
    const currentTime = this.source.mediaElement.duration * duration
    this.source.mediaElement.currentTime= currentTime
  }

  setLrc(currentTime) {
    currentTime = currentTime % this.source.mediaElement.duration
    let text = ['', '', '']
    LRCARR.forEach((line, index) => {
      const time = line.substring(1,9).split(':')
      const durtion = parseInt(time[0]) * 60 + Number(time[1])
      if (currentTime > durtion) {
        text[0] = (LRCARR[index - 1] ?? '').substring(10)
        text[1] = line.substring(10)
        text[2] = (LRCARR[index + 1] ?? '').substring(10)
      }
    })
    document.querySelector('.before').innerHTML = text[0]
    document.querySelector('.current').innerHTML = text[1]
    document.querySelector('.next').innerHTML = text[2]
  }

  animate() {
    this.render()
    this.interval = requestAnimationFrame(() => {
      this.animate()
    })
  }

  render() {
    const width = this.canvas.width
    const height = this.canvas.height
    const length = this.analyser.frequencyBinCount*44100 / this.context.sampleRate|0
    const arrBuffer = new Uint8Array(length)
    this.analyser.getByteFrequencyData(arrBuffer)

    const BAR_WIDTH = 5
    const BAR_SPACE = 10
    const BAR_COUNT = Math.floor((width + BAR_SPACE) / (BAR_WIDTH + BAR_SPACE))

    this.ctx.clearRect(0, 0, width, height)
    for(let i = 0; i < BAR_COUNT; i++) {
      const index = Math.floor(arrBuffer.length / BAR_COUNT * i * 0.5)
      const barHeight = arrBuffer[index] / 255 * height
      this.ctx.beginPath()
      this.ctx.rect(i * (BAR_WIDTH + BAR_SPACE), 0, BAR_WIDTH, barHeight * 0.5)
      this.ctx.closePath()
      this.ctx.fill()
    }
    const currentTime = this.source.mediaElement.currentTime
    const duration = currentTime / this.source.mediaElement.duration * 100 % 100
    document.querySelector('.processbar-side').style.width = duration + '%'
    this.setLrc(currentTime)
  }  

}

const music = new Music(document.querySelector('canvas'))

 

 

相关资料: https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext

发表评论说说你的看法吧

精品模板蓝瞳原创精品网站模板

^