摘要内容……

阅读全文 »
l

一个函数秒杀 2Sum 3Sum 4Sum 问题

经常刷 LeetCode 的读者肯定知道鼎鼎有名的 twoSum 问题,我们的旧文 Two Sum 问题的核心思想twoSum 的几个变种做了解析。

但是除了 twoSum 问题,LeetCode 上面还有 3Sum4Sum 问题,我估计以后出个 5Sum6Sum 也不是不可能。

那么,对于这种问题有没有什么好办法用套路解决呢?本文就由浅入深,层层推进,用一个函数来解决所有 nSum 类型的问题。

一、twoSum 问题

[leetcode1](1. 两数之和 - 力扣(LeetCode) (leetcode-cn.com))

力扣上的 twoSum 问题,题目要求返回的是索引,这里我来编一道 twoSum 题目,不要返回索引,返回元素的值:

如果假设输入一个数组 nums 和一个目标和 target请你返回 nums 中能够凑出 target 的两个元素的值,比如输入 nums = [5,3,1,6], target = 9,那么算法返回两个元素 [3,6]。可以假设只有且仅有一对儿元素可以凑出 target

我们可以先对 nums 排序,然后利用前文「双指针技巧汇总」写过的左右双指针技巧,从两端相向而行就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vector<int> twoSum(vector<int>& nums, int target) {
// 先对数组排序
sort(nums.begin(), nums.end());
// 左右指针
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if (sum < target) {
lo++;
} else if (sum > target) {
hi--;
} else if (sum == target) {
return {nums[lo], nums[hi]};
}
}
return {};
}

这样就可以解决这个问题,不过我们要继续魔改题目,把这个题目变得更泛化,更困难一点:

nums 中可能有多对儿元素之和都等于 target,请你的算法返回所有和为 target 的元素对儿,其中不能出现重复

函数签名如下:

1
vector<vector<int>> twoSumTarget(vector<int>& nums, int target);

比如说输入为 nums = [1,3,1,2,2,3], target = 4,那么算法返回的结果就是:[[1,3],[2,2]]

对于修改后的问题,关键难点是现在可能有多个和为 target 的数对儿,还不能重复,比如上述例子中 [1,3][3,1] 就算重复,只能算一次。

首先,基本思路肯定还是排序加双指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vector<vector<int>> twoSumTarget(vector<int>& nums, int target {
// 先对数组排序
sort(nums.begin(), nums.end());
vector<vector<int>> res;
int lo = 0, hi = nums.size() - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 根据 sum 和 target 的比较,移动左右指针
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({lo, hi});
lo++; hi--;
}
}
return res;
}

但是,这样实现会造成重复的结果,比如说 nums = [1,1,1,2,2,3,3], target = 4,得到的结果中 [1,3] 肯定会重复。

出问题的地方在于 sum == target 条件的 if 分支,当给 res 加入一次结果后,lohi 不应该改变 1 的同时,还应该跳过所有重复的元素:

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
while (lo < hi) {
int sum = nums[lo] + nums[hi];
// 记录索引 lo 和 hi 最初对应的值
int left = nums[lo], right = nums[hi];
if (sum < target) lo++;
else if (sum > target) hi--;
else {
res.push_back({left, right});
// 跳过所有重复的元素
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}

这样就可以保证一个答案只被添加一次,重复的结果都会被跳过,可以得到正确的答案。不过,受这个思路的启发,其实前两个 if 分支也是可以做一点效率优化,跳过相同的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vector<vector<int>> twoSumTarget(vector<int>& nums, int target) {
// nums 数组必须有序
sort(nums.begin(), nums.end());
int lo = 0, hi = nums.size() - 1;
vector<vector<int>> res;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
return res;
}

这样,一个通用化的 twoSum 函数就写出来了,请确保你理解了该算法的逻辑,我们后面解决 3Sum4Sum 的时候会复用这个函数。

这个函数的时间复杂度非常容易看出来,双指针操作的部分虽然有那么多 while 循环,但是时间复杂度还是 O(N),而排序的时间复杂度是 O(NlogN),所以这个函数的时间复杂度是 O(NlogN)

二、3Sum 问题

Leetcode15

这是力扣第 15 题「三数之和」:

图片

题目就是让我们找 nums 中和为 0 的三个元素,返回所有可能的三元组(triple),函数签名如下:

1
vector<vector<int>> threeSum(vector<int>& nums);

这样,我们再泛化一下题目,不要光和为 0 的三元组了,计算和为 target 的三元组吧,同上面的 twoSum 一样,也不允许重复的结果:

1
2
3
4
5
6
7
8
vector<vector<int>> threeSum(vector<int>& nums) {
// 求和为 0 的三元组
return threeSumTarget(nums, 0);
}

vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
// 输入数组 nums,返回所有和为 target 的三元组
}

这个问题怎么解决呢?很简单,穷举呗。现在我们想找和为 target 的三个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!

那么,确定了第一个数字之后,剩下的两个数字可以是什么呢?其实就是和为 target - nums[i] 的两个数字呗,那不就是 twoSum 函数解决的问题么🤔

可以直接写代码了,需要把 twoSum 函数稍作修改即可复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 从 nums[start] 开始,计算有序数组
* nums 中所有和为 target 的二元组 */
vector<vector<int>> twoSumTarget(
vector<int>& nums, int start, int target) {
// 左指针改为从 start 开始,其他不变
int lo = start, hi = nums.size() - 1;
vector<vector<int>> res;
while (lo < hi) {
...
}
return res;
}

