用Vue如何从0开始做一个在线的web音频编辑器

2,395

一、音频编辑器是什么?

  • 音频编辑器是一款在线的音频编辑软件,供up主或专业的音乐制作人使用,需要具备一定的乐理知识。比如认识节拍、伴奏、小节、4/4音符、音高、响度、张力等等。这样他们可以创作出一些音频出来。同时提供了对应的音源、歌曲等,提高他们的创作效率

  • 在线web音频编辑器链接:yan.qq.com/audioEditor这个链接因为项目暂停已经无法访问

  • 编辑器使用的技术包括:Vue2.x + Vuex + Vue-cli + Vue-router + svg + axios + element-ui 等

二、音频编辑器怎么用?

音频编辑器.gif

三、音频编辑器怎么做?

【1】、实现基本原理

  • 音高块:用鼠标在页面画音块,每个音块有自己宽高坐标位置,单位为px -> 通过bpm(曲速)节拍与px的相应关系换算成时间 -> 合成每个音块的时间去给后端合成 -> 合成音频链接后回来播放并移动播放线。
  • 音高线:获取用户在屏幕上鼠标移动的位置->将这些位置与AI返回的这首歌该有的音高线进行拟合,从而得到一条用户画的音高线->再根据节拍、曲速和px的对应关系换算成数据集合,传递给后端,从而合成一首歌-> 后端返回这首歌的链接给我,在前端进行播放,并做相应操作。

【2】、整体数据架构设计

整体数据结构.png

  • 整个编辑器的数据设计大概如上图所示,主要是依托Vuex这个数据管理仓库,在开发大型单页应用的时候,可以帮助我们管理共享状态,管理整个页面的数据流向。主要管理的数据分成几大模块:
    • 1、编辑器的基本元素,如:曲速、音源、整个舞台相关、宽度、高度、节拍、播放线等。
    • 2、存储整个舞台的音块数据:stagePitches, 这个在合成的时候会转成 pitchList,给后台合成声音音频并回来播放。
    • 3、音高线:分成了AI合成的参考音高线和用户编辑过的音高线,还有px对应本地编辑部分。
    • 4、操作标志:因为这个编辑器的合成依托了很多状态的修改,一修改就会要求去判断是否需要合成播放,和接下来要说的播放暂停控制状态来回切换强相关
    • 5、模式的切换:如音符/音高线/音素模式切换

【3】、整体组件框架

音频编辑器.png

【4】、重难点突破

(1)、播放状态切换的问题 -> 使用有限状态机解决:

image.png播放控制按钮一点击就有几个状态,每个状态需要不同处理,同时里面的操作又有相同的操作,互相耦合,这时如何分配就成了一个问题。后面梳理了下,参考了一些源码,就发现这是一个状态机的转换。接下来如何做呢?具体如下:

1. 首先,列出页面需要用到的所有状态

image.png

2. 一开始代码是这样的,有很多的if-else,每个if else里面又有很多判断。

image.png

3. 改造成状态机是如下的形式。

image.png

  • 为什么要改造成如下的方式呢,主要是代码中有太多的if else会导致扩展性比较差,而后面如果你要扩展新的状态就会不知道从何入手,而使用状态机的方式,就可以不断的往里面扩展新的状态。这也是参考了Typescript源码是利用状态机使流程更清晰。
Typescript源码中的状态机
  • 首先 tsc 划分了很多状态,每种状态处理一种逻辑。比如: 1). CreateProgram 把源码 parse 成 ast 2). SyntaxDiagnostics 处理语法错误 3). SemanticDiagnostics 处理语义错误 然后Typescript 就通过这种状态的修改来完成不同处理逻辑的流转,如果处理到结束状态就代表流程结束。这样使得整体流程可以很轻易的扩展和修改,比如想扩展一个阶段,只要增加一个状态,想修改某种状态的处理逻辑,只需要修改下状态机的该状态的转向。而不是大量的 if else 混杂在一起,难以扩展和修改。

image.png

4. 最终就形成状态机,状态切换流程图如下:

播放状态的转换.png

