女友工做是音頻後期,日常會收集一些音頻音樂,須要看音頻的頻譜波形,每次用au這種大型軟件播放音樂看波形,很不方便,看到她這麼辛苦,身爲程序猿的我痛心疾首,因而,就有了這麼一個小軟件,軟件涉及到的技術主要爲electron,vue,node,波形的展現主要經過wavesurfer生成。html
項目經過vue腳手架搭建的,因此須要安裝cli工具,若是已經裝了,能夠跳過這一步.vue
npm install -g @vue/cli # OR yarn global add @vue/cli
裝好後,經過腳手架搭建項目node
vue create music
vue須要與electron集成,這裏社區已經有比較成熟的vue插件了,Vue CLI Plugin Electron Builder。git
vue add electron-builder
懶人能夠直接去clone個人搭建好得架子直接開發, 戳這裏。github
首先先明確下這個播放器的功能需求,主要有這幾個web
如何實現關聯播放?由於對electron不是很熟,查了好久electron的資料,終於找到了配置項,須要配置fileAssociationsvue-cli
fileAssociations: [ { ext: ["mp3", "wav", "flac", "ogg", "m4a"], name: "music", role: "Editor" } ],
配置好後,經過electron的open-file事件,獲取打開的音頻文件的本地路徑。對於windows,須要經過process.argv,來獲取文件路徑。shell
const filePath = process.argv[1];
上一步經過配置拿到文件的本地路徑後,下一步就是經過路徑讀取音頻文件的信息。因爲音頻的插件沒法解析絕對路徑,因此須要經過node的文件系統,經過fs.readFileSync讀取到文件的buffer信息。macos
let buffer = fs.readFileSync(diskPath); //讀取文件,並將緩存區進行轉換
讀取後須要將buffer轉換成node可讀流npm
const stream = this.bufferToStream(buffer);//將buffer數據轉換成node 可讀流
轉換方法 bufferToStream
bufferToStream(binary) { const readableInstanceStream = new Readable({ read() { this.push(binary); this.push(null); } }); return readableInstanceStream; }
轉換成流後須要將音頻流轉換成blob對象來加載,實現方法
module.exports = streamToBlob function streamToBlob (stream, mimeType) { if (mimeType != null && typeof mimeType !== 'string') { throw new Error('Invalid mimetype, expected string.') } return new Promise((resolve, reject) => { const chunks = [] stream .on('data', chunk => chunks.push(chunk)) .once('end', () => { const blob = mimeType != null ? new Blob(chunks, { type: mimeType }) : new Blob(chunks) resolve(blob) }) .once('error', reject) }) }
轉blob
let fileUrl; // blob對象 streamToBlob(stream) .then(res => { fileUrl = res; // console.log(fileUrl); //將blob對象轉成blob連接 let filePath = window.URL.createObjectURL(fileUrl); // console.log(filePath); this.wavesurfer.load(filePath); // 自動播放 this.wavesurfer.play(); this.playing = true; }) .catch(err => { console.log(err); });
這樣就實現了加載本地文件播放了
這裏的上一首下一首的功能是基於上面獲取到的文件的絕對路徑,經過node的path模塊,path.dirname獲取到文件的父級目錄。
const dirPath = path.dirname(diskPath);
而後經過fs.readdir讀取目錄下全部文件,會返回一個文件名數組,找到該目錄下正在播放的文件的下標,經過數組下標判斷前一首和後一首歌曲的名稱,而後再組裝成絕對路徑,讀取資源播放
playFileList(diskPath, pos) { let isInFiles; let fileIndex; let preIndex; let nextIndex; let fullPath; let dirPath = path.dirname(diskPath); let basename = path.basename(diskPath); fs.readdir(dirPath, (err, files) => { isInFiles = files.includes(basename); if (isInFiles && pos === "pre") { fileIndex = files.indexOf(basename); preIndex = fileIndex - 1; fullPath = path.resolve(dirPath, files[preIndex]); this.loadMusic(fullPath); } if (isInFiles && pos === "next") { fileIndex = files.indexOf(basename); nextIndex = fileIndex + 1; fullPath = path.resolve(dirPath, files[nextIndex]); this.loadMusic(fullPath); } }); },
音量控制須要經過監聽input的鍵入事件,獲取到range的值,而後經過設置樣式background-image,動態計算百分比,而後調用wavesurfer的setVolume方法調節音量
:style="`background-image:linear-gradient( to right, ${fillColor}, ${fillColor} ${percent}, ${emptyColor} ${percent})`"
改變音量changeVol事件
changeVol(e) { let val = e.target.value; let min = e.target.min; let max = e.target.max; let rate = (val - min) / (max - min); this.percent = 100 * rate + "%"; console.log(this.percent, rate); this.wavesurfer.setVolume(Number(rate)); },
我的以爲系統自帶的菜單欄太醜了,就給設置了無邊框再本身加上最小化,關閉的功能。最小化,關閉是經過ipc通訊,渲染進程監聽到有點擊操做後,通知主進程進行相應的操做。
渲染進程
close() { ipcRenderer.send("close"); }, minimize() { ipcRenderer.send("minimize"); }
主進程
ipcMain.on("close", () => { win.close(); app.quit(); }); ipcMain.on("minimize", () => { win.minimize(); });
在實際測試的過程當中發現會出現,打開一首新的音樂播放,就會出現從新開一個實例的現象,不能實現覆蓋播放,後面查閱資料發現electron有一個second-instance事件,能夠監聽是否打開了第二個實例。當第二個實例被執行而且調用 app.requestSingleInstanceLock()") 時,這個事件將在應用程序的首個實例中觸發,而且會返回第二個實例的相關信息,而後經過主進程通知渲染進程,告知渲染進程第二個實例的本地絕對路徑,渲染進程接收到信息後,立馬加載第二個實例的資源。app.requestSingleInstanceLock(),表示應用程序實例是否成功取得了鎖。 若是它取得鎖失敗,能夠假設另外一個應用實例已經取得了鎖而且仍舊在運行,因此能夠直接關閉掉,這樣就避免了打開多個實例的問題
主進程
const gotTheLock = app.requestSingleInstanceLock(); if (gotTheLock) { app.on("second-instance", (event, commandLine, workingDirectory) => { // 監聽是否有第二個實例,向渲染進程發送第二個實例的本地路徑 win.webContents.send("path", `${commandLine[commandLine.length - 1]}`); if (win) { if (win.isMinimized()) win.restore(); win.focus(); } }); app.on("ready", async () => { createWindow(); }); } else { app.quit(); }
渲染進程
ipcRenderer.on("path", (event, arg) => { const newOriginPath = arg; // console.log(newOriginPath); this.loadMusic(newOriginPath); });
需求的原由是,在很興奮的給女友成品的時候,尷尬的被女友試出不少bug(捂臉ing),而後頻繁的修改打包,而後經過私發傳給她。特別麻煩,因此這個需求很急迫。最後查了資料,經過electron-updater實現了這個需求.
安裝electron-updater
yarn add electron-updater
發佈設置
electronBuilder: { builderOptions: { publish: ['github'] } }
主進程監聽
autoUpdater.on("checking-for-update", () => {}); autoUpdater.on("update-available", info => { dialog.showMessageBox({ title: "新版本發佈", message: "有新內容更新,稍後將從新爲您安裝", buttons: ["肯定"], type: "info", noLink: true }); }); autoUpdater.on("update-downloaded", info => { autoUpdater.quitAndInstall(); });
生成Github Access Token
由於是用github做爲更新站,因此本地須要相應的操做權限,去這裏生成token,戳這,生成後,在powershell中設置
[Environment]::SetEnvironmentVariable("GH_TOKEN","<YOUR_TOKEN_HERE>","User") # 例如 [Environment]::SetEnvironmentVariable("GH_TOKEN","sdfdsfgsdg14463232","User")
打包上傳Github
yarn electron:build -p always
完成上面步驟後軟件會自動上傳打包後的文件到release,而後編輯下release就能夠直接發佈了,軟件是基於版本號更新的,因此記得必定要改版本號
做爲程序猿最開心的事莫過於獲得女友的誇獎,雖然這是一個小程序,實現難度也不高,可是最後作出最小可用的版本呈如今女友面前的時候,看到女盆友感動的眼神,我想,這應該是我做爲程序猿惟一感到欣慰的時候。軟件還有不少能夠改進的地方,源碼在此,戳這裏Github