@audio/speaker 2.3.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,468 @@
1
+ /*
2
+ * audio-speaker native addon
3
+ * Minimal miniaudio N-API binding: ring buffer bridging push writes ↔ pull callback
4
+ *
5
+ * Architecture:
6
+ * - writeSync: non-blocking memcpy into ring buffer (for small/real-time writes)
7
+ * - writeAsync: blocks on worker thread until all data written (for large buffers)
8
+ * - JS tries sync first, falls back to async when ring buffer is full
9
+ * - Optional capture ring buffer for output verification
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
+ /* Speaker instance */
38
+ typedef struct {
39
+ ma_device device;
40
+ ma_pcm_rb ring_buffer;
41
+ ma_pcm_rb capture_rb;
42
+ ma_uint32 channels;
43
+ ma_uint32 sample_rate;
44
+ ma_format format;
45
+ ma_uint32 start_threshold;
46
+ ma_uint32 pull_threshold; /* fire callback when available_read drops below this */
47
+ int started;
48
+ volatile int closed;
49
+ int capture;
50
+ } speaker_t;
51
+
52
+ /* Async write work */
53
+ typedef struct {
54
+ speaker_t* sp;
55
+ void* data;
56
+ size_t byte_length;
57
+ ma_uint32 frames_written;
58
+ napi_async_work work;
59
+ napi_ref callback_ref;
60
+ napi_ref buffer_ref;
61
+ } write_work_t;
62
+
63
+ /* Try to start device if threshold reached */
64
+ static void maybe_start(speaker_t* sp) {
65
+ if (!sp->started && ma_pcm_rb_available_read(&sp->ring_buffer) >= sp->start_threshold) {
66
+ if (ma_device_start(&sp->device) == MA_SUCCESS) {
67
+ sp->started = 1;
68
+ }
69
+ }
70
+ }
71
+
72
+ /* Playback callback — pulls from ring buffer */
73
+ static void playback_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
74
+ speaker_t* sp = (speaker_t*)device->pUserData;
75
+ ma_uint32 bpf = ma_get_bytes_per_frame(sp->format, sp->channels);
76
+
77
+ ma_uint32 total_read = 0;
78
+ while (total_read < frame_count) {
79
+ ma_uint32 to_read = frame_count - total_read;
80
+ void* read_buf;
81
+ if (ma_pcm_rb_acquire_read(&sp->ring_buffer, &to_read, &read_buf) != MA_SUCCESS || to_read == 0) break;
82
+ memcpy((ma_uint8*)output + total_read * bpf, read_buf, to_read * bpf);
83
+ ma_pcm_rb_commit_read(&sp->ring_buffer, to_read);
84
+ total_read += to_read;
85
+ }
86
+ if (total_read < frame_count) {
87
+ memset((ma_uint8*)output + total_read * bpf, 0, (frame_count - total_read) * bpf);
88
+ }
89
+
90
+ if (sp->capture) {
91
+ ma_uint32 total_cap = 0;
92
+ while (total_cap < frame_count) {
93
+ ma_uint32 to_cap = frame_count - total_cap;
94
+ void* cap_buf;
95
+ if (ma_pcm_rb_acquire_write(&sp->capture_rb, &to_cap, &cap_buf) != MA_SUCCESS || to_cap == 0) break;
96
+ memcpy(cap_buf, (ma_uint8*)output + total_cap * bpf, to_cap * bpf);
97
+ ma_pcm_rb_commit_write(&sp->capture_rb, to_cap);
98
+ total_cap += to_cap;
99
+ }
100
+ }
101
+
102
+ (void)input;
103
+ }
104
+
105
+ /* GC destructor */
106
+ static void speaker_destructor(napi_env env, void* data, void* hint) {
107
+ speaker_t* sp = (speaker_t*)data;
108
+ if (!sp) return;
109
+ if (!sp->closed) {
110
+ /* explicit close() was never called — do full device teardown */
111
+ sp->closed = 1;
112
+ if (sp->started) {
113
+ ma_device_stop(&sp->device);
114
+ sp->started = 0;
115
+ }
116
+ ma_device_uninit(&sp->device);
117
+ }
118
+ /* Ring buffer is always freed here, never in speaker_close().
119
+ * This avoids a race: worker thread may still be in its pull-wait loop
120
+ * when close() is called. setting closed=1 exits that loop, the worker
121
+ * finishes, JS fires the callback, then GC collects and we arrive here. */
122
+ ma_pcm_rb_uninit(&sp->ring_buffer);
123
+ if (sp->capture) ma_pcm_rb_uninit(&sp->capture_rb);
124
+ free(sp);
125
+ (void)env;
126
+ (void)hint;
127
+ }
128
+
129
+ /* speaker_open(sampleRate, channels, bitDepth, bufferMs, capture) → external */
130
+ static napi_value speaker_open(napi_env env, napi_callback_info info) {
131
+ size_t argc = 5;
132
+ napi_value argv[5];
133
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
134
+
135
+ if (argc < 3) {
136
+ napi_throw_error(env, NULL, "speaker_open requires (sampleRate, channels, bitDepth[, bufferMs, capture])");
137
+ return NULL;
138
+ }
139
+
140
+ ma_uint32 sample_rate, channels, bit_depth, buffer_ms;
141
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[0], &sample_rate));
142
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[1], &channels));
143
+ NAPI_CALL(env, napi_get_value_uint32(env, argv[2], &bit_depth));
144
+
145
+ buffer_ms = 50;
146
+ if (argc > 3) NAPI_CALL(env, napi_get_value_uint32(env, argv[3], &buffer_ms));
147
+ if (buffer_ms < 10) buffer_ms = 10;
148
+ if (buffer_ms > 2000) buffer_ms = 2000;
149
+
150
+ int capture = 0;
151
+ if (argc > 4) {
152
+ bool val;
153
+ NAPI_CALL(env, napi_get_value_bool(env, argv[4], &val));
154
+ capture = val ? 1 : 0;
155
+ }
156
+
157
+ ma_format format;
158
+ switch (bit_depth) {
159
+ case 8: format = ma_format_u8; break;
160
+ case 16: format = ma_format_s16; break;
161
+ case 24: format = ma_format_s24; break;
162
+ case 32: format = ma_format_f32; break;
163
+ default:
164
+ napi_throw_error(env, NULL, "Unsupported bitDepth (use 8, 16, 24, 32)");
165
+ return NULL;
166
+ }
167
+
168
+ speaker_t* sp = (speaker_t*)calloc(1, sizeof(speaker_t));
169
+ if (!sp) {
170
+ napi_throw_error(env, NULL, "Failed to allocate speaker");
171
+ return NULL;
172
+ }
173
+
174
+ sp->channels = channels;
175
+ sp->sample_rate = sample_rate;
176
+ sp->format = format;
177
+ sp->capture = capture;
178
+
179
+ /* ring buffer */
180
+ ma_uint32 rb_frames = (sample_rate * buffer_ms) / 1000;
181
+ ma_uint32 rb_pow2 = 1;
182
+ while (rb_pow2 < rb_frames) rb_pow2 <<= 1;
183
+ sp->start_threshold = rb_pow2 / 2;
184
+ sp->pull_threshold = rb_pow2 / 2; /* callback fires when buffer drops below 50% — gives ~25ms headroom, absorbs Windows ~15ms sleep granularity */
185
+
186
+ ma_result result = ma_pcm_rb_init(format, channels, rb_pow2, NULL, NULL, &sp->ring_buffer);
187
+ if (result != MA_SUCCESS) {
188
+ free(sp);
189
+ napi_throw_error(env, NULL, "Failed to init ring buffer");
190
+ return NULL;
191
+ }
192
+
193
+ /* capture ring buffer */
194
+ if (capture) {
195
+ ma_uint32 cap_frames = sample_rate * 5;
196
+ ma_uint32 cap_pow2 = 1;
197
+ while (cap_pow2 < cap_frames) cap_pow2 <<= 1;
198
+ result = ma_pcm_rb_init(format, channels, cap_pow2, NULL, NULL, &sp->capture_rb);
199
+ if (result != MA_SUCCESS) {
200
+ ma_pcm_rb_uninit(&sp->ring_buffer);
201
+ free(sp);
202
+ napi_throw_error(env, NULL, "Failed to init capture ring buffer");
203
+ return NULL;
204
+ }
205
+ }
206
+
207
+ /* audio device (started later) */
208
+ ma_device_config config = ma_device_config_init(ma_device_type_playback);
209
+ config.playback.format = format;
210
+ config.playback.channels = channels;
211
+ config.sampleRate = sample_rate;
212
+ config.dataCallback = playback_callback;
213
+ config.pUserData = sp;
214
+ config.performanceProfile = ma_performance_profile_low_latency;
215
+
216
+ result = ma_device_init(NULL, &config, &sp->device);
217
+ if (result != MA_SUCCESS) {
218
+ ma_pcm_rb_uninit(&sp->ring_buffer);
219
+ if (capture) ma_pcm_rb_uninit(&sp->capture_rb);
220
+ free(sp);
221
+ napi_throw_error(env, NULL, "Failed to init audio device");
222
+ return NULL;
223
+ }
224
+
225
+ sp->started = 0;
226
+
227
+ napi_value external;
228
+ NAPI_CALL(env, napi_create_external(env, sp, speaker_destructor, NULL, &external));
229
+ return external;
230
+ }
231
+
232
+ /*
233
+ * speaker_writeSync(handle, buffer) → framesWritten
234
+ * Non-blocking: writes as many frames as ring buffer can accept right now.
235
+ * Returns immediately. Zero overhead per call.
236
+ */
237
+ static napi_value speaker_write_sync(napi_env env, napi_callback_info info) {
238
+ size_t argc = 2;
239
+ napi_value argv[2];
240
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
241
+
242
+ speaker_t* sp;
243
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&sp));
244
+
245
+ if (sp->closed) {
246
+ napi_value result;
247
+ NAPI_CALL(env, napi_create_int32(env, 0, &result));
248
+ return result;
249
+ }
250
+
251
+ void* data;
252
+ size_t byte_length;
253
+ NAPI_CALL(env, napi_get_buffer_info(env, argv[1], &data, &byte_length));
254
+
255
+ ma_uint32 bpf = ma_get_bytes_per_frame(sp->format, sp->channels);
256
+ if (bpf == 0) {
257
+ napi_value result;
258
+ NAPI_CALL(env, napi_create_int32(env, 0, &result));
259
+ return result;
260
+ }
261
+
262
+ ma_uint32 total = (ma_uint32)(byte_length / bpf);
263
+ ma_uint32 written = 0;
264
+
265
+ while (written < total) {
266
+ ma_uint32 to_write = total - written;
267
+ void* write_buf;
268
+ ma_result res = ma_pcm_rb_acquire_write(&sp->ring_buffer, &to_write, &write_buf);
269
+ if (res != MA_SUCCESS || to_write == 0) break;
270
+ memcpy(write_buf, (ma_uint8*)data + written * bpf, to_write * bpf);
271
+ ma_pcm_rb_commit_write(&sp->ring_buffer, to_write);
272
+ written += to_write;
273
+ }
274
+
275
+ maybe_start(sp);
276
+
277
+ napi_value result;
278
+ NAPI_CALL(env, napi_create_uint32(env, written, &result));
279
+ return result;
280
+ }
281
+
282
+ /* Async write — blocks on worker thread until all data written */
283
+ static void write_execute(napi_env env, void* data) {
284
+ write_work_t* w = (write_work_t*)data;
285
+ speaker_t* sp = w->sp;
286
+ ma_uint32 bpf = ma_get_bytes_per_frame(sp->format, sp->channels);
287
+ if (bpf == 0) { w->frames_written = 0; return; }
288
+
289
+ ma_uint32 total = (ma_uint32)(w->byte_length / bpf);
290
+ ma_uint32 written = 0;
291
+
292
+ while (written < total && !sp->closed) {
293
+ ma_uint32 to_write = total - written;
294
+ void* write_buf;
295
+ ma_result res = ma_pcm_rb_acquire_write(&sp->ring_buffer, &to_write, &write_buf);
296
+ if (res == MA_SUCCESS && to_write > 0) {
297
+ memcpy(write_buf, (ma_uint8*)w->data + written * bpf, to_write * bpf);
298
+ ma_pcm_rb_commit_write(&sp->ring_buffer, to_write);
299
+ written += to_write;
300
+ maybe_start(sp);
301
+ } else {
302
+ ma_sleep(1);
303
+ }
304
+ }
305
+
306
+ if (!sp->started && written > 0 && !sp->closed) {
307
+ if (ma_device_start(&sp->device) == MA_SUCCESS) {
308
+ sp->started = 1;
309
+ }
310
+ }
311
+
312
+ /* Pull pacing: wait until hardware consumes enough data before firing callback.
313
+ * This makes the callback fire when the device NEEDS more data, not when the
314
+ * ring buffer has space. Eliminates render-loop overrun without wall-clock hacks. */
315
+ if (sp->started && !sp->closed) {
316
+ while (!sp->closed && ma_pcm_rb_available_read(&sp->ring_buffer) >= sp->pull_threshold) {
317
+ ma_sleep(1);
318
+ }
319
+ }
320
+
321
+ w->frames_written = written;
322
+ }
323
+
324
+ static void write_complete(napi_env env, napi_status status, void* data) {
325
+ write_work_t* w = (write_work_t*)data;
326
+
327
+ napi_value callback, global, argv[2];
328
+ napi_get_reference_value(env, w->callback_ref, &callback);
329
+ napi_get_global(env, &global);
330
+
331
+ napi_get_null(env, &argv[0]);
332
+ napi_create_uint32(env, w->frames_written, &argv[1]);
333
+ napi_call_function(env, global, callback, 2, argv, NULL);
334
+
335
+ napi_delete_reference(env, w->callback_ref);
336
+ napi_delete_reference(env, w->buffer_ref);
337
+ napi_delete_async_work(env, w->work);
338
+ free(w);
339
+ }
340
+
341
+ /* speaker_writeAsync(handle, buffer, callback) */
342
+ static napi_value speaker_write_async(napi_env env, napi_callback_info info) {
343
+ size_t argc = 3;
344
+ napi_value argv[3];
345
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
346
+
347
+ speaker_t* sp;
348
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&sp));
349
+
350
+ if (sp->closed) {
351
+ napi_throw_error(env, NULL, "Speaker is closed");
352
+ return NULL;
353
+ }
354
+
355
+ void* data;
356
+ size_t byte_length;
357
+ NAPI_CALL(env, napi_get_buffer_info(env, argv[1], &data, &byte_length));
358
+
359
+ write_work_t* w = (write_work_t*)calloc(1, sizeof(write_work_t));
360
+ w->sp = sp;
361
+ w->data = data;
362
+ w->byte_length = byte_length;
363
+
364
+ NAPI_CALL(env, napi_create_reference(env, argv[2], 1, &w->callback_ref));
365
+ NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &w->buffer_ref));
366
+
367
+ napi_value work_name;
368
+ NAPI_CALL(env, napi_create_string_utf8(env, "speaker_write", NAPI_AUTO_LENGTH, &work_name));
369
+ NAPI_CALL(env, napi_create_async_work(env, NULL, work_name, write_execute, write_complete, w, &w->work));
370
+ NAPI_CALL(env, napi_queue_async_work(env, w->work));
371
+
372
+ return NULL;
373
+ }
374
+
375
+ /* speaker_read(handle, buffer) → frames */
376
+ static napi_value speaker_read(napi_env env, napi_callback_info info) {
377
+ size_t argc = 2;
378
+ napi_value argv[2];
379
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
380
+
381
+ speaker_t* sp;
382
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&sp));
383
+
384
+ if (!sp->capture) {
385
+ napi_throw_error(env, NULL, "Capture not enabled");
386
+ return NULL;
387
+ }
388
+
389
+ void* data;
390
+ size_t byte_length;
391
+ NAPI_CALL(env, napi_get_buffer_info(env, argv[1], &data, &byte_length));
392
+
393
+ ma_uint32 bpf = ma_get_bytes_per_frame(sp->format, sp->channels);
394
+ ma_uint32 max_frames = (ma_uint32)(byte_length / bpf);
395
+ ma_uint32 frames_read = 0;
396
+
397
+ while (frames_read < max_frames) {
398
+ ma_uint32 to_read = max_frames - frames_read;
399
+ void* read_buf;
400
+ if (ma_pcm_rb_acquire_read(&sp->capture_rb, &to_read, &read_buf) != MA_SUCCESS || to_read == 0) break;
401
+ memcpy((ma_uint8*)data + frames_read * bpf, read_buf, to_read * bpf);
402
+ ma_pcm_rb_commit_read(&sp->capture_rb, to_read);
403
+ frames_read += to_read;
404
+ }
405
+
406
+ napi_value result;
407
+ NAPI_CALL(env, napi_create_uint32(env, frames_read, &result));
408
+ return result;
409
+ }
410
+
411
+ /* speaker_flush(handle) → boolean */
412
+ static napi_value speaker_flush(napi_env env, napi_callback_info info) {
413
+ size_t argc = 1;
414
+ napi_value argv[1];
415
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
416
+
417
+ speaker_t* sp;
418
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&sp));
419
+
420
+ if (!sp->started && ma_pcm_rb_available_read(&sp->ring_buffer) > 0) {
421
+ if (ma_device_start(&sp->device) == MA_SUCCESS) {
422
+ sp->started = 1;
423
+ }
424
+ }
425
+
426
+ int flushed = (ma_pcm_rb_available_read(&sp->ring_buffer) == 0);
427
+ napi_value result;
428
+ NAPI_CALL(env, napi_get_boolean(env, flushed, &result));
429
+ return result;
430
+ }
431
+
432
+ /* speaker_close(handle) */
433
+ static napi_value speaker_close(napi_env env, napi_callback_info info) {
434
+ size_t argc = 1;
435
+ napi_value argv[1];
436
+ NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
437
+
438
+ speaker_t* sp;
439
+ NAPI_CALL(env, napi_get_value_external(env, argv[0], (void**)&sp));
440
+
441
+ if (!sp->closed) {
442
+ sp->closed = 1; /* signals worker pull-wait loop to exit */
443
+ if (sp->started) {
444
+ ma_device_stop(&sp->device);
445
+ sp->started = 0;
446
+ }
447
+ ma_device_uninit(&sp->device);
448
+ /* Ring buffer freed by GC destructor — worker may still be in pull-wait loop */
449
+ }
450
+
451
+ return NULL;
452
+ }
453
+
454
+ /* Module init */
455
+ static napi_value init(napi_env env, napi_value exports) {
456
+ napi_property_descriptor props[] = {
457
+ { "open", NULL, speaker_open, NULL, NULL, NULL, napi_default, NULL },
458
+ { "writeSync", NULL, speaker_write_sync, NULL, NULL, NULL, napi_default, NULL },
459
+ { "writeAsync", NULL, speaker_write_async, NULL, NULL, NULL, napi_default, NULL },
460
+ { "read", NULL, speaker_read, NULL, NULL, NULL, napi_default, NULL },
461
+ { "flush", NULL, speaker_flush, NULL, NULL, NULL, napi_default, NULL },
462
+ { "close", NULL, speaker_close, NULL, NULL, NULL, napi_default, NULL },
463
+ };
464
+ NAPI_CALL(env, napi_define_properties(env, exports, 6, props));
465
+ return exports;
466
+ }
467
+
468
+ NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@audio/speaker",
3
+ "version": "2.3.1",
4
+ "description": "Output audio data to speaker in browser/node",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "browser": "./browser.js",
13
+ "default": "./index.js"
14
+ },
15
+ "./stream": {
16
+ "types": "./stream.d.ts",
17
+ "browser": "./browser.js",
18
+ "default": "./stream.js"
19
+ }
20
+ },
21
+ "browser": {
22
+ "./index.js": "./browser.js"
23
+ },
24
+ "scripts": {
25
+ "build": "node-gyp rebuild",
26
+ "test": "node test.js",
27
+ "version": "node -e \"const v=require('./package.json').version,fs=require('fs');for(const d of fs.readdirSync('packages')){const f='packages/'+d+'/package.json',p=JSON.parse(fs.readFileSync(f));p.version=v;fs.writeFileSync(f,JSON.stringify(p,null,2)+'\\n')}\" && git add packages/*/package.json"
28
+ },
29
+ "files": [
30
+ "index.js",
31
+ "index.d.ts",
32
+ "stream.js",
33
+ "stream.d.ts",
34
+ "browser.js",
35
+ "browser.d.ts",
36
+ "src/",
37
+ "native/speaker.c",
38
+ "native/miniaudio.h",
39
+ "binding.gyp",
40
+ "prebuilds/"
41
+ ],
42
+ "gypfile": true,
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/audiojs/speaker.git"
46
+ },
47
+ "keywords": [
48
+ "audio",
49
+ "audiojs",
50
+ "pcm",
51
+ "speaker",
52
+ "web-audio",
53
+ "sound",
54
+ "sink",
55
+ "miniaudio"
56
+ ],
57
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
58
+ "license": "MIT",
59
+ "bugs": {
60
+ "url": "https://github.com/audiojs/speaker/issues"
61
+ },
62
+ "homepage": "https://github.com/audiojs/speaker#readme",
63
+ "engines": {
64
+ "node": ">=18"
65
+ },
66
+ "optionalDependencies": {
67
+ "@audio/speaker-darwin-arm64": ">=2.0.0",
68
+ "@audio/speaker-darwin-x64": ">=2.0.0",
69
+ "@audio/speaker-darwin-arm64": ">=2.0.0",
70
+ "@audio/speaker-linux-arm64": ">=2.0.0",
71
+ "@audio/speaker-linux-x64": ">=2.0.0",
72
+ "@audio/speaker-win32-x64": ">=2.0.0"
73
+ },
74
+ "devDependencies": {
75
+ "prebuildify": "^6.0.1",
76
+ "tst": "^9.3.0"
77
+ },
78
+ "publishConfig": {
79
+ "access": "public"
80
+ }
81
+ }
package/readme.md ADDED
@@ -0,0 +1,123 @@
1
+ # @audio/speaker [![test](https://github.com/audiojs/speaker/actions/workflows/test.yml/badge.svg)](https://github.com/audiojs/speaker/actions/workflows/test.yml) [![npm](https://img.shields.io/npm/v/@audio/speaker)](https://npmjs.org/package/@audio/speaker)
2
+
3
+ Output audio data to speaker in node or browser.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ import speaker from '@audio/speaker'
9
+
10
+ let write = speaker({
11
+ sampleRate: 44100,
12
+ channels: 2,
13
+ bitDepth: 16,
14
+ // bufferSize: 50, // ring buffer ms (default 50)
15
+ // backend: 'miniaudio', // force backend
16
+ })
17
+
18
+ write(pcmBuffer, (err) => {
19
+ // ready for next chunk
20
+ })
21
+ write(null) // end playback
22
+ ```
23
+
24
+ ### Async iterable
25
+
26
+ Consume an async iterable source directly:
27
+
28
+ ```js
29
+ import speaker from '@audio/speaker'
30
+
31
+ await speaker.from(audioSource, { sampleRate: 44100, channels: 2 })
32
+ ```
33
+
34
+ ### Stream
35
+
36
+ ```js
37
+ import SpeakerWritable from '@audio/speaker/stream'
38
+
39
+ source.pipe(SpeakerWritable({ sampleRate: 44100, channels: 2 }))
40
+ ```
41
+
42
+ ## Backends
43
+
44
+ Tried in order; first successful one wins.
45
+
46
+ | Backend | How | Latency | Install |
47
+ |---|---|---|---|
48
+ | `miniaudio` | N-API addon wrapping [miniaudio.h](https://github.com/mackron/miniaudio) | Low | Prebuilt via `@audio/speaker-*` packages |
49
+ | `process` | Pipes PCM to ffplay/sox/aplay | High | System tool must be installed |
50
+ | `null` | Silent, maintains timing contract | — | Built-in (CI/headless fallback) |
51
+ | `webaudio` | Web Audio API (browser only) | Low | Built-in |
52
+
53
+ ## API
54
+
55
+ ### `write = speaker(opts?)`
56
+
57
+ Returns a sink function. Options:
58
+
59
+ - `sampleRate` — default `44100`
60
+ - `channels` — default `2`
61
+ - `bitDepth` — `8`, `16` (default), `24`, `32`
62
+ - `bufferSize` — ring buffer in ms, default `50`
63
+ - `backend` — force a specific backend
64
+
65
+ ### `write(buffer, cb?)`
66
+
67
+ Write PCM data. Accepts `Buffer`, `Uint8Array`, or `AudioBuffer`. Callback fires when ready for next chunk.
68
+
69
+ ### `write(null)`
70
+
71
+ End playback. Flushes remaining audio then closes device.
72
+
73
+ ### `write.flush(cb?)`
74
+
75
+ Wait for buffered audio to finish playing.
76
+
77
+ ### `write.close()`
78
+
79
+ Immediately close the audio device.
80
+
81
+ ### `write.backend`
82
+
83
+ Name of the active backend (`'miniaudio'`, `'process'`, `'null'`, `'webaudio'`).
84
+
85
+ ## Building
86
+
87
+ ```sh
88
+ npm run build # compile native addon locally
89
+ npm test # run tests
90
+ ```
91
+
92
+ ## Publishing
93
+
94
+ ```sh
95
+ # JS-only change (no native code changed):
96
+ npm version patch && git push && git push --tags
97
+ npm publish
98
+
99
+ # Native code changed — rebuild platform packages:
100
+ npm version patch && git push && git push --tags
101
+ gh run watch # wait for CI
102
+ rm -rf artifacts
103
+ gh run download --dir artifacts \
104
+ -n speaker-darwin-arm64 -n speaker-darwin-x64 \
105
+ -n speaker-linux-x64 -n speaker-linux-arm64 -n speaker-win32-x64
106
+
107
+ # (fallback) If darwin-x64 CI is unavailable, cross-compile locally
108
+ npx node-gyp@latest rebuild --arch=x64
109
+ mkdir -p artifacts/speaker-darwin-x64
110
+ cp build/Release/speaker.node artifacts/speaker-darwin-x64/
111
+
112
+ for pkg in packages/speaker-*/; do
113
+ cp artifacts/$(basename $pkg)/speaker.node $pkg/
114
+ (cd $pkg && npm publish)
115
+ done
116
+ npm publish
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
122
+
123
+ <a href="https://github.com/krishnized/license/">ॐ</a>
package/src/backend.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Backend detection — static imports, open() tries in priority order
3
+ */
4
+ import { open as miniaudioOpen } from './backends/miniaudio.js'
5
+ import { open as processOpen } from './backends/process.js'
6
+ import { open as nullOpen } from './backends/null.js'
7
+
8
+ const backends = [
9
+ { name: 'miniaudio', open: miniaudioOpen },
10
+ { name: 'process', open: processOpen },
11
+ { name: 'null', open: nullOpen },
12
+ ]
13
+
14
+ export function open(opts, preference) {
15
+ const order = preference
16
+ ? backends.filter(b => b.name === preference)
17
+ : backends
18
+ let lastErr
19
+
20
+ for (const { name, open } of order) {
21
+ try { return { name, device: open(opts) } }
22
+ catch (e) { lastErr = e }
23
+ }
24
+
25
+ throw new Error(
26
+ 'No audio backend available. Install @audio/speaker-* for your platform or run npm run build.' +
27
+ (lastErr ? ` Last error: ${lastErr.message}` : '')
28
+ )
29
+ }