feat: 录音组件

This commit is contained in:
WANGFan 2025-06-25 13:37:38 +08:00
parent 1657a81c75
commit 43f5661be5
9 changed files with 329 additions and 63 deletions

View File

@ -54,6 +54,7 @@
"pinyin-pro": "^3.26.0", "pinyin-pro": "^3.26.0",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"recorder-core": "^1.3.25011100",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"vue": "^3.5.15", "vue": "^3.5.15",
"vue-codemirror6": "^1.3.0", "vue-codemirror6": "^1.3.0",

53
pnpm-lock.yaml generated
View File

@ -74,6 +74,9 @@ importers:
qrcode: qrcode:
specifier: ^1.5.4 specifier: ^1.5.4
version: 1.5.4 version: 1.5.4
recorder-core:
specifier: ^1.3.25011100
version: 1.3.25011100
sortablejs: sortablejs:
specifier: ^1.15.2 specifier: ^1.15.2
version: 1.15.6 version: 1.15.6
@ -1218,9 +1221,6 @@ packages:
'@types/svgo@2.6.4': '@types/svgo@2.6.4':
resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@ -1420,21 +1420,12 @@ packages:
'@vueuse/core@12.8.2': '@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@12.8.2': '@vueuse/metadata@12.8.2':
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@12.8.2': '@vueuse/shared@12.8.2':
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
'@wangeditor/basic-modules@1.1.7': '@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==} resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
peerDependencies: peerDependencies:
@ -3940,6 +3931,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
recorder-core@1.3.25011100:
resolution: {integrity: sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -4821,9 +4815,6 @@ packages:
vscode-uri@3.1.0: vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-audio-visual@3.0.11:
resolution: {integrity: sha512-toXUXswQqo/oHZuzVnhuZ+m9rM9OU5lgOkckQHGpXizF00XI1gZ1f2KpRrLMKj4e7LC23tZjcgMxDeBJRTenvg==}
vue-codemirror6@1.3.15: vue-codemirror6@1.3.15:
resolution: {integrity: sha512-KjcC1ru686qpsOXwHMWVE7Pv5+u1R1JX+nGd/ovXaLSju/3R1ywMU7HFydmEiFKnsSz9aksDcEy3GNCJMyMFWQ==} resolution: {integrity: sha512-KjcC1ru686qpsOXwHMWVE7Pv5+u1R1JX+nGd/ovXaLSju/3R1ywMU7HFydmEiFKnsSz9aksDcEy3GNCJMyMFWQ==}
engines: {pnpm: '>=10.3.0'} engines: {pnpm: '>=10.3.0'}
@ -5961,8 +5952,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.15.21 '@types/node': 22.15.21
'@types/web-bluetooth@0.0.16': {}
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
@ -6338,33 +6327,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@vueuse/core@9.13.0(vue@3.5.15(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.16
'@vueuse/metadata': 9.13.0
'@vueuse/shared': 9.13.0(vue@3.5.15(typescript@5.8.3))
vue-demi: 0.14.10(vue@3.5.15(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@12.8.2': {} '@vueuse/metadata@12.8.2': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@12.8.2(typescript@5.8.3)': '@vueuse/shared@12.8.2(typescript@5.8.3)':
dependencies: dependencies:
vue: 3.5.15(typescript@5.8.3) vue: 3.5.15(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@vueuse/shared@9.13.0(vue@3.5.15(typescript@5.8.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.15(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': '@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)':
dependencies: dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)
@ -9010,6 +8980,8 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
recorder-core@1.3.25011100: {}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -9421,7 +9393,6 @@ snapshots:
qrcode: 1.5.4 qrcode: 1.5.4
sortablejs: 1.15.6 sortablejs: 1.15.6
vue: 3.5.15(typescript@5.8.3) vue: 3.5.15(typescript@5.8.3)
vue-audio-visual: 3.0.11(typescript@5.8.3)
vue-codemirror6: 1.3.15(@popperjs/core@2.11.8)(core-js@3.42.0)(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3)) vue-codemirror6: 1.3.15(@popperjs/core@2.11.8)(core-js@3.42.0)(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))
vue-color-kit: 1.0.6(vue@3.5.15(typescript@5.8.3)) vue-color-kit: 1.0.6(vue@3.5.15(typescript@5.8.3))
vue-i18n: 10.0.0-alpha.3(vue@3.5.15(typescript@5.8.3)) vue-i18n: 10.0.0-alpha.3(vue@3.5.15(typescript@5.8.3))
@ -10043,14 +10014,6 @@ snapshots:
vscode-uri@3.1.0: {} vscode-uri@3.1.0: {}
vue-audio-visual@3.0.11(typescript@5.8.3):
dependencies:
'@vueuse/core': 9.13.0(vue@3.5.15(typescript@5.8.3))
vue: 3.5.15(typescript@5.8.3)
transitivePeerDependencies:
- '@vue/composition-api'
- typescript
vue-codemirror6@1.3.15(@popperjs/core@2.11.8)(core-js@3.42.0)(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3)): vue-codemirror6@1.3.15(@popperjs/core@2.11.8)(core-js@3.42.0)(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3)):
dependencies: dependencies:
'@codemirror/commands': 6.8.1 '@codemirror/commands': 6.8.1

36
src/components.d.ts vendored
View File

@ -5,23 +5,25 @@
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
export {} export {}
declare module "vue" { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AudioVisualizer: (typeof import("./components/audio-visualizer/index.vue"))["default"]; AudioVisualizer: typeof import('./components/audio-visualizer/index.vue')['default']
BarcodeDraw: (typeof import("./components/barcode-draw/index.vue"))["default"]; BarcodeDraw: typeof import('./components/barcode-draw/index.vue')['default']
CodeView: (typeof import("./components/code-view/index.vue"))["default"]; CodeView: typeof import('./components/code-view/index.vue')['default']
ExternalLinkPage: (typeof import("./components/external-link-page/index.vue"))["default"]; ExternalLinkPage: typeof import('./components/external-link-page/index.vue')['default']
FillPage: (typeof import("./components/fill-page/index.vue"))["default"]; FillPage: typeof import('./components/fill-page/index.vue')['default']
InternalLinkPage: (typeof import("./components/internal-link-page/index.vue"))["default"]; InternalLinkPage: typeof import('./components/internal-link-page/index.vue')['default']
LangProvider: (typeof import("./components/lang-provider/index.vue"))["default"]; LangProvider: typeof import('./components/lang-provider/index.vue')['default']
MainTransition: (typeof import("./components/main-transition/index.vue"))["default"]; MainTransition: typeof import('./components/main-transition/index.vue')['default']
PinyinPro: (typeof import("./components/pinyin-pro/index.vue"))["default"]; PinyinPro: typeof import('./components/pinyin-pro/index.vue')['default']
QrcodeDraw: (typeof import("./components/qrcode-draw/index.vue"))["default"]; QrcodeDraw: typeof import('./components/qrcode-draw/index.vue')['default']
RouterLink: (typeof import("vue-router"))["RouterLink"]; Recorder: (typeof import("./components/recorder/index.vue"))["default"]
RouterView: (typeof import("vue-router"))["RouterView"]; RecorderPcm: typeof import('./components/recorder-pcm/index.vue')['default']
SelectIcon: (typeof import("./components/select-icon/index.vue"))["default"]; RouterLink: typeof import('vue-router')['RouterLink']
SvgAndIcon: (typeof import("./components/svg-and-icon/index.vue"))["default"]; RouterView: typeof import('vue-router')['RouterView']
SvgIcon: (typeof import("./components/svg-icon/index.vue"))["default"]; SelectIcon: typeof import('./components/select-icon/index.vue')['default']
VerifyCode: (typeof import("./components/verify-code/index.vue"))["default"]; SvgAndIcon: typeof import('./components/svg-and-icon/index.vue')['default']
SvgIcon: typeof import('./components/svg-icon/index.vue')['default']
VerifyCode: typeof import('./components/verify-code/index.vue')['default']
} }
} }

