audio-mic 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/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # audio-mic
2
+
3
+ Capture audio from microphone in node or browser.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ import mic from 'audio-mic'
9
+
10
+ let read = mic({
11
+ sampleRate: 44100,
12
+ channels: 1,
13
+ bitDepth: 16,
14
+ // bufferSize: 50, // ring buffer ms (default 50)
15
+ // backend: 'miniaudio', // force backend
16
+ })
17
+
18
+ read((err, pcmBuffer) => {
19
+ // process chunk
20
+ })
21
+ read(null) // stop capture
22
+ ```
23
+
24
+ ### Node Readable
25
+
26
+ ```js
27
+ import MicReadable from 'audio-mic/stream'
28
+
29
+ MicReadable({ sampleRate: 44100, channels: 1 }).pipe(dest)
30
+ ```
31
+
32
+ ## Backends
33
+
34
+ Tried in order; first successful one wins.
35
+
36
+ | Backend | How | Latency | Install |
37
+ |---|---|---|---|
38
+ | `miniaudio` | N-API addon wrapping [miniaudio.h](https://github.com/mackron/miniaudio) | Low | Prebuilt via `@audio/mic-*` packages |
39
+ | `process` | Pipes from ffmpeg/sox/arecord | High | System tool must be installed |
40
+ | `null` | Silent, maintains timing contract | — | Built-in (CI/headless fallback) |
41
+ | `mediastream` | getUserMedia + AudioWorklet (browser) | Low | Built-in |
42
+
43
+ ## API
44
+
45
+ ### `read = mic(opts?)`
46
+
47
+ Returns a source function. Options:
48
+
49
+ - `sampleRate` — default `44100`
50
+ - `channels` — default `1`
51
+ - `bitDepth` — `8`, `16` (default), `24`, `32`
52
+ - `bufferSize` — ring buffer in ms, default `50`
53
+ - `backend` — force a specific backend
54
+
55
+ ### `read(cb)`
56
+
57
+ Read PCM data. Callback fires with each captured chunk: `(err, buffer) => {}`.
58
+
59
+ ### `read(null)`
60
+
61
+ Stop capture. Closes the audio device.
62
+
63
+ ### `read.close()`
64
+
65
+ Immediately close the audio device.
66
+
67
+ ### `read.backend`
68
+
69
+ Name of the active backend (`'miniaudio'`, `'process'`, `'null'`, `'mediastream'`).
70
+
71
+ ## Building
72
+
73
+ ```sh
74
+ npm run build # compile native addon locally
75
+ npm test # run tests
76
+ ```
77
+
78
+ ## Publishing
79
+
80
+ ```sh
81
+ # JS-only change (no native code changed):
82
+ npm version patch && git push && git push --tags
83
+ npm publish
84
+
85
+ # Native code changed — rebuild platform packages:
86
+ npm version patch && git push && git push --tags
87
+ gh run watch # wait for CI
88
+ rm -rf artifacts
89
+ gh run download --dir artifacts \
90
+ -n mic-darwin-arm64 -n mic-darwin-x64 \
91
+ -n mic-linux-x64 -n mic-linux-arm64 -n mic-win32-x64
92
+
93
+ # (fallback) If darwin-x64 CI is unavailable, cross-compile locally:
94
+ npx node-gyp@latest rebuild --arch=x64
95
+ mkdir -p artifacts/mic-darwin-x64
96
+ cp build/Release/mic.node artifacts/mic-darwin-x64/
97
+
98
+ for pkg in packages/mic-*/; do
99
+ cp artifacts/$(basename $pkg)/mic.node $pkg/
100
+ (cd $pkg && npm publish)
101
+ done
102
+ npm publish
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT
108
+
109
+ <a href="https://github.com/krishnized/license/">ॐ</a>
package/binding.gyp ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "targets": [{
3
+ "target_name": "mic",
4
+ "sources": ["native/mic.c"],
5
+ "include_dirs": ["native"],
6
+ "cflags": ["-std=c99", "-O2"],
7
+ "xcode_settings": {
8
+ "OTHER_CFLAGS": ["-std=c99", "-O2"],
9
+ "MACOSX_DEPLOYMENT_TARGET": "10.13"
10
+ },
11
+ "msvs_settings": {
12
+ "VCCLCompilerTool": {
13
+ "AdditionalOptions": ["/O2"]
14
+ }
15
+ },
16
+ "conditions": [
17
+ ["OS=='mac'", {
18
+ "libraries": [
19
+ "-framework CoreAudio",
20
+ "-framework AudioToolbox",
21
+ "-framework CoreFoundation"
22
+ ]
23
+ }],
24
+ ["OS=='linux'", {
25
+ "libraries": [
26
+ "-lpthread",
27
+ "-lm",
28
+ "-ldl"
29
+ ]
30
+ }],
31
+ ["OS=='win'", {
32
+ "libraries": [
33
+ "ole32.lib"
34
+ ]
35
+ }]
36
+ ]
37
+ }]
38
+ }
package/browser.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface BrowserMicOptions {
2
+ sampleRate?: number
3
+ channels?: number
4
+ bitDepth?: 8 | 16 | 32
5
+ context?: AudioContext
6
+ echoCancellation?: boolean
7
+ noiseSuppression?: boolean
8
+ autoGainControl?: boolean
9
+ }
10
+
11
+ export interface ReadFn {
12
+ (cb: (err: Error | null, chunk?: Uint8Array | null) => void): void
13
+ (cb: null): void
14
+ end(): void
15
+ close(): void
16
+ backend: 'mediastream'
17
+ }
18
+
19
+ export default function mic(opts?: BrowserMicOptions): Promise<ReadFn>
package/browser.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @module audio-mic/browser
3
+ *
4
+ * Browser audio capture via getUserMedia + AudioWorklet.
5
+ * Falls back to ScriptProcessorNode if AudioWorklet unavailable.
6
+ *
7
+ * Note: unlike node, browser mic() is async because getUserMedia requires permission.
8
+ */
9
+ export default async function mic(opts = {}) {
10
+ const channels = opts.channels || 1
11
+ const sampleRate = opts.sampleRate || 44100
12
+ const bitDepth = opts.bitDepth || 16
13
+
14
+ const constraints = {
15
+ audio: {
16
+ sampleRate: { ideal: sampleRate },
17
+ channelCount: { ideal: channels },
18
+ echoCancellation: opts.echoCancellation ?? false,
19
+ noiseSuppression: opts.noiseSuppression ?? false,
20
+ autoGainControl: opts.autoGainControl ?? false,
21
+ }
22
+ }
23
+
24
+ const stream = await navigator.mediaDevices.getUserMedia(constraints)
25
+
26
+ const ownCtx = !opts.context
27
+ const ctx = opts.context || new AudioContext({ sampleRate })
28
+ const source = ctx.createMediaStreamSource(stream)
29
+
30
+ let closed = false
31
+ let pending = null
32
+
33
+ // try AudioWorklet, fall back to ScriptProcessor
34
+ let node
35
+ if (ctx.audioWorklet) {
36
+ const workletCode = `
37
+ class MicProcessor extends AudioWorkletProcessor {
38
+ process(inputs) {
39
+ const input = inputs[0]
40
+ if (input && input.length > 0) {
41
+ const channels = []
42
+ for (let i = 0; i < input.length; i++) channels.push(input[i].slice())
43
+ this.port.postMessage(channels)
44
+ }
45
+ return true
46
+ }
47
+ }
48
+ registerProcessor('mic-processor', MicProcessor)
49
+ `
50
+ const blob = new Blob([workletCode], { type: 'application/javascript' })
51
+ const url = URL.createObjectURL(blob)
52
+ await ctx.audioWorklet.addModule(url)
53
+ URL.revokeObjectURL(url)
54
+
55
+ node = new AudioWorkletNode(ctx, 'mic-processor', {
56
+ numberOfInputs: 1,
57
+ numberOfOutputs: 0,
58
+ channelCount: channels
59
+ })
60
+ source.connect(node)
61
+
62
+ node.port.onmessage = (e) => {
63
+ if (closed || !pending) return
64
+ const cb = pending
65
+ pending = null
66
+ cb(null, float32ToPCM(e.data, bitDepth))
67
+ }
68
+ } else {
69
+ // ScriptProcessorNode fallback (deprecated but wider support)
70
+ const bufSize = 2048
71
+ node = ctx.createScriptProcessor(bufSize, channels, 1)
72
+ source.connect(node)
73
+ node.connect(ctx.destination) // required for processing to run
74
+
75
+ node.onaudioprocess = (e) => {
76
+ if (closed || !pending) return
77
+ const cb = pending
78
+ pending = null
79
+ const chans = []
80
+ for (let i = 0; i < channels; i++) chans.push(e.inputBuffer.getChannelData(i).slice())
81
+ cb(null, float32ToPCM(chans, bitDepth))
82
+ }
83
+ }
84
+
85
+ read.close = close
86
+ read.end = close
87
+ read.backend = 'mediastream'
88
+
89
+ return read
90
+
91
+ function read(cb) {
92
+ if (cb == null || closed) {
93
+ close()
94
+ return
95
+ }
96
+ // resume suspended context (autoplay policy)
97
+ if (ctx.state === 'suspended') ctx.resume()
98
+ pending = cb
99
+ }
100
+
101
+ function close() {
102
+ if (closed) return
103
+ closed = true
104
+ pending = null
105
+ source.disconnect()
106
+ stream.getTracks().forEach(t => t.stop())
107
+ if (ownCtx) ctx.close?.()
108
+ }
109
+
110
+ function float32ToPCM(channelData, bits) {
111
+ const ch = channelData.length
112
+ const len = channelData[0].length
113
+ const bps = bits / 8
114
+ const buf = new Uint8Array(len * ch * bps)
115
+ const view = new DataView(buf.buffer)
116
+
117
+ for (let i = 0; i < len; i++) {
118
+ for (let c = 0; c < ch; c++) {
119
+ const sample = channelData[c][i]
120
+ const offset = (i * ch + c) * bps
121
+ if (bits === 16) {
122
+ view.setInt16(offset, Math.max(-32768, Math.min(32767, Math.round(sample * 32767))), true)
123
+ } else if (bits === 32) {
124
+ view.setFloat32(offset, sample, true)
125
+ } else if (bits === 8) {
126
+ buf[offset] = Math.max(0, Math.min(255, Math.round((sample + 1) * 127.5)))
127
+ }
128
+ }
129
+ }
130
+ return buf
131
+ }
132
+ }
package/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export interface MicOptions {
2
+ sampleRate?: number
3
+ channels?: number
4
+ bitDepth?: 8 | 16 | 24 | 32
5
+ bufferSize?: number
6
+ backend?: 'miniaudio' | 'process' | 'null'
7
+ }
8
+
9
+ export interface ReadFn {
10
+ (cb: (err: Error | null, chunk?: Buffer | Uint8Array | null) => void): void
11
+ (cb: null): void
12
+ end(): void
13
+ close(): void
14
+ backend: string
15
+ }
16
+
17
+ export default function mic(opts?: MicOptions): ReadFn
package/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module audio-mic
3
+ *
4
+ * Capture audio data from microphone.
5
+ * let read = mic({ sampleRate: 44100 })
6
+ * read((err, chunk) => {})
7
+ * read(null) // stop
8
+ */
9
+ import { open } from './src/backend.js'
10
+
11
+ const defaults = {
12
+ sampleRate: 44100,
13
+ channels: 1,
14
+ bitDepth: 16,
15
+ bufferSize: 50
16
+ }
17
+
18
+ export default function mic(opts) {
19
+ const config = { ...defaults, ...opts }
20
+ const { name, device } = open(config, config.backend)
21
+
22
+ read.close = () => { device.close() }
23
+ read.end = () => { device.close() }
24
+ read.backend = name
25
+
26
+ return read
27
+
28
+ function read(cb) {
29
+ if (cb == null) {
30
+ device.close()
31
+ return
32
+ }
33
+ device.read(cb)
34
+ }
35
+ }
package/native/mic.c ADDED
@@ -0,0 +1,346 @@
1
+ /*
2
+ * audio-mic native addon
3
+ * Minimal miniaudio N-API binding: capture device → ring buffer → JS reads
4
+ *
5
+ * Architecture:
6
+ * - Capture callback pushes frames into ring buffer
7
+ * - readSync: non-blocking memcpy from ring buffer (for polling)
8
+ * - readAsync: blocks on worker thread until data available, then fires callback
9
+ * - JS calls readAsync in a loop — callback fires with each captured chunk
10
+ */
11
+
12
+ #define MA_NO_DECODING
13
+ #define MA_NO_ENCODING
14
+ #define MA_NO_RESOURCE_MANAGER
15
+ #define MA_NO_NODE_GRAPH
16
+ #define MA_NO_ENGINE
17
+ #define MA_NO_GENERATION
18
+ #define MINIAUDIO_IMPLEMENTATION
19
+ #include "miniaudio.h"
20
+
21
+ #include <node_api.h>
22
+ #include <string.h>
23
+
24
+ #define NAPI_CALL(env, call) \
25
+ do { \
26
+ napi_status status = (call); \
27
+ if (status != napi_ok) { \
28
+ const napi_extended_error_info* error_info = NULL; \
29
+ napi_get_last_error_info((env), &error_info); \
30
+ const char* msg = (error_info && error_info->error_message) \
31
+ ? error_info->error_message : "Unknown N-API error"; \
32
+ napi_throw_error((env), NULL, msg); \
33
+ return NULL; \
34
+ } \
35
+ } while (0)
36
+
37
+ /* Mic instance */
38
+ typedef struct {
39
+ ma_device device;
40
+ ma_pcm_rb ring_buffer;
41
+ ma_uint32 channels;
42
+ ma_uint32 sample_rate;
43
+ ma_format format;
44
+ int started;
45
+ volatile int closed;
46
+ } mic_t;
47
+
48
+ /* Async read work */
49
+ typedef struct {
50
+ mic_t* mic;
51
+ void* data;
52
+ size_t byte_length;
53
+ ma_uint32 frames_read;
54
+ napi_async_work work;
55
+ napi_ref callback_ref;
56
+ napi_ref buffer_ref;
57
+ } read_work_t;
58
+
59
+ /* Capture callback — pushes into ring buffer */
60
+ static void capture_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
61
+ mic_t* mic = (mic_t*)device->pUserData;
62
+ ma_uint32 bpf = ma_get_bytes_per_frame(mic->format, mic->channels);
63
+
64
+ ma_uint32 total_written = 0;
65
+ while (total_written < frame_count) {
66
+ ma_uint32 to_write = frame_count - total_written;
67
+ void* write_buf;
68
+ if (ma_pcm_rb_acquire_write(&mic->ring_buffer, &to_write, &write_buf) != MA_SUCCESS || to_write == 0) break;
69
+ memcpy(write_buf, (const ma_uint8*)input + total_written * bpf, to_write * bpf);
70
+ ma_pcm_rb_commit_write(&mic->ring_buffer, to_write);
71
+ total_written += to_write;
72
+ }
73
+
74
+ (void)output;
75
+ }
76
+
77
+ /* GC destructor */
78
+ static void mic_destructor(napi_env env, void* data, void* hint) {
79
+ mic_t* mic = (mic_t*)data;
80
+ if (!mic) return;
81
+ if (!mic->closed) {
82
+ mic->closed = 1;
83
+ if (mic->started) {
84
+ ma_device_stop(&mic->device);
85
+ mic->started = 0;
86
+ }
87
+ ma_device_uninit(&mic->device);
88
+ }
89
+ /* Ring buffer freed here, not in close — avoids race with async worker */
90
+ ma_pcm_rb_uninit(&mic->ring_buffer);
91
+ free(mic);
92
+ (void)env;
93
+ (void)hint;
94
+ }
95
+
96
+ /* mic_open(sampleRate, channels, bitDepth, bufferMs) → external */
97
+ static napi_value mic_open(napi_env env, napi_callback_info info) {
98
+ size_t argc = 4;
99
+ napi_value argv[4];
100
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
101
+
102
+ if (argc < 3) {
103
+ napi_throw_error(env, NULL, "mic_open requires (sampleRate, channels, bitDepth[, bufferMs])");
104
+ return NULL;
105
+ }
106
+
107
+ ma_uint32 sample_rate, channels, bit_depth, buffer_ms;
108
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[0], &sample_rate));
109
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[1], &channels));
110
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[2], &bit_depth));
111
+
112
+ buffer_ms = 50;
113
+ if (argc > 3) NAPI_CALL(env, napi_get_value_uint32(env, argv[3], &buffer_ms));
114
+ if (buffer_ms < 10) buffer_ms = 10;
115
+ if (buffer_ms > 2000) buffer_ms = 2000;
116
+
117
+ ma_format format;
118
+ switch (bit_depth) {
119
+ case 8: format = ma_format_u8; break;
120
+ case 16: format = ma_format_s16; break;
121
+ case 24: format = ma_format_s24; break;
122
+ case 32: format = ma_format_f32; break;
123
+ default:
124
+ napi_throw_error(env, NULL, "Unsupported bitDepth (use 8, 16, 24, 32)");
125
+ return NULL;
126
+ }
127
+
128
+ mic_t* mic = (mic_t*)calloc(1, sizeof(mic_t));
129
+ if (!mic) {
130
+ napi_throw_error(env, NULL, "Failed to allocate mic");
131
+ return NULL;
132
+ }
133
+
134
+ mic->channels = channels;
135
+ mic->sample_rate = sample_rate;
136
+ mic->format = format;
137
+
138
+ /* ring buffer — power of 2 sized */
139
+ ma_uint32 rb_frames = (sample_rate * buffer_ms) / 1000;
140
+ ma_uint32 rb_pow2 = 1;
141
+ while (rb_pow2 < rb_frames) rb_pow2 <<= 1;
142
+
143
+ ma_result result = ma_pcm_rb_init(format, channels, rb_pow2, NULL, NULL, &mic->ring_buffer);
144
+ if (result != MA_SUCCESS) {
145
+ free(mic);
146
+ napi_throw_error(env, NULL, "Failed to init ring buffer");
147
+ return NULL;
148
+ }
149
+
150
+ /* capture device */
151
+ ma_device_config config = ma_device_config_init(ma_device_type_capture);
152
+ config.capture.format = format;
153
+ config.capture.channels = channels;
154
+ config.sampleRate = sample_rate;
155
+ config.dataCallback = capture_callback;
156
+ config.pUserData = mic;
157
+ config.performanceProfile = ma_performance_profile_low_latency;
158
+
159
+ result = ma_device_init(NULL, &config, &mic->device);
160
+ if (result != MA_SUCCESS) {
161
+ ma_pcm_rb_uninit(&mic->ring_buffer);
162
+ free(mic);
163
+ napi_throw_error(env, NULL, "Failed to init capture device");
164
+ return NULL;
165
+ }
166
+
167
+ /* start capturing immediately */
168
+ result = ma_device_start(&mic->device);
169
+ if (result != MA_SUCCESS) {
170
+ ma_device_uninit(&mic->device);
171
+ ma_pcm_rb_uninit(&mic->ring_buffer);
172
+ free(mic);
173
+ napi_throw_error(env, NULL, "Failed to start capture device");
174
+ return NULL;
175
+ }
176
+ mic->started = 1;
177
+
178
+ napi_value external;
179
+ NAPI_CALL(env, napi_create_external(env, mic, mic_destructor, NULL, &external));
180
+ return external;
181
+ }
182
+
183
+ /*
184
+ * mic_readSync(handle, buffer) → framesRead
185
+ * Non-blocking: reads as many frames as available right now.
186
+ */
187
+ static napi_value mic_read_sync(napi_env env, napi_callback_info info) {
188
+ size_t argc = 2;
189
+ napi_value argv[2];
190
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
191
+
192
+ mic_t* mic;
193
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&mic));
194
+
195
+ if (mic->closed) {
196
+ napi_value result;
197
+ NAPI_CALL(env, napi_create_int32(env, 0, &result));
198
+ return result;
199
+ }
200
+
201
+ void* data;
202
+ size_t byte_length;
203
+ NAPI_CALL(env, napi_get_buffer_info(env, argv[1], &data, &byte_length));
204
+
205
+ ma_uint32 bpf = ma_get_bytes_per_frame(mic->format, mic->channels);
206
+ if (bpf == 0) {
207
+ napi_value result;
208
+ NAPI_CALL(env, napi_create_int32(env, 0, &result));
209
+ return result;
210
+ }
211
+
212
+ ma_uint32 max_frames = (ma_uint32)(byte_length / bpf);
213
+ ma_uint32 frames_read = 0;
214
+
215
+ while (frames_read < max_frames) {
216
+ ma_uint32 to_read = max_frames - frames_read;
217
+ void* read_buf;
218
+ ma_result res = ma_pcm_rb_acquire_read(&mic->ring_buffer, &to_read, &read_buf);
219
+ if (res != MA_SUCCESS || to_read == 0) break;
220
+ memcpy((ma_uint8*)data + frames_read * bpf, read_buf, to_read * bpf);
221
+ ma_pcm_rb_commit_read(&mic->ring_buffer, to_read);
222
+ frames_read += to_read;
223
+ }
224
+
225
+ napi_value result;
226
+ NAPI_CALL(env, napi_create_uint32(env, frames_read, &result));
227
+ return result;
228
+ }
229
+
230
+ /* Async read — blocks on worker thread until data available */
231
+ static void read_execute(napi_env env, void* data) {
232
+ read_work_t* w = (read_work_t*)data;
233
+ mic_t* mic = w->mic;
234
+ ma_uint32 bpf = ma_get_bytes_per_frame(mic->format, mic->channels);
235
+ if (bpf == 0) { w->frames_read = 0; return; }
236
+
237
+ ma_uint32 max_frames = (ma_uint32)(w->byte_length / bpf);
238
+ ma_uint32 frames_read = 0;
239
+
240
+ /* Wait until we have data or device is closed.
241
+ * Read whatever is available once data arrives — don't wait to fill the whole buffer.
242
+ * This gives lowest latency: callback fires as soon as any data is captured. */
243
+ while (frames_read == 0 && !mic->closed) {
244
+ ma_uint32 to_read = max_frames - frames_read;
245
+ void* read_buf;
246
+ ma_result res = ma_pcm_rb_acquire_read(&mic->ring_buffer, &to_read, &read_buf);
247
+ if (res == MA_SUCCESS && to_read > 0) {
248
+ memcpy((ma_uint8*)w->data + frames_read * bpf, read_buf, to_read * bpf);
249
+ ma_pcm_rb_commit_read(&mic->ring_buffer, to_read);
250
+ frames_read += to_read;
251
+ } else {
252
+ ma_sleep(1);
253
+ }
254
+ }
255
+
256
+ w->frames_read = frames_read;
257
+ (void)env;
258
+ }
259
+
260
+ static void read_complete(napi_env env, napi_status status, void* data) {
261
+ read_work_t* w = (read_work_t*)data;
262
+
263
+ napi_value callback, global, argv[2];
264
+ napi_get_reference_value(env, w->callback_ref, &callback);
265
+ napi_get_global(env, &global);
266
+
267
+ napi_get_null(env, &argv[0]);
268
+ napi_create_uint32(env, w->frames_read, &argv[1]);
269
+ napi_call_function(env, global, callback, 2, argv, NULL);
270
+
271
+ napi_delete_reference(env, w->callback_ref);
272
+ napi_delete_reference(env, w->buffer_ref);
273
+ napi_delete_async_work(env, w->work);
274
+ free(w);
275
+ (void)status;
276
+ }
277
+
278
+ /* mic_readAsync(handle, buffer, callback) */
279
+ static napi_value mic_read_async(napi_env env, napi_callback_info info) {
280
+ size_t argc = 3;
281
+ napi_value argv[3];
282
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
283
+
284
+ mic_t* mic;
285
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&mic));
286
+
287
+ if (mic->closed) {
288
+ napi_throw_error(env, NULL, "Mic is closed");
289
+ return NULL;
290
+ }
291
+
292
+ void* data;
293
+ size_t byte_length;
294
+ NAPI_CALL(env, napi_get_buffer_info(env, argv[1], &data, &byte_length));
295
+
296
+ read_work_t* w = (read_work_t*)calloc(1, sizeof(read_work_t));
297
+ w->mic = mic;
298
+ w->data = data;
299
+ w->byte_length = byte_length;
300
+
301
+ NAPI_CALL(env, napi_create_reference(env, argv[2], 1, &w->callback_ref));
302
+ NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &w->buffer_ref));
303
+
304
+ napi_value work_name;
305
+ NAPI_CALL(env, napi_create_string_utf8(env, "mic_read", NAPI_AUTO_LENGTH, &work_name));
306
+ NAPI_CALL(env, napi_create_async_work(env, NULL, work_name, read_execute, read_complete, w, &w->work));
307
+ NAPI_CALL(env, napi_queue_async_work(env, w->work));
308
+
309
+ return NULL;
310
+ }
311
+
312
+ /* mic_close(handle) */
313
+ static napi_value mic_close(napi_env env, napi_callback_info info) {
314
+ size_t argc = 1;
315
+ napi_value argv[1];
316
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
317
+
318
+ mic_t* mic;
319
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&mic));
320
+
321
+ if (!mic->closed) {
322
+ mic->closed = 1;
323
+ if (mic->started) {
324
+ ma_device_stop(&mic->device);
325
+ mic->started = 0;
326
+ }
327
+ ma_device_uninit(&mic->device);
328
+ /* Ring buffer freed by GC destructor — worker may still be blocked */
329
+ }
330
+
331
+ return NULL;
332
+ }
333
+
334
+ /* Module init */
335
+ static napi_value init(napi_env env, napi_value exports) {
336
+ napi_property_descriptor props[] = {
337
+ { "open", NULL, mic_open, NULL, NULL, NULL, napi_default, NULL },
338
+ { "readSync", NULL, mic_read_sync, NULL, NULL, NULL, napi_default, NULL },
339
+ { "readAsync", NULL, mic_read_async, NULL, NULL, NULL, napi_default, NULL },
340
+ { "close", NULL, mic_close, NULL, NULL, NULL, napi_default, NULL },
341
+ };
342
+ NAPI_CALL(env, napi_define_properties(env, exports, 4, props));
343
+ return exports;
344
+ }
345
+
346
+ NAPI_MODULE(NODE_GYP_MODULE_NAME, init)