使用js实现音谱

使用js实现音谱,网页音谱、音谱实现

阅读量:1134

点赞量:14

评论量:0
Skill
标签摘要
使用js实现音谱,网页音谱、音谱实现
正文


效果:

screenshots.gif

实现思路:

1、创建音频上下文。

2、创建音频乐谱分析仪

3、将分析出的数据画到canvas上。

实现步骤:

1、创建音频上下文

this.audioContext = new AudioContext()

2、创建源及乐谱分析仪,并链接

// 创建音频源节点
this.source = this.audioContext.createMediaElementSource(this.audioElement)
// 创建乐谱分析器
this.analyser = this.audioContext.createAnalyser()
// 设置分析精度
this.analyser.fftSize = this.options.fftSize
// 将源链接到分析器
this.source.connect(this.analyser)
//将分析器链接到音频上下文目的地
this.analyser.connect(this.audioContext.destination)

3、绘制到canvas上

// 清除画布
const { width, height } = this.canvasElement
this.ctx.clearRect(0, 0, width, height)
// 让分析器节点将分析出的数据放到存储容器内
this.analyser.getByteFrequencyData(this.dataArray)
// 计算要绘制的区域
const l = this.dataArray.length * (this.options.range / 100)
// 计算每个块的宽度
const barWidth = width / l / 2
this.ctx.fillStyle = this.options.color
for (let i = 0; i < l; i++) {
  // 获取每个的数据,此值小于256
  const item = this.dataArray[i]
  // 计算块高度
  const h = (item / 255) * height
  // 计算块开始的y
  const y = height - h
  // 计算块开始的x,对称
  const x_r = i * barWidth + width / 2
  const x_l = width - x_r
  this.ctx.fillRect(x_r, y, barWidth, height)
  this.ctx.fillRect(x_l, y, barWidth, height)
}

4、使用requestAnimationFrame重复绘制

this.animateId = requestAnimationFrame(this.draw.bind(this))

完整代码:

music-score.ts

interface IOptionsType {
  // audio元素|audio元素选择器|网络音频地址
  audio: string | HTMLAudioElement
  canvas: string | HTMLCanvasElement
  fftSize?: number
  color?: string
  range?: number
}

export class MpMusicScore {
  private options: Partial<IOptionsType> = {
    fftSize: 512,
    color: '#9adcff',
    range: 70
  }
  private audioElement: HTMLAudioElement
  private canvasElement: HTMLCanvasElement
  private audioContext: AudioContext
  private analyser: AnalyserNode
  private source: MediaElementAudioSourceNode
  private dataArray: Uint8Array
  private animateId: number
  private ctx: CanvasRenderingContext2D
  private isInitAudio: boolean = false

  constructor(options: IOptionsType) {
    this.options = { ...this.options, ...options }
    this.init()
  }

  // 初始化参数
  private init() {
    // 获取音频元素
    if (typeof this.options.audio === 'string') {
      if (
        this.options.audio.includes('//') &&
        this.options.audio.endsWith('.mp3')
      ) {
        this.audioElement = new Audio(this.options.audio)
      } else {
        this.audioElement = document.querySelector(this.options.audio)
      }
    } else if (this.options.audio instanceof HTMLAudioElement) {
      this.audioElement = this.options.audio
    }
    if (!(this.audioElement instanceof HTMLAudioElement)) {
      throw '音频不存在!'
    }

    // 获取画布元素
    if (typeof this.options.canvas === 'string') {
      this.canvasElement = document.querySelector(this.options.canvas)
    } else if (this.options.canvas instanceof HTMLCanvasElement) {
      this.canvasElement = this.options.canvas
    }
    if (!(this.canvasElement instanceof HTMLCanvasElement)) {
      throw '画布不存在!'
    }

    if (Math.log2(this.options.fftSize) % 1 !== 0) {
      throw 'fftSize为2的n次幂!'
    }

    // 初始化画布及画布大小
    this.ctx = this.canvasElement.getContext('2d')
    const { clientWidth, clientHeight } = this.canvasElement
    this.canvasElement.width = clientWidth
    this.canvasElement.height = clientHeight

    // 添加绘制事件
    this.audioElement.addEventListener('play', this.draw.bind(this))
    this.audioElement.addEventListener('pause', this.cancelDraw.bind(this))
  }

  // 初始化音频
  private initAudio() {
    if (this.isInitAudio) {
      return
    }
    this.isInitAudio = true
    // 创建audio上下文
    this.audioContext = new AudioContext()
    // 创建音频源节点
    this.source = this.audioContext.createMediaElementSource(this.audioElement)
    // 创建乐谱分析器
    this.analyser = this.audioContext.createAnalyser()
    // 设置分析精度
    this.analyser.fftSize = this.options.fftSize
    // 将源链接到分析器
    this.source.connect(this.analyser)
    //将分析器链接到音频上下文目的地
    this.analyser.connect(this.audioContext.destination)
    // 创建乐谱存储容器
    this.dataArray = new Uint8Array(this.analyser.frequencyBinCount)
  }

  // 绘制乐谱
  private draw() {
    // 初始化Audio
    this.initAudio()
    this.animateId = requestAnimationFrame(this.draw.bind(this))
    // 清除画布
    const { width, height } = this.canvasElement
    this.ctx.clearRect(0, 0, width, height)
    // 让分析器节点将分析出的数据放到存储容器内
    this.analyser.getByteFrequencyData(this.dataArray)
    // 计算要绘制的区域
    const l = this.dataArray.length * (this.options.range / 100)
    // 计算每个块的宽度
    const barWidth = width / l / 2
    this.ctx.fillStyle = this.options.color
    for (let i = 0; i < l; i++) {
      // 获取每个的数据,此值小于256
      const item = this.dataArray[i]
      // 计算块高度
      const h = (item / 255) * height
      // 计算块开始的y
      const y = height - h
      // 计算块开始的x,对称
      const x_r = i * barWidth + width / 2
      const x_l = width - x_r
      this.ctx.fillRect(x_r, y, barWidth, height)
      this.ctx.fillRect(x_l, y, barWidth, height)
    }
  }

  // 取消绘制
  private cancelDraw() {
    cancelAnimationFrame(this.animateId)
  }

  // 播放
  public play() {
    this.audioElement.play()
  }

  // 暂停
  public pause() {
    this.audioElement.pause()
  }

  // 获取audio元素
  public getAudioElement() {
    return this.audioElement
  }
}

使用方法:

接收一个配置对象,有5个可配置属性:

audio(audio元素 | audio元素选择器 | 网络音频地址)、

canvas(canvas元素或选择器)、

fftSize(精度,越大越精细,必须为2的n次幂)、

color(乐谱块颜色)、

range(乐谱的取值,因乐谱后面的频率一般为0,没有显示,可以通过此值,只取前面部分)。

使用案例:

new MpMusicScore({
  audio: 'audio',
  canvas: 'canvas',
  color: '#58ee43',
  range: 50,
  fftSize: 1024
})
const score = new MpMusicScore({
  audio: 'http://127.0.0.1:5173/1.mp3',
  canvas: 'canvas',
  color: 'red',
  range: 75,
  fftSize: 2048
})
setTimeout(() => {
  score.play()
}, 1000)

结语:

文章参考:https://v.douyin.com/B7tMkcK/

发布于: 2/20/2023, 5:39:36 PM
最后更新: 12/2/2024, 7:21:45 PM