mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/lyswhut/lx-music-desktop.git
synced 2026-09-20 16:14:05 +08:00
Compare commits
48 Commits
api-source
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c24d1d1e41 | ||
|
|
1b2daef90c | ||
|
|
6284fd8024 | ||
|
|
763874ac55 | ||
|
|
4defaa103e | ||
|
|
181ac937d2 | ||
|
|
f6884524e2 | ||
|
|
23f118fb1a | ||
|
|
a3b8f31b1e | ||
|
|
c6fb2232d0 | ||
|
|
ef411ddb47 | ||
|
|
7aed2828bb | ||
|
|
560445a518 | ||
|
|
3549ed2da0 | ||
|
|
ec9f86c450 | ||
|
|
c6e829be4f | ||
|
|
5ef84916bb | ||
|
|
94403be9a8 | ||
|
|
6403b8ce38 | ||
|
|
3f34fa26bf | ||
|
|
b1a6c6b59c | ||
|
|
76981a59d4 | ||
|
|
d93fbd699d | ||
|
|
fd2dca0769 | ||
|
|
5097102ca1 | ||
|
|
fa3fd8d8a4 | ||
|
|
5582e78315 | ||
|
|
b7fd32067f | ||
|
|
2a2fdc57bd | ||
|
|
025739e35c | ||
|
|
c8f6153831 | ||
|
|
3c02e3dddc | ||
|
|
40997c34ff | ||
|
|
677c3f16e4 | ||
|
|
0adb4f5ae5 | ||
|
|
cbbcf914ad | ||
|
|
d68a29a5a2 | ||
|
|
368eeed0d0 | ||
|
|
a522b78496 | ||
|
|
dab81116e9 | ||
|
|
b7f355bd1a | ||
|
|
dbeaebaee3 | ||
|
|
fe94a1eac9 | ||
|
|
bd5107bbaf | ||
|
|
8018d9b418 | ||
|
|
0454d300ec | ||
|
|
1e5c9ebc91 | ||
|
|
5c7e9728e6 |
119
.eslintrc
119
.eslintrc
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"plugins": [
|
||||
"html"
|
||||
],
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
// "requireConfigFile": false
|
||||
},
|
||||
"rules": {
|
||||
"no-new": "off",
|
||||
"camelcase": "off",
|
||||
"no-return-assign": "off",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"no-var": "error",
|
||||
"no-fallthrough": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"eqeqeq": "off",
|
||||
"no-multiple-empty-lines": [1, {"max": 2}],
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"standard/no-callback-literal": "off",
|
||||
"prefer-const": "off",
|
||||
"no-labels": "off",
|
||||
"node/no-callback-literal": "off"
|
||||
},
|
||||
"ignorePatterns": ["vendors", "*.min.js", "dist"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [ "*.vue" ],
|
||||
"rules": {
|
||||
"no-new": "off",
|
||||
"camelcase": "off",
|
||||
"no-return-assign": "off",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"no-var": "error",
|
||||
"no-fallthrough": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"eqeqeq": "off",
|
||||
"no-multiple-empty-lines": [1, {"max": 2}],
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"standard/no-callback-literal": "off",
|
||||
"prefer-const": "off",
|
||||
"no-labels": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/space-before-function-paren": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/use-v-on-exact": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
// "no-undef": "off"
|
||||
},
|
||||
"parser": "vue-eslint-parser",
|
||||
"extends": [
|
||||
"plugin:vue/base",
|
||||
// "plugin:vue/strongly-recommended"
|
||||
"plugin:vue/vue3-recommended",
|
||||
"standard-with-typescript",
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"parser": {
|
||||
// Script parser for `<script>`
|
||||
"js": "@typescript-eslint/parser",
|
||||
|
||||
// Script parser for `<script lang="ts">`
|
||||
"ts": "@typescript-eslint/parser"
|
||||
},
|
||||
"extraFileExtensions": [".vue"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [ "*.ts" ],
|
||||
"rules": {
|
||||
"no-new": "off",
|
||||
"camelcase": "off",
|
||||
"no-return-assign": "off",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"no-var": "error",
|
||||
"no-fallthrough": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"eqeqeq": "off",
|
||||
"no-multiple-empty-lines": [1, {"max": 2}],
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"standard/no-callback-literal": "off",
|
||||
"prefer-const": "off",
|
||||
"no-labels": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/space-before-function-paren": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": [1, {
|
||||
"allowBoolean": true
|
||||
}],
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/return-await": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"multiline-ternary": "off",
|
||||
"@typescript-eslint/comma-dangle": "off",
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"standard-with-typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./src/**/tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
91
.eslintrc.cjs
Normal file
91
.eslintrc.cjs
Normal file
@@ -0,0 +1,91 @@
|
||||
const javascript = {
|
||||
'no-new': 'off',
|
||||
camelcase: 'off',
|
||||
'no-return-assign': 'off',
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
'no-var': 'error',
|
||||
'no-fallthrough': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
eqeqeq: 'off',
|
||||
'no-multiple-empty-lines': [1, { max: 2 }],
|
||||
'comma-dangle': [2, 'always-multiline'],
|
||||
'standard/no-callback-literal': 'off',
|
||||
'prefer-const': 'off',
|
||||
'no-labels': 'off',
|
||||
'node/no-callback-literal': 'off',
|
||||
'multiline-ternary': 'off',
|
||||
}
|
||||
const typescript = {
|
||||
...javascript,
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': [1, {
|
||||
allowBoolean: true,
|
||||
}],
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/comma-dangle': 'off',
|
||||
}
|
||||
const vue = {
|
||||
...typescript,
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/use-v-on-exact': 'off',
|
||||
}
|
||||
delete vue['@typescript-eslint/restrict-template-expressions']
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'standard',
|
||||
],
|
||||
plugins: [
|
||||
'html',
|
||||
],
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
// "requireConfigFile": false
|
||||
},
|
||||
rules: javascript,
|
||||
ignorePatterns: ['vendors', '*.min.js', 'dist'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: vue,
|
||||
parser: 'vue-eslint-parser',
|
||||
extends: [
|
||||
'plugin:vue/base',
|
||||
// "plugin:vue/strongly-recommended"
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue-pug/vue3-recommended',
|
||||
'standard-with-typescript',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
parser: {
|
||||
// Script parser for `<script>`
|
||||
js: '@typescript-eslint/parser',
|
||||
|
||||
// Script parser for `<script lang="ts">`
|
||||
ts: '@typescript-eslint/parser',
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.ts'],
|
||||
rules: typescript,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'standard-with-typescript',
|
||||
],
|
||||
parserOptions: {
|
||||
project: './src/**/tsconfig.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
10
.ncurc.js
10
.ncurc.js
@@ -5,15 +5,25 @@ module.exports = {
|
||||
'chalk',
|
||||
'del',
|
||||
'comlink',
|
||||
'vue',
|
||||
'eslint-plugin-n',
|
||||
'@types/ws',
|
||||
],
|
||||
|
||||
// target: 'newest',
|
||||
// filter: [
|
||||
// 'electron-builder',
|
||||
// 'electron-updater',
|
||||
// ],
|
||||
|
||||
// target: 'patch',
|
||||
// filter: [
|
||||
// 'vue',
|
||||
// ],
|
||||
|
||||
// target: 'minor',
|
||||
// filter: [
|
||||
// 'eslint-plugin-n',
|
||||
// 'electron',
|
||||
// ],
|
||||
}
|
||||
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -6,6 +6,37 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
|
||||
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
|
||||
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
|
||||
|
||||
## [2.3.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.2...v2.3.0) - 2023-06-29
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增音效设置(实验性功能),支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效(由于升降调需要实时处理音频数据,这会导致额外的CPU占用,已知问题:如果CPU资源不够时将处理导致任务堆积而出现声音异常,这时需要暂停播放一段时间等堆积的任务处理完毕再播放)
|
||||
- 播放速率设置面板新增是否音调补偿设置,在调整播放速率后,可以选择是否启用音调补偿,默认启用
|
||||
|
||||
### 优化
|
||||
|
||||
- Windows、MacOS平台下的字体列表改用原生方式获取,现在Windows平台下能显示当前已安装的更多类型字体了(注:MacOS平台未测,可用性未知)
|
||||
- 移除桌面歌词窗口透明边距,在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了
|
||||
- 过滤嵌入、下载的翻译、罗马音歌词时间标签,与主歌词时间不匹配的歌词将被丢弃,防止出现原歌词与翻译歌词顺序错乱的问题(#1358)
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复列表名翻译显示
|
||||
- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题,同时也可能导致同步数据丢失的问题(要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本)
|
||||
- 修复下载时出现302错误的问题
|
||||
- 修复播放某些在线音频会没有声音的问题
|
||||
- 修复改变播放速率时会导致歌词报错的问题
|
||||
- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)
|
||||
- 修复wy源热搜词失效的问题(#1401, @Folltoshe)
|
||||
- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题(#1288)
|
||||
- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题
|
||||
- 修复mg歌单搜索歌单播放数量显示问题
|
||||
- 修复tx翻译歌词解析丢失的问题(更新版本后需手动清理一次歌词缓存)
|
||||
|
||||
### 其他
|
||||
|
||||
- 更新 electron 到 v22.3.15
|
||||
|
||||
## [2.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.1...v2.2.2) - 2023-05-01
|
||||
|
||||
### 修复
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,7 @@ module.exports = {
|
||||
externals: {
|
||||
'font-list': 'font-list',
|
||||
'better-sqlite3': 'better-sqlite3',
|
||||
'electron-font-manager': 'electron-font-manager',
|
||||
bufferutil: 'bufferutil',
|
||||
'utf-8-validate': 'utf-8-validate',
|
||||
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),
|
||||
|
||||
@@ -17,6 +17,7 @@ const { Worker, isMainThread, parentPort } = require('worker_threads')
|
||||
|
||||
|
||||
function build() {
|
||||
console.time('build')
|
||||
del.sync(['dist/**', 'build/**'])
|
||||
|
||||
const spinners = new Spinnies({ color: 'blue' })
|
||||
@@ -36,6 +37,7 @@ function build() {
|
||||
process.stdout.write('\x1B[2J\x1B[0f')
|
||||
console.log(`\n\n${results}`)
|
||||
console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
|
||||
console.timeEnd('build')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
type: 'commonjs2',
|
||||
},
|
||||
path: path.join(__dirname, '../../dist'),
|
||||
publicPath: 'auto',
|
||||
publicPath: '',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = {
|
||||
type: 'commonjs2',
|
||||
},
|
||||
path: path.join(__dirname, '../../dist'),
|
||||
publicPath: 'auto',
|
||||
publicPath: '',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
type: 'commonjs2',
|
||||
},
|
||||
path: path.join(__dirname, '../../dist'),
|
||||
publicPath: 'auto',
|
||||
publicPath: '',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -49,6 +49,12 @@ module.exports = {
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
},
|
||||
},
|
||||
parser: {
|
||||
worker: [
|
||||
'*audioContext.audioWorklet.addModule()',
|
||||
'...',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
|
||||
@@ -9,5 +9,10 @@
|
||||
"@common/*": ["src/common/*"],
|
||||
}
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@vue/language-plugin-pug"
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules", "build", "dist"]
|
||||
}
|
||||
|
||||
7020
package-lock.json
generated
7020
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lx-music-desktop",
|
||||
"version": "2.3.0-beta.3",
|
||||
"version": "2.3.0",
|
||||
"description": "一个免费的音乐查找助手",
|
||||
"main": "./dist/main.js",
|
||||
"productName": "lx-music-desktop",
|
||||
@@ -97,6 +97,9 @@
|
||||
"node_modules/better-sqlite3/lib",
|
||||
"node_modules/better-sqlite3/package.json",
|
||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
"node_modules/electron-font-manager/index.js",
|
||||
"node_modules/electron-font-manager/package.json",
|
||||
"node_modules/electron-font-manager/build/Release/font_manager.node",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/bufferutil",
|
||||
"node_modules/utf-8-validate",
|
||||
@@ -205,83 +208,88 @@
|
||||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/eslint-parser": "^7.21.8",
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/eslint-parser": "^7.22.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-umd": "^7.18.6",
|
||||
"@babel/plugin-transform-runtime": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@babel/plugin-transform-modules-umd": "^7.22.5",
|
||||
"@babel/plugin-transform-runtime": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"@babel/preset-typescript": "^7.22.5",
|
||||
"@tsconfig/recommended": "^1.0.2",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/needle": "^3.2.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"@volar/vue-language-plugin-pug": "^1.6.4",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@volar/vue-language-plugin-pug": "^1.6.5",
|
||||
"@vue/language-plugin-pug": "^1.8.3",
|
||||
"babel-loader": "^9.1.2",
|
||||
"browserslist": "^4.21.5",
|
||||
"browserslist": "^4.21.9",
|
||||
"chalk": "^4.1.2",
|
||||
"changelog-parser": "^3.0.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"core-js": "^3.30.2",
|
||||
"core-js": "^3.31.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"css-minimizer-webpack-plugin": "^5.0.1",
|
||||
"del": "^6.1.1",
|
||||
"electron": "^22.3.8",
|
||||
"electron-builder": "^24.3.0",
|
||||
"electron": "^22.3.15",
|
||||
"electron-builder": "^24.5.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-to-chromium": "^1.4.385",
|
||||
"electron-updater": "^6.1.0",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
||||
"electron-to-chromium": "^1.4.445",
|
||||
"electron-updater": "^6.1.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-config-standard-with-typescript": "^35.0.0",
|
||||
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.11.1",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"eslint-plugin-vue-pug": "^0.6.0",
|
||||
"eslint-webpack-plugin": "^4.0.1",
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.1.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"less-loader": "^11.1.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"node-loader": "^2.0.0",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-loader": "^7.3.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"postcss-pxtorem": "^6.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"svg-transform-loader": "^2.0.13",
|
||||
"svgo-loader": "^4.0.0",
|
||||
"terser": "^5.17.1",
|
||||
"terser-webpack-plugin": "^5.3.8",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vue-eslint-parser": "^9.2.1",
|
||||
"vue-loader": "^17.1.0",
|
||||
"terser": "^5.18.2",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-eslint-parser": "^9.3.1",
|
||||
"vue-loader": "^17.2.2",
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"webpack": "^5.82.0",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"webpack": "^5.88.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
|
||||
"webpack-merge": "^5.8.0"
|
||||
"webpack-merge": "^5.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "^1.8.2",
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"better-sqlite3": "^8.4.0",
|
||||
"bufferutil": "^4.0.7",
|
||||
"comlink": "~4.3.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"electron-font-manager": "github:lyswhut/electron-font-manager#6d2f5ecf850c4fe34812b9394913680462ee0dae",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"font-list": "^1.4.5",
|
||||
"font-list": "^1.5.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"image-size": "^1.0.2",
|
||||
"jschardet": "^3.0.0",
|
||||
@@ -292,14 +300,15 @@
|
||||
"sortablejs": "^1.15.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"utf-8-validate": "^6.0.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue": "~3.2.47",
|
||||
"vue-router": "^4.2.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "^11",
|
||||
"json5": "latest",
|
||||
"minimatch": "latest",
|
||||
"semver": "latest",
|
||||
"svg-sprite-loader": {
|
||||
"postcss": "latest"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
### 新增
|
||||
|
||||
- 新增音效设置(实验性功能),支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
|
||||
- 新增音效设置(实验性功能),支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效(由于升降调需要实时处理音频数据,这会导致额外的CPU占用,已知问题:如果CPU资源不够时将处理导致任务堆积而出现声音异常,这时需要暂停播放一段时间等堆积的任务处理完毕再播放)
|
||||
- 播放速率设置面板新增是否音调补偿设置,在调整播放速率后,可以选择是否启用音调补偿,默认启用
|
||||
|
||||
### 优化
|
||||
|
||||
- Windows、MacOS平台下的字体列表改用原生方式获取,现在Windows平台下能显示当前已安装的更多类型字体了(注:MacOS平台未测,可用性未知)
|
||||
- 移除桌面歌词窗口透明边距,在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了
|
||||
- 过滤嵌入、下载的翻译、罗马音歌词时间标签,与主歌词时间不匹配的歌词将被丢弃,防止出现原歌词与翻译歌词顺序错乱的问题(#1358)
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复列表名翻译显示
|
||||
- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题,同时也可能导致同步数据丢失的问题(要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本)
|
||||
- 修复下载时出现302错误的问题
|
||||
- 修复播放某些在线音频会没有声音的问题
|
||||
- 修复改变播放速率时会导致歌词报错的问题
|
||||
- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)
|
||||
- 修复wy源热搜词失效的问题(#1401, @Folltoshe)
|
||||
- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题(#1288)
|
||||
- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题
|
||||
- 修复mg歌单搜索歌单播放数量显示问题
|
||||
- 修复tx翻译歌词解析丢失的问题(更新版本后需手动清理一次歌词缓存)
|
||||
|
||||
### 其他
|
||||
|
||||
- 更新 electron 到 v22.3.8
|
||||
- 更新 electron 到 v22.3.15
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'player.volume': 1,
|
||||
'player.isMute': false,
|
||||
'player.playbackRate': 1,
|
||||
'player.preservesPitch': true,
|
||||
'player.mediaDeviceId': 'default',
|
||||
'player.isMediaDeviceRemovedStopPlay': false,
|
||||
'player.isShowLyricTranslation': false,
|
||||
@@ -56,6 +57,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'player.soundEffect.panner.enable': false,
|
||||
'player.soundEffect.panner.soundR': 5,
|
||||
'player.soundEffect.panner.speed': 25,
|
||||
'player.soundEffect.pitchShifter.playbackRate': 1,
|
||||
|
||||
'playDetail.isZoomActiveLrc': false,
|
||||
'playDetail.isShowLyricProgressSetting': false,
|
||||
@@ -73,7 +75,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'desktopLyric.height': 300,
|
||||
'desktopLyric.x': null,
|
||||
'desktopLyric.y': null,
|
||||
'desktopLyric.isLockScreen': true,
|
||||
'desktopLyric.isLockScreen': isWin,
|
||||
'desktopLyric.isDelayScroll': true,
|
||||
'desktopLyric.scrollAlign': 'center',
|
||||
'desktopLyric.isHoverHide': false,
|
||||
@@ -134,8 +136,8 @@ const defaultSetting: LX.AppSetting = {
|
||||
'sync.server.maxSsnapshotNum': 5,
|
||||
'sync.client.host': '',
|
||||
|
||||
'theme.id': 'blue_plus',
|
||||
// 'theme.id': 'green',
|
||||
// 'theme.id': 'blue_plus',
|
||||
'theme.id': 'green',
|
||||
'theme.lightId': 'green',
|
||||
'theme.darkId': 'black',
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ const modules = {
|
||||
save_sound_effect_eq_preset: 'save_sound_effect_eq_preset',
|
||||
get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset',
|
||||
save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset',
|
||||
// get_sound_effect_pitch_shifter_preset: 'get_sound_effect_pitch_shifter_preset',
|
||||
// save_sound_effect_pitch_shifter_preset: 'save_sound_effect_pitch_shifter_preset',
|
||||
get_hot_key: 'get_hot_key',
|
||||
|
||||
import_user_api: 'import_user_api',
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"paths": {
|
||||
"@common/*": ["common/*"],
|
||||
},
|
||||
"typeRoots": [
|
||||
"./types"
|
||||
],
|
||||
},
|
||||
// "include": [
|
||||
// "**/*.ts",
|
||||
// "**/*.js",
|
||||
// "**/*.vue",
|
||||
// "**/*.json",
|
||||
// ],
|
||||
}
|
||||
|
||||
10
src/common/types/app_setting.d.ts
vendored
10
src/common/types/app_setting.d.ts
vendored
@@ -113,6 +113,11 @@ declare global {
|
||||
*/
|
||||
'player.playbackRate': number
|
||||
|
||||
/**
|
||||
* 是否自动调整音频的音高以补偿对播放速率设置所做的更改
|
||||
*/
|
||||
'player.preservesPitch': boolean
|
||||
|
||||
/**
|
||||
* 音频输出设备id
|
||||
*/
|
||||
@@ -243,6 +248,11 @@ declare global {
|
||||
*/
|
||||
'player.soundEffect.panner.speed': number
|
||||
|
||||
/**
|
||||
* 升降声调
|
||||
*/
|
||||
'player.soundEffect.pitchShifter.playbackRate': number
|
||||
|
||||
/**
|
||||
* 是否启用音频加载失败时自动切歌
|
||||
*/
|
||||
|
||||
5
src/common/types/sound_effect.d.ts
vendored
5
src/common/types/sound_effect.d.ts
vendored
@@ -21,5 +21,10 @@ declare namespace LX {
|
||||
mainGain: number
|
||||
sendGain: number
|
||||
}
|
||||
// interface PitchShifterPreset {
|
||||
// id: string
|
||||
// name: string
|
||||
// playbackRate: number
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class Task extends EventEmitter {
|
||||
progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }
|
||||
statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }
|
||||
requestInstance: http.ClientRequest | null = null
|
||||
maxRedirectNum = 2
|
||||
private redirectNum = 0
|
||||
|
||||
|
||||
constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {
|
||||
@@ -59,6 +61,7 @@ class Task extends EventEmitter {
|
||||
|
||||
async __init() {
|
||||
const { path, startByte, endByte } = this.chunkInfo
|
||||
this.redirectNum = 0
|
||||
this.progress.downloaded = 0
|
||||
this.progress.progress = 0
|
||||
this.progress.speed = 0
|
||||
@@ -112,6 +115,7 @@ class Task extends EventEmitter {
|
||||
|
||||
__httpFetch(url: string, options: Options['requestOptions']) {
|
||||
// console.log(options)
|
||||
let redirected = false
|
||||
this.requestInstance = request(url, options)
|
||||
.on('response', response => {
|
||||
if (response.statusCode !== 200 && response.statusCode !== 206) {
|
||||
@@ -125,6 +129,15 @@ class Task extends EventEmitter {
|
||||
})
|
||||
return
|
||||
}
|
||||
if ((response.statusCode == 301 || response.statusCode == 302) && response.headers.location && this.redirectNum < this.maxRedirectNum) {
|
||||
console.log('current url:', url)
|
||||
console.log('redirect to:', response.headers.location)
|
||||
redirected = true
|
||||
this.redirectNum++
|
||||
const location = response.headers.location
|
||||
this.__httpFetch(location, options)
|
||||
return
|
||||
}
|
||||
this.status = STATUS.failed
|
||||
this.emit('fail', response)
|
||||
this.__closeRequest()
|
||||
@@ -153,6 +166,7 @@ class Task extends EventEmitter {
|
||||
})
|
||||
.on('error', err => { this.__handleError(err) })
|
||||
.on('close', () => {
|
||||
if (redirected) return
|
||||
void this.__closeWriteStream()
|
||||
})
|
||||
.end()
|
||||
|
||||
110
src/common/utils/effects/snow.min.js
vendored
110
src/common/utils/effects/snow.min.js
vendored
@@ -4,24 +4,24 @@
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
// Change the following variables to customize the appearance of the particles
|
||||
var numParticles = window.innerWidth / 50;
|
||||
var maxSpeed = 2.5;
|
||||
var minSpeed = 1;
|
||||
var maxSize = 5;
|
||||
var minSize = 3;
|
||||
var maxOpacity = 1;
|
||||
var minOpacity = 0.2;
|
||||
let numParticles = window.innerWidth / 50
|
||||
let maxSpeed = 2.5
|
||||
let minSpeed = 1
|
||||
let maxSize = 5
|
||||
let minSize = 3
|
||||
let maxOpacity = 1
|
||||
let minOpacity = 0.2
|
||||
|
||||
// If the following is true, the canvas resolution will be set to match the full viewport width and height.
|
||||
// var fixCanvasResolution = true;
|
||||
|
||||
var snow_particles = [];
|
||||
let snow_particles = []
|
||||
|
||||
var canvas
|
||||
let canvas
|
||||
|
||||
window.onload = function() {
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.style.position = 'fixed';
|
||||
canvas.style.position = 'fixed'
|
||||
canvas.style.top = '0px'
|
||||
canvas.style.left = '0px'
|
||||
canvas.style.pointerEvents = 'none'
|
||||
@@ -30,62 +30,62 @@ window.onload = function() {
|
||||
canvas.height = document.documentElement.clientHeight
|
||||
document.body.appendChild(canvas)
|
||||
|
||||
// if (fixCanvasResolution) {
|
||||
// // canvas.width = window.innerWidth;
|
||||
// canvas.height = window.innerHeight;
|
||||
// }
|
||||
// if (fixCanvasResolution) {
|
||||
// // canvas.width = window.innerWidth;
|
||||
// canvas.height = window.innerHeight;
|
||||
// }
|
||||
|
||||
InitPoints();
|
||||
InitPoints()
|
||||
|
||||
requestAnimationFrame(Redraw, 15);
|
||||
window.requestAnimationFrame(Redraw, 15)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
function onWindowResize (e) {
|
||||
canvas.width = document.documentElement.clientWidth
|
||||
canvas.height = document.documentElement.clientHeight
|
||||
}
|
||||
// function onWindowResize(e) {
|
||||
// canvas.width = document.documentElement.clientWidth
|
||||
// canvas.height = document.documentElement.clientHeight
|
||||
// }
|
||||
|
||||
function Redraw() {
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||||
for (var i = 0; i < snow_particles.length; i++) {
|
||||
var newYPos = snow_particles[i].yPos + snow_particles[i].speed;
|
||||
if (newYPos > window.innerHeight) {
|
||||
newYPos = getRandomInt(-100,-10);
|
||||
snow_particles[i].xPos = getRandomInt(0, window.innerWidth);
|
||||
snow_particles[i].speed = Math.random() * (maxSpeed - minSpeed) + minSpeed;
|
||||
snow_particles[i].opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity;
|
||||
snow_particles[i].size = Math.random() * (maxSize - minSize) + minSize;
|
||||
}
|
||||
snow_particles[i].yPos = newYPos;
|
||||
ctx.beginPath();
|
||||
ctx.arc(snow_particles[i].xPos, newYPos, snow_particles[i].size, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "rgba(255, 255, 255, " + snow_particles[i].opacity + ")";
|
||||
ctx.fill();
|
||||
}
|
||||
requestAnimationFrame(Redraw)
|
||||
let ctx = canvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
for (let i = 0; i < snow_particles.length; i++) {
|
||||
let newYPos = snow_particles[i].yPos + snow_particles[i].speed
|
||||
if (newYPos > window.innerHeight) {
|
||||
newYPos = getRandomInt(-100, -10)
|
||||
snow_particles[i].xPos = getRandomInt(0, window.innerWidth)
|
||||
snow_particles[i].speed = Math.random() * (maxSpeed - minSpeed) + minSpeed
|
||||
snow_particles[i].opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity
|
||||
snow_particles[i].size = Math.random() * (maxSize - minSize) + minSize
|
||||
}
|
||||
snow_particles[i].yPos = newYPos
|
||||
ctx.beginPath()
|
||||
ctx.arc(snow_particles[i].xPos, newYPos, snow_particles[i].size, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, ' + snow_particles[i].opacity + ')'
|
||||
ctx.fill()
|
||||
}
|
||||
window.requestAnimationFrame(Redraw)
|
||||
}
|
||||
|
||||
function InitPoints() {
|
||||
for (var i = 0; i < numParticles; i++) {
|
||||
var startX = getRandomInt(0, window.innerWidth);
|
||||
var startY = getRandomInt(0, window.innerHeight);
|
||||
var speed = Math.random() * (maxSpeed - minSpeed) + minSpeed;
|
||||
var opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity;
|
||||
var size = Math.random() * (maxSize - minSize) + minSize;
|
||||
for (let i = 0; i < numParticles; i++) {
|
||||
let startX = getRandomInt(0, window.innerWidth)
|
||||
let startY = getRandomInt(0, window.innerHeight)
|
||||
let speed = Math.random() * (maxSpeed - minSpeed) + minSpeed
|
||||
let opacity = Math.random() * (maxOpacity - minOpacity) + minOpacity
|
||||
let size = Math.random() * (maxSize - minSize) + minSize
|
||||
|
||||
snow_particles.push({
|
||||
"xPos": startX,
|
||||
"yPos": startY,
|
||||
"speed": speed,
|
||||
"opacity": opacity,
|
||||
"size": size,
|
||||
});
|
||||
}
|
||||
snow_particles.push({
|
||||
xPos: startX,
|
||||
yPos: startY,
|
||||
speed,
|
||||
opacity,
|
||||
size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
min = Math.ceil(min)
|
||||
max = Math.floor(max)
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ module.exports = class FontPlayer {
|
||||
// let lineText = ''
|
||||
let lrcShadowContent
|
||||
for (const font of fonts) {
|
||||
if (!timeRxp.test(font)) return this._handleLineParse()
|
||||
text = font.replace(timeRxp, '')
|
||||
if (RegExp.$2 == '') return this._handleLineParse()
|
||||
const time = parseInt(RegExp.$2)
|
||||
|
||||
const dom = document.createElement('span')
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
type ComputedRef,
|
||||
type Ref,
|
||||
type ShallowRef,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
defineExpose,
|
||||
withDefaults,
|
||||
} from 'vue'
|
||||
// import { useStore } from 'vuex'
|
||||
@@ -75,11 +71,7 @@ export {
|
||||
unref,
|
||||
onMounted,
|
||||
markRaw,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
defineExpose,
|
||||
withDefaults,
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"player__play_toggle_mode_off": "Disable",
|
||||
"player__play_toggle_mode_random": "List Random",
|
||||
"player__play_toggle_mode_single_loop": "Single Loop",
|
||||
"player__playback_preserves_pitch": "Pitch compensation",
|
||||
"player__playback_rate": "Current playback rate:",
|
||||
"player__playback_rate_reset_btn": "Reset",
|
||||
"player__playing": "Now playing...",
|
||||
@@ -263,6 +264,10 @@
|
||||
"player__sound_effect_panner_enabled": "enable",
|
||||
"player__sound_effect_panner_sound_r": "Sound distance",
|
||||
"player__sound_effect_panner_sound_speed": "Surround speed",
|
||||
"player__sound_effect_pitch_shifter": "Pitch adjustment",
|
||||
"player__sound_effect_pitch_shifter_preset_semitones": "{num} semitones",
|
||||
"player__sound_effect_pitch_shifter_reset_btn": "Reset",
|
||||
"player__sound_effect_pitch_shifter_tip": "Since ups and downs need to process audio data in real time, this will cause additional CPU usage\n\nKnown issues:\nIf the CPU resources are not enough, the processing will cause the task to accumulate and the sound will be abnormal. At this time, it is necessary to pause the playback for a period of time and wait for the accumulated tasks to be processed before playing.",
|
||||
"player__stop": "Paused",
|
||||
"player__volume": "Volume: ",
|
||||
"player__volume_mute_label": "Mute",
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext", /* Specify what module code is generated. */
|
||||
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "typeRoots": [
|
||||
// "./types"
|
||||
// ],
|
||||
},
|
||||
// "include": [
|
||||
// "**/*.ts",
|
||||
// "**/*.js",
|
||||
// "**/*.vue",
|
||||
// "**/*.json",
|
||||
// ],
|
||||
// "exclude": ["dbService"]
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"player__play_toggle_mode_off": "禁用",
|
||||
"player__play_toggle_mode_random": "列表随机",
|
||||
"player__play_toggle_mode_single_loop": "单曲循环",
|
||||
"player__playback_preserves_pitch": "音调补偿",
|
||||
"player__playback_rate": "当前播放速率:",
|
||||
"player__playback_rate_reset_btn": "重置",
|
||||
"player__playing": "播放中...",
|
||||
@@ -262,6 +263,10 @@
|
||||
"player__sound_effect_panner_enabled": "启用",
|
||||
"player__sound_effect_panner_sound_r": "声音距离",
|
||||
"player__sound_effect_panner_sound_speed": "环绕速度",
|
||||
"player__sound_effect_pitch_shifter": "音调升降调节",
|
||||
"player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音",
|
||||
"player__sound_effect_pitch_shifter_reset_btn": "重置",
|
||||
"player__sound_effect_pitch_shifter_tip": "由于升降调需要实时处理音频数据,这会导致额外的CPU占用\n\n已知问题:\n如果CPU资源不够时将处理导致任务堆积而出现声音异常,这时需要暂停播放一段时间等堆积的任务处理完毕再播放",
|
||||
"player__stop": "暂停播放",
|
||||
"player__volume": "当前音量:",
|
||||
"player__volume_mute_label": "静音",
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"player__play_toggle_mode_off": "禁用",
|
||||
"player__play_toggle_mode_random": "列表隨機",
|
||||
"player__play_toggle_mode_single_loop": "單曲循環",
|
||||
"player__playback_preserves_pitch": "音調補償",
|
||||
"player__playback_rate": "當前播放速率:",
|
||||
"player__playback_rate_reset_btn": "重置",
|
||||
"player__playing": "播放中...",
|
||||
@@ -244,7 +245,7 @@
|
||||
"player__sound_effect_biquad_filter_save_input": "新預設...",
|
||||
"player__sound_effect_convolution": "環境混響音效",
|
||||
"player__sound_effect_convolution_file_bright_hall": "大廳",
|
||||
"player__sound_effect_convolution_file_cardiod_35_10_spread": "搖滾",
|
||||
"player__sound_effect_convolution_file_cardiod_35_10_spread": "心形擴散",
|
||||
"player__sound_effect_convolution_file_cinema_diningroom": "電影院",
|
||||
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐廳",
|
||||
"player__sound_effect_convolution_file_feedback_spring": "反饋彈簧",
|
||||
@@ -263,6 +264,10 @@
|
||||
"player__sound_effect_panner_enabled": "啟用",
|
||||
"player__sound_effect_panner_sound_r": "聲音距離",
|
||||
"player__sound_effect_panner_sound_speed": "環繞速度",
|
||||
"player__sound_effect_pitch_shifter": "音調升降調節",
|
||||
"player__sound_effect_pitch_shifter_preset_semitones": "{num} 半音",
|
||||
"player__sound_effect_pitch_shifter_reset_btn": "重置",
|
||||
"player__sound_effect_pitch_shifter_tip": "由於升降調需要實時處理音頻數據,這會導致額外的CPU佔用\n\n已知問題:\n如果CPU資源不夠時將處理導致任務堆積而出現聲音異常,這時需要暫停播放一段時間等堆積的任務處理完畢再播放",
|
||||
"player__stop": "暫停播放",
|
||||
"player__volume": "當前音量:",
|
||||
"player__volume_mute_label": "靜音",
|
||||
|
||||
@@ -102,4 +102,4 @@ declare class EventType extends Event {
|
||||
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
|
||||
}
|
||||
|
||||
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
|
||||
export type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
|
||||
|
||||
@@ -14,6 +14,18 @@ import { EventEmitter } from 'events'
|
||||
// musicOverwrite,
|
||||
// } from '@main/workers/dbService/modules/list'
|
||||
|
||||
// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题,确保所有ID都是字符串类型
|
||||
const fixListIdType = (lists: LX.List.UserListInfo[] | LX.List.UserListInfoFull[]) => {
|
||||
for (const list of lists) {
|
||||
if (typeof list.sourceListId == 'number') {
|
||||
list.sourceListId = String(list.sourceListId)
|
||||
if (typeof list.id == 'number') {
|
||||
list.id = String(list.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Event extends EventEmitter {
|
||||
list_changed() {
|
||||
this.emit('list_changed')
|
||||
@@ -25,6 +37,7 @@ export class Event extends EventEmitter {
|
||||
* @param isRemote 是否属于远程操作
|
||||
*/
|
||||
async list_data_overwrite(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) {
|
||||
fixListIdType(listData.userList)
|
||||
await global.lx.worker.dbService.listDataOverwrite(listData)
|
||||
this.emit('list_data_overwrite', listData, isRemote)
|
||||
this.list_changed()
|
||||
@@ -37,6 +50,7 @@ export class Event extends EventEmitter {
|
||||
* @param isRemote 是否属于远程操作
|
||||
*/
|
||||
async list_create(position: number, lists: LX.List.UserListInfo[], isRemote: boolean = false) {
|
||||
fixListIdType(lists)
|
||||
await global.lx.worker.dbService.createUserLists(position, lists)
|
||||
this.emit('list_create', position, lists, isRemote)
|
||||
this.list_changed()
|
||||
@@ -171,4 +185,4 @@ declare class EventType extends Event {
|
||||
once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
|
||||
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
|
||||
}
|
||||
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
|
||||
export type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { app } from 'electron'
|
||||
import * as electronDebug from 'electron-debug'
|
||||
import electronDebug from 'electron-debug'
|
||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
|
||||
// Install `electron-debug` with `devtron`
|
||||
electronDebug({
|
||||
|
||||
@@ -110,10 +110,10 @@ const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.Li
|
||||
})
|
||||
})
|
||||
|
||||
type UserDataObj = Record<string, LX.List.UserListInfoFull>
|
||||
type UserDataObj = Map<string, LX.List.UserListInfoFull>
|
||||
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
|
||||
const userListDataObj: UserDataObj = {}
|
||||
for (const list of listData.userList) userListDataObj[list.id] = list
|
||||
const userListDataObj: UserDataObj = new Map()
|
||||
for (const list of listData.userList) userListDataObj.set(list.id, list)
|
||||
return userListDataObj
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ const mergeList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync.Lis
|
||||
|
||||
targetListData.userList.forEach((list, index) => {
|
||||
const targetUpdateTime = list?.locationUpdateTime ?? 0
|
||||
const sourceList = userListDataObj[list.id]
|
||||
const sourceList = userListDataObj.get(list.id)
|
||||
if (sourceList) {
|
||||
sourceList.list = handleMergeList(sourceList.list, list.list, addMusicLocationType)
|
||||
|
||||
@@ -205,7 +205,7 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync
|
||||
newListData.userList = [...sourceListData.userList]
|
||||
|
||||
targetListData.userList.forEach((list, index) => {
|
||||
if (userListDataObj[list.id]) return
|
||||
if (userListDataObj.has(list.id)) return
|
||||
if (list?.locationUpdateTime) {
|
||||
newListData.userList.splice(index, 0, list)
|
||||
} else {
|
||||
@@ -367,10 +367,10 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna
|
||||
let newUserList: LX.List.UserListInfoFull[] = []
|
||||
for (const list of localListData.userList) {
|
||||
if (removedListIds.has(list.id)) continue
|
||||
const remoteList = remoteUserListData[list.id]
|
||||
const remoteList = remoteUserListData.get(list.id)
|
||||
let newList: LX.List.UserListInfoFull
|
||||
if (remoteList) {
|
||||
newList = { ...list, list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotUserListData[list.id].list, addMusicLocationType) }
|
||||
newList = { ...list, list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotUserListData.get(list.id)?.list ?? [], addMusicLocationType) }
|
||||
} else {
|
||||
newList = { ...list }
|
||||
}
|
||||
@@ -380,8 +380,8 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna
|
||||
remoteListData.userList.forEach((list, index) => {
|
||||
if (removedListIds.has(list.id)) return
|
||||
const remoteUpdateTime = list?.locationUpdateTime ?? 0
|
||||
if (localUserListData[list.id]) {
|
||||
const localUpdateTime = localUserListData[list.id]?.locationUpdateTime ?? 0
|
||||
if (localUserListData.has(list.id)) {
|
||||
const localUpdateTime = localUserListData.get(list.id)?.locationUpdateTime ?? 0
|
||||
if (localUpdateTime >= remoteUpdateTime) return
|
||||
// 调整位置
|
||||
const [newList] = newUserList.splice(newUserList.findIndex(l => l.id == list.id), 1)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from 'path'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { debounce, isLinux } from '@common/utils'
|
||||
import { debounce, isLinux, isWin } from '@common/utils'
|
||||
import { initWindowSize } from './utils'
|
||||
import { mainSend } from '@common/mainIpc'
|
||||
import { encodePath } from '@common/utils/electron'
|
||||
@@ -41,7 +41,7 @@ const winEvent = () => {
|
||||
'desktopLyric.width': bounds.width,
|
||||
'desktopLyric.height': bounds.height,
|
||||
})
|
||||
} else {
|
||||
} else if (isWin) { // Linux 不允许将窗口设置出屏幕之外,MacOS未知,故只在Windows下执行强制设置
|
||||
// 非主动调整窗口触发的窗口位置变化将重置回设置值
|
||||
browserWindow!.setBounds({
|
||||
x: global.lx.appSetting['desktopLyric.x'] ?? 0,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// 设置窗口位置、大小
|
||||
export const padding = 8
|
||||
export let minWidth = 80
|
||||
export let minHeight = 50
|
||||
|
||||
@@ -21,25 +20,23 @@ export const getLyricWindowBounds = (bounds: Electron.Rectangle, { x = 0, y = 0,
|
||||
|
||||
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
|
||||
if (!global.envParams.workAreaSize) return bounds
|
||||
const maxWinW = global.envParams.workAreaSize.width + padding * 2
|
||||
const maxWinH = global.envParams.workAreaSize.height + padding * 2
|
||||
const maxWinW = global.envParams.workAreaSize.width
|
||||
const maxWinH = global.envParams.workAreaSize.height
|
||||
|
||||
if (w > maxWinW) w = maxWinW
|
||||
if (h > maxWinH) h = maxWinH
|
||||
|
||||
const maxX = global.envParams.workAreaSize.width + padding - w
|
||||
const minX = -padding
|
||||
const maxY = global.envParams.workAreaSize.height + padding - h
|
||||
const minY = -padding
|
||||
const maxX = global.envParams.workAreaSize.width - w
|
||||
const maxY = global.envParams.workAreaSize.height - h
|
||||
|
||||
x += bounds.x
|
||||
y += bounds.y
|
||||
|
||||
if (x > maxX) x = maxX
|
||||
else if (x < minX) x = minX
|
||||
else if (x < 0) x = 0
|
||||
|
||||
if (y > maxY) y = maxY
|
||||
else if (y < minY) y = minY
|
||||
else if (y < 0) y = 0
|
||||
} else {
|
||||
y += bounds.y
|
||||
x += bounds.x
|
||||
@@ -101,10 +98,10 @@ export const initWindowSize = (x: LX.AppSetting['desktopLyric.x'], y: LX.AppSett
|
||||
if (width < minWidth) width = minWidth
|
||||
if (height < minHeight) height = minHeight
|
||||
if (global.envParams.workAreaSize) {
|
||||
x = global.envParams.workAreaSize.width + padding - width
|
||||
y = global.envParams.workAreaSize.height + padding - height
|
||||
x = global.envParams.workAreaSize.width - width
|
||||
y = global.envParams.workAreaSize.height - height
|
||||
} else {
|
||||
x = y = -padding
|
||||
x = y = 0
|
||||
}
|
||||
} else {
|
||||
let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height })
|
||||
|
||||
@@ -17,4 +17,11 @@ export default () => {
|
||||
mainOn<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => {
|
||||
getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params)
|
||||
})
|
||||
|
||||
// mainHandle<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset, async() => {
|
||||
// return getStore(STORE_NAMES.SOUND_EFFECT).get('pitchShifterPreset') as LX.SoundEffect.PitchShifterPreset[] | null ?? []
|
||||
// })
|
||||
// mainOn<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, ({ params }) => {
|
||||
// getStore(STORE_NAMES.SOUND_EFFECT).set('pitchShifterPreset', params)
|
||||
// })
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext", /* Specify what module code is generated. */
|
||||
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "module": "esnext",
|
||||
"typeRoots": [
|
||||
"./types"
|
||||
],
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@main/*": ["main/*"],
|
||||
"@static/*": ["static/*"],
|
||||
"@/*": ["./*"],
|
||||
},
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"./types",
|
||||
],
|
||||
},
|
||||
// "include": [
|
||||
// "**/*.ts",
|
||||
// "**/*.js",
|
||||
// "**/*.vue",
|
||||
// "**/*.json",
|
||||
// ],
|
||||
// "exclude": ["dbService"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,18 @@
|
||||
|
||||
// exports.getAvailableFontFamilies = getAvailableFontFamilies
|
||||
|
||||
import { getFonts as getFontsByCommand } from 'font-list'
|
||||
import { getAvailableFontFamilies } from 'electron-font-manager'
|
||||
|
||||
import { getFonts } from 'font-list'
|
||||
|
||||
const getFonts = async() => {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
case 'darwin':
|
||||
return getAvailableFontFamilies()
|
||||
default: return getFontsByCommand()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getFonts,
|
||||
|
||||
@@ -9,7 +9,7 @@ const common = {
|
||||
|
||||
exposeWorker(Object.assign(common, list, lyric, music_url, music_other_source, download))
|
||||
|
||||
export declare type workerDBSeriveTypes = typeof common
|
||||
export type workerDBSeriveTypes = typeof common
|
||||
& typeof list
|
||||
& typeof lyric
|
||||
& typeof music_url
|
||||
|
||||
@@ -19,13 +19,20 @@ import {
|
||||
createMusicInfoByMusicInfoIdQueryStatement,
|
||||
} from './statements'
|
||||
|
||||
|
||||
const idFixRxp = /\.0$/
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @returns
|
||||
*/
|
||||
export const queryAllUserList = () => {
|
||||
return createListQueryStatement().all() as LX.DBService.UserListInfo[]
|
||||
const list = createListQueryStatement().all() as LX.DBService.UserListInfo[]
|
||||
for (const info of list) {
|
||||
// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题
|
||||
if (info.sourceListId?.endsWith?.('.0')) {
|
||||
info.sourceListId = info.sourceListId.replace(idFixRxp, '')
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Worker } from 'worker_threads'
|
||||
import * as Comlink from 'comlink'
|
||||
import nodeEndpoint from 'comlink/dist/esm/node-adapter'
|
||||
|
||||
export declare type DBSeriveTypes = Comlink.Remote<LX.WorkerDBSeriveListTypes>
|
||||
export type DBSeriveTypes = Comlink.Remote<LX.WorkerDBSeriveListTypes>
|
||||
|
||||
export const createDBServiceWorker = () => {
|
||||
const worker: Worker = new Worker(new URL(
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<common-audio-visualizer v-if="setting['desktopLyric.audioVisualization']" />
|
||||
</transition>
|
||||
</div>
|
||||
<div class="resize-left" @mousedown.self="handleMouseDown('left', $event)" @touchstart.self="handleTouchDown('left', $event)" />
|
||||
<div class="resize-top" @mousedown.self="handleMouseDown('top', $event)" @touchstart.self="handleTouchDown('top', $event)" />
|
||||
<div class="resize-right" @mousedown.self="handleMouseDown('right', $event)" @touchstart.self="handleTouchDown('right', $event)" />
|
||||
<div class="resize-bottom" @mousedown.self="handleMouseDown('bottom', $event)" @touchstart.self="handleTouchDown('bottom', $event)" />
|
||||
<div class="resize-top-left" @mousedown.self="handleMouseDown('top-left', $event)" @touchstart.self="handleTouchDown('top-left', $event)" />
|
||||
<div class="resize-top-right" @mousedown.self="handleMouseDown('top-right', $event)" @touchstart.self="handleTouchDown('top-right', $event)" />
|
||||
<div class="resize-bottom-left" @mousedown.self="handleMouseDown('bottom-left', $event)" @touchstart.self="handleTouchDown('bottom-left', $event)" />
|
||||
<div class="resize-bottom-right" @mousedown.self="handleMouseDown('bottom-right', $event)" @touchstart.self="handleTouchDown('bottom-right', $event)" />
|
||||
<div class="resize resize-left" @mousedown.self="handleMouseDown('left', $event)" @touchstart.self="handleTouchDown('left', $event)" />
|
||||
<div class="resize resize-top" @mousedown.self="handleMouseDown('top', $event)" @touchstart.self="handleTouchDown('top', $event)" />
|
||||
<div class="resize resize-right" @mousedown.self="handleMouseDown('right', $event)" @touchstart.self="handleTouchDown('right', $event)" />
|
||||
<div class="resize resize-bottom" @mousedown.self="handleMouseDown('bottom', $event)" @touchstart.self="handleTouchDown('bottom', $event)" />
|
||||
<div class="resize resize-top-left" @mousedown.self="handleMouseDown('top-left', $event)" @touchstart.self="handleTouchDown('top-left', $event)" />
|
||||
<div class="resize resize-top-right" @mousedown.self="handleMouseDown('top-right', $event)" @touchstart.self="handleTouchDown('top-right', $event)" />
|
||||
<div class="resize resize-bottom-left" @mousedown.self="handleMouseDown('bottom-left', $event)" @touchstart.self="handleTouchDown('bottom-left', $event)" />
|
||||
<div class="resize resize-bottom-right" @mousedown.self="handleMouseDown('bottom-right', $event)" @touchstart.self="handleTouchDown('bottom-right', $event)" />
|
||||
<layout-icons />
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,7 +70,6 @@ body {
|
||||
}
|
||||
|
||||
#container {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
transition: opacity .3s ease;
|
||||
@@ -85,12 +84,16 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@resize-width: 6px;
|
||||
.resize {
|
||||
z-index: 2;
|
||||
}
|
||||
.resize-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
width: @resize-width;
|
||||
cursor: ew-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
@@ -99,14 +102,14 @@ body {
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
width: @resize-width;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.resize-top {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 10px;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
@@ -114,7 +117,7 @@ body {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 10px;
|
||||
height: @resize-width;
|
||||
width: 100%;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
@@ -122,8 +125,8 @@ body {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: @resize-width;
|
||||
height: @resize-width;
|
||||
cursor: nwse-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
@@ -131,26 +134,17 @@ body {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: @resize-width;
|
||||
height: @resize-width;
|
||||
cursor: nesw-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
.resize-top-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: nwse-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
.resize-bottom-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: @resize-width;
|
||||
height: @resize-width;
|
||||
cursor: nesw-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
@@ -158,8 +152,8 @@ body {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: @resize-width;
|
||||
height: @resize-width;
|
||||
cursor: nwse-resize;
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const setting = shallowReactive<LX.DesktopLyric.Config>({
|
||||
// },
|
||||
// ])
|
||||
|
||||
// export declare type Status = 'playing' | 'paused' | 'stopped'
|
||||
// export type Status = 'playing' | 'paused' | 'stopped'
|
||||
|
||||
// export const status = ref<Status>('stopped')
|
||||
export const isPlay = ref(false)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"isolatedModules": true,
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
// "@renderer/*": ["renderer/*"],
|
||||
@@ -15,11 +13,11 @@
|
||||
"./types"
|
||||
],
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@volar/vue-language-plugin-pug"
|
||||
]
|
||||
}
|
||||
// "vueCompilerOptions": {
|
||||
// "plugins": [
|
||||
// "@volar/vue-language-plugin-pug"
|
||||
// ]
|
||||
// }
|
||||
// "include": [
|
||||
// "./**/*.ts",
|
||||
// // "./**/*.js",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11 7V9H13V7H11M14 17V15H13V11H10V13H11V15H10V17H14M22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2C17.5 2 22 6.5 22 12M20 12C20 7.58 16.42 4 12 4C7.58 4 4 7.58 4 12C4 16.42 7.58 20 12 20C16.42 20 20 16.42 20 12Z" /></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -51,7 +51,7 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
emits: ['update:model-value', 'change'],
|
||||
data() {
|
||||
return {
|
||||
checked: false,
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
modelValue = checked
|
||||
} else modelValue = checked ? this.value : ''
|
||||
}
|
||||
this.$emit('update:modelValue', modelValue)
|
||||
this.$emit('update:model-value', modelValue)
|
||||
this.$emit('change', modelValue)
|
||||
},
|
||||
setValue(value) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
// default: true,
|
||||
// },
|
||||
},
|
||||
emits: ['update:modelValue', 'submit', 'change'],
|
||||
emits: ['update:model-value', 'submit', 'change'],
|
||||
methods: {
|
||||
handleInput(event) {
|
||||
let value = event.target.value
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
value = value.trim()
|
||||
event.target.value = value
|
||||
}
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
focus() {
|
||||
this.$refs.dom_input.focus()
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
// if (dom_input.selectionStart == dom_input.selectionEnd) {
|
||||
const value = text.substring(0, dom_input.selectionStart) + str + text.substring(dom_input.selectionEnd, text.length)
|
||||
event.target.value = value
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit('update:model-value', value)
|
||||
// } else {
|
||||
// clipboardWriteText(text.substring(dom_input.selectionStart, dom_input.selectionEnd))
|
||||
// }
|
||||
|
||||
@@ -48,13 +48,13 @@ export default {
|
||||
default: 'name',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'menu-click'],
|
||||
emits: ['update:model-value', 'menu-click'],
|
||||
setup(props, { emit }) {
|
||||
const visible = computed(() => props.modelValue)
|
||||
const location = computed(() => props.xy)
|
||||
|
||||
const onHide = () => {
|
||||
emit('update:modelValue', false)
|
||||
emit('update:model-value', false)
|
||||
menuClick(null)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount, reactive, defineEmits } from '@common/utils/vueTools'
|
||||
import { ref, watch, onMounted, onBeforeUnmount, reactive } from '@common/utils/vueTools'
|
||||
|
||||
// https://github.com/vuejs/core/issues/2855#issuecomment-768388962
|
||||
import {
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
emits: ['update:model-value', 'change'],
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
handleClick(item) {
|
||||
// console.log(this.modelValue)
|
||||
if (item === this.modelValue) return
|
||||
this.$emit('update:modelValue', this.itemKey ? item[this.itemKey] : item)
|
||||
this.$emit('update:model-value', this.itemKey ? item[this.itemKey] : item)
|
||||
this.$emit('change', item)
|
||||
},
|
||||
handleShow() {
|
||||
|
||||
@@ -37,11 +37,11 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
emits: ['update:model-value', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const handleToggle = id => {
|
||||
if (id == props.modelValue) return
|
||||
emit('update:modelValue', id)
|
||||
emit('update:model-value', id)
|
||||
emit('change', id)
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
handleSaveList(event) {
|
||||
let name = event.target.value
|
||||
this.newListName = event.target.value = ''
|
||||
// this.isEditing = false
|
||||
this.isEditing = false
|
||||
if (!name) return
|
||||
createUserList({ name })
|
||||
},
|
||||
|
||||
@@ -9,9 +9,17 @@
|
||||
<div :class="$style.setting">
|
||||
<div :class="$style.info">
|
||||
<span>{{ playbackRate.toFixed(2) }}x</span>
|
||||
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
|
||||
<div :class="$style.control">
|
||||
<base-checkbox
|
||||
id="player__playback_preserves_pitch"
|
||||
:model-value="appSetting['player.preservesPitch']"
|
||||
:label="$t('player__playback_preserves_pitch')"
|
||||
@update:model-value="updatePreservesPitch"
|
||||
/>
|
||||
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
|
||||
</div>
|
||||
</div>
|
||||
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="60" :max="200" @change="handleUpdatePlaybackRate" />
|
||||
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="50" :max="200" @change="handleUpdatePlaybackRate" />
|
||||
</div>
|
||||
</template>
|
||||
</material-popup-btn>
|
||||
@@ -20,11 +28,17 @@
|
||||
<script setup>
|
||||
// import { computed } from '@common/utils/vueTools'
|
||||
import { playbackRate } from '@renderer/store/player/playbackRate'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
const handleUpdatePlaybackRate = (val) => {
|
||||
window.app_event.setPlaybackRate(Math.round(val) / 100)
|
||||
}
|
||||
|
||||
|
||||
const updatePreservesPitch = (enabled) => {
|
||||
updateSetting({ 'player.preservesPitch': enabled })
|
||||
}
|
||||
|
||||
// const icon = computed(() => {
|
||||
// return playbackRate.value == 0
|
||||
// ? '#icon-volume-off-outline'
|
||||
@@ -100,6 +114,11 @@ const handleUpdatePlaybackRate = (val) => {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
.control {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
|
||||
<svg-icon name="plus" />
|
||||
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
|
||||
</base-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { saveUserPitchShifterPreset } from '@renderer/store/soundEffect'
|
||||
|
||||
const isEditing = ref(false)
|
||||
const input = ref(false)
|
||||
const newPresetName = ref('')
|
||||
|
||||
const handleEditing = () => {
|
||||
if (isEditing.value) return
|
||||
// if (!this.newPresetName) this.newPresetName = this.listName
|
||||
isEditing.value = true
|
||||
nextTick(() => {
|
||||
input.value.$el.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = (event) => {
|
||||
let name = event.target.value.trim()
|
||||
newPresetName.value = event.target.value = ''
|
||||
isEditing.value = false
|
||||
if (!name) return
|
||||
if (name.length > 20) name = name.substring(0, 20)
|
||||
saveUserPitchShifterPreset({
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
playbackRate: appSetting['player.soundEffect.pitchShifter.playbackRate'],
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.newPreset {
|
||||
position: relative;
|
||||
border: 1px dashed var(--color-primary-font-hover);
|
||||
// background-color: var(--color-main-background);
|
||||
color: var(--color-primary-font-hover);
|
||||
opacity: .7;
|
||||
height: 22px;
|
||||
|
||||
&.editing {
|
||||
opacity: 1;
|
||||
width: 90px;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
.newPresetInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.svg-icon {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.newPresetInput {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// line-height: 16px;
|
||||
background: none !important;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
padding: 0 3px;
|
||||
border-radius: 0;
|
||||
display: none;
|
||||
&::placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['scroll', $style.saveList]">
|
||||
<div :class="$style.saveList">
|
||||
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
|
||||
<AddConvolutionPresetBtn v-if="userPresetList.length < 31" />
|
||||
</div>
|
||||
@@ -88,6 +88,7 @@ onMounted(() => {
|
||||
flex-flow: column nowrap;
|
||||
gap: 3px;
|
||||
min-height: 0;
|
||||
flex: none;
|
||||
}
|
||||
.convolution {
|
||||
display: flex;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span :class="$style.value">{{ appSetting[`player.soundEffect.biquadFilter.hz${v}`] }}db</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['scroll', $style.saveList]">
|
||||
<div :class="$style.saveList">
|
||||
<!-- <base-btn min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_slow`) }}</base-btn> -->
|
||||
<base-btn v-for="item in freqsPreset" :key="item.name" min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_${item.name}`) }}</base-btn>
|
||||
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
|
||||
@@ -83,6 +83,7 @@ onMounted(() => {
|
||||
flex-flow: column nowrap;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
flex: none;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
152
src/renderer/components/common/SoundEffectBtn/PitchShifter.vue
Normal file
152
src/renderer/components/common/SoundEffectBtn/PitchShifter.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div :class="$style.contnet">
|
||||
<div class="player__sound_effect_title" :class="$style.header">
|
||||
<h3>
|
||||
{{ $t('player__sound_effect_pitch_shifter') }}
|
||||
<svg-icon class="help-icon" name="information-slab-circle-outline" :aria-label="$t('player__sound_effect_pitch_shifter_tip')" />
|
||||
</h3>
|
||||
<base-btn min @click="handleSetPreset(1)">{{ $t('player__sound_effect_pitch_shifter_reset_btn') }}</base-btn>
|
||||
</div>
|
||||
<div :class="$style.eqList">
|
||||
<div :class="$style.eqItem">
|
||||
<span :class="$style.label">{{ playbackRate.toFixed(2) }}x</span>
|
||||
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="50" :max="150" @change="handleUpdatePlaybackRate" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div :class="$style.saveList">
|
||||
<base-btn v-for="num in semitones" :key="num" min @click="handleSetSemitones(num)">{{ $t(`player__sound_effect_pitch_shifter_preset_semitones`, { num: num > 0 ? `+${num}` : num }) }}</base-btn>
|
||||
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item.playbackRate)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
|
||||
<AddPitchShifterPresetBtn v-if="userPresetList.length < 31" />
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
// import AddPitchShifterPresetBtn from './AddPitchShifterPresetBtn.vue'
|
||||
// import { getUserPitchShifterPresetList, removeUserPitchShifterPreset } from '@renderer/store/soundEffect'
|
||||
// import { semitones } from '@renderer/plugins/player'
|
||||
|
||||
// const setting = reactive({
|
||||
// enabled: false,
|
||||
// soundR: 5,
|
||||
// speed: 25,
|
||||
// })
|
||||
|
||||
|
||||
const playbackRate = computed(() => appSetting['player.soundEffect.pitchShifter.playbackRate'])
|
||||
|
||||
const handleSetPreset = (value) => {
|
||||
updateSetting({ 'player.soundEffect.pitchShifter.playbackRate': value })
|
||||
}
|
||||
|
||||
// const handleSetSemitones = (value) => {
|
||||
// // https://zpl.fi/pitch-shifting-in-web-audio-api/
|
||||
// handleSetPreset(2 ** (value / 12))
|
||||
// }
|
||||
|
||||
const handleUpdatePlaybackRate = (value) => {
|
||||
value = parseFloat((Math.round(value) / 100).toFixed(2))
|
||||
handleSetPreset(value)
|
||||
}
|
||||
|
||||
|
||||
// const userPresetList = ref([])
|
||||
|
||||
// const handleRemovePreset = id => {
|
||||
// removeUserPitchShifterPreset(id)
|
||||
// }
|
||||
|
||||
// onMounted(() => {
|
||||
// getUserPitchShifterPresetList().then(list => {
|
||||
// userPresetList.value = list
|
||||
// })
|
||||
// })
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.contnet {
|
||||
padding-top: 15px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
flex: none;
|
||||
&:before {
|
||||
.mixin-after;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-top: 1px dashed var(--color-primary-light-100-alpha-700);
|
||||
}
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 5px;
|
||||
// padding-top: 5px;
|
||||
}
|
||||
.eqList {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.eqItem {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
// width: 50px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.value {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-font);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
// justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// font-size: 13px;
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.saveList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -8,11 +8,12 @@
|
||||
<!-- <main :class="$style.main"> -->
|
||||
<!-- <h2 :class="$style.title">{{ $t('theme_edit_modal__title') }}</h2> -->
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.row">
|
||||
<div :class="['scroll', $style.row]">
|
||||
<AudioConvolution />
|
||||
<PitchShifter />
|
||||
<AudioPanner />
|
||||
</div>
|
||||
<div :class="$style.row">
|
||||
<div :class="['scroll', $style.row]">
|
||||
<BiquadFilter />
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,6 +32,7 @@ import { ref } from '@common/utils/vueTools'
|
||||
import BiquadFilter from './BiquadFilter'
|
||||
import AudioPanner from './AudioPanner'
|
||||
import AudioConvolution from './AudioConvolution'
|
||||
import PitchShifter from './PitchShifter.vue'
|
||||
|
||||
defineProps({
|
||||
teleport: {
|
||||
@@ -97,9 +99,9 @@ const visible = ref(false)
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 0 15px;
|
||||
padding: 0 5px;
|
||||
margin: 15px 0;
|
||||
gap: 30px;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
|
||||
@@ -129,6 +131,7 @@ const visible = ref(false)
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template lang="pug">
|
||||
material-modal(:show="isShowChangeLog" @close="isShowChangeLog = false" max-width="60%")
|
||||
material-modal(:show="isShowChangeLog" max-width="60%" @close="isShowChangeLog = false")
|
||||
main(:class="$style.main")
|
||||
h2 当前版本更新日志
|
||||
div.scroll.select(:class="$style.info")
|
||||
div(:class="$style.current")
|
||||
h3 当前版本:{{versionInfo.version}}
|
||||
h3 当前版本:{{ versionInfo.version }}
|
||||
template(v-if="info.desc")
|
||||
h3 版本变化:
|
||||
pre(:class="$style.desc" v-text="info.desc")
|
||||
div(:class="[$style.history, $style.desc]" v-if="info.history.length")
|
||||
div(v-if="info.history.length" :class="[$style.history, $style.desc]")
|
||||
h3 历史版本:
|
||||
div(:class="$style.item" v-for="ver in info.history")
|
||||
h4 v{{ver.version}}
|
||||
div(v-for="(ver, index) in info.history" :key="index" :class="$style.item")
|
||||
h4 v{{ ver.version }}
|
||||
pre(v-text="ver.desc")
|
||||
|
||||
div(:class="$style.footer")
|
||||
div(:class="$style.desc")
|
||||
p 📢 为了减少疑问,我们墙裂建议阅读版本更新日志来了解当前所用版本的变化!
|
||||
p 📢 若遇到问题可以去设置-关于查看常见问题
|
||||
p(v-if="!info.isLatest") 🚀 发现新版本(v{{versionInfo.newVersion.version}}),建议 去设置-软件更新 更新新版本
|
||||
p(v-if="!info.isLatest") 🚀 发现新版本(v{{ versionInfo.newVersion.version }}),建议 去设置-软件更新 更新新版本
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
setup() {
|
||||
const visible = ref(false)
|
||||
const visibleProgress = ref(false)
|
||||
const dom_btn = ref<HTMLElement | null>(null)
|
||||
const dom_btn = ref(null)
|
||||
|
||||
const handleShowPopup = (evt) => {
|
||||
if (visible.value) {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.footerLeftControlBtns")
|
||||
button(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle")
|
||||
svg(v-show="appSetting['desktopLyric.enable']" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
|
||||
use(xlink:href='#icon-desktop-lyric-on')
|
||||
svg(v-show="!appSetting['desktopLyric.enable']" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
|
||||
use(xlink:href='#icon-desktop-lyric-off')
|
||||
button(:class="[$style.footerLeftControlBtn, { [$style.active]: appSetting['player.audioVisualization'] }]" @click="toggleAudioVisualization" :aria-label="$t('audio_visualization')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-audio-wave')
|
||||
button(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="toggleVisibleLrc" :aria-label="$t('lyric__select')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-text')
|
||||
button(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" @click="toggleVisibleComment" :aria-label="$t('comment__show')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-comment')
|
||||
button(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" :aria-label="toggleDesktopLyricBtnTitle" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric")
|
||||
svg(v-show="appSetting['desktopLyric.enable']" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="125%" viewBox="0 0 512 512" space="preserve")
|
||||
use(xlink:href="#icon-desktop-lyric-on")
|
||||
svg(v-show="!appSetting['desktopLyric.enable']" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="125%" viewBox="0 0 512 512" space="preserve")
|
||||
use(xlink:href="#icon-desktop-lyric-off")
|
||||
button(:class="[$style.footerLeftControlBtn, { [$style.active]: appSetting['player.audioVisualization'] }]" :aria-label="$t('audio_visualization')" @click="toggleAudioVisualization")
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="95%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-audio-wave")
|
||||
button(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" :aria-label="$t('lyric__select')" @click="toggleVisibleLrc")
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="95%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-text")
|
||||
button(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" :aria-label="$t('comment__show')" @click="toggleVisibleComment")
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="95%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-comment")
|
||||
common-sound-effect-btn
|
||||
common-playback-rate-btn
|
||||
common-volume-btn
|
||||
common-toggle-play-mode-btn
|
||||
button(:class="$style.footerLeftControlBtn" @click="isShowAddMusicTo = true" :aria-label="$t('player__add_music_to')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
|
||||
use(xlink:href='#icon-add-2')
|
||||
common-list-add-modal(v-model:show="isShowAddMusicTo" :musicInfo="playMusicInfo.musicInfo")
|
||||
button(:class="$style.footerLeftControlBtn" :aria-label="$t('player__add_music_to')" @click="isShowAddMusicTo = true")
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" space="preserve")
|
||||
use(xlink:href="#icon-add-2")
|
||||
common-list-add-modal(v-model:show="isShowAddMusicTo" :music-info="playMusicInfo.musicInfo")
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['updateLyric', 'update:modelValue'],
|
||||
emits: ['updateLyric', 'update:model-value'],
|
||||
setup(props, { emit }) {
|
||||
// const appSetting = useRefGetter('appSetting')
|
||||
// const playDetailSetting = useRefGetter('playDetailSetting')
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
const location = computed(() => props.xy)
|
||||
|
||||
const onHide = () => {
|
||||
emit('update:modelValue', false)
|
||||
emit('update:model-value', false)
|
||||
}
|
||||
|
||||
const setFontAlign = val => {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.container")
|
||||
ul
|
||||
li(v-for="(item, index) in comments" :key="item.id" :class="$style.listItem")
|
||||
li(v-for="item in comments" :key="item.id" :class="$style.listItem")
|
||||
div(:class="$style.content")
|
||||
div(:class="$style.left")
|
||||
img( :class="$style.avatar" :src="item.avatar || commentDefImg" @error="handleUserImg")
|
||||
div(:class="$style.right")
|
||||
div(:class="$style.info")
|
||||
div(:class="$style.baseInfo")
|
||||
div.select(:class="$style.name") {{item.userName}}
|
||||
div.select(:class="$style.name") {{ item.userName }}
|
||||
div(:class="$style.metaInfo")
|
||||
time(:class="$style.label" v-if="item.timeStr") {{timeFormat(item.timeStr)}}
|
||||
div(:class="$style.label" v-if="item.location") {{$t('comment__location', { location: item.location })}}
|
||||
div(:class="$style.likes" v-if="item.likedCount != null")
|
||||
svg(:class="$style.likesIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
|
||||
use(xlink:href='#icon-thumbs-up')
|
||||
time(v-if="item.timeStr" :class="$style.label") {{ timeFormat(item.timeStr) }}
|
||||
div(v-if="item.location" :class="$style.label") {{$t('comment__location', { location: item.location })}}
|
||||
div(v-if="item.likedCount != null" :class="$style.likes")
|
||||
svg(:class="$style.likesIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" space="preserve")
|
||||
use(xlink:href="#icon-thumbs-up")
|
||||
| {{item.likedCount}}
|
||||
p.select(:class="$style.comment_text") {{item.text}}
|
||||
p.select(:class="$style.comment_text") {{ item.text }}
|
||||
div(v-if="item.images?.length" :class="$style.comment_images")
|
||||
img(v-for="url in item.images" :src="url" loading="lazy" decoding="async")
|
||||
img(v-for="(url, index) in item.images" :key="index" :src="url" loading="lazy" decoding="async")
|
||||
comment-floor(v-if="item.reply && item.reply.length" :class="$style.reply_floor" :comments="item.reply")
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<template lang="pug">
|
||||
div.comment(:class="$style.comment" ref="dom_container")
|
||||
div.comment(ref="dom_container" :class="$style.comment")
|
||||
div(:class="$style.commentHeader")
|
||||
h3 {{$t('comment__title', { name: title })}}
|
||||
h3 {{ $t('comment__title', { name: title }) }}
|
||||
div(:class="$style.commentHeaderBtns")
|
||||
div(:class="$style.commentHeaderBtn" @click="handleShowComment" :aria-label="$t('comment__refresh')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style='transform: rotate(45deg);' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-refresh')
|
||||
div(:class="$style.commentHeaderBtn" :aria-label="$t('comment__refresh')" @click="handleShowComment")
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-refresh")
|
||||
div(:class="$style.commentHeaderBtn" @click="$emit('close')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-close')
|
||||
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-close")
|
||||
|
||||
div(:class="$style.commentMain")
|
||||
template(v-if="available")
|
||||
header(:class="$style.tab_header")
|
||||
button(type="button" @click="handleToggleTab('hot')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]") {{$t('comment__hot_title')}} ({{hotComment.total}})
|
||||
button(type="button" @click="handleToggleTab('new')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]") {{$t('comment__new_title')}} ({{newComment.total}})
|
||||
main(:class="$style.tab_main" ref="dom_tabMain")
|
||||
button(type="button" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]" @click="handleToggleTab('hot')") {{ $t('comment__hot_title') }} ({{ hotComment.total }})
|
||||
button(type="button" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]" @click="handleToggleTab('new')") {{ $t('comment__new_title') }} ({{ newComment.total }})
|
||||
main(ref="dom_tabMain" :class="$style.tab_main")
|
||||
div(:class="$style.tab_content")
|
||||
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentHot")
|
||||
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="hotComment.isLoadError" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{$t('comment__hot_load_error')}}
|
||||
p(:class="$style.commentLabel" v-else-if="hotComment.isLoading && !hotComment.list.length") {{$t('comment__hot_loading')}}
|
||||
div.scroll(ref="dom_commentHot" :class="$style.tab_content_scroll")
|
||||
p(v-if="hotComment.isLoadError" :class="$style.commentLabel" style="cursor: pointer;" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{ $t('comment__hot_load_error') }}
|
||||
p(v-else-if="hotComment.isLoading && !hotComment.list.length" :class="$style.commentLabel") {{ $t('comment__hot_loading') }}
|
||||
comment-floor(v-if="!hotComment.isLoadError && hotComment.list.length" :class="[$style.commentFloor, hotComment.isLoading ? $style.loading : null]" :comments="hotComment.list")
|
||||
p(:class="$style.commentLabel" v-else-if="!hotComment.isLoadError && !hotComment.isLoading") {{$t('comment__no_content')}}
|
||||
p(v-else-if="!hotComment.isLoadError && !hotComment.isLoading" :class="$style.commentLabel") {{ $t('comment__no_content') }}
|
||||
div(:class="$style.pagination")
|
||||
material-pagination(:count="hotComment.total" :btnLength="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
|
||||
material-pagination(:count="hotComment.total" :btn-length="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
|
||||
div(:class="$style.tab_content")
|
||||
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentNew")
|
||||
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="newComment.isLoadError" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{$t('comment__new_load_error')}}
|
||||
p(:class="$style.commentLabel" v-else-if="newComment.isLoading && !newComment.list.length") {{$t('comment__new_loading')}}
|
||||
div.scroll(ref="dom_commentNew" :class="$style.tab_content_scroll")
|
||||
p(v-if="newComment.isLoadError" :class="$style.commentLabel" style="cursor: pointer;" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{ $t('comment__new_load_error') }}
|
||||
p(v-else-if="newComment.isLoading && !newComment.list.length" :class="$style.commentLabel") {{ $t('comment__new_loading') }}
|
||||
comment-floor(v-if="!newComment.isLoadError && newComment.list.length" :class="[$style.commentFloor, newComment.isLoading ? $style.loading : null]" :comments="newComment.list")
|
||||
p(:class="$style.commentLabel" v-else-if="!newComment.isLoadError && !newComment.isLoading") {{$t('comment__no_content')}}
|
||||
p(v-else-if="!newComment.isLoadError && !newComment.isLoading" :class="$style.commentLabel") {{ $t('comment__no_content') }}
|
||||
div(:class="$style.pagination")
|
||||
material-pagination(:count="newComment.total" :btnLength="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
|
||||
material-pagination(:count="newComment.total" :btn-length="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
|
||||
div(v-else :class="$style.unavailable")
|
||||
p {{$t('comment__unavailable')}}
|
||||
</template>
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
<template lang="pug">
|
||||
transition(enter-active-class="animated slideInRight" leave-active-class="animated slideOutDown" @after-enter="handleAfterEnter" @after-leave="handleAfterLeave")
|
||||
div(:class="[$style.container, { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu" v-if="isShowPlayerDetail")
|
||||
div(v-if="isShowPlayerDetail" :class="[$style.container, { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu")
|
||||
div(:class="$style.bg")
|
||||
//- div(:class="$style.bg" :style="bgStyle")
|
||||
//- div(:class="$style.bg2")
|
||||
div(:class="[$style.header, $style.controlBtnLeft]" v-if="appSetting['common.controlBtnPosition'] == 'left'")
|
||||
div(v-if="appSetting['common.controlBtnPosition'] == 'left'" :class="[$style.header, $style.controlBtnLeft]")
|
||||
div(:class="$style.controBtn")
|
||||
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='80%' viewBox='0 0 30.727 30.727' space='preserve')
|
||||
use(xlink:href='#icon-window-hide')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="80%" viewBox="0 0 30.727 30.727" space="preserve")
|
||||
use(xlink:href="#icon-window-hide")
|
||||
button(type="button" :class="$style.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%')
|
||||
use(xlink:href='#icon-fullscreen-exit')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%")
|
||||
use(xlink:href="#icon-fullscreen-exit")
|
||||
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-window-minimize')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-window-minimize")
|
||||
|
||||
//- button(type="button" :class="$style.max" @click="max")
|
||||
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-window-close')
|
||||
div(:class="[$style.header, $style.controlBtnRight]" v-else)
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-window-close")
|
||||
div(v-else :class="[$style.header, $style.controlBtnRight]")
|
||||
div(:class="$style.controBtn")
|
||||
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='35%' viewBox='0 0 30.727 30.727' space='preserve')
|
||||
use(xlink:href='#icon-window-hide')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="35%" viewBox="0 0 30.727 30.727" space="preserve")
|
||||
use(xlink:href="#icon-window-hide")
|
||||
button(type="button" :class="$style.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%')
|
||||
use(xlink:href='#icon-fullscreen-exit')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%")
|
||||
use(xlink:href="#icon-fullscreen-exit")
|
||||
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-window-minimize-2')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-window-minimize-2")
|
||||
|
||||
//- button(type="button" :class="$style.max" @click="max")
|
||||
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
|
||||
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
|
||||
use(xlink:href='#icon-window-close-2')
|
||||
svg(:class="$style.controBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve")
|
||||
use(xlink:href="#icon-window-close-2")
|
||||
div(:class="[$style.main, {[$style.showComment]: isShowPlayComment}]")
|
||||
div.left(:class="$style.left")
|
||||
//- div(:class="$style.info")
|
||||
div(:class="$style.info")
|
||||
img(:class="$style.img" :src="musicInfo.pic" v-if="musicInfo.pic")
|
||||
img(v-if="musicInfo.pic" :class="$style.img" :src="musicInfo.pic")
|
||||
div.description(:class="$style.description")
|
||||
p {{$t('player__music_name')}}{{musicInfo.name}}
|
||||
p {{$t('player__music_singer')}}{{musicInfo.singer}}
|
||||
p(v-if="musicInfo.album") {{$t('player__music_album')}}{{musicInfo.album}}
|
||||
p {{ $t('player__music_name') }}{{ musicInfo.name }}
|
||||
p {{ $t('player__music_singer') }}{{ musicInfo.singer }}
|
||||
p(v-if="musicInfo.album") {{ $t('player__music_album') }}{{musicInfo.album}}
|
||||
|
||||
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
|
||||
LyricPlayer(v-if="visibled")
|
||||
music-comment(:class="$style.comment" :show="isShowPlayComment" :musicInfo="playMusicInfo.musicInfo" @close="hideComment" v-if="visibled")
|
||||
music-comment(v-if="visibled" :class="$style.comment" :show="isShowPlayComment" :music-info="playMusicInfo.musicInfo" @close="hideComment")
|
||||
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
|
||||
play-bar(v-if="visibled")
|
||||
transition(enter-active-class="animated-slow fadeIn" leave-active-class="animated-slow fadeOut")
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
<template lang="pug">
|
||||
material-modal(:show="versionInfo.showModal" @close="handleClose" max-width="60%")
|
||||
main(:class="$style.main" v-if="versionInfo.isLatest")
|
||||
material-modal(:show="versionInfo.showModal" max-width="60%" @close="handleClose")
|
||||
main(v-if="versionInfo.isLatest" :class="$style.main")
|
||||
h2 🎉 已是最新版本 🎉
|
||||
div.scroll.select(:class="$style.info")
|
||||
div(:class="$style.current")
|
||||
h3 最新版本:{{versionInfo.newVersion?.version}}
|
||||
h3 当前版本:{{versionInfo.version}}
|
||||
h3 最新版本:{{ versionInfo.newVersion?.version }}
|
||||
h3 当前版本:{{ versionInfo.version }}
|
||||
h3 版本变化:
|
||||
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
|
||||
div(:class="$style.footer")
|
||||
div(:class="$style.btns")
|
||||
base-btn(v-if="versionInfo.status == 'checking'" :class="$style.btn" disabled) 检查更新中...
|
||||
base-btn(v-else :class="$style.btn" @click="handleCheckUpdate") 重新检查更新
|
||||
main(:class="$style.main" v-else-if="versionInfo.isUnknown")
|
||||
main(v-else-if="versionInfo.isUnknown" :class="$style.main")
|
||||
h2 ❓ 获取最新版本信息失败 ❓
|
||||
div.scroll.select(:class="$style.info")
|
||||
div(:class="$style.current")
|
||||
h3 当前版本:{{versionInfo.version}}
|
||||
h3 当前版本:{{ versionInfo.version }}
|
||||
div(:class="$style.desc")
|
||||
p 更新信息获取失败,可能是无法访问Github导致的,请手动检查更新!
|
||||
p
|
||||
| 检查方法:打开
|
||||
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
|
||||
base-btn(min aria-label="点击打开" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')") 软件发布页
|
||||
| 或
|
||||
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
|
||||
base-btn(min aria-label="点击打开" @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘
|
||||
| (密码:
|
||||
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
|
||||
strong.hover(aria-label="点击复制" @click="handleCopy('glqw')") glqw
|
||||
| )查看它们的
|
||||
strong 版本号
|
||||
| 与当前版本({{versionInfo.version}})对比是否一样,
|
||||
| 与当前版本({{ versionInfo.version }})对比是否一样,
|
||||
p 若一样则不必理会该弹窗,直接关闭即可,否则请手动下载新版本更新。
|
||||
div(:class="$style.footer")
|
||||
div(:class="$style.btns")
|
||||
base-btn(v-if="versionInfo.status == 'error'" :class="$style.btn" @click="handleCheckUpdate") 重新检查更新
|
||||
base-btn(v-else :class="$style.btn" disabled) 检查更新中...
|
||||
main(:class="$style.main" v-else-if="versionInfo.status == 'downloaded'")
|
||||
main(v-else-if="versionInfo.status == 'downloaded'" :class="$style.main")
|
||||
h2 🚀程序更新🚀
|
||||
|
||||
div.scroll.select(:class="$style.info")
|
||||
div(:class="$style.current")
|
||||
h3 最新版本:{{versionInfo.newVersion?.version}}
|
||||
h3 当前版本:{{versionInfo.version}}
|
||||
h3 最新版本:{{ versionInfo.newVersion?.version }}
|
||||
h3 当前版本:{{ versionInfo.version }}
|
||||
h3 版本变化:
|
||||
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
|
||||
div(:class="[$style.history, $style.desc]" v-if="history.length")
|
||||
div(v-if="history.length" :class="[$style.history, $style.desc]")
|
||||
h3 历史版本:
|
||||
div(:class="$style.item" v-for="ver in history")
|
||||
h4 v{{ver.version}}
|
||||
div(v-for="(ver, index) in history" :key="index" :class="$style.item")
|
||||
h4 v{{ ver.version }}
|
||||
pre(v-text="ver.desc")
|
||||
div(:class="$style.footer")
|
||||
div(:class="$style.desc")
|
||||
@@ -58,37 +58,37 @@ material-modal(:show="versionInfo.showModal" @close="handleClose" max-width="60%
|
||||
strong 关闭程序时
|
||||
| 自动更新~
|
||||
div(:class="$style.btns")
|
||||
base-btn(:class="$style.btn" @click.onec="handleRestartClick") 立即重启更新
|
||||
main(:class="$style.main" v-else)
|
||||
base-btn(:class="$style.btn" @click="handleRestartClick") 立即重启更新
|
||||
main(v-else :class="$style.main")
|
||||
h2 🌟发现新版本🌟
|
||||
div.scroll.select(:class="$style.info")
|
||||
div(:class="$style.current")
|
||||
h3 最新版本:{{versionInfo.newVersion?.version}}
|
||||
h3 当前版本:{{versionInfo.version}}
|
||||
h3 最新版本:{{ versionInfo.newVersion?.version }}
|
||||
h3 当前版本:{{ versionInfo.version }}
|
||||
h3 版本变化:
|
||||
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
|
||||
div(:class="[$style.history, $style.desc]" v-if="history.length")
|
||||
div(v-if="history.length" :class="[$style.history, $style.desc]")
|
||||
h3 历史版本:
|
||||
div(:class="$style.item" v-for="ver in history")
|
||||
h4 v{{ver.version}}
|
||||
div(v-for="(ver, index) in history" :key="index" :class="$style.item")
|
||||
h4 v{{ ver.version }}
|
||||
pre(v-text="ver.desc")
|
||||
|
||||
div(:class="$style.footer")
|
||||
div(:class="$style.desc")
|
||||
p 发现有新版本啦,你可以尝试使用自动更新或手动更新,
|
||||
p 手动更新可以去
|
||||
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
|
||||
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')") 软件发布页
|
||||
| 或
|
||||
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
|
||||
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘
|
||||
| (密码:
|
||||
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
|
||||
strong.hover(aria-label="点击复制" @click="handleCopy('glqw')") glqw
|
||||
| ) 下载
|
||||
p 注:国内Windows/MAC用户推荐到网盘下载,若遇到问题可以看
|
||||
strong.hover.underline(@click="handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')" aria-label="点击打开") 常见问题
|
||||
p(v-if="progress") 当前下载进度:{{progress}}
|
||||
strong.hover.underline(aria-label="点击打开" @click="handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')") 常见问题
|
||||
p(v-if="progress") 当前下载进度:{{ progress }}
|
||||
p(v-else)
|
||||
div(:class="$style.btns")
|
||||
base-btn(:class="$style.btn2" @click="handleIgnoreClick") {{ isIgnored ? '取消忽略' : '忽略更新该版本'}}
|
||||
base-btn(:class="$style.btn2" @click="handleIgnoreClick") {{ isIgnored ? '取消忽略' : '忽略更新该版本' }}
|
||||
base-btn(v-if="versionInfo.status == 'downloading'" :class="$style.btn2" disabled) 下载更新中...
|
||||
base-btn(v-else :class="$style.btn2" @click="handleDonwloadClick") 下载更新
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineExpose } from '@common/utils/vueTools'
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
|
||||
const visible = ref(false)
|
||||
const dom_btn = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:placeholder="placeholder"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="$emit('update:modelValue', text)"
|
||||
@input="$emit('update:model-value', text)"
|
||||
@change="sendEvent('change')"
|
||||
@keyup.enter="handleSearch"
|
||||
@keydown.arrow-down.arrow-up.prevent
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'event'],
|
||||
emits: ['update:model-value', 'event'],
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
@@ -174,11 +174,11 @@ export default {
|
||||
str = str.replace(/\s+/g, ' ')
|
||||
let dom_input = this.$refs.dom_input
|
||||
this.text = this.text.substring(0, dom_input.selectionStart) + str + this.text.substring(dom_input.selectionEnd, this.text.length)
|
||||
this.$emit('update:modelValue', this.text)
|
||||
this.$emit('update:model-value', this.text)
|
||||
},
|
||||
handleClearList() {
|
||||
this.text = ''
|
||||
this.$emit('update:modelValue', this.text)
|
||||
this.$emit('update:model-value', this.text)
|
||||
this.sendEvent('submit')
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { onBeforeUnmount, watch } from '@common/utils/vueTools'
|
||||
import { setPlaybackRate as setPlayerPlaybackRate } from '@renderer/plugins/player'
|
||||
import { setPlaybackRate as setPlayerPlaybackRate, setPreservesPitch } from '@renderer/plugins/player'
|
||||
|
||||
import { debounce } from '@common/utils'
|
||||
// import { HOTKEY_PLAYER } from '@common/hotKey'
|
||||
@@ -11,6 +11,7 @@ export default () => {
|
||||
|
||||
setPlaybackRate(appSetting['player.playbackRate'])
|
||||
setPlayerPlaybackRate(appSetting['player.playbackRate'])
|
||||
setPreservesPitch(appSetting['player.preservesPitch'])
|
||||
|
||||
|
||||
const handleSetPlaybackRate = (num: number) => {
|
||||
@@ -41,6 +42,11 @@ export default () => {
|
||||
})
|
||||
|
||||
|
||||
watch(() => appSetting['player.preservesPitch'], preservesPitch => {
|
||||
setPreservesPitch(preservesPitch)
|
||||
})
|
||||
|
||||
|
||||
// window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
|
||||
// window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)
|
||||
window.app_event.on('setPlaybackRate', handleSetPlaybackRate)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
stopPanner,
|
||||
setConvolverMainGain,
|
||||
setConvolverSendGain,
|
||||
setPitchShifter,
|
||||
} from '@renderer/plugins/player'
|
||||
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
@@ -51,7 +52,7 @@ const loadBuffer = async(name: string) => new Promise<AudioBuffer>((resolve, rej
|
||||
})
|
||||
|
||||
export default () => {
|
||||
console.log(appSetting['player.soundEffect.panner.enable'])
|
||||
// console.log(appSetting['player.soundEffect.panner.enable'])
|
||||
if (appSetting['player.soundEffect.panner.enable']) startPanner()
|
||||
setPannerSoundR(appSetting['player.soundEffect.panner.soundR'] / 10)
|
||||
setPannerSpeed(2 * (appSetting['player.soundEffect.panner.speed'] / 10))
|
||||
@@ -66,6 +67,9 @@ export default () => {
|
||||
setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)
|
||||
})
|
||||
}
|
||||
if (appSetting['player.soundEffect.pitchShifter.playbackRate'] != 1) {
|
||||
setPitchShifter(appSetting['player.soundEffect.pitchShifter.playbackRate'])
|
||||
}
|
||||
|
||||
|
||||
watch(() => appSetting['player.soundEffect.panner.enable'], (enable) => {
|
||||
@@ -139,6 +143,10 @@ export default () => {
|
||||
bfs.get('hz16000')!.gain.value = hz16000
|
||||
})
|
||||
|
||||
watch(() => appSetting['player.soundEffect.pitchShifter.playbackRate'], (playbackRate) => {
|
||||
setPitchShifter(playbackRate)
|
||||
})
|
||||
|
||||
|
||||
// window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)
|
||||
// window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)
|
||||
|
||||
@@ -84,6 +84,7 @@ export default {
|
||||
pointer-events: none;
|
||||
// text-align: justify;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
:global(.tips-fade-enter-active), :global(.tips-fade-leave-active) {
|
||||
|
||||
@@ -38,12 +38,19 @@ export const convolutions = [
|
||||
{ name: 'feedback_spring', mainGain: 1.8, sendGain: 0.8, source: 'feedback-spring.wav' },
|
||||
// { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' },
|
||||
] as const
|
||||
// 半音
|
||||
// export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const
|
||||
|
||||
let convolver: ConvolverNode
|
||||
let convolverSourceGainNode: GainNode
|
||||
let convolverOutputGainNode: GainNode
|
||||
let convolverDynamicsCompressor: DynamicsCompressorNode
|
||||
let gainNode: GainNode
|
||||
let panner: PannerNode
|
||||
let pitchShifterNode: AudioWorkletNode
|
||||
let pitchShifterNodePitchFactor: AudioParam
|
||||
let pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none'
|
||||
let pitchShifterNodeTempValue = 1
|
||||
export const soundR = 0.5
|
||||
|
||||
|
||||
@@ -53,6 +60,7 @@ export const createAudio = () => {
|
||||
audio.controls = false
|
||||
audio.autoplay = true
|
||||
audio.preload = 'auto'
|
||||
audio.crossOrigin = 'anonymous'
|
||||
}
|
||||
|
||||
const initAnalyser = () => {
|
||||
@@ -100,26 +108,21 @@ const initAdvancedAudioFeatures = () => {
|
||||
if (audioContext) return
|
||||
if (!audio) throw new Error('audio not defined')
|
||||
audioContext = new window.AudioContext()
|
||||
mediaSource = audioContext.createMediaElementSource(audio)
|
||||
|
||||
initAnalyser()
|
||||
mediaSource.connect(analyser)
|
||||
// analyser.connect(audioContext.destination)
|
||||
|
||||
initBiquadFilter()
|
||||
analyser.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
|
||||
|
||||
initConvolver()
|
||||
initPanner()
|
||||
initGain()
|
||||
// source -> analyser -> biquadFilter -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain
|
||||
mediaSource = audioContext.createMediaElementSource(audio)
|
||||
mediaSource.connect(analyser)
|
||||
analyser.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
|
||||
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
|
||||
lastBiquadFilter.connect(convolverSourceGainNode)
|
||||
lastBiquadFilter.connect(convolver)
|
||||
|
||||
initPanner()
|
||||
convolverDynamicsCompressor.connect(panner)
|
||||
|
||||
initGain()
|
||||
panner.connect(gainNode)
|
||||
|
||||
gainNode.connect(audioContext.destination)
|
||||
}
|
||||
|
||||
@@ -216,6 +219,101 @@ export const startPanner = () => {
|
||||
}, pannerInfo.speed * 10)
|
||||
}
|
||||
|
||||
let isConnected = true
|
||||
const connectNode = () => {
|
||||
if (isConnected) return
|
||||
console.log('connect Node')
|
||||
analyser?.connect(biquads.get(`hz${freqs[0]}`) as BiquadFilterNode)
|
||||
isConnected = true
|
||||
if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {
|
||||
disconnectPitchShifterNode()
|
||||
}
|
||||
}
|
||||
const disconnectNode = () => {
|
||||
if (!isConnected) return
|
||||
console.log('disconnect Node')
|
||||
analyser?.disconnect()
|
||||
isConnected = false
|
||||
if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {
|
||||
disconnectPitchShifterNode()
|
||||
}
|
||||
}
|
||||
const connectPitchShifterNode = () => {
|
||||
console.log('connect Pitch Shifter Node')
|
||||
audio!.addEventListener('playing', connectNode)
|
||||
audio!.addEventListener('pause', disconnectNode)
|
||||
audio!.addEventListener('waiting', disconnectNode)
|
||||
audio!.addEventListener('emptied', disconnectNode)
|
||||
if (audio!.paused) disconnectNode()
|
||||
|
||||
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
|
||||
lastBiquadFilter.disconnect()
|
||||
lastBiquadFilter.connect(pitchShifterNode)
|
||||
|
||||
pitchShifterNode.connect(convolver)
|
||||
pitchShifterNode.connect(convolverSourceGainNode)
|
||||
// convolverDynamicsCompressor.disconnect(panner)
|
||||
// convolverDynamicsCompressor.connect(pitchShifterNode)
|
||||
// pitchShifterNode.connect(panner)
|
||||
pitchShifterNodeLoadStatus = 'connected'
|
||||
pitchShifterNodePitchFactor.value = pitchShifterNodeTempValue
|
||||
}
|
||||
const disconnectPitchShifterNode = () => {
|
||||
console.log('disconnect Pitch Shifter Node')
|
||||
const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1) as Freqs}`) as BiquadFilterNode)
|
||||
lastBiquadFilter.disconnect()
|
||||
lastBiquadFilter.connect(convolver)
|
||||
lastBiquadFilter.connect(convolverSourceGainNode)
|
||||
pitchShifterNodeLoadStatus = 'unconnect'
|
||||
|
||||
audio!.removeEventListener('playing', connectNode)
|
||||
audio!.removeEventListener('pause', disconnectNode)
|
||||
audio!.removeEventListener('waiting', disconnectNode)
|
||||
audio!.removeEventListener('emptied', disconnectNode)
|
||||
connectNode()
|
||||
}
|
||||
const loadPitchShifterNode = () => {
|
||||
pitchShifterNodeLoadStatus = 'loading'
|
||||
initAdvancedAudioFeatures()
|
||||
// source -> analyser -> biquadFilter -> audioWorklet(pitch shifter) -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain
|
||||
void audioContext.audioWorklet.addModule(new URL(
|
||||
/* webpackChunkName: 'pitch_shifter.audioWorklet' */
|
||||
'./pitch-shifter/phase-vocoder.js',
|
||||
import.meta.url,
|
||||
)).then(() => {
|
||||
console.log('pitch shifter audio worklet loaded')
|
||||
// https://github.com/olvb/phaze/issues/26#issuecomment-1574629971
|
||||
pitchShifterNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor', { outputChannelCount: [2] })
|
||||
let pitchFactorParam = pitchShifterNode.parameters.get('pitchFactor')
|
||||
if (!pitchFactorParam) return
|
||||
pitchShifterNodePitchFactor = pitchFactorParam
|
||||
pitchShifterNodeLoadStatus = 'unconnect'
|
||||
if (pitchShifterNodeTempValue == 1) return
|
||||
|
||||
connectPitchShifterNode()
|
||||
})
|
||||
}
|
||||
|
||||
export const setPitchShifter = (val: number) => {
|
||||
// console.log('setPitchShifter', val)
|
||||
pitchShifterNodeTempValue = val
|
||||
switch (pitchShifterNodeLoadStatus) {
|
||||
case 'loading':
|
||||
break
|
||||
case 'none':
|
||||
loadPitchShifterNode()
|
||||
break
|
||||
case 'connected':
|
||||
// a: 1 = 半音
|
||||
// value = 2 ** (a / 12)
|
||||
pitchShifterNodePitchFactor.value = val
|
||||
break
|
||||
case 'unconnect':
|
||||
connectPitchShifterNode()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export const hasInitedAdvancedAudioFeatures = (): boolean => audioContext != null
|
||||
|
||||
export const setResource = (src: string) => {
|
||||
@@ -253,6 +351,11 @@ export const setPlaybackRate = (rate: number) => {
|
||||
audio.playbackRate = rate
|
||||
}
|
||||
|
||||
export const setPreservesPitch = (preservesPitch: boolean) => {
|
||||
if (!audio) return
|
||||
audio.preservesPitch = preservesPitch
|
||||
}
|
||||
|
||||
export const getMute = (): boolean => {
|
||||
return audio?.muted ?? false
|
||||
}
|
||||
500
src/renderer/plugins/player/pitch-shifter/fft.js
Normal file
500
src/renderer/plugins/player/pitch-shifter/fft.js
Normal file
@@ -0,0 +1,500 @@
|
||||
// https://github.com/indutny/fft.js
|
||||
|
||||
|
||||
function FFT(size) {
|
||||
this.size = size | 0
|
||||
if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) { throw new Error('FFT size must be a power of two and bigger than 1') }
|
||||
|
||||
this._csize = size << 1
|
||||
|
||||
// NOTE: Use of `var` is intentional for old V8 versions
|
||||
let table = new Array(this.size * 2)
|
||||
for (let i = 0; i < table.length; i += 2) {
|
||||
const angle = Math.PI * i / this.size
|
||||
table[i] = Math.cos(angle)
|
||||
table[i + 1] = -Math.sin(angle)
|
||||
}
|
||||
this.table = table
|
||||
|
||||
// Find size's power of two
|
||||
let power = 0
|
||||
for (let t = 1; this.size > t; t <<= 1) { power++ }
|
||||
|
||||
// Calculate initial step's width:
|
||||
// * If we are full radix-4 - it is 2x smaller to give inital len=8
|
||||
// * Otherwise it is the same as `power` to give len=4
|
||||
this._width = power % 2 === 0 ? power - 1 : power
|
||||
|
||||
// Pre-compute bit-reversal patterns
|
||||
this._bitrev = new Array(1 << this._width)
|
||||
for (let j = 0; j < this._bitrev.length; j++) {
|
||||
this._bitrev[j] = 0
|
||||
for (let shift = 0; shift < this._width; shift += 2) {
|
||||
let revShift = this._width - shift - 2
|
||||
this._bitrev[j] |= ((j >>> shift) & 3) << revShift
|
||||
}
|
||||
}
|
||||
|
||||
this._out = null
|
||||
this._data = null
|
||||
this._inv = 0
|
||||
}
|
||||
|
||||
FFT.prototype.fromComplexArray = function fromComplexArray(complex, storage) {
|
||||
let res = storage || new Array(complex.length >>> 1)
|
||||
for (let i = 0; i < complex.length; i += 2) { res[i >>> 1] = complex[i] }
|
||||
return res
|
||||
}
|
||||
|
||||
FFT.prototype.createComplexArray = function createComplexArray() {
|
||||
const res = new Array(this._csize)
|
||||
for (let i = 0; i < res.length; i++) { res[i] = 0 }
|
||||
return res
|
||||
}
|
||||
|
||||
FFT.prototype.toComplexArray = function toComplexArray(input, storage) {
|
||||
let res = storage || this.createComplexArray()
|
||||
for (let i = 0; i < res.length; i += 2) {
|
||||
res[i] = input[i >>> 1]
|
||||
res[i + 1] = 0
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
FFT.prototype.completeSpectrum = function completeSpectrum(spectrum) {
|
||||
let size = this._csize
|
||||
let half = size >>> 1
|
||||
for (let i = 2; i < half; i += 2) {
|
||||
spectrum[size - i] = spectrum[i]
|
||||
spectrum[size - i + 1] = -spectrum[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
FFT.prototype.transform = function transform(out, data) {
|
||||
if (out === data) { throw new Error('Input and output buffers must be different') }
|
||||
|
||||
this._out = out
|
||||
this._data = data
|
||||
this._inv = 0
|
||||
this._transform4()
|
||||
this._out = null
|
||||
this._data = null
|
||||
}
|
||||
|
||||
FFT.prototype.realTransform = function realTransform(out, data) {
|
||||
if (out === data) { throw new Error('Input and output buffers must be different') }
|
||||
|
||||
this._out = out
|
||||
this._data = data
|
||||
this._inv = 0
|
||||
this._realTransform4()
|
||||
this._out = null
|
||||
this._data = null
|
||||
}
|
||||
|
||||
FFT.prototype.inverseTransform = function inverseTransform(out, data) {
|
||||
if (out === data) { throw new Error('Input and output buffers must be different') }
|
||||
|
||||
this._out = out
|
||||
this._data = data
|
||||
this._inv = 1
|
||||
this._transform4()
|
||||
for (let i = 0; i < out.length; i++) { out[i] /= this.size }
|
||||
this._out = null
|
||||
this._data = null
|
||||
}
|
||||
|
||||
// radix-4 implementation
|
||||
//
|
||||
// NOTE: Uses of `var` are intentional for older V8 version that do not
|
||||
// support both `let compound assignments` and `const phi`
|
||||
FFT.prototype._transform4 = function _transform4() {
|
||||
let out = this._out
|
||||
let size = this._csize
|
||||
|
||||
// Initial step (permute and transform)
|
||||
let width = this._width
|
||||
let step = 1 << width
|
||||
let len = (size / step) << 1
|
||||
|
||||
let outOff
|
||||
let t
|
||||
let bitrev = this._bitrev
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t]
|
||||
this._singleTransform2(outOff, off, step)
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t]
|
||||
this._singleTransform4(outOff, off, step)
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
let inv = this._inv ? -1 : 1
|
||||
let table = this.table
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1
|
||||
let quarterLen = len >>> 2
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
// Full case
|
||||
let limit = outOff + quarterLen
|
||||
for (let i = outOff, k = 0; i < limit; i += 2, k += step) {
|
||||
const A = i
|
||||
const B = A + quarterLen
|
||||
const C = B + quarterLen
|
||||
const D = C + quarterLen
|
||||
|
||||
// Original values
|
||||
const Ar = out[A]
|
||||
const Ai = out[A + 1]
|
||||
const Br = out[B]
|
||||
const Bi = out[B + 1]
|
||||
const Cr = out[C]
|
||||
const Ci = out[C + 1]
|
||||
const Dr = out[D]
|
||||
const Di = out[D + 1]
|
||||
|
||||
// Middle values
|
||||
const MAr = Ar
|
||||
const MAi = Ai
|
||||
|
||||
const tableBr = table[k]
|
||||
const tableBi = inv * table[k + 1]
|
||||
const MBr = Br * tableBr - Bi * tableBi
|
||||
const MBi = Br * tableBi + Bi * tableBr
|
||||
|
||||
const tableCr = table[2 * k]
|
||||
const tableCi = inv * table[2 * k + 1]
|
||||
const MCr = Cr * tableCr - Ci * tableCi
|
||||
const MCi = Cr * tableCi + Ci * tableCr
|
||||
|
||||
const tableDr = table[3 * k]
|
||||
const tableDi = inv * table[3 * k + 1]
|
||||
const MDr = Dr * tableDr - Di * tableDi
|
||||
const MDi = Dr * tableDi + Di * tableDr
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = MAr + MCr
|
||||
const T0i = MAi + MCi
|
||||
const T1r = MAr - MCr
|
||||
const T1i = MAi - MCi
|
||||
const T2r = MBr + MDr
|
||||
const T2i = MBi + MDi
|
||||
const T3r = inv * (MBr - MDr)
|
||||
const T3i = inv * (MBi - MDi)
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r
|
||||
const FAi = T0i + T2i
|
||||
|
||||
const FCr = T0r - T2r
|
||||
const FCi = T0i - T2i
|
||||
|
||||
const FBr = T1r + T3i
|
||||
const FBi = T1i - T3r
|
||||
|
||||
const FDr = T1r - T3i
|
||||
const FDi = T1i + T3r
|
||||
|
||||
out[A] = FAr
|
||||
out[A + 1] = FAi
|
||||
out[B] = FBr
|
||||
out[B + 1] = FBi
|
||||
out[C] = FCr
|
||||
out[C + 1] = FCi
|
||||
out[D] = FDr
|
||||
out[D + 1] = FDi
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// radix-2 implementation
|
||||
//
|
||||
// NOTE: Only called for len=4
|
||||
FFT.prototype._singleTransform2 = function _singleTransform2(outOff, off,
|
||||
step) {
|
||||
const out = this._out
|
||||
const data = this._data
|
||||
|
||||
const evenR = data[off]
|
||||
const evenI = data[off + 1]
|
||||
const oddR = data[off + step]
|
||||
const oddI = data[off + step + 1]
|
||||
|
||||
const leftR = evenR + oddR
|
||||
const leftI = evenI + oddI
|
||||
const rightR = evenR - oddR
|
||||
const rightI = evenI - oddI
|
||||
|
||||
out[outOff] = leftR
|
||||
out[outOff + 1] = leftI
|
||||
out[outOff + 2] = rightR
|
||||
out[outOff + 3] = rightI
|
||||
}
|
||||
|
||||
// radix-4
|
||||
//
|
||||
// NOTE: Only called for len=8
|
||||
FFT.prototype._singleTransform4 = function _singleTransform4(outOff, off,
|
||||
step) {
|
||||
const out = this._out
|
||||
const data = this._data
|
||||
const inv = this._inv ? -1 : 1
|
||||
const step2 = step * 2
|
||||
const step3 = step * 3
|
||||
|
||||
// Original values
|
||||
const Ar = data[off]
|
||||
const Ai = data[off + 1]
|
||||
const Br = data[off + step]
|
||||
const Bi = data[off + step + 1]
|
||||
const Cr = data[off + step2]
|
||||
const Ci = data[off + step2 + 1]
|
||||
const Dr = data[off + step3]
|
||||
const Di = data[off + step3 + 1]
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr
|
||||
const T0i = Ai + Ci
|
||||
const T1r = Ar - Cr
|
||||
const T1i = Ai - Ci
|
||||
const T2r = Br + Dr
|
||||
const T2i = Bi + Di
|
||||
const T3r = inv * (Br - Dr)
|
||||
const T3i = inv * (Bi - Di)
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r
|
||||
const FAi = T0i + T2i
|
||||
|
||||
const FBr = T1r + T3i
|
||||
const FBi = T1i - T3r
|
||||
|
||||
const FCr = T0r - T2r
|
||||
const FCi = T0i - T2i
|
||||
|
||||
const FDr = T1r - T3i
|
||||
const FDi = T1i + T3r
|
||||
|
||||
out[outOff] = FAr
|
||||
out[outOff + 1] = FAi
|
||||
out[outOff + 2] = FBr
|
||||
out[outOff + 3] = FBi
|
||||
out[outOff + 4] = FCr
|
||||
out[outOff + 5] = FCi
|
||||
out[outOff + 6] = FDr
|
||||
out[outOff + 7] = FDi
|
||||
}
|
||||
|
||||
// Real input radix-4 implementation
|
||||
FFT.prototype._realTransform4 = function _realTransform4() {
|
||||
let out = this._out
|
||||
let size = this._csize
|
||||
|
||||
// Initial step (permute and transform)
|
||||
let width = this._width
|
||||
let step = 1 << width
|
||||
let len = (size / step) << 1
|
||||
|
||||
let outOff
|
||||
let t
|
||||
let bitrev = this._bitrev
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t]
|
||||
this._singleRealTransform2(outOff, off >>> 1, step >>> 1)
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t]
|
||||
this._singleRealTransform4(outOff, off >>> 1, step >>> 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
let inv = this._inv ? -1 : 1
|
||||
let table = this.table
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1
|
||||
let halfLen = len >>> 1
|
||||
let quarterLen = halfLen >>> 1
|
||||
let hquarterLen = quarterLen >>> 1
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) {
|
||||
let A = outOff + i
|
||||
let B = A + quarterLen
|
||||
let C = B + quarterLen
|
||||
let D = C + quarterLen
|
||||
|
||||
// Original values
|
||||
let Ar = out[A]
|
||||
let Ai = out[A + 1]
|
||||
let Br = out[B]
|
||||
let Bi = out[B + 1]
|
||||
let Cr = out[C]
|
||||
let Ci = out[C + 1]
|
||||
let Dr = out[D]
|
||||
let Di = out[D + 1]
|
||||
|
||||
// Middle values
|
||||
let MAr = Ar
|
||||
let MAi = Ai
|
||||
|
||||
let tableBr = table[k]
|
||||
let tableBi = inv * table[k + 1]
|
||||
let MBr = Br * tableBr - Bi * tableBi
|
||||
let MBi = Br * tableBi + Bi * tableBr
|
||||
|
||||
let tableCr = table[2 * k]
|
||||
let tableCi = inv * table[2 * k + 1]
|
||||
let MCr = Cr * tableCr - Ci * tableCi
|
||||
let MCi = Cr * tableCi + Ci * tableCr
|
||||
|
||||
let tableDr = table[3 * k]
|
||||
let tableDi = inv * table[3 * k + 1]
|
||||
let MDr = Dr * tableDr - Di * tableDi
|
||||
let MDi = Dr * tableDi + Di * tableDr
|
||||
|
||||
// Pre-Final values
|
||||
let T0r = MAr + MCr
|
||||
let T0i = MAi + MCi
|
||||
let T1r = MAr - MCr
|
||||
let T1i = MAi - MCi
|
||||
let T2r = MBr + MDr
|
||||
let T2i = MBi + MDi
|
||||
let T3r = inv * (MBr - MDr)
|
||||
let T3i = inv * (MBi - MDi)
|
||||
|
||||
// Final values
|
||||
let FAr = T0r + T2r
|
||||
let FAi = T0i + T2i
|
||||
|
||||
let FBr = T1r + T3i
|
||||
let FBi = T1i - T3r
|
||||
|
||||
out[A] = FAr
|
||||
out[A + 1] = FAi
|
||||
out[B] = FBr
|
||||
out[B + 1] = FBi
|
||||
|
||||
// Output final middle point
|
||||
if (i === 0) {
|
||||
let FCr = T0r - T2r
|
||||
let FCi = T0i - T2i
|
||||
out[C] = FCr
|
||||
out[C + 1] = FCi
|
||||
continue
|
||||
}
|
||||
|
||||
// Do not overwrite ourselves
|
||||
if (i === hquarterLen) { continue }
|
||||
|
||||
// In the flipped case:
|
||||
// MAi = -MAi
|
||||
// MBr=-MBi, MBi=-MBr
|
||||
// MCr=-MCr
|
||||
// MDr=MDi, MDi=MDr
|
||||
let ST0r = T1r
|
||||
let ST0i = -T1i
|
||||
let ST1r = T0r
|
||||
let ST1i = -T0i
|
||||
let ST2r = -inv * T3i
|
||||
let ST2i = -inv * T3r
|
||||
let ST3r = -inv * T2i
|
||||
let ST3i = -inv * T2r
|
||||
|
||||
let SFAr = ST0r + ST2r
|
||||
let SFAi = ST0i + ST2i
|
||||
|
||||
let SFBr = ST1r + ST3i
|
||||
let SFBi = ST1i - ST3r
|
||||
|
||||
let SA = outOff + quarterLen - i
|
||||
let SB = outOff + halfLen - i
|
||||
|
||||
out[SA] = SFAr
|
||||
out[SA + 1] = SFAi
|
||||
out[SB] = SFBr
|
||||
out[SB + 1] = SFBi
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// radix-2 implementation
|
||||
//
|
||||
// NOTE: Only called for len=4
|
||||
FFT.prototype._singleRealTransform2 = function _singleRealTransform2(outOff,
|
||||
off,
|
||||
step) {
|
||||
const out = this._out
|
||||
const data = this._data
|
||||
|
||||
const evenR = data[off]
|
||||
const oddR = data[off + step]
|
||||
|
||||
const leftR = evenR + oddR
|
||||
const rightR = evenR - oddR
|
||||
|
||||
out[outOff] = leftR
|
||||
out[outOff + 1] = 0
|
||||
out[outOff + 2] = rightR
|
||||
out[outOff + 3] = 0
|
||||
}
|
||||
|
||||
// radix-4
|
||||
//
|
||||
// NOTE: Only called for len=8
|
||||
FFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff,
|
||||
off,
|
||||
step) {
|
||||
const out = this._out
|
||||
const data = this._data
|
||||
const inv = this._inv ? -1 : 1
|
||||
const step2 = step * 2
|
||||
const step3 = step * 3
|
||||
|
||||
// Original values
|
||||
const Ar = data[off]
|
||||
const Br = data[off + step]
|
||||
const Cr = data[off + step2]
|
||||
const Dr = data[off + step3]
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr
|
||||
const T1r = Ar - Cr
|
||||
const T2r = Br + Dr
|
||||
const T3r = inv * (Br - Dr)
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r
|
||||
|
||||
const FBr = T1r
|
||||
const FBi = -T3r
|
||||
|
||||
const FCr = T0r - T2r
|
||||
|
||||
const FDr = T1r
|
||||
const FDi = T3r
|
||||
|
||||
out[outOff] = FAr
|
||||
out[outOff + 1] = 0
|
||||
out[outOff + 2] = FBr
|
||||
out[outOff + 3] = FBi
|
||||
out[outOff + 4] = FCr
|
||||
out[outOff + 5] = 0
|
||||
out[outOff + 6] = FDr
|
||||
out[outOff + 7] = FDi
|
||||
}
|
||||
|
||||
export default FFT
|
||||
224
src/renderer/plugins/player/pitch-shifter/ola-processor.js
Normal file
224
src/renderer/plugins/player/pitch-shifter/ola-processor.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/* eslint-disable no-var */
|
||||
|
||||
const WEBAUDIO_BLOCK_SIZE = 128
|
||||
|
||||
|
||||
/** Overlap-Add Node */
|
||||
class OLAProcessor extends globalThis.AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
this.keepReturnTrue = true
|
||||
this.processNow = false
|
||||
|
||||
this.nbInputs = options.numberOfInputs
|
||||
this.nbOutputs = options.numberOfOutputs
|
||||
|
||||
this.blockSize = options.processorOptions.blockSize
|
||||
// TODO for now, the only support hop size is the size of a web audio block
|
||||
this.hopSize = WEBAUDIO_BLOCK_SIZE
|
||||
|
||||
this.nbOverlaps = this.blockSize / this.hopSize
|
||||
|
||||
this.lastSilencedHopCount = 0
|
||||
this.nbOverlaps2x = this.nbOverlaps * 2
|
||||
this.fakeEmptyInputs = [new Array(2).fill(new Float32Array(WEBAUDIO_BLOCK_SIZE))]
|
||||
|
||||
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.inputBuffers = new Array(this.nbInputs)
|
||||
this.inputBuffersHead = new Array(this.nbInputs)
|
||||
this.inputBuffersToSend = new Array(this.nbInputs)
|
||||
// assume 2 channels per input
|
||||
for (var i = 0; i < this.nbInputs; i++) {
|
||||
this.allocateInputChannels(i, 2)
|
||||
}
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.outputBuffers = new Array(this.nbOutputs)
|
||||
this.outputBuffersToRetrieve = new Array(this.nbOutputs)
|
||||
// assume 2 channels per output
|
||||
for (i = 0; i < this.nbOutputs; i++) {
|
||||
this.allocateOutputChannels(i, 2)
|
||||
}
|
||||
|
||||
this.port.onmessage = (e) => this.keepReturnTrue = false
|
||||
}
|
||||
|
||||
/** Handles dynamic reallocation of input/output channels buffer
|
||||
(channel numbers may vary during lifecycle) **/
|
||||
reallocateChannelsIfNeeded(inputs, outputs, force) {
|
||||
for (var i = 0; i < this.nbInputs; i++) {
|
||||
let nbChannels = inputs[i].length
|
||||
if (force || (nbChannels != this.inputBuffers[i].length)) {
|
||||
this.allocateInputChannels(i, nbChannels)
|
||||
// console.log("reallocateChannelsIfNeeded");
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < this.nbOutputs; i++) {
|
||||
let nbChannels = outputs[i].length
|
||||
if (force || (nbChannels != this.outputBuffers[i].length)) {
|
||||
this.allocateOutputChannels(i, nbChannels)
|
||||
// console.log("reallocateChannelsIfNeeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allocateInputChannels(inputIndex, nbChannels) {
|
||||
// allocate input buffers
|
||||
// console.log("allocateInputChannels");
|
||||
|
||||
this.inputBuffers[inputIndex] = new Array(nbChannels)
|
||||
for (var i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE)
|
||||
this.inputBuffers[inputIndex][i].fill(0)
|
||||
}
|
||||
|
||||
// allocate input buffers to send and head pointers to copy from
|
||||
// (cannot directly send a pointer/subarray because input may be modified)
|
||||
this.inputBuffersHead[inputIndex] = new Array(nbChannels)
|
||||
this.inputBuffersToSend[inputIndex] = new Array(nbChannels)
|
||||
for (i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize)
|
||||
this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize)
|
||||
}
|
||||
}
|
||||
|
||||
allocateOutputChannels(outputIndex, nbChannels) {
|
||||
// allocate output buffers
|
||||
this.outputBuffers[outputIndex] = new Array(nbChannels)
|
||||
for (var i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize)
|
||||
this.outputBuffers[outputIndex][i].fill(0)
|
||||
}
|
||||
|
||||
// allocate output buffers to retrieve
|
||||
// (cannot send a pointer/subarray because new output has to be add to existing output)
|
||||
this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels)
|
||||
for (i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize)
|
||||
this.outputBuffersToRetrieve[outputIndex][i].fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
checkForNotSilence(value) {
|
||||
return value !== 0
|
||||
}
|
||||
|
||||
/** Read next web audio block to input buffers **/
|
||||
readInputs(inputs) {
|
||||
// when playback is paused, we may stop receiving new samples
|
||||
/* if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
for (var i = 0; i < this.nbInputs; i++) {
|
||||
for (var j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].fill(0, this.blockSize);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} */
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = inputs[i][j]
|
||||
this.inputBuffers[i][j]?.set(webAudioBlock, this.blockSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of input buffers to receive new web audio block **/
|
||||
shiftInputBuffers() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy contents of input buffers to buffer actually sent to process **/
|
||||
prepareInputBuffersToSend() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add contents of output buffers just processed to output buffers **/
|
||||
handleOutputBuffersToRetrieve() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
for (let k = 0; k < this.blockSize; k++) {
|
||||
this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Write next web audio block from output buffers **/
|
||||
writeOutputs(outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE)
|
||||
outputs[i][j]?.set(webAudioBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of output buffers to receive new web audio block **/
|
||||
shiftOutputBuffers() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)
|
||||
this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
// console.log(inputs[0].length ? "active" : "inactive");
|
||||
// this.reallocateChannelsIfNeeded(inputs, outputs);
|
||||
// if (inputs[0][0].some(this.checkForNotSilence) || inputs[0][1].some(this.checkForNotSilence))
|
||||
// console.log(inputs[0].length)
|
||||
if (inputs[0].length < 2) {
|
||||
// DUE TO CHROME BUG/INCONSISTENCY, WHEN INACTIVE SILENT NODE IS CONNECTED, inputs[0] IS EITHER EMPTY OR CONTAINS 1 CHANNEL OF SILENT AUDIO DATA, REQUIRES SPECIAL HANDLING
|
||||
// if (inputs[0][0].some(this.checkForNotSilence)) console.warn("single channel not silence exception!");
|
||||
if (this.lastSilencedHopCount < this.nbOverlaps2x) {
|
||||
// ALLOW nbOverlaps2x BLOCKS OF SILENCE TO COME THROUGH TO ACCOMODATE LATENCY TAIL
|
||||
this.lastSilencedHopCount++
|
||||
inputs = this.fakeEmptyInputs
|
||||
this.processNow = true
|
||||
} else {
|
||||
// console.warn("skipping processing");
|
||||
if (this.lastSilencedHopCount === this.nbOverlaps2x) {
|
||||
this.lastSilencedHopCount++
|
||||
this.reallocateChannelsIfNeeded(this.fakeEmptyInputs, outputs, true)
|
||||
// console.warn("reallocateChannels");
|
||||
}
|
||||
this.processNow = false // ENABLES SKIPPING UNNEEDED PROCESSING OF SILENT INPUT
|
||||
}
|
||||
} else {
|
||||
if (this.lastSilencedHopCount) {
|
||||
this.lastSilencedHopCount = 0
|
||||
// this.reallocateChannelsIfNeeded(inputs, outputs, true);
|
||||
// console.warn("reallocateChannels");
|
||||
}
|
||||
this.processNow = true
|
||||
}
|
||||
if (this.processNow) {
|
||||
this.readInputs(inputs)
|
||||
this.shiftInputBuffers()
|
||||
this.prepareInputBuffersToSend()
|
||||
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params)
|
||||
this.handleOutputBuffersToRetrieve()
|
||||
this.writeOutputs(outputs)
|
||||
this.shiftOutputBuffers()
|
||||
}
|
||||
return this.keepReturnTrue
|
||||
}
|
||||
|
||||
/* processOLA(inputs, outputs, params) {
|
||||
console.assert(false, "Not overriden");
|
||||
} */
|
||||
}
|
||||
|
||||
export default OLAProcessor
|
||||
176
src/renderer/plugins/player/pitch-shifter/phase-vocoder.js
Normal file
176
src/renderer/plugins/player/pitch-shifter/phase-vocoder.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// https://github.com/olvb/phaze/issues/26#issuecomment-1573938170
|
||||
// https://github.com/olvb/phaze
|
||||
import FFT from './fft'
|
||||
import OLAProcessor from './ola-processor'
|
||||
|
||||
|
||||
const DEFAULT_BUFFERED_BLOCK_SIZE = 4096
|
||||
|
||||
function genHannWindow(length) {
|
||||
let win = new Float32Array(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length))
|
||||
}
|
||||
return win
|
||||
}
|
||||
|
||||
class PhaseVocoderProcessor extends OLAProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [{
|
||||
name: 'pitchFactor',
|
||||
defaultValue: 1.0,
|
||||
automationRate: 'k-rate',
|
||||
}, /* ,
|
||||
{
|
||||
name: 'pitchCents',
|
||||
defaultValue: 0.0,
|
||||
automationRate: 'k-rate'
|
||||
} */]
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
(options.processorOptions ??= {}).blockSize ??= DEFAULT_BUFFERED_BLOCK_SIZE
|
||||
super(options)
|
||||
|
||||
this.fftSize = this.blockSize
|
||||
this.timeCursor = 0
|
||||
|
||||
this.hannWindow = genHannWindow(this.blockSize)
|
||||
|
||||
// prepare FFT and pre-allocate buffers
|
||||
this.fft = new FFT(this.fftSize)
|
||||
this.freqComplexBuffer = this.fft.createComplexArray()
|
||||
this.freqComplexBufferShifted = this.fft.createComplexArray()
|
||||
this.timeComplexBuffer = this.fft.createComplexArray()
|
||||
this.magnitudes = new Float32Array(this.fftSize / 2 + 1)
|
||||
this.peakIndexes = new Int32Array(this.magnitudes.length)
|
||||
this.nbPeaks = 0
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, parameters) {
|
||||
// k-rate automation, param arrays only have single value
|
||||
const pitchFactor = parameters.pitchFactor[0]/* || Math.pow(2, (parameters.pitchCents[0]/12)) */
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < inputs[i].length; j++) {
|
||||
// big assumption here: output is symetric to input
|
||||
let input = inputs[i][j]
|
||||
let output = outputs[i][j]
|
||||
|
||||
this.applyHannWindow(input)
|
||||
|
||||
this.fft.realTransform(this.freqComplexBuffer, input)
|
||||
|
||||
this.computeMagnitudes()
|
||||
this.findPeaks()
|
||||
this.shiftPeaks(pitchFactor)
|
||||
|
||||
this.fft.completeSpectrum(this.freqComplexBufferShifted)
|
||||
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted)
|
||||
this.fft.fromComplexArray(this.timeComplexBuffer, output)
|
||||
|
||||
this.applyHannWindow(output)
|
||||
}
|
||||
}
|
||||
|
||||
this.timeCursor += this.hopSize
|
||||
}
|
||||
|
||||
/** Apply Hann window in-place */
|
||||
applyHannWindow(input) {
|
||||
for (let i = 0; i < this.blockSize; i++) {
|
||||
input[i] = input[i] * this.hannWindow[i]
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute squared magnitudes for peak finding **/
|
||||
computeMagnitudes() {
|
||||
let i = 0; let j = 0
|
||||
while (i < this.magnitudes.length) {
|
||||
let real = this.freqComplexBuffer[j]
|
||||
let imag = this.freqComplexBuffer[j + 1]
|
||||
// no need to sqrt for peak finding
|
||||
this.magnitudes[i] = real ** 2 + imag ** 2
|
||||
i += 1
|
||||
j += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Find peaks in spectrum magnitudes **/
|
||||
findPeaks() {
|
||||
this.nbPeaks = 0
|
||||
let i = 2
|
||||
let end = this.magnitudes.length - 2
|
||||
|
||||
while (i < end) {
|
||||
let mag = this.magnitudes[i]
|
||||
|
||||
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
this.peakIndexes[this.nbPeaks] = i
|
||||
this.nbPeaks++
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm */
|
||||
shiftPeaks(pitchFactor) {
|
||||
// zero-fill new spectrum
|
||||
this.freqComplexBufferShifted.fill(0)
|
||||
|
||||
for (let i = 0; i < this.nbPeaks; i++) {
|
||||
let peakIndex = this.peakIndexes[i]
|
||||
let peakIndexShifted = Math.round(peakIndex * pitchFactor)
|
||||
|
||||
if (peakIndexShifted > this.magnitudes.length) break
|
||||
|
||||
// find region of influence
|
||||
let startIndex = 0
|
||||
let endIndex = this.fftSize
|
||||
if (i > 0) {
|
||||
startIndex = peakIndex - Math.floor((peakIndex - this.peakIndexes[i - 1]) / 2)
|
||||
}
|
||||
if (i < this.nbPeaks - 1) {
|
||||
endIndex = peakIndex + Math.ceil((this.peakIndexes[i + 1] - peakIndex) / 2)
|
||||
}
|
||||
|
||||
// shift whole region of influence around peak to shifted peak
|
||||
let startOffset = startIndex - peakIndex
|
||||
let endOffset = endIndex - peakIndex
|
||||
for (let j = startOffset; j < endOffset; j++) {
|
||||
let binIndex = peakIndex + j
|
||||
let binIndexShifted = peakIndexShifted + j
|
||||
|
||||
if (binIndexShifted >= this.magnitudes.length) break
|
||||
|
||||
// apply phase correction
|
||||
let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize
|
||||
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor)
|
||||
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor)
|
||||
|
||||
let indexReal = binIndex * 2
|
||||
let indexImag = indexReal + 1
|
||||
let valueReal = this.freqComplexBuffer[indexReal]
|
||||
let valueImag = this.freqComplexBuffer[indexImag]
|
||||
|
||||
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag
|
||||
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal
|
||||
|
||||
let indexShiftedReal = binIndexShifted * 2
|
||||
let indexShiftedImag = indexShiftedReal + 1
|
||||
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal
|
||||
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor)
|
||||
|
||||
@@ -148,19 +148,22 @@ const saveMeta = (downloadInfo: LX.Download.ListItem) => {
|
||||
: Promise.resolve(null),
|
||||
]
|
||||
void Promise.all(tasks).then(([imgUrl, lyrics]) => {
|
||||
let lyric: null | string = null
|
||||
if (lyrics?.lyric) {
|
||||
lyric = fixKgLyric(lyrics.lyric)
|
||||
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lyric += '\n' + lyrics.tlyric + '\n'
|
||||
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lyric += '\n' + lyrics.rlyric + '\n'
|
||||
const lrcData = {
|
||||
lrc: '',
|
||||
tlrc: null as string | null,
|
||||
rlrc: null as string | null,
|
||||
}
|
||||
if (lyrics) {
|
||||
lrcData.lrc = lyrics.lyric
|
||||
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lrcData.tlrc = lyrics.tlyric
|
||||
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lrcData.rlrc = lyrics.rlyric
|
||||
}
|
||||
void window.lx.worker.download.writeMeta(downloadInfo.metadata.filePath, {
|
||||
title: downloadInfo.metadata.musicInfo.name,
|
||||
artist: downloadInfo.metadata.musicInfo.singer,
|
||||
album: downloadInfo.metadata.musicInfo.meta.albumName,
|
||||
APIC: imgUrl,
|
||||
lyrics: lyric,
|
||||
})
|
||||
}, lrcData)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,11 +180,13 @@ const downloadLyric = (downloadInfo: LX.Download.ListItem) => {
|
||||
}).then(lrcs => {
|
||||
if (lrcs.lyric) {
|
||||
lrcs.lyric = fixKgLyric(lrcs.lyric)
|
||||
let lyric = lrcs.lyric
|
||||
if (appSetting['download.isDownloadTLrc'] && lrcs.tlyric) lyric += '\n' + lrcs.tlyric + '\n'
|
||||
if (appSetting['download.isDownloadRLrc'] && lrcs.rlyric) lyric += '\n' + lrcs.rlyric + '\n'
|
||||
void window.lx.worker.download.saveLrc(downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'),
|
||||
lyric, appSetting['download.lrcFormat'])
|
||||
const lrcData = {
|
||||
lrc: lrcs.lyric,
|
||||
tlrc: appSetting['download.isDownloadTLrc'] && lrcs.tlyric ? lrcs.tlyric : null,
|
||||
rlrc: appSetting['download.isDownloadRLrc'] && lrcs.rlyric ? lrcs.rlyric : null,
|
||||
}
|
||||
void window.lx.worker.download.saveLrc(lrcData, downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'),
|
||||
appSetting['download.lrcFormat'])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { reactive, markRaw, shallowReactive } from '@common/utils/vueTools'
|
||||
import music from '@renderer/utils/musicSdk'
|
||||
|
||||
export declare type Source = LX.OnlineSource
|
||||
export type Source = LX.OnlineSource
|
||||
|
||||
export const sources: LX.OnlineSource[] = markRaw([])
|
||||
|
||||
@@ -10,12 +10,12 @@ for (const source of music.sources) {
|
||||
sources.push(source.id as LX.OnlineSource)
|
||||
}
|
||||
|
||||
export declare interface BoardItem {
|
||||
export interface BoardItem {
|
||||
id: string
|
||||
name: string
|
||||
bangid: string
|
||||
}
|
||||
export declare interface Board {
|
||||
export interface Board {
|
||||
list: BoardItem[]
|
||||
source: LX.OnlineSource
|
||||
}
|
||||
@@ -23,7 +23,7 @@ type Boards = Partial<Record<LX.OnlineSource, Board>>
|
||||
|
||||
export const boards = shallowReactive<Boards>({})
|
||||
|
||||
export declare interface ListDetailInfo {
|
||||
export interface ListDetailInfo {
|
||||
list: LX.Music.MusicInfoOnline[]
|
||||
total: number
|
||||
page: number
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, shallowReactive } from '@common/utils/vueTools'
|
||||
|
||||
export const searchText = ref('')
|
||||
|
||||
export declare type onlineSource = LX.OnlineSource
|
||||
export type onlineSource = LX.OnlineSource
|
||||
|
||||
|
||||
export const historyList = shallowReactive<string[]>([])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { reactive, markRaw, ref, shallowReactive } from '@common/utils/vueTools'
|
||||
import music from '@renderer/utils/musicSdk'
|
||||
|
||||
export declare interface SortInfo {
|
||||
export interface SortInfo {
|
||||
name: string
|
||||
id: string
|
||||
}
|
||||
@@ -16,18 +16,18 @@ for (const source of music.sources) {
|
||||
sortList[source.id as LX.OnlineSource] = songList.sortList as SortInfo[]
|
||||
}
|
||||
|
||||
export declare interface TagInfoItem<T extends LX.OnlineSource = LX.OnlineSource> {
|
||||
export interface TagInfoItem<T extends LX.OnlineSource = LX.OnlineSource> {
|
||||
parent_id: string
|
||||
parent_name: string
|
||||
id: string
|
||||
name: string
|
||||
source: T
|
||||
}
|
||||
export declare interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {
|
||||
export interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {
|
||||
name: string
|
||||
list: Array<TagInfoItem<T>>
|
||||
}
|
||||
export declare interface TagInfo<Source extends LX.OnlineSource = LX.OnlineSource> {
|
||||
export interface TagInfo<Source extends LX.OnlineSource = LX.OnlineSource> {
|
||||
tags: Array<TagInfoTypeItem<Source>>
|
||||
hotTag: Array<TagInfoItem<Source>>
|
||||
source: Source
|
||||
@@ -38,7 +38,7 @@ type Tags = Partial<Record<LX.OnlineSource, TagInfo>>
|
||||
export const tags = shallowReactive<Tags>({})
|
||||
|
||||
|
||||
export declare interface ListInfoItem {
|
||||
export interface ListInfoItem {
|
||||
play_count: string
|
||||
id: string
|
||||
author: string
|
||||
@@ -50,7 +50,7 @@ export declare interface ListInfoItem {
|
||||
source: LX.OnlineSource
|
||||
total?: string
|
||||
}
|
||||
export declare interface ListInfo {
|
||||
export interface ListInfo {
|
||||
list: ListInfoItem[]
|
||||
total: number
|
||||
page: number
|
||||
@@ -62,7 +62,7 @@ export declare interface ListInfo {
|
||||
sortId: string
|
||||
}
|
||||
|
||||
export declare interface ListDetailInfo {
|
||||
export interface ListDetailInfo {
|
||||
list: LX.Music.MusicInfoOnline[]
|
||||
source: LX.OnlineSource
|
||||
desc: string | null
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { reactive, toRaw } from '@common/utils/vueTools'
|
||||
import { getUserSoundEffectConvolutionPresetList, getUserSoundEffectEQPresetList, saveUserSoundEffectConvolutionPresetList, saveUserSoundEffectEQPresetList } from '@renderer/utils/ipc'
|
||||
import {
|
||||
getUserSoundEffectConvolutionPresetList,
|
||||
getUserSoundEffectEQPresetList,
|
||||
// getUserSoundEffectPitchShifterPresetList,
|
||||
saveUserSoundEffectConvolutionPresetList,
|
||||
saveUserSoundEffectEQPresetList,
|
||||
// saveUserSoundEffectPitchShifterPresetList,
|
||||
} from '@renderer/utils/ipc'
|
||||
|
||||
let userEqPresetList: LX.SoundEffect.EQPreset[] | null = null
|
||||
|
||||
@@ -54,3 +61,30 @@ export const removeUserConvolutionPreset = async(id: string) => {
|
||||
userConvolutionPresetList.splice(index, 1)
|
||||
saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))
|
||||
}
|
||||
|
||||
|
||||
// let userPitchShifterPresetList: LX.SoundEffect.PitchShifterPreset[] | null = null
|
||||
// export const getUserPitchShifterPresetList = async() => {
|
||||
// if (userEqPresetList == null) {
|
||||
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
|
||||
// }
|
||||
// return userPitchShifterPresetList
|
||||
// }
|
||||
// export const saveUserPitchShifterPreset = async(preset: LX.SoundEffect.PitchShifterPreset) => {
|
||||
// if (userPitchShifterPresetList == null) {
|
||||
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
|
||||
// }
|
||||
// const target = userPitchShifterPresetList.find(p => p.id == preset.id)
|
||||
// if (target) Object.assign(target, preset)
|
||||
// else userPitchShifterPresetList.push(preset)
|
||||
// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))
|
||||
// }
|
||||
// export const removeUserPitchShifterPreset = async(id: string) => {
|
||||
// if (userPitchShifterPresetList == null) {
|
||||
// userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())
|
||||
// }
|
||||
// const index = userPitchShifterPresetList.findIndex(p => p.id == id)
|
||||
// if (index < 0) return
|
||||
// userPitchShifterPresetList.splice(index, 1)
|
||||
// saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))
|
||||
// }
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"isolatedModules": true,
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@common/*": ["common/*"],
|
||||
"@renderer/*": ["renderer/*"],
|
||||
@@ -12,14 +10,14 @@
|
||||
"@/*": ["./*"],
|
||||
},
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"./types"
|
||||
"renderer/types"
|
||||
],
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@volar/vue-language-plugin-pug"
|
||||
]
|
||||
}
|
||||
// "vueCompilerOptions": {
|
||||
// "plugins": [
|
||||
// "@volar/vue-language-plugin-pug"
|
||||
// ]
|
||||
// }
|
||||
// "include": [
|
||||
// "./**/*.ts",
|
||||
// // "./**/*.js",
|
||||
|
||||
@@ -307,6 +307,13 @@ export const saveUserSoundEffectConvolutionPresetList = (list: LX.SoundEffect.Co
|
||||
rendererSend<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, list)
|
||||
}
|
||||
|
||||
// export const getUserSoundEffectPitchShifterPresetList = async() => {
|
||||
// return await rendererInvoke<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset)
|
||||
// }
|
||||
|
||||
// export const saveUserSoundEffectPitchShifterPresetList = (list: LX.SoundEffect.PitchShifterPreset[]) => {
|
||||
// rendererSend<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, list)
|
||||
// }
|
||||
|
||||
export const allHotKeys = markRaw({
|
||||
local: [
|
||||
|
||||
@@ -2,11 +2,11 @@ import { isMac } from '@common/utils'
|
||||
|
||||
const downKeys = new Set<string>()
|
||||
|
||||
export declare type KeyActionType = LX.KeyDownEevent['type']
|
||||
export declare type Keys = LX.KeyDownEevent['keys']
|
||||
export declare type Key = LX.KeyDownEevent['key']
|
||||
export declare type EventKey = LX.KeyDownEevent['eventKey']
|
||||
export declare type Event = LX.KeyDownEevent['event']
|
||||
export type KeyActionType = LX.KeyDownEevent['type']
|
||||
export type Keys = LX.KeyDownEevent['keys']
|
||||
export type Key = LX.KeyDownEevent['key']
|
||||
export type EventKey = LX.KeyDownEevent['eventKey']
|
||||
export type Event = LX.KeyDownEevent['event']
|
||||
|
||||
const handleEvent = (type: KeyActionType, event: LX.KeyEvent, keys: Keys, isEditing: boolean) => {
|
||||
let eventKey = event.key
|
||||
|
||||
@@ -12,10 +12,10 @@ const api_test = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(requestMsg.fail))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
filterList(rawData) {
|
||||
return rawData.map(item => ({
|
||||
play_count: this.formatPlayCount(item.listen_num),
|
||||
id: item.list_id,
|
||||
id: String(item.list_id),
|
||||
author: item.username,
|
||||
name: item.title,
|
||||
// time: item.publish_time,
|
||||
|
||||
@@ -12,10 +12,10 @@ const api_test = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(requestMsg.fail))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
|
||||
let timestamp = Date.now()
|
||||
const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=0&p=${page}&pagesize=${limit}&uuid=0&ver=10`
|
||||
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/v1/cmtlist?${params}&signature=${signatureParams(params)}`, {
|
||||
const _requestObj = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/newest?${params}&signature=${signatureParams(params)}`, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24',
|
||||
},
|
||||
|
||||
85
src/renderer/utils/musicSdk/kg/singer.js
Normal file
85
src/renderer/utils/musicSdk/kg/singer.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { getMusicInfosByList } from './musicInfo'
|
||||
import { createHttpFetch } from './util'
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取歌手信息
|
||||
* @param {*} id
|
||||
*/
|
||||
getInfo(id) {
|
||||
if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0
|
||||
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => {
|
||||
if (!body) throw new Error('get singer info faild.')
|
||||
|
||||
return {
|
||||
source: 'kg',
|
||||
id: body.singerid,
|
||||
info: {
|
||||
name: body.singername,
|
||||
desc: body.intro,
|
||||
avatar: body.imgurl.replace('{size}', 480),
|
||||
gender: body.grade === 1 ? 'man' : 'woman',
|
||||
},
|
||||
count: {
|
||||
music: body.songcount,
|
||||
album: body.albumcount,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取歌手专辑列表
|
||||
* @param {*} id
|
||||
* @param {*} page
|
||||
* @param {*} limit
|
||||
*/
|
||||
getAlbumList(id, page = 1, limit = 10) {
|
||||
if (id == 0) throw new Error('歌手不存在')
|
||||
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => {
|
||||
if (!body.info) throw new Error('get singer album list faild.')
|
||||
|
||||
const list = this.filterAlbumList(body.info)
|
||||
return {
|
||||
source: 'kg',
|
||||
list,
|
||||
limit,
|
||||
page,
|
||||
total: body.total,
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取歌手歌曲列表
|
||||
* @param {*} id
|
||||
* @param {*} page
|
||||
* @param {*} limit
|
||||
*/
|
||||
async getSongList(id, page = 1, limit = 100) {
|
||||
if (id == 0) throw new Error('歌手不存在')
|
||||
const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`)
|
||||
if (!body.info) throw new Error('get singer song list faild.')
|
||||
|
||||
const list = await getMusicInfosByList(body.info)
|
||||
return {
|
||||
source: 'kg',
|
||||
list,
|
||||
limit,
|
||||
page,
|
||||
total: body.total,
|
||||
}
|
||||
},
|
||||
filterAlbumList(raw) {
|
||||
return raw.map(item => {
|
||||
return {
|
||||
id: item.albumid,
|
||||
count: item.songcount,
|
||||
info: {
|
||||
name: item.albumname,
|
||||
author: item.singername,
|
||||
img: item.replaceAll('{size}', '480'),
|
||||
desc: item.intro,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -12,10 +12,10 @@ const api_temp = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(body.msg))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,10 +23,10 @@ const api_test = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(requestMsg.fail))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -467,7 +467,7 @@ export default {
|
||||
list: body.abslist.map(item => {
|
||||
return {
|
||||
play_count: this.formatPlayCount(item.playcnt),
|
||||
id: item.playlistid,
|
||||
id: String(item.playlistid),
|
||||
author: decodeName(item.nickname),
|
||||
name: decodeName(item.name),
|
||||
total: item.songnum,
|
||||
|
||||
@@ -12,10 +12,10 @@ const api_test = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(requestMsg.fail))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
|
||||
import { sizeFormate, formatPlayTime } from '../../index'
|
||||
import { toMD5, formatSingerName } from '../utils'
|
||||
|
||||
const sign = (time, str) => {
|
||||
export const createSignature = (time, str) => {
|
||||
const deviceId = '963B7AA0D21511ED807EE5846EC87D20'
|
||||
const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73'
|
||||
const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`)
|
||||
@@ -111,7 +111,7 @@ export default {
|
||||
|
||||
musicSearch(str, page, limit) {
|
||||
const time = Date.now().toString()
|
||||
const signData = sign(time, str)
|
||||
const signData = createSignature(time, str)
|
||||
const searchRequest = httpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
|
||||
headers: {
|
||||
uiVersion: 'A_music_3.6.1',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,10 +12,10 @@ const api_messoer = {
|
||||
lookup: dnsLookup,
|
||||
family: 4,
|
||||
})
|
||||
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
|
||||
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
switch (body.code) {
|
||||
case 0: return Promise.resolve({ type, url: body.data })
|
||||
case 429: return Promise.reject(new Error(requestMsg.tooManyRequests))
|
||||
default: return Promise.reject(new Error(requestMsg.fail))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -241,7 +241,7 @@ export default {
|
||||
text: item.Content ? this.replaceEmoji(item.Content).replace(/\\n/g, '\n') : '',
|
||||
time: item.PubTime ? this.formatTime(item.PubTime) : null,
|
||||
timeStr: item.PubTime ? dateFormat2(this.formatTime(item.PubTime)) : null,
|
||||
userName: item.Nick ? item.Nick.substring(1) : '',
|
||||
userName: item.Nick ?? '',
|
||||
images: item.Pic ? [item.Pic] : [],
|
||||
avatar: item.Avatar,
|
||||
location: item.Location ? item.Location : '',
|
||||
@@ -254,7 +254,7 @@ export default {
|
||||
text: this.replaceEmoji(c.Content).replace(/\\n/g, '\n'),
|
||||
time: c.PubTime ? this.formatTime(c.PubTime) : null,
|
||||
timeStr: c.PubTime ? dateFormat2(this.formatTime(c.PubTime)) : null,
|
||||
userName: c.Nick ? c.Nick.substring(1) : '',
|
||||
userName: c.Nick ?? '',
|
||||
avatar: c.Avatar,
|
||||
images: c.Pic ? [c.Pic] : [],
|
||||
userId: c.EncryptUin,
|
||||
|
||||
@@ -100,8 +100,10 @@ const parseTools = {
|
||||
return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '')
|
||||
},
|
||||
getIntv(interval) {
|
||||
let [m, s, ms] = interval.split(/:|\./)
|
||||
|
||||
if (!interval.includes('.')) interval += '.0'
|
||||
let arr = interval.split(/:|\./)
|
||||
while (arr.length < 3) arr.unshift('0')
|
||||
const [m, s, ms] = arr
|
||||
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
|
||||
},
|
||||
fixRlrcTimeTag(rlrc, lrc) {
|
||||
@@ -123,7 +125,7 @@ const parseTools = {
|
||||
const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
|
||||
if (!lrcLineResult) continue
|
||||
const t2 = this.getIntv(lrcLineResult[1])
|
||||
if (Math.abs(t1 - t2) < 10) {
|
||||
if (Math.abs(t1 - t2) < 100) {
|
||||
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
|
||||
break
|
||||
}
|
||||
@@ -140,21 +142,25 @@ const parseTools = {
|
||||
const tlrcLines = tlrc.split('\n')
|
||||
let lrcLines = lrc.split('\n')
|
||||
// let temp = []
|
||||
const timeTagRxp = /^\[[\d:.]+\]/
|
||||
let newLrc = []
|
||||
tlrcLines.forEach((line) => {
|
||||
const result = timeTagRxp.exec(line)
|
||||
const result = this.rxps.lineTime2.exec(line)
|
||||
if (!result) return
|
||||
const words = line.replace(timeTagRxp, '')
|
||||
const words = line.replace(this.rxps.lineTime2, '')
|
||||
if (!words.trim()) return
|
||||
const tag = result[0].replace(/\d]/, '').replace(this.rxps.timeLabelFixRxp, '')
|
||||
let time = result[1]
|
||||
if (time.includes('.')) {
|
||||
time += ''.padStart(3 - time.split('.')[1].length, '0')
|
||||
}
|
||||
const t1 = this.getIntv(time)
|
||||
|
||||
while (lrcLines.length) {
|
||||
const lrcLine = lrcLines.shift()
|
||||
const lrcLineResult = timeTagRxp.exec(lrcLine)
|
||||
const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
|
||||
if (!lrcLineResult) continue
|
||||
if (lrcLineResult[0].includes(tag)) {
|
||||
newLrc.push(line.replace(timeTagRxp, lrcLineResult[0]))
|
||||
const t2 = this.getIntv(lrcLineResult[1])
|
||||
if (Math.abs(t1 - t2) < 100) {
|
||||
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
|
||||
break
|
||||
}
|
||||
// temp.push(line)
|
||||
|
||||
249
src/renderer/utils/musicSdk/tx/singer.js
Normal file
249
src/renderer/utils/musicSdk/tx/singer.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { httpFetch } from '../../request'
|
||||
|
||||
import { formatPlayTime, sizeFormate } from '../../index'
|
||||
import { formatSingerName } from '../utils'
|
||||
|
||||
export const filterMusicInfoItem = item => {
|
||||
const types = []
|
||||
const _types = {}
|
||||
if (item.file.size_128mp3 != 0) {
|
||||
let size = sizeFormate(item.file.size_128mp3)
|
||||
types.push({ type: '128k', size })
|
||||
_types['128k'] = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (item.file.size_320mp3 !== 0) {
|
||||
let size = sizeFormate(item.file.size_320mp3)
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (item.file.size_flac !== 0) {
|
||||
let size = sizeFormate(item.file.size_flac)
|
||||
types.push({ type: 'flac', size })
|
||||
_types.flac = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
if (item.file.size_hires !== 0) {
|
||||
let size = sizeFormate(item.file.size_hires)
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
const albumId = item.album.id ?? ''
|
||||
const albumMid = item.album.mid ?? ''
|
||||
const albumName = item.album.name ?? ''
|
||||
return {
|
||||
source: 'tx',
|
||||
singer: formatSingerName(item.singer, 'name'),
|
||||
name: item.name,
|
||||
albumName,
|
||||
albumId,
|
||||
albumMid,
|
||||
interval: formatPlayTime(item.interval),
|
||||
songId: item.id,
|
||||
songmid: item.mid,
|
||||
strMediaMid: item.file.media_mid,
|
||||
img: (albumId === '' || albumId === '空')
|
||||
? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''
|
||||
: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumMid}.jpg`,
|
||||
types,
|
||||
_types,
|
||||
typeUrl: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建一个适用于TX的Http请求
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
* @param {*} retryNum
|
||||
*/
|
||||
const createMusicuFetch = async(data, options, retryNum = 0) => {
|
||||
if (retryNum > 2) throw new Error('try max num')
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
comm: {
|
||||
cv: 4747474,
|
||||
ct: 24,
|
||||
format: 'json',
|
||||
inCharset: 'utf-8',
|
||||
outCharset: 'utf-8',
|
||||
uin: 0,
|
||||
},
|
||||
...data,
|
||||
},
|
||||
headers: {
|
||||
'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
},
|
||||
}).promise
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return createMusicuFetch(data, options, ++retryNum)
|
||||
}
|
||||
if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum)
|
||||
|
||||
return result.body
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取歌手信息
|
||||
* @param {*} id
|
||||
*/
|
||||
getInfo(id) {
|
||||
return createMusicuFetch({
|
||||
req_1: {
|
||||
module: 'music.musichallSinger.SingerInfoInter',
|
||||
method: 'GetSingerDetail',
|
||||
param: {
|
||||
singer_mid: [id],
|
||||
ex_singer: 1,
|
||||
wiki_singer: 1,
|
||||
group_singer: 0,
|
||||
pic: 1,
|
||||
photos: 0,
|
||||
},
|
||||
},
|
||||
req_2: {
|
||||
module: 'music.musichallAlbum.AlbumListServer',
|
||||
method: 'GetAlbumList',
|
||||
param: {
|
||||
singerMid: id,
|
||||
order: 0,
|
||||
begin: 0,
|
||||
num: 1,
|
||||
songNumTag: 0,
|
||||
singerID: 0,
|
||||
},
|
||||
},
|
||||
req_3: {
|
||||
module: 'musichall.song_list_server',
|
||||
method: 'GetSingerSongList',
|
||||
param: {
|
||||
singerMid: id,
|
||||
order: 1,
|
||||
begin: 0,
|
||||
num: 1,
|
||||
},
|
||||
},
|
||||
}).then(body => {
|
||||
if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.')
|
||||
|
||||
const info = body.req_1.data.singer_list[0]
|
||||
const music = body.req_3.data
|
||||
const album = body.req_3.data
|
||||
return {
|
||||
source: 'tx',
|
||||
id: info.basic_info.singer_mid,
|
||||
info: {
|
||||
name: info.basic_info.name,
|
||||
desc: info.ex_info.desc,
|
||||
avatar: info.pic.pic,
|
||||
gender: info.ex_info.genre === 1 ? 'man' : 'woman',
|
||||
},
|
||||
count: {
|
||||
music: music.totalNum,
|
||||
album: album.total,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取歌手专辑列表
|
||||
* @param {*} id
|
||||
* @param {*} page
|
||||
* @param {*} limit
|
||||
*/
|
||||
getAlbumList(id, page = 1, limit = 10) {
|
||||
if (page === 1) page = 0
|
||||
return createMusicuFetch({
|
||||
req: {
|
||||
module: 'music.musichallAlbum.AlbumListServer',
|
||||
method: 'GetAlbumList',
|
||||
param: {
|
||||
singerMid: id,
|
||||
order: 0,
|
||||
begin: page * limit,
|
||||
num: limit,
|
||||
songNumTag: 0,
|
||||
singerID: 0,
|
||||
},
|
||||
},
|
||||
}).then(body => {
|
||||
if (body.req.code != 0) throw new Error('get singer album faild.')
|
||||
|
||||
const list = this.filterAlbumList(body.req.data.albumList)
|
||||
return {
|
||||
source: 'tx',
|
||||
list,
|
||||
limit,
|
||||
page,
|
||||
total: body.req.data.total,
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取歌手歌曲列表
|
||||
* @param {*} id
|
||||
* @param {*} page
|
||||
* @param {*} limit
|
||||
*/
|
||||
async getSongList(id, page = 1, limit = 100) {
|
||||
if (page === 1) page = 0
|
||||
return createMusicuFetch({
|
||||
req: {
|
||||
module: 'musichall.song_list_server',
|
||||
method: 'GetSingerSongList',
|
||||
param: {
|
||||
singerMid: id,
|
||||
order: 1,
|
||||
begin: page * limit,
|
||||
num: limit,
|
||||
},
|
||||
},
|
||||
}).then(body => {
|
||||
if (body.req.code != 0) throw new Error('get singer song list faild.')
|
||||
|
||||
const list = this.filterSongList(body.req.data.songList)
|
||||
return {
|
||||
source: 'tx',
|
||||
list,
|
||||
limit,
|
||||
page,
|
||||
total: body.req.data.totalNum,
|
||||
}
|
||||
})
|
||||
},
|
||||
filterAlbumList(raw) {
|
||||
return raw.map(item => {
|
||||
return {
|
||||
id: item.albumID,
|
||||
mid: item.albumMid,
|
||||
count: item.totalNum,
|
||||
info: {
|
||||
name: item.albumName,
|
||||
author: item.singerName,
|
||||
img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.albumMid}.jpg`,
|
||||
desc: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
filterSongList(raw) {
|
||||
raw.map(item => {
|
||||
return filterMusicInfoItem(item.songInfo)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export default {
|
||||
return {
|
||||
list: data.v_playlist.map(item => ({
|
||||
play_count: formatPlayCount(item.access_num),
|
||||
id: item.tid,
|
||||
id: String(item.tid),
|
||||
author: item.creator_info.nick,
|
||||
name: item.title,
|
||||
time: item.modify_time ? dateFormat(item.modify_time * 1000, 'Y-M-D') : '',
|
||||
@@ -151,7 +151,7 @@ export default {
|
||||
return {
|
||||
list: content.v_item.map(({ basic }) => ({
|
||||
play_count: formatPlayCount(basic.play_cnt),
|
||||
id: basic.tid,
|
||||
id: String(basic.tid),
|
||||
author: basic.creator.nick,
|
||||
name: basic.title,
|
||||
// time: basic.publish_time,
|
||||
@@ -304,7 +304,7 @@ export default {
|
||||
list: body.data.list.map(item => {
|
||||
return {
|
||||
play_count: formatPlayCount(item.listennum),
|
||||
id: item.dissid,
|
||||
id: String(item.dissid),
|
||||
author: item.creator.name,
|
||||
name: item.dissname,
|
||||
time: dateFormat(item.createtime, 'Y-M-D'),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user