在线录音+上传录音(react+hooks+ts)

需求:需要录音的文本可分为多段和单个,我们可以进行上传录音和在线录音的形式生成最终的录音。

在这里用到了自定义Audio,Audio代码在我的博客里有,博客地址是:https://blog.csdn.net/qq_40657321/article/details/120329405

其中在线录音用到了js-audio-recorder包,详情链接:https://www.npmjs.com/package/js-audio-recorder/v/0.2.3
在这里插入图片描述
在线录音参考文章:
1.在线录音演示地址:https://recorder.zhuyuntao.cn/
2.在线录音代码:https://github.com/2fps/recorder
3.在线录音实现:https://www.jb51.net/article/159849.htm

上传录音

1.1 上传录音(单个)

上传之前
在这里插入图片描述
上传后:生成录音并且可删除
在这里插入图片描述
flag是表示文件是否上传的标志

{
    
    
 !detailItem?.voiceItems[0]?.ossPath ?
 	<div className={
    
    styles.upload_btn}>
 //上传文件
       <Upload
          action={
    
    `url 上传接口`}
          headers={
    
    {
    
    
            _security_token:
              ((token || Cookie.get('token的key')) as string) ||
              getState().global.token,
          }}
          data={
    
    () => ({
    
    
          // 除了file额外的参数
            id: detail?.id,
            type: 1,
          })}
          showUploadList={
    
    false} // 不显示上传后的文件
          onChange={
    
    (data: any) => handleUpload(data, 0)}
          maxCount={
    
    1} // 最大上传个数1个
        >
          <Button className={
    
    styles.btn}>上传文件</Button>

        </Upload>
        <div className={
    
    styles.tit}>
          <span>支持拓展名:mp3/wav,大小不要超过10M</span>
        </div>
      </div> : detailItem?.voiceItems[0]?.flag ? <div className={
    
    styles.upload_btn}>
      //上传后
      //自定义Audio,代码在文章开头的链接里
        <Audio
          src={
    
    path} // 文件路径
          id={
    
    detailItem.id + '12'}
          type={
    
    'delete'}
          onClick={
    
    () => deleteRecord(0)}
          bg={
    
    '#F2F3F5'}
        />
      </div> : ''
    }
  </div>

1.2 上传录音(多个)

部分上传成功:生成录音并且可删除,其他未上传文件可继续上传
在这里插入图片描述
全部上传后:生成录音并且可逐个删除
在这里插入图片描述

在线录音

2.1 在线录音(单个)

录音初始状态:点击按钮可开始录音
在这里插入图片描述
录音中:点击按钮暂停录音并且生成录音
在这里插入图片描述
录音结束后:生成录音并且可删除
在这里插入图片描述

 <div className={
    
    styles.online_btn}>
   {
    
    
      (!detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
    
    styles.upload_btn}>
        <Button className={
    
    styles.btn} onClick={
    
    () => startRecording(0)}>
          <img src={
    
    play} className={
    
    styles.btn_img} />
          开始录音
        </Button>
      </div> : (detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
    
    styles.upload_btn}>
        <Button className={
    
    styles.btn} onClick={
    
    () => stopRecording(0)}>
          <img src={
    
    pause} className={
    
    styles.btn_img} ></img>
          {
    
    detailItem?.voiceItems[0].timeValue || '00:00'}
        </Button>
      </div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
    
    styles.upload_btn}>
        <Audio
          src={
    
    detailItem?.voiceItems[0]?.ossPath}
          id={
    
    detailItem.id + '11'}
          type={
    
    'delete'}
          onClick={
    
    () => deleteRecord(0)}
          bg={
    
    '#F2F3F5'}
        />
      </div> : ''
    }
  </div>

2.2 在线录音(多个)

部分录音状态:点击按钮可开始录音,点击删除可删除录音,重新录音
在这里插入图片描述
全部录音完成:点击删除按钮,删除当前录音,可重新开始录音
在这里插入图片描述

多个分段(数字转汉字)

拿到的数据是一个数组,第0个录音数据,是第一段,依次递增展示。
所以需要把下标+1,并且转成汉字。例子如下:
1=》一
12=》一十二
在这里插入图片描述
上代码:


