audio.mjs 3.6 KB

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