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

fix: 实时音频传输与测试

parent 458fb496
...@@ -297,16 +297,16 @@ declare class Jensen { ...@@ -297,16 +297,16 @@ declare class Jensen {
getRealtimeSettings: () => Promise<any>; getRealtimeSettings: () => Promise<any>;
// 开始实时音频流传输 // 开始实时音频流传输
startRealtime: () => Promise<ReturnStruct['common']>; startRealtime: (frames: number, seconds?: number) => Promise<ReturnStruct['common']>;
// 暂停实时音频流传输 // 暂停实时音频流传输
pauseRealtime: () => 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>; readFile: (fname: string, offset: number, length: number) => Promise<Uint8Array>;
......
...@@ -78,4 +78,18 @@ button:hover ...@@ -78,4 +78,18 @@ button:hover
outline: none; outline: none;
padding: 5px; padding: 5px;
box-sizing: border-box; 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 Jensen, { BluetoothDevice } from '..';
import './index.css'; import './index.css';
import { Logger } from './Logger' import { Logger } from './Logger'
...@@ -13,6 +13,9 @@ const firmwareVersions = [ ...@@ -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: '' } { 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() { export function Home() {
const [files, setFiles] = useState<Jensen.FileInfo[]>([]); const [files, setFiles] = useState<Jensen.FileInfo[]>([]);
...@@ -21,6 +24,13 @@ export function Home() { ...@@ -21,6 +24,13 @@ export function Home() {
const [firmwares, setFirmwares] = useState<typeof firmwareVersions|null>(null); const [firmwares, setFirmwares] = useState<typeof firmwareVersions|null>(null);
const [logs, setLogs] = useState<string[]>([]); const [logs, setLogs] = useState<string[]>([]);
const [sn, setSn] = useState<string|null>(null); 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) => { mgr.onconnectionstatechanged((state, dinfo) => {
console.log('onconnectionstatechanged', state, dinfo); console.log('onconnectionstatechanged', state, dinfo);
...@@ -65,13 +75,13 @@ export function Home() { ...@@ -65,13 +75,13 @@ export function Home() {
// 创建日志更新定时器 // 创建日志更新定时器
const logUpdateInterval = setInterval(() => { const logUpdateInterval = setInterval(() => {
// 从Logger.messages中获取最后500条记录 /*
let last = Logger.messages.slice(-500).reverse(); let last = Logger.messages.slice(-500).reverse();
// 将last转换为字符串数组
let logStr: string[] = last.map((item) => { let logStr: string[] = last.map((item) => {
return `[${item.level === 'error' ? 'x' : '*'}] [${new Date(item.time).toLocaleString()}] (${item.module} - ${item.procedure}) ${item.message}`; return `[${item.level === 'error' ? 'x' : '*'}] [${new Date(item.time).toLocaleString()}] (${item.module} - ${item.procedure}) ${item.message}`;
}); });
setLogs(logStr); setLogs(logStr);
*/
}, 1000); }, 1000);
// 清理函数 // 清理函数
...@@ -80,6 +90,14 @@ export function Home() { ...@@ -80,6 +90,14 @@ export function Home() {
}; };
}, []); }, []);
useEffect(() => {
return () => {
if (liveTimeoutRef.current !== null) {
window.clearTimeout(liveTimeoutRef.current);
}
};
}, []);
const info = async () => { const info = async () => {
// alert(sn); // alert(sn);
let jensen = getJensen(); let jensen = getJensen();
...@@ -146,7 +164,7 @@ export function Home() { ...@@ -146,7 +164,7 @@ export function Home() {
console.log(r); console.log(r);
} }
const clsBtnConnect = { const clsBtnConnect: React.CSSProperties = {
height: '30px', height: '30px',
padding: '0px 20px', padding: '0px 20px',
position: 'absolute', position: 'absolute',
...@@ -155,7 +173,7 @@ export function Home() { ...@@ -155,7 +173,7 @@ export function Home() {
cursor: 'pointer' cursor: 'pointer'
} }
const clsBleDevice = { const clsBleDevice: React.CSSProperties = {
height: '40px', height: '40px',
lineHeight: '40px', lineHeight: '40px',
minWidth: '500px', minWidth: '500px',
...@@ -163,7 +181,7 @@ export function Home() { ...@@ -163,7 +181,7 @@ export function Home() {
display: 'inline-block' display: 'inline-block'
} }
const clsBleName = { const clsBleName: React.CSSProperties = {
fontSize: '16px', fontSize: '16px',
fontFamily: 'consolas' fontFamily: 'consolas'
} }
...@@ -443,6 +461,90 @@ export function Home() { ...@@ -443,6 +461,90 @@ export function Home() {
Logger.info('jensen', 'bluetooth', 'Clear Paired Devices: ' + JSON.stringify(rst)); 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 ( return (
<> <>
<div className="btn-container" style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '16px', alignItems: 'center', flexWrap: 'wrap' }}> <div className="btn-container" style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '16px', alignItems: 'center', flexWrap: 'wrap' }}>
...@@ -472,6 +574,18 @@ export function Home() { ...@@ -472,6 +574,18 @@ export function Home() {
<button onClick={turnPopupOn}>Turn Popup On</button> <button onClick={turnPopupOn}>Turn Popup On</button>
<button onClick={turnPopupOff}>Turn Popup Off</button> <button onClick={turnPopupOff}>Turn Popup Off</button>
<button onClick={getSettings}>Get Settings</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>
<div className="result-container"> <div className="result-container">
<div className="list-container"> <div className="list-container">
...@@ -489,6 +603,15 @@ export function Home() { ...@@ -489,6 +603,15 @@ export function Home() {
</div> </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' }}> <div id="files" style={{ padding: '0px 0px 0px 30px', marginBottom: '20px' }}>
<h3>Files: </h3> <h3>Files: </h3>
<ol style={{ padding: '0px 0px 0px 30px', 'listStyle': 'none' }}> <ol style={{ padding: '0px 0px 0px 30px', 'listStyle': 'none' }}>
......
...@@ -1330,21 +1330,21 @@ Jensen.prototype.sendScheduleInfo = function (infos) { ...@@ -1330,21 +1330,21 @@ Jensen.prototype.sendScheduleInfo = function (infos) {
Jensen.prototype.getRealtimeSettings = async function () { Jensen.prototype.getRealtimeSettings = async function () {
return this.send(new Command(REALTIME_READ_SETTING)); return this.send(new Command(REALTIME_READ_SETTING));
}; };
Jensen.prototype.startRealtime = async function () { Jensen.prototype.startRealtime = async function (channels, seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])); return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, channels & 0x03]), seconds);
}; };
Jensen.prototype.pauseRealtime = async function () { Jensen.prototype.pauseRealtime = async function (seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00]), seconds);
}; };
Jensen.prototype.stopRealtime = async function () { Jensen.prototype.stopRealtime = async function (seconds) {
return this.send(new Command(REALTIME_CONTROL).body([0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01])); 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 a = (frames >> 24) & 0xff;
let b = (frames >> 16) & 0xff; let b = (frames >> 16) & 0xff;
let c = (frames >> 8) & 0xff; let c = (frames >> 8) & 0xff;
let d = (frames >> 0) & 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) { Jensen.prototype.requestToneUpdate = async function(signature, size, seconds) {
let data = []; let data = [];
...@@ -1398,8 +1398,14 @@ Jensen.registerHandler(REALTIME_TRANSFER, (msg) => { ...@@ -1398,8 +1398,14 @@ Jensen.registerHandler(REALTIME_TRANSFER, (msg) => {
let b = msg.body[1] & 0xff; let b = msg.body[1] & 0xff;
let c = msg.body[2] & 0xff; let c = msg.body[2] & 0xff;
let d = msg.body[3] & 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 { return {
rest: (a << 24) | (b << 16) | (c << 8) | d, rest: (a << 24) | (b << 16) | (c << 8) | d,
muted: muted,
data: msg.body 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