├── .eslintrc
├── .github
└── workflows
│ └── package.yml
├── .gitignore
├── .releaserc
├── PAGE.md
├── README.md
├── icon.svg
├── package-lock.json
├── package.json
├── plugin.json
├── scripts
└── bump-manifest.ts
├── src
├── api.ts
├── config.ts
└── index.ts
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2020": true
5 | },
6 | "extends": [
7 | "plugin:prettier/recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 2020,
14 | "sourceType": "module"
15 | },
16 | "plugins": ["@typescript-eslint", "prettier"],
17 | "rules": {
18 | "prettier/prettier": "warn"
19 | },
20 | "settings": {
21 | "import/resolver": {
22 | "node": {
23 | "extensions": [".js", ".ts"]
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/package.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - "main"
5 |
6 | jobs:
7 | build:
8 | name: Build and pack
9 | runs-on: windows-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - run: npm ci
13 | - run: npm run build
14 | - run: npx semantic-release
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 |
84 | # Gatsby files
85 | .cache/
86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # public
89 |
90 | # vuepress build output
91 | .vuepress/dist
92 |
93 | # Serverless directories
94 | .serverless/
95 |
96 | # FuseBox cache
97 | .fusebox/
98 |
99 | # DynamoDB Local files
100 | .dynamodb/
101 |
102 | # TernJS port file
103 | .tern-port
104 |
105 | # TypeScript output
106 | lib/
107 |
108 | # Plugin built output
109 | *.midiMixerPlugin
110 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer",
5 | "@semantic-release/release-notes-generator",
6 | ["@semantic-release/npm", {
7 | "npmPublish": false
8 | }],
9 | ["@semantic-release/github", {
10 | "assets": ["*.midiMixerPlugin"]
11 | }],
12 | ["@semantic-release/git", {
13 | "assets": ["package.json", "package-lock.json", "plugin.json"]
14 | }]
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/PAGE.md:
--------------------------------------------------------------------------------
1 | # Discord Plugin
2 |
3 | A proof-of-concept plugin that provides very basic Discord integration.
4 |
5 | ## Getting started
6 |
7 | Discord only lets "[self-bots](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-)" control your local Discord client, meaning you have to create your own Discord Application in order to use this plugin.
8 |
9 | 1. Download the latest `com.midi-mixer.discord-x.y.z.midiMixerPlugin` file from the [releases](https://github.com/midi-mixer/plugin-discord/releases) page and double-click it to install the plugin
10 | 2. Go to the [Discord Developer Portal](https://discordapp.com/developers) and create an application
11 | 3. Go to "OAuth2" on the left hand side
12 | 4. Add `http://localhost/` as a redirect URI
13 | 5. Copy the "Client ID" in to the plugin's settings page in MIDI Mixer
14 | 6. Copy the "Client Secret" in to the plugin's settings page in MIDI Mixer
15 | 7. Activate the plugin
16 | 8. Click "Authorize" on the prompt that appears, allowing the plugin access to information like voice settings
17 |
18 | If there's a problem, check the `Status` message on the plugin's settings page or run the plugin in dev mode to see what might be causing the issue.
19 |
20 | Once all this is done, you should be able to see the below faders and buttons available in MIDI Mixer:
21 |
22 | ### Faders
23 |
24 | - Input device
25 | - Controls your mic level
26 | - Mute button toggles mute
27 | - Output device
28 | - Controls incoming audio volume levels
29 | - Mute button toggle deafen
30 |
31 | ### Buttons
32 |
33 | - Toggle mute
34 | - Toggle deafen
35 | - Toggle automatic gain control
36 | - Toggle echo cancellation
37 | - Toggle noise reduction
38 | - Toggle Quality of Service High Packet Priority
39 | - Toggle silence warnings
40 | - Toggle push-to-talk / voice activity `SOON™`
41 | - Toggle automatic input sensitivity `SOON™`
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Discord Plugin
2 |
3 | A proof-of-concept plugin that provides very basic Discord integration.
4 |
5 | ## Getting started
6 |
7 | Discord only lets "[self-bots](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-)" control your local Discord client, meaning you have to create your own Discord Application in order to use this plugin.
8 |
9 | 1. Download the latest `com.midi-mixer.discord-x.y.z.midiMixerPlugin` file from the [releases](https://github.com/midi-mixer/plugin-discord/releases) page and double-click it to install the plugin
10 | 2. Go to the [Discord Developer Portal](https://discordapp.com/developers) and create an application
11 | 3. Go to "OAuth2" on the left hand side
12 | 4. Add `http://localhost/` as a redirect URI
13 | 5. Copy the "Client ID" in to the plugin's settings page in MIDI Mixer
14 | 6. Copy the "Client Secret" in to the plugin's settings page in MIDI Mixer
15 | 7. Activate the plugin
16 | 8. Click "Authorize" on the prompt that appears, allowing the plugin access to information like voice settings
17 |
18 | If there's a problem, check the `Status` message on the plugin's settings page or run the plugin in dev mode to see what might be causing the issue.
19 |
20 | Once all this is done, you should be able to see the below faders and buttons available in MIDI Mixer:
21 |
22 | ### Faders
23 |
24 | - Input device
25 | - Controls your mic level
26 | - Mute button toggles mute
27 | - Output device
28 | - Controls incoming audio volume levels
29 | - Mute button toggle deafen
30 |
31 | ### Buttons
32 |
33 | - Toggle mute
34 | - Toggle deafen
35 | - Toggle automatic gain control
36 | - Toggle echo cancellation
37 | - Toggle noise reduction
38 | - Toggle Quality of Service High Packet Priority
39 | - Toggle silence warnings
40 | - Toggle push-to-talk / voice activity `SOON™`
41 | - Toggle automatic input sensitivity `SOON™`
42 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.midi-mixer.discord",
3 | "version": "1.1.2",
4 | "private": true,
5 | "description": "A Discord plugin for MIDI Mixer.",
6 | "files": [
7 | "icon.svg",
8 | "PAGE.md",
9 | "plugin.json",
10 | "lib"
11 | ],
12 | "scripts": {
13 | "test": "echo \"Error: no test specified\" && exit 1",
14 | "build": "tsc",
15 | "semantic-release": "semantic-release",
16 | "postversion": "npx ts-node scripts/bump-manifest.ts && midi-mixer pack"
17 | },
18 | "license": "ISC",
19 | "devDependencies": {
20 | "@semantic-release/exec": "^5.0.0",
21 | "@semantic-release/git": "^9.0.0",
22 | "@types/discord-rpc": "^3.0.5",
23 | "@types/node": "^15.12.1",
24 | "@typescript-eslint/eslint-plugin": "^4.14.2",
25 | "@typescript-eslint/parser": "^4.14.2",
26 | "eslint": "^7.19.0",
27 | "eslint-config-prettier": "^7.2.0",
28 | "eslint-plugin-import": "^2.22.1",
29 | "eslint-plugin-prettier": "^3.3.1",
30 | "midi-mixer-cli": "^1.0.1",
31 | "prettier": "^2.2.1",
32 | "semantic-release": "^17.4.3",
33 | "typescript": "^4.1.3"
34 | },
35 | "dependencies": {
36 | "conf": "^10.0.1",
37 | "discord-rpc": "^3.2.0",
38 | "midi-mixer-plugin": "^0.3.0"
39 | },
40 | "bundledDependencies": [
41 | "conf",
42 | "discord-rpc",
43 | "midi-mixer-plugin"
44 | ],
45 | "volta": {
46 | "node": "14.15.4",
47 | "npm": "6.14.11"
48 | },
49 | "repository": {
50 | "type": "git",
51 | "url": "https://github.com/midi-mixer/plugin-discord.git"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/midi-mixer-plugin/plugin.schema.json",
3 | "version": "1.1.2",
4 | "id": "com.midi-mixer.discord",
5 | "name": "Discord",
6 | "type": "node",
7 | "main": "lib/index.js",
8 | "icon": "icon.svg",
9 | "settings": {
10 | "clientId": {
11 | "label": "Client ID",
12 | "required": true,
13 | "type": "text"
14 | },
15 | "clientSecret": {
16 | "label": "Client Secret",
17 | "required": true,
18 | "type": "password"
19 | },
20 | "presence": {
21 | "label": "Presence (populate to disable)",
22 | "type": "text",
23 | "fallback": "On"
24 | },
25 | "status": {
26 | "label": "Status",
27 | "type": "status",
28 | "fallback": "Disconnected"
29 | },
30 | "reconnect": {
31 | "label": "Reconnect",
32 | "type": "button"
33 | }
34 | },
35 | "author": "MIDI Mixer"
36 | }
--------------------------------------------------------------------------------
/scripts/bump-manifest.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from "fs";
2 | import { version } from "../package.json";
3 | import manifest from "../plugin.json";
4 |
5 | writeFileSync("plugin.json", JSON.stringify({ ...manifest, version }, null, 2));
6 |
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import RPC, { VoiceSettings } from "discord-rpc";
2 | import { Assignment, ButtonType } from "midi-mixer-plugin";
3 | import { config, Keys } from "./config";
4 |
5 | enum DiscordButton {
6 | ToggleAutomaticGainControl = "toggleAutomaticGainControl",
7 | ToggleEchoCancellation = "toggleEchoCancellation",
8 | ToggleNoiseSuppression = "toggleNoiseSuppression",
9 | ToggleQos = "toggleQos",
10 | ToggleSilenceWarning = "toggleSilenceWarning",
11 | ToggleDeafen = "toggleDeafen",
12 | ToggleMute = "toggleMute",
13 | // TogglePushToTalk = "togglePushToTalk",
14 | // ToggleAutoThreshold = "toggleAutoThreshold",
15 | }
16 |
17 | enum DiscordFader {
18 | InputVolume = "inputVolume",
19 | OutputVolume = "outputVolume",
20 | // VoiceActivityThreshold = "voiceActivityThreshold",
21 | }
22 |
23 | export class DiscordApi {
24 | private static scopes = [
25 | "rpc",
26 | "rpc.activities.write",
27 | "rpc.voice.read",
28 | "rpc.voice.write",
29 | ];
30 | private static syncGap = 1000 * 30;
31 |
32 | private rpc: RPC.Client;
33 | private clientId: string;
34 | private clientSecret: string;
35 | private buttons: Record | null = null;
36 | private faders: Record | null = null;
37 | private syncInterval: ReturnType | null = null;
38 | private settings?: RPC.VoiceSettings;
39 |
40 | constructor(rpc: RPC.Client, clientId: string, clientSecret: string) {
41 | this.rpc = rpc;
42 | this.clientId = clientId;
43 | this.clientSecret = clientSecret;
44 | }
45 |
46 | public async disconnect(): Promise {
47 | await this.rpc.destroy();
48 |
49 | if (this.syncInterval) clearInterval(this.syncInterval);
50 |
51 | Object.values(this.buttons ?? {}).forEach((button) => void button.remove());
52 | Object.values(this.faders ?? {}).forEach((fader) => void fader.remove());
53 | }
54 |
55 | public async bootstrap(): Promise {
56 | $MM.setSettingsStatus("status", "Connecting to Discord...");
57 |
58 | let authToken = "";
59 |
60 | try {
61 | [authToken] = await Promise.all([
62 | new Promise((resolve, reject) => {
63 | try {
64 | resolve(config.get(Keys.AuthToken) as string);
65 | } catch (err) {
66 | reject(err);
67 | }
68 | }),
69 | this.rpc.connect(this.clientId),
70 | ]);
71 | } catch (err) {
72 | console.error(err);
73 |
74 | $MM.setSettingsStatus(
75 | "status",
76 | "Disconnected; couldn't find Discord running"
77 | );
78 |
79 | throw err;
80 | }
81 |
82 | if (authToken && typeof authToken === "string") {
83 | $MM.setSettingsStatus(
84 | "status",
85 | "Logging in with existing credentials..."
86 | );
87 |
88 | try {
89 | await this.rpc.login({
90 | clientId: this.clientId,
91 | accessToken: authToken,
92 | scopes: DiscordApi.scopes,
93 | });
94 | } catch (err) {
95 | console.warn(
96 | "Failed to authorise using existing token; stripping from config"
97 | );
98 |
99 | config.delete(Keys.AuthToken);
100 | }
101 | }
102 |
103 | const isAuthed = Boolean(this.rpc.application);
104 |
105 | if (!isAuthed) {
106 | try {
107 | await this.authorize();
108 | } catch (err) {
109 | console.error(err);
110 |
111 | return $MM.setSettingsStatus(
112 | "status",
113 | "User declined authorisation; cannot continue."
114 | );
115 | }
116 | }
117 |
118 | this.rpc.subscribe("VOICE_SETTINGS_UPDATE", (data: VoiceSettings) => {
119 | this.sync(data);
120 | });
121 |
122 | $MM.setSettingsStatus("status", "Syncing voice settings...");
123 | this.settings = await this.rpc.getVoiceSettings();
124 | $MM.setSettingsStatus("status", "Connected");
125 |
126 | this.buttons = {
127 | // [DiscordButton.ToggleAutoThreshold]: new ButtonType(
128 | // DiscordButton.ToggleAutoThreshold,
129 | // {
130 | // name: "Toggle auto threshold",
131 | // active: this.settings.mode.auto_threshold,
132 | // }
133 | // ).on("pressed", async () => {
134 | // if (!this.settings) return;
135 |
136 | // await this.rpc.setVoiceSettings({
137 | // mode: {
138 | // ...this.settings.mode,
139 | // auto_threshold: !this.settings?.mode?.auto_threshold,
140 | // },
141 | // });
142 | // }),
143 | [DiscordButton.ToggleMute]: new ButtonType(DiscordButton.ToggleMute, {
144 | name: "Toggle mute",
145 | active: Boolean(this.settings.mute),
146 | }).on("pressed", async () => {
147 | const currentState = Boolean(
148 | this.settings?.mute || this.settings?.deaf
149 | );
150 | const muted = !currentState;
151 |
152 | /**
153 | * If we're unmuting our mic, make sure to undeafen too.
154 | */
155 | const args: Partial = {
156 | mute: muted,
157 | };
158 |
159 | if (!muted) args.deaf = muted;
160 |
161 | await (this.rpc as any).setVoiceSettings(args);
162 | }),
163 | [DiscordButton.ToggleDeafen]: new ButtonType(DiscordButton.ToggleDeafen, {
164 | name: "Toggle deafen",
165 | active: Boolean(this.settings.deaf),
166 | }).on("pressed", async () => {
167 | await (this.rpc as any).setVoiceSettings({
168 | deaf: !this.settings?.deaf,
169 | });
170 | }),
171 | [DiscordButton.ToggleSilenceWarning]: new ButtonType(
172 | DiscordButton.ToggleSilenceWarning,
173 | {
174 | name: "Toggle muted mic warning",
175 | active: Boolean(
176 | this.settings.silenceWarning ??
177 | (this.settings as any).silence_warning
178 | ),
179 | }
180 | ).on("pressed", async () => {
181 | await (this.rpc as any).setVoiceSettings({
182 | silenceWarning: !(
183 | this.settings?.silenceWarning ??
184 | (this.settings as any).silence_warning
185 | ),
186 | });
187 | }),
188 | [DiscordButton.ToggleQos]: new ButtonType(DiscordButton.ToggleQos, {
189 | name: "Toggle QoS",
190 | active: Boolean(this.settings.qos),
191 | }).on("pressed", async () => {
192 | await (this.rpc as any).setVoiceSettings({
193 | qos: !this.settings?.qos,
194 | });
195 | }),
196 | [DiscordButton.ToggleNoiseSuppression]: new ButtonType(
197 | DiscordButton.ToggleNoiseSuppression,
198 | {
199 | name: "Toggle noise reduction",
200 | active: Boolean(
201 | this.settings.noiseSuppression ??
202 | (this.settings as any).noise_suppression
203 | ),
204 | }
205 | ).on("pressed", async () => {
206 | await (this.rpc as any).setVoiceSettings({
207 | noiseSuppression: !(
208 | this.settings?.noiseSuppression ??
209 | (this.settings as any).noise_suppression
210 | ),
211 | });
212 | }),
213 | [DiscordButton.ToggleEchoCancellation]: new ButtonType(
214 | DiscordButton.ToggleEchoCancellation,
215 | {
216 | name: "Toggle echo cancellation",
217 | active: Boolean(
218 | this.settings.echoCancellation ??
219 | (this.settings as any).echo_cancellation
220 | ),
221 | }
222 | ).on("pressed", async () => {
223 | await (this.rpc as any).setVoiceSettings({
224 | echoCancellation: !(
225 | this.settings?.echoCancellation ??
226 | (this.settings as any).echo_cancellation
227 | ),
228 | });
229 | }),
230 | [DiscordButton.ToggleAutomaticGainControl]: new ButtonType(
231 | DiscordButton.ToggleAutomaticGainControl,
232 | {
233 | name: "Toggle automatic gain control",
234 | active: Boolean(
235 | this.settings.automaticGainControl ??
236 | (this.settings as any).automatic_gain_control
237 | ),
238 | }
239 | ).on("pressed", async () => {
240 | await (this.rpc as any).setVoiceSettings({
241 | automaticGainControl: !(
242 | this.settings?.automaticGainControl ??
243 | (this.settings as any).automatic_gain_control
244 | ),
245 | });
246 | }),
247 | };
248 |
249 | this.faders = {
250 | [DiscordFader.InputVolume]: new Assignment(DiscordFader.InputVolume, {
251 | name: "Input device",
252 | muted: this.settings.mute,
253 | throttle: 150,
254 | })
255 | .on("mutePressed", async () => {
256 | const currentState = Boolean(
257 | this.settings?.mute || this.settings?.deaf
258 | );
259 | const muted = !currentState;
260 |
261 | /**
262 | * If we're unmuting our mic, make sure to undeafen too.
263 | */
264 | const args: Partial = {
265 | mute: muted,
266 | };
267 |
268 | if (!muted) args.deaf = muted;
269 |
270 | await (this.rpc as any).setVoiceSettings(args);
271 | })
272 | .on("volumeChanged", async (level: number) => {
273 | if (!this.faders) return;
274 |
275 | this.faders[DiscordFader.InputVolume].volume = level;
276 |
277 | await (this.rpc as any).setVoiceSettings({
278 | input: {
279 | volume: level * 100,
280 | },
281 | });
282 | }),
283 | [DiscordFader.OutputVolume]: new Assignment(DiscordFader.OutputVolume, {
284 | name: "Output device",
285 | muted: this.settings.deaf,
286 | throttle: 150,
287 | })
288 | .on("mutePressed", async () => {
289 | await (this.rpc as any).setVoiceSettings({
290 | deaf: !this.settings?.deaf,
291 | });
292 | })
293 | .on("volumeChanged", async (level: number) => {
294 | if (!this.faders) return;
295 |
296 | this.faders[DiscordFader.OutputVolume].volume = level;
297 |
298 | await (this.rpc as any).setVoiceSettings({
299 | output: {
300 | volume: level * 200,
301 | },
302 | });
303 | }),
304 | };
305 |
306 | this.syncInterval = setInterval(() => void this.sync(), DiscordApi.syncGap);
307 | this.sync();
308 | }
309 |
310 | /**
311 | * Authorize with Discord, providing scopes and requesting an access token for
312 | * future use.
313 | */
314 | private async authorize() {
315 | $MM.setSettingsStatus("status", "Waiting for user authorisation...");
316 |
317 | await this.rpc.login({
318 | clientId: this.clientId,
319 | clientSecret: this.clientSecret,
320 | scopes: DiscordApi.scopes,
321 | redirectUri: "http://localhost/",
322 | } as any);
323 |
324 | const accessToken = (this.rpc as any).accessToken;
325 |
326 | if (!accessToken)
327 | throw new Error("Logged in, but not access token available");
328 |
329 | config.set(Keys.AuthToken, accessToken);
330 | }
331 |
332 | private async sync(settings?: VoiceSettings) {
333 | this.settings = settings ?? (await this.rpc.getVoiceSettings());
334 | const boolSettings = this.normaliseSettings(this.settings);
335 |
336 | console.log("Syncing settings from:", this.settings);
337 |
338 | if (this.buttons) {
339 | this.buttons[DiscordButton.ToggleAutomaticGainControl].active =
340 | boolSettings.automaticGainControl;
341 |
342 | this.buttons[DiscordButton.ToggleEchoCancellation].active =
343 | boolSettings.echoCancellation;
344 |
345 | this.buttons[DiscordButton.ToggleNoiseSuppression].active =
346 | boolSettings.noiseSuppression;
347 |
348 | this.buttons[DiscordButton.ToggleQos].active = boolSettings.qos;
349 |
350 | this.buttons[DiscordButton.ToggleSilenceWarning].active =
351 | boolSettings.silenceWarning;
352 |
353 | this.buttons[DiscordButton.ToggleDeafen].active = boolSettings.deaf;
354 |
355 | this.buttons[DiscordButton.ToggleMute].active =
356 | boolSettings.mute || boolSettings.deaf;
357 | }
358 |
359 | if (this.faders) {
360 | this.faders[DiscordFader.InputVolume].muted =
361 | boolSettings.mute || boolSettings.deaf;
362 |
363 | if (this.settings.input) {
364 | this.faders[DiscordFader.InputVolume].volume =
365 | this.settings.input.volume / 100;
366 | }
367 |
368 | this.faders[DiscordFader.OutputVolume].muted = boolSettings.deaf;
369 |
370 | if (this.settings.output) {
371 | this.faders[DiscordFader.OutputVolume].volume =
372 | this.settings.output.volume / 200;
373 | }
374 | }
375 | }
376 |
377 | private normaliseSettings(
378 | rawSettings: Record
379 | ): Omit {
380 | return {
381 | ...rawSettings,
382 | automaticGainControl: Boolean(
383 | rawSettings.automaticGainControl ?? rawSettings.automatic_gain_control
384 | ),
385 | deaf: Boolean(rawSettings.deaf),
386 | echoCancellation: Boolean(
387 | rawSettings.echoCancellation ?? rawSettings.echo_cancellation
388 | ),
389 | mute: Boolean(rawSettings.mute),
390 | noiseSuppression: Boolean(
391 | rawSettings.noiseSuppression ?? rawSettings.noise_suppression
392 | ),
393 | qos: Boolean(rawSettings.qos),
394 | silenceWarning: Boolean(
395 | rawSettings.silenceWarning ?? rawSettings.silence_warning
396 | ),
397 | };
398 | }
399 | }
400 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import Conf from "conf";
2 |
3 | export const config = new Conf({
4 | configName: "com.midi-mixer.discord",
5 | });
6 |
7 | export enum Keys {
8 | AuthToken = "authToken",
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import RPC from "discord-rpc";
2 | import "midi-mixer-plugin";
3 | import { DiscordApi } from "./api";
4 |
5 | let midiMixerRpc: RPC.Client | null = null;
6 | let api: DiscordApi | null = null;
7 |
8 | const cleanUpConnections = async () => {
9 | await Promise.all([
10 | new Promise((resolve) => {
11 | if (!midiMixerRpc) return resolve();
12 |
13 | midiMixerRpc.destroy().finally(() => {
14 | midiMixerRpc = null;
15 | resolve();
16 | });
17 | }),
18 | new Promise((resolve) => {
19 | if (!api) return resolve();
20 |
21 | api.disconnect().finally(() => {
22 | api = null;
23 | resolve();
24 | });
25 | }),
26 | ]);
27 |
28 | $MM.setSettingsStatus("status", "Disconnected");
29 | };
30 |
31 | $MM.onClose(async () => {
32 | await cleanUpConnections();
33 | });
34 |
35 | const connectPresence = async () => {
36 | const midiMixerClientId = "802892683936268328";
37 |
38 | midiMixerRpc = new RPC.Client({
39 | transport: "ipc",
40 | });
41 |
42 | await midiMixerRpc.connect(midiMixerClientId);
43 |
44 | await midiMixerRpc.setActivity({
45 | details: "Controlling volumes",
46 | state: "Using MIDI",
47 | largeImageKey: "logo",
48 | largeImageText: "MIDI Mixer",
49 | buttons: [
50 | {
51 | label: "Get MIDI Mixer",
52 | url: "https://www.midi-mixer.com",
53 | },
54 | ],
55 | });
56 | };
57 |
58 | const connect = async () => {
59 | /**
60 | * Disconnect any running instances.
61 | */
62 | await cleanUpConnections();
63 |
64 | $MM.setSettingsStatus("status", "Getting plugin settings...");
65 | const settings = await $MM.getSettings();
66 |
67 | const clientId = settings.clientId as string;
68 | const clientSecret = settings.clientSecret as string;
69 | const presence = settings.presence as string;
70 |
71 | /**
72 | * A blank presence field means presence SHOULD HAPPEN.
73 | */
74 | const showPresence = !presence;
75 | if (showPresence) connectPresence();
76 |
77 | const clientIdValid = Boolean(clientId) && typeof clientId === "string";
78 | const clientSecretValid =
79 | Boolean(clientSecret) && typeof clientSecret === "string";
80 |
81 | if (!clientIdValid || !clientSecretValid) {
82 | return void $MM.setSettingsStatus(
83 | "status",
84 | "Error: No or incorrect Client ID or Client Secret."
85 | );
86 | }
87 |
88 | const rpc = new RPC.Client({
89 | transport: "ipc",
90 | });
91 |
92 | api = new DiscordApi(rpc, clientId, clientSecret);
93 |
94 | try {
95 | await api.bootstrap();
96 | } catch (err) {
97 | console.error(err);
98 | cleanUpConnections();
99 | }
100 | };
101 |
102 | $MM.onSettingsButtonPress("reconnect", connect);
103 | connect();
104 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/**/*"],
3 | "exclude": ["node_modules"],
4 | "compilerOptions": {
5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
6 |
7 | /* Basic Options */
8 | // "incremental": true, /* Enable incremental compilation */
9 | "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
10 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
11 | "lib": [
12 | "ES2020"
13 | ] /* Specify library files to be included in the compilation. */,
14 | // "allowJs": true, /* Allow javascript files to be compiled. */
15 | // "checkJs": true, /* Report errors in .js files. */
16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
17 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
18 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
19 | "sourceMap": true /* Generates corresponding '.map' file. */,
20 | // "outFile": "./", /* Concatenate and emit output to single file. */
21 | "outDir": "./lib" /* Redirect output structure to the directory. */,
22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
23 | // "composite": true, /* Enable project compilation */
24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
25 | // "removeComments": true, /* Do not emit comments to output. */
26 | "noEmit": false /* Do not emit outputs. */,
27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
30 |
31 | /* Strict Type-Checking Options */
32 | "strict": true /* Enable all strict type-checking options. */,
33 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
34 | // "strictNullChecks": true, /* Enable strict null checks. */
35 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
36 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
37 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
38 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
39 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
40 |
41 | /* Additional Checks */
42 | // "noUnusedLocals": true, /* Report errors on unused locals. */
43 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
44 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
45 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
46 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
47 |
48 | /* Module Resolution Options */
49 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
50 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
51 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
53 | // "typeRoots": [], /* List of folders to include type definitions from. */
54 | // "types": [], /* Type declaration files to be included in compilation. */
55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
56 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
59 |
60 | /* Source Map Options */
61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
65 |
66 | /* Experimental Options */
67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
69 |
70 | /* Advanced Options */
71 | "skipLibCheck": true /* Skip type checking of declaration files. */,
72 | "resolveJsonModule": true,
73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
74 | }
75 | }
76 |
--------------------------------------------------------------------------------