//将小数部分的数字转换为字符串的方法:
var chnNumChar = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
var chnUnitSection = ["", "万", "亿", "万亿", "亿亿"];
var chnUnitChar = ["", "十", "百", "千"];

  //定义在每个小节的内部进行转化的方法,其他部分则与小节内部转化方法相同
  const sectionToChinese = (section: number) => {
    
    
    var str = '', chnstr = '', zero = false, count = 0;   //zero为是否进行补零, 第一次进行取余由于为个位数,默认不补零
    while (section > 0) {
    
    
      var v = section % 10;  //对数字取余10,得到的数即为个位数
      if (v == 0) {
    
                        //如果数字为零,则对字符串进行补零
        if (zero) {
    
    
          zero = false;        //如果遇到连续多次取余都是0,那么只需补一个零即可
          chnstr = chnNumChar[v] + chnstr;
        }
      } else {
    
    
        zero = true;           //第一次取余之后,如果再次取余为零,则需要补零
        str = chnNumChar[v];
        str += chnUnitChar[count];
        chnstr = str + chnstr;
      }
      count++;
      section = Math.floor(section / 10);
    }
    return chnstr;
  }

  //定义整个数字全部转换的方法,需要依次对数字进行10000为单位的取余,然后分成小节,按小节计算,当每个小节的数不足1000时,则需要进行补零
  const TransformToChinese = (num: number) => {
    
    
    num = Math.floor(num);
    var unitPos = 0;
    var strIns = '', chnStr = '';
    var needZero = false;

    if (num === 0) {
    
    
      return chnNumChar[0];
    }
    while (num > 0) {
    
    
      var section = num % 10000;
      if (needZero) {
    
    
        chnStr = chnNumChar[0] + chnStr;
      }
      strIns = sectionToChinese(section);
      strIns += (section !== 0) ? chnUnitSection[unitPos] : chnUnitSection[0];
      chnStr = strIns + chnStr;
      needZero = (section < 1000) && (section > 0);
      num = Math.floor(num / 10000);
      unitPos++;
    }

    return chnStr;
  }

完整代码

import React, {
    
     useEffect, useState } from 'react';
import {
    
     Modal, Form, Button, Upload, message } from 'antd';
import {
    
     getState, } from '@@/store';
import {
    
     CONFIG } from '@/services';
import Cookie from 'js-cookie';
import util from '@souche-f2e/souche-util';
import {
    
     TVoiceSave, TConfigDetail } from '../types';
import styles from './index.less'
import pause from '@/assets/images/pause.png';
import play from '@/assets/images/play.png';
// import { default as Recorder }  from '@/utils/record';
// import { Recorder } from '@/utils';
import Recorder from 'js-audio-recorder';
import Audio from '@/components/Audio';



type ISyncConfigModalProps = {
    
    
  visible: boolean;
  detail: TConfigDetail | null;
  loading: boolean;
  uploadType: string;
  onCancel: () => void;
  onConfirm: (data: TVoiceSave) => void;
};

const token = util.getParams().token;
let recorder: any = null;
//将小数部分的数字转换为字符串的方法:
var chnNumChar = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
var chnUnitSection = ["", "万", "亿", "万亿", "亿亿"];
var chnUnitChar = ["", "十", "百", "千"];

