使用 electron-vue 實(shí)現(xiàn) B站上很火的 音頻可視化播放器

本人博客文章地址:點(diǎn)擊進(jìn)入

項(xiàng)目地址:https://github.com/geminate/mwave
下載地址:https://pan.baidu.com/s/1boIHExuBOkMqzXVEE4u3gA 僅為測試項(xiàng)目

一. 啟發(fā)
經(jīng)常逛B站的小伙伴們中應(yīng)該不少看到過 使用 AE 制作的音頻可視化視頻蔑舞,例如


image

image

image

看起來是不是很酷炫,當(dāng)初我還傻傻的在視頻下面問別人這是什么播放器QAQ靡挥,其實(shí)這種視頻都是使用 AE 做的視頻儿倒,并沒有相關(guān)的播放器實(shí)現(xiàn)這種效果凄诞。

由于本人的開發(fā)方向是前端恐疲,猛然想起之前看到的 electron 這個(gè)使用前端語言寫桌面程序的開源項(xiàng)目让禀,之后點(diǎn)進(jìn)去了解了一下屁桑,感覺可以嘗試實(shí)現(xiàn)一下上圖中的效果。于是花了幾天的閑暇時(shí)間搞出了這么個(gè) ***僅服務(wù)于本人興趣與學(xué)習(xí) ***的 玩意赵讯。

本人參照的模板是上圖中的第三個(gè)盈咳,未聞花名那張(B站上的視頻連接),最終完成的播放器效果上來看大約有 視頻中的 70% 左右边翼,沒做到視頻里那么好看(主要是時(shí)間原因鱼响,粒子效果、彩色漸變和波形的細(xì)致過濾沒做)组底,如果硬要做的話應(yīng)該能實(shí)現(xiàn)的差不多丈积,有興趣的前端小伙伴可以自己嘗試下。

下面這張是最終的實(shí)現(xiàn)效果Gif

image

二. Electron 相關(guān)

1. 項(xiàng)目結(jié)構(gòu)

image

項(xiàng)目使用的是 electron-vue 作為骨架债鸡,如上圖江滨。其中renderer文件夾中的東西和Vue項(xiàng)目基本一致,多出來的東西是main文件夾和外面的IPC.js娘锁。main文件夾里面的東西是主進(jìn)程文件,renderer則是渲染進(jìn)程文件饺鹃。IPC.js用來在在兩個(gè)進(jìn)程中定義通信渠道莫秆。

2. 主進(jìn)程與渲染進(jìn)程

electron中一個(gè)很重要的概念就是主進(jìn)程與渲染進(jìn)程,簡單來說 主進(jìn)程負(fù)責(zé)操作系統(tǒng)相關(guān)的操作悔详,而渲染進(jìn)程則負(fù)責(zé)使用前端語言實(shí)現(xiàn)界面展示镊屎,運(yùn)行在webview中。而兩個(gè)進(jìn)程之間的通信則要通過 IPC 通道實(shí)現(xiàn)茄螃。在任意一端監(jiān)聽一條消息缝驳,之后在另一端發(fā)出這條消息即可,需要注意的是归苍,渲染進(jìn)程->主進(jìn)程用狱、主進(jìn)程->渲染進(jìn)程的消息發(fā)送略有不同。

主進(jìn)程發(fā)消息拼弃,渲染進(jìn)程接收消息:

// 主進(jìn)程使用minWindow發(fā)送消息
mainWindow.webContents.send(IPC.SET_MUSIC_LIST, {message:'message'});

// 渲染進(jìn)程使用 electron.ipcRenderer 監(jiān)聽消息
import electron from 'electron';

electron.ipcRenderer.on(IPC.SET_MUSIC_LIST, (event, message) => {
    console.log(message);
});

渲染進(jìn)程發(fā)消息夏伊,主進(jìn)程接收消息:

// 渲染進(jìn)程使用 electron.ipcRenderer 發(fā)送消息
import electron from 'electron';
 
electron.ipcRenderer.send(IPC.RENDER_READY,{message:'message'});
 
//主進(jìn)程使用 electron.ipcMain 監(jiān)聽消息
import electron from 'electron';
 
electron.ipcMain.on(IPC.RENDER_READY, (event, arg) => {
    console.log(arg);
});

使用起來相當(dāng)方便,而主進(jìn)程和渲染進(jìn)程內(nèi)部通信與狀態(tài)管理則分別用各自的store實(shí)現(xiàn)吻氧。主進(jìn)程涉及用戶配置的可直接以文件形式保存在用戶文件夾溺忧,而Vue的狀態(tài)管理直接使用Vuex即可咏连。

3. window 創(chuàng)建

