audio.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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. private playBuffer: Int16Array[] = [];
  11. constructor() {
  12. this.context = new AudioContext({ sampleRate: this.sampleRate });
  13. }
  14. async initialize() {
  15. await this.context.audioWorklet.addModule("/audio-processor.js");
  16. }
  17. async startRecording(onChunk: (chunk: Uint8Array) => void) {
  18. try {
  19. if (!this.workletNode) {
  20. await this.initialize();
  21. }
  22. this.stream = await navigator.mediaDevices.getUserMedia({
  23. audio: {
  24. channelCount: 1,
  25. sampleRate: this.sampleRate,
  26. echoCancellation: true,
  27. noiseSuppression: true,
  28. },
  29. });
  30. await this.context.resume();
  31. this.source = this.context.createMediaStreamSource(this.stream);
  32. this.workletNode = new AudioWorkletNode(
  33. this.context,
  34. "audio-recorder-processor",
  35. );
  36. this.workletNode.port.onmessage = (event) => {
  37. if (event.data.eventType === "audio") {
  38. const float32Data = event.data.audioData;
  39. const int16Data = new Int16Array(float32Data.length);
  40. for (let i = 0; i < float32Data.length; i++) {
  41. const s = Math.max(-1, Math.min(1, float32Data[i]));
  42. int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
  43. }
  44. const uint8Data = new Uint8Array(int16Data.buffer);
  45. onChunk(uint8Data);
  46. }
  47. };
  48. this.source.connect(this.workletNode);
  49. this.workletNode.connect(this.context.destination);
  50. this.workletNode.port.postMessage({ command: "START_RECORDING" });
  51. } catch (error) {
  52. console.error("Error starting recording:", error);
  53. throw error;
  54. }
  55. }
  56. stopRecording() {
  57. if (!this.workletNode || !this.source || !this.stream) {
  58. throw new Error("Recording not started");
  59. }
  60. this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
  61. this.workletNode.disconnect();
  62. this.source.disconnect();
  63. this.stream.getTracks().forEach((track) => track.stop());
  64. }
  65. startStreamingPlayback() {
  66. this.isPlaying = true;
  67. this.nextPlayTime = this.context.currentTime;
  68. }
  69. stopStreamingPlayback() {
  70. this.isPlaying = false;
  71. this.playbackQueue.forEach((source) => source.stop());
  72. this.playbackQueue = [];
  73. this.playBuffer = [];
  74. }
  75. playChunk(chunk: Uint8Array) {
  76. if (!this.isPlaying) return;
  77. const int16Data = new Int16Array(chunk.buffer);
  78. this.playBuffer.push.apply(this.playBuffer, int16Data); // save playBuffer
  79. const float32Data = new Float32Array(int16Data.length);
  80. for (let i = 0; i < int16Data.length; i++) {
  81. float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
  82. }
  83. const audioBuffer = this.context.createBuffer(
  84. 1,
  85. float32Data.length,
  86. this.sampleRate,
  87. );
  88. audioBuffer.getChannelData(0).set(float32Data);
  89. const source = this.context.createBufferSource();
  90. source.buffer = audioBuffer;
  91. source.connect(this.context.destination);
  92. const chunkDuration = audioBuffer.length / this.sampleRate;
  93. source.start(this.nextPlayTime);
  94. this.playbackQueue.push(source);
  95. source.onended = () => {
  96. const index = this.playbackQueue.indexOf(source);
  97. if (index > -1) {
  98. this.playbackQueue.splice(index, 1);
  99. }
  100. };
  101. this.nextPlayTime += chunkDuration;
  102. if (this.nextPlayTime < this.context.currentTime) {
  103. this.nextPlayTime = this.context.currentTime;
  104. }
  105. }
  106. _saveData(data: Int16Array, bytesPerSample = 16): Blob {
  107. const headerLength = 44;
  108. const numberOfChannels = 1;
  109. const dataLength = data.length;
  110. const wav = new Uint8Array(headerLength + dataLength * 2);
  111. const view = new DataView(wav.buffer);
  112. view.setUint32(0, 1380533830, false); // RIFF identifier 'RIFF'
  113. view.setUint32(4, 36 + dataLength * 2, true); // file length minus RIFF identifier length and file description length
  114. view.setUint32(8, 1463899717, false); // RIFF type 'WAVE'
  115. view.setUint32(12, 1718449184, false); // format chunk identifier 'fmt '
  116. view.setUint32(16, 16, true); // format chunk length
  117. view.setUint16(20, 1, true); // sample format (raw)
  118. view.setUint16(22, numberOfChannels, true); // channel count
  119. view.setUint32(24, this.sampleRate, true); // sample rate
  120. view.setUint32(28, this.sampleRate * 4, true); // byte rate (sample rate * block align)
  121. view.setUint16(32, numberOfChannels * 2, true); // block align (channel count * bytes per sample)
  122. view.setUint16(34, bytesPerSample, true); // bits per sample
  123. view.setUint32(36, 1684108385, false); // data chunk identifier 'data'
  124. view.setUint32(40, dataLength * 2, true); // data chunk length
  125. for (let i = 0; i < dataLength; i++) {
  126. view.setInt16(44 + i * 2, data[i], true);
  127. }
  128. return new Blob([view], { type: "audio/mpeg" });
  129. }
  130. savePlayFile() {
  131. return this._saveData(this.playBuffer);
  132. }
  133. async close() {
  134. this.workletNode?.disconnect();
  135. this.source?.disconnect();
  136. this.stream?.getTracks().forEach((track) => track.stop());
  137. await this.context.close();
  138. }
  139. }