为什么要做?
女朋友工作是音频前期,平时会收集一些音频音乐,须要看音频的频谱波形,每次用 au 这种大型软件播放音乐看波形,很不不便,看到她这么辛苦,身为程序猿的我痛心疾首,于是,就有了这么一个小软件,软件波及到的技术次要为 electron,vue,node, 波形的展现次要通过 wavesurfer 生成。
从零开始 - 搭建我的项目
我的项目通过 vue 脚手架搭建的,所以须要装置 cli 工具, 如果曾经装了,能够跳过这一步.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
装好后, 通过脚手架搭建我的项目
vue create music
vue 须要与 electron 集成,这里社区曾经有比拟成熟的 vue 插件了,Vue CLI Plugin Electron Builder。
vue add electron-builder
懒人能够间接去 clone 我的搭建好得架子间接开发,戳这里。
从零开始 - 我的项目开发
首先先明确下这个播放器的性能需要,次要有这几个
- 不增加文件目录,加载任意的本地文件系统内的音频文件,间接调用播放器播放
- 前一首后一首性能
- 声音音量控制
- 自定义软件窗口
如何关联播放
如何实现关联播放?因为对 electron 不是很熟,查了很久 electron 的材料,终于找到了配置项,须要配置 fileAssociations
fileAssociations: [
{ext: ["mp3", "wav", "flac", "ogg", "m4a"],
name: "music",
role: "Editor"
}
],
配置好后,通过 electron 的 open-file 事件,获取关上的音频文件的本地门路。对于 windows, 须要通过 process.argv,来获取文件门路。
const filePath = process.argv[1];
如何加载本地音频文件
上一步通过配置拿到文件的本地门路后,下一步就是通过门路读取音频文件的信息。因为音频的插件无奈解析绝对路径,所以须要通过 node 的文件系统,通过 fs.readFileSync 读取到文件的 buffer 信息。
let buffer = fs.readFileSync(diskPath); // 读取文件,并将缓存区进行转换
读取后须要将 buffer 转换成 node 可读流
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