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/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
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
|
+
}
|