Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
梧桐林的兔子君
csdnVueTest
提交
60c1a4ba
C
csdnVueTest
项目概览
梧桐林的兔子君
/
csdnVueTest
与 Fork 源项目一致
Fork自
inscode / VueJS
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
C
csdnVueTest
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
提交
60c1a4ba
编写于
7月 24, 2024
作者:
W
weixin_41859887
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Wed Jul 24 18:44:00 CST 2024 inscode
上级
79c5bd79
变更
3
隐藏空白更改
内联
并排
Showing
3 changed file
with
352 addition
and
38 deletion
+352
-38
src/App.vue
src/App.vue
+52
-38
src/utils/recorder.js
src/utils/recorder.js
+188
-0
src/utils/transcode.worker.js
src/utils/transcode.worker.js
+112
-0
未找到文件。
src/App.vue
浏览文件 @
60c1a4ba
<
script
setup
>
<
script
setup
>
import
HelloWorld
from
'
./components/HelloWorld.vue
'
import
TheWelcome
from
'
./components/TheWelcome.vue
'
import
{
onMounted
}
from
'
vue
'
;
// 导入 onMounted 钩子
import
recorder
from
'
./utils/recorder
'
;
let
audioRecorder
=
null
;
// 用于存储录音对象的变量
// 初始化录音对象
const
initializeRecorder
=
()
=>
{
audioRecorder
=
new
recorder
();
// 假设 recorder 是一个类,并且通过 new 创建实例
console
.
log
(
'
Recorder initialized:
'
,
audioRecorder
);
}
const
startRecording
=
()
=>
{
console
.
log
(
'
开始录音
'
);
if
(
!
audioRecorder
)
{
console
.
error
(
'
Recorder not initialized!
'
);
return
;
}
audioRecorder
.
recorderStart
();
// 调用 recorder 中的 start 方法开始录音
}
const
stopRecording
=
()
=>
{
console
.
log
(
'
结束录音
'
);
if
(
!
audioRecorder
)
{
console
.
error
(
'
Recorder not initialized!
'
);
return
;
}
audioRecorder
.
recorderStop
();
// 调用 recorder 中的 stop 方法结束录音
}
const
download
=
()
=>
{
if
(
audioRecorder
.
status
!==
'
end
'
)
{
alert
(
'
请完成录音后点击下载
'
)
return
;
}
audioRecorder
.
download
()
}
// 在组件加载时自动初始化录音对象
onMounted
(()
=>
{
initializeRecorder
();
});
</
script
>
</
script
>
<
template
>
<
template
>
<header>
<div
id=
"app"
>
<img
alt=
"Vue logo"
class=
"logo"
src=
"./assets/logo.svg"
width=
"125"
height=
"125"
/>
<button
@
click=
"startRecording"
>
开始录音
</button>
<br/>
<div
class=
"wrapper"
>
<br/>
<HelloWorld
msg=
"You did it!"
/>
<button
@
click=
"stopRecording"
>
结束录音
</button>
<br/>
<br/>
<button
@
click=
"download"
>
下载录音文件 PCM
</button>
</div>
</div>
</header>
<main>
<TheWelcome
/>
</main>
</
template
>
</
template
>
<
style
scoped
>
<
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
);
}
.logo
{
margin
:
0
2rem
0
0
;
}
header
.wrapper
{
display
:
flex
;
place-items
:
flex-start
;
flex-wrap
:
wrap
;
}
}
</
style
>
</
style
>
src/utils/recorder.js
0 → 100644
浏览文件 @
60c1a4ba
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
src/utils/transcode.worker.js
0 → 100644
浏览文件 @
60c1a4ba
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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录