mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
feat: add voice ducking
This commit is contained in:
1 parent
b51b5a2317
commit
97e8a87305
17 files changed
+1550
-10
No files matched your search
@@ -48,6 +48,79 @@ describe("config", () => {
|
||||
expect(config).toEqual(getDefaultConfig());
|
||||
});
|
||||
|
||||
it("defaults voice ducking to disabled at 30 percent", () => {
|
||||
expect(getDefaultConfig().voiceDucking).toEqual({
|
||||
enabled: false,
|
||||
volumePercent: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it("fills voiceDucking defaults for legacy and partial configs", () => {
|
||||
const dir = makeTmpDir();
|
||||
const legacyPath = join(dir, "legacy.json");
|
||||
writeFileSync(legacyPath, JSON.stringify({ webPort: 4000 }));
|
||||
expect(loadConfig(legacyPath).voiceDucking).toEqual({
|
||||
enabled: false,
|
||||
volumePercent: 30,
|
||||
});
|
||||
|
||||
const partialPath = join(dir, "partial.json");
|
||||
writeFileSync(partialPath, JSON.stringify({ voiceDucking: { enabled: true } }));
|
||||
expect(loadConfig(partialPath).voiceDucking).toEqual({
|
||||
enabled: true,
|
||||
volumePercent: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it("loadConfig preserves valid voiceDucking values including range endpoints", () => {
|
||||
const dir = makeTmpDir();
|
||||
for (const volumePercent of [0, 37.5, 100]) {
|
||||
const path = join(dir, `voice-ducking-${volumePercent}.json`);
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify({ voiceDucking: { enabled: true, volumePercent } }),
|
||||
);
|
||||
expect(loadConfig(path).voiceDucking).toEqual({ enabled: true, volumePercent });
|
||||
}
|
||||
});
|
||||
|
||||
it("loadConfig strictly sanitizes malformed voiceDucking values", () => {
|
||||
const dir = makeTmpDir();
|
||||
const malformed: Array<{ name: string; json: string }> = [
|
||||
{ name: "null-block", json: JSON.stringify({ voiceDucking: null }) },
|
||||
{ name: "array-block", json: JSON.stringify({ voiceDucking: [true, 10] }) },
|
||||
{ name: "string-block", json: JSON.stringify({ voiceDucking: "on" }) },
|
||||
{
|
||||
name: "wrong-types",
|
||||
json: JSON.stringify({ voiceDucking: { enabled: "yes", volumePercent: "25" } }),
|
||||
},
|
||||
{
|
||||
name: "below-range",
|
||||
json: JSON.stringify({ voiceDucking: { enabled: true, volumePercent: -1 } }),
|
||||
},
|
||||
{
|
||||
name: "above-range",
|
||||
json: JSON.stringify({ voiceDucking: { enabled: true, volumePercent: 101 } }),
|
||||
},
|
||||
// JSON.parse("1e309") produces Infinity, exercising the finite-number guard.
|
||||
{
|
||||
name: "non-finite",
|
||||
json: '{"voiceDucking":{"enabled":true,"volumePercent":1e309}}',
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of malformed) {
|
||||
const path = join(dir, `${testCase.name}.json`);
|
||||
writeFileSync(path, testCase.json);
|
||||
const loaded = loadConfig(path).voiceDucking;
|
||||
if (testCase.name === "below-range" || testCase.name === "above-range" || testCase.name === "non-finite") {
|
||||
expect(loaded).toEqual({ enabled: true, volumePercent: 30 });
|
||||
} else {
|
||||
expect(loaded).toEqual({ enabled: false, volumePercent: 30 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults to the online sources with jellyfin as opt-in (disabled)", () => {
|
||||
const config = getDefaultConfig();
|
||||
expect(config.enabledProviders).toEqual(["netease", "qq", "bilibili", "youtube", "kugou"]);
|
||||
|
||||
@@ -54,6 +54,12 @@ export interface AudioQualityConfig {
|
||||
jellyfin: string;
|
||||
}
|
||||
|
||||
export interface VoiceDuckingConfig {
|
||||
enabled: boolean;
|
||||
/** Percentage of the normal playback volume retained while someone speaks. */
|
||||
volumePercent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Providers gated by `enabledProviders`. Not listed here:
|
||||
* - "local" → governed by the existing `localAudioEnabled` flag
|
||||
@@ -113,6 +119,8 @@ export interface BotConfig {
|
||||
adminGroups: number[];
|
||||
autoReturnDelay: number;
|
||||
autoPauseOnEmpty: boolean;
|
||||
/** Lower music volume while voice from another client is being received. */
|
||||
voiceDucking: VoiceDuckingConfig;
|
||||
idleTimeoutMinutes: number;
|
||||
/** Enable uploading and playback of server-stored local audio files. */
|
||||
localAudioEnabled: boolean;
|
||||
@@ -176,6 +184,10 @@ export function getDefaultConfig(): BotConfig {
|
||||
// command, which is unreliable on some servers (it can time out when other
|
||||
// clients are present). Users can opt in from the web UI.
|
||||
autoPauseOnEmpty: false,
|
||||
voiceDucking: {
|
||||
enabled: false,
|
||||
volumePercent: 30,
|
||||
},
|
||||
idleTimeoutMinutes: 0,
|
||||
localAudioEnabled: true,
|
||||
savedQueuesEnabled: false,
|
||||
@@ -383,6 +395,31 @@ export function loadConfig(path: string): BotConfig {
|
||||
const savedQueuesEnabled = partial.savedQueuesEnabled === true;
|
||||
const playKeepsQueue = partial.playKeepsQueue === true;
|
||||
|
||||
// Voice ducking is opt-in and the retained-volume percentage is consumed
|
||||
// directly by the audio path. Only a plain-object block with correctly
|
||||
// typed, finite and in-range fields may override the safe defaults.
|
||||
const rawVoiceDucking = partial.voiceDucking;
|
||||
const partialVoiceDucking =
|
||||
rawVoiceDucking !== null &&
|
||||
typeof rawVoiceDucking === "object" &&
|
||||
!Array.isArray(rawVoiceDucking)
|
||||
? (rawVoiceDucking as Partial<VoiceDuckingConfig>)
|
||||
: {};
|
||||
const rawVolumePercent = partialVoiceDucking.volumePercent;
|
||||
const voiceDucking: VoiceDuckingConfig = {
|
||||
enabled:
|
||||
typeof partialVoiceDucking.enabled === "boolean"
|
||||
? partialVoiceDucking.enabled
|
||||
: defaults.voiceDucking.enabled,
|
||||
volumePercent:
|
||||
typeof rawVolumePercent === "number" &&
|
||||
Number.isFinite(rawVolumePercent) &&
|
||||
rawVolumePercent >= 0 &&
|
||||
rawVolumePercent <= 100
|
||||
? rawVolumePercent
|
||||
: defaults.voiceDucking.volumePercent,
|
||||
};
|
||||
|
||||
// defaultPlatform → an explicit operator default (issue #126). Keep it only
|
||||
// when it names a KNOWN gateable provider that is ALSO currently enabled;
|
||||
// anything else (unknown value, disabled source, wrong type, missing) becomes
|
||||
@@ -420,6 +457,7 @@ export function loadConfig(path: string): BotConfig {
|
||||
enabledProviders,
|
||||
savedQueuesEnabled,
|
||||
playKeepsQueue,
|
||||
voiceDucking,
|
||||
defaultPlatform: defaultPlatformPref,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user