feat: 录音组件
This commit is contained in:
parent
1657a81c75
commit
43f5661be5
@ -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
53
pnpm-lock.yaml
generated
@ -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
36
src/components.d.ts
vendored
@ -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']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/components/recorder-pcm/index.vue
Normal file
115
src/components/recorder-pcm/index.vue
Normal 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.ts声明文件(不过是any类型)
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
// 所有的pcm数据queue,缓存采样率,是否结束
|
||||||
|
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) {
|
||||||
|
//【关键代码】借用SampleData函数进行数据的连续处理,采样率转换是顺带的,得到新的pcm数据
|
||||||
|
// send_chunk为上次分割点
|
||||||
|
let chunk = Recorder.SampleData(buffers, bufferSampleRate, testSampleRate, send_chunk);
|
||||||
|
send_chunk = chunk; // 保存本次分割点,用于下次使用
|
||||||
|
pcm = chunk.data; //此时的pcm就是原始的音频16位pcm数据(小端LE),直接保存即为16位pcm文件、加个wav头即为wav文件、丢给mp3编码器转一下码即为mp3文件
|
||||||
|
}
|
||||||
|
|
||||||
|
//没有指定固定的帧大小,直接把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>
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -105,6 +105,7 @@ export default {
|
|||||||
["test-common-route"]: "测试普通路由",
|
["test-common-route"]: "测试普通路由",
|
||||||
["test-dynamic-route"]: "测试动态路由",
|
["test-dynamic-route"]: "测试动态路由",
|
||||||
["audio"]: "音频",
|
["audio"]: "音频",
|
||||||
|
["recorder"]: "录音",
|
||||||
["test"]: "测试"
|
["test"]: "测试"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
162
src/views/component/recorder/recorder.vue
Normal file
162
src/views/component/recorder/recorder.vue
Normal 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" });
|
||||||
|
|
||||||
|
// 为Blob创建URL对象
|
||||||
|
const audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
|
||||||
|
// 设置audio元素的src
|
||||||
|
audioElement.value.src = audioUrl;
|
||||||
|
|
||||||
|
// 开始播放
|
||||||
|
audioElement.value.play();
|
||||||
|
|
||||||
|
// 播放结束后释放URL对象
|
||||||
|
audioElement.value.onended = function () {
|
||||||
|
URL.revokeObjectURL(audioUrl);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// PCM转WAV函数
|
||||||
|
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); // 音频格式(1表示PCM)
|
||||||
|
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>
|
||||||
Loading…
x
Reference in New Issue
Block a user