native-recorder-nodejs 1.0.1

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.
@@ -0,0 +1,449 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "WASAPIEngine.h"
4
+ #include <algorithm>
5
+ #include <functiondiscoverykeys_devpkey.h>
6
+ #include <iostream>
7
+ #include <vector>
8
+
9
+ const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
10
+ const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
11
+ const IID IID_IAudioClient = __uuidof(IAudioClient);
12
+ const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
13
+
14
+ WASAPIEngine::WASAPIEngine() : isRecording(false) {
15
+ CoInitialize(NULL);
16
+ HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
17
+ IID_IMMDeviceEnumerator, (void **)&enumerator);
18
+
19
+ if (FAILED(hr)) {
20
+ std::cerr << "Failed to create IMMDeviceEnumerator" << std::endl;
21
+ }
22
+ }
23
+
24
+ WASAPIEngine::~WASAPIEngine() {
25
+ Stop();
26
+ enumerator.Reset();
27
+ CoUninitialize();
28
+ }
29
+
30
+ void WASAPIEngine::Start(const std::string &deviceType,
31
+ const std::string &deviceId, DataCallback dataCb,
32
+ ErrorCallback errorCb) {
33
+ if (isRecording) {
34
+ return;
35
+ }
36
+
37
+ this->dataCallback = dataCb;
38
+ this->errorCallback = errorCb;
39
+ this->currentDeviceId = deviceId;
40
+ this->currentDeviceType = deviceType;
41
+ this->isRecording = true;
42
+ this->recordingThread = std::thread(&WASAPIEngine::RecordingThread, this);
43
+ }
44
+
45
+ void WASAPIEngine::Stop() {
46
+ if (isRecording) {
47
+ isRecording = false;
48
+ if (recordingThread.joinable()) {
49
+ recordingThread.join();
50
+ }
51
+ }
52
+ }
53
+
54
+ std::vector<AudioDevice> WASAPIEngine::GetDevices() {
55
+ std::vector<AudioDevice> devices;
56
+ if (!enumerator)
57
+ return devices;
58
+
59
+ // Get default input device ID
60
+ std::string defaultInputId;
61
+ ComPtr<IMMDevice> pDefaultInputDevice;
62
+ HRESULT hr = enumerator->GetDefaultAudioEndpoint(eCapture, eConsole,
63
+ &pDefaultInputDevice);
64
+ if (SUCCEEDED(hr)) {
65
+ LPWSTR pwszID = NULL;
66
+ hr = pDefaultInputDevice->GetId(&pwszID);
67
+ if (SUCCEEDED(hr)) {
68
+ std::wstring wsId(pwszID);
69
+ defaultInputId = std::string(wsId.begin(), wsId.end());
70
+ CoTaskMemFree(pwszID);
71
+ }
72
+ }
73
+
74
+ // Get default output device ID
75
+ std::string defaultOutputId;
76
+ ComPtr<IMMDevice> pDefaultOutputDevice;
77
+ hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole,
78
+ &pDefaultOutputDevice);
79
+ if (SUCCEEDED(hr)) {
80
+ LPWSTR pwszID = NULL;
81
+ hr = pDefaultOutputDevice->GetId(&pwszID);
82
+ if (SUCCEEDED(hr)) {
83
+ std::wstring wsId(pwszID);
84
+ defaultOutputId = std::string(wsId.begin(), wsId.end());
85
+ CoTaskMemFree(pwszID);
86
+ }
87
+ }
88
+
89
+ // Enumerate input devices (microphones)
90
+ ComPtr<IMMDeviceCollection> pInputCollection;
91
+ hr = enumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE,
92
+ &pInputCollection);
93
+ if (SUCCEEDED(hr)) {
94
+ UINT count;
95
+ pInputCollection->GetCount(&count);
96
+
97
+ for (UINT i = 0; i < count; i++) {
98
+ ComPtr<IMMDevice> pEndpoint;
99
+ hr = pInputCollection->Item(i, &pEndpoint);
100
+ if (FAILED(hr))
101
+ continue;
102
+
103
+ LPWSTR pwszID = NULL;
104
+ hr = pEndpoint->GetId(&pwszID);
105
+ if (FAILED(hr))
106
+ continue;
107
+
108
+ std::wstring wsId(pwszID);
109
+ std::string id(wsId.begin(), wsId.end());
110
+ CoTaskMemFree(pwszID);
111
+
112
+ std::string name = GetDeviceName(pEndpoint.Get());
113
+ bool isDefault = (id == defaultInputId);
114
+
115
+ AudioDevice device;
116
+ device.id = id;
117
+ device.name = name;
118
+ device.type = AudioEngine::DEVICE_TYPE_INPUT;
119
+ device.isDefault = isDefault;
120
+ devices.push_back(device);
121
+ }
122
+ }
123
+
124
+ // Enumerate output devices (speakers for loopback)
125
+ ComPtr<IMMDeviceCollection> pOutputCollection;
126
+ hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
127
+ &pOutputCollection);
128
+ if (SUCCEEDED(hr)) {
129
+ UINT count;
130
+ pOutputCollection->GetCount(&count);
131
+
132
+ for (UINT i = 0; i < count; i++) {
133
+ ComPtr<IMMDevice> pEndpoint;
134
+ hr = pOutputCollection->Item(i, &pEndpoint);
135
+ if (FAILED(hr))
136
+ continue;
137
+
138
+ LPWSTR pwszID = NULL;
139
+ hr = pEndpoint->GetId(&pwszID);
140
+ if (FAILED(hr))
141
+ continue;
142
+
143
+ std::wstring wsId(pwszID);
144
+ std::string id(wsId.begin(), wsId.end());
145
+ CoTaskMemFree(pwszID);
146
+
147
+ std::string name = GetDeviceName(pEndpoint.Get());
148
+ bool isDefault = (id == defaultOutputId);
149
+
150
+ AudioDevice device;
151
+ device.id = id;
152
+ device.name = name;
153
+ device.type = AudioEngine::DEVICE_TYPE_OUTPUT;
154
+ device.isDefault = isDefault;
155
+ devices.push_back(device);
156
+ }
157
+ }
158
+
159
+ return devices;
160
+ }
161
+
162
+ std::string WASAPIEngine::GetDeviceName(IMMDevice *device) {
163
+ ComPtr<IPropertyStore> pProps;
164
+ HRESULT hr = device->OpenPropertyStore(STGM_READ, &pProps);
165
+ if (FAILED(hr))
166
+ return "Unknown Device";
167
+
168
+ PROPVARIANT varName;
169
+ PropVariantInit(&varName);
170
+ hr = pProps->GetValue(PKEY_Device_FriendlyName, &varName);
171
+ if (FAILED(hr))
172
+ return "Unknown Device";
173
+
174
+ std::wstring wsName(varName.pwszVal);
175
+ std::string name(wsName.begin(), wsName.end());
176
+ PropVariantClear(&varName);
177
+ return name;
178
+ }
179
+
180
+ AudioFormat WASAPIEngine::GetDeviceFormat(const std::string &deviceId) {
181
+ AudioFormat format = {0, 0, 0, 0};
182
+ HRESULT hr;
183
+ ComPtr<IMMDevice> pDevice;
184
+ ComPtr<IAudioClient> pAudioClient;
185
+ WAVEFORMATEX *pwfx = NULL;
186
+
187
+ if (!enumerator)
188
+ return format;
189
+
190
+ std::wstring wsId(deviceId.begin(), deviceId.end());
191
+ hr = enumerator->GetDevice(wsId.c_str(), &pDevice);
192
+
193
+ if (FAILED(hr))
194
+ return format;
195
+
196
+ hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL,
197
+ (void **)&pAudioClient);
198
+ if (FAILED(hr))
199
+ return format;
200
+
201
+ hr = pAudioClient->GetMixFormat(&pwfx);
202
+ if (FAILED(hr))
203
+ return format;
204
+
205
+ format.sampleRate = pwfx->nSamplesPerSec;
206
+ format.channels = pwfx->nChannels;
207
+ format.rawBitDepth = pwfx->wBitsPerSample;
208
+ format.bitDepth = 16; // We always convert to 16-bit PCM
209
+
210
+ if (pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
211
+ WAVEFORMATEXTENSIBLE *pEx = (WAVEFORMATEXTENSIBLE *)pwfx;
212
+ if (pEx->Samples.wValidBitsPerSample > 0) {
213
+ format.rawBitDepth = pEx->Samples.wValidBitsPerSample;
214
+ }
215
+ }
216
+
217
+ CoTaskMemFree(pwfx);
218
+ return format;
219
+ }
220
+
221
+ void WASAPIEngine::RecordingThread() {
222
+ CoInitialize(NULL);
223
+
224
+ HRESULT hr;
225
+ ComPtr<IMMDeviceEnumerator> pEnumerator;
226
+ ComPtr<IMMDevice> pDevice;
227
+ ComPtr<IAudioClient> pAudioClient;
228
+ ComPtr<IAudioCaptureClient> pCaptureClient;
229
+ WAVEFORMATEX *pwfx = NULL;
230
+ UINT32 bufferFrameCount;
231
+ UINT32 numFramesAvailable;
232
+ UINT32 packetLength = 0;
233
+ BYTE *pData;
234
+ DWORD flags;
235
+ HANDLE hEvent = NULL;
236
+
237
+ // Determine if this is output (loopback) or input based on deviceType
238
+ bool isLoopback = (currentDeviceType == AudioEngine::DEVICE_TYPE_OUTPUT);
239
+
240
+ do {
241
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
242
+ IID_IMMDeviceEnumerator, (void **)&pEnumerator);
243
+
244
+ if (FAILED(hr)) {
245
+ if (errorCallback)
246
+ errorCallback(
247
+ "Failed to create IMMDeviceEnumerator in recording thread");
248
+ break;
249
+ }
250
+
251
+ // Get device by ID
252
+ std::wstring wsId(currentDeviceId.begin(), currentDeviceId.end());
253
+ hr = pEnumerator->GetDevice(wsId.c_str(), &pDevice);
254
+
255
+ if (FAILED(hr)) {
256
+ if (errorCallback)
257
+ errorCallback("Failed to get audio device: " + currentDeviceId);
258
+ break;
259
+ }
260
+
261
+ hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL,
262
+ (void **)&pAudioClient);
263
+ if (FAILED(hr)) {
264
+ if (errorCallback)
265
+ errorCallback("Failed to activate audio client");
266
+ break;
267
+ }
268
+
269
+ hr = pAudioClient->GetMixFormat(&pwfx);
270
+ if (FAILED(hr)) {
271
+ if (errorCallback)
272
+ errorCallback("Failed to get mix format");
273
+ break;
274
+ }
275
+
276
+ // Initialize Audio Client
277
+ DWORD streamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
278
+ if (isLoopback) {
279
+ streamFlags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
280
+ }
281
+
282
+ hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, streamFlags,
283
+ 10000000, 0, pwfx, NULL);
284
+
285
+ if (FAILED(hr)) {
286
+ if (errorCallback)
287
+ errorCallback("Failed to initialize audio client");
288
+ break;
289
+ }
290
+
291
+ hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
292
+ hr = pAudioClient->SetEventHandle(hEvent);
293
+ if (FAILED(hr)) {
294
+ if (errorCallback)
295
+ errorCallback("Failed to set event handle");
296
+ break;
297
+ }
298
+
299
+ hr = pAudioClient->GetService(IID_IAudioCaptureClient,
300
+ (void **)&pCaptureClient);
301
+ if (FAILED(hr)) {
302
+ if (errorCallback)
303
+ errorCallback("Failed to get capture client");
304
+ break;
305
+ }
306
+
307
+ hr = pAudioClient->Start();
308
+ if (FAILED(hr)) {
309
+ if (errorCallback)
310
+ errorCallback("Failed to start recording");
311
+ break;
312
+ }
313
+
314
+ while (isRecording) {
315
+ DWORD retval = WaitForSingleObject(hEvent, 2000);
316
+ if (retval != WAIT_OBJECT_0) {
317
+ continue;
318
+ }
319
+
320
+ hr = pCaptureClient->GetNextPacketSize(&packetLength);
321
+ if (FAILED(hr)) {
322
+ if (errorCallback)
323
+ errorCallback("Failed to get next packet size");
324
+ break;
325
+ }
326
+
327
+ while (packetLength != 0) {
328
+ hr = pCaptureClient->GetBuffer(&pData, &numFramesAvailable, &flags,
329
+ NULL, NULL);
330
+
331
+ if (FAILED(hr)) {
332
+ if (errorCallback)
333
+ errorCallback("Failed to get buffer");
334
+ break;
335
+ }
336
+
337
+ if (numFramesAvailable > 0) {
338
+ // Convert to Float32
339
+ std::vector<float> inputFloats;
340
+ size_t numSamples = numFramesAvailable * pwfx->nChannels;
341
+ inputFloats.resize(numSamples);
342
+
343
+ if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
344
+ std::fill(inputFloats.begin(), inputFloats.end(), 0.0f);
345
+ } else {
346
+ bool isFloat = false;
347
+ if (pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
348
+ WAVEFORMATEXTENSIBLE *pEx = (WAVEFORMATEXTENSIBLE *)pwfx;
349
+ if (IsEqualGUID(pEx->SubFormat,
350
+ KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
351
+ isFloat = true;
352
+ }
353
+ } else if (pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
354
+ isFloat = true;
355
+ }
356
+
357
+ if (isFloat) {
358
+ float *floatData = (float *)pData;
359
+ std::copy(floatData, floatData + numSamples, inputFloats.begin());
360
+ } else {
361
+ if (pwfx->wBitsPerSample == 16) {
362
+ int16_t *pcmData = (int16_t *)pData;
363
+ for (size_t i = 0; i < numSamples; i++) {
364
+ inputFloats[i] = pcmData[i] / 32768.0f;
365
+ }
366
+ } else if (pwfx->wBitsPerSample == 24) {
367
+ uint8_t *ptr = (uint8_t *)pData;
368
+ for (size_t i = 0; i < numSamples; i++) {
369
+ int32_t sample =
370
+ (ptr[0] << 8) | (ptr[1] << 16) | (ptr[2] << 24);
371
+ inputFloats[i] = sample / 2147483648.0f;
372
+ ptr += 3;
373
+ }
374
+ } else if (pwfx->wBitsPerSample == 32) {
375
+ int32_t *ptr = (int32_t *)pData;
376
+ for (size_t i = 0; i < numSamples; i++) {
377
+ inputFloats[i] = ptr[i] / 2147483648.0f;
378
+ }
379
+ } else {
380
+ std::fill(inputFloats.begin(), inputFloats.end(), 0.0f);
381
+ }
382
+ }
383
+ }
384
+
385
+ // Convert to Int16 and Callback
386
+ if (!inputFloats.empty()) {
387
+ std::vector<int16_t> pcmData;
388
+ pcmData.reserve(inputFloats.size());
389
+
390
+ for (float sample : inputFloats) {
391
+ if (sample > 1.0f)
392
+ sample = 1.0f;
393
+ if (sample < -1.0f)
394
+ sample = -1.0f;
395
+ pcmData.push_back((int16_t)(sample * 32767.0f));
396
+ }
397
+
398
+ if (dataCallback) {
399
+ dataCallback((uint8_t *)pcmData.data(),
400
+ pcmData.size() * sizeof(int16_t));
401
+ }
402
+ }
403
+ }
404
+
405
+ hr = pCaptureClient->ReleaseBuffer(numFramesAvailable);
406
+ if (FAILED(hr)) {
407
+ if (errorCallback)
408
+ errorCallback("Failed to release buffer");
409
+ break;
410
+ }
411
+
412
+ hr = pCaptureClient->GetNextPacketSize(&packetLength);
413
+ if (FAILED(hr)) {
414
+ if (errorCallback)
415
+ errorCallback("Failed to get next packet size loop");
416
+ break;
417
+ }
418
+ }
419
+ if (FAILED(hr))
420
+ break;
421
+ }
422
+
423
+ if (pAudioClient)
424
+ pAudioClient->Stop();
425
+
426
+ } while (false);
427
+
428
+ if (pwfx)
429
+ CoTaskMemFree(pwfx);
430
+ if (hEvent)
431
+ CloseHandle(hEvent);
432
+ CoUninitialize();
433
+ }
434
+
435
+ PermissionStatus WASAPIEngine::CheckPermission() {
436
+ // Windows doesn't require explicit permissions for audio recording
437
+ PermissionStatus status;
438
+ status.mic = true;
439
+ status.system = true;
440
+ return status;
441
+ }
442
+
443
+ bool WASAPIEngine::RequestPermission(PermissionType type) {
444
+ // Windows doesn't require explicit permissions for audio recording
445
+ // Always return true
446
+ return true;
447
+ }
448
+
449
+ #endif
@@ -0,0 +1,44 @@
1
+ #pragma once
2
+
3
+ #ifdef _WIN32
4
+
5
+ #include "../AudioEngine.h"
6
+ #include <atomic>
7
+ #include <audioclient.h>
8
+ #include <mmdeviceapi.h>
9
+ #include <thread>
10
+ #include <windows.h>
11
+ #include <wrl/client.h>
12
+
13
+ using Microsoft::WRL::ComPtr;
14
+
15
+ class WASAPIEngine : public AudioEngine {
16
+ public:
17
+ WASAPIEngine();
18
+ ~WASAPIEngine();
19
+
20
+ void Start(const std::string &deviceType, const std::string &deviceId,
21
+ DataCallback dataCb, ErrorCallback errorCb) override;
22
+ void Stop() override;
23
+ std::vector<AudioDevice> GetDevices() override;
24
+ AudioFormat GetDeviceFormat(const std::string &deviceId) override;
25
+
26
+ // Permission handling (Windows doesn't require explicit permissions)
27
+ PermissionStatus CheckPermission() override;
28
+ bool RequestPermission(PermissionType type) override;
29
+
30
+ private:
31
+ void RecordingThread();
32
+ std::string GetDeviceName(IMMDevice *device);
33
+
34
+ ComPtr<IMMDeviceEnumerator> enumerator;
35
+ std::atomic<bool> isRecording;
36
+ std::thread recordingThread;
37
+
38
+ DataCallback dataCallback;
39
+ ErrorCallback errorCallback;
40
+ std::string currentDeviceId;
41
+ std::string currentDeviceType;
42
+ };
43
+
44
+ #endif
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "native-recorder-nodejs",
3
+ "version": "1.0.1",
4
+ "description": "Cross-platform (Win/Mac) Native Audio SDK for Node.js",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "install": "prebuild-install || cmake-js compile",
9
+ "build": "npm run build:native && npm run build:ts",
10
+ "build:ts": "tsc",
11
+ "build:native": "cmake-js compile",
12
+ "build:native:debug": "cmake-js compile --debug",
13
+ "build:tests": "cmake -S . -B build -DBUILD_TESTS=ON && cmake --build build --config Release --target NativeTests",
14
+ "prebuild": "prebuild --backend cmake-js -r napi --strip",
15
+ "prebuild:upload": "prebuild --upload-all ${GITHUB_TOKEN}",
16
+ "test": "jest",
17
+ "test:native": "npm run build:tests && cd build && ctest -C Release --output-on-failure",
18
+ "test:cli": "node ./test/cli_test.js",
19
+ "clean": "rimraf dist build prebuilds",
20
+ "prepublishOnly": "npm run build:ts"
21
+ },
22
+ "keywords": [
23
+ "audio",
24
+ "sdk",
25
+ "native",
26
+ "cpp",
27
+ "wasapi",
28
+ "avfoundation",
29
+ "screencapturekit",
30
+ "loopback",
31
+ "recording",
32
+ "capture"
33
+ ],
34
+ "author": "Yidadaa",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Yidadaa/Native-Recorder-NodeJS.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Yidadaa/Native-Recorder-NodeJS/issues"
42
+ },
43
+ "homepage": "https://github.com/Yidadaa/Native-Recorder-NodeJS#readme",
44
+ "files": [
45
+ "dist",
46
+ "prebuilds",
47
+ "native",
48
+ "src",
49
+ "CMakeLists.txt",
50
+ "binding.gyp"
51
+ ],
52
+ "binary": {
53
+ "napi_versions": [
54
+ 8
55
+ ]
56
+ },
57
+ "dependencies": {
58
+ "node-addon-api": "^7.0.0",
59
+ "prebuild-install": "^7.1.2"
60
+ },
61
+ "devDependencies": {
62
+ "@types/jest": "^29.5.0",
63
+ "@types/node": "^18.0.0",
64
+ "cmake-js": "^7.2.0",
65
+ "jest": "^29.5.0",
66
+ "prebuild": "^13.0.1",
67
+ "rimraf": "^5.0.0",
68
+ "ts-jest": "^29.1.0",
69
+ "typescript": "^5.0.0"
70
+ },
71
+ "engines": {
72
+ "node": ">=16.0.0"
73
+ }
74
+ }
@@ -0,0 +1,39 @@
1
+ import { join, dirname } from "path";
2
+ import { existsSync } from "fs";
3
+
4
+ function getBinding() {
5
+ const moduleName = "NativeAudioSDK.node";
6
+
7
+ // Try prebuild first (installed via npm)
8
+ const prebuildsDir = join(__dirname, "..", "prebuilds");
9
+ const platform = process.platform;
10
+ const arch = process.arch;
11
+
12
+ // prebuild-install stores binaries in: prebuilds/{platform}-{arch}/
13
+ const prebuildPath = join(prebuildsDir, `${platform}-${arch}`, moduleName);
14
+
15
+ if (existsSync(prebuildPath)) {
16
+ return require(prebuildPath);
17
+ }
18
+
19
+ // Fallback to local build (development)
20
+ const localPath = join(__dirname, "..", "build", "Release", moduleName);
21
+ if (existsSync(localPath)) {
22
+ return require(localPath);
23
+ }
24
+
25
+ // Final fallback for different build configurations
26
+ const debugPath = join(__dirname, "..", "build", "Debug", moduleName);
27
+ if (existsSync(debugPath)) {
28
+ return require(debugPath);
29
+ }
30
+
31
+ throw new Error(
32
+ `Could not find native module ${moduleName}. ` +
33
+ `Tried:\n - ${prebuildPath}\n - ${localPath}\n - ${debugPath}\n` +
34
+ `Please run 'npm run build:native' or reinstall the package.`
35
+ );
36
+ }
37
+
38
+ const bindings = getBinding();
39
+ export default bindings;