function createWindow() {
    mainWindow = new BrowserWindow({
        height: 600,
        width: 600,
        titleBarStyle: 'hidden-inset',
        frame: false,
        transparent: true,
    });
    mainWindow.loadURL(winURL);
    mainWindow.on('closed', () => {
        mainWindow = null
    });
}

上面為主窗體創(chuàng)建的配置,由于我們的播放器需要整體透明且無上部的標(biāo)題欄鲁森,因此設(shè)置 titleBarStyle: ‘hidden-inset’ 和 transparent: true 祟滴,注意在這樣設(shè)置之后,我們還需要對窗口內(nèi)的 body 設(shè)置 -webkit-app-region: drag; 的Css 使整個(gè)窗口可拖動(dòng)歌溉,之后在對需要有點(diǎn)擊效果的地方(不需要拖動(dòng))設(shè)置-webkit-app-region: no-drag;

4. 右下角托盤圖標(biāo) 創(chuàng)建

/**
 * Create Tray
 */
function createTray() {
    let iconPath = path.join(__static, 'icons/256x256.png');
    tray = new Tray(iconPath);
    const contextMenu = Menu.buildFromTemplate([
        {
            label: '選擇文件夾', type: 'normal', click: onChooseFolderClick
        },
        {label: '退出', type: 'normal', role: 'quit'}
    ]);
    contextMenu.items[1].checked = false;
    tray.setContextMenu(contextMenu);
    tray.setToolTip("mwave");
}
 
/**
 * when choose folder btn click
 */
function onChooseFolderClick() {
    const musicPaths = dialog.showOpenDialog({
        properties: ['openDirectory']
    });
    if (musicPaths != null && musicPaths != 'undefined') {
        sendMusicList(musicPaths);
    }
}

由于播放器上的按鈕有限垄懂,需要將一些功能性的按鈕放在 托盤圖標(biāo)的右鍵菜單中,可使用electron的Tray對象實(shí)現(xiàn)研底,這里主要是將文件夾選擇的功能放在了這里埠偿,文件選擇可用dialog.showOpenDialog 實(shí)現(xiàn)。

5. Vue 相關(guān)組件

Vue組件并沒有什么特殊的榜晦,我這里拆成了musicAudio冠蒋、musicCanvas、musicControl乾胶、musicInfo抖剿、musicName、musicProgress這幾個(gè)組件识窿,其中進(jìn)度條的處理需要稍加留意斩郎,因?yàn)樯婕包c(diǎn)擊與拖動(dòng),導(dǎo)致需要判斷的邏輯比較多喻频。

三. 音頻 相關(guān)

由于electron中無論是主進(jìn)程還是渲染進(jìn)程中均支持node模塊缩宜,因此播放音頻十分方便,我這里是使用 mediaserver 在主進(jìn)程中創(chuàng)建了一個(gè)音樂server甥温,之后在渲染進(jìn)程中使用audio標(biāo)簽即可锻煌。

class MusicServer {
 
    start() {
        const server = http.createServer((req, res) => {
            this.pipeMusic(req, res);
        }).listen(8580);
        return server;
    }
 
    pipeMusic(req, res) {
        if (store.get("MUSIC_PATHS") == undefined || store.get("MUSIC_PATHS").length <= 0) {
            return this.notFound(res);
        }
        const musicUrl = decodeURIComponent(req.url);
        const fileUrl = path.join(store.get("MUSIC_PATHS")[0], musicUrl.substring(1));
        if (musicUrl.substring(1) == '' || !fs.existsSync(fileUrl)) {
            return this.notFound(res);
        }
        ms.pipe(req, res, fileUrl);
    }
 
    notFound(res) {
        res.writeHead(200);
        res.end('not found');
    }
}

四. Canvas 相關(guān)

播放器使用 Canvas 實(shí)現(xiàn)那一圈 音頻可視化效果,主要有兩個(gè)部分姻蚓,外圈的柱狀條和內(nèi)圈的跳動(dòng)顆粒宋梧。這里是使用了 WebAudio Api 實(shí)現(xiàn)的。

createAnalyser() {
                const AC = new (window.AudioContext || window.webkitAudioContext)();
                const analyser = AC.createAnalyser();
                const gainnode = AC.createGain();
                gainnode.gain.value = 1;
                const source = AC.createMediaElementSource(this.$refs.audio);
                source.connect(analyser);
                analyser.connect(gainnode);
                gainnode.connect(AC.destination);
                return analyser;
            }

常用的api中我們可以用 AC.createGain() 控制音頻增益(即音量大小)狰挡,可以使用AC.createAnalyser()對音頻進(jìn)行分析捂龄。我們在實(shí)現(xiàn)音頻可視化的時(shí)候就是使用 AC.createAnalyser().getByteFrequencyData() 生成頻率數(shù)組。具體使用方式如下

