首页 > 其他分享 >HTML5的音频录制和播放

HTML5的音频录制和播放

时间:2024-12-27 10:46:12浏览次数:5  
标签:null const 音频 录制 source HTML5 onSpeakChange audioContext onEnd

背景

  • 公司内部需要利用第三方接口实现ttsstt的功能,就涉及到了音频的录制和播放,所以就看了一下最新的音频api,实现了一个简单的。

实现

import { useEffect, useMemo, useState } from "react";

export type RecorderParam = {
  onEnd: (chunks: Blob[]) => void;
  onSpeakChange?: (rms: number) => void;
  onStreamChange?: (buffer: ArrayBuffer) => void; // 处理流式数据可以传入这个回调,拿到buffer
  mimeType?: string;
};

export class Recorder {
  chunks: Blob[];
  onEnd: (chunks: Blob[]) => void;
  onSpeakChange?: (rms: number) => void;
  onStreamChange?: (buffer: ArrayBuffer) => void;
  stream: MediaStream | null;
  mediaRecorder: MediaRecorder | null;
  audioContext: AudioContext | null;
  mediaStreamSource: MediaStreamAudioSourceNode | null;
  processor: ScriptProcessorNode | null;
  mimeType: string;
  isCancel: boolean; // 是否是取消了录音
  constructor(param: RecorderParam) {
    const { onEnd, onSpeakChange, onStreamChange, mimeType } = param;
    this.chunks = [];
    this.onEnd = onEnd;
    this.onSpeakChange = onSpeakChange;
    this.onStreamChange = onStreamChange;
    this.stream = null;
    this.mediaRecorder = null;
    this.audioContext = null;
    this.mediaStreamSource = null;
    this.processor = null;
    this.isCancel = false;
    this.mimeType = mimeType || "audio/mp4"; // 目前MediaRecorder在safari上只支持MP4的格式
  }

  async start() {
    this.isCancel = false;
    this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    this.audioContext = new AudioContext();
    this.mediaStreamSource = this.audioContext.createMediaStreamSource(
      this.stream
    );
    this.processor = this.audioContext.createScriptProcessor(2048, 1, 1); // 其实这个已经是要废弃的,推荐使用AudioWorklet,这个需要动态去加载js,感觉如果网络不好的话就不太行
    this.processor.onaudioprocess = (e) => {
      let buffer = e.inputBuffer.getChannelData(0);
      const numberArr: number[] = [];
      for (let i = 0; i < buffer.length; i++) {
        numberArr.push(buffer[i]);
      }
      let rms = Math.max.apply(null, numberArr);
      if (!this.isCancel) {
        this.onSpeakChange?.(rms);
        this.onStreamChange?.(buffer); // 流式交互可以用这个处理
      }
    };
    this.mediaStreamSource.connect(this.processor);
    this.processor.connect(this.audioContext.destination);

    this.mediaRecorder = new MediaRecorder(this.stream, {
      mimeType: this.mimeType,
    });
    this.mediaRecorder.ondataavailable = (event) => {
      this.chunks.push(event.data);
    };
    this.mediaRecorder.onstop = () => {
      if (!this.isCancel) {
        this.onEnd?.([...this.chunks]);
        this.onSpeakChange?.(0);
      }
      this.chunks = [];
    };
    this.mediaRecorder.start();
  }

  async stop() {
    if (this.mediaRecorder) {
      this.mediaRecorder.stop();
      this.mediaRecorder = null;
    }
    if (this.stream) {
      this.stream.getTracks().forEach((track) => track.stop());
      this.stream = null;
    }
    if (this.audioContext) {
      await this.audioContext.close();
      this.audioContext = null;
    }
    this.mediaStreamSource = null;
    this.processor = null;
  }

  // 取消录音
  async cancel() {
    this.isCancel = true;
    await this.stop();
  }
}

export type GetRecorderFuncParam = {
  onEnd: (file: File) => void;
  onSpeakChange?: (rms: number) => void;
  mimeType?: string;
};

export const useRecordAudioFunc = (param: GetRecorderFuncParam) => {
  const { onEnd, onSpeakChange } = param;

  const [recorder, setRecorder] = useState<Recorder>();

  useEffect(() => {
    setRecorder(
      new Recorder({
        onEnd: (chunks: Blob[]) => {
          const blob = new Blob(chunks, {
            type: "audio/webm",
          });
          const file = new File([blob], "record.webm", {
            type: "audio/webm",
          });
          onEnd?.(file);
        },
        onSpeakChange: (rms: number) => {
          onSpeakChange?.(rms);
        },
      })
    );
  }, []);

  // 不需要每次组件更新都重新获取
  return useMemo(
    () => ({
      start: () => recorder?.start(), // 开始录音
      stop: () => recorder?.stop(), // 结束录音
      cancel: () => recorder?.cancel(), // 取消录音
    }),
    [recorder]
  );
};

export type AudioPlayerProps = {
  arrayBuffer: ArrayBuffer; // 音频流
  rate: number; // 播放速率
  onEnd: () => {}; // 播放结束后的回调
  onReady: () => {}; // 准备好播放之后
};
export class AudioPlayer {
  audioBuffer: AudioBuffer | null;
  audioContext: AudioContext;
  source: AudioBufferSourceNode | null;
  startTime: number;
  offset: number;
  rate: number;
  isEndByStop: boolean;
  onEnd: () => void;
  onReady: () => void;
  constructor(props) {
    const { arrayBuffer, rate = 1, onEnd, onReady } = props;
    this.audioContext = new AudioContext();
    this.rate = rate;
    this.onEnd = onEnd;
    this.onReady = onReady;
    this.isEndByStop = false;
    this.startTime = 0;
    this.offset = 0;
    this.audioBuffer = null;
    this.source = null;
    this.audioContext.decodeAudioData(arrayBuffer).then((buffer) => {
      this.audioBuffer = buffer;
      this.onReady?.();
    });
  }