View File

@ -0,0 +1,115 @@
<template>
<div style="width: 100%; height: 100%" ref="recwave"></div>
</template>
<script setup lang="ts">
//
import Recorder from "recorder-core";
//pcm
import "recorder-core/src/engine/pcm";
//
import "recorder-core/src/extensions/waveview";
//ts import npm.d.tsany
const emit = defineEmits(["change"]);
let wave: any; //
const recwave = ref(null); // dom
let rec: any; // Recorder
let send_chunk: any; //
let testSampleRate = 16000; //
//
const RealTimeSendReset = () => {
send_chunk = null;
};
//
const recOpen = () => {
RealTimeSendReset();
//
rec = Recorder({
type: "unknown", //使unknown便
onProcess: (buffers: any, powerLevel: any, _: any, bufferSampleRate: any) => {
// pcmqueue
RealTimeSendTry(buffers, bufferSampleRate, false);
if (wave) {
wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
}
}
});
if (!rec) {
alert("当前浏览器不支持录音功能!");
return;
}
//
rec.open(
() => {
if (recwave.value) {
//
wave = Recorder.WaveView({ elem: recwave.value });
}
},
(msg: any, isUserNotAllow: any) => {
//
console.log((isUserNotAllow ? "UserNotAllow" : "") + "无法录音:" + msg);
}
);
};
const RealTimeSendTry = (buffers: any, bufferSampleRate: any, isClose: any) => {
//pcm
let pcm = new Int16Array(0);
if (buffers.length > 0) {
//SampleDatapcm
// send_chunk
let chunk = Recorder.SampleData(buffers, bufferSampleRate, testSampleRate, send_chunk);
send_chunk = chunk; // 使
pcm = chunk.data; //pcm16pcmLE16pcmwavwavmp3mp3
}
//pcm
TransferUpload(pcm, isClose);
return;
};
//===============
const TransferUpload = (pcmFrame: any, isClose: any) => {
if (isClose && pcmFrame.length == 0) {
// ws
emit("change", { arrayBuffer: pcmFrame.buffer, close: true });
// ws.send(arrayBuffer,true)
return; //return
}
//
let arrayBuffer = pcmFrame.buffer;
emit("change", { arrayBuffer, close: false });
//
//WebSocket send(arrayBuffer) ...
//WebRTC send(arrayBuffer) ...
//XMLHttpRequest send(arrayBuffer) ...
};
//
const recStart = () => {
if (!rec) return console.error("未打开录音");
rec.start();
//
};
//
const recStop = () => {
if (!rec) return console.error("未打开录音");
rec.close(); //
rec = null;
RealTimeSendTry([], 0, true); //
};
defineExpose({
recOpen,
recStart,
recStop
});
</script>
<style lang="scss" scoped></style>

