audio.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { spawn } from 'node:child_process';
  2. import { Readable } from 'node:stream';
  3. import { platform, versions } from 'node:process';
  4. import { checkFileSupport } from "../internal/uploads.mjs";
  5. const DEFAULT_SAMPLE_RATE = 24000;
  6. const DEFAULT_CHANNELS = 1;
  7. const isNode = Boolean(versions?.node);
  8. const recordingProviders = {
  9. win32: 'dshow',
  10. darwin: 'avfoundation',
  11. linux: 'alsa',
  12. aix: 'alsa',
  13. android: 'alsa',
  14. freebsd: 'alsa',
  15. haiku: 'alsa',
  16. sunos: 'alsa',
  17. netbsd: 'alsa',
  18. openbsd: 'alsa',
  19. cygwin: 'dshow',
  20. };
  21. function isResponse(stream) {
  22. return typeof stream.body !== 'undefined';
  23. }
  24. function isFile(stream) {
  25. checkFileSupport();
  26. return stream instanceof File;
  27. }
  28. async function nodejsPlayAudio(stream) {
  29. return new Promise((resolve, reject) => {
  30. try {
  31. const ffplay = spawn('ffplay', ['-autoexit', '-nodisp', '-i', 'pipe:0']);
  32. if (isResponse(stream)) {
  33. stream.body.pipe(ffplay.stdin);
  34. }
  35. else if (isFile(stream)) {
  36. Readable.from(stream.stream()).pipe(ffplay.stdin);
  37. }
  38. else {
  39. stream.pipe(ffplay.stdin);
  40. }
  41. ffplay.on('close', (code) => {
  42. if (code !== 0) {
  43. reject(new Error(`ffplay process exited with code ${code}`));
  44. }
  45. resolve();
  46. });
  47. }
  48. catch (error) {
  49. reject(error);
  50. }
  51. });
  52. }
  53. export async function playAudio(input) {
  54. if (isNode) {
  55. return nodejsPlayAudio(input);
  56. }
  57. throw new Error('Play audio is not supported in the browser yet. Check out https://npm.im/wavtools as an alternative.');
  58. }
  59. function nodejsRecordAudio({ signal, device, timeout } = {}) {
  60. checkFileSupport();
  61. return new Promise((resolve, reject) => {
  62. const data = [];
  63. const provider = recordingProviders[platform];
  64. try {
  65. const ffmpeg = spawn('ffmpeg', [
  66. '-f',
  67. provider,
  68. '-i',
  69. `:${device ?? 0}`, // default audio input device; adjust as needed
  70. '-ar',
  71. DEFAULT_SAMPLE_RATE.toString(),
  72. '-ac',
  73. DEFAULT_CHANNELS.toString(),
  74. '-f',
  75. 'wav',
  76. 'pipe:1',
  77. ], {
  78. stdio: ['ignore', 'pipe', 'pipe'],
  79. });
  80. ffmpeg.stdout.on('data', (chunk) => {
  81. data.push(chunk);
  82. });
  83. ffmpeg.on('error', (error) => {
  84. console.error(error);
  85. reject(error);
  86. });
  87. ffmpeg.on('close', (code) => {
  88. returnData();
  89. });
  90. function returnData() {
  91. const audioBuffer = Buffer.concat(data);
  92. const audioFile = new File([audioBuffer], 'audio.wav', { type: 'audio/wav' });
  93. resolve(audioFile);
  94. }
  95. if (typeof timeout === 'number' && timeout > 0) {
  96. const internalSignal = AbortSignal.timeout(timeout);
  97. internalSignal.addEventListener('abort', () => {
  98. ffmpeg.kill('SIGTERM');
  99. });
  100. }
  101. if (signal) {
  102. signal.addEventListener('abort', () => {
  103. ffmpeg.kill('SIGTERM');
  104. });
  105. }
  106. }
  107. catch (error) {
  108. reject(error);
  109. }
  110. });
  111. }
  112. export async function recordAudio(options = {}) {
  113. if (isNode) {
  114. return nodejsRecordAudio(options);
  115. }
  116. throw new Error('Record audio is not supported in the browser. Check out https://npm.im/wavtools as an alternative.');
  117. }
  118. //# sourceMappingURL=audio.mjs.map