/* 计算数组 nums 中所有和为 target 的三元组 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
// 数组得排个序
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> res;
// 穷举 threeSum 的第一个数
for (int i = 0; i < n; i++) {
// 对 target - nums[i] 计算 twoSum
vector<vector<int>>
tuples = twoSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的二元组,再加上 nums[i] 就是结果三元组
for (vector<int>& tuple : tuples) {
tuple.push_back(nums[i]);
res.push_back(tuple);
}
// 跳过第一个数字重复的情况,否则会出现重复结果
while (i < n - 1 && nums[i] == nums[i + 1]) i++;
}
return res;
}

需要注意的是,类似 twoSum3Sum 的结果也可能重复,比如输入是 nums = [1,1,1,2,3], target = 6,结果就会重复。

关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。

至此,3Sum 问题就解决了,时间复杂度不难算,排序的复杂度为 O(NlogN)twoSumTarget 函数中的双指针操作为 O(N)threeSumTarget 函数在 for 循环中调用 twoSumTarget 所以总的时间复杂度就是 O(NlogN + N^2) = O(N^2)

三、4Sum 问题

Leetcode18

这是力扣第 18 题「四数之和」:

图片

函数签名如下:

1
vector<vector<int>> fourSum(vector<int>& nums, int target);

都到这份上了,4Sum 完全就可以用相同的思路:穷举第一个数字,然后调用 3Sum 函数计算剩下三个数,最后组合出和为 target 的四元组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
vector<vector<int>> fourSum(vector<int>& nums, int target) {
// 数组需要排序
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> res;
// 穷举 fourSum 的第一个数
for (int i = 0; i < n; i++) {
// 对 target - nums[i] 计算 threeSum
vector<vector<int>>
triples = threeSumTarget(nums, i + 1, target - nums[i]);
// 如果存在满足条件的三元组,再加上 nums[i] 就是结果四元组
for (vector<int>& triple : triples) {
triple.push_back(nums[i]);
res.push_back(triple);
}
// fourSum 的第一个数不能重复
while (i < n - 1 && nums[i] == nums[i + 1]) i++;
}
return res;
}

/* 从 nums[start] 开始,计算有序数组
* nums 中所有和为 target 的三元组 */
vector<vector<int>>
threeSumTarget(vector<int>& nums, int start, int target) {
int n = nums.size();
vector<vector<int>> res;
// i 从 start 开始穷举,其他都不变
for (int i = start; i < n; i++) {
...
}
return res;

这样,按照相同的套路,4Sum 问题就解决了,时间复杂度的分析和之前类似,for 循环中调用了 threeSumTarget 函数,所以总的时间复杂度就是 O(N^3)

四、100Sum 问题?

在 LeetCode 上,4Sum 就到头了,但是回想刚才写 3Sum4Sum 的过程,实际上是遵循相同的模式的。我相信你只要稍微修改一下 4Sum 的函数就可以复用并解决 5Sum 问题,然后解决 6Sum 问题……

那么,如果我让你求 100Sum 问题,怎么办呢?其实我们可以观察上面这些解法,统一出一个 nSum 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* 注意:调用这个函数之前一定要先给 nums 排序 */
vector<vector<int>> nSumTarget(
vector<int>& nums, int n, int start, int target) {

int sz = nums.size();
vector<vector<int>> res;
// 至少是 2Sum,且数组大小不应该小于 n
if (n < 2 || sz < n) return res;
// 2Sum 是 base case
if (n == 2) {
// 双指针那一套操作
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (int i = start; i < sz; i++) {
vector<vector<int>>
sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (vector<int>& arr : sub) {
// (n-1)Sum 加上 nums[i] 就是 nSum
arr.push_back(nums[i]);
res.push_back(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}

嗯,看起来很长,实际上就是把之前的题目解法合并起来了,n == 2 时是 twoSum 的双指针解法,n > 2 时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案。

需要注意的是,调用这个 nSum 函数之前一定要先给 nums 数组排序,因为 nSum 是一个递归函数,如果在 nSum 函数里调用排序函数,那么每次递归都会进行没有必要的排序,效率会非常低。

比如说现在我们写 LeetCode 上的 4Sum 问题:

1
2
3
4
5
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
// n 为 4,从 nums[0] 开始计算和为 target 的四元组
return nSumTarget(nums, 4, 0, target);
}

再比如 LeetCode 的 3Sum 问题,找 target == 0 的三元组:

1
2
3
4
5
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
// n 为 3,从 nums[0] 开始计算和为 0 的三元组
return nSumTarget(nums, 3, 0, 0);
}

那么,如果让你计算 100Sum 问题,直接调用这个函数就完事儿了。

l

vim快捷键

行跳转快捷键

  • 文件行跳转
    • gg: 跳转到文件第一行。
    • G(Shift+g) : 跳转到文件最后一行。
    • nG(如10G) : 跳转到第n行。
    • :n(如:10+ 回车) : 跳转到第𝑛行。
    • H/ M/L : 跳转到当前屏幕的最高行(High)、中间行(Middle)、最低行(Low)。
  • 行内跳转
    • 0(数字零) : 跳转到当前行行首。
    • ^(Shift+6) : 跳转到当前行第一个非空字符。
    • $(Shift+4) : 跳转到当前行行尾。
  • 其他
    • Ctrl+o: 返回到跳转前的位置。
    • %: 跳转到与之匹配的括号行
l

Navidrome、Audiobookshelf 客户端推荐【自测】

https://makifx.com/boutique-software)

本人自建 音乐服(音海拾贝)| 有声书服(悦耳声阅)

群组链接:https://t.me/yinhai_chat

通知频道:https://t.me/yinhai_notify

【悦耳声阅】有声书分享频道:https://t.me/YueErFM

# Navidrome(音乐)

## IOS

DS Cloud
https://apps.apple.com/app/id6476057278

维克音乐

https://apps.apple.com/us/app/offline-music-player-wake/id6544783918?platform=iphone

LMP

https://apps.apple.com/us/app/lmp-a-better-music-player/id6451009326

音流
https://github.com/gitbobobo/StreamMusic

箭头音乐
https://cn.amcfy.com

Cinetry

https://github.com/gstory0404/Cinetry

LMP 音乐{TF}

https://testflight.apple.com/join/H0qG853j

## mac

DS Cloud
https://apps.apple.com/app/id6476057278

维克音乐

https://apps.apple.com/us/app/offline-music-player-wake/id6544783918?platform=iphone

FeiShin

https://github.com/jeffvli/feishin

Pure Music (棉花音乐)

https://github.com/pure-music/PureMusic/releases

Cinetry

https://github.com/gstory0404/Cinetry

## APTV

DS Cloud
https://apps.apple.com/app/id6476057278

维克音乐

https://apps.apple.com/us/app/offline-music-player-wake/id6544783918?platform=iphone

LMP

https://apps.apple.com/us/app/lmp-a-better-music-player/id6451009326

## Windows

FeiShin

https://github.com/jeffvli/feishin

音流
https://github.com/gitbobobo/StreamMusic

Pure Music (棉花音乐)

https://github.com/pure-music/PureMusic/releases

Cinetry

https://github.com/gstory0404/Cinetry

## Android

DS One(DS Cloud)

https://play.google.com/store/apps/details?id=com.cz.player.dsone

音流
https://github.com/gitbobobo/StreamMusic

tempus

https://github.com/eddyizm/tempus

tempo

https://github.com/CappielloAntonio/tempo/releases

箭头音乐
https://cn.amcfy.com

Cinetry

https://github.com/gstory0404/Cinetry

Symfonium

https://symfonium.app/

Pure Music (棉花音乐)

https://github.com/pure-music/PureMusic/releases

Musicfree(需配合插件使用)
https://github.com/CTZZG/MusicFree/releases 插件地址:

1
2
3
4
5
https://gitee.com/Rrance/WP/raw/master/音海拾贝.js




## Android TV

音流
https://github.com/gitbobobo/StreamMusic

Symfonium

https://symfonium.app/

Pure Music (棉花音乐)

https://github.com/pure-music/PureMusic/releases

## Linux

FeiShin

https://github.com/jeffvli/feishin

Pure Music (棉花音乐)

https://github.com/pure-music/PureMusic/releases

Cinetry

https://github.com/gstory0404/Cinetry

# Audiobookshelf(有声书)

##IOS 端

Audiobookshelf:
https://testflight.apple.com/join/wiic7QIW

Tonspur:

https://testflight.apple.com/join/E92V6bRM

Prologue:

https://testflight.apple.com/join/zTWS6ahB

Cinetry

https://github.com/gstory0404/Cinetry

希声:
https://apps.apple.com/cn/app/%E5%B8%8C%E5%A3%B0-%E4%B8%BA-audiobookshelf-%E6%89%93%E9%80%A0/id6754208326

AudioBooth:

https://testflight.apple.com/join/cAG6dVeN

dscloud:
https://apps.apple.com/app/id6476057278

SoundLeaf:
https://github.com/SoundLeaf/SoundLeafApp

ShelfPlayer:
https://apps.apple.com/us/app/shelfplayer/id6475221163

## 安卓

audiobookshelf:
https://play.google.com/store/apps/details?id=com.audiobookshelf.app

DS One(DS Cloud)

https://play.google.com/store/apps/details?id=com.cz.player.dsone

Cinetry

https://github.com/gstory0404/Cinetry

Lissen:
https://github.com/GrakovNe/lissen-android

LitLyric:
https://github.com/shane9b3/LitLyric---Beta

Kitzi:
https://github.com/bennybar/kitzi_abs_player

Buchable:
https://github.com/Vito0912/abs_flutter

## Windows

musicfree(需配合插件使用):

https://github.com/CTZZG/MusicFreeDesktop-1/releases

插件地址:

1
2
3
4
5
https://gitee.com/Rrance/WP/raw/master/audiobookshelf.js




Cinetry

https://github.com/gstory0404/Cinetry

Buchable:
https://github.com/Vito0912/abs_flutter

## MAC

Cinetry

https://github.com/gstory0404/Cinetry

希声:
https://apps.apple.com/cn/app/%E5%B8%8C%E5%A3%B0-%E4%B8%BA-audiobookshelf-%E6%89%93%E9%80%A0/id6754208326

l

WindowsPE和MacOS安装盘二合一制作方法

一、准备U盘

准备一个U盘,安装包一般大小都在12G以上,所以建议至少16G,或者直接使用移动硬盘。

插入U盘,打开Mac的磁盘工具

选择U盘-抹掉

  • 名称:随意

  • 格式:格式Mac OS扩展(日志式)

  • 方案:GUID分区图

notion image

抹掉完成之后,再对U盘进行分区用于制作Mac OS安装盘,点击+号,增加新的分区

分区信息:

  • 名称:UOS

  • 格式:Mac OS扩展(日志式)

  • 大小:必须大于你的安装包,我这里直接分了16G

新增分区之后,就可以准备制作Mac OS安装盘了。

二、制作Mac OS安装盘

下载好的Mac OS安装包默认放在应用程序中按照一下方式进行安装盘制作:

  1. 敲“sudo”(无引号)
  2. 空格
  3. 拖拽文件“createinstallmedia”到终端(文件位置在“安装macOS Ventura.app”-右键-显示包文件-Contents-Resources里)
  4. 敲“--volume”(无引号)
  5. 空格
  6. 从桌面把你之前插入的U盘的图标拖进终端(我这里U盘名字为UOS)
  7. 敲“--nointeraction”(无引号)
  8. 回车
  9. 输入密码(密码不显示)
  10. 回车,静待安装盘建立完成。

以上步骤综合为这么一串代码,请根据自己系统版本和U盘名字对应修改:

1
sudo /Applications/Install\ macOS\ Ventura.app/Contents/Resources/createinstallmedia --volume /Volumes/UOS --nointeraction

整个制作过程如下,到最后显示出这个就表示安装盘制作完成。

1
2
3
4
5
6
7
8
sudo /Applications/Install\ macOS\ Ventura.app/Contents/Resources/createinstallmedia --volume /Volumes/Install\ macOS\ Ventura --nointeraction
Password:
Erasing disk: 0%... 10%... 20%... 30%... 100%
Copying essential files...
Copying the macOS RecoveryOS...
Making disk bootable...
Copying to disk: 0%... 10%... 20%... 30%... 40%... 50%... 60%... 70%... 80%... 90%... 100%
Install media now available at "/Volumes/Install macOS Ventura"

到这里,如果你的引导没有屏蔽多余的启动项,在启动中已经可以看到这个安装盘了,选择既可以使用。

三、加入黑苹果EFI

安装盘制作完成之后,为了可以以后将U盘作为启动盘从BIOS的启动界面启动安装盘,就需要将自己配置好的引导文件EFI文件放到EFI分区中。

使用工具挂载EFI分区,比如用OpenCore Configurator 或者Hackintool 或者 OCAuxiliaryTools都可以,挂载U盘的EFI分区之后,将自己的配置好的EFI文件放进去,就成为了自己黑苹果的专用安装盘了。

那如果需要用于不同的机器来安装黑苹果,或者调试的时候修改EFI文件,总是需要挂载EFI分区,就比较麻烦了。

我们可以在引导分区之外,新建一个公开的引导分区。

打开磁盘工具,选择U盘,点分区,选择之前制作Mac OS安装盘剩下没有使用的部分,点+号,增加新的分区。

分区信息:

  • 名称:MAC_EFI(随意,不能有中文)

  • 格式:MS-DOS (FAT32)

  • 大小:1G(引导文件很小,一般大于200M即可)

notion image

这个新建的引导分区同样也可以放黑苹果的EFI引导文件,而且是无需挂载的,适合频繁修改调试引导配置和更换文件。

四、制作Windows PE

按照上面的方式同样新增一个分区

分区信息:

  • 名称:WINPE(随意,同样不能有中文)

  • 格式:MS-DOS (FAT32)

  • 大小:大于你的PE文件

新增分区完成之后,这里就需要一个支持UEFI启动的WinPE,一般PE主流格式都是exe格式,不符合我们的需要,所以需要在Window系统下先将PE制作成iso文件。

这里推荐老毛桃PE,功能比较多,可以联网,适合大多数PE需求。

然后在Mac下,打开iso文件,将里面的文件复制到新增的WINPE分区中。

结束

这样,一个Mac OS的安装盘和PE盘二合一的U盘就制作好了,剩下还有多余空间,可以当做正常存储U盘使用。

l

将威联通NAS的目录通过NFS共享,并挂载到内网Docker宿主机,作为容器目录使用

核心思路

先将远程内网主机的共享目录挂载到 Docker 宿主机的一个本地目录上,然后再将这个本地目录作为数据卷(Volume)或绑定挂载(Bind Mount)提供给 Docker 容器使用。

1
2
3
4
5
6
7
8
9
10
[ 内网共享主机 (NAS/Samba Server) ]
|
| (网络共享协议,如 NFS/SMB)
|
[Docker 宿主机 ]
| (本地文件系统挂载点,如 /mnt/nas/downloads)
|
| (Docker 卷或绑定挂载)
|
[ Docker 容器 (如 Transmission, qBittorrent) ]

威联通nas开启NFS共享

因为我的内网装有qBittorrent 的docker宿主机是基于ubuntu的服务器,所以推荐使用NFS。NFS 在 Linux 环境下性能好,开销低,是首选方案。如果docker宿主机是 Windows ,或启用了NAS的 Samba服务,这种方法更合适。

创建共享目录

威联通依次打开控制台⇨权限⇨共享目录⇨创建⇨共享文件夹

填写共享目录名称pt,记住文件夹名称,之后会用到。然后手动输入共享文件夹的路径,点击创建。

激活威联通nas的nfs功能

威联通依次打开控制台⇨网络和文件服务⇨Win/Mac/NFS⇨NFS服务

激活NFS v4服务和启用manage-gids,关闭写入高速缓存。

  • 启用最新的 NFS 主要版本v4:v4 在安全性(强认证)、性能(复合操作)和跨防火墙友好性(固定端口)方面有显著改进。
  • 启用manage-gids:对于 Docker 挂载场景,这可以极大地简化容器内用户(如 PUID=1000, PGID=1000)对挂载目录的读写权限管理,避免出现 “Permission denied” 错误。
  • 关闭写入高速缓存:对于 Docker 挂载,如果共享目录中存储的是重要的、不可再生的数据(如文档、数据库、个人文件),为了数据安全,建议停用。如果存储的是可以重新下载的缓存、临时文件或可丢弃的数据,可以启用以获得更好性能。

设置共享文件夹权限

创建完的共享文件夹,会在这里显示。点击图中按钮,编辑共享文件夹权限。

依次选择⇨选择权限类别:NFS主机访问⇨勾选访问权限⇨设置访问的IP地址或域名。

主机ip:192.168.60.0/24CIDR(无类别域间路由) 的表示方法,它表示了一个包含256个IP地址的网络段,范围从192.168.60.0 到 192.168.60.255。。

Squash:选择读写。需要docker宿主机能够读写共享目录。

Squash选项:选择不Squash所有用户。

内网docker宿主机设置

创建挂载目录/mnt/nas/downloads

1
sudo mkdir /mnt/nas/downloads

挂载在目录,192.168.50.218:/pt

1
sudo mount -t nfs4 -o rw,hard,intr,timeo=300,retrans=3 192.168.50.218:/pt /mnt/nas/downloads

参数解释

  • hard:在 NFS 服务器故障时,程序会一直等待直到服务器恢复,保证数据一致性。
  • intr:允许用户中断正在等待的 NFS 操作。
  • timeo=300:设置超时时间为十分之300秒(即30秒)。
  • retrans=3:设置重传次数为3次。

开机自动挂载

编辑 /etc/fstab文件,sudo vim /etc/fstab

1
2
# 在 /etc/fstab 末尾添加
192.168.50.218:/pt /mnt/nas/downloads nfs4 rw,hard,intr,timeo=300,retrans=3,_netdev,auto 0 0

注意_netdev 选项非常重要,它告诉系统在网络就绪后再挂载。

整体含义

将位于 IP 地址为 192.168.50.218 的服务器上的共享目录 /pt,通过网络文件系统版本 4 挂载到本地目录 /mnt/nas/downloads


参数解释

该配置行由 6 个字段组成,字段间用空格或制表符分隔:

  1. 192.168.50.218:/pt
    • 含义:NFS 服务器和它提供的共享路径。
    • 192.168.50.218:NFS 服务器的 IP 地址。
    • :/pt:服务器上被共享出来的目录路径。
  2. /mnt/nas/downloads
    • 含义挂载点。这是本地的一个空目录,远程共享的内容将出现在这个目录里。
  3. nfs4
    • 含义:文件系统类型。这里指定使用 NFS 版本 4
  4. rw,hard,intr,timeo=300,retrans=3,_netdev,auto
    • 含义:挂载选项,多个选项用逗号分隔。
    • rw:以读写模式挂载(而非只读 ro)。
    • hard硬挂载。如果服务器无响应,客户端会持续重试。这对于保证数据一致性非常重要,但可能导致进程在访问挂载点时卡住。
    • intr:允许中断正在等待服务器响应的 I/O 操作。当使用 hard 挂载时,如果服务器宕机,可以用这个选项来终止被挂起的进程。
    • timeo=300:设置超时时间为 300 十分之一秒(即 30 秒)。这是客户端等待服务器响应的时间。
    • retrans=3:在放弃并返回错误之前,进行 3 次 重传。
    • _netdev:这是一个非常重要的选项,它告诉系统这是一个网络设备。系统会等待网络就绪后再尝试挂载,避免系统启动时因网络未准备好而挂载失败。
    • auto:使用 mount -a 命令(通常在系统启动时执行)时会自动挂载此文件系统。
  5. 0
    • 含义:供 dump 备份工具使用。0 表示不需要被 dump 备份。
  6. 0
    • 含义:供 fsck 磁盘检查工具使用。0 表示在启动时不需要检查此文件系统(因为它是网络文件系统,不在本地磁盘上)。

docker宿主机运行qbittorrent容器,并挂载共享目录

1
2
3
4
5
6
7
8
9
10
docker run -d \
--name=qbittorrent \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-e TZ=Asia/Shanghai \
--net=host \
-v ~/software/docker/qbittorrent/config:/config \
-v /mnt/nas/downloads/qbittorrent/downloads:/downloads \
--restart unless-stopped \
lscr.io/linuxserver/qbittorrent:latest

qbittorrent默认端口8080,可在config目录里的qbittorrent.conf配置文件修改。

l

安装OpenVpn的Ubuntu22.04服务端和Windows10客户端

®
本文将在Ubuntu22.04服务器安装OpenVpn服务端,并在配置OpenVpn客户端ovpn文件。

安装OpenVpn和Easy-RSA

Easy-RSA主要用来生成CA证书,服务端证书和key,客户端证书和key。先登录Ubuntu服务器,然后用root身份执行下文的命令。

1
2
3
4
5
6
sudo apt update
sudo apt install openvpn easy-rsa

# 安装完可以查看下openvpn版本
openvpn --version
# 我的是OpenVPN 2.4.12版本

制作所需的证书

执行如下命令

1
2
3
4
5
6
7
8
9
10
cd /usr/share/easy-rsa

# 拷贝一份vars文件
cp vars.example vars

# 编辑vars文件
vim vars

# 在最后一行添加如下内容,这个 KEY_NAME 下文会用到,请记住
export KEY_NAME="myserver"

开始制作证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 执行完这个命令,会在/usr/share/easy-rsa下多出个pki目录
./easyrsa init-pki
chmod 777 pki

# 创建ca证书,此时在pki目录下会多出 ca.crt
./easyrsa build-ca nopass

# 创建服务端证书,命令中的myserver要换成前面vars文件中设置的KEY_NAME
# 此时,pki下的issued和private目录会多出myserver.crt、myserver.key 等文件
./easyrsa build-server-full myserver nopass

# 创建客户端证书
# 再次查看,pki下的issued和private目录会多出myclient.crt、myclient.key 等文件
./easyrsa build-client-full myclient nopass

# 创建DH密钥
./easyrsa gen-dh

配置OpenVpn服务端

拷贝配置文件

1
cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf /etc/openvpn/

拷贝刚才创建的CA证书,服务端证书和key(私钥),客户端证书和key,以及创建tls的key

1
2
3
4
5
6
7
8
9
10
cd /etc/openvpn/
cp /usr/share/easy-rsa/pki/ca.crt .
cp /usr/share/easy-rsa/pki/dh.pem .
cp /usr/share/easy-rsa/pki/issued/myserver.crt server/
cp /usr/share/easy-rsa/pki/private/myserver.key server/
cp /usr/share/easy-rsa/pki/issued/myclient.crt client/
cp /usr/share/easy-rsa/pki/private/myclient.key client/

# 生成tls需要的key
openvpn --genkey tls-auth ta.key

编辑服务端配置文件server.conf,改成如下内容,安全起见我把默认端口1194,改成了11000:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# vpn服务端口
port 11000

# 使用tcp协议
proto tcp

# vpn使用的虚拟网卡设备
dev tun

# CA以及证书和私钥
ca /etc/openvpn/ca.crt
cert /etc/openvpn/server/myserver.crt
key /etc/openvpn/server/myserver.key # This file should be kept secret
dh /etc/openvpn/dh.pem

# vpn服务端要给客户端分配的IP地址(以及网段)
server 10.8.100.0 255.255.255.0

ifconfig-pool-persist /var/log/openvpn/ipp.txt

# vpn服务端要帮客户端转发的网段(即192.168.50.0/255.255.255.254这些地址,才会经过vpn)
# 代表 vpn 转发 192.168.50.1 - 192.168.50.254的请求
push "route 192.168.50.0 255.255.255.0"

# 让客户端使用下面的dns服务器
push "dhcp-option DNS 114.114.114.114"
push "dhcp-option DNS 8.8.8.8"

# 客户端之间可以互相访问
client-to-client

# 支持多个客户端连接服务端
duplicate-cn
keepalive 10 120

# 开启tls安全,上文的ta.key就用在这里
tls-auth ta.key 0 # This file is secret
cipher AES-256-CBC

# 启用lzo压缩
comp-lzo

# 运行最多30个客户端连接
max-clients 30

# 持久key和tun设备
persist-key
persist-tun

status /var/log/openvpn/openvpn-status.log
log /var/log/openvpn/openvpn.log

# 记录的日期等级为3
verb 3

启动vpn服务端

1
2
3
4
5
systemctl start openvpn@server
systemctl status openvpn@server

# 感兴趣的话,可以查看日志
tail -f /var/log/openvpn/openvpn.log

如果你的Ubuntu系统是云服务器,通常管理后台可以设置防火墙,建议在Ubuntu关闭ufw:

1
2
3
4
5
# 停止ufw
sudo ufw disable
# 禁用ufw
sudo systemctl stop ufw
sudo systemctl disable ufw

设置IP转发

如果你搭建的VPN服务器所在的内网还有其他云服务器,你想访问其他云服务器就应该设置ip转发。就如上文描述的,我们在VPN服务端配置文件server.conf中配置了 push "route 192.168.0.0 255.255.255.224"

打开内核ip转发:

1
2
3
4
5
6
7
8
# 编辑文件/etc/sysctl.conf
vim /etc/sysctl.conf

# 打开如下一行,改为:
net.ipv4.ip_forward = 1

# 然修改永久生效
sysctl -p

设置防火墙(iptables )转发规则:

1
2
3
4
# 设置源地址翻译SNAT:把来自10.8.100.0/24的流量在离开网络接口ens3前,修改源ip为192.168.50.14
# enp0s31f6为你自己的网卡名称,可使用`ip addr show`查看。
# 192.168.50.14为安装openvpn 服务端的主机ip
iptables -t nat -A POSTROUTING -s 10.8.100.0/24 -o enp0s31f6 -j SNAT --to-source 192.168.50.14

重启下vpn服务端:systemctl start openvpn@server。到这里,服务端安装完成。

配置客户端ovpn文件

在openvpn官网下载客户端:这里。下载好以后点击运行,进行安装。

1
2
3
4
5
# 准备好Ubuntu上的这4个文件
/etc/openvpn/ca.crt
/etc/openvpn/ta.key
/etc/openvpn/client/myclient.crt
/etc/openvpn/client/myclient.key

创建客户端配置文件

打开终端,复制粘贴以下脚本指令,脚本会在~/openvpn-clients/目录下,创建一个client1.ovpn客户端配置文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 创建客户端的配置
cat > ~/openvpn-clients/client1.ovpn << 'EOF'
client
dev tun
proto tcp
remote 你的公网IP地址 11000
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verb 3
ca ca.crt
cert myclient.crt
key myclient.key
tls-auth ta.key 1
comp-lzo
route 192.168.50.0 255.255.255.0

<ca>
EOF

sudo cat /etc/openvpn/ca.crt >> ~/openvpn-clients/client.ovpn
cat >> ~/openvpn-clients/client1.ovpn << 'EOF'
</ca>

<cert>
EOF

sudo cat /etc/openvpn/client/myclient.crt >> ~/openvpn-clients/client1.ovpn
cat >> ~/openvpn-clients/client1.ovpn << 'EOF'
</cert>

<key>
EOF

sudo cat /etc/openvpn/client/myclient.key >> ~/openvpn-clients/client1.ovpn
cat >> ~/openvpn-clients/client1.ovpn << 'EOF'
</key>

<tls-auth>
EOF

sudo cat /etc/openvpn/ta.key >> ~/openvpn-clients/client1.ovpn
cat >> ~/openvpn-clients/client1.ovpn << 'EOF'
</tls-auth>
EOF

注意替换,你的公网IP地址和修改路由转发参数

上面的 route 192.168.50.0 255.255.255.0 也是告诉服务端,客户端访问这些 192.168.50.0/255.255.255.0 地址时,才经过VPN。

保存配置文件,然后启动 openvpn gui 客户端。通常,客户端会有opvn文件导入功能,导入客户端,点连接即可。连接成功显示如下:

img

然后,在CMD中ping VPN服务端的地址(下面10.8.100.1是vpn分配的网关地址,vpn客户端分配到的是10.8.100.6,192.168.50.28是vpn服务器内网ip,192.168.50.4是vpn服务所在内网的另一条机器的内网ip):
img
至此,vpn客户端和服务端全部安装完成。

l

HIFI日记:HQPlayer embedded (HQPlayer OS)嵌入版使用教程

HQPlayer播放器系列

一、前言

这篇文章继承自“DIY高性能音乐服务器”和“ROON+HQPlayer+NAA串流数播/网播完整教程”两篇文章,如果对于自己制作一台Core感兴趣的,可以跳转了解。经过大半年对于X86 Core的搭建,BLOG主在当前有限的环境下有了一些新的感悟。

二、什么是HQPlayer embedded

HQPlayer embedded是HQPlayer Linxu版本的延伸,是一个完全客制化的Linux系统,里面搭载了专门制作的HQPlayer OS系统,其定制的Network Audio Adapter协议是当前世界上最强的网络音频传输协议,同时它还提供了丰富的可调节选项,是当前实际上功能最多、性能最强、音质最好的播放系统。

img官方提供的NAA拓扑图

具体的请查阅官方介绍:点我

三、如何安装HQPlayer embedded

1、打开官方下载链接:点我
试用版每半小时将自动关机一次,其他功能与付费版一致

2、下载最新版本镜像并解压得到img文件
注意,当前HQPlayer embedded分为两个INTEL和AMD两个版本,各位可以根据自己的CPU自行选择,根据官方介绍,这似乎是指令集上有些不同,可能影响某些时候的运行效率。他们一般名字命名为:
INTEL:hqplayer-embedded-4.29.3-x64gen.7z
AMD:hqplayer-embedded-4.29.3-x64amd.7z

PS:国外论坛中有人提出AMD支持的指令集更完善,对音质的影响未知。intel系列CPU的玩家可以直接选择AMD版本,也没什么问题。

3、在主板上安装sata/nvme硬盘,或者插入USB设备,系统安装将完全清空整个硬盘,且不可继续做他用。根据安装方式不同,我们可以在硬盘中制作HQPlayer embedded系统,开机即可使用。也可以制作USB随身携带版本,但经过USB后,音质必然存在劣化。

4、下载镜像烧录软件
我们采用的是“balenaEtcher”进行烧录,他的优势是可以直接对硬盘和USB设备进行烧录,速度快且稳定。
官网下载:点我

5、安装balenaEtcher,并根据下方提示进行烧录镜像

img

(1)点击Flash from file选择下载好的HQE的img镜像

img

(2)选择需要写入的硬盘,注意这将完全破坏写入硬盘的所有数据。考虑到HQE本身就是个常驻运行系统,因此建议使用高性能的SSD作为启动盘为好。这里BLOG主选择的是INTEL傲腾,也是理论上同级别SSD中延迟最低、4K性能最好对硬盘,但考虑到BLOG主以前做过不同SSD测评,实在找不到什么音质规律,因此选择仅供参考。

img

img

(3)最后点击Flash!按钮,在弹出框中选择Yes,I’am sure,然后等待完成即可。

6、完成后将制作好的SSD装入音频服务器(Core)中,开机在主板设置成该硬盘启动,等待约30秒即可完成系统载入。

四、如何使用HQPlayer embedded

当前HQPlayer embedded本地播放功能难用到极点,而且本地播放到音质在没处理的情况下还不如NAA,因此这里仅推荐联网使用。很多朋友可能对网络处理没有特别多的概念,下面提供一下BLOG主当前系统部分拓扑图,各位可以参考,也可以搜索BLOG主之前写的相关文章。

img

五、HQE设置

安装完成HQE后,各位首先需要进入路由器界面,找到HQE服务器的内网IP地址,如下方,BLOG主自动分配的IP地址为:192.168.1.197

然后,将这个IP地址复制到浏览器中打开,即可进入HQE后台,在这里可以对HQE进行各种设置。在这个界面上,如果出现账号和密码需求,默认账号为“hqplayer”,默认密码则是“password”。如果需要修改密码则可以点到“Authentication”页面进行设置。

接下来,我们详细介绍设置页面下,各个选项的意义:

1、设置Backend(后端)

img

如果需要本地播放选择ALSA,如果需要通过网络进行NAA推送播放,则选择Network Audio

2、设置Output mode(音频输出模式)

img

Auto是根据音频本身的格式,输出PCM或者SDM。但如果希望强制音频转换成PCM或者SDM,可以选择下面两项强制输出。

3、Fixed volume(绝对音量)

img

该选项可以将音量锁定在某一个响度,非必要选项。如果不希望数字音量调节,可以勾选“enabled”(开启)并填入数字“0”。

4、Max volume/Min volume(最大音量/最小音量)

img

该选项可以自行决定音量响度的刻度尺,设定最小和最大两个数值,后面就可以在这个规定的范围内进行调节。如果打开了上面的选项,则此选项不生效。

5、Startup volume (启动音量)

img

本选项相当于音乐开头提供一个衰减,以降低对人耳的刺激,对音质无影响,可以不设置。

6、Adaptive volume(适应性音量)

img

自动平衡各音频的响度,保证每首曲子的音量基本一致,可能对音质有负面影响,建议不开启。

7、PCM gain compensation(PCM增益补偿)

img

播放PCM音频时,为每首曲子固定增加的音量,,可能对音质有负面影响,建议不开启。

8、Channels(声道)

img

选择输出声道数量,一般填写“2”,即两声道。如果是多声道音频,在硬件条件允许情况下,需要对此进行配对设置。

9、Options(其它设置)

img

Auto rate family:根据音频原采样率进行倍数升频,如果不选,则是根据设置的最高采样率进行升频。建议一定要开启,以保证不会出现SRC偏差。

Quick pause:快速暂停,允许在播放音频时迅速启动暂停/重新启动播放的功能。

Short buffer:短缓冲,可以有效降低播放延迟,但可能影响最终音质。除非是观看视频时出现明显音画不同步的情况,一般听音乐不选择此选项。

10、Log file(日志文件)

记录HQE系统运行时的日志文件,一般用户无需理会该选项,甚至可以选择关闭。

11、DSD sources(DSD信号源)

img

该部分为DSD信号源部分的设置:

Direct SDM:直接输出SDM信号,关闭升频。

Gain +6 dB:为所有DSD音频信号默认追加6dB响度。

Intergrator:低通滤波器,当前提供了5种可选,请根据听感自行选择。

SDM-SDM Conversion:DSD升频滤波设置,当前有wide和narrow两种选择。

img仅供参考

Noise filter:噪声滤波器,当前有9种可选,请根据实际听感选择。

SDM-PCM Conversion:DSD升频PCM滤波选择,当前有14个可选,请根据实际听感选择。

12、PCM settings/SDM settings(PCM播放设置/SDM播放设置)

img

该部分设置与桌面版HQPlayer完全一致,请参考这里

13、ALSA backend(ALSA后端设置/本地播放设置)

img

Device:输出设备选择

Channel offset:通道偏移,不知道有什么用,默认为0,请勿改动。

DAC bits:DAC的转换位深,根据硬件实际性能选择,如果不知道填数字“0”

Buffer time:缓冲时间,默认为100ms,可以根据硬件和体验实际进行调整。

DoP:将SDM信号打包成PCM信号输出

48k DSD:强制PCM升频DSD时采用48k整数倍升频(44.1Khz进行DSD升频为非整数倍升频,SRC可能导致失真),一般不开启,因为不推荐将PCM升频至DSD。

14、Network Audio backend(网络后端设置/NAA播放设置)

img

IPv6:打开对IPv6的IP地址支持,需要路由支持。

六、连接APP推送音乐

所有东西设置好!我们就可以开始听音乐了。将手机/平板连接进入与HQE同一个网络,然后下载合适的软件,选择HQE推送即可!下面推荐几个BLOG主尝试过后认为合适的方案供参考:

1、BubbleUPnP(Android)

支持连接Qobuz、TIDAL、Google Drive、Dropbox、Box、OneDrive以及本地NAS进行流媒体NAA推送播放。功能全面、响应迅速、软件免费、无广告,如果不需要ROON的数据功能,可以全面替代。

2、mconnect(Android/iOS)

持连接Qobuz、TIDAL、OneDrive以及本地NAS进行流媒体NAA推送播放。界面美观、功能全面、响应稍慢、有付费和免费版本可选择。

3、AirMusic(Android)

可以将手机/平板中所有音频信号通过UPNP协议发送,这样就可以通过整套HIFI系统看Youtube、Bilibili、抖音等视频了!注意本软件有付费和免费版,可能需要ROOT支持。

4、QQ音乐(Android/iOS)

在最新版本的QQ音乐中,HQ(含)以下音质可以直接推送到HQE中播放了!虽然并非无损格式,整体音质受损,但是瑕不掩瑜,毕竟大量无版权音乐可能只能在这里听到!顺便BS一下网易云,客户端稀烂,音质为所有播放器中垫底。

l

OpenCore添加ubuntu引导,引导双系统

问题描述

在此前已经安装好了Mac,并能正常引导但是Ubuntu的引导需要手动按F10进行选择,比较麻烦,尝试将Ubuntu的引导也加入到OC中。

参考国外大佬Ayush Sahay Chaudhary的博客[GUIDE] Opencore Dualboot/ Multiboot Guide进行的配置,该博客中有完整的win+mac+linux的安装步骤。

实现步骤

请确保你的OC中存在OpenShell.efi文件,没有可前往opencore官方网站进行抄作业

下载OpenCore Configurator

进入MAC,在网上下载OpenCore Configurator工具,工具版本要和OC文件版本对应。

在OpenShell中找到EFI的位置

启动到 OpenCore 并选择 OpenShell.efi

如果在OC中无法找到该选项而EFI中确实放置了OpenShell.efi,那么进入Mac中启动OpenCore Configurator。

转到工具> 挂载EFI

选择自己电脑EFI所在的硬盘并挂载

找到其中的EFI> OC> config.plist,在OpenCore Configurator中打开该文件

将其启用保存再进入OC中就可以看到OpenShell.efi

重启到 OpenCore 并选择 OpenShell.efi

你会看到这样的屏幕(这时请注意及时按一下按键)

这里的东西是,FS1, FS2,… FS7: 是所有连接到系统的驱动器的分区,你必须确定哪个是Linux 分区。

要查找分区开始输入

1
FS1:

然后输入

1
dir

继续这个操作一直到你在输入 DIR 后找到 EFI 的文件夹

输入

1
FS2:

然后输入

1
dir

再次这样输入

我在 FS7中找到了我的 总 EFI 分区:

检查你的EFI文件夹

1
2
3
cd EFI

dir

如果ubuntu的文件夹是在这个分区里面

输入

1
cd ../

回到你的根 EFI 分区,即 FS7

再次输入

1
map > map-table-linux.txt

将地址信息导出到txt文件

制作启动项地址

现在重新启动系统并从启动菜单中选择 macOS 驱动器启动到 macOS。

同样挂载EFI分区

打开 EFI分区可以看到,map-table-linux.txt

将其剪切到桌面,打开它

查到你在OpenShell中找到的驱动器号(我的是FS7),复制 PCI 行(图中选中的那一行)并将其粘贴到一个新的文本文件中,然后在 PCI 行之后添加这一行,如下所示,

1
/\EFI\ubuntu\grubx64.efi

注意:前面不能有空格

之后可以看到是这样子

1
PciRoot(0x0)/Pci(0x1D,0x0)/Pci(0x0,0x0)/NVMe(0x1,20-60-A0-49-8B-44-1B-00)/HD(1,GPT,B2C56269-054C-45C4-8ACS-C321A6B52982,0x800,0xAF000)/\EFI\ubuntu\grubx64.efi

在OpenCore Configurator中添加启动项

同样打开OpenCore Configurator,进入目录EFI> OC> config.plist,将config.plist在OpenCore Configurator中打开,然后去Misc-其它设置中,选择Entries-自定义条目。

在右下角添加自定义条目

路径中添加创建好的路径,名称填写Ubuntu,风格自动,勾选启用。

大功告成

效果展示

l

hqplayer和roon二合一

第1步:安装并配置宿主机(Debian)

1.安装Debian:从Debian官网下载网络安装镜像,进行最小化安装。在软件选择时,只勾选“SSH server”和“Standard system utilities”。

2.更新系统:

1
sudo apt update && sudo apt upgrade -y

3.安装必要工具:

1
sudo apt install -y p7zip-full squashfs-tools systemd-container

p7zip-full用于解压7z文件,systemd-container提供了更现代的容器化管理工具(但我们仍会使用传统chroot)

第2步:准备HQPlayer OS的chroot环境

1.创建工作目录:

1
sudo mkdir -p /opt/hqplayer-chroot

2.解压HQPlayer Embedded:

1
sudo 7z x hqplayer-embedded-5.8.1-x64sse42.7z -o/opt/hqplayer-chroot

这会在/opt/hqplayer-chroot下得到hqplayer-embedded-5.8.1/文件夹,里面包含bin, lib, usr等目录。这就是我们chroot的基础。
我们需要把它挂载到一个临时目录,然后复制其中的所有文件。

1
2
# 创建挂载点
sudo mkdir -p /mnt/hqplayer-image

3.挂载镜像中的分区
您需要先找到镜像中的分区偏移量,然后挂载正确的分区。
第1步:查看镜像的分区信息

1
sudo fdisk -l hqplayer-embedded-5.8.1-x64sse42.img

fdisk 输出显示镜像中有两个分区:
分区1:Start=2048, Size=27.8M, Type=”Microsoft basic data” (这很可能是EFI系统分区)
分区2:Start=59392, Size=3G, Type=”Linux filesystem” (这是我们要的根文件系统分区)
挂载分区(分区2)

1
2
3
4
5
# 计算分区2的偏移量 (59392 sectors * 512 bytes/sector)
OFFSET=$((59392 * 512))

# 挂载分区2到挂载点
sudo mount -o loop,offset=$OFFSET hqplayer-embedded-5.8.1-x64sse42.img /mnt/hqplayer-image

复制文件到chroot目录

1
2
3
4
5
6
7
8
# 查看挂载的内容
ls -la /mnt/hqplayer-image/

# 复制所有文件到chroot目录(保留权限和属性)
sudo cp -a /mnt/hqplayer-image/* /opt/hqplayer-chroot/

# 卸载镜像
sudo umount /mnt/hqplayer-image

4.创建必要的系统目录:

1
2
sudo mkdir -p /opt/hqplayer-chroot/{proc,sys,dev,run,var/lib,etc,tmp}
sudo chmod 1777 /opt/hqplayer-chroot/tmp

5.复制DNS解析配置:

1
sudo cp /etc/resolv.conf /opt/hqplayer-chroot/etc/

6.让 chroot 环境访问主机音频设备
首先识别音频设备

1
2
3
4
5
6
7
8
# 在宿主机上查看所有音频设备
aplay -l

# 或者使用更详细的方式
cat /proc/asound/cards

# 查看 USB 音频设备详情
lsusb | grep -i audio

可以看到您的音频设备配置:

card 0: AB13X USB Audio (USB DAC) - 这是您的外部USB音频设备

card 1: Ensoniq AudioPCI - 这是主板内置声卡

现在让我们确保chroot环境中的HQPlayer能够访问这些设备。

配置chroot环境访问USB DAC, 修改启动脚本确保正确映射

第3步:配置chroot环境

我们需要一个脚本来自动挂载必要的文件系统并进入chroot。

创建挂载和进入chroot的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sudo nano /usr/local/bin/start-hqplayer-chroot.sh
#!/bin/bash
HQPLAYER_ROOT="/opt/hqplayer-chroot"

# 挂载必要的文件系统
mount --bind /proc "$HQPLAYER_ROOT/proc"
mount --bind /sys "$HQPLAYER_ROOT/sys"
mount --bind /dev "$HQPLAYER_ROOT/dev"
mount --bind /dev/pts "$HQPLAYER_ROOT/dev/pts"
mount --bind /dev/snd "$HQPLAYER_ROOT/dev/snd"
mount --bind /run "$HQPLAYER_ROOT/run"

# 确保USB设备可访问(重要!)
mount --bind /dev/bus/usb "$HQPLAYER_ROOT/dev/bus/usb" 2>/dev/null || true

# 确保使用最新的DNS配置
cp /etc/resolv.conf "$HQPLAYER_ROOT/etc/resolv.conf"

# 运行HQPlayer
exec chroot "$HQPLAYER_ROOT" /usr/bin/hqplayerd

停止脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo nano /usr/local/bin/stop-hqplayer-chroot.sh
#!/bin/bash
HQPLAYER_ROOT="/opt/hqplayer-chroot"

# 杀死所有hqplayerd进程
pkill -9 -f "hqplayerd" 2>/dev/null

# 卸载文件系统(确保正确的顺序)
umount -f "$HQPLAYER_ROOT/dev/bus/usb" 2>/dev/null
umount -f "$HQPLAYER_ROOT/dev/snd" 2>/dev/null
umount -f "$HQPLAYER_ROOT/dev/pts" 2>/dev/null
umount -f "$HQPLAYER_ROOT/dev" 2>/dev/null
umount -f "$HQPLAYER_ROOT/sys" 2>/dev/null
umount -f "$HQPLAYER_ROOT/proc" 2>/dev/null
umount -f "$HQPLAYER_ROOT/run" 2>/dev/null

sleep 2

设置正确的权限

1
2
3
4
sudo chmod +x /usr/local/bin/start-hqplayer-chroot.sh
sudo chmod +x /usr/local/bin/stop-hqplayer-chroot.sh

sudo systemctl daemon-reload

重新加载并测试服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 重新加载systemd配置
sudo systemctl daemon-reload

# 手动测试启动脚本
sudo /usr/local/bin/start-hqplayer-chroot.sh

# 检查hqplayerd是否运行
ps aux | grep hqplayerd

# 检查端口监听
sudo ss -tlnp | grep :8088

# 如果运行正常,停止它
sudo /usr/local/bin/stop-hqplayer-chroot.sh

系统使用的是 cgroup2(统一层次结构),而不是传统的cgroup1。这可能是导致服务启动失败的原因,因为一些旧的应用或脚本可能不完全兼容cgroup2。
第1步:检查系统是否支持cgroup1

1
2
3
4
5
# 检查是否启用了cgroup1兼容性
cat /proc/filesystems | grep cgroup

# 检查内核启动参数
cat /proc/cmdline

修改systemd服务文件以兼容cgroup2

1
sudo nano /etc/systemd/system/hqplayer-chroot.service

使用以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[Unit]
Description=HQPlayer Embedded Daemon in Chroot
After=network.target
Requires=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/start-hqplayer-chroot.sh
ExecStop=/usr/local/bin/stop-hqplayer-chroot.sh
Restart=on-failure
RestartSec=5
TimeoutStartSec=300
TimeoutStopSec=30

# 关键配置:禁用所有cgroup相关功能
Delegate=no
CPUAccounting=no
MemoryAccounting=no
IOAccounting=no
TasksAccounting=no
IPAccounting=no
BlockIOAccounting=no

# 避免cgroup2相关问题
#Slice=
#ControlGroup=
[Install]
WantedBy=multi-user.target

启动服务并检查

1
2
sudo systemctl start hqplayer-chroot.service
systemctl status hqplayer-chroot.service

dns或者删除符号链接并创建实际文件

1
2
3
4
5
6
7
8
# 删除符号链接
sudo rm /opt/hqplayer-chroot/etc/resolv.conf

# 创建实际的 resolv.conf 文件
sudo cp /etc/resolv.conf /opt/hqplayer-chroot/etc/resolv.conf

# 确认现在是普通文件
ls -la /opt/hqplayer-chroot/etc/resolv.conf

第4步:执行精简

首先查看当前大小

1
sudo du -sh /opt/hqplayer-chroot/ --exclude=proc --exclude=sys --exclude=dev --exclude=run

删除最大的文件(镜像文件)

1
2
3
4
5
6
# 删除原始镜像文件(节省最多空间)
sudo rm /opt/hqplayer-chroot/hqplayer-embedded-5.8.1-x64sse42.img
sudo rm /opt/hqplayer-chroot/hqplayer-embedded-5.8.1-x64sse42.7z

# 删除备份文件(如果有)
sudo rm -rf /opt/hqplayer-chroot-backup 2>/dev/null || true

删除firmware文件

1
2
# 删除firmware(通常不需要)
sudo rm -rf /opt/hqplayer-chroot/lib/firmware

查看精简效果

1
2
3
4
5
6
7
8
# 查看当前大小
sudo du -sh /opt/hqplayer-chroot/ --exclude=proc --exclude=sys --exclude=dev --exclude=run

# 查看各目录大小
sudo du -sh /opt/hqplayer-chroot/* --exclude=proc --exclude=sys --exclude=dev --exclude=run | sort -hr

# 查看usr目录大小
sudo du -sh /opt/hqplayer-chroot/usr/*

验证功能正常

1
2
3
4
5
6
7
8
9
10
11
# 重启HQPlayer服务测试
sudo systemctl restart hqplayer-chroot.service

# 检查服务状态
systemctl status hqplayer-chroot.service

# 测试音频功能
sudo chroot /opt/hqplayer-chroot aplay -l

# 测试网络功能
sudo chroot /opt/hqplayer-chroot /bin/bash -c "ping -c 2 8.8.8.8"
l