Commit 458a8c08 authored by martin hou's avatar martin hou

fix: 实时音频传输与测试

parent 458fb496
......@@ -297,16 +297,16 @@ declare class Jensen {
getRealtimeSettings: () => Promise<any>;
// 开始实时音频流传输
startRealtime: () => Promise<ReturnStruct['common']>;
startRealtime: (frames: number, seconds?: number) => Promise<ReturnStruct['common']>;
// 暂停实时音频流传输
pauseRealtime: () => Promise<ReturnStruct['common']>;
// 停止/结束实时音频流传输
stopRealtime: () => Promise<ReturnStruct['common']>;
stopRealtime: (seconds?: number) => Promise<ReturnStruct['common']>;
// 获取实时音频流数据
getRealtime: (frames: number) => Promise<{ data: Uint8Array; rest: number }>;
getRealtime: (frames: number, mute: boolean, seconds?: number) => Promise<{ data: Uint8Array | null; rest: number, muted: boolean }>;
// 录音文件随机读取,从指定位置起读取指定数量个字节
readFile: (fname: string, offset: number, length: number) => Promise<Uint8Array>;
......
......@@ -78,4 +78,18 @@ button:hover
outline: none;
padding: 5px;
box-sizing: border-box;
}
.bar
{
width: 400px;
height: 30px;
border: solid 1px #ccc;
overflow: hidden;
}
.bar div
{
background-color: #090;
border: 0px;
height: 100%;
min-width: 10px;
}
\ No newline at end of file
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import Jensen, { BluetoothDevice } from '..';
import './index.css';
import { Logger } from './Logger'
......@@ -13,6 +13,9 @@ const firmwareVersions = [
{ model: 'hidock-p1', version: '1.2.25', url: 'https://jensen.test.hidock.com/firmwares/hidock-p1-1.2.25.bin', remark: '' }
];
const sleep = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function Home() {
const [files, setFiles] = useState<Jensen.FileInfo[]>([]);
......@@ -21,6 +24,13 @@ export function Home() {
const [firmwares, setFirmwares] = useState<typeof firmwareVersions|null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [sn, setSn] = useState<string|null>(null);
const [liveStates, setLiveStates] = useState<string>('');
const [rmsLeft, setRmsLeft] = useState<number>(0);
const [rmsRight, setRmsRight] = useState<number>(0);
const [liveDelay, setLiveDelay] = useState<number>(100);
const liveTimeoutRef = useRef<number | null>(null);
const liveIntervalRef = useRef<number>(liveDelay);
useEffect(() => { liveIntervalRef.current = liveDelay; }, [liveDelay]);
mgr.onconnectionstatechanged((state, dinfo) => {
console.log('onconnectionstatechanged', state, dinfo);
......@@ -65,13 +75,13 @@ export function Home() {
// 创建日志更新定时器
const logUpdateInterval = setInterval(() => {
// 从Logger.messages中获取最后500条记录
/*
let last = Logger.messages.slice(-500).reverse();
// 将last转换为字符串数组
let logStr: string[] = last.map((item) => {
return `[${item.level === 'error' ? 'x' : '*'}] [${new Date(item.time).toLocaleString()}] (${item.module} - ${item.procedure}) ${item.message}`;
});
setLogs(logStr);
*/
}, 1000);
// 清理函数
......@@ -80,6 +90,14 @@ export function Home() {
};
}, []);
useEffect(() => {
return () => {
if (liveTimeoutRef.current !== null) {
window.clearTimeout(liveTimeoutRef.current);
}
};
}, []);
const info = async () => {
// alert(sn);
let jensen = getJensen();
......@@ -146,7 +164,7 @@ export function Home() {
console.log(r);
}
const clsBtnConnect = {
const clsBtnConnect: React.CSSProperties = {
height: '30px',
padding: '0px 20px',
position: 'absolute',
......@@ -155,7 +173,7 @@ export function Home() {
cursor: 'pointer'
}
const clsBleDevice = {
const clsBleDevice: React.CSSProperties = {
height: '40px',
lineHeight: '40px',
minWidth: '500px',
......@@ -163,7 +181,7 @@ export function Home() {
display: 'inline-block'
}
const clsBleName = {
const clsBleName: React.CSSProperties = {
fontSize: '16px',
fontFamily: 'consolas'
}
......@@ -443,6 +461,90 @@ export function Home() {
Logger.info('jensen', 'bluetooth', 'Clear Paired Devices: ' + JSON.stringify(rst));
}
const rms = (u8: Uint8Array) => {
let sumL = 0;
let sumR = 0;
const frameCount = (u8.length - 8) / 4;
for (let i = 8; i < u8.length; i+=4) {
const a = u8[i + 0] & 0xff;
const b = u8[i + 1] & 0xff;
const c = u8[i + 2] & 0xff;
const d = u8[i + 3] & 0xff;
let l = (b << 8 | a) & 0xffff;
let r = (d << 8 | c) & 0xffff;
if (l & 0x8000) l = (l & 0x7fff) - 0x8000;
if (r & 0x8000) r = (r & 0x7fff) - 0x8000;
// console.log('live, l', l, 'r', r);
const nl = l / 32768;
const nr = r / 32768;
sumL += nl * nl;
sumR += nr * nr;
}
// console.log('live, sumL', sumL, 'sumR', sumR);
return [Math.sqrt(sumL / frameCount), Math.sqrt(sumR / frameCount)];
}
const blocksRef = useRef<Uint8Array[]>([]);
const scheduleLiveTick = async () => {
let jensen = getJensen();
if (jensen == null) return;
let live = await jensen.getRealtime(1, false, 1);
// 若无数据直接调度下一次
if (live?.data == null) return;
setLiveStates('live: ' + live.data.length + ' rest: ' + live.rest + ' muted: ' + live.muted);
// 仅计算,不保留引用;如需录音导出再使用 blocksRef.current.push(live.data.slice(8))
const [rms1, rms2] = rms(live.data);
// console.log('live, rms1', rms1, 'rms2', rms2);
setRmsLeft(Math.floor(rms1 * 4 * 400));
setRmsRight(Math.floor(rms2 * 4 * 400));
// 根据live.rest的值决定timeout的间隔时长,如果>1则为50,否则为100
let timeout = live.rest > 1 ? 50 : 100;
liveTimeoutRef.current = window.setTimeout(scheduleLiveTick, Math.max(0, timeout | 0));
}
const startLive = async () => {
let jensen = getJensen();
if (jensen == null) return;
let rst = await jensen.startRealtime(2, 1);
Logger.info('jensen', 'live', 'Start Live: ' + JSON.stringify(rst));
if (liveTimeoutRef.current !== null) {
window.clearTimeout(liveTimeoutRef.current);
}
// 立即触发一次,并在回调末尾调度下一次
scheduleLiveTick();
}
const stopLive = async () => {
let jensen = getJensen();
if (jensen == null) return;
let rst = await jensen.stopRealtime(1);
Logger.info('jensen', 'live', 'Stop Live: ' + JSON.stringify(rst));
if (liveTimeoutRef.current !== null) {
window.clearTimeout(liveTimeoutRef.current);
liveTimeoutRef.current = null;
}
console.log('live stopped');
// 将blocks拼接成一个Uint8Array,并下载到本地
const totalLen = blocksRef.current.reduce((sum, b) => sum + b.length, 0);
const data = new Uint8Array(totalLen);
let offset = 0;
for (const b of blocksRef.current) {
data.set(b, offset);
offset += b.length;
}
// 清空缓存
/*
blocksRef.current = [];
let blob = new Blob([data], { type: 'audio/pcm' });
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = 'live.pcm';
a.click();
URL.revokeObjectURL(url);
*/
}
return (
<>
<div className="btn-container" style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '16px', alignItems: 'center', flexWrap: 'wrap' }}>
......@@ -472,6 +574,18 @@ export function Home() {
<button onClick={turnPopupOn}>Turn Popup On</button>
<button onClick={turnPopupOff}>Turn Popup Off</button>
<button onClick={getSettings}>Get Settings</button>
<button onClick={startLive}>Start Live</button>
<button onClick={stopLive}>Stop Live</button>
<span style={{ marginLeft: '8px' }}>Interval(ms): </span>
<input
type="number"
value={liveDelay}
onChange={(e) => {
const v = parseInt(e.target.value || '0');
setLiveDelay(isNaN(v) ? 0 : v);
}}
style={{ width: '90px' }}
/>
</div>
<div className="result-container">
<div className="list-container">
......@@ -489,6 +603,15 @@ export function Home() {
</div>
) : (<></>)
}
{
liveStates ? (<>
<div>
<h3>{liveStates}</h3>
<div className="bar"><div style={{ width: rmsLeft + 'px' }}></div></div>
<div className="bar"><div style={{ width: rmsRight + 'px' }}></div></div>
</div>
</>) : <></>
}
<div id="files" style={{ padding: '0px 0px 0px 30px', marginBottom: '20px' }}>
<h3>Files: </h3>
<ol style={{ padding: '0px 0px 0px 30px', 'listStyle': 'none' }}>
......
......@@ -1330,21 +1330,21 @@ Jensen.prototype.sendScheduleInfo = function (infos) {
Jensen.prototype.getRealtimeSettings = async function () {
return this.send(new Command(REALTIME_READ_SETTING));
};
Jensen.prototype.startRealtime = async function () {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
Jensen.prototype.startRealtime = async function (channels, seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, channels & 0x03]), seconds);
};
Jensen.prototype.pauseRealtime = async function () {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]));
Jensen.prototype.pauseRealtime = async function (seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00]), seconds);
};
Jensen.prototype.stopRealtime = async function () {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]));
Jensen.prototype.stopRealtime = async function (seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), seconds);
};
Jensen.prototype.getRealtime = async function (frames) {
Jensen.prototype.getRealtime = async function (frames, mute, seconds) {
let a = (frames >> 24) & 0xff;
let b = (frames >> 16) & 0xff;
let c = (frames >> 8) & 0xff;
let d = (frames >> 0) & 0xff;
return this.send(new Command(REALTIME_TRANSFER).body([a, b, c, d]));
return this.send(new Command(REALTIME_TRANSFER).body([a, b, c, d, 0x00, 0x00, 0x00, mute ? 0x01 : 0x00]), seconds);
};
Jensen.prototype.requestToneUpdate = async function(signature, size, seconds) {
let data = [];
......@@ -1398,8 +1398,14 @@ Jensen.registerHandler(REALTIME_TRANSFER, (msg) => {
let b = msg.body[1] & 0xff;
let c = msg.body[2] & 0xff;
let d = msg.body[3] & 0xff;
let a1 = msg.body[4] & 0xff;
let b1 = msg.body[5] & 0xff;
let c1 = msg.body[6] & 0xff;
let d1 = msg.body[7] & 0xff;
let muted = (a1 << 24 | b1 << 16 | c1 << 8 | d1) == 0x01;
return {
rest: (a << 24) | (b << 16) | (c << 8) | d,
muted: muted,
data: msg.body
};
});
......
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