怎么录制语音并播放

录制语音主要借助js-audio-recorder库,官方案例展示,官方案例代码

这边也尝试用下api,写个简短的DEMO。

DEMO体验

shell 复制代码
npm i send-record
# <SendRecord/>

需要的数据

  • 录制的三种状态:录制前、录制中、录制后。
  • 录制器实例
  • 录制时间显示:这里根据录制器的时间自动计算
  • 录制器当前状态
js 复制代码
import { reactive, computed, ref } from 'vue';

// @ts-ignore
import Recorder from 'js-audio-recorder';
const WILL_RECORD = 1; // 录制前
const RECORDING = 2; // 正在录制,点击可以停止
const RECORDED = 3; // 录制结束,可以其他操作

// 录制器
const recorder = reactive < any > new Recorder();
// 录制时间显示,这里根据录制器的时间自动计算
const durationShow = computed(() => durationToStr(recorder.duration));
// 当前是哪种状态
const curRecordStatus = ref(WILL_RECORD);

/**
 * 将秒数转换为时分秒: 2.1 => 00:02
 * @param duration 秒数 2
 * @returns {string} 00:02
 */
function durationToStr(duration: number) {
  const sumSeconds = Math.floor(duration);
  const minute = Math.floor(sumSeconds / 60);
  const second = sumSeconds % 60;
  return `${minute < 10 ? '0' + minute : minute}:${
    second < 10 ? '0' + second : second
  }`;
}

开始录制

  • 开始录音
  • 设置状态为 录制中
js 复制代码
function startRecord() {
  recorder.start().then(
    () => {
      console.log('开始录音');
    },
    (error) => {
      console.log(`异常了,${error.name}:${error.message}`);
    }
  );
  curRecordStatus.value = RECORDING;
}

结束录制

  • 结束录制
  • 设置状态为 已录制
js 复制代码
function endRecord() {
  recorder?.stop();
  curRecordStatus.value = RECORDED;
}

播放录制的音频

js 复制代码
function playRecord() {
  recorder.play();
}

停止录制的音频

js 复制代码
function pauseRecord() {
  recorder.pausePlay();
}

重置到录制前

js 复制代码
function resetRecord() {
  curRecordStatus.value = WILL_RECORD;
}

获取wav文件和下载

js 复制代码
function downloadWAV(){
   console.log('wav: ', recorder.getWAVBlob());
   recorder.downloadWAV();
}

录制器的监听事件和常用方法

  • onprogress: 监听录制中
  • onplay: 监听播放
  • onpauseplay: 监听暂停播放
  • onresumeplay: 监听恢复播放
  • onstopplay: 监听停止播放
  • onplayend: 监听播放结束

与之相对应的的方法:

  • start: 开始录制
  • stop: 结束录制
  • play: 播放
  • pausePlay: 暂停播放
  • resumePlay: 恢复播放
  • stopPlay: 停止播放
js 复制代码
recorder.onprogress = (params) => {
  console.log('音频总数据:', params);
};
recorder.onplay = () => {
  console.log('%c回调监听,开始播放音频', 'color: #2196f3');
};
recorder.onpauseplay = () => {
  console.log('%c回调监听,暂停播放音频', 'color: #2196f3');
};
recorder.onresumeplay = () => {
  console.log('%c回调监听,恢复播放音频', 'color: #2196f3');
};
recorder.onstopplay = () => {
  console.log('%c回调监听,停止播放音频', 'color: #2196f3');
};
recorder.onplayend = () => {
  recorder.isplaying = false;
  console.log('%c回调监听,音频已经完成播放', 'color: #2196f3');
};

其他属性和方法如下,其文档地址好像丢失:

哦,还有一层

上传音频文件并播放

音频文件播放,可以借助audio元素,播放前必须要先加载。

js 复制代码
const audioEl = ref(new Audio());
  
 async function playUploadedAudio (e)  {
    const audioFile = e.target.files[0]
    if(audioFile.isLoaded) {
        audioEl.value.play();
        return;
    }
    await readyAudio(audioFile);
    audioEl.value.play();
 }
  
function readyAudio(audioFile: any) {
  return new Promise((resolve, reject) => {
    var url = URL.createObjectURL(audioFile);
    var audioElement = new Audio(url);
    audioEl.value = audioElement

    audioElement.addEventListener('loadedmetadata', () => {
      audioFile.isLoaded = true;
      resolve(audioElement);
    });
    // 出错就reject
    audioElement.addEventListener('error', reject);
  });
}

DEMO代码

