| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- type AudioCaptureNapi = {
- startRecording(
- onData: (data: Buffer) => void,
- onEnd: () => void,
- ): boolean
- stopRecording(): void
- isRecording(): boolean
- startPlayback(sampleRate: number, channels: number): boolean
- writePlaybackData(data: Buffer): void
- stopPlayback(): void
- isPlaying(): boolean
- // TCC microphone authorization status (macOS only):
- // 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
- // Linux: always returns 3 (authorized) — no system-level microphone permission API.
- // Windows: returns 3 (authorized) if registry key absent or allowed,
- // 2 (denied) if microphone access is explicitly denied.
- microphoneAuthorizationStatus?(): number
- }
- let cachedModule: AudioCaptureNapi | null = null
- let loadAttempted = false
- function loadModule(): AudioCaptureNapi | null {
- if (loadAttempted) {
- return cachedModule
- }
- loadAttempted = true
- // Supported platforms: macOS (darwin), Linux, Windows (win32)
- const platform = process.platform
- if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
- return null
- }
- // Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
- // defined at build time in build-with-plugins.ts for native builds only — the
- // define resolves it to the static literal "../../audio-capture.node" so bun
- // compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
- // direct require(env var) — bun cannot analyze require(variable) from a loop.
- if (process.env.AUDIO_CAPTURE_NODE_PATH) {
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- cachedModule = require(
- process.env.AUDIO_CAPTURE_NODE_PATH,
- ) as AudioCaptureNapi
- return cachedModule
- } catch {
- // fall through to runtime fallbacks below
- }
- }
- // Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
- // fine here — in bundled output (node --target build) require() resolves at
- // runtime relative to cli.js at the package root; in dev it resolves
- // relative to this file (vendor/audio-capture-src/index.ts).
- const platformDir = `${process.arch}-${platform}`
- const fallbacks = [
- `./vendor/audio-capture/${platformDir}/audio-capture.node`,
- `../audio-capture/${platformDir}/audio-capture.node`,
- ]
- for (const p of fallbacks) {
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- cachedModule = require(p) as AudioCaptureNapi
- return cachedModule
- } catch {
- // try next
- }
- }
- return null
- }
- export function isNativeAudioAvailable(): boolean {
- return loadModule() !== null
- }
- export function startNativeRecording(
- onData: (data: Buffer) => void,
- onEnd: () => void,
- ): boolean {
- const mod = loadModule()
- if (!mod) {
- return false
- }
- return mod.startRecording(onData, onEnd)
- }
- export function stopNativeRecording(): void {
- const mod = loadModule()
- if (!mod) {
- return
- }
- mod.stopRecording()
- }
- export function isNativeRecordingActive(): boolean {
- const mod = loadModule()
- if (!mod) {
- return false
- }
- return mod.isRecording()
- }
- export function startNativePlayback(
- sampleRate: number,
- channels: number,
- ): boolean {
- const mod = loadModule()
- if (!mod) {
- return false
- }
- return mod.startPlayback(sampleRate, channels)
- }
- export function writeNativePlaybackData(data: Buffer): void {
- const mod = loadModule()
- if (!mod) {
- return
- }
- mod.writePlaybackData(data)
- }
- export function stopNativePlayback(): void {
- const mod = loadModule()
- if (!mod) {
- return
- }
- mod.stopPlayback()
- }
- export function isNativePlaying(): boolean {
- const mod = loadModule()
- if (!mod) {
- return false
- }
- return mod.isPlaying()
- }
- // Returns the microphone authorization status.
- // On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
- // On Linux, always returns 3 (authorized) — no system-level mic permission API.
- // On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
- // Returns 0 (notDetermined) if the native module is unavailable.
- export function microphoneAuthorizationStatus(): number {
- const mod = loadModule()
- if (!mod || !mod.microphoneAuthorizationStatus) {
- return 0
- }
- return mod.microphoneAuthorizationStatus()
- }
|