@raphiiko/wavelink-cli 0.0.5 → 0.0.7
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/LICENSE +21 -0
- package/README.md +45 -29
- package/dist/index.js +4891 -4793
- package/package.json +57 -57
- package/src/commands/channel.ts +243 -0
- package/src/commands/info.ts +19 -0
- package/src/commands/input.ts +128 -0
- package/src/commands/mix.ts +98 -0
- package/src/commands/output.ts +207 -0
- package/src/index.ts +14 -754
- package/src/services/client.ts +24 -0
- package/src/services/finders.ts +150 -0
- package/src/types/index.ts +23 -0
- package/src/utils/error.ts +4 -0
- package/src/utils/format.ts +15 -0
- package/src/utils/validation.ts +9 -0
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@raphiiko/wavelink-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Command line interface for Elgato Wave Link 3.0",
|
|
5
|
-
"author": "Raphiiko",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/Raphiiko/wavelink-cli",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/Raphiiko/wavelink-cli.git"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/Raphiiko/wavelink-cli/issues"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"wavelink",
|
|
17
|
-
"cli",
|
|
18
|
-
"typescript"
|
|
19
|
-
],
|
|
20
|
-
"main": "dist/index.js",
|
|
21
|
-
"module": "dist/index.js",
|
|
22
|
-
"type": "module",
|
|
23
|
-
"bin": {
|
|
24
|
-
"wavelink-cli": "dist/index.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"dist",
|
|
28
|
-
"src"
|
|
29
|
-
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "bun
|
|
32
|
-
"start": "bun run src/index.ts",
|
|
33
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
34
|
-
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
35
|
-
"lint": "eslint src/**/*.ts",
|
|
36
|
-
"lint:fix": "eslint src/**/*.ts --fix",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"check": "bun run format:check && bun run lint && bun run typecheck",
|
|
39
|
-
"prepublishOnly": "bun run build && bun run check"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@types/bun": "latest",
|
|
43
|
-
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
44
|
-
"@typescript-eslint/parser": "^8.52.0",
|
|
45
|
-
"eslint": "^9.39.2",
|
|
46
|
-
"eslint-config-prettier": "^10.1.8",
|
|
47
|
-
"eslint-plugin-prettier": "^5.5.4",
|
|
48
|
-
"prettier": "^3.7.4",
|
|
49
|
-
"typescript": "^5"
|
|
50
|
-
},
|
|
51
|
-
"peerDependencies": {
|
|
52
|
-
"typescript": "^5"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@raphiiko/wavelink-ts": "^1.
|
|
56
|
-
"commander": "^14.0.2"
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@raphiiko/wavelink-cli",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Command line interface for Elgato Wave Link 3.0",
|
|
5
|
+
"author": "Raphiiko",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/Raphiiko/wavelink-cli",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Raphiiko/wavelink-cli.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Raphiiko/wavelink-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"wavelink",
|
|
17
|
+
"cli",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"module": "dist/index.js",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"wavelink-cli": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "bun run scripts/build.js",
|
|
32
|
+
"start": "bun run src/index.ts",
|
|
33
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
34
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
35
|
+
"lint": "eslint src/**/*.ts",
|
|
36
|
+
"lint:fix": "eslint src/**/*.ts --fix",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"check": "bun run format:check && bun run lint && bun run typecheck",
|
|
39
|
+
"prepublishOnly": "bun run build && bun run check"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
45
|
+
"eslint": "^9.39.2",
|
|
46
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
48
|
+
"prettier": "^3.7.4",
|
|
49
|
+
"typescript": "^5"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "^5"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@raphiiko/wavelink-ts": "^1.3.0",
|
|
56
|
+
"commander": "^14.0.2"
|
|
57
|
+
}
|
|
58
58
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { WaveLinkClient } from "@raphiiko/wavelink-ts";
|
|
2
|
+
import { Argument, Command } from "commander";
|
|
3
|
+
import { withClient } from "../services/client.js";
|
|
4
|
+
import { requireChannel, requireMix } from "../services/finders.js";
|
|
5
|
+
import { formatPercent, formatMuted, getChannelName } from "../utils/format.js";
|
|
6
|
+
import { parsePercent } from "../utils/validation.js";
|
|
7
|
+
import { exitWithError } from "../utils/error.js";
|
|
8
|
+
|
|
9
|
+
export async function listChannels(client: WaveLinkClient): Promise<void> {
|
|
10
|
+
const { channels } = await client.getChannels();
|
|
11
|
+
|
|
12
|
+
console.log("\n=== Channels ===\n");
|
|
13
|
+
|
|
14
|
+
if (channels.length === 0) {
|
|
15
|
+
console.log("No channels found.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const channel of channels) {
|
|
20
|
+
const channelName = getChannelName(channel);
|
|
21
|
+
console.log(`Channel: ${channelName}`);
|
|
22
|
+
console.log(` ID: ${channel.id}`);
|
|
23
|
+
console.log(` Type: ${channel.type}`);
|
|
24
|
+
console.log(` Level: ${formatPercent(channel.level)}`);
|
|
25
|
+
console.log(` Muted: ${formatMuted(channel.isMuted)}`);
|
|
26
|
+
|
|
27
|
+
if (channel.apps?.length) {
|
|
28
|
+
console.log(` Apps: ${channel.apps.map((a) => a.name || a.id).join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (channel.mixes?.length) {
|
|
32
|
+
console.log(" Mix Assignments:");
|
|
33
|
+
for (const mix of channel.mixes) {
|
|
34
|
+
console.log(
|
|
35
|
+
` ${mix.id}: Level ${formatPercent(mix.level)}, Muted: ${formatMuted(mix.isMuted)}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerChannelCommands(program: Command): void {
|
|
44
|
+
const channelCmd = program.command("channel").description("Manage channels");
|
|
45
|
+
|
|
46
|
+
channelCmd
|
|
47
|
+
.command("list")
|
|
48
|
+
.description("List all channels with their IDs and names")
|
|
49
|
+
.action(() => withClient(listChannels));
|
|
50
|
+
|
|
51
|
+
channelCmd
|
|
52
|
+
.command("set-volume")
|
|
53
|
+
.description("Set channel master volume")
|
|
54
|
+
.addArgument(
|
|
55
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
56
|
+
)
|
|
57
|
+
.addArgument(new Argument("<volume>", "Volume level (0-100)"))
|
|
58
|
+
.action((channelId: string, volume: string) => {
|
|
59
|
+
const volumePercent = parsePercent(volume, "Volume");
|
|
60
|
+
return withClient(async (client) => {
|
|
61
|
+
const channel = await requireChannel(client, channelId);
|
|
62
|
+
await client.setChannelVolume(channel.id, volumePercent / 100);
|
|
63
|
+
console.log(`Successfully set channel '${channel.name}' volume to ${volumePercent}%`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
channelCmd
|
|
68
|
+
.command("mute")
|
|
69
|
+
.description("Mute a channel")
|
|
70
|
+
.addArgument(
|
|
71
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
72
|
+
)
|
|
73
|
+
.action((channelId: string) =>
|
|
74
|
+
withClient(async (client) => {
|
|
75
|
+
const channel = await requireChannel(client, channelId);
|
|
76
|
+
await client.setChannelMute(channel.id, true);
|
|
77
|
+
console.log(`Successfully muted channel '${channel.name}'`);
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
channelCmd
|
|
82
|
+
.command("unmute")
|
|
83
|
+
.description("Unmute a channel")
|
|
84
|
+
.addArgument(
|
|
85
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
86
|
+
)
|
|
87
|
+
.action((channelId: string) =>
|
|
88
|
+
withClient(async (client) => {
|
|
89
|
+
const channel = await requireChannel(client, channelId);
|
|
90
|
+
await client.setChannelMute(channel.id, false);
|
|
91
|
+
console.log(`Successfully unmuted channel '${channel.name}'`);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
channelCmd
|
|
96
|
+
.command("toggle-mute")
|
|
97
|
+
.description("Toggle channel mute state")
|
|
98
|
+
.addArgument(
|
|
99
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
100
|
+
)
|
|
101
|
+
.action((channelId: string) =>
|
|
102
|
+
withClient(async (client) => {
|
|
103
|
+
const channel = await requireChannel(client, channelId);
|
|
104
|
+
await client.toggleChannelMute(channel.id);
|
|
105
|
+
console.log(`Successfully toggled mute for channel '${channel.name}'`);
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
channelCmd
|
|
110
|
+
.command("set-mix-volume")
|
|
111
|
+
.description("Set channel volume in a specific mix")
|
|
112
|
+
.addArgument(
|
|
113
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
114
|
+
)
|
|
115
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
116
|
+
.addArgument(new Argument("<volume>", "Volume level (0-100)"))
|
|
117
|
+
.action((channelId: string, mixId: string, volume: string) => {
|
|
118
|
+
const volumePercent = parsePercent(volume, "Volume");
|
|
119
|
+
return withClient(async (client) => {
|
|
120
|
+
const channel = await requireChannel(client, channelId);
|
|
121
|
+
const mix = await requireMix(client, mixId);
|
|
122
|
+
await client.setChannelMixVolume(channel.id, mix.id, volumePercent / 100);
|
|
123
|
+
console.log(
|
|
124
|
+
`Successfully set channel '${channel.name}' volume to ${volumePercent}% in mix '${mix.name}'`
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
channelCmd
|
|
130
|
+
.command("mute-in-mix")
|
|
131
|
+
.description("Mute a channel in a specific mix")
|
|
132
|
+
.addArgument(
|
|
133
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
134
|
+
)
|
|
135
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
136
|
+
.action((channelId: string, mixId: string) =>
|
|
137
|
+
withClient(async (client) => {
|
|
138
|
+
const channel = await requireChannel(client, channelId);
|
|
139
|
+
const mix = await requireMix(client, mixId);
|
|
140
|
+
await client.setChannelMixMute(channel.id, mix.id, true);
|
|
141
|
+
console.log(`Successfully muted channel '${channel.name}' in mix '${mix.name}'`);
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
channelCmd
|
|
146
|
+
.command("unmute-in-mix")
|
|
147
|
+
.description("Unmute a channel in a specific mix")
|
|
148
|
+
.addArgument(
|
|
149
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
150
|
+
)
|
|
151
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
152
|
+
.action((channelId: string, mixId: string) =>
|
|
153
|
+
withClient(async (client) => {
|
|
154
|
+
const channel = await requireChannel(client, channelId);
|
|
155
|
+
const mix = await requireMix(client, mixId);
|
|
156
|
+
await client.setChannelMixMute(channel.id, mix.id, false);
|
|
157
|
+
console.log(`Successfully unmuted channel '${channel.name}' in mix '${mix.name}'`);
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
channelCmd
|
|
162
|
+
.command("toggle-mute-in-mix")
|
|
163
|
+
.description("Toggle channel mute state in a specific mix")
|
|
164
|
+
.addArgument(
|
|
165
|
+
new Argument("<channel-id-or-name>", "ID or name of the channel (case-insensitive)")
|
|
166
|
+
)
|
|
167
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
168
|
+
.action((channelId: string, mixId: string) =>
|
|
169
|
+
withClient(async (client) => {
|
|
170
|
+
const channel = await requireChannel(client, channelId);
|
|
171
|
+
const mix = await requireMix(client, mixId);
|
|
172
|
+
|
|
173
|
+
// Get full channel info to check mix assignment
|
|
174
|
+
const { channels } = await client.getChannels();
|
|
175
|
+
const fullChannel = channels.find((c) => c.id === channel.id);
|
|
176
|
+
|
|
177
|
+
if (!fullChannel) exitWithError(`Channel '${channelId}' not found`);
|
|
178
|
+
|
|
179
|
+
const mixAssignment = fullChannel.mixes?.find((m) => m.id === mix.id);
|
|
180
|
+
|
|
181
|
+
if (!mixAssignment) {
|
|
182
|
+
exitWithError(`Channel '${channel.name}' is not available in mix '${mix.name}'`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const newMuted = !mixAssignment.isMuted;
|
|
186
|
+
await client.setChannelMixMute(channel.id, mix.id, newMuted);
|
|
187
|
+
console.log(`Successfully toggled mute for channel '${channel.name}' in mix '${mix.name}'`);
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
channelCmd
|
|
192
|
+
.command("isolate")
|
|
193
|
+
.description("Mute all channels in a mix except for the specified one")
|
|
194
|
+
.addArgument(
|
|
195
|
+
new Argument(
|
|
196
|
+
"<channel-id-or-name>",
|
|
197
|
+
"ID or name of the channel to isolate (case-insensitive)"
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
201
|
+
.action((channelId: string, mixId: string) =>
|
|
202
|
+
withClient(async (client) => {
|
|
203
|
+
const mix = await requireMix(client, mixId);
|
|
204
|
+
const targetChannel = await requireChannel(client, channelId);
|
|
205
|
+
const { channels } = await client.getChannels();
|
|
206
|
+
|
|
207
|
+
let mutedCount = 0;
|
|
208
|
+
let alreadyMutedCount = 0;
|
|
209
|
+
|
|
210
|
+
for (const channel of channels) {
|
|
211
|
+
// Find the channel's assignment for this mix
|
|
212
|
+
const assignment = channel.mixes?.find((m) => m.id === mix.id);
|
|
213
|
+
|
|
214
|
+
// If the channel isn't in this mix (unexpected for Wave Link, but possible in API types), skip
|
|
215
|
+
if (!assignment) continue;
|
|
216
|
+
|
|
217
|
+
if (channel.id === targetChannel.id) {
|
|
218
|
+
// This is the chosen channel: Unmute it
|
|
219
|
+
if (assignment.isMuted) {
|
|
220
|
+
await client.setChannelMixMute(channel.id, mix.id, false);
|
|
221
|
+
console.log(`Unmuted target channel '${getChannelName(channel)}'`);
|
|
222
|
+
} else {
|
|
223
|
+
console.log(`Target channel '${getChannelName(channel)}' is already unmuted`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
// This is NOT the chosen channel: Mute it
|
|
227
|
+
if (!assignment.isMuted) {
|
|
228
|
+
await client.setChannelMixMute(channel.id, mix.id, true);
|
|
229
|
+
mutedCount++;
|
|
230
|
+
} else {
|
|
231
|
+
alreadyMutedCount++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(
|
|
237
|
+
`SUCCESS: Isolated '${targetChannel.name}' in mix '${mix.name}'.\n` +
|
|
238
|
+
` - Muted ${mutedCount} other channels.\n` +
|
|
239
|
+
` - ${alreadyMutedCount} channels were already muted.`
|
|
240
|
+
);
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WaveLinkClient } from "@raphiiko/wavelink-ts";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { withClient } from "../services/client.js";
|
|
4
|
+
|
|
5
|
+
export async function showApplicationInfo(client: WaveLinkClient): Promise<void> {
|
|
6
|
+
const info = await client.getApplicationInfo();
|
|
7
|
+
console.log("\n=== Wave Link Application Info ===\n");
|
|
8
|
+
console.log(`Application ID: ${info.appID}`);
|
|
9
|
+
console.log(`Name: ${info.name}`);
|
|
10
|
+
console.log(`Interface Revision: ${info.interfaceRevision}`);
|
|
11
|
+
console.log();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerInfoCommand(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command("info")
|
|
17
|
+
.description("Show Wave Link application information")
|
|
18
|
+
.action(() => withClient(showApplicationInfo));
|
|
19
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { WaveLinkClient } from "@raphiiko/wavelink-ts";
|
|
2
|
+
import { Argument, Command } from "commander";
|
|
3
|
+
import { withClient } from "../services/client.js";
|
|
4
|
+
import { requireInput } from "../services/finders.js";
|
|
5
|
+
import { formatPercent, formatMuted } from "../utils/format.js";
|
|
6
|
+
import { parsePercent } from "../utils/validation.js";
|
|
7
|
+
|
|
8
|
+
export async function setInputGain(
|
|
9
|
+
client: WaveLinkClient,
|
|
10
|
+
inputId: string,
|
|
11
|
+
gainPercent: number
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const input = await requireInput(client, inputId);
|
|
14
|
+
await client.setInputGain(input.deviceId, input.inputId, gainPercent / 100);
|
|
15
|
+
console.log(`Successfully set input '${input.inputName}' gain to ${gainPercent}%`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function setInputMute(
|
|
19
|
+
client: WaveLinkClient,
|
|
20
|
+
inputId: string,
|
|
21
|
+
isMuted: boolean
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const input = await requireInput(client, inputId);
|
|
24
|
+
await client.setInputMute(input.deviceId, input.inputId, isMuted);
|
|
25
|
+
console.log(`Successfully ${isMuted ? "muted" : "unmuted"} input '${input.inputName}'`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function listInputs(client: WaveLinkClient): Promise<void> {
|
|
29
|
+
const { inputDevices } = await client.getInputDevices();
|
|
30
|
+
|
|
31
|
+
console.log("\n=== Input Devices ===\n");
|
|
32
|
+
|
|
33
|
+
if (inputDevices.length === 0) {
|
|
34
|
+
console.log("No input devices found.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const device of inputDevices) {
|
|
39
|
+
console.log(`Device: ${device.name || device.id}`);
|
|
40
|
+
console.log(` Device ID: ${device.id}`);
|
|
41
|
+
console.log(` Wave Device: ${formatMuted(device.isWaveDevice)}`);
|
|
42
|
+
|
|
43
|
+
if (device.inputs.length === 0) {
|
|
44
|
+
console.log(" No inputs available");
|
|
45
|
+
} else {
|
|
46
|
+
for (const input of device.inputs) {
|
|
47
|
+
console.log(` Input: ${input.name || input.id}`);
|
|
48
|
+
console.log(` Input ID: ${input.id}`);
|
|
49
|
+
const gainMin = input.gain.min !== undefined ? formatPercent(input.gain.min) : "unknown";
|
|
50
|
+
const gainMax =
|
|
51
|
+
input.gain.max !== undefined
|
|
52
|
+
? formatPercent(input.gain.max)
|
|
53
|
+
: input.gain.maxRange !== undefined
|
|
54
|
+
? formatPercent(input.gain.maxRange)
|
|
55
|
+
: "unknown";
|
|
56
|
+
console.log(
|
|
57
|
+
` Gain: ${formatPercent(input.gain.value)} (min: ${gainMin}, max: ${gainMax})`
|
|
58
|
+
);
|
|
59
|
+
if (input.isGainLockOn !== undefined) {
|
|
60
|
+
console.log(` Gain Lock: ${formatMuted(input.isGainLockOn)}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(` Muted: ${formatMuted(input.isMuted)}`);
|
|
63
|
+
if (input.micPcMix) {
|
|
64
|
+
console.log(
|
|
65
|
+
` Mic/PC Mix: ${formatPercent(input.micPcMix.value)}${input.micPcMix.isInverted ? " (inverted)" : ""}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (input.effects?.length) {
|
|
69
|
+
const effectsList = input.effects
|
|
70
|
+
.map((e) => `${e.name || e.id} (${e.isEnabled ? "ON" : "OFF"})`)
|
|
71
|
+
.join(", ");
|
|
72
|
+
console.log(` Effects: ${effectsList}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function registerInputCommands(program: Command): void {
|
|
81
|
+
const inputCmd = program.command("input").description("Manage input devices");
|
|
82
|
+
|
|
83
|
+
inputCmd
|
|
84
|
+
.command("list")
|
|
85
|
+
.description("List all input devices with their IDs")
|
|
86
|
+
.action(() => withClient(listInputs));
|
|
87
|
+
|
|
88
|
+
inputCmd
|
|
89
|
+
.command("set-gain")
|
|
90
|
+
.description("Set input device gain")
|
|
91
|
+
.addArgument(
|
|
92
|
+
new Argument("<input-id-or-name>", "ID or name of the input device (case-insensitive)")
|
|
93
|
+
)
|
|
94
|
+
.addArgument(new Argument("<gain>", "Gain level (0-100)"))
|
|
95
|
+
.action((inputId: string, gain: string) => {
|
|
96
|
+
const gainPercent = parsePercent(gain, "Gain");
|
|
97
|
+
return withClient((client) => setInputGain(client, inputId, gainPercent));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
inputCmd
|
|
101
|
+
.command("mute")
|
|
102
|
+
.description("Mute an input device")
|
|
103
|
+
.addArgument(
|
|
104
|
+
new Argument("<input-id-or-name>", "ID or name of the input device (case-insensitive)")
|
|
105
|
+
)
|
|
106
|
+
.action((inputId: string) => withClient((client) => setInputMute(client, inputId, true)));
|
|
107
|
+
|
|
108
|
+
inputCmd
|
|
109
|
+
.command("unmute")
|
|
110
|
+
.description("Unmute an input device")
|
|
111
|
+
.addArgument(
|
|
112
|
+
new Argument("<input-id-or-name>", "ID or name of the input device (case-insensitive)")
|
|
113
|
+
)
|
|
114
|
+
.action((inputId: string) => withClient((client) => setInputMute(client, inputId, false)));
|
|
115
|
+
|
|
116
|
+
inputCmd
|
|
117
|
+
.command("toggle-mute")
|
|
118
|
+
.description("Toggle input device mute state")
|
|
119
|
+
.addArgument(
|
|
120
|
+
new Argument("<input-id-or-name>", "ID or name of the input device (case-insensitive)")
|
|
121
|
+
)
|
|
122
|
+
.action((inputId: string) =>
|
|
123
|
+
withClient(async (client) => {
|
|
124
|
+
const input = await requireInput(client, inputId);
|
|
125
|
+
await setInputMute(client, inputId, !input.isMuted);
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { WaveLinkClient } from "@raphiiko/wavelink-ts";
|
|
2
|
+
import { Argument, Command } from "commander";
|
|
3
|
+
import { withClient } from "../services/client.js";
|
|
4
|
+
import { requireMix } from "../services/finders.js";
|
|
5
|
+
import { formatPercent, formatMuted } from "../utils/format.js";
|
|
6
|
+
import { parsePercent } from "../utils/validation.js";
|
|
7
|
+
import { setSingleOutputForMix } from "./output.js";
|
|
8
|
+
|
|
9
|
+
export async function listMixes(client: WaveLinkClient): Promise<void> {
|
|
10
|
+
const { mixes } = await client.getMixes();
|
|
11
|
+
|
|
12
|
+
console.log("\n=== Mixes ===\n");
|
|
13
|
+
|
|
14
|
+
if (mixes.length === 0) {
|
|
15
|
+
console.log("No mixes found.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const mix of mixes) {
|
|
20
|
+
console.log(`Mix: ${mix.name}`);
|
|
21
|
+
console.log(` ID: ${mix.id}`);
|
|
22
|
+
console.log(` Level: ${formatPercent(mix.level)}`);
|
|
23
|
+
console.log(` Muted: ${formatMuted(mix.isMuted)}`);
|
|
24
|
+
console.log();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerMixCommands(program: Command): void {
|
|
29
|
+
const mixCmd = program.command("mix").description("Manage mixes");
|
|
30
|
+
|
|
31
|
+
mixCmd
|
|
32
|
+
.command("list")
|
|
33
|
+
.description("List all mixes with their IDs and names")
|
|
34
|
+
.action(() => withClient(listMixes));
|
|
35
|
+
|
|
36
|
+
mixCmd
|
|
37
|
+
.command("set-output")
|
|
38
|
+
.description(
|
|
39
|
+
"Set a device as the ONLY output for a mix (removes all other outputs from that mix)"
|
|
40
|
+
)
|
|
41
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
42
|
+
.addArgument(
|
|
43
|
+
new Argument("<output-id-or-name>", "ID or name of the output device (case-insensitive)")
|
|
44
|
+
)
|
|
45
|
+
.action((mixId: string, outputId: string) =>
|
|
46
|
+
withClient((client) => setSingleOutputForMix(client, outputId, mixId))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
mixCmd
|
|
50
|
+
.command("set-volume")
|
|
51
|
+
.description("Set mix master volume")
|
|
52
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
53
|
+
.addArgument(new Argument("<volume>", "Volume level (0-100)"))
|
|
54
|
+
.action((mixId: string, volume: string) => {
|
|
55
|
+
const volumePercent = parsePercent(volume, "Volume");
|
|
56
|
+
return withClient(async (client) => {
|
|
57
|
+
const mix = await requireMix(client, mixId);
|
|
58
|
+
await client.setMixVolume(mix.id, volumePercent / 100);
|
|
59
|
+
console.log(`Successfully set mix '${mix.name}' volume to ${volumePercent}%`);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
mixCmd
|
|
64
|
+
.command("mute")
|
|
65
|
+
.description("Mute a mix")
|
|
66
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
67
|
+
.action((mixId: string) =>
|
|
68
|
+
withClient(async (client) => {
|
|
69
|
+
const mix = await requireMix(client, mixId);
|
|
70
|
+
await client.setMixMute(mix.id, true);
|
|
71
|
+
console.log(`Successfully muted mix '${mix.name}'`);
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
mixCmd
|
|
76
|
+
.command("unmute")
|
|
77
|
+
.description("Unmute a mix")
|
|
78
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
79
|
+
.action((mixId: string) =>
|
|
80
|
+
withClient(async (client) => {
|
|
81
|
+
const mix = await requireMix(client, mixId);
|
|
82
|
+
await client.setMixMute(mix.id, false);
|
|
83
|
+
console.log(`Successfully unmuted mix '${mix.name}'`);
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
mixCmd
|
|
88
|
+
.command("toggle-mute")
|
|
89
|
+
.description("Toggle mix mute state")
|
|
90
|
+
.addArgument(new Argument("<mix-id-or-name>", "ID or name of the mix (case-insensitive)"))
|
|
91
|
+
.action((mixId: string) =>
|
|
92
|
+
withClient(async (client) => {
|
|
93
|
+
const mix = await requireMix(client, mixId);
|
|
94
|
+
await client.toggleMixMute(mix.id);
|
|
95
|
+
console.log(`Successfully toggled mute for mix '${mix.name}'`);
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|