bionic-audio 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/AudioRecorder.d.ts +96 -0
- package/dist/AudioRecorder.d.ts.map +1 -0
- package/dist/AudioRecorder.js +256 -0
- package/dist/AudioRecorder.js.map +1 -0
- package/lib/AudioRecorder.ts +292 -0
- package/package.json +63 -0
- package/recorder.exe +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bionic Audio Contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# bionic-audio
|
|
2
|
+
|
|
3
|
+
Professional Windows WASAPI audio recorder library for Node.js and Bun. Record microphone, system loopback, or both simultaneously with support for multiple audio formats and advanced quality settings.
|
|
4
|
+
|
|
5
|
+
**Status:** Production Ready | **Platform:** Windows 10/11 | **Architecture:** x64, ia32
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ๐ค **Microphone Recording**: Capture input from your microphone
|
|
10
|
+
- ๐ **Loopback Recording**: Capture system audio (what's playing on speakers)
|
|
11
|
+
- ๐๏ธ **Dual Recording**: Record both microphone and loopback simultaneously
|
|
12
|
+
- ๐ **Multiple Formats**: 16-bit PCM, 24-bit PCM, or 32-bit float WAV files
|
|
13
|
+
- โฑ๏ธ **Duration Control**: Record for a specific duration or indefinitely
|
|
14
|
+
- ๐๏ธ **Gain Control**: Amplify or reduce signal strength
|
|
15
|
+
- ๐ก **Stream to stdout**: Raw PCM streaming for real-time processing
|
|
16
|
+
- ๐งต **Threaded Capture**: Dedicated thread for capture operations
|
|
17
|
+
- ๐ **Easy Integration**: EventEmitter-based interface with promise support
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install bionic-audio
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or globally for CLI access:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g bionic-audio
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Windows 10/11
|
|
34
|
+
- Node.js 14+ or Bun
|
|
35
|
+
- `recorder.exe` binary (included in package, or build from source)
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### Basic Recording
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { recordToFile } from 'bionic-audio';
|
|
43
|
+
|
|
44
|
+
// Simple file recording - 5 seconds of system audio
|
|
45
|
+
await recordToFile('./output.wav', { duration: 5 });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### EventEmitter Interface
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { AudioRecorder } from 'bionic-audio';
|
|
52
|
+
|
|
53
|
+
const recorder = new AudioRecorder({
|
|
54
|
+
output: './recording.wav',
|
|
55
|
+
duration: 10,
|
|
56
|
+
preserve32: true // 32-bit float format
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
recorder.on('started', () => console.log('๐ค Recording started'));
|
|
60
|
+
recorder.on('data', (bytes) => console.log(`๐ Wrote ${bytes} bytes`));
|
|
61
|
+
recorder.on('error', (err) => console.error('โ Error:', err));
|
|
62
|
+
recorder.on('complete', (result) => {
|
|
63
|
+
console.log('โ
Recording complete:', result);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
recorder.start();
|
|
67
|
+
|
|
68
|
+
// Stop manually (or wait for duration to finish)
|
|
69
|
+
setTimeout(() => recorder.stop(), 5000);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Promise-Based Interface
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { AudioRecorder } from 'bionic-audio';
|
|
76
|
+
|
|
77
|
+
const recorder = new AudioRecorder();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await recorder.record({
|
|
81
|
+
duration: 5,
|
|
82
|
+
output: './output.wav'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log(`โ
Recording finished with exit code ${result.code}`);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('โ Recording failed:', error);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Convenience Functions
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import {
|
|
95
|
+
recordToFile,
|
|
96
|
+
recordMicrophone,
|
|
97
|
+
recordLoopback,
|
|
98
|
+
recordBoth
|
|
99
|
+
} from 'bionic-audio';
|
|
100
|
+
|
|
101
|
+
// Record system audio (loopback) to file
|
|
102
|
+
await recordToFile('./output.wav', { duration: 10 });
|
|
103
|
+
|
|
104
|
+
// Record microphone only
|
|
105
|
+
await recordMicrophone('./mic.wav', { duration: 10 });
|
|
106
|
+
|
|
107
|
+
// Record system audio only
|
|
108
|
+
await recordLoopback('./system.wav', { duration: 10 });
|
|
109
|
+
|
|
110
|
+
// Record both simultaneously (two separate files)
|
|
111
|
+
await recordBoth('./combined.wav', { duration: 10 });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Streaming to stdout
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { AudioRecorder } from 'bionic-audio';
|
|
118
|
+
import { createWriteStream } from 'fs';
|
|
119
|
+
|
|
120
|
+
const recorder = new AudioRecorder();
|
|
121
|
+
const output = createWriteStream('./stream.wav');
|
|
122
|
+
|
|
123
|
+
recorder.pipe(output);
|
|
124
|
+
recorder.start({ duration: 5, pcmOnly: true });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### `new AudioRecorder(options?)`
|
|
130
|
+
|
|
131
|
+
Creates a new audio recorder instance.
|
|
132
|
+
|
|
133
|
+
**Options:**
|
|
134
|
+
- `output?: string` - Output file path (required for file recording)
|
|
135
|
+
- `duration?: number` - Recording duration in seconds (0 = infinite, default: 0)
|
|
136
|
+
- `gain?: number` - Signal gain/amplitude multiplier (default: 1.0)
|
|
137
|
+
- `preserve24?: boolean` - Keep 24-bit native format (default: false)
|
|
138
|
+
- `preserve32?: boolean` - Keep 32-bit float format (default: false)
|
|
139
|
+
- `pcmOnly?: boolean` - Output raw PCM without WAV header (default: false)
|
|
140
|
+
- `loopback?: boolean` - Record system audio instead of microphone (default: false)
|
|
141
|
+
- `microphone?: boolean` - Record microphone (default: true)
|
|
142
|
+
- `both?: boolean` - Record both simultaneously (default: false)
|
|
143
|
+
- `threaded?: boolean` - Use dedicated capture thread (default: false)
|
|
144
|
+
|
|
145
|
+
### Methods
|
|
146
|
+
|
|
147
|
+
#### `start(options?): void`
|
|
148
|
+
Starts recording with optional runtime options.
|
|
149
|
+
|
|
150
|
+
#### `stop(): boolean`
|
|
151
|
+
Stops the recording process. Returns true if stopped, false if not running.
|
|
152
|
+
|
|
153
|
+
#### `isRecording(): boolean`
|
|
154
|
+
Check if currently recording.
|
|
155
|
+
|
|
156
|
+
#### `record(options?): Promise<RecorderResult>`
|
|
157
|
+
Promise-based recording. Blocks until recording completes or errors.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const result = await recorder.record({ duration: 5 });
|
|
161
|
+
// result: { code: number, signal: string | null, stderr: string }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `pipe(destination: NodeJS.WritableStream): void`
|
|
165
|
+
Pipe raw audio data to a writable stream (e.g., for real-time processing).
|
|
166
|
+
|
|
167
|
+
#### `getProcess(): ChildProcess | null`
|
|
168
|
+
Access the underlying child process directly.
|
|
169
|
+
|
|
170
|
+
### Events
|
|
171
|
+
|
|
172
|
+
- `started` - Recording has started
|
|
173
|
+
- `data(bytes: number)` - Data written (number of bytes)
|
|
174
|
+
- `stderr(data: string)` - Stderr output from recorder
|
|
175
|
+
- `exit(code: number, signal: string | null)` - Process exited
|
|
176
|
+
- `complete(result: RecorderResult)` - Recording completed successfully
|
|
177
|
+
- `error(error: Error)` - Error occurred
|
|
178
|
+
|
|
179
|
+
### Convenience Functions
|
|
180
|
+
|
|
181
|
+
#### `recordToFile(path: string, options?: RecorderOptions): Promise<RecorderResult>`
|
|
182
|
+
Quick file recording.
|
|
183
|
+
|
|
184
|
+
#### `recordMicrophone(path: string, options?: RecorderOptions): Promise<RecorderResult>`
|
|
185
|
+
Record microphone only.
|
|
186
|
+
|
|
187
|
+
#### `recordLoopback(path: string, options?: RecorderOptions): Promise<RecorderResult>`
|
|
188
|
+
Record system audio only.
|
|
189
|
+
|
|
190
|
+
#### `recordBoth(path: string, options?: RecorderOptions): Promise<RecorderResult>`
|
|
191
|
+
Record both microphone and loopback simultaneously.
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
### Audio Quality
|
|
196
|
+
|
|
197
|
+
- **16-bit PCM** (default): Balanced quality and file size, ~1.5 MB/min for stereo at 48kHz
|
|
198
|
+
- **24-bit PCM** (`preserve24: true`): Higher quality, ~2.3 MB/min
|
|
199
|
+
- **32-bit float** (`preserve32: true`): Highest quality, lossless internal format, ~3 MB/min
|
|
200
|
+
|
|
201
|
+
### Gain Control
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Amplify by 2x
|
|
205
|
+
recorder.start({ gain: 2.0 });
|
|
206
|
+
|
|
207
|
+
// Reduce by half
|
|
208
|
+
recorder.start({ gain: 0.5 });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Gain is applied before clipping to prevent distortion.
|
|
212
|
+
|
|
213
|
+
## Electron Integration
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { AudioRecorder } from 'bionic-audio';
|
|
217
|
+
import { ipcMain } from 'electron';
|
|
218
|
+
|
|
219
|
+
let recorder: AudioRecorder | null = null;
|
|
220
|
+
|
|
221
|
+
ipcMain.handle('start-recording', async (event, options) => {
|
|
222
|
+
recorder = new AudioRecorder(options);
|
|
223
|
+
|
|
224
|
+
recorder.on('error', (err) => {
|
|
225
|
+
event.sender.send('recording-error', err.message);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
recorder.start();
|
|
229
|
+
return { ok: true };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
ipcMain.handle('stop-recording', async () => {
|
|
233
|
+
recorder?.stop();
|
|
234
|
+
return { ok: true };
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Building from Source
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Install dependencies
|
|
242
|
+
npm install
|
|
243
|
+
|
|
244
|
+
# Compile TypeScript
|
|
245
|
+
npm run build
|
|
246
|
+
|
|
247
|
+
# Build C++ recorder binary (requires MinGW or MSVC)
|
|
248
|
+
gcc -o recorder.exe main.c -lole32 -std=c99
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
### "recorder.exe not found"
|
|
254
|
+
Ensure the binary is in PATH or in one of these locations:
|
|
255
|
+
- `./recorder.exe`
|
|
256
|
+
- `./bin/recorder.exe`
|
|
257
|
+
- `../recorder.exe`
|
|
258
|
+
- `./dist/recorder.exe`
|
|
259
|
+
|
|
260
|
+
### Distorted/Noisy Audio
|
|
261
|
+
Try enabling gain control with values < 1.0:
|
|
262
|
+
```typescript
|
|
263
|
+
recorder.start({ gain: 0.8 });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### File Won't Play
|
|
267
|
+
Ensure you're using a compatible audio player. 32-bit float WAV support varies. Try 16-bit output:
|
|
268
|
+
```typescript
|
|
269
|
+
recorder.start({ preserve32: false });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
export interface RecorderOptions {
|
|
4
|
+
/** Duration in seconds (0 = infinite) */
|
|
5
|
+
duration?: number;
|
|
6
|
+
/** Output file path or '-' for stdout */
|
|
7
|
+
output?: string;
|
|
8
|
+
/** PCM only (no WAV header) */
|
|
9
|
+
pcmOnly?: boolean;
|
|
10
|
+
/** Record loopback (system audio) */
|
|
11
|
+
loopback?: boolean;
|
|
12
|
+
/** Record microphone */
|
|
13
|
+
mic?: boolean;
|
|
14
|
+
/** Record both loopback and microphone simultaneously */
|
|
15
|
+
both?: boolean;
|
|
16
|
+
/** Gain multiplier (default 1.0) */
|
|
17
|
+
gain?: number;
|
|
18
|
+
/** Preserve 24-bit if available */
|
|
19
|
+
preserve24?: boolean;
|
|
20
|
+
/** Preserve 32-bit float (highest quality) */
|
|
21
|
+
preserve32?: boolean;
|
|
22
|
+
/** Run capture in dedicated thread */
|
|
23
|
+
threaded?: boolean;
|
|
24
|
+
/** Path to recorder.exe (default: auto-detect) */
|
|
25
|
+
recorderPath?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RecorderResult {
|
|
28
|
+
code: number;
|
|
29
|
+
signal: string | null;
|
|
30
|
+
stderr: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* AudioRecorder - wrapper for Windows WASAPI recorder
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* const rec = new AudioRecorder({ output: 'session.wav', duration: 10, gain: 1.5 });
|
|
37
|
+
* rec.start();
|
|
38
|
+
* rec.on('complete', (result) => console.log('Done!'));
|
|
39
|
+
* rec.on('error', (err) => console.error(err));
|
|
40
|
+
*
|
|
41
|
+
* // or stop manually:
|
|
42
|
+
* setTimeout(() => rec.stop(), 5000);
|
|
43
|
+
*/
|
|
44
|
+
export declare class AudioRecorder extends EventEmitter {
|
|
45
|
+
private process;
|
|
46
|
+
private options;
|
|
47
|
+
private recorderExePath;
|
|
48
|
+
private stderrBuffer;
|
|
49
|
+
constructor(options?: RecorderOptions);
|
|
50
|
+
private findRecorderExe;
|
|
51
|
+
/**
|
|
52
|
+
* Build command-line arguments based on options
|
|
53
|
+
*/
|
|
54
|
+
private buildArgs;
|
|
55
|
+
/**
|
|
56
|
+
* Start recording
|
|
57
|
+
*/
|
|
58
|
+
start(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Stop recording
|
|
61
|
+
*/
|
|
62
|
+
stop(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Check if recording is active
|
|
65
|
+
*/
|
|
66
|
+
isRecording(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Get the underlying child process (for advanced usage)
|
|
69
|
+
*/
|
|
70
|
+
getProcess(): ChildProcess | null;
|
|
71
|
+
/**
|
|
72
|
+
* Promise-based interface
|
|
73
|
+
*/
|
|
74
|
+
record(): Promise<RecorderResult>;
|
|
75
|
+
/**
|
|
76
|
+
* Stream data to a file or another stream
|
|
77
|
+
*/
|
|
78
|
+
pipe(destination: NodeJS.WritableStream): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Convenience function: record to file
|
|
82
|
+
*/
|
|
83
|
+
export declare function recordToFile(output: string, durationSeconds?: number, options?: Partial<RecorderOptions>): Promise<RecorderResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Convenience function: record microphone
|
|
86
|
+
*/
|
|
87
|
+
export declare function recordMicrophone(output: string, durationSeconds?: number, gain?: number): Promise<RecorderResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Convenience function: record system audio
|
|
90
|
+
*/
|
|
91
|
+
export declare function recordLoopback(output: string, durationSeconds?: number, gain?: number): Promise<RecorderResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Convenience function: record both
|
|
94
|
+
*/
|
|
95
|
+
export declare function recordBoth(output: string, durationSeconds?: number, gain?: number): Promise<RecorderResult>;
|
|
96
|
+
//# sourceMappingURL=AudioRecorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioRecorder.d.ts","sourceRoot":"","sources":["../lib/AudioRecorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,YAAY,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,MAAM,WAAW,eAAe;IAC9B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAc;gBAEtB,OAAO,GAAE,eAAoB;IAoBzC,OAAO,CAAC,eAAe;IAuBvB;;OAEG;IACH,OAAO,CAAC,SAAS;IA0CjB;;OAEG;IACI,KAAK,IAAI,IAAI;IAkDpB;;OAEG;IACI,IAAI,IAAI,OAAO;IAQtB;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACI,UAAU,IAAI,YAAY,GAAG,IAAI;IAIxC;;OAEG;IACU,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC;IAQ9C;;OAEG;IACI,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI;CAStD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAW,EAC5B,OAAO,GAAE,OAAO,CAAC,eAAe,CAAM,GACrC,OAAO,CAAC,cAAc,CAAC,CAOzB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAW,EAC5B,IAAI,GAAE,MAAY,GACjB,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAW,EAC5B,IAAI,GAAE,MAAY,GACjB,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAW,EAC5B,IAAI,GAAE,MAAY,GACjB,OAAO,CAAC,cAAc,CAAC,CAEzB"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AudioRecorder = void 0;
|
|
37
|
+
exports.recordToFile = recordToFile;
|
|
38
|
+
exports.recordMicrophone = recordMicrophone;
|
|
39
|
+
exports.recordLoopback = recordLoopback;
|
|
40
|
+
exports.recordBoth = recordBoth;
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const events_1 = require("events");
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
/**
|
|
45
|
+
* AudioRecorder - wrapper for Windows WASAPI recorder
|
|
46
|
+
*
|
|
47
|
+
* Usage:
|
|
48
|
+
* const rec = new AudioRecorder({ output: 'session.wav', duration: 10, gain: 1.5 });
|
|
49
|
+
* rec.start();
|
|
50
|
+
* rec.on('complete', (result) => console.log('Done!'));
|
|
51
|
+
* rec.on('error', (err) => console.error(err));
|
|
52
|
+
*
|
|
53
|
+
* // or stop manually:
|
|
54
|
+
* setTimeout(() => rec.stop(), 5000);
|
|
55
|
+
*/
|
|
56
|
+
class AudioRecorder extends events_1.EventEmitter {
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
super();
|
|
59
|
+
this.process = null;
|
|
60
|
+
this.stderrBuffer = '';
|
|
61
|
+
this.options = {
|
|
62
|
+
duration: options.duration ?? 10,
|
|
63
|
+
output: options.output ?? 'recording.wav',
|
|
64
|
+
pcmOnly: options.pcmOnly ?? false,
|
|
65
|
+
loopback: !options.mic && !options.both,
|
|
66
|
+
mic: options.mic ?? false,
|
|
67
|
+
both: options.both ?? false,
|
|
68
|
+
gain: options.gain ?? 1.0,
|
|
69
|
+
preserve24: options.preserve24 ?? false,
|
|
70
|
+
preserve32: options.preserve32 ?? false,
|
|
71
|
+
threaded: options.threaded ?? false,
|
|
72
|
+
...options,
|
|
73
|
+
};
|
|
74
|
+
// Try to find recorder.exe
|
|
75
|
+
this.recorderExePath = options.recorderPath || this.findRecorderExe();
|
|
76
|
+
}
|
|
77
|
+
findRecorderExe() {
|
|
78
|
+
// Common locations
|
|
79
|
+
const candidates = [
|
|
80
|
+
path.join(__dirname, '..', 'bin', 'recorder.exe'),
|
|
81
|
+
path.join(__dirname, 'recorder.exe'),
|
|
82
|
+
path.join(process.cwd(), 'recorder.exe'),
|
|
83
|
+
'd:\\recorder\\recorder.exe',
|
|
84
|
+
'recorder.exe', // rely on PATH
|
|
85
|
+
];
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
try {
|
|
88
|
+
require('fs').accessSync(candidate);
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
//
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Default fallback
|
|
96
|
+
return 'recorder.exe';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build command-line arguments based on options
|
|
100
|
+
*/
|
|
101
|
+
buildArgs() {
|
|
102
|
+
const args = [];
|
|
103
|
+
if (this.options.duration !== undefined && this.options.duration >= 0) {
|
|
104
|
+
args.push('-d', String(this.options.duration));
|
|
105
|
+
}
|
|
106
|
+
if (this.options.output) {
|
|
107
|
+
args.push('-o', this.options.output);
|
|
108
|
+
}
|
|
109
|
+
if (this.options.pcmOnly) {
|
|
110
|
+
args.push('-p');
|
|
111
|
+
}
|
|
112
|
+
if (this.options.both) {
|
|
113
|
+
args.push('-b');
|
|
114
|
+
}
|
|
115
|
+
else if (this.options.mic) {
|
|
116
|
+
args.push('-m');
|
|
117
|
+
}
|
|
118
|
+
else if (this.options.loopback) {
|
|
119
|
+
args.push('-l');
|
|
120
|
+
}
|
|
121
|
+
if (this.options.threaded) {
|
|
122
|
+
args.push('-t');
|
|
123
|
+
}
|
|
124
|
+
if (this.options.gain && this.options.gain !== 1.0) {
|
|
125
|
+
args.push('-g', String(this.options.gain));
|
|
126
|
+
}
|
|
127
|
+
if (this.options.preserve24) {
|
|
128
|
+
args.push('-24');
|
|
129
|
+
}
|
|
130
|
+
if (this.options.preserve32) {
|
|
131
|
+
args.push('-32');
|
|
132
|
+
}
|
|
133
|
+
return args;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Start recording
|
|
137
|
+
*/
|
|
138
|
+
start() {
|
|
139
|
+
if (this.process) {
|
|
140
|
+
throw new Error('Recording already in progress');
|
|
141
|
+
}
|
|
142
|
+
const args = this.buildArgs();
|
|
143
|
+
this.stderrBuffer = '';
|
|
144
|
+
this.process = (0, child_process_1.spawn)(this.recorderExePath, args, {
|
|
145
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
146
|
+
windowsHide: true,
|
|
147
|
+
});
|
|
148
|
+
// Capture stderr for diagnostics
|
|
149
|
+
this.process.stderr.on('data', (data) => {
|
|
150
|
+
const chunk = data.toString();
|
|
151
|
+
this.stderrBuffer += chunk;
|
|
152
|
+
this.emit('stderr', chunk);
|
|
153
|
+
});
|
|
154
|
+
// If output is stdout, emit data
|
|
155
|
+
if (this.options.output === '-') {
|
|
156
|
+
this.process.stdout.on('data', (data) => {
|
|
157
|
+
this.emit('data', data);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
this.process.on('error', (err) => {
|
|
161
|
+
this.process = null;
|
|
162
|
+
this.emit('error', err);
|
|
163
|
+
});
|
|
164
|
+
this.process.on('exit', (code, signal) => {
|
|
165
|
+
this.process = null;
|
|
166
|
+
const result = {
|
|
167
|
+
code: code ?? -1,
|
|
168
|
+
signal,
|
|
169
|
+
stderr: this.stderrBuffer,
|
|
170
|
+
};
|
|
171
|
+
this.emit('exit', result);
|
|
172
|
+
if (code === 0) {
|
|
173
|
+
this.emit('complete', result);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
this.emit('error', new Error(`recorder exited with code ${code}`));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
this.emit('started');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Stop recording
|
|
183
|
+
*/
|
|
184
|
+
stop() {
|
|
185
|
+
if (!this.process) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
this.process.kill('SIGTERM');
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if recording is active
|
|
193
|
+
*/
|
|
194
|
+
isRecording() {
|
|
195
|
+
return this.process !== null && !this.process.killed;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get the underlying child process (for advanced usage)
|
|
199
|
+
*/
|
|
200
|
+
getProcess() {
|
|
201
|
+
return this.process;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Promise-based interface
|
|
205
|
+
*/
|
|
206
|
+
async record() {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
this.once('complete', resolve);
|
|
209
|
+
this.once('error', reject);
|
|
210
|
+
this.start();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Stream data to a file or another stream
|
|
215
|
+
*/
|
|
216
|
+
pipe(destination) {
|
|
217
|
+
if (!this.isRecording() || this.options.output !== '-') {
|
|
218
|
+
throw new Error('Must start recording with output="-" to pipe data');
|
|
219
|
+
}
|
|
220
|
+
const proc = this.getProcess();
|
|
221
|
+
if (proc?.stdout) {
|
|
222
|
+
proc.stdout.pipe(destination);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.AudioRecorder = AudioRecorder;
|
|
227
|
+
/**
|
|
228
|
+
* Convenience function: record to file
|
|
229
|
+
*/
|
|
230
|
+
async function recordToFile(output, durationSeconds = 10, options = {}) {
|
|
231
|
+
const recorder = new AudioRecorder({
|
|
232
|
+
output,
|
|
233
|
+
duration: durationSeconds,
|
|
234
|
+
...options,
|
|
235
|
+
});
|
|
236
|
+
return recorder.record();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Convenience function: record microphone
|
|
240
|
+
*/
|
|
241
|
+
async function recordMicrophone(output, durationSeconds = 10, gain = 1.0) {
|
|
242
|
+
return recordToFile(output, durationSeconds, { mic: true, gain });
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Convenience function: record system audio
|
|
246
|
+
*/
|
|
247
|
+
async function recordLoopback(output, durationSeconds = 10, gain = 1.0) {
|
|
248
|
+
return recordToFile(output, durationSeconds, { loopback: true, gain });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Convenience function: record both
|
|
252
|
+
*/
|
|
253
|
+
async function recordBoth(output, durationSeconds = 10, gain = 1.0) {
|
|
254
|
+
return recordToFile(output, durationSeconds, { both: true, gain });
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=AudioRecorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioRecorder.js","sourceRoot":"","sources":["../lib/AudioRecorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface RecorderOptions {
|
|
6
|
+
/** Duration in seconds (0 = infinite) */
|
|
7
|
+
duration?: number;
|
|
8
|
+
/** Output file path or '-' for stdout */
|
|
9
|
+
output?: string;
|
|
10
|
+
/** PCM only (no WAV header) */
|
|
11
|
+
pcmOnly?: boolean;
|
|
12
|
+
/** Record loopback (system audio) */
|
|
13
|
+
loopback?: boolean;
|
|
14
|
+
/** Record microphone */
|
|
15
|
+
mic?: boolean;
|
|
16
|
+
/** Record both loopback and microphone simultaneously */
|
|
17
|
+
both?: boolean;
|
|
18
|
+
/** Gain multiplier (default 1.0) */
|
|
19
|
+
gain?: number;
|
|
20
|
+
/** Preserve 24-bit if available */
|
|
21
|
+
preserve24?: boolean;
|
|
22
|
+
/** Preserve 32-bit float (highest quality) */
|
|
23
|
+
preserve32?: boolean;
|
|
24
|
+
/** Run capture in dedicated thread */
|
|
25
|
+
threaded?: boolean;
|
|
26
|
+
/** Path to recorder.exe (default: auto-detect) */
|
|
27
|
+
recorderPath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RecorderResult {
|
|
31
|
+
code: number;
|
|
32
|
+
signal: string | null;
|
|
33
|
+
stderr: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* AudioRecorder - wrapper for Windows WASAPI recorder
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* const rec = new AudioRecorder({ output: 'session.wav', duration: 10, gain: 1.5 });
|
|
41
|
+
* rec.start();
|
|
42
|
+
* rec.on('complete', (result) => console.log('Done!'));
|
|
43
|
+
* rec.on('error', (err) => console.error(err));
|
|
44
|
+
*
|
|
45
|
+
* // or stop manually:
|
|
46
|
+
* setTimeout(() => rec.stop(), 5000);
|
|
47
|
+
*/
|
|
48
|
+
export class AudioRecorder extends EventEmitter {
|
|
49
|
+
private process: ChildProcess | null = null;
|
|
50
|
+
private options: RecorderOptions;
|
|
51
|
+
private recorderExePath: string;
|
|
52
|
+
private stderrBuffer: string = '';
|
|
53
|
+
|
|
54
|
+
constructor(options: RecorderOptions = {}) {
|
|
55
|
+
super();
|
|
56
|
+
this.options = {
|
|
57
|
+
duration: options.duration ?? 10,
|
|
58
|
+
output: options.output ?? 'recording.wav',
|
|
59
|
+
pcmOnly: options.pcmOnly ?? false,
|
|
60
|
+
loopback: !options.mic && !options.both,
|
|
61
|
+
mic: options.mic ?? false,
|
|
62
|
+
both: options.both ?? false,
|
|
63
|
+
gain: options.gain ?? 1.0,
|
|
64
|
+
preserve24: options.preserve24 ?? false,
|
|
65
|
+
preserve32: options.preserve32 ?? false,
|
|
66
|
+
threaded: options.threaded ?? false,
|
|
67
|
+
...options,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Try to find recorder.exe
|
|
71
|
+
this.recorderExePath = options.recorderPath || this.findRecorderExe();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private findRecorderExe(): string {
|
|
75
|
+
// Common locations
|
|
76
|
+
const candidates = [
|
|
77
|
+
path.join(__dirname, '..', 'bin', 'recorder.exe'),
|
|
78
|
+
path.join(__dirname, 'recorder.exe'),
|
|
79
|
+
path.join(process.cwd(), 'recorder.exe'),
|
|
80
|
+
'd:\\recorder\\recorder.exe',
|
|
81
|
+
'recorder.exe', // rely on PATH
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
try {
|
|
86
|
+
require('fs').accessSync(candidate);
|
|
87
|
+
return candidate;
|
|
88
|
+
} catch {
|
|
89
|
+
//
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Default fallback
|
|
94
|
+
return 'recorder.exe';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build command-line arguments based on options
|
|
99
|
+
*/
|
|
100
|
+
private buildArgs(): string[] {
|
|
101
|
+
const args: string[] = [];
|
|
102
|
+
|
|
103
|
+
if (this.options.duration !== undefined && this.options.duration >= 0) {
|
|
104
|
+
args.push('-d', String(this.options.duration));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.options.output) {
|
|
108
|
+
args.push('-o', this.options.output);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.options.pcmOnly) {
|
|
112
|
+
args.push('-p');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.options.both) {
|
|
116
|
+
args.push('-b');
|
|
117
|
+
} else if (this.options.mic) {
|
|
118
|
+
args.push('-m');
|
|
119
|
+
} else if (this.options.loopback) {
|
|
120
|
+
args.push('-l');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.options.threaded) {
|
|
124
|
+
args.push('-t');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.options.gain && this.options.gain !== 1.0) {
|
|
128
|
+
args.push('-g', String(this.options.gain));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.options.preserve24) {
|
|
132
|
+
args.push('-24');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.options.preserve32) {
|
|
136
|
+
args.push('-32');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return args;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Start recording
|
|
144
|
+
*/
|
|
145
|
+
public start(): void {
|
|
146
|
+
if (this.process) {
|
|
147
|
+
throw new Error('Recording already in progress');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const args = this.buildArgs();
|
|
151
|
+
this.stderrBuffer = '';
|
|
152
|
+
|
|
153
|
+
this.process = spawn(this.recorderExePath, args, {
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
windowsHide: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Capture stderr for diagnostics
|
|
159
|
+
this.process.stderr!.on('data', (data) => {
|
|
160
|
+
const chunk = data.toString();
|
|
161
|
+
this.stderrBuffer += chunk;
|
|
162
|
+
this.emit('stderr', chunk);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// If output is stdout, emit data
|
|
166
|
+
if (this.options.output === '-') {
|
|
167
|
+
this.process.stdout!.on('data', (data) => {
|
|
168
|
+
this.emit('data', data);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.process.on('error', (err) => {
|
|
173
|
+
this.process = null;
|
|
174
|
+
this.emit('error', err);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.process.on('exit', (code, signal) => {
|
|
178
|
+
this.process = null;
|
|
179
|
+
const result: RecorderResult = {
|
|
180
|
+
code: code ?? -1,
|
|
181
|
+
signal,
|
|
182
|
+
stderr: this.stderrBuffer,
|
|
183
|
+
};
|
|
184
|
+
this.emit('exit', result);
|
|
185
|
+
if (code === 0) {
|
|
186
|
+
this.emit('complete', result);
|
|
187
|
+
} else {
|
|
188
|
+
this.emit('error', new Error(`recorder exited with code ${code}`));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.emit('started');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Stop recording
|
|
197
|
+
*/
|
|
198
|
+
public stop(): boolean {
|
|
199
|
+
if (!this.process) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
this.process.kill('SIGTERM');
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if recording is active
|
|
208
|
+
*/
|
|
209
|
+
public isRecording(): boolean {
|
|
210
|
+
return this.process !== null && !this.process.killed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get the underlying child process (for advanced usage)
|
|
215
|
+
*/
|
|
216
|
+
public getProcess(): ChildProcess | null {
|
|
217
|
+
return this.process;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Promise-based interface
|
|
222
|
+
*/
|
|
223
|
+
public async record(): Promise<RecorderResult> {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
this.once('complete', resolve);
|
|
226
|
+
this.once('error', reject);
|
|
227
|
+
this.start();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Stream data to a file or another stream
|
|
233
|
+
*/
|
|
234
|
+
public pipe(destination: NodeJS.WritableStream): void {
|
|
235
|
+
if (!this.isRecording() || this.options.output !== '-') {
|
|
236
|
+
throw new Error('Must start recording with output="-" to pipe data');
|
|
237
|
+
}
|
|
238
|
+
const proc = this.getProcess();
|
|
239
|
+
if (proc?.stdout) {
|
|
240
|
+
proc.stdout.pipe(destination);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Convenience function: record to file
|
|
247
|
+
*/
|
|
248
|
+
export async function recordToFile(
|
|
249
|
+
output: string,
|
|
250
|
+
durationSeconds: number = 10,
|
|
251
|
+
options: Partial<RecorderOptions> = {}
|
|
252
|
+
): Promise<RecorderResult> {
|
|
253
|
+
const recorder = new AudioRecorder({
|
|
254
|
+
output,
|
|
255
|
+
duration: durationSeconds,
|
|
256
|
+
...options,
|
|
257
|
+
});
|
|
258
|
+
return recorder.record();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Convenience function: record microphone
|
|
263
|
+
*/
|
|
264
|
+
export async function recordMicrophone(
|
|
265
|
+
output: string,
|
|
266
|
+
durationSeconds: number = 10,
|
|
267
|
+
gain: number = 1.0
|
|
268
|
+
): Promise<RecorderResult> {
|
|
269
|
+
return recordToFile(output, durationSeconds, { mic: true, gain });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Convenience function: record system audio
|
|
274
|
+
*/
|
|
275
|
+
export async function recordLoopback(
|
|
276
|
+
output: string,
|
|
277
|
+
durationSeconds: number = 10,
|
|
278
|
+
gain: number = 1.0
|
|
279
|
+
): Promise<RecorderResult> {
|
|
280
|
+
return recordToFile(output, durationSeconds, { loopback: true, gain });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Convenience function: record both
|
|
285
|
+
*/
|
|
286
|
+
export async function recordBoth(
|
|
287
|
+
output: string,
|
|
288
|
+
durationSeconds: number = 10,
|
|
289
|
+
gain: number = 1.0
|
|
290
|
+
): Promise<RecorderResult> {
|
|
291
|
+
return recordToFile(output, durationSeconds, { both: true, gain });
|
|
292
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bionic-audio",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional Windows WASAPI audio recorder library for Node.js and Bun - record microphone and system audio with advanced features",
|
|
5
|
+
"main": "dist/AudioRecorder.js",
|
|
6
|
+
"types": "dist/AudioRecorder.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bionic-audio": "./recorder.exe"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"recorder.exe",
|
|
13
|
+
"lib/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepare": "npm run build",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"audio",
|
|
24
|
+
"recording",
|
|
25
|
+
"WASAPI",
|
|
26
|
+
"windows",
|
|
27
|
+
"loopback",
|
|
28
|
+
"microphone",
|
|
29
|
+
"audio-recording",
|
|
30
|
+
"system-audio",
|
|
31
|
+
"capture",
|
|
32
|
+
"wav",
|
|
33
|
+
"stereo",
|
|
34
|
+
"nodejs",
|
|
35
|
+
"bun",
|
|
36
|
+
"bionic"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": ""
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": ""
|
|
46
|
+
},
|
|
47
|
+
"homepage": "",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=14.0.0",
|
|
50
|
+
"npm": ">=6.0.0"
|
|
51
|
+
},
|
|
52
|
+
"os": [
|
|
53
|
+
"win32"
|
|
54
|
+
],
|
|
55
|
+
"cpu": [
|
|
56
|
+
"x64",
|
|
57
|
+
"ia32"
|
|
58
|
+
],
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^20.0.0",
|
|
61
|
+
"typescript": "^5.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/recorder.exe
ADDED
|
Binary file
|