Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
J
jensen
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Skye Yu
jensen
Commits
c2355191
Commit
c2355191
authored
Aug 05, 2025
by
martin hou
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 录音试听测试
parent
2589667c
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
325 additions
and
122 deletions
+325
-122
jensen.d.ts
jensen.d.ts
+17
-6
audio-worker.js
src/audio-worker.js
+12
-7
index.tsx
src/index.tsx
+89
-17
jensen.js
src/utils/jensen.js
+130
-92
waveform-worker.js
src/waveform-worker.js
+77
-0
No files found.
jensen.d.ts
View file @
c2355191
...
@@ -5,6 +5,13 @@ export type DeviceInfo = {
...
@@ -5,6 +5,13 @@ export type DeviceInfo = {
versionCode
:
string
;
// 固件版本号的字符串表示,如1.2.3
versionCode
:
string
;
// 固件版本号的字符串表示,如1.2.3
};
};
// 电池电量信息
export
type
BatteryStatus
=
{
status
:
'idle'
|
'charging'
|
'full'
;
// 电池状态,idle:闲置,charging:充电中,full:满电
battery
:
number
;
// 电量百分比,整型,0~100
voltage
:
number
;
// 电池电压,单位:微伏
}
// 常规指令的应答结构,表示指令的结果是成功或是失败
// 常规指令的应答结构,表示指令的结果是成功或是失败
export
type
ReturnStruct
=
{
export
type
ReturnStruct
=
{
common
:
{
result
:
'failed'
|
'success'
};
common
:
{
result
:
'failed'
|
'success'
};
...
@@ -168,14 +175,17 @@ declare class Jensen {
...
@@ -168,14 +175,17 @@ declare class Jensen {
// 注意:文件传输进度和数据并不是同时到达的,在文件传输到100%时才会开始调用on回调进行转移数据
// 注意:文件传输进度和数据并不是同时到达的,在文件传输到100%时才会开始调用on回调进行转移数据
getFile
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
onprogress
?:
(
size
:
number
)
=>
void
)
=>
void
;
getFile
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
onprogress
?:
(
size
:
number
)
=>
void
)
=>
void
;
// 持续传输文件,filename为文件名称,length为文件长度,ondata为数据回调,seconds为超时时长
transferFile
:
(
filename
:
string
,
length
:
number
,
ondata
:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
seconds
?:
number
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
// 获取指定长度的录音文件内容,length参数可以小于或等于文件的原始长度,其它参数与getFile()方法一致
// 获取指定长度的录音文件内容,length参数可以小于或等于文件的原始长度,其它参数与getFile()方法一致
getFilePart
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
onprogress
?:
(
size
:
number
)
=>
void
)
=>
void
;
getFilePart
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
onprogress
?:
(
size
:
number
)
=>
void
)
=>
void
;
// 暂不使用
// 暂不使用
getFileBlock
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
getFileBlock
:
(
fileName
:
string
,
length
:
number
,
on
?:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
// 录音文件随机读取,从指定位置起读取指定数量个字节
readFile
:
(
fname
:
string
,
offset
:
number
,
length
:
number
)
=>
Promise
<
Uint8Array
>
;
// 传输文件
transferFile
:
(
filename
:
string
,
length
:
number
,
ondata
:
(
msg
:
Uint8Array
|
'fail'
)
=>
void
,
seconds
?:
number
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
// 请求固件更新
// 请求固件更新
// #versionNumber 新固件的版本号
// #versionNumber 新固件的版本号
...
@@ -183,6 +193,9 @@ declare class Jensen {
...
@@ -183,6 +193,9 @@ declare class Jensen {
// #time 超时时长
// #time 超时时长
requestFirmwareUpgrade
:
(
versionNumber
:
number
,
length
:
number
,
time
?:
number
)
=>
Promise
<
{
result
:
'accepted'
|
'fail'
}
>
;
requestFirmwareUpgrade
:
(
versionNumber
:
number
,
length
:
number
,
time
?:
number
)
=>
Promise
<
{
result
:
'accepted'
|
'fail'
}
>
;
// 获取电池电量信息
getBatteryStatus
:
(
time
?:
number
)
=>
Promise
<
BatteryStatus
>
;
// 发送新固件内容到设备端
// 发送新固件内容到设备端
// #data 为固件数据内容
// #data 为固件数据内容
// #seconds 为超时时长
// #seconds 为超时时长
...
@@ -277,9 +290,6 @@ declare class Jensen {
...
@@ -277,9 +290,6 @@ declare class Jensen {
// 获取实时音频流数据
// 获取实时音频流数据
getRealtime
:
(
frames
:
number
)
=>
Promise
<
{
data
:
Uint8Array
;
rest
:
number
}
>
;
getRealtime
:
(
frames
:
number
)
=>
Promise
<
{
data
:
Uint8Array
;
rest
:
number
}
>
;
// 录音文件随机读取,从指定位置起读取指定数量个字节
readFile
:
(
fname
:
string
,
offset
:
number
,
length
:
number
)
=>
Promise
<
Uint8Array
>
;
// 请求设备提示音更新
// 请求设备提示音更新
requestToneUpdate
:
(
signature
:
string
,
size
:
number
,
seconds
?:
number
)
=>
Promise
<
ToneUpdateResponse
>
;
requestToneUpdate
:
(
signature
:
string
,
size
:
number
,
seconds
?:
number
)
=>
Promise
<
ToneUpdateResponse
>
;
...
@@ -291,6 +301,7 @@ declare class Jensen {
...
@@ -291,6 +301,7 @@ declare class Jensen {
// 更新设备UAC固件
// 更新设备UAC固件
updateUAC
:
(
data
:
Uint8Array
,
seconds
?:
number
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
updateUAC
:
(
data
:
Uint8Array
,
seconds
?:
number
)
=>
Promise
<
ReturnStruct
[
'common'
]
>
;
}
}
export
=
Jensen
;
export
=
Jensen
;
src/audio-worker.js
View file @
c2355191
...
@@ -31,36 +31,41 @@ async function initFFmpeg() {
...
@@ -31,36 +31,41 @@ async function initFFmpeg() {
}
}
// 解码mp3数据为pcm数据
// 解码mp3数据为pcm数据
async
function
decode_mp3
(
data
)
{
async
function
decode_mp3
(
req
)
{
try
{
try
{
const
ffmpeg
=
await
initFFmpeg
();
const
ffmpeg
=
await
initFFmpeg
();
// 检查FFmpeg是否已加载
// 检查FFmpeg是否已加载
if
(
!
ffmpeg
||
!
ffmpeg
.
loaded
)
{
if
(
!
ffmpeg
||
!
ffmpeg
.
loaded
)
{
throw
new
Error
(
'FFmpeg not properly loaded'
);
// 如果没有加载完成,就一直等待
while
(
!
ffmpeg
||
!
ffmpeg
.
loaded
)
{
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
100
));
}
// throw new Error('FFmpeg not properly loaded');
}
}
console
.
log
(
'FFmpeg is ready, starting MP3 decode...'
);
console
.
log
(
'FFmpeg is ready, starting MP3 decode...'
);
// 将MP3数据写入FFmpeg
// 将MP3数据写入FFmpeg
await
ffmpeg
.
writeFile
(
'input.mp3'
,
data
);
let
fname
=
req
.
name
;
await
ffmpeg
.
writeFile
(
fname
+
'.mp3'
,
req
.
data
);
// 使用FFmpeg解码MP3为PCM s16le格式
// 使用FFmpeg解码MP3为PCM s16le格式
await
ffmpeg
.
exec
([
await
ffmpeg
.
exec
([
'-i'
,
'input
.mp3'
,
'-i'
,
fname
+
'
.mp3'
,
'-f'
,
's16le'
,
'-f'
,
's16le'
,
'-acodec'
,
'pcm_s16le'
,
'-acodec'
,
'pcm_s16le'
,
'-ar'
,
'16000'
,
'-ar'
,
'16000'
,
'-ac'
,
'1'
,
'-ac'
,
'1'
,
'
output.pcm'
fname
+
'-
output.pcm'
]);
]);
// 读取解码后的PCM数据
// 读取解码后的PCM数据
const
pcmData
=
await
ffmpeg
.
readFile
(
'output.pcm'
);
const
pcmData
=
await
ffmpeg
.
readFile
(
'output.pcm'
);
// 清理临时文件
// 清理临时文件
await
ffmpeg
.
deleteFile
(
'input
.mp3'
);
await
ffmpeg
.
deleteFile
(
fname
+
'
.mp3'
);
await
ffmpeg
.
deleteFile
(
'
output.pcm'
);
await
ffmpeg
.
deleteFile
(
fname
+
'-
output.pcm'
);
// 将Uint8Array转换为Uint16Array (s16le格式)
// 将Uint8Array转换为Uint16Array (s16le格式)
const
uint8Array
=
new
Uint8Array
(
pcmData
);
const
uint8Array
=
new
Uint8Array
(
pcmData
);
...
...
src/index.tsx
View file @
c2355191
...
@@ -98,7 +98,9 @@ export function Home() {
...
@@ -98,7 +98,9 @@ export function Home() {
}
}
const
getTime
=
async
()
=>
{
const
getTime
=
async
()
=>
{
let
time
=
await
jensen
.
getTime
();
console
.
log
(
jensen
);
jensen
.
dump
();
let
time
=
await
jensen
.
getTime
(
5
);
alert
(
JSON
.
stringify
(
time
));
alert
(
JSON
.
stringify
(
time
));
}
}
...
@@ -223,11 +225,17 @@ export function Home() {
...
@@ -223,11 +225,17 @@ export function Home() {
// 先创建audio-worker,给到后面使用
// 先创建audio-worker,给到后面使用
const
audioWorker
=
new
Worker
(
new
URL
(
'./audio-worker.js'
,
import
.
meta
.
url
),
{
type
:
'module'
});
const
audioWorker
=
new
Worker
(
new
URL
(
'./audio-worker.js'
,
import
.
meta
.
url
),
{
type
:
'module'
});
const
waveformWorker
=
new
Worker
(
new
URL
(
'./waveform-worker.js'
,
import
.
meta
.
url
),
{
type
:
'module'
});
waveformWorker
.
onmessage
=
(
e
)
=>
{
console
.
log
(
'waveformWorker'
,
e
);
}
audioWorker
.
onmessage
=
(
e
)
=>
{
audioWorker
.
onmessage
=
(
e
)
=>
{
// console.log(e);
// console.log(e);
if
(
e
.
data
.
waveformData
)
if
(
e
.
data
.
waveformData
)
{
{
console
.
log
(
e
.
data
.
waveformData
);
console
.
log
(
'waveform'
,
e
.
data
.
waveformData
);
// 把e.data.waveformData追加到waveformData的末尾
// 把e.data.waveformData追加到waveformData的末尾
// let waveform = [];
// let waveform = [];
/*
/*
...
@@ -292,6 +300,7 @@ export function Home() {
...
@@ -292,6 +300,7 @@ export function Home() {
const
prepareAudio
=
()
=>
{
const
prepareAudio
=
()
=>
{
const
mediaSource
=
new
MediaSource
();
const
mediaSource
=
new
MediaSource
();
const
audioElement
=
document
.
createElement
(
'audio'
);
const
audioElement
=
document
.
createElement
(
'audio'
);
audioElement
.
autoplay
=
true
;
audioElement
.
src
=
URL
.
createObjectURL
(
mediaSource
);
audioElement
.
src
=
URL
.
createObjectURL
(
mediaSource
);
document
.
body
.
appendChild
(
audioElement
);
document
.
body
.
appendChild
(
audioElement
);
...
@@ -299,34 +308,34 @@ export function Home() {
...
@@ -299,34 +308,34 @@ export function Home() {
// Create source buffer for MP3
// Create source buffer for MP3
let
sourceBuffer
=
mediaSource
.
addSourceBuffer
(
'audio/mpeg'
);
let
sourceBuffer
=
mediaSource
.
addSourceBuffer
(
'audio/mpeg'
);
setSourceBuffer
(
sourceBuffer
);
setSourceBuffer
(
sourceBuffer
);
// sourceBuffer.mode = 'sequence';
alert
(
'sourceopen'
);
alert
(
'sourceopen'
);
// Feed MP3 data to source buffer
// Feed MP3 data to source buffer
sourceBuffer
.
addEventListener
(
'updateend'
,
()
=>
{
sourceBuffer
.
addEventListener
(
'updateend'
,
()
=>
{
if
(
!
sourceBuffer
.
updating
&&
mediaSource
.
readyState
===
'open'
)
{
// dequeue();
mediaSource
.
endOfStream
();
audioElement
.
play
();
}
if
(
mp3
.
length
>
0
)
{
sourceBuffer
.
appendBuffer
(
mp3
.
shift
());
}
});
});
});
});
}
}
const
enqueue
=
(
clip
:
Uint8Array
)
=>
{
const
dequeue
=
()
=>
{
if
(
!
sourceBuffer
)
return
;
if
(
sourceBuffer
?.
updating
==
false
&&
mp3
.
length
>
0
&&
sourceBuffer
.
buffered
.
length
<
100
)
if
(
!
sourceBuffer
.
updating
&&
mp3
.
length
===
0
)
{
{
let
clip
=
mp3
.
shift
();
sourceBuffer
.
appendBuffer
(
clip
);
sourceBuffer
.
appendBuffer
(
clip
);
}
else
{
console
.
log
(
'appendBuffer'
,
clip
.
length
);
mp3
.
push
(
clip
);
}
else
{
setTimeout
(()
=>
dequeue
(),
100
);
}
}
}
}
const
mp3data
:
any
[]
=
[];
const
test
=
async
()
=>
{
const
test
=
async
()
=>
{
let
idx
=
prompt
(
'请输入文件序号'
,
'
0
'
);
let
idx
=
prompt
(
'请输入文件序号'
,
'
8
'
);
if
(
idx
===
null
||
idx
===
undefined
)
return
;
if
(
idx
===
null
||
idx
===
undefined
)
return
;
let
file
=
files
[
parseInt
(
idx
)];
let
file
=
files
[
parseInt
(
idx
)];
if
(
file
===
null
||
file
===
undefined
)
return
alert
(
'文件不存在'
);
if
(
file
===
null
||
file
===
undefined
)
return
alert
(
'文件不存在'
);
...
@@ -334,11 +343,73 @@ export function Home() {
...
@@ -334,11 +343,73 @@ export function Home() {
jensen
.
transferFile
(
file
.
name
,
file
.
length
,
(
data
:
Uint8Array
|
'fail'
)
=>
{
jensen
.
transferFile
(
file
.
name
,
file
.
length
,
(
data
:
Uint8Array
|
'fail'
)
=>
{
if
(
data
instanceof
Uint8Array
)
if
(
data
instanceof
Uint8Array
)
{
{
enqueue
(
data
);
if
(
sourceBuffer
?.
updating
==
false
)
{
sourceBuffer
.
appendBuffer
(
data
);
console
.
log
(
'directly appendBuffer'
,
data
.
length
);
}
else
{
console
.
log
(
'enqueue'
,
data
.
length
);
mp3
.
push
(
data
);
dequeue
();
}
mp3data
.
push
(
data
);
let
totalLength
=
0
;
for
(
let
i
=
0
;
i
<
mp3data
.
length
;
i
++
)
{
totalLength
+=
mp3data
[
i
].
length
;
}
if
(
totalLength
==
file
.
length
)
{
let
audioData
=
new
Uint8Array
(
totalLength
);
let
offset
=
0
;
for
(
let
i
=
0
;
i
<
mp3data
.
length
;
i
++
)
{
audioData
.
set
(
mp3data
[
i
],
offset
);
offset
+=
mp3data
[
i
].
length
;
}
decodeAudioData
(
audioData
);
}
}
}
});
});
}
}
const
decodeAudioData
=
(
audioData
:
Uint8Array
)
=>
{
// 将audioData分成10块,分别发送给audioWorker进行处理
let
clips
=
1
;
let
blockSize
=
Math
.
ceil
(
audioData
.
length
/
clips
);
let
audioContext
=
new
AudioContext
();
for
(
let
i
=
0
;
i
<
clips
;
i
++
)
{
let
block
=
audioData
.
buffer
.
slice
(
i
*
blockSize
,
(
i
+
1
)
*
blockSize
);
let
name
=
'block-'
+
i
;
// audioWorker.postMessage({ data: block, name: name });
audioContext
.
decodeAudioData
(
block
).
then
((
buffer
)
=>
{
// console.log('decodeAudioData' + i, buffer);
waveformWorker
.
postMessage
({
id
:
name
,
pcmdata
:
buffer
});
}).
catch
((
err
)
=>
{
console
.
log
(
'decodeAudioData'
+
i
,
err
);
});
}
}
const
transmit
=
()
=>
{
let
idx
=
prompt
(
'请输入文件序号'
,
'10'
);
if
(
idx
===
null
||
idx
===
undefined
)
return
;
let
file
=
files
[
parseInt
(
idx
)];
if
(
file
===
null
||
file
===
undefined
)
return
alert
(
'文件不存在'
);
jensen
.
getFile
(
file
.
name
,
file
.
length
,
(
data
:
Uint8Array
|
'fail'
)
=>
{},
(
b
:
number
)
=>
{
console
.
log
(
'progress'
,
b
)});
setTimeout
(()
=>
{
jensen
.
stopReceive
();
console
.
log
(
'stop receive.......'
);
setTimeout
(()
=>
{
jensen
.
resumeReceive
();
console
.
log
(
'resume receive.......'
);
},
1500
);
},
5000
);
}
return
(
return
(
<>
<>
<
div
style=
{
{
display
:
'flex'
,
flexDirection
:
'row'
,
gap
:
'16px'
,
padding
:
'16px'
,
alignItems
:
'center'
,
flexWrap
:
'wrap'
}
}
>
<
div
style=
{
{
display
:
'flex'
,
flexDirection
:
'row'
,
gap
:
'16px'
,
padding
:
'16px'
,
alignItems
:
'center'
,
flexWrap
:
'wrap'
}
}
>
...
@@ -346,6 +417,7 @@ export function Home() {
...
@@ -346,6 +417,7 @@ export function Home() {
<
button
onClick=
{
listFiles
}
>
文件列表
</
button
>
<
button
onClick=
{
listFiles
}
>
文件列表
</
button
>
<
button
onClick=
{
prepareAudio
}
>
准备音频
</
button
>
<
button
onClick=
{
prepareAudio
}
>
准备音频
</
button
>
<
button
onClick=
{
test
}
>
传输与播放
</
button
>
<
button
onClick=
{
test
}
>
传输与播放
</
button
>
<
button
onClick=
{
transmit
}
>
传输中断测试
</
button
>
<
button
onClick=
{
getFilePart
}
>
获取文件
</
button
>
<
button
onClick=
{
getFilePart
}
>
获取文件
</
button
>
<
button
onClick=
{
writeSN
}
>
SN写号
</
button
>
<
button
onClick=
{
writeSN
}
>
SN写号
</
button
>
<
button
onClick=
{
getTime
}
>
获取时间
</
button
>
<
button
onClick=
{
getTime
}
>
获取时间
</
button
>
...
...
src/utils/jensen.js
View file @
c2355191
This diff is collapsed.
Click to expand it.
src/waveform-worker.js
0 → 100644
View file @
c2355191
/**
* 模拟 Recorder.getRecordAnalyseData 的功能
* @param {Int16Array} pcmData PCM 原始数据,s16le 格式
* @returns {Uint8Array} 长度固定 1024,范围 0–255 的波形数据
*/
function
getRecordAnalyseData
(
pcmData
)
{
const
BUF_LEN
=
800
;
const
out
=
new
Uint8Array
(
BUF_LEN
);
const
pcm
=
[];
for
(
let
i
=
0
;
i
<
pcmData
.
length
;
i
+=
2
)
{
let
h
=
pcmData
[
i
]
&
0xff
;
let
l
=
pcmData
[
i
+
1
]
&
0xff
;
let
p
=
(
l
<<
8
)
|
h
;
// 如果p是负数,则取反
if
(
p
&
0x8000
)
p
=
(
p
&
0x7fff
)
-
0x8000
;
pcm
.
push
(
p
);
}
const
len
=
pcm
.
length
;
if
(
len
===
0
)
return
out
.
fill
(
128
);
// 空数据,填充中值
const
step
=
len
/
BUF_LEN
;
let
rst
=
[];
for
(
let
i
=
0
;
i
<
BUF_LEN
;
i
++
)
{
const
start
=
Math
.
floor
(
i
*
step
);
const
end
=
Math
.
min
(
Math
.
floor
((
i
+
1
)
*
step
),
len
);
// 计算这一段的平均值(保持正负号,显示真实波形)
let
sum
=
0
;
for
(
let
j
=
start
;
j
<
end
;
j
++
)
{
sum
+=
Math
.
abs
(
pcm
[
j
]);
}
const
avg
=
Math
.
floor
(
sum
/
(
end
-
start
));
rst
.
push
(
avg
);
// 将平均值从 [-32768, 32767] 映射到 [0, 255]
// 128 是中心线(静音),0是最负值,255是最正值
const
normalized
=
avg
/
32767
;
// 归一化到 [0, 1]
out
[
i
]
=
Math
.
max
(
0
,
Math
.
min
(
127
,
Math
.
round
(
normalized
*
127
)));
}
return
out
;
}
self
.
addEventListener
(
'message'
,
async
function
(
e
)
{
try
{
const
task
=
e
.
data
;
// pcmdata的值为浮点类型的0~1,需要转换为s16le格式
const
pcmData
=
new
Int16Array
(
task
.
pcmdata
.
length
);
for
(
let
i
=
0
;
i
<
task
.
pcmdata
.
length
;
i
++
)
{
pcmData
[
i
]
=
Math
.
round
(
task
.
pcmdata
[
i
]
*
32767
);
}
// 生成波形分析数据
const
waveformData
=
getRecordAnalyseData
(
pcmData
);
// 返回结果给主线程
self
.
postMessage
({
id
:
task
.
id
,
success
:
true
,
waveform
:
waveformData
,
});
}
catch
(
error
)
{
console
.
error
(
'Worker error:'
,
error
);
// 返回错误信息给主线程
self
.
postMessage
({
id
:
task
.
id
,
success
:
false
,
error
:
error
.
message
});
}
});
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment