提交 60c1a4ba 编写于 作者: W weixin_41859887

Wed Jul 24 18:44:00 CST 2024 inscode

上级 79c5bd79
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
import { onMounted } from 'vue'; // 导入 onMounted 钩子
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
import recorder from './utils/recorder';
let audioRecorder = null; // 用于存储录音对象的变量
<main>
<TheWelcome />
</main>
</template>
// 初始化录音对象
const initializeRecorder = () => {
audioRecorder = new recorder(); // 假设 recorder 是一个类,并且通过 new 创建实例
console.log('Recorder initialized:', audioRecorder);
}
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
const startRecording = () => {
console.log('开始录音');
if (!audioRecorder) {
console.error('Recorder not initialized!');
return;
}
audioRecorder.recorderStart(); // 调用 recorder 中的 start 方法开始录音
}
.logo {
margin: 0 2rem 0 0;
const stopRecording = () => {
console.log('结束录音');
if (!audioRecorder) {
console.error('Recorder not initialized!');
return;
}
audioRecorder.recorderStop(); // 调用 recorder 中的 stop 方法结束录音
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
const download = () => {
if (audioRecorder.status !== 'end') {
alert('请完成录音后点击下载')
return;
}
audioRecorder.download()
}
}
// 在组件加载时自动初始化录音对象
onMounted(() => {
initializeRecorder();
});
</script>
<template>
<div id="app">
<button @click="startRecording">开始录音</button>
<br/>
<br/>
<button @click="stopRecording">结束录音</button>
<br/>
<br/>
<button @click="download">下载录音文件 PCM</button>
</div>
</template>
<style scoped>
</style>
import recordWorker from './transcode.worker'
const URL = window.URL || window.webkitURL // 获取浏览器API
// 加载并启动 record worker
let workerString = recordWorker.toString()
// 移除函数包裹
workerString = workerString.substr(workerString.indexOf("{") + 1)
workerString = workerString.substr(0, workerString.lastIndexOf("}"))
const workerBlob = new Blob([workerString])
const workerURL = URL.createObjectURL(workerBlob)
const worker = new Worker(workerURL)
/**
* class IatRecorder 语音听写类
* @param {Object} config 参数
*/
class IatRecorder {
constructor(config) {
this.status = "null" // 当前录音的状态 null为开始 ing为录音中 end为结束
// 记录音频数据
this.audioData = []
this.buffer = []
// 记录听写结果
worker.onmessage = (event) => {
this.audioData.push(...event.data)
console.log('处理后的数据长度', this.audioData.length)
}
}
// 修改录音听写状态
setStatus (status) {
this.onWillStatusChange &&
this.status !== status &&
this.onWillStatusChange(this.status, status)
this.status = status
}
// 初始化浏览器录音
recorderInit () {
navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
// 创建音频环境
try {
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)()
console.log('audioContext sampleRate:', this.audioContext.sampleRate)
console.log('UA', navigator.userAgent)
if (!this.audioContext) {
alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
return
}
} catch (e) {
if (!this.audioContext) {
alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
return
}
}
// 获取浏览器录音权限
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
audio: true,
video: false,
})
.then((stream) => {
getMediaSuccess(stream)
})
.catch((e) => {
getMediaFail(e)
})
} else if (navigator.getUserMedia) {
navigator.getUserMedia(
{
audio: true,
video: false,
},
(stream) => {
getMediaSuccess(stream)
},
function (e) {
getMediaFail(e)
}
)
} else {
if (
navigator.userAgent.toLowerCase().match(/chrome/) &&
location.origin.indexOf("https://") < 0
) {
alert(
"chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限"
)
} else {
alert("无法获取浏览器录音功能,请升级浏览器或使用chrome")
}
this.audioContext && this.audioContext.close()
return
}
// 获取浏览器录音权限成功的回调
let getMediaSuccess = (stream) => {
// 创建一个用于通过JavaScript直接处理音频
this.scriptProcessor = this.audioContext.createScriptProcessor(
4096,
1,
1
)
this.scriptProcessor.onaudioprocess = (e) => {
// 去处理音频数据
this.buffer.push(...e.inputBuffer.getChannelData(0))
console.log('录音原始数据长度,类型为 Float32Array', e.inputBuffer.getChannelData(0).length)
worker.postMessage({
command: "transform",
buffer: e.inputBuffer.getChannelData(0),
is16K: this.audioContext.sampleRate,
})
}
// 创建一个新的MediaStreamAudioSourceNode 对象,使来自MediaStream的音频可以被播放和操作
this.mediaSource = this.audioContext.createMediaStreamSource(stream)
// 连接
this.mediaSource.connect(this.scriptProcessor)
this.scriptProcessor.connect(this.audioContext.destination)
};
let getMediaFail = () => {
alert("请求麦克风失败")
this.audioContext && this.audioContext.close()
this.audioContext = undefined
}
}
/**
* 开始录音
*/
recorderStart () {
this.setStatus("ing")
this.recorderStop()
this.recorderInit()
}
/**
* 暂停录音
* @param e
*/
recorderStop (e) {
// this.download();
this.setStatus("end")
// 关闭录音
this.audioContext && this.audioContext.close()
this.audioContext = undefined
}
getBuffer () {
let output = this.to16BitPCM(this.buffer)
return output
}
to16BitPCM (input) {
var dataLength = input.length * (16 / 8)
var dataBuffer = new ArrayBuffer(dataLength)
var dataView = new DataView(dataBuffer)
var offset = 0
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]))
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
}
return dataView
}
download () {
const blob = new Blob([this.getBuffer()]) //处理文档流
if ("msSaveOrOpenBlob" in navigator) {
window.navigator.msSaveOrOpenBlob(blob, "test.pcm")
return
}
const elink = document.createElement("a")
elink.download = "test.pcm"
elink.style.display = "none"
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
}
}
export default IatRecorder
const recordWorker = function () {
let self = this;
this.onmessage = function (e) {
switch (e.data.command) {
case "transform":
transform.transaction(e.data, e.data.is16K);
break;
}
};
let transform = {
transaction(audioData, is16K) {
let output;
if (is16K === 16000) {
output = transform.to16BitPCM(audioData.buffer, is16K);
} else if (is16K > 16000) {
output = transform.to16kHz(audioData.buffer, is16K);
output = transform.to16BitPCM(output);
} else if (is16K < 16000) {
output = transform.lessTo16kHz(audioData.buffer, is16K);
output = transform.to16BitPCM(output);
}
output = Array.from(new Uint8Array(output.buffer));
self.postMessage(output);
},
/**
* 大于16kHz降采样到16kHz
* @param audioData
* @param originalSampleRate
* @return {*|Float32Array}
*/
to16kHz(audioData, originalSampleRate) {
if (originalSampleRate <= 16000) {
return audioData;
}
let data = new Float32Array(audioData);
let fitCount = Math.round(data.length * (16000 / originalSampleRate));
let newData = new Float32Array(fitCount);
let springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];
for (let i = 1; i < fitCount - 1; i++) {
let tmp = i * springFactor;
let before = Math.floor(tmp).toFixed();
let after = Math.ceil(tmp).toFixed();
let atPoint = tmp - before;
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
newData[fitCount - 1] = data[data.length - 1];
return newData;
},
/**
* 小于16kHz升采样到16kHz
* @param audioData
* @param originalSampleRate
* @return {*|Float32Array}
*/
lessTo16kHz(audioData, originalSampleRate) {
// 如果原始采样率已经是16kHz或更高,直接返回原始数据
if (originalSampleRate >= 16000) {
return audioData;
}
// 计算新的采样率与原始采样率的比例
const upsampleFactor = 16000 / originalSampleRate;
// 计算新的数据长度
const newLength = Math.ceil(audioData.length * upsampleFactor);
// 创建一个新的 Float32Array 用于存储升采样后的数据
const newData = new Float32Array(newLength);
// 进行最近邻插值
for (let i = 0; i < newLength; i++) {
// 计算原始数据中对应的样本索引
const originalIndex = Math.floor(i / upsampleFactor);
// 将原始样本值复制到新数据中
newData[i] = audioData[originalIndex];
}
return newData;
},
to16BitPCM(input) {
let dataLength = input.length * (16 / 8); // 计算了将要创建的缓冲区的长度,这里使用了位操作来表示字节数,即 16 / 8 表示每个样本占用2个字节(16位PCM格式)。
let dataBuffer = new ArrayBuffer(dataLength); // 是一个用于存储二进制数据的固定大小的缓冲区,其长度为 dataLength 字节。
let dataView = new DataView(dataBuffer); // 则是一个用于读写 dataBuffer 中数据的视图,通过它可以以不同的数据格式(如整数、浮点数)访问缓冲区中的数据。
let offset = 0; // offset 变量用于追踪当前写入到 dataView 中的位置。初始为0,每次迭代增加2,因为每个16位样本占用2个字节。
for (let i = 0; i < input.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, input[i])); // Math.max(-1, Math.min(1, input[i])) 确保将输入值限制在 -1 到 1 之间,因为16位PCM格式只能表示在 -32768 到 32767 之间的整数。
// dataView.setInt16(offset, value, littleEndian) 将处理过的音频数据写入到 dataView 中:
// offset 是数据在缓冲区中的偏移量。
// value 是通过将输入数据映射到16位有符号整数的方式得到的数值。
// littleEndian 参数设为 true,表示使用小端序(低位字节在前,高位字节在后)存储数据,这是常见的音频格式存储方式。
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return dataView;
},
to8BitPCM(input) {
var dataLength = input.length;
var dataBuffer = new ArrayBuffer(dataLength);
var dataView = new DataView(dataBuffer);
var offset = 0;
for (var i = 0; i < input.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, input[i]));
var val = s < 0 ? s * 0x80 : s * 0x7F;
var intVal = Math.round(val); // Round to nearest integer
dataView.setInt8(offset, intVal);
}
return dataView;
},
};
};
export default recordWorker;
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册