audio.ts 5.9 KB

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