audio.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. // @ts-ignore
  49. this.recordBuffer.push.apply(this.recordBuffer, int16Data);
  50. }
  51. };
  52. this.source.connect(this.workletNode);
  53. this.workletNode.connect(this.context.destination);
  54. this.workletNode.port.postMessage({ command: "START_RECORDING" });
  55. } catch (error) {
  56. console.error("Error starting recording:", error);
  57. throw error;
  58. }
  59. }
  60. stopRecording() {
  61. if (!this.workletNode || !this.source || !this.stream) {
  62. throw new Error("Recording not started");
  63. }
  64. this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
  65. this.workletNode.disconnect();
  66. this.source.disconnect();
  67. this.stream.getTracks().forEach((track) => track.stop());
  68. }
  69. startStreamingPlayback() {
  70. this.isPlaying = true;
  71. this.nextPlayTime = this.context.currentTime;
  72. }
  73. stopStreamingPlayback() {
  74. this.isPlaying = false;
  75. this.playbackQueue.forEach((source) => source.stop());
  76. this.playbackQueue = [];
  77. this.playBuffer = [];
  78. }
  79. playChunk(chunk: Uint8Array) {
  80. if (!this.isPlaying) return;
  81. const int16Data = new Int16Array(chunk.buffer);
  82. // @ts-ignore
  83. this.playBuffer.push.apply(this.playBuffer, int16Data); // save playBuffer
  84. const float32Data = new Float32Array(int16Data.length);
  85. for (let i = 0; i < int16Data.length; i++) {
  86. float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
  87. }
  88. const audioBuffer = this.context.createBuffer(
  89. 1,
  90. float32Data.length,
  91. this.sampleRate,
  92. );
  93. audioBuffer.getChannelData(0).set(float32Data);
  94. const source = this.context.createBufferSource();
  95. source.buffer = audioBuffer;
  96. source.connect(this.context.destination);
  97. const chunkDuration = audioBuffer.length / this.sampleRate;
  98. source.start(this.nextPlayTime);
  99. this.playbackQueue.push(source);
  100. source.onended = () => {
  101. const index = this.playbackQueue.indexOf(source);
  102. if (index > -1) {
  103. this.playbackQueue.splice(index, 1);
  104. }
  105. };
  106. this.nextPlayTime += chunkDuration;
  107. if (this.nextPlayTime < this.context.currentTime) {
  108. this.nextPlayTime = this.context.currentTime;
  109. }
  110. }
  111. _saveData(data: Int16Array, bytesPerSample = 16): Blob {
  112. const headerLength = 44;
  113. const numberOfChannels = 1;
  114. const byteLength = data.buffer.byteLength;
  115. const header = new Uint8Array(headerLength);
  116. const view = new DataView(header.buffer);
  117. view.setUint32(0, 1380533830, false); // RIFF identifier 'RIFF'
  118. view.setUint32(4, 36 + byteLength, true); // file length minus RIFF identifier length and file description length
  119. view.setUint32(8, 1463899717, false); // RIFF type 'WAVE'
  120. view.setUint32(12, 1718449184, false); // format chunk identifier 'fmt '
  121. view.setUint32(16, 16, true); // format chunk length
  122. view.setUint16(20, 1, true); // sample format (raw)
  123. view.setUint16(22, numberOfChannels, true); // channel count
  124. view.setUint32(24, this.sampleRate, true); // sample rate
  125. view.setUint32(28, this.sampleRate * 4, true); // byte rate (sample rate * block align)
  126. view.setUint16(32, numberOfChannels * 2, true); // block align (channel count * bytes per sample)
  127. view.setUint16(34, bytesPerSample, true); // bits per sample
  128. view.setUint32(36, 1684108385, false); // data chunk identifier 'data'
  129. view.setUint32(40, byteLength, true); // data chunk length
  130. // using data.buffer, so no need to setUint16 to view.
  131. return new Blob([view, data.buffer], { type: "audio/mpeg" });
  132. }
  133. savePlayFile() {
  134. // @ts-ignore
  135. return this._saveData(new Int16Array(this.playBuffer));
  136. }
  137. saveRecordFile(
  138. audioStartMillis: number | undefined,
  139. audioEndMillis: number | undefined,
  140. ) {
  141. const startIndex = audioStartMillis
  142. ? Math.floor((audioStartMillis * this.sampleRate) / 1000)
  143. : 0;
  144. const endIndex = audioEndMillis
  145. ? Math.floor((audioEndMillis * this.sampleRate) / 1000)
  146. : this.recordBuffer.length;
  147. return this._saveData(
  148. // @ts-ignore
  149. new Int16Array(this.recordBuffer.slice(startIndex, endIndex)),
  150. );
  151. }
  152. async close() {
  153. this.recordBuffer = [];
  154. this.workletNode?.disconnect();
  155. this.source?.disconnect();
  156. this.stream?.getTracks().forEach((track) => track.stop());
  157. await this.context.close();
  158. }
  159. }