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 +109 -0
- package/binding.gyp +38 -0
- package/browser.d.ts +19 -0
- package/browser.js +132 -0
- package/index.d.ts +17 -0
- package/index.js +35 -0
- package/native/mic.c +346 -0
- package/native/miniaudio.h +95864 -0
- package/package.json +78 -0
- package/src/backend.js +31 -0
- package/src/backends/miniaudio.js +54 -0
- package/src/backends/null.js +22 -0
- package/src/backends/process.js +68 -0
- package/stream.d.ts +4 -0
- package/stream.js +28 -0
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)
|