this.analyser.fftSize = 1024;
const arrayLength = this.analyser.frequencyBinCount;
const array = new Uint8Array(arrayLength);
this.analyser.getByteFrequencyData(array);

之后我們可以根據(jù) Array 里面的 頻率數(shù)據(jù)進(jìn)行取值加叁,然后繪制Canvas倦沧。繪制的過程就不再詳細(xì)說明,主要是數(shù)學(xué)上的計(jì)算它匕,涉及到圍繞圓的半圈進(jìn)行繪制刀脏,之后取鏡像。在處理時(shí)為了美觀超凳,對內(nèi)圈數(shù)值做過濾處理愈污,對外圈數(shù)值做發(fā)散處理耀态。我這里只是簡單處理了一下,想要更加美觀還需要更多的數(shù)學(xué)處理暂雹。

            /**
             * 繪制內(nèi)圈 point
             */
            drawInner(array, i, ctx) {
                if (i < 136) {
                    var point = i % 9 > 4 ? (9 - i % 9) : (i % 9);
                    var value = (array[i]) * 120 / 256 * ((5 - point) / 5);
                    if (value > 70) {
                        value = ((value - 70) * 120 / 50);
                    } else {
                        value = 0;
                    }
                    ctx.moveTo(( Math.sin(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300), Math.cos(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300);
                    ctx.arc(( Math.sin(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300), Math.cos(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300, 0.6, 0, 2 * Math.PI);
 
                    ctx.moveTo((-Math.sin(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300), Math.cos(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300);
                    ctx.arc(( -Math.sin(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300), Math.cos(((i) * 4 / 3) / 180 * Math.PI) * (198 - value) + 300, 0.6, 0, 2 * Math.PI);
                }
            },
 
            /**
             * 繪制外圈 bar
             */
            drawOuter(array, i, ctx) {
                if (i > 130 && i < 271) {
                    var value = (array[i]) * 120 / 256;
                    if (value > 20) {
                        value = (value - 20) * 120 / 100;
                    } else {
                        value = 0;
                    }
                    ctx.moveTo(( Math.sin((i * 4 / 3) / 180 * Math.PI) * 200 + 300), Math.cos((i * 4 / 3) / 180 * Math.PI) * 200 + 300);
                    ctx.lineTo(( Math.sin((i * 4 / 3) / 180 * Math.PI) * (200 + value) + 300), Math.cos((i * 4 / 3) / 180 * Math.PI) * (200 + value) + 300);
 
                    ctx.moveTo(( -Math.sin((i * 4 / 3) / 180 * Math.PI) * 200 + 300), Math.cos((i * 4 / 3) / 180 * Math.PI) * 200 + 300);
                    ctx.lineTo(( -Math.sin((i * 4 / 3) / 180 * Math.PI) * (200 + value) + 300), Math.cos((i * 4 / 3) / 180 * Math.PI) * (200 + value) + 300);
 
                }
            }

最后首装,本項(xiàng)目僅是本人處于興趣與學(xué)習(xí)的目的搞出來的小玩具,很多功能不完善杭跪,以后有時(shí)間會(huì)考慮再優(yōu)化美化一下~

作者博客地址:https://liuhuihao.com
作者gitHub:https://github.com/geminate

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仙逻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涧尿,更是在濱河造成了極大的恐慌系奉,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姑廉,死亡現(xiàn)場離奇詭異缺亮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)桥言,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門萌踱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人号阿,你說我怎么就攤上這事并鸵。” “怎么了扔涧?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵园担,是天一觀的道長。 經(jīng)常有香客問我枯夜,道長弯汰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任卤档,我火速辦了婚禮蝙泼,結(jié)果婚禮上程剥,老公的妹妹穿的比我還像新娘劝枣。我一直安慰自己,他們只是感情好织鲸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布舔腾。 她就那樣靜靜地躺著,像睡著了一般搂擦。 火紅的嫁衣襯著肌膚如雪稳诚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天瀑踢,我揣著相機(jī)與錄音扳还,去河邊找鬼才避。 笑死,一個(gè)胖子當(dāng)著我的面吹牛氨距,可吹牛的內(nèi)容都是我干的桑逝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼俏让,長吁一口氣:“原來是場噩夢啊……” “哼楞遏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起首昔,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤寡喝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后勒奇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體预鬓,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年撬陵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了珊皿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巨税,死狀恐怖蟋定,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情草添,我是刑警寧澤驶兜,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站远寸,受9級(jí)特大地震影響抄淑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜驰后,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一肆资、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧灶芝,春花似錦郑原、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至女器,卻和暖如春酸役,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國打工涣澡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贱呐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓入桂,卻偏偏與公主長得像吼句,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子事格,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容