  start() {
    this.isEndByStop = false;
    this.source = this.audioContext.createBufferSource();
    this.source.buffer = this.audioBuffer;
    this.source.playbackRate.value = this.rate || 1;
    this.source.connect(this.audioContext.destination);
    this.source.onended = () => {
      if (!this.isEndByStop) {
        this.offset = 0; // 正常播放完成的重置offset
      } else {
        // 记录播放的时长
        this.offset =
          this.audioContext.currentTime - this.startTime + this.offset;
      }
      this.onEnd?.();
    };
    this.source.start(0, this.offset);
    this.startTime = this.audioContext.currentTime; // 记录开始时间
  }

  stop() {
    if (this.source) {
      // 手动停止的播放
      this.isEndByStop = true;
      this.source.stop();
      this.source.disconnect(this.audioContext.destination);
      this.source = null;
    }
  }
}
  // recorder
  const { start, stop, cancel } = useRecordAudioFunc({
    onEnd: async (file: File) => {
      try {
        // 调api进行stt
        const resp = await fetch(FETCH_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/octet-stream",
          },
          body: file,
        });
        const data = await resp.json();
        onEnd?.(data?.transcription?.trim() || "");
      } catch (err) {
        onEnd?.("failed");
      } finally {
        setSpeaking(0);
        setRecording(false);
      }
    },
    onSpeakChange: (rms: number) => {
      setSpeaking(rms);
    },
  });
  // player
      new AudioPlayer({
            arrayBuffer, // 获取到的音频流
            rate, // 播放速率
            onEnd: () => {
                // 播放结束的回调
            },
            onReady: () => {
              // 准备播放之前
            },
          });
      });

标签:null,const,音频,录制,source,HTML5,onSpeakChange,audioContext,onEnd
From: https://www.cnblogs.com/aloneMing/p/18634963

相关文章

  • 你有完整看过html或html5的手册吗?
    是的,我熟悉HTML和HTML5的手册内容,并且可以作为前端开发的重要参考。以下是我对HTML和HTML5手册的理解与归纳:一、HTML与HTML5概述HTML(超文本标记语言)是构建网页的基础,通过标记标签来描述页面文档结构和表现形式。HTML5作为HTML的最新版本,引入了许多新特性和API,使得网页开发更加......
  • 使用HTML5画一个拱桥的布局
    在HTML5中,创建一个拱桥的布局主要依赖于CSS样式来定义形状和布局,而HTML则用于结构。下面是一个简单的示例,展示如何使用HTML5和CSS来绘制一个基本的拱桥布局:HTML结构:<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content=......
  • 使用html5写一个刮刮乐奖券的布局
    当然,以下是一个简单的刮刮乐奖券的前端HTML5和CSS布局示例:HTML:<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0">......
  • 如何使用HTML5生成一张缩略图?
    在HTML5中,直接生成缩略图的功能并不内置。然而,你可以使用HTML5的<canvas>元素与JavaScript来创建一个缩略图。以下是一个简单的步骤说明:获取原始图像:首先,你需要一个原始图像。这可以通过HTML的<img>标签获取,或者通过JavaScript的Image对象动态加载。创建Canvas元素:然后,创建一个......
  • 使用html5写一个背景粒子特效
    创建一个背景粒子特效需要综合运用HTML5、CSS3和JavaScript。以下是一个简单的示例,演示如何使用HTML5的<canvas>元素和JavaScript来创建一个粒子动画背景。HTML<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"......
  • html5中的meta标签keywords有什么作用?
    在HTML5中,<meta>标签的keywords属性曾经被用来为网页提供关键词,以便搜索引擎能够更好地理解和索引网页内容。然而,这个属性的实际作用在近年来已经大大减弱。过去,搜索引擎会依赖keywords属性来了解网页的主题和内容。网站开发者会在keywords属性中填入一系列与网页内容相关的关键......
  • html5中的meta标签revised有什么作用?
    在HTML5中,并没有一个官方定义的revised属性或meta标签。可能你是指的meta标签中的content属性被用来表示页面的修订日期或版本号,但这并不是HTML5标准中的一部分。通常,meta标签被用于提供有关HTML文档的元数据。这些元数据不会显示在页面上,但是对于机器是可读的。它可用于浏览器(如......
  • html5中的meta标签scheme有什么作用?
    在HTML5中,<meta>标签的scheme属性主要用于定义用于解释content属性值的方案。然而,这个属性在HTML5中已经不再被推荐使用,并且在HTML5规范中已被移除。在HTML4.01或更早的版本中,scheme属性可能更常见。在早期的HTML版本中,scheme属性被用来指定一个解释content属性中所包......
  • HTML5期末大作业:旅游网页设计——西安旅游9页(代码质量好) 学生DW网页设计作业源码 we
    ......
  • 英锐芯AD8002B高性能的AB类音频功率放大器
    8002B是一款高性能的AB类音频功率放大器。它具有以下特点:1.单声道带关断模式:8002B可以通过控制进入休眠模式,从而减少功耗。它还具有过热自动关断保护机制,确保芯片的安全运行。2.高驱动功率:8002B在4Ω负载下,THD小于10%时可以提供最大驱动功率为3W,而在THD小于1%时可以提供2......