├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 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 | --------------------------------------------------------------------------------