koishi-plugin-chatsound 1.0.0 → 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 +1 -0
- package/lib/index.js +69 -33
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -39,9 +39,53 @@ var Config = import_koishi.Schema.object({
|
|
|
39
39
|
defaultPitch: import_koishi.Schema.number().default(100).description("默认的语音音调(百分比)"),
|
|
40
40
|
minPitch: import_koishi.Schema.number().default(80).min(0).description("最小Pitch,不小于0"),
|
|
41
41
|
maxPitch: import_koishi.Schema.number().default(200).description("最大Pitch"),
|
|
42
|
+
keepSoundLength: import_koishi.Schema.boolean().default(false).description("保持音频播放长度,仅进行 变调"),
|
|
42
43
|
audioType: import_koishi.Schema.union(["mp3", "silk"]).default("mp3").description("最终发送的类型,QQ及微信选择SILK")
|
|
43
44
|
});
|
|
44
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");
|
|
45
89
|
async function findAudioFile(trigger, soundPaths) {
|
|
46
90
|
for (const basePath of soundPaths) {
|
|
47
91
|
try {
|
|
@@ -78,49 +122,41 @@ async function runFFmpeg(commandArgs) {
|
|
|
78
122
|
});
|
|
79
123
|
}
|
|
80
124
|
__name(runFFmpeg, "runFFmpeg");
|
|
81
|
-
async function applyPitch(inputPath, pitch, audioType) {
|
|
125
|
+
async function applyPitch(inputPath, pitch, audioType, keepSoundLength) {
|
|
82
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
|
+
}
|
|
83
140
|
if (audioType === "mp3") {
|
|
84
|
-
|
|
85
|
-
"-i",
|
|
86
|
-
inputPath,
|
|
87
|
-
"-vn",
|
|
88
|
-
"-sn",
|
|
89
|
-
"-dn",
|
|
90
|
-
"-filter:a",
|
|
91
|
-
`rubberband=pitch=${pitchFactor}`,
|
|
92
|
-
"-f",
|
|
93
|
-
"mp3",
|
|
94
|
-
"-y",
|
|
95
|
-
"pipe:1"
|
|
96
|
-
];
|
|
141
|
+
args.push("-f", "mp3", "pipe:1");
|
|
97
142
|
const data = await runFFmpeg(args);
|
|
98
143
|
return { data, mimeType: "audio/mp3" };
|
|
99
144
|
} else {
|
|
100
|
-
|
|
101
|
-
"-i",
|
|
102
|
-
inputPath,
|
|
103
|
-
"-vn",
|
|
104
|
-
"-sn",
|
|
105
|
-
"-dn",
|
|
106
|
-
"-filter:a",
|
|
107
|
-
`rubberband=pitch=${pitchFactor}`,
|
|
108
|
-
"-f",
|
|
109
|
-
"s16le",
|
|
110
|
-
"-ac",
|
|
111
|
-
"1",
|
|
112
|
-
"-ar",
|
|
113
|
-
"24000",
|
|
114
|
-
"-y",
|
|
115
|
-
"pipe:1"
|
|
116
|
-
];
|
|
145
|
+
args.push("-f", "s16le", "-ac", "1", "-ar", "24000", "pipe:1");
|
|
117
146
|
const data = await runFFmpeg(args);
|
|
118
147
|
return { data, mimeType: "audio/pcm" };
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
__name(applyPitch, "applyPitch");
|
|
122
151
|
function apply(ctx, config) {
|
|
123
|
-
|
|
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) => {
|
|
124
160
|
if (!trigger) {
|
|
125
161
|
return "至少给个名字吧大王";
|
|
126
162
|
}
|
|
@@ -130,7 +166,7 @@ function apply(ctx, config) {
|
|
|
130
166
|
return `没有这种音频`;
|
|
131
167
|
}
|
|
132
168
|
try {
|
|
133
|
-
const { data, mimeType } = await applyPitch(audioPath, actualPitch, config.audioType);
|
|
169
|
+
const { data, mimeType } = await applyPitch(audioPath, actualPitch, config.audioType, config.keepSoundLength);
|
|
134
170
|
if (config.audioType === "silk") {
|
|
135
171
|
if (ctx.silk) {
|
|
136
172
|
const silkResult = await ctx.silk.encode(data, 24e3);
|