用Vue如何從0開始作一個在線的web音頻編輯器

1、音頻編輯器是什麼?

  • 音頻編輯器是一款在線的音頻編輯軟件,供up主或專業的音樂製做人使用,須要具有必定的樂理知識。好比認識節拍、伴奏、小節、4/4音符、音高、響度、張力等等。這樣他們能夠創做出一些音頻出來。同時提供了對應的音源、歌曲等,提升他們的創做效率
  • 在線web音頻編輯器連接:yan.qq.com/audioEditor
  • 編輯器使用的技術包括:Vue2.x + Vuex + Vue-cli + Vue-router + svg + axios + element-ui 等

2、音頻編輯器怎麼用?

音頻編輯器.gif 下面是幾個使用的視頻說明前端

3、音頻編輯器怎麼作?

【1】、實現基本原理

  • 音高塊:用鼠標在頁面畫音塊,每一個音塊有本身寬高座標位置,單位爲px -> 經過bpm(曲速)節拍與px的相應關係換算成時間 -> 合成每一個音塊的時間去給後端合成 -> 合成音頻連接後回來播放並移動播放線。
  • 音高線:獲取用戶在屏幕上鼠標移動的位置->將這些位置與AI返回的這首歌該有的音高線進行擬合,從而獲得一條用戶畫的音高線->再根據節拍、曲速和px的對應關係換算成數據集合,傳遞給後端,從而合成一首歌-> 後端返回這首歌的連接給我,在前端進行播放,並作相應操做。

【2】、總體數據架構設計

總體數據結構.png

  • 整個編輯器的數據設計大概如上圖所示,主要是依託Vuex這個數據管理倉庫,在開發大型單頁應用的時候,能夠幫助咱們管理共享狀態,管理整個頁面的數據流向。主要管理的數據分紅幾大模塊:
    • 一、編輯器的基本元素,如:曲速、音源、整個舞臺相關、寬度、高度、節拍、播放線等。
    • 二、存儲整個舞臺的音塊數據:stagePitches, 這個在合成的時候會轉成 pitchList,給後臺合成聲音音頻並回來播放。
    • 三、音高線:分紅了AI合成的參考音高線和用戶編輯過的音高線,還有px對應本地編輯部分。
    • 四、操做標誌:由於這個編輯器的合成依託了不少狀態的修改,一修改就會要求去判斷是否須要合成播放,和接下來要說的播放暫停控制狀態來回切換強相關
    • 五、模式的切換:如音符/音高線/音素模式切換

【3】、總體組件框架

音頻編輯器.png

【4】、重難點突破

(1)、播放狀態切換的問題 -> 使用有限狀態機解決:

image.png播放控制按鈕一點擊就有幾個狀態,每一個狀態須要不一樣處理,同時裏面的操做又有相同的操做,互相耦合,這時如何分配就成了一個問題。後面梳理了下,參考了一些源碼,就發現這是一個狀態機的轉換。接下來如何作呢?具體以下:node

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 混雜在一塊兒,難以擴展和修改。ios

image.png

4. 最終就造成狀態機,狀態切換流程圖以下:

播放狀態的轉換.png

頁面一來都是一個初始狀態,當去播放的時候,就會切換到播放狀態,這時若是還在播放,點擊那就是暫停播放,而且切換到暫停狀態,而後當暫停狀態去播放的時候,又會切換到播放狀態,播放狀態完了,會切換到結束狀態,結束狀態再去播放,會從新切換到播放狀態。直到整個音頻結束。web

5. 另外在流程圖中,咱們發現每次播放的時候,都須要去判斷是否須要合成,這時候就用到了上面說到的操做標誌狀態的監聽,就是隻要有一個狀態改變了,就會去從新合成。

image.png

(2)、播放進度不流暢的問題 -> 借用requestAnimationFrame去解決

去播放以後,在播放的時候,出現播放進度不流暢。出現線移動很卡頓的問題。這個是爲何呢,主要是由於瀏覽器16ms渲染一次頁面。那爲何瀏覽器16ms渲染一次頁面呢?chrome

  • 由於如今普遍使用的屏幕都有固定的刷新率(好比最新的通常在 60Hz), 在兩次硬件刷新之間瀏覽器進行兩次重繪是沒有意義的只會消耗性能。因此瀏覽器會利用這個間隔 16ms(1000ms/60)適當地對繪製進行節流
  • 這時候就須要使用requestAnimationFrame去解決,爲何他能夠解決呢?這個主要就是由於瀏覽器的爲了讓開發者能把握渲染前的那個點,在每次渲染以前,執行完宏任務後去執行requestAnimationFrame。以後,再去執行下一次渲染,固然執行宏任務以前要先把宏任務裏面的微任務先執行完。具體的步驟就是:執行本次宏任務下的全部微任務 -> 執行本次宏任務 -> 執行requestAnimationFrame(若是有) ->執行下一次宏任務下的全部微任務 -> ....,以此類推。
  1. 首先聲明一個playAudio的方法,這個方法就是負責播放這個音頻,設置完音頻的基本屬性,如播放連接等,監聽播放的屬性。

image.png 2.接着在播放的時候監聽到播放了,就移動播放線,怎麼移動呢,使用requestAnimationFrame在播放的時候將線進行移動。就能解決播放進度不流暢的問題。element-ui

image.png

(3)、拖音塊的時候,鼠標移出了音塊,失去了焦點,怎麼解決這個問題? -> 藉助虛擬塊

  • 咱們在用鼠標移動音塊,可能不當心觸碰了鼠標滾動按鈕,致使鼠標和音塊失去了焦點,就不能脫離了。問題展現以下

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

  • 這個主要是參考了chrome瀏覽器的作法,鼠標點擊後粘住了一個虛擬的塊,這樣鼠標永遠就不會出現失去焦點的狀況canvas

  • 解決方案:在點擊鼠標的時候,起一個蒙層,透明的,粘住鼠標,鼠標到哪裏,蒙層就跟到哪裏,而後當鼠標一放開的時候就去掉這個蒙層。從而達到比較順滑的效果。下面就是有一個透明的紅色透明的蒙層蓋在音塊上面,而後就永遠都不會失去焦點了。axios

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… 實現出的代碼就是以下:windows

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機制,引入打包工具,這樣就能夠實現跑瀏覽器代碼了。

4、對將來的展望

  1. svg換成canvas實現,可是須要構建canvas基礎事件,並且也會遇到瀏覽器渲染問題,這個考慮是否要這樣作。
  2. 將音源識別出來與數據合成一首歌?若是能實現的話,能夠解決用戶調試等待時間的問題與消耗機器和CPU資源的問題,有待驗證。
  3. 若是有時間的話,能夠將代碼重構成Vue3+ Typescript

以上,就是個人全部分享,若有錯誤與遺漏,望指出。