@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/README.md ADDED
@@ -0,0 +1,120 @@
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
+ ### Async iterable
25
+
26
+ ```js
27
+ import mic from '@audio/mic'
28
+
29
+ let read = mic({ sampleRate: 44100, channels: 1 })
30
+ for await (let chunk of read) {
31
+ // process PCM chunk
32
+ }
33
+ ```
34
+
35
+ ### Node Readable
36
+
37
+ ```js
38
+ import MicReadable from '@audio/mic/stream'
39
+
40
+ MicReadable({ sampleRate: 44100, channels: 1 }).pipe(dest)
41
+ ```
42
+
43
+ ## Backends
44
+
45
+ Tried in order; first successful one wins.
46
+
47
+ | Backend | How | Latency | Install |
48
+ |---|---|---|---|
49
+ | `miniaudio` | N-API addon wrapping [miniaudio.h](https://github.com/mackron/miniaudio) | Low | Prebuilt via `@audio/mic-*` packages |
50
+ | `process` | Pipes from ffmpeg/sox/arecord | High | System tool must be installed |
51
+ | `null` | Silent, maintains timing contract | — | Built-in (CI/headless fallback) |
52
+ | `mediastream` | getUserMedia + AudioWorklet (browser) | Low | Built-in |
53
+
54
+ ## API
55
+
56
+ ### `read = mic(opts?)`
57
+
58
+ Returns a source function. Options:
59
+
60
+ - `sampleRate` — default `44100`
61
+ - `channels` — default `1`
62
+ - `bitDepth` — `8`, `16` (default), `24`, `32`
63
+ - `bufferSize` — ring buffer in ms, default `50`
64
+ - `backend` — force a specific backend
65
+
66
+ ### `read(cb)`
67
+
68
+ Read PCM data. Callback fires with each captured chunk: `(err, buffer) => {}`.
69
+
70
+ ### `read(null)`
71
+
72
+ Stop capture. Closes the audio device.
73
+
74
+ ### `read.close()`
75
+
76
+ Immediately close the audio device.
77
+
78
+ ### `read.backend`
79
+
80
+ Name of the active backend (`'miniaudio'`, `'process'`, `'null'`, `'mediastream'`).
81
+
82
+ ## Building
83
+
84
+ ```sh
85
+ npm run build # compile native addon locally
86
+ npm test # run tests
87
+ ```
88
+
89
+ ## Publishing
90
+
91
+ ```sh
92
+ # JS-only change (no native code changed):
93
+ npm version patch && git push && git push --tags
94
+ npm publish
95
+
96
+ # Native code changed — rebuild platform packages:
97
+ npm version patch && git push && git push --tags
98
+ gh run watch # wait for CI
99
+ rm -rf artifacts
100
+ gh run download --dir artifacts \
101
+ -n mic-darwin-arm64 -n mic-darwin-x64 \
102
+ -n mic-linux-x64 -n mic-linux-arm64 -n mic-win32-x64
103
+
104
+ # (fallback) If darwin-x64 CI is unavailable, cross-compile locally:
105
+ npx node-gyp@latest rebuild --arch=x64
106
+ mkdir -p artifacts/mic-darwin-x64
107
+ cp build/Release/mic.node artifacts/mic-darwin-x64/
108
+
109
+ for pkg in packages/mic-*/; do
110
+ cp artifacts/$(basename $pkg)/mic.node $pkg/
111
+ (cd $pkg && npm publish)
112
+ done
113
+ npm publish
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT
119
+
120
+ <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,142 @@
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
+ read[Symbol.asyncIterator] = () => ({
89
+ next: () => new Promise((resolve) => {
90
+ if (closed) return resolve({ done: true, value: undefined })
91
+ if (ctx.state === 'suspended') ctx.resume()
92
+ pending = (err, chunk) => {
93
+ if (err || !chunk) return resolve({ done: true, value: undefined })
94
+ resolve({ done: false, value: chunk })
95
+ }
96
+ })
97
+ })
98
+
99
+ return read
100
+
101
+ function read(cb) {
102
+ if (cb == null || closed) {
103
+ close()
104
+ return
105
+ }
106
+ // resume suspended context (autoplay policy)
107
+ if (ctx.state === 'suspended') ctx.resume()
108
+ pending = cb
109
+ }
110
+
111
+ function close() {
112
+ if (closed) return
113
+ closed = true
114
+ pending = null
115
+ source.disconnect()
116
+ stream.getTracks().forEach(t => t.stop())
117
+ if (ownCtx) ctx.close?.()
118
+ }
119
+
120
+ function float32ToPCM(channelData, bits) {
121
+ const ch = channelData.length
122
+ const len = channelData[0].length
123
+ const bps = bits / 8
124
+ const buf = new Uint8Array(len * ch * bps)
125
+ const view = new DataView(buf.buffer)
126
+
127
+ for (let i = 0; i < len; i++) {
128
+ for (let c = 0; c < ch; c++) {
129
+ const sample = channelData[c][i]
130
+ const offset = (i * ch + c) * bps
131
+ if (bits === 16) {
132
+ view.setInt16(offset, Math.max(-32768, Math.min(32767, Math.round(sample * 32767))), true)
133
+ } else if (bits === 32) {
134
+ view.setFloat32(offset, sample, true)
135
+ } else if (bits === 8) {
136
+ buf[offset] = Math.max(0, Math.min(255, Math.round((sample + 1) * 127.5)))
137
+ }
138
+ }
139
+ }
140
+ return buf
141
+ }
142
+ }
package/index.d.ts ADDED
@@ -0,0 +1,18 @@
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
+ [Symbol.asyncIterator](): AsyncIterator<Buffer | Uint8Array>
16
+ }
17
+
18
+ export default function mic(opts?: MicOptions): ReadFn
package/index.js ADDED
@@ -0,0 +1,44 @@
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
+ read[Symbol.asyncIterator] = () => ({
26
+ next: () => new Promise((resolve, reject) => {
27
+ device.read((err, chunk) => {
28
+ if (err) return reject(err)
29
+ if (!chunk) return resolve({ done: true, value: undefined })
30
+ resolve({ done: false, value: chunk })
31
+ })
32
+ })
33
+ })
34
+
35
+ return read
36
+
37
+ function read(cb) {
38
+ if (cb == null) {
39
+ device.close()
40
+ return
41
+ }
42
+ device.read(cb)
43
+ }
44
+ }