koishi-plugin-chatsound 0.0.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +2 -1
- package/lib/index.js +77 -41
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -7,13 +7,14 @@ declare module 'koishi' {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const name = "chatsound";
|
|
9
9
|
export declare const inject: {
|
|
10
|
-
|
|
10
|
+
optional: string[];
|
|
11
11
|
};
|
|
12
12
|
export interface Config {
|
|
13
13
|
soundPath: string[];
|
|
14
14
|
defaultPitch: number;
|
|
15
15
|
minPitch: number;
|
|
16
16
|
maxPitch: number;
|
|
17
|
+
keepSoundLength: boolean;
|
|
17
18
|
audioType: "mp3" | "silk";
|
|
18
19
|
}
|
|
19
20
|
export declare const Config: Schema<Config>;
|
package/lib/index.js
CHANGED
|
@@ -30,20 +30,62 @@ var import_koishi = require("koishi");
|
|
|
30
30
|
var import_promises = require("fs/promises");
|
|
31
31
|
var import_path = require("path");
|
|
32
32
|
var import_child_process = require("child_process");
|
|
33
|
-
var import_util = require("util");
|
|
34
|
-
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
35
33
|
var name = "chatsound";
|
|
36
34
|
var inject = {
|
|
37
|
-
|
|
35
|
+
optional: ["silk"]
|
|
38
36
|
};
|
|
39
37
|
var Config = import_koishi.Schema.object({
|
|
40
|
-
soundPath: import_koishi.Schema.array(import_koishi.Schema.string()).description("
|
|
38
|
+
soundPath: import_koishi.Schema.array(import_koishi.Schema.string()).description("用于搜索音频的*绝对*路径,支持搜索mp3 wav ogg flac m4a aac格式"),
|
|
41
39
|
defaultPitch: import_koishi.Schema.number().default(100).description("默认的语音音调(百分比)"),
|
|
42
40
|
minPitch: import_koishi.Schema.number().default(80).min(0).description("最小Pitch,不小于0"),
|
|
43
41
|
maxPitch: import_koishi.Schema.number().default(200).description("最大Pitch"),
|
|
42
|
+
keepSoundLength: import_koishi.Schema.boolean().default(false).description("保持音频播放长度,仅进行 变调"),
|
|
44
43
|
audioType: import_koishi.Schema.union(["mp3", "silk"]).default("mp3").description("最终发送的类型,QQ及微信选择SILK")
|
|
45
44
|
});
|
|
46
45
|
var AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"];
|
|
46
|
+
var ffmpegAvailable = null;
|
|
47
|
+
var ffmpegCheckPromise = null;
|
|
48
|
+
async function checkFFmpeg() {
|
|
49
|
+
if (ffmpegAvailable !== null)
|
|
50
|
+
return ffmpegAvailable;
|
|
51
|
+
if (ffmpegCheckPromise)
|
|
52
|
+
return ffmpegCheckPromise;
|
|
53
|
+
ffmpegCheckPromise = new Promise((resolve) => {
|
|
54
|
+
const { spawn: spawn2 } = require("child_process");
|
|
55
|
+
let resolved = false;
|
|
56
|
+
const timeout = setTimeout(() => {
|
|
57
|
+
if (!resolved) {
|
|
58
|
+
resolved = true;
|
|
59
|
+
resolve(false);
|
|
60
|
+
}
|
|
61
|
+
}, 3e3);
|
|
62
|
+
try {
|
|
63
|
+
const ffmpeg = spawn2("ffmpeg", ["-version"]);
|
|
64
|
+
ffmpeg.on("error", () => {
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
resolve(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
ffmpeg.on("close", (code) => {
|
|
72
|
+
if (!resolved) {
|
|
73
|
+
resolved = true;
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
resolve(code === 0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
resolve(false);
|
|
81
|
+
}
|
|
82
|
+
}).then((result) => {
|
|
83
|
+
ffmpegAvailable = result;
|
|
84
|
+
return result;
|
|
85
|
+
});
|
|
86
|
+
return ffmpegCheckPromise;
|
|
87
|
+
}
|
|
88
|
+
__name(checkFFmpeg, "checkFFmpeg");
|
|
47
89
|
async function findAudioFile(trigger, soundPaths) {
|
|
48
90
|
for (const basePath of soundPaths) {
|
|
49
91
|
try {
|
|
@@ -69,8 +111,6 @@ async function runFFmpeg(commandArgs) {
|
|
|
69
111
|
const ffmpeg = (0, import_child_process.spawn)("ffmpeg", commandArgs);
|
|
70
112
|
const chunks = [];
|
|
71
113
|
ffmpeg.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
72
|
-
ffmpeg.stderr.on("data", (chunk) => {
|
|
73
|
-
});
|
|
74
114
|
ffmpeg.on("close", (code) => {
|
|
75
115
|
if (code === 0) {
|
|
76
116
|
resolve(Buffer.concat(chunks));
|
|
@@ -82,49 +122,41 @@ async function runFFmpeg(commandArgs) {
|
|
|
82
122
|
});
|
|
83
123
|
}
|
|
84
124
|
__name(runFFmpeg, "runFFmpeg");
|
|
85
|
-
async function applyPitch(inputPath, pitch, audioType) {
|
|
125
|
+
async function applyPitch(inputPath, pitch, audioType, keepSoundLength) {
|
|
86
126
|
const pitchFactor = pitch / 100;
|
|
127
|
+
let args = [
|
|
128
|
+
"-i",
|
|
129
|
+
inputPath,
|
|
130
|
+
"-vn",
|
|
131
|
+
"-sn",
|
|
132
|
+
"-dn",
|
|
133
|
+
"-y"
|
|
134
|
+
];
|
|
135
|
+
if (keepSoundLength) {
|
|
136
|
+
args.push("-af", `rubberband=pitch=${pitchFactor}`);
|
|
137
|
+
} else {
|
|
138
|
+
args.push("-af", `asetrate=sample_rate*${pitchFactor}`);
|
|
139
|
+
}
|
|
87
140
|
if (audioType === "mp3") {
|
|
88
|
-
|
|
89
|
-
"-i",
|
|
90
|
-
inputPath,
|
|
91
|
-
"-vn",
|
|
92
|
-
"-sn",
|
|
93
|
-
"-dn",
|
|
94
|
-
"-filter:a",
|
|
95
|
-
`rubberband=pitch=${pitchFactor}`,
|
|
96
|
-
"-f",
|
|
97
|
-
"mp3",
|
|
98
|
-
"-y",
|
|
99
|
-
"pipe:1"
|
|
100
|
-
];
|
|
141
|
+
args.push("-f", "mp3", "pipe:1");
|
|
101
142
|
const data = await runFFmpeg(args);
|
|
102
143
|
return { data, mimeType: "audio/mp3" };
|
|
103
144
|
} else {
|
|
104
|
-
|
|
105
|
-
"-i",
|
|
106
|
-
inputPath,
|
|
107
|
-
"-vn",
|
|
108
|
-
"-sn",
|
|
109
|
-
"-dn",
|
|
110
|
-
"-filter:a",
|
|
111
|
-
`rubberband=pitch=${pitchFactor}`,
|
|
112
|
-
"-f",
|
|
113
|
-
"s16le",
|
|
114
|
-
"-ac",
|
|
115
|
-
"1",
|
|
116
|
-
"-ar",
|
|
117
|
-
"24000",
|
|
118
|
-
"-y",
|
|
119
|
-
"pipe:1"
|
|
120
|
-
];
|
|
145
|
+
args.push("-f", "s16le", "-ac", "1", "-ar", "24000", "pipe:1");
|
|
121
146
|
const data = await runFFmpeg(args);
|
|
122
147
|
return { data, mimeType: "audio/pcm" };
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
150
|
__name(applyPitch, "applyPitch");
|
|
126
151
|
function apply(ctx, config) {
|
|
127
|
-
|
|
152
|
+
checkFFmpeg().then((available) => {
|
|
153
|
+
if (!available) {
|
|
154
|
+
ctx.logger.warn("FFmpeg not found! Audio processing will fail.");
|
|
155
|
+
} else {
|
|
156
|
+
ctx.logger.debug("FFmpeg is available.");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
ctx.command("v <trigger:string> [pitch:number] 发送一个噪音").action(async ({ session }, trigger, pitch) => {
|
|
128
160
|
if (!trigger) {
|
|
129
161
|
return "至少给个名字吧大王";
|
|
130
162
|
}
|
|
@@ -134,10 +166,14 @@ function apply(ctx, config) {
|
|
|
134
166
|
return `没有这种音频`;
|
|
135
167
|
}
|
|
136
168
|
try {
|
|
137
|
-
const { data, mimeType } = await applyPitch(audioPath, actualPitch, config.audioType);
|
|
169
|
+
const { data, mimeType } = await applyPitch(audioPath, actualPitch, config.audioType, config.keepSoundLength);
|
|
138
170
|
if (config.audioType === "silk") {
|
|
139
|
-
|
|
140
|
-
|
|
171
|
+
if (ctx.silk) {
|
|
172
|
+
const silkResult = await ctx.silk.encode(data, 24e3);
|
|
173
|
+
await session.send(import_koishi.h.audio(silkResult.data, "audio/silk"));
|
|
174
|
+
} else {
|
|
175
|
+
return "没有安装必要的SILK插件";
|
|
176
|
+
}
|
|
141
177
|
} else {
|
|
142
178
|
await session.send(import_koishi.h.audio(data, mimeType));
|
|
143
179
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"contributors": [
|
|
5
5
|
"Dr.Abc <me@drabc.net>"
|
|
6
6
|
],
|
|
7
|
-
"version": "
|
|
7
|
+
"version": "1.0.1",
|
|
8
8
|
"homepage": "https://github.com/DrAbcOfficial/koishi-plugin-chatsound",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"zh": "发送预定的噪音,可以变调,依赖ffmpeg"
|
|
34
34
|
},
|
|
35
35
|
"service":{
|
|
36
|
-
"
|
|
36
|
+
"optional": ["silk"]
|
|
37
37
|
},
|
|
38
38
|
"preview": true
|
|
39
39
|
}
|