Commit c2355191 authored by martin hou's avatar martin hou

feat: 录音试听测试

parent 2589667c
...@@ -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;
...@@ -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);
......
...@@ -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>
......
This diff is collapsed.
/**
* 模拟 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment