index.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. type AudioCaptureNapi = {
  2. startRecording(
  3. onData: (data: Buffer) => void,
  4. onEnd: () => void,
  5. ): boolean
  6. stopRecording(): void
  7. isRecording(): boolean
  8. startPlayback(sampleRate: number, channels: number): boolean
  9. writePlaybackData(data: Buffer): void
  10. stopPlayback(): void
  11. isPlaying(): boolean
  12. // TCC microphone authorization status (macOS only):
  13. // 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
  14. // Linux: always returns 3 (authorized) — no system-level microphone permission API.
  15. // Windows: returns 3 (authorized) if registry key absent or allowed,
  16. // 2 (denied) if microphone access is explicitly denied.
  17. microphoneAuthorizationStatus?(): number
  18. }
  19. let cachedModule: AudioCaptureNapi | null = null
  20. let loadAttempted = false
  21. function loadModule(): AudioCaptureNapi | null {
  22. if (loadAttempted) {
  23. return cachedModule
  24. }
  25. loadAttempted = true
  26. // Supported platforms: macOS (darwin), Linux, Windows (win32)
  27. const platform = process.platform
  28. if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
  29. return null
  30. }
  31. // Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
  32. // defined at build time in build-with-plugins.ts for native builds only — the
  33. // define resolves it to the static literal "../../audio-capture.node" so bun
  34. // compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
  35. // direct require(env var) — bun cannot analyze require(variable) from a loop.
  36. if (process.env.AUDIO_CAPTURE_NODE_PATH) {
  37. try {
  38. // eslint-disable-next-line @typescript-eslint/no-require-imports
  39. cachedModule = require(
  40. process.env.AUDIO_CAPTURE_NODE_PATH,
  41. ) as AudioCaptureNapi
  42. return cachedModule
  43. } catch {
  44. // fall through to runtime fallbacks below
  45. }
  46. }
  47. // Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
  48. // fine here — in bundled output (node --target build) require() resolves at
  49. // runtime relative to cli.js at the package root; in dev it resolves
  50. // relative to this file (vendor/audio-capture-src/index.ts).
  51. const platformDir = `${process.arch}-${platform}`
  52. const fallbacks = [
  53. `./vendor/audio-capture/${platformDir}/audio-capture.node`,
  54. `../audio-capture/${platformDir}/audio-capture.node`,
  55. ]
  56. for (const p of fallbacks) {
  57. try {
  58. // eslint-disable-next-line @typescript-eslint/no-require-imports
  59. cachedModule = require(p) as AudioCaptureNapi
  60. return cachedModule
  61. } catch {
  62. // try next
  63. }
  64. }
  65. return null
  66. }
  67. export function isNativeAudioAvailable(): boolean {
  68. return loadModule() !== null
  69. }
  70. export function startNativeRecording(
  71. onData: (data: Buffer) => void,
  72. onEnd: () => void,
  73. ): boolean {
  74. const mod = loadModule()
  75. if (!mod) {
  76. return false
  77. }
  78. return mod.startRecording(onData, onEnd)
  79. }
  80. export function stopNativeRecording(): void {
  81. const mod = loadModule()
  82. if (!mod) {
  83. return
  84. }
  85. mod.stopRecording()
  86. }
  87. export function isNativeRecordingActive(): boolean {
  88. const mod = loadModule()
  89. if (!mod) {
  90. return false
  91. }
  92. return mod.isRecording()
  93. }
  94. export function startNativePlayback(
  95. sampleRate: number,
  96. channels: number,
  97. ): boolean {
  98. const mod = loadModule()
  99. if (!mod) {
  100. return false
  101. }
  102. return mod.startPlayback(sampleRate, channels)
  103. }
  104. export function writeNativePlaybackData(data: Buffer): void {
  105. const mod = loadModule()
  106. if (!mod) {
  107. return
  108. }
  109. mod.writePlaybackData(data)
  110. }
  111. export function stopNativePlayback(): void {
  112. const mod = loadModule()
  113. if (!mod) {
  114. return
  115. }
  116. mod.stopPlayback()
  117. }
  118. export function isNativePlaying(): boolean {
  119. const mod = loadModule()
  120. if (!mod) {
  121. return false
  122. }
  123. return mod.isPlaying()
  124. }
  125. // Returns the microphone authorization status.
  126. // On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
  127. // On Linux, always returns 3 (authorized) — no system-level mic permission API.
  128. // On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
  129. // Returns 0 (notDetermined) if the native module is unavailable.
  130. export function microphoneAuthorizationStatus(): number {
  131. const mod = loadModule()
  132. if (!mod || !mod.microphoneAuthorizationStatus) {
  133. return 0
  134. }
  135. return mod.microphoneAuthorizationStatus()
  136. }