const SyncConfigModal: React.FC<ISyncConfigModalProps> = ({
     
     
  visible,
  loading,
  onCancel,
  onConfirm,
  detail,
  uploadType
}) => {
    
    
  const [form] = Form.useForm();
  const [detailItem, setDetailItem] = useState<TConfigDetail | null>(null);
  const [lastIndex, setLastIndex] = useState<any>(null);

  useEffect(() => {
    
    
    if (!visible) {
    
    
      form.resetFields();
      setDetailItem(null);
    } else {
    
    
      setLastIndex(null);
      if (visible && detail && detail?.voiceItems?.length > 1) {
    
    
        detail.voiceItems = detail?.voiceItems?.filter(v => !v.paramName)
        setDetailItem(detail);
      } else {
    
    
        setDetailItem(detail);
      }
    }

  }, [visible]);

  // const handleFormSubmit = (values: { groupId: string; syncType: '1' | '2' }) => {
    
    
  //   onConfirm({
    
    
  //     ...values,
  //   });
  // };

  //定义在每个小节的内部进行转化的方法,其他部分则与小节内部转化方法相同
  const sectionToChinese = (section: number) => {
    
    
    var str = '', chnstr = '', zero = false, count = 0;   //zero为是否进行补零, 第一次进行取余由于为个位数,默认不补零
    while (section > 0) {
    
    
      var v = section % 10;  //对数字取余10,得到的数即为个位数
      if (v == 0) {
    
                        //如果数字为零,则对字符串进行补零
        if (zero) {
    
    
          zero = false;        //如果遇到连续多次取余都是0,那么只需补一个零即可
          chnstr = chnNumChar[v] + chnstr;
        }
      } else {
    
    
        zero = true;           //第一次取余之后,如果再次取余为零,则需要补零
        str = chnNumChar[v];
        str += chnUnitChar[count];
        chnstr = str + chnstr;
      }
      count++;
      section = Math.floor(section / 10);
    }
    return chnstr;
  }

  //定义整个数字全部转换的方法,需要依次对数字进行10000为单位的取余,然后分成小节,按小节计算,当每个小节的数不足1000时,则需要进行补零
  const TransformToChinese = (num: number) => {
    
    
    num = Math.floor(num);
    var unitPos = 0;
    var strIns = '', chnStr = '';
    var needZero = false;

    if (num === 0) {
    
    
      return chnNumChar[0];
    }
    while (num > 0) {
    
    
      var section = num % 10000;
      if (needZero) {
    
    
        chnStr = chnNumChar[0] + chnStr;
      }
      strIns = sectionToChinese(section);
      strIns += (section !== 0) ? chnUnitSection[unitPos] : chnUnitSection[0];
      chnStr = strIns + chnStr;
      needZero = (section < 1000) && (section > 0);
      num = Math.floor(num / 10000);
      unitPos++;
    }

    return chnStr;
  }

  const handleUpload = (data: any, index: number) => {
    
    
    if (data.fileList[0].status === 'done') {
    
    
      if (data.file.response.code === '200' && detailItem) {
    
    

        let list = {
    
     ...detailItem };
        const item = {
    
     ...list.voiceItems[index] }
        console.log(item, 'item---')

        list.voiceItems[index] = {
    
     ...item, ...data.file.response.data }|| {
    
    };
        console.log(list.voiceItems[index], 'list.voiceItems[index]---')
        setDetailItem(list);
      }
    }

  };

  const transferTime = (time: number) => {
    
    
    let min: number | string = Math.floor(time / 60);
    if (min < 10) {
    
    
      min = `0${
      
      min}`;
    }
    let sec: number | string = Math.floor((time) % 60);
    if (sec < 10) {
    
    
      sec = `0${
      
      sec}`;
    }
    return `${
      
      min}:${
      
      sec}`;
  }

  // 开始录音
  const startRecording = (index: number) => {
    
    
    if (detailItem && detailItem?.voiceItems) {
    
    
      let list = {
    
     ...detailItem };
      list.voiceItems[index].audioFlag = true;
      // 播放其他的停止播放
      if (lastIndex !== null && lastIndex !== index) {
    
    
        console.log(lastIndex)
        list.voiceItems[lastIndex].audioFlag = false;
        list.voiceItems[lastIndex].timeValue = "00:00"; // 时间重置
        setDetailItem(list);
        stopRecording(lastIndex); // 销毁
      }
      create(index);
    }
    setLastIndex(index);
  }

  const create = (index: number) => {
    
    
    recorder = new Recorder({
    
    
      sampleBits: 16,                 // 采样位数,支持 8 或 16,默认是16
      sampleRate: 16000,              // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
      numChannels: 1,
    })

    recorder.start().then(() => {
    
    
      // 开始录音
      recorder.onprogress = function (params: any) {
    
    
        if (detailItem && detailItem?.voiceItems) {
    
    
          let list = {
    
     ...detailItem };
          list.voiceItems[index].timeValue = transferTime(params?.duration);
          setDetailItem(list);
        }
      }

    }, (error: any) => {
    
    
      // 出错了
      console.log(`${
      
      error.name} : ${
      
      error.message}`);
    });
  }
  // 停止录音
  const stopRecording = (index: number) => {
    
    
    recorder && recorder.stop()
    if (detailItem && detailItem?.voiceItems) {
    
    
      let list = {
    
     ...detailItem };
      list.voiceItems[index].audioFlag = false;
      setDetailItem(list);
    }
    createDownloadLink(index)
  }

  // 删除录音
  const deleteRecord = (index: number) => {
    
    
    if (detailItem && detailItem?.voiceItems) {
    
    
      let list = {
    
     ...detailItem };
      list.voiceItems[index].audioFlag = false;
      list.voiceItems[index].ossPath = '';
      setDetailItem(list);
    }
  }

  // 生成文件
  const createDownloadLink = async (index: number) => {
    
    

    const blob = recorder.getWAVBlob()
    console.log('b;ol', blob);
    let formDate = new FormData();
    formDate.append("file", blob, "blob.wav");
    formDate.append("serviceId", detailItem?.id || '');
    formDate.append("serviceModel", "1");
    try {
    
    
      const res = await CONFIG.uploadFile(formDate);
      if (res) {
    
    
        if (detailItem && detailItem?.voiceItems) {
    
    
          let list = {
    
     ...detailItem };
          const item = {
    
     ...list.voiceItems[index] }
          console.log(item, 'item---')

          list.voiceItems[index] = {
    
     ...item, ...res, duration: undefined } || {
    
    };
          console.log(list.voiceItems[index], 'list.voiceItems[index]---')
          setDetailItem(list);
        }
      }
    } catch (e) {
    
    
    }
  }

  // 保存
  const handleOk = async () => {
    
    

    const params = {
    
    
      id: detailItem?.id || '',
      voiceItems: detailItem?.voiceItems || [],
      // voiceType: uploadType === 'upload'? "UPLOAD" : 'ONLINE'
    }
    onConfirm(params);
  }

  return (
    <Modal
      visible={
    
    visible}
      title={
    
    uploadType === 'upload' ? "上传录音" : "在线录音"}
      onOk={
    
    handleOk}
      onCancel={
    
    onCancel}
      confirmLoading={
    
    loading}
      width={
    
    560}
      className={
    
    styles.modal_height}
    >
      {
    
    
        detailItem?.voiceItems?.length === 1 ?
          <div className={
    
    styles.upload_one}>
            <h4>话术内容</h4>
            <p>{
    
    detailItem?.voiceItems[0]?.text}</p>
            {
    
    
              uploadType === 'upload' ? <div>
                {
    
    
                  !detailItem?.voiceItems[0]?.ossPath ? <div className={
    
    styles.upload_btn}>
                    <Upload
                      action={
    
    `${
      
      process.env.MUJI_APP_CUSTOMER_SYSTEM_SERVER}/speechVoiceController/uploadFile.json`}
                      headers={
    
    {
    
    
                        _security_token:
                          ((token || Cookie.get('_security_token_ai')) as string) ||
                          getState().global.token,
                      }}
                      data={
    
    () => ({
    
    
                        serviceId: detail?.id,
                        serviceModel: 1,
                      })}
                      showUploadList={
    
    false}
                      onChange={
    
    (data: any) => handleUpload(data, 0)}
                      maxCount={
    
    1}
                    >
                      <Button className={
    
    styles.btn}>上传文件</Button>

                    </Upload>
                    <div className={
    
    styles.tit}>
                      <span>支持拓展名:mp3/wav,大小不要超过10M</span>
                    </div>
                  </div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
    
    styles.upload_btn}>
                    <Audio
                      src={
    
    detailItem?.voiceItems[0]?.ossPath}
                      id={
    
    detailItem.id + '12'}
                      type={
    
    'delete'}
                      onClick={
    
    () => deleteRecord(0)}
                      bg={
    
    '#F2F3F5'}
                    />
                  </div> : ''
                }
              </div> :
                <div className={
    
    styles.online_btn}>
                  {
    
    
                    (!detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
    
    styles.upload_btn}>
                      <Button className={
    
    styles.btn} onClick={
    
    () => startRecording(0)}>
                        <img src={
    
    play} className={
    
    styles.btn_img} />
                        开始录音
                      </Button>
                    </div> : (detailItem?.voiceItems[0]?.audioFlag && !detailItem?.voiceItems[0]?.ossPath) ? <div className={
    
    styles.upload_btn}>
                      <Button className={
    
    styles.btn} onClick={
    
    () => stopRecording(0)}>
                        <img src={
    
    pause} className={
    
    styles.btn_img} ></img>
                        {
    
    detailItem?.voiceItems[0].timeValue || '00:00'}
                      </Button>
                    </div> : detailItem?.voiceItems[0]?.ossPath ? <div className={
    
    styles.upload_btn}>
                      <Audio
                        src={
    
    detailItem?.voiceItems[0]?.ossPath}
                        id={
    
    detailItem.id + '11'}
                        type={
    
    'delete'}
                        onClick={
    
    () => deleteRecord(0)}
                        bg={
    
    '#F2F3F5'}
                      />
                    </div> : ''
                  }
                </div>
            }
          </div> :
          <div>
            {
    
    
              detailItem?.voiceItems?.map((v: any, i: number) => {
    
    
                return (
                  <div key={
    
    i}>
                    {
    
    
                      <div className={
    
    styles.upload_one} key={
    
    i} >
                        <h4>{
    
    TransformToChinese(i + 1)}</h4>
                        <p>{
    
    v?.text}</p>
                        {
    
    
                          (uploadType === 'upload' && !v?.ossPath) ? <div className={
    
    styles.upload_btn}>
                            <Upload
                              action={
    
    `${
      
      process.env.MUJI_APP_CUSTOMER_SYSTEM_SERVER}/speechVoiceController/uploadFile.json`}
                              headers={
    
    {
    
    
                                _security_token:
                                  ((token || Cookie.get('_security_token_ai')) as string) ||
                                  getState().global.token,
                              }}
                              data={
    
    () => ({
    
    
                                serviceId: detail?.id,
                                serviceModel: 1,
                              })}
                              showUploadList={
    
    false}
                              onChange={
    
    (data: any) => handleUpload(data, i)}
                              maxCount={
    
    1}
                            >
                              <Button className={
    
    styles.btn}>上传文件</Button>

                            </Upload>
                            <div className={
    
    styles.tit}>
                              <p>含有变量的话术需要分段录入</p>
                              <p> 支持拓展名:mp3/wav,大小不要超过10M</p>
                            </div>
                          </div> : (uploadType === 'upload' && v?.ossPath) ? <div className={
    
    styles.upload_btn}>
                            <Audio
                              src={
    
    v?.ossPath}
                              id={
    
    i}
                              type={
    
    'delete'}
                              onClick={
    
    () => deleteRecord(i)}
                              bg={
    
    '#F2F3F5'}
                            />
                          </div> : ''
                        }
                        {
    
    
                          (uploadType === 'online' && !v?.audioFlag && !v?.ossPath) ? <div className={
    
    styles.upload_btn}>
                            <Button className={
    
    styles.btn} onClick={
    
    () => startRecording(i)}>
                              <img src={
    
    play} className={
    
    styles.btn_img} />
                              开始录音
                            </Button>
                            <div className={
    
    styles.tit}>
                              <div>含有变量的话术需要分段录入</div>
                            </div>
                          </div> : (uploadType === 'online' && v?.audioFlag && !v?.ossPath) ? <div className={
    
    styles.upload_btn}>
                            <Button className={
    
    styles.btn} onClick={
    
    () => stopRecording(i)}>
                              <img src={
    
    pause} className={
    
    styles.btn_img} ></img>
                              {
    
    v.timeValue || '00:00'}
                            </Button>
                            <div className={
    
    styles.tit}>
                              <div>含有变量的话术需要分段录入</div>
                            </div>
                          </div> :
                            (uploadType === 'online' && v?.ossPath) ? <div className={
    
    styles.upload_btn}>
                              <Audio
                                src={
    
    v?.ossPath}
                                id={
    
    i}
                                type={
    
    'delete'}
                                onClick={
    
    () => deleteRecord(i)}
                                bg={
    
    '#F2F3F5'}
                              />
                            </div> : ''
                        }
                      </div>
                    }
                  </div>
                )
              })
            }
          </div>
      }
    </Modal >
  );
};

export default SyncConfigModal;

猜你喜欢

转载自blog.csdn.net/qq_40657321/article/details/120430792