vue 复制代码
<script setup name="SendRecord" lang="ts">
import { reactive, computed, ref } from 'vue';
// @ts-ignore
import Recorder from 'js-audio-recorder';
const WILL_RECORD = 1; // 录制前
const RECORDING = 2; // 正在录制,点击可以停止
const RECORDED = 3; // 录制结束,可以其他操作

const recorder = reactive<any>(new Recorder());
const durationShow = computed(() => durationToStr(recorder.duration));
const curRecordStatus = ref(WILL_RECORD);

function startOrendRecord() {
  if (curRecordStatus.value === WILL_RECORD) {
    startRecord();
    return;
  }
  if (curRecordStatus.value === RECORDING) {
    endRecord();
    return;
  }
}
function startRecord() {
  console.log(recorder);
  recorder.start().then(
    () => {
      console.log('开始录音');
    },
    (error) => {
      console.log(`异常了,${error.name}:${error.message}`);
    }
  );
  curRecordStatus.value = RECORDING;
}

function endRecord() {
  recorder?.stop();
  console.log(recorder.file);
  curRecordStatus.value = RECORDED;
}
function playRecord() {
  recorder.play();
}

function pauseRecord() {
  recorder.pausePlay();
}

function resetRecord() {
  curRecordStatus.value = WILL_RECORD;
}
function downloadWAV() {
  console.log('wav: ', recorder.getWAVBlob());
  recorder.downloadWAV();
}
/**
 * 将秒数转换为时分秒: 2.1 => 00:02
 * @param duration 秒数 2
 * @returns {string} 00:02
 */
function durationToStr(duration: number) {
  const sumSeconds = Math.floor(duration);
  const minute = Math.floor(sumSeconds / 60);
  const second = sumSeconds % 60;
  return `${minute < 10 ? '0' + minute : minute}:${
    second < 10 ? '0' + second : second
  }`;
}
recorder.onprogress = (params) => {
  console.log('音频总数据:', params);
};
recorder.onplay = () => {
  console.log('%c回调监听,开始播放音频', 'color: #2196f3');
};
recorder.onpauseplay = () => {
  console.log('%c回调监听,暂停播放音频', 'color: #2196f3');
};
recorder.onresumeplay = () => {
  console.log('%c回调监听,恢复播放音频', 'color: #2196f3');
};
recorder.onstopplay = () => {
  console.log('%c回调监听,停止播放音频', 'color: #2196f3');
};
recorder.onplayend = () => {
  recorder.isplaying = false;
  console.log('%c回调监听,音频已经完成播放', 'color: #2196f3');
};

const audioEl = ref(new Audio());

async function playUploadedAudio(e) {
  const audioFile = e.target.files[0];
  if (audioFile.isLoaded) {
    audioEl.value.play();
    return;
  }
  await readyAudio(audioFile);
  audioEl.value.play();
}

function readyAudio(audioFile: any) {
  return new Promise((resolve, reject) => {
    var url = URL.createObjectURL(audioFile);
    var audioElement = new Audio(url);
    audioEl.value = audioElement;

    audioElement.addEventListener('loadedmetadata', () => {
      audioFile.isLoaded = true;
      resolve(audioElement);
    });
    // 出错就reject
    audioElement.addEventListener('error', reject);
  });
}
</script>

