audio.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. export class AudioHandler {
  2. private context: AudioContext;
  3. private workletNode: AudioWorkletNode | null = null;
  4. private stream: MediaStream | null = null;
  5. private source: MediaStreamAudioSourceNode | null = null;
  6. private readonly sampleRate = 24000;
  7. private nextPlayTime: number = 0;
  8. private isPlaying: boolean = false;
  9. private playbackQueue: AudioBufferSourceNode[] = [];
  10. constructor() {
  11. this.context = new AudioContext({ sampleRate: this.sampleRate });
  12. }
  13. async initialize() {
  14. await this.context.audioWorklet.addModule("/audio-processor.js");
  15. }
  16. async startRecording(onChunk: (chunk: Uint8Array) => void) {
  17. try {
  18. if (!this.workletNode) {
  19. await this.initialize();
  20. }
  21. this.stream = await navigator.mediaDevices.getUserMedia({
  22. audio: {
  23. channelCount: 1,
  24. sampleRate: this.sampleRate,
  25. echoCancellation: true,
  26. noiseSuppression: true,
  27. },
  28. });
  29. await this.context.resume();
  30. this.source = this.context.createMediaStreamSource(this.stream);
  31. this.workletNode = new AudioWorkletNode(
  32. this.context,
  33. "audio-recorder-processor",
  34. );
  35. this.workletNode.port.onmessage = (event) => {
  36. if (event.data.eventType === "audio") {
  37. const float32Data = event.data.audioData;
  38. const int16Data = new Int16Array(float32Data.length);
  39. for (let i = 0; i < float32Data.length; i++) {
  40. const s = Math.max(-1, Math.min(1, float32Data[i]));
  41. int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
  42. }
  43. const uint8Data = new Uint8Array(int16Data.buffer);
  44. onChunk(uint8Data);
  45. }
  46. };
  47. this.source.connect(this.workletNode);
  48. this.workletNode.connect(this.context.destination);
  49. this.workletNode.port.postMessage({ command: "START_RECORDING" });
  50. } catch (error) {
  51. console.error("Error starting recording:", error);
  52. throw error;
  53. }
  54. }
  55. stopRecording() {
  56. if (!this.workletNode || !this.source || !this.stream) {
  57. throw new Error("Recording not started");
  58. }
  59. this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
  60. this.workletNode.disconnect();
  61. this.source.disconnect();
  62. this.stream.getTracks().forEach((track) => track.stop());
  63. }
  64. startStreamingPlayback() {
  65. this.isPlaying = true;
  66. this.nextPlayTime = this.context.currentTime;
  67. }
  68. stopStreamingPlayback() {
  69. this.isPlaying = false;
  70. this.playbackQueue.forEach((source) => source.stop());
  71. this.playbackQueue = [];
  72. }
  73. playChunk(chunk: Uint8Array) {
  74. if (!this.isPlaying) return;
  75. const int16Data = new Int16Array(chunk.buffer);
  76. const float32Data = new Float32Array(int16Data.length);
  77. for (let i = 0; i < int16Data.length; i++) {
  78. float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
  79. }
  80. const audioBuffer = this.context.createBuffer(
  81. 1,
  82. float32Data.length,
  83. this.sampleRate,
  84. );
  85. audioBuffer.getChannelData(0).set(float32Data);
  86. const source = this.context.createBufferSource();
  87. source.buffer = audioBuffer;
  88. source.connect(this.context.destination);
  89. const chunkDuration = audioBuffer.length / this.sampleRate;
  90. source.start(this.nextPlayTime);
  91. this.playbackQueue.push(source);
  92. source.onended = () => {
  93. const index = this.playbackQueue.indexOf(source);
  94. if (index > -1) {
  95. this.playbackQueue.splice(index, 1);
  96. }
  97. };
  98. this.nextPlayTime += chunkDuration;
  99. if (this.nextPlayTime < this.context.currentTime) {
  100. this.nextPlayTime = this.context.currentTime;
  101. }
  102. }
  103. async close() {
  104. this.workletNode?.disconnect();
  105. this.source?.disconnect();
  106. this.stream?.getTracks().forEach((track) => track.stop());
  107. await this.context.close();
  108. }
  109. }