页面一来都是一个初始状态,当去播放的时候,就会切换到播放状态,这时如果还在播放,点击那就是暂停播放,并且切换到暂停状态,然后当暂停状态去播放的时候,又会切换到播放状态,播放状态完了,会切换到结束状态,结束状态再去播放,会重新切换到播放状态。直到整个音频结束。

5. 另外在流程图中,我们发现每次播放的时候,都需要去判断是否需要合成,这时候就用到了上面说到的操作标志状态的监听,就是只要有一个状态改变了,就会去重新合成。

image.png

(2)、播放进度不流畅的问题 -> 借用requestAnimationFrame去解决

去播放之后,在播放的时候,出现播放进度不流畅。出现线移动很卡顿的问题。这个是为什么呢,主要是因为浏览器16ms渲染一次页面。那为什么浏览器16ms渲染一次页面呢?

  • 因为现在广泛使用的屏幕都有固定的刷新率(比如最新的一般在 60Hz), 在两次硬件刷新之间浏览器进行两次重绘是没有意义的只会消耗性能。所以浏览器会利用这个间隔 16ms(1000ms/60)适当地对绘制进行节流
  • 这时候就需要使用requestAnimationFrame去解决,为什么他可以解决呢?这个主要就是因为浏览器的为了让开发者能把握渲染前的那个点,在每次渲染之前,执行完宏任务后去执行requestAnimationFrame。之后,再去执行下一次渲染,当然执行宏任务之前要先把宏任务里面的微任务先执行完。具体的步骤就是:执行本次宏任务下的所有微任务 -> 执行本次宏任务 -> 执行requestAnimationFrame(如果有) ->执行下一次宏任务下的所有微任务 -> ....,以此类推。
  1. 首先声明一个playAudio的方法,这个方法就是负责播放这个音频,设置完音频的基本属性,如播放链接等,监听播放的属性。 image.png 2.接着在播放的时候监听到播放了,就移动播放线,怎么移动呢,使用requestAnimationFrame在播放的时候将线进行移动。就能解决播放进度不流畅的问题。

image.png

(3)、拖音块的时候,鼠标移出了音块,失去了焦点,怎么解决这个问题? -> 借助虚拟块

  • 我们在用鼠标移动音块,可能不小心触碰了鼠标滚动按钮,导致鼠标和音块失去了焦点,就不能脱离了。问题展示如下

鼠标和音符粘住了,甩不掉.gif

  • 这个主要是参考了chrome浏览器的做法,鼠标点击后粘住了一个虚拟的块,这样鼠标永远就不会出现失去焦点的情况

  • 解决方案:在点击鼠标的时候,起一个蒙层,透明的,粘住鼠标,鼠标到哪里,蒙层就跟到哪里,然后当鼠标一放开的时候就去掉这个蒙层。从而达到比较顺滑的效果。下面就是有一个透明的红色透明的蒙层盖在音块上面,然后就永远都不会失去焦点了。 image.png

(4)、钢琴键如何发音的问题 -> 借助Web Audio API

image.png

  • 点击左边这个钢琴键,要出声音,怎么出呢? 问题就是音源怎么来,再如何定位一个他的音高。
  • 首先音源很简单,加载一个C4标准音C4.mp4 然后通过Web Audio API 这个,然后拿到这段音频之后转成二进制数据流,然后将这段音频的音高进行偏移,偏移的公式主要是playback-rate(a,b)=2 ** ((note - 60) / 12)。60就是C4的音高,然后note就是传入的note值,然后进行音高的转换。 具体可以参考这篇文章zpl.fi/pitch-shift… 实现出的代码就是如下:

image.png

(5)、音高线的相关问题

image.png

1. 音高线画线技术选型,调研了一波,对比了canvas和svg.
  • canvas:优点:性能好,流畅。缺点:前期需要搭建大量的基础代码,来操作,而且绘制的时候需要的工作量也很大。代码比较多。
  • svg: 优点:是一个dom元素,自带一些基本dom操作的功能。缺点:性能不是很好,因为直接操作dom会引起整个浏览器的重排,重排就会导致浏览器的重新渲染过程。但是写的代码量少很多。
  • 综合考虑,这是一个pc端页面,用户的电脑性能肯定比手机h5页面好,而且时间紧张,就采用了svg.