<template>
  <div>
    <div ref="recorderIconBox" class="icon-box" @click="startOrendRecord">
      <svg width="20" height="20" t="1693473966123" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4112" > <path d="M827.246871 451.075419c-12.94994-0.588401-23.925922 9.432837-24.51637 22.382776-0.093121 2.062985-0.418532 6.353708-1.106194 12.542664-1.170662 10.54824-2.959402 22.35924-5.490038 35.106566-7.226588 36.413328-18.898419 72.794933-35.917024 106.534362-47.672766 94.508467-126.925784 150.334937-248.217245 150.71663-121.290437-0.381693-200.546525-56.208163-248.217245-150.71663-17.018605-33.739429-28.692482-70.120011-35.919071-106.534362-2.529613-12.747325-4.317329-24.558325-5.487991-35.106566-0.687662-6.188956-1.014096-10.479679-1.108241-12.542664-0.588401-12.94994-11.564383-22.971178-24.514323-22.382776-12.951987 0.588401-22.973224 11.564383-22.382776 24.51637 0.5137 11.339256 2.63092 30.394241 7.446599 54.654784 8.000208 40.316218 20.946055 80.665181 40.051181 118.537743 51.840692 102.776781 138.972145 167.127392 265.456884 175.017082l0 85.599563L291.185872 909.400962c-12.96529 0-23.473621 10.510378-23.473621 23.473621 0 12.96529 10.508331 23.473621 23.473621 23.473621l441.857477 0c12.963243 0 23.473621-10.508331 23.473621-23.473621 0-12.963243-10.510378-23.473621-23.473621-23.473621L534.272259 909.400962l0-85.454254c127.791501-7.209192 215.690434-71.734788 267.86063-175.162392 19.104103-37.872562 32.050973-78.221526 40.051181-118.537743 4.815679-24.260543 6.930853-43.315528 7.446599-54.654784C850.217025 462.639802 840.197834 451.66382 827.246871 451.075419zM510.171352 700.19215c106.568131 0 193.353706-86.506213 193.353706-193.220676L703.525058 260.871449c0-106.59269-86.567611-193.220676-193.353706-193.220676-106.570177 0-193.353706 86.508259-193.353706 193.220676l0 246.100024C316.817646 613.567233 403.385257 700.19215 510.171352 700.19215zM363.764887 260.871449c0-80.693834 65.674769-146.273435 146.407488-146.273435 80.8197 0 146.407488 65.570391 146.407488 146.273435l0 246.100024c0 80.69588-65.674769 146.273435-146.407488 146.273435-80.8197 0-146.407488-65.568345-146.407488-146.273435L363.764887 260.871449z" fill="#272636" p-id="4113" ></path> </svg>
    </div>
    <div>
      <span v-if="curRecordStatus === RECORDING || curRecordStatus === RECORDED" >{{ durationShow }}</span >
    </div>
    <div v-show="curRecordStatus === RECORDED" class="icon-box">
      <!-- 播放图标 -->
      <svg  @click="playRecord" v-if="!recorder.isplaying" width="20" height="20" t="1693477278174" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5083" > <path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 981.333333C253.866667 981.333333 42.666667 770.133333 42.666667 512S253.866667 42.666667 512 42.666667s469.333333 211.2 469.333333 469.333333-211.2 469.333333-469.333333 469.333333z" fill="#515151" p-id="5084" ></path> <path d="M672 441.6l-170.666667-113.066667c-57.6-38.4-106.666667-12.8-106.666666 57.6v256c0 70.4 46.933333 96 106.666666 57.6l170.666667-113.066666c57.6-42.666667 57.6-106.666667 0-145.066667z" fill="#515151" p-id="5085" ></path> </svg>
      <!-- 暂停图标 -->
      <svg v-else @click="pauseRecord" width="20" height="20" t="1693477687958" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6103" > <path d="M511.474 884.11c-205.692 0-373.042-167.083-373.042-372.46 0-205.371 167.35-372.46 373.042-372.46 205.698 0 373.042 167.089 373.042 372.46 0 205.376-167.344 372.46-373.042 372.46m0-819.412c-246.84 0-447.649 200.496-447.649 446.951 0 246.457 200.809 446.951 447.649 446.951 246.842 0 447.649-200.494 447.649-446.951 0-246.455-200.808-446.951-447.649-446.951m127.768 580.701V378.6c0-20.573-16.701-37.247-37.301-37.247-20.606 0-37.307 16.675-37.307 37.247v266.799c0 20.574 16.701 37.249 37.307 37.249 20.601 0 37.301-16.675 37.301-37.249m-179.876 0V378.6c0-20.573-16.701-37.247-37.301-37.247-20.606 0-37.307 16.675-37.307 37.247v266.799c0 20.574 16.701 37.249 37.307 37.249 20.6 0 37.301-16.675 37.301-37.249" fill="#515151" p-id="6104" ></path> </svg>
      <button @click="downloadWAV">下载wav</button><br />
    </div>
    <div v-show="curRecordStatus === RECORDED" class="icon-box" @click="resetRecord" >
      <svg t="1693479917670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7988" width="20" height="20" > <path d="M510.825 162c-98.288 0-187.097 40.292-250.839 105.148V182h-60.201v200h200.672v-60H291.104c53.349-61.229 131.967-100 219.721-100 160.7 0 290.974 129.837 290.974 290 0 160.162-130.274 290-290.974 290-147.075 0-268.592-108.777-288.152-250H162c19.926 174.451 168.456 310 348.825 310C704.773 862 862 705.3 862 512S704.773 162 510.825 162z" fill="#515151" p-id="7989" ></path> </svg>
    </div>
    <div style="margin-top: 300px">
      <input type="file" @change="playUploadedAudio" />
    </div>
  </div>
</template>

<style scoped>
.icon-box {
  padding: 10px;
}
</style>
相关推荐
lovingsoft2 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly2 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝2 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦2 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95273 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人3 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载3 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队3 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端