You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

168 lines
4.4 KiB

export default class PcmPlayer {
constructor(options) {
this._options = options;
this.audioCtx = null;
this.rate = this._options.rate ?? 16000;
this.ch = this._options.ch ?? 1;
this.fadeMs = this._options.fadeMs ?? 20;
this.fadeSamples = (this.rate * this.fadeMs) / 1000;
this.queue = [];
this.isPlaying = false;
this.startTime = 0;
this._t = null;
this.AllTexts = [];
this.totalLen = 0
}
feed(data) {
if (!this.audioCtx) {
this.audioCtx = wx.createWebAudioContext();
}
this.queue.push(new Int16Array(data));
if (!this.isPlaying) this._play();
}
loadAudio = (url) => {
return new Promise((resolve) => {
wx.request({
url,
responseType: "arraybuffer",
success: (res) => {
console.log("res.data", res.data);
audioCtx.decodeAudioData(
res.data,
(buffer) => {
resolve(buffer);
},
(err) => {
console.error("decodeAudioData fail", err);
reject();
}
);
},
fail: (res) => {
console.error("request fail", res);
reject();
},
});
});
};
async _play() {
this.isPlaying = true;
if (!this.audioCtx) {
return;
}
// 开始播放
this.startTime = this.audioCtx.currentTime ?? 0;
setTimeout(() => {
this.syncLoop();
});
while (this.queue.length) {
const pcm = this.queue.shift();
const len = pcm.length / this.ch;
const buf = this.audioCtx.createBuffer(this.ch, len, this.rate);
// 1. 归一化(防溢出)
for (let c = 0; c < this.ch; c++) {
const data = buf.getChannelData(c);
for (let i = 0; i < len; i++) {
const val = pcm[i * this.ch + c];
data[i] = val > 0 ? val / 0x7fff : val / 0x8000;
}
}
// 2. 帧边界淡入淡出(防不连续滋滋)
const fadeSamples = Math.min(this.fadeSamples, len); // 5ms@16kHz
const data = buf.getChannelData(0);
for (let j = 0; j < fadeSamples; j++) {
const gain = j / fadeSamples;
data[j] *= gain; // fade-in
data[len - 1 - j] *= gain; // fade-out
}
// 3. 播放并等待结束
await new Promise((resolve, reject) => {
if (!this.isPlaying) {return reject()}
const src = this.audioCtx.createBufferSource();
src.buffer = buf;
src.connect(this.audioCtx.destination);
src.onended = resolve;
src.start();
});
}
this.isPlaying = false;
}
// 监听音频播放进度
syncLoop() {
const loop = () => {
if (!this.audioCtx) {
return;
}
const now = this.audioCtx.currentTime - this.startTime;
// 找当前字幕
const index = this.AllTexts.findIndex(
(s) => now >= s.beginTime && now < s.endTime
);
if (index > -1) {
const obj = this.AllTexts[index];
if (this.AllTexts.length == 1) {
// 最后一条数据
obj.is_final = true;
}
const tmp = this.AllTexts.shift();
this._options.onPlay && this._options.onPlay(tmp);
if (!this.AllTexts.length) {
clearTimeout(this._t);
return;
}
}
if (!this.AllTexts.length) {
this._t = setTimeout(loop, 16);
} else {
if (now < this.AllTexts[this.AllTexts.length - 1].endTime) {
this._t = setTimeout(loop, 16);
} else {
// 结束
clearTimeout(this._t);
console.log("语音播放结束");
this.destroy();
}
}
};
loop();
}
// 增加文字
addText(res) {
if (res.result && res.result.subtitles && res.result.subtitles.length) {
const tmpText = res.result.subtitles.map((it) => {
return {
...it,
content: it.text,
beginTime: it.beginTime / 1000,
endTime: it.endTime / 1000,
request_id: res.requestId,
sessionId: res.sessionId,
};
});
this.AllTexts = [...this.AllTexts, ...tmpText];
}
}
close() {
this.isPlaying = false;
this.audioCtx.close();
}
destroy() {
this.queue = [];
this.isPlaying = false;
this.audioCtx && this.audioCtx.close();
this.audioCtx = null;
this.AllTexts = [];
this.startTime = 0;
this._options.onAudioEnd && this._options.onAudioEnd();
}
}