View File

@ -105,6 +105,7 @@ export default {
["test-common-route"]: "test common route", ["test-common-route"]: "test common route",
["test-dynamic-route"]: "test dynamic route", ["test-dynamic-route"]: "test dynamic route",
["audio"]: "audio", ["audio"]: "audio",
["recorder"]: "recorder",
["test"]: "test" ["test"]: "test"
} }
}; };

View File

@ -105,6 +105,7 @@ export default {
["test-common-route"]: "测试普通路由", ["test-common-route"]: "测试普通路由",
["test-dynamic-route"]: "测试动态路由", ["test-dynamic-route"]: "测试动态路由",
["audio"]: "音频", ["audio"]: "音频",
["recorder"]: "录音",
["test"]: "测试" ["test"]: "测试"
} }
}; };

View File

@ -632,6 +632,27 @@ export const systemMenu = [
}, },
children: null children: null
}, },
{
id: "0613",
parentId: "06",
path: "/component/recorder",
name: "recorder",
component: "component/recorder/recorder",
meta: {
title: "recorder",
hide: false,
disable: false,
keepAlive: true,
affix: false,
link: "",
iframe: false,
roles: ["admin"],
icon: "icon-menu",
sort: 13,
type: 2
},
children: null
},
{ {
id: "07", id: "07",
parentId: "0", parentId: "0",

View File

@ -101,7 +101,7 @@ const onAudio = (blob: Blob) => {
<div class="snow-inner"> <div class="snow-inner">
<a-alert>本地录音需要https或localhost安全环境是否为安全环境{{ isSecureEnvironment() }}</a-alert> <a-alert>本地录音需要https或localhost安全环境是否为安全环境{{ isSecureEnvironment() }}</a-alert>
<a-space direction="vertical" fill> <a-space direction="vertical" fill>
<div :span="18" class="audio-box"> <div class="audio-box">
<AudioVisualizer ref="AudioVisualizerRef" v-model="config" @audio="onAudio" /> <AudioVisualizer ref="AudioVisualizerRef" v-model="config" @audio="onAudio" />
</div> </div>
<a-space direction="vertical" fill> <a-space direction="vertical" fill>

View File

@ -0,0 +1,162 @@
<template>
<div class="snow-page">
<div class="snow-inner">
<a-space direction="vertical" fill>
<a-alert>本地录音需要https或localhost安全环境是否为安全环境{{ isSecureEnvironment() }}</a-alert>
<a-alert type="success"
>基于<a-link href="https://github.com/xiangyuecn/Recorder" target="_blank">Recorder.js</a-link
>采用PCM实时帧回调的方式用于流式语音传输</a-alert
>
<a-space direction="vertical" fill>
<!-- 波形绘制区域 -->
<div class="waveview-box">
<recorder-pcm ref="RecorderPCM" @change="onChange" />
</div>
<a-space>
<a-button type="primary" @click="recOpen">打开录音权限</a-button>
<a-button type="primary" @click="recStart">开始录音</a-button>
<a-button @click="recStop">结束录音</a-button>
<a-button @click="audioList.length = 0">清空列表</a-button>
</a-space>
</a-space>
<audio ref="audioElement" controls v-if="audioList.length"></audio>
<a-list v-if="audioList.length">
<template #header>音频帧列表</template>
<a-list-item v-for="(item, index) in audioList" :key="index">
<a-space>
<span v-if="!item.close">PCM音频实时帧 - {{ index }}</span>
<span v-else>PCM完整音频</span>
<a-button-group>
<a-button @click="playAudio(item)">播放</a-button>
</a-button-group>
</a-space>
</a-list-item>
</a-list>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { isSecureEnvironment } from "@/utils/index";
const RecorderPCM = ref();
const recOpen = () => {
audioList.value.length = 0;
RecorderPCM.value.recOpen();
};
const recStart = () => {
RecorderPCM.value.recStart();
};
const recStop = () => {
RecorderPCM.value.recStop();
};
interface AudioInfo {
arrayBuffer: ArrayBuffer;
close: boolean;
}
//
const audioList = ref<AudioInfo[]>([]);
//
const onChange = async (e: AudioInfo) => {
if (!e.close) {
audioList.value.push(e);
} else {
//
let complete = mergePcmChunks(audioList.value.map(item => item.arrayBuffer));
audioList.value.push({ arrayBuffer: complete, close: true });
}
};
// PCM
const mergePcmChunks = (chunks: ArrayBuffer[]) => {
//
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
// ArrayBuffer
const mergedBuffer = new ArrayBuffer(totalLength);
const mergedView = new Uint8Array(mergedBuffer);
//
let offset = 0;
chunks.forEach(chunk => {
mergedView.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
});
return mergedBuffer;
};
const audioElement = ref<any>();
const playAudio = (e: AudioInfo) => {
const wavBuffer = pcmToWav(e.arrayBuffer);
const audioBlob = new Blob([wavBuffer], { type: "audio/wav" });
// BlobURL
const audioUrl = URL.createObjectURL(audioBlob);
// audiosrc
audioElement.value.src = audioUrl;
//
audioElement.value.play();
// URL
audioElement.value.onended = function () {
URL.revokeObjectURL(audioUrl);
};
};
// PCMWAV
const pcmToWav = (pcmArrayBuffer: ArrayBuffer, sampleRate = 16000, numChannels = 1, bitDepth = 16) => {
const buffer = new ArrayBuffer(44 + pcmArrayBuffer.byteLength);
const view = new DataView(buffer);
// WAV
writeString(view, 0, "RIFF");
view.setUint32(4, 36 + pcmArrayBuffer.byteLength, true);
writeString(view, 8, "WAVE");
// fmt
writeString(view, 12, "fmt ");
view.setUint32(16, 16, true); //
view.setUint16(20, 1, true); // 1PCM
view.setUint16(22, numChannels, true); //
view.setUint32(24, sampleRate, true); //
view.setUint32(28, sampleRate * numChannels * (bitDepth / 8), true); //
view.setUint16(32, numChannels * (bitDepth / 8), true); //
view.setUint16(34, bitDepth, true); //
// data
writeString(view, 36, "data");
view.setUint32(40, pcmArrayBuffer.byteLength, true);
// PCM
const pcmData = new Uint8Array(pcmArrayBuffer);
const resultData = new Uint8Array(buffer);
resultData.set(pcmData, 44);
return buffer;
};
const writeString = (view: DataView, offset: number, string: string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
</script>
<style lang="scss" scoped>
.waveview-box {
display: inline-block;
width: 400px;
height: 100px;
border: 1px solid $color-border-2;
}
</style>