├── .github └── workflows │ └── publish-package.yaml ├── .gitignore ├── LICENSE ├── README.md ├── audio-napi.cc ├── binding.gyp ├── package.json ├── pnpm-lock.yaml ├── sample └── index.ts ├── src └── index.ts └── tsconfig.json /.github/workflows/publish-package.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | build: 6 | runs-on: windows-latest 7 | permissions: 8 | contents: read 9 | id-token: write 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: "20.x" 15 | registry-url: "https://registry.npmjs.org" 16 | - run: npm install -g npm pnpm 17 | - run: pnpm i 18 | - run: pnpm run build 19 | - run: npm publish --provenance --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | package-lock.json 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | dist 41 | build 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Francesco Cannizzaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # win-audio 2 | 3 | Get, set and watch speaker/microphone volumes on Windows 4 | 5 | [![Publish package to npmjs](https://github.com/fcannizzaro/win-audio/actions/workflows/publish-package.yaml/badge.svg)](https://github.com/fcannizzaro/win-audio/actions/workflows/publish-package.yaml) 6 | [![npm](https://img.shields.io/npm/v/win-audio.svg)](https://www.npmjs.com/package/win-audio) 7 | [![npm](https://img.shields.io/npm/dm/win-audio.svg)](https://www.npmjs.com/package/win-audio) 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm i --save win-audio 13 | # or 14 | yarn add win-audio 15 | # or 16 | pnpm add win-audio 17 | ``` 18 | 19 | ## Requirements 20 | 21 | [node-gyp](https://github.com/nodejs/node-gyp#installation) to build **audio-napi.cc** 22 | 23 | This version requires **N-API**, and **node** version **>= 8.6.0** 24 | 25 | ## Module 26 | 27 | ```typescript 28 | import audio from 'win-audio'; 29 | 30 | // manage speaker volume 31 | const speaker = audio.speaker; 32 | 33 | // manage mic volume 34 | const microphone = audio.mic; 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```javascript 40 | import audio from 'win-audio'; 41 | 42 | const speaker = audio.speaker; 43 | 44 | // start watching audio devices 45 | speaker.start(200); 46 | 47 | // listen for volume changes 48 | speaker.on('change', (volume) => { 49 | console.log("old %d%% -> new %d%%", volume.old, volume.new); 50 | }); 51 | 52 | // listen for mute changes 53 | speaker.on('toggle', (status) => { 54 | console.log("muted: %s -> %s", status.old, status.new); 55 | }); 56 | 57 | // set volume to 40% 58 | speaker.set(40); 59 | 60 | // increase volume by 20% 61 | speaker.increase(20); 62 | 63 | // decrease volume by 10% 64 | speaker.decrease(10); 65 | 66 | // mute volume 67 | speaker.mute(); 68 | 69 | // unmute volume 70 | speaker.unmute(); 71 | 72 | // get volume 73 | console.log(speaker.get()); 74 | 75 | // stop watching audio devices 76 | speaker.stop() 77 | ``` 78 | 79 | ## Thanks to 80 | 81 | [Sebastian R (11AND2)](https://github.com/11AND2) 82 | 83 | [Emilijus436](https://github.com/Emilijus436) 84 | 85 | ## Author 86 | 87 | Francesco Saverio Cannizzaro (fcannizzaro) 88 | -------------------------------------------------------------------------------- /audio-napi.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | IAudioEndpointVolume* getVolume(int mic){ 10 | HRESULT hr; 11 | IMMDeviceEnumerator *enumerator = NULL; 12 | IAudioEndpointVolume *volume = NULL; 13 | IMMDevice *defaultDevice = NULL; 14 | CoInitialize(NULL); 15 | hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID *) &enumerator); 16 | hr = enumerator->GetDefaultAudioEndpoint(mic ? eCapture : eRender, eConsole, &defaultDevice); 17 | if (hr != 0) { 18 | return volume; 19 | } 20 | hr = defaultDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (LPVOID *) &volume); 21 | enumerator->Release(); 22 | defaultDevice->Release(); 23 | CoUninitialize(); 24 | return volume; 25 | } 26 | 27 | int *getArgs(napi_env env, napi_callback_info info){ 28 | napi_value argv[2]; 29 | size_t argc = 2; 30 | napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 31 | int *out = (int*) malloc(sizeof(int) * argc); 32 | for(int i = 0; i < (int)argc; i++){ 33 | napi_get_value_int32(env, argv[i], &out[i]); 34 | } 35 | return out; 36 | } 37 | 38 | napi_value toValue(napi_env env, int value){ 39 | napi_value nvalue = 0; 40 | napi_create_int32(env, value, &nvalue); 41 | return nvalue; 42 | } 43 | 44 | napi_value get(napi_env env, napi_callback_info info) { 45 | 46 | int *argv = getArgs(env, info); 47 | float volume = 0; 48 | 49 | IAudioEndpointVolume *tmp_volume = getVolume(argv[0]); 50 | 51 | if (tmp_volume == NULL) { 52 | return toValue(env, -1); 53 | } 54 | 55 | tmp_volume->GetMasterVolumeLevelScalar(&volume); 56 | tmp_volume->Release(); 57 | return toValue(env, (int) round(volume*100)); 58 | } 59 | 60 | napi_value isMuted(napi_env env, napi_callback_info info) { 61 | 62 | int *argv = getArgs(env, info); 63 | int mute = 0; 64 | 65 | IAudioEndpointVolume *tmp_volume = getVolume(argv[0]); 66 | 67 | if (tmp_volume == NULL) { 68 | return toValue(env, -999); 69 | } 70 | 71 | tmp_volume->GetMute(&mute); 72 | tmp_volume->Release(); 73 | return toValue(env, mute); 74 | 75 | } 76 | 77 | napi_value mute(napi_env env, napi_callback_info info) { 78 | 79 | int *argv = getArgs(env, info); 80 | IAudioEndpointVolume *tmp_volume = getVolume(argv[0]); 81 | 82 | if (tmp_volume == NULL) { 83 | return toValue(env, -1); 84 | } 85 | 86 | tmp_volume->SetMute(argv[1], NULL); 87 | tmp_volume->Release(); 88 | return toValue(env, 1); 89 | 90 | } 91 | 92 | napi_value set(napi_env env, napi_callback_info info) { 93 | 94 | int *argv = getArgs(env, info); 95 | float newVolume = ((float)argv[0])/100.0f; 96 | int mic = argv[1]; 97 | 98 | IAudioEndpointVolume *tmp_volume = getVolume(mic); 99 | 100 | if (tmp_volume == NULL) { 101 | return toValue(env, -1); 102 | } 103 | 104 | tmp_volume->SetMasterVolumeLevelScalar(newVolume, NULL); 105 | tmp_volume->Release(); 106 | return toValue(env, 1); 107 | 108 | } 109 | 110 | napi_value Init(napi_env env, napi_value exports) { 111 | 112 | napi_status status; 113 | napi_value get_fn, set_fn, mute_fn, is_mute_fn; 114 | 115 | status = napi_create_function(env, NULL, 0, get, NULL, &get_fn); 116 | status = napi_create_function(env, NULL, 0, set, NULL, &set_fn); 117 | status = napi_create_function(env, NULL, 0, mute, NULL, &mute_fn); 118 | status = napi_create_function(env, NULL, 0, isMuted, NULL, &is_mute_fn); 119 | 120 | status = napi_set_named_property(env, exports, "get", get_fn); 121 | status = napi_set_named_property(env, exports, "set", set_fn); 122 | status = napi_set_named_property(env, exports, "mute", mute_fn); 123 | status = napi_set_named_property(env, exports, "isMuted", is_mute_fn); 124 | 125 | return exports; 126 | } 127 | 128 | NAPI_MODULE(addon, Init) -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "audio", 5 | "sources": [ "audio-napi.cc" ], 6 | "cflags" : [ "-lole32", "-loleaut32"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "win-audio", 3 | "version": "3.0.5", 4 | "description": "Get, Set and Watch Speaker/Microphone Volume on Windows", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "os": [ 8 | "win32" 9 | ], 10 | "files": [ 11 | "dist", 12 | "audio-napi.cc", 13 | "binding.gyp", 14 | "package.json", 15 | "README.md", 16 | "LICENSE" 17 | ], 18 | "scripts": { 19 | "build": "tsc" 20 | }, 21 | "author": "Francesco Saverio Cannizzaro ", 22 | "license": "MIT", 23 | "gypfile": true, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/fcannizzaro/win-audio.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/fcannizzaro/win-audio/issues" 30 | }, 31 | "homepage": "https://github.com/fcannizzaro/win-audio#readme", 32 | "keywords": [ 33 | "audio", 34 | "microphone", 35 | "speaker", 36 | "windows" 37 | ], 38 | "devDependencies": { 39 | "@biomejs/biome": "^1.9.4", 40 | "@tsconfig/node20": "^20.1.4", 41 | "@types/bindings": "^1.5.5", 42 | "@types/node": "^22.10.2", 43 | "typescript": "^5.7.2" 44 | }, 45 | "dependencies": { 46 | "bindings": "^1.5.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | bindings: 12 | specifier: ^1.5.0 13 | version: 1.5.0 14 | devDependencies: 15 | '@biomejs/biome': 16 | specifier: ^1.9.4 17 | version: 1.9.4 18 | '@tsconfig/node20': 19 | specifier: ^20.1.4 20 | version: 20.1.4 21 | '@types/bindings': 22 | specifier: ^1.5.5 23 | version: 1.5.5 24 | '@types/node': 25 | specifier: ^22.10.2 26 | version: 22.10.2 27 | typescript: 28 | specifier: ^5.7.2 29 | version: 5.7.2 30 | 31 | packages: 32 | 33 | '@biomejs/biome@1.9.4': 34 | resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} 35 | engines: {node: '>=14.21.3'} 36 | hasBin: true 37 | 38 | '@biomejs/cli-darwin-arm64@1.9.4': 39 | resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} 40 | engines: {node: '>=14.21.3'} 41 | cpu: [arm64] 42 | os: [darwin] 43 | 44 | '@biomejs/cli-darwin-x64@1.9.4': 45 | resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} 46 | engines: {node: '>=14.21.3'} 47 | cpu: [x64] 48 | os: [darwin] 49 | 50 | '@biomejs/cli-linux-arm64-musl@1.9.4': 51 | resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} 52 | engines: {node: '>=14.21.3'} 53 | cpu: [arm64] 54 | os: [linux] 55 | 56 | '@biomejs/cli-linux-arm64@1.9.4': 57 | resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} 58 | engines: {node: '>=14.21.3'} 59 | cpu: [arm64] 60 | os: [linux] 61 | 62 | '@biomejs/cli-linux-x64-musl@1.9.4': 63 | resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} 64 | engines: {node: '>=14.21.3'} 65 | cpu: [x64] 66 | os: [linux] 67 | 68 | '@biomejs/cli-linux-x64@1.9.4': 69 | resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} 70 | engines: {node: '>=14.21.3'} 71 | cpu: [x64] 72 | os: [linux] 73 | 74 | '@biomejs/cli-win32-arm64@1.9.4': 75 | resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} 76 | engines: {node: '>=14.21.3'} 77 | cpu: [arm64] 78 | os: [win32] 79 | 80 | '@biomejs/cli-win32-x64@1.9.4': 81 | resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} 82 | engines: {node: '>=14.21.3'} 83 | cpu: [x64] 84 | os: [win32] 85 | 86 | '@tsconfig/node20@20.1.4': 87 | resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==} 88 | 89 | '@types/bindings@1.5.5': 90 | resolution: {integrity: sha512-y59PRZBTo2/HuN94qRjyJD+465vGoXMsqz9MMJDbtJL9oT5/B+tAL6c3k10epIinC2/BBkLqKzKC6keukl8wdQ==} 91 | 92 | '@types/node@22.10.2': 93 | resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} 94 | 95 | bindings@1.5.0: 96 | resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 97 | 98 | file-uri-to-path@1.0.0: 99 | resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} 100 | 101 | typescript@5.7.2: 102 | resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} 103 | engines: {node: '>=14.17'} 104 | hasBin: true 105 | 106 | undici-types@6.20.0: 107 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 108 | 109 | snapshots: 110 | 111 | '@biomejs/biome@1.9.4': 112 | optionalDependencies: 113 | '@biomejs/cli-darwin-arm64': 1.9.4 114 | '@biomejs/cli-darwin-x64': 1.9.4 115 | '@biomejs/cli-linux-arm64': 1.9.4 116 | '@biomejs/cli-linux-arm64-musl': 1.9.4 117 | '@biomejs/cli-linux-x64': 1.9.4 118 | '@biomejs/cli-linux-x64-musl': 1.9.4 119 | '@biomejs/cli-win32-arm64': 1.9.4 120 | '@biomejs/cli-win32-x64': 1.9.4 121 | 122 | '@biomejs/cli-darwin-arm64@1.9.4': 123 | optional: true 124 | 125 | '@biomejs/cli-darwin-x64@1.9.4': 126 | optional: true 127 | 128 | '@biomejs/cli-linux-arm64-musl@1.9.4': 129 | optional: true 130 | 131 | '@biomejs/cli-linux-arm64@1.9.4': 132 | optional: true 133 | 134 | '@biomejs/cli-linux-x64-musl@1.9.4': 135 | optional: true 136 | 137 | '@biomejs/cli-linux-x64@1.9.4': 138 | optional: true 139 | 140 | '@biomejs/cli-win32-arm64@1.9.4': 141 | optional: true 142 | 143 | '@biomejs/cli-win32-x64@1.9.4': 144 | optional: true 145 | 146 | '@tsconfig/node20@20.1.4': {} 147 | 148 | '@types/bindings@1.5.5': 149 | dependencies: 150 | '@types/node': 22.10.2 151 | 152 | '@types/node@22.10.2': 153 | dependencies: 154 | undici-types: 6.20.0 155 | 156 | bindings@1.5.0: 157 | dependencies: 158 | file-uri-to-path: 1.0.0 159 | 160 | file-uri-to-path@1.0.0: {} 161 | 162 | typescript@5.7.2: {} 163 | 164 | undici-types@6.20.0: {} 165 | -------------------------------------------------------------------------------- /sample/index.ts: -------------------------------------------------------------------------------- 1 | import audio from ".."; 2 | 3 | const speaker = audio.speaker; 4 | 5 | speaker.start(200); 6 | 7 | speaker.on("change", (volume) => { 8 | console.log("old %d%% -> new %d%%", volume.old, volume.new); 9 | }); 10 | 11 | speaker.on("toggle", (status) => { 12 | console.log("muted: %s -> %s", status.old, status.new); 13 | }); 14 | 15 | speaker.set(40); 16 | 17 | speaker.increase(20); 18 | 19 | speaker.decrease(10); 20 | 21 | speaker.mute(); 22 | 23 | console.log("isMuted", speaker.isMuted()); 24 | 25 | speaker.stop(); 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | import bindings from "bindings"; 3 | 4 | const audio = bindings("../build/Release/audio.node"); 5 | 6 | type Event = T extends "change" 7 | ? { 8 | old: number; 9 | new: number; 10 | } 11 | : { 12 | old: boolean; 13 | new: boolean; 14 | }; 15 | 16 | type EventType = "change" | "toggle"; 17 | 18 | const initDevice = (device: number) => { 19 | const events = new EventEmitter(); 20 | 21 | const data = { 22 | audio: audio.get(device), 23 | status: audio.isMuted(device), 24 | }; 25 | 26 | let interval: NodeJS.Timeout; 27 | 28 | /** 29 | * Check and update current volume. [Generic] 30 | */ 31 | const update = ( 32 | fn: (v: number) => number | boolean, 33 | key: "status" | "audio", 34 | event: EventType 35 | ) => { 36 | let now = fn(Number(device)); 37 | 38 | if (key === "status") { 39 | now = Boolean(now); 40 | } 41 | 42 | if (data[key] !== now) { 43 | events.emit(event, { 44 | old: data[key], 45 | new: now, 46 | }); 47 | } 48 | 49 | data[key] = now; 50 | }; 51 | 52 | /** 53 | * Check and update current volume. 54 | */ 55 | const check = () => { 56 | update(audio.get, "audio", "change"); 57 | update(audio.isMuted, "status", "toggle"); 58 | }; 59 | 60 | /** 61 | * Get current audio 62 | */ 63 | const get = (): number => audio.get(device); 64 | 65 | /** 66 | * Update current and delegate audio set to native module. 67 | */ 68 | const set = (value: number) => { 69 | audio.set(value, device); 70 | check(); 71 | }; 72 | 73 | /** 74 | * Save current status and mute volume. 75 | */ 76 | const mute = (): void => audio.mute(device, 1); 77 | 78 | /** 79 | * Restore previous volume. 80 | */ 81 | const unmute = (): void => audio.mute(device, 0); 82 | 83 | /** 84 | * Mute/Unmute volume. 85 | */ 86 | const toggle = () => { 87 | const fn = audio.isMuted(device) ? unmute : mute; 88 | fn(); 89 | }; 90 | 91 | /** 92 | * React to volume changes using polling check. 93 | */ 94 | const start = (every: number) => { 95 | interval = setInterval(check, every || 500); 96 | }; 97 | 98 | /** 99 | * Stop polling check. 100 | */ 101 | const stop = () => { 102 | clearInterval(interval); 103 | }; 104 | 105 | /** 106 | * Increase current volume of value% 107 | */ 108 | const increase = (value: number) => { 109 | unmute(); 110 | set(Math.min(Math.max(0, data.audio + value), 100)); 111 | }; 112 | 113 | /** 114 | * Decrease current volume of value% 115 | */ 116 | const decrease = (value: number) => increase(-value); 117 | 118 | /** 119 | * Check if is muted 120 | */ 121 | const isMuted = () => audio.isMuted(device) === 1; 122 | 123 | return { 124 | // Events 125 | on: (event: T, cb: (v: Event) => void) => 126 | events.on(event, cb), 127 | off: (event: T, cb: (v: Event) => void) => 128 | events.off(event, cb), 129 | once: (event: T, cb: (v: Event) => void) => 130 | events.once(event, cb), 131 | removeAllListeners: () => events.removeAllListeners(), 132 | // Methods 133 | start, 134 | stop, 135 | get, 136 | set, 137 | increase, 138 | decrease, 139 | mute, 140 | unmute, 141 | isMuted, 142 | toggle, 143 | }; 144 | }; 145 | 146 | type Instance = ReturnType; 147 | 148 | type ProxyInstances = { 149 | speaker: Instance; 150 | mic: Instance; 151 | }; 152 | 153 | const instances: Partial = {}; 154 | 155 | const proxy = new Proxy( 156 | {}, 157 | { 158 | get: (_, prop: "speaker" | "mic") => { 159 | let instance = instances[prop]; 160 | if (!instance) { 161 | instance = initDevice(prop === "mic" ? 1 : 0); 162 | instances[prop] = instance; 163 | } 164 | return instance; 165 | }, 166 | } 167 | ) as ProxyInstances; 168 | 169 | export default proxy; 170 | 171 | module.exports = proxy; 172 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "esModuleInterop": true, 6 | "declaration": true 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | --------------------------------------------------------------------------------