2. 音高线如何画 -> 借助svg
  • 使用svg的path 属性通过音块解析出来的音高线的每隔10ms的点,转换成屏幕上对应的px,然后将每个点绘上去。
3. 画线出现锯齿怎么解决 -> 补帧

音高线出现锯齿.gif

  • 如上所示,当鼠标移动的非常快的时候,音高线出现锯齿,这是什么原因呢。主要也是浏览器每隔16ms渲染一次页面导致。当鼠标移动非常快,浏览器来不及渲染,就丢失鼠标经过的位置,这时候应该怎么办呢,就只能补桢,把丢失的数据补回去。具体实现如下:

    image.png 这里补帧逻辑就是算出上一个鼠标的x轴和现在的x轴,然后得出他们的y轴相差的距离,然后循环下,将数据给补上去。

4. 数据量大的问题怎么解决 -> 优化字符串操作+分组渲染

画线慢的问题.gif

  • 当数据量非常大的时候,就会出现浏览器渲染慢的问题,导致音高线的修改不会跟着鼠标走,这个主要是因为数据量非常大的时候,dom渲染太大。所以我进行了分组渲染+字符串操作改成数组操作。从原来的一个svg放所有的数据,变成多个svg分开放数组,这样更改的时候,只改了这个svg中的数据。 image.png
5. 切换bpm后怎么解决音高线突变问题 -> 拦截数据请求,在数据请求前保存不变,数据请求后才改变

音高线突变.gif

  • 因为我们音高线和bpm是强相关的(因为我们需要AI的参考线)。所以当bpm一改的时候。线就会突变,这是因为Vue是MVVM模式的,当数据一改的时候,页面就会相应改动,再去拿到音高线AI合成的数据后去拟合。最终得到我们想要的。所以就会有一个变化的过程

  • 这个问题怎么解决呢,就只能是在去获取音高线的时候,等数据来之前先把bpm设置回原来的bpm,然后再改回新的bpm.代码如下:

image.png

(6)、撤回(ctrl+z)和取消撤回(ctrl+y)快捷键 -> 使用命令模式进行解决。

这里主要是参考了three.js中编辑器的做法,使用命令模式,将需要undo和redo的操作都放在command里面去处理。

three.js

image.png 查看three.js中带的编辑器的源码可以看到,里面有一个自带的commands文件,然后放着所有需要撤回的操作。每个command里面都要自己写undo和redo.

所以借着three.js的思路,就来设计自己编辑器的undo和redo.

  1. 首先定义一个history.js文件,来存储所有undo和redo,然后定义两个栈去保存所有的操作,分别是撤回栈和前进栈。然后每次操作的时候,将操作放到撤回栈中,当需要撤回的时候,将撤回栈中最后一个拿出来,然后执行,顺便给前进栈放进去。当需要前进的时候,将前进栈中最后一个拿出来,执行,并且将这个操作放进撤回栈中,然后下次需要撤回,可以在撤回栈中继续执行。 具体代码如下: image.png
  2. 定义一个编辑器类,将我们的history引入进来,同时注册到全局,并且提供相应的undo.redo等方法。

image.png

3.在需要撤回的地方就注册相应的command,并在command里面做相应操作。 比如在删除音块的时候注册command

image.png 在DeletePitchCommand中做相应操作

image.png

这样就实现了undo和redo了。

(7)、如何实现开发一次,三端(mac/windows/web)都能用 -> 借助electron

调研了一下现在市场上的方案,主要我们前端常用的vscode都用的electron去打包实现pc客户端,而且比较成熟了,所以就选用electron去打包我的代码,然后生成客户端安装包,在页面上有入口image.png点击之后就可以去下载了。具体实现,就是借助electron中是带了chrome浏览器内核+node.js机制,引入打包工具,这样就可以实现跑浏览器代码了。

四、源码链接

因为项目暂停,这个已经暂停维护,但是源码贡献出来,可以自己试试。 音频编辑器源码:https://github.com/designerbaby/yanliao

以上,就是我的所有分享,如有错误与遗漏,望指出。