audio.ts 6.7 KB

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