@audio/mic 1.1.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/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)