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/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "audio-mic",
3
+ "version": "1.0.0",
4
+ "description": "Capture audio from microphone in browser/node",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "browser": "./browser.js",
12
+ "default": "./index.js"
13
+ },
14
+ "./stream": {
15
+ "types": "./stream.d.ts",
16
+ "browser": "./browser.js",
17
+ "default": "./stream.js"
18
+ }
19
+ },
20
+ "browser": {
21
+ "./index.js": "./browser.js"
22
+ },
23
+ "scripts": {
24
+ "build": "node-gyp rebuild",
25
+ "build:x64": "npx node-gyp@latest rebuild --arch=x64 && mkdir -p packages/mic-darwin-x64 && cp build/Release/mic.node packages/mic-darwin-x64/",
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
+ "browser.js",
33
+ "browser.d.ts",
34
+ "stream.js",
35
+ "stream.d.ts",
36
+ "src/",
37
+ "native/",
38
+ "binding.gyp",
39
+ "prebuilds/"
40
+ ],
41
+ "gypfile": true,
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/audiojs/audio-mic"
45
+ },
46
+ "keywords": [
47
+ "audio",
48
+ "audiojs",
49
+ "pcm",
50
+ "microphone",
51
+ "mic",
52
+ "capture",
53
+ "record",
54
+ "input",
55
+ "web-audio",
56
+ "miniaudio"
57
+ ],
58
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
59
+ "license": "MIT",
60
+ "bugs": {
61
+ "url": "https://github.com/audiojs/audio-mic/issues"
62
+ },
63
+ "homepage": "https://github.com/audiojs/audio-mic",
64
+ "engines": {
65
+ "node": ">=18"
66
+ },
67
+ "optionalDependencies": {
68
+ "@audio/mic-darwin-x64": ">=0.0.0",
69
+ "@audio/mic-darwin-arm64": ">=0.0.0",
70
+ "@audio/mic-linux-x64": ">=0.0.0",
71
+ "@audio/mic-linux-arm64": ">=0.0.0",
72
+ "@audio/mic-win32-x64": ">=0.0.0"
73
+ },
74
+ "devDependencies": {
75
+ "prebuildify": "^6.0.1",
76
+ "tst": "^9.4.0"
77
+ }
78
+ }
package/src/backend.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Backend detection — static imports, tries in priority order
3
+ */
4
+ let miniaudio, process_, null_
5
+
6
+ try { miniaudio = await import('./backends/miniaudio.js') } catch {}
7
+ try { process_ = await import('./backends/process.js') } catch {}
8
+ try { null_ = await import('./backends/null.js') } catch {}
9
+
10
+ const backends = [
11
+ miniaudio && { name: 'miniaudio', open: miniaudio.open },
12
+ process_ && { name: 'process', open: process_.open },
13
+ null_ && { name: 'null', open: null_.open },
14
+ ].filter(Boolean)
15
+
16
+ export function open(opts, preference) {
17
+ const order = preference
18
+ ? backends.filter(b => b.name === preference)
19
+ : backends
20
+ let lastErr
21
+
22
+ for (const { name, open } of order) {
23
+ try { return { name, device: open(opts) } }
24
+ catch (e) { lastErr = e }
25
+ }
26
+
27
+ throw new Error(
28
+ 'No audio backend available. Install @audio/mic-* for your platform or run npm run build.' +
29
+ (lastErr ? ` Last error: ${lastErr.message}` : '')
30
+ )
31
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Miniaudio backend — N-API addon wrapping miniaudio.h capture
3
+ *
4
+ * Read strategy:
5
+ * - readAsync: blocks on worker thread until capture data available
6
+ * - Callback fires with each captured chunk — true push from hardware
7
+ *
8
+ * Load order:
9
+ * 1. @audio/mic-{platform}-{arch} (platform package)
10
+ * 2. packages/mic-{platform}-{arch}/
11
+ * 3. prebuilds/{platform}-{arch}/
12
+ * 4. build/Release/ or build/Debug/ (local node-gyp)
13
+ */
14
+ import { createRequire } from 'node:module'
15
+ import { join, dirname } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { arch, platform } from 'node:os'
18
+
19
+ const require = createRequire(import.meta.url)
20
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../..')
21
+ const plat = `${platform()}-${arch()}`
22
+
23
+ let addon
24
+ const loaders = [
25
+ () => require(`@audio/mic-${plat}`),
26
+ () => require(join(root, 'packages', `mic-${plat}`, 'mic.node')),
27
+ () => require(join(root, 'prebuilds', plat, 'audio-mic.node')),
28
+ () => require(join(root, 'prebuilds', plat, 'mic.node')),
29
+ () => require(join(root, 'build', 'Release', 'mic.node')),
30
+ () => require(join(root, 'build', 'Debug', 'mic.node')),
31
+ ]
32
+ for (const load of loaders) {
33
+ try { addon = load(); break } catch {}
34
+ }
35
+ if (!addon) throw new Error('miniaudio addon not found — install @audio/mic-' + plat + ' or run npm run build')
36
+
37
+ export function open({ sampleRate = 44100, channels = 1, bitDepth = 16, bufferSize = 50 } = {}) {
38
+ const handle = addon.open(sampleRate, channels, bitDepth, bufferSize)
39
+ const bpf = channels * (bitDepth / 8)
40
+
41
+ return {
42
+ read(cb) {
43
+ const buf = Buffer.alloc(Math.round(sampleRate * bufferSize / 1000) * bpf)
44
+ addon.readAsync(handle, buf, (err, frames) => {
45
+ if (frames > 0) cb?.(err, buf.subarray(0, frames * bpf))
46
+ else cb?.(err, null)
47
+ })
48
+ },
49
+
50
+ close() {
51
+ addon.close(handle)
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Null backend — silent fallback for headless/CI environments.
3
+ * Maintains timing contract: callback fires after audio duration,
4
+ * delivering silence buffers at real-time rate.
5
+ */
6
+ export function open({ sampleRate = 44100, channels = 1, bitDepth = 16, bufferSize = 50 } = {}) {
7
+ const bpf = channels * (bitDepth / 8)
8
+ const chunkFrames = Math.round(sampleRate * bufferSize / 1000)
9
+ const chunkBytes = chunkFrames * bpf
10
+ let closed = false
11
+
12
+ return {
13
+ read(cb) {
14
+ if (closed) return cb?.(null, null)
15
+ setTimeout(() => {
16
+ if (closed) return cb?.(null, null)
17
+ cb?.(null, Buffer.alloc(chunkBytes))
18
+ }, bufferSize)
19
+ },
20
+ close() { closed = true }
21
+ }
22
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Process backend — capture PCM from system audio tools (last resort)
3
+ */
4
+ import { spawn, execSync } from 'node:child_process'
5
+ import { platform } from 'node:os'
6
+
7
+ function tryExec(cmd) {
8
+ try { execSync(cmd, { stdio: 'ignore' }); return true } catch { return false }
9
+ }
10
+
11
+ function findRecorder(sampleRate, channels, bitDepth) {
12
+ const os = platform()
13
+ const fmt = bitDepth === 8 ? 'u8' : bitDepth === 32 ? 'f32le' : `s${bitDepth}le`
14
+
15
+ if (os === 'darwin' && tryExec('ffmpeg -version'))
16
+ return ['ffmpeg', ['-f', 'avfoundation', '-i', ':default', '-f', fmt, '-ar', sampleRate, '-ac', channels, '-']]
17
+
18
+ if (os === 'linux' && tryExec('ffmpeg -version'))
19
+ return ['ffmpeg', ['-f', 'pulse', '-i', 'default', '-f', fmt, '-ar', sampleRate, '-ac', channels, '-']]
20
+
21
+ if (tryExec('sox --version'))
22
+ return ['sox', ['-d', '-t', 'raw', '-r', sampleRate, '-c', channels, '-b', bitDepth, '-e', 'signed-integer', '-L', '-']]
23
+
24
+ if (os === 'linux' && tryExec('arecord --version')) {
25
+ const afmt = bitDepth === 8 ? 'U8' : bitDepth === 32 ? 'FLOAT_LE' : `S${bitDepth}_LE`
26
+ return ['arecord', ['-f', afmt, '-r', sampleRate, '-c', channels, '-t', 'raw']]
27
+ }
28
+
29
+ return null
30
+ }
31
+
32
+ export function open({ sampleRate = 44100, channels = 1, bitDepth = 16 } = {}) {
33
+ const recorder = findRecorder(sampleRate, channels, bitDepth)
34
+ if (!recorder) throw new Error('No audio recorder found (install ffmpeg or sox)')
35
+
36
+ const [cmd, args] = recorder
37
+ const proc = spawn(cmd, args.map(String), { stdio: ['ignore', 'pipe', 'ignore'] })
38
+
39
+ let closed = false
40
+ let pending = null
41
+
42
+ proc.on('close', () => { closed = true })
43
+
44
+ proc.stdout.on('data', (chunk) => {
45
+ if (pending) {
46
+ const cb = pending
47
+ pending = null
48
+ cb(null, chunk)
49
+ }
50
+ })
51
+
52
+ proc.stdout.on('error', () => {})
53
+
54
+ return {
55
+ read(cb) {
56
+ if (closed) return cb?.(new Error('Process exited'), null)
57
+ pending = cb
58
+ },
59
+
60
+ close() {
61
+ if (closed) return
62
+ closed = true
63
+ pending = null
64
+ proc.stdout.destroy()
65
+ proc.kill()
66
+ }
67
+ }
68
+ }
package/stream.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Readable } from 'node:stream'
2
+ import { MicOptions } from './index.js'
3
+
4
+ export default function readable(opts?: MicOptions): Readable
package/stream.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module audio-mic/stream
3
+ *
4
+ * Node.js Readable stream for audio capture.
5
+ */
6
+ import { Readable } from 'node:stream'
7
+ import mic from './index.js'
8
+
9
+ export default function readable(opts) {
10
+ const { sampleRate = 44100, channels = 1, bitDepth = 16, bufferSize = 50 } = opts || {}
11
+ const read = mic(opts)
12
+
13
+ return new Readable({
14
+ highWaterMark: Math.round(sampleRate * channels * (bitDepth / 8) * bufferSize / 1000),
15
+ read() { pull(this) },
16
+ destroy(err, cb) { read.close(); cb(err) }
17
+ })
18
+
19
+ function pull(stream) {
20
+ if (stream.destroyed) return
21
+ read((err, chunk) => {
22
+ if (stream.destroyed) return
23
+ if (err) return stream.destroy(err)
24
+ if (!chunk) return stream.push(null)
25
+ if (stream.push(chunk)) pull(stream)
26
+ })
27
+ }
28
+ }