本人博客文章地址:點(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 制作的音頻可視化視頻蔑舞,例如
看起來是不是很酷炫,當(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
二. Electron 相關(guān)
1. 項(xiàng)目結(jié)構(gòu)
項(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