Files
aistream-test/测试流式传输uniapp/uni_modules/sdx-StreamPlayer/plugins/StreamPlayer.js
T
2025-12-01 21:00:54 +08:00

201 lines
6.1 KiB
JavaScript

/**
* 支持安卓、iOS、H5的pcm格式,
* 如果想支持mp3,需要增加mp3相关解码合并处理等,个人测试效果不太好,所以没有加
*/
export class StreamPlayer {
constructor({
inputSampleRate = 16000,
numChannels = 1,
bitDepth = 16,
littleEndian = true,
pcmType = 'int',
callback = ()=>{}
} = {}) {
// 参数校验
if (![16, 32].includes(bitDepth)) throw new Error('bitDepth 必须是 16 或 32');
if (inputSampleRate <= 0) throw new Error('采样率必须大于 0');
if (!['int', 'float'].includes(pcmType)) throw new Error('pcmType 必须是 int 或 float');
this.audioContext = new(window.AudioContext || window.webkitAudioContext)();
this.inputSampleRate = inputSampleRate;
this.numChannels = numChannels;
this.bitDepth = bitDepth;
this.littleEndian = littleEndian;
this.pcmType = pcmType;
this.audioQueue = [];
this.playbackEndTime = 0;
this.currentSource = null;
this.lastBlockTail = null; // 用于交叉淡入淡出
this.callback = callback;
}
appendChunk(audioData) {
console.log('this.audioQueue', this.audioQueue.length);
this.audioQueue.push(audioData);
this._processQueue();
}
destroy() {
this.audioQueue = [];
this.playbackEndTime = 0;
if (this.currentSource) {
this.currentSource.stop();
this.currentSource = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
}
async _processQueue() {
if(this.audioQueue.length === 0) {
this.callback('ended')
return
}
if (!this.audioContext || this.currentSource) return;
const buffer = this._mergeArrayBuffers(this.audioQueue)
this.audioQueue = []
try {
const audioBuffer = this._convertPCM(buffer);
await this._schedulePlay(audioBuffer);
this._processQueue();
} catch (err) {
console.error('音频处理失败:', err);
}
}
_convertPCM(buffer) {
// 1. 数据对齐:确保长度是样本大小的整数倍
const bytesPerSample = this.bitDepth / 8;
const requiredBytes = bytesPerSample * this.numChannels;
const validByteLength = Math.floor(buffer.byteLength / requiredBytes) * requiredBytes;
const validBuffer = buffer.slice(0, validByteLength);
const dataView = new DataView(validBuffer);
const numSamples = validByteLength / requiredBytes;
// 2. 创建音频缓冲区
const audioBuffer = this.audioContext.createBuffer(
this.numChannels,
numSamples,
this.inputSampleRate
);
// 3. 解析PCM数据
for (let ch = 0; ch < this.numChannels; ch++) {
const channelData = audioBuffer.getChannelData(ch);
for (let i = 0; i < numSamples; i++) {
// 计算字节位置(修正后的多声道索引)
const pos = (i * this.numChannels * bytesPerSample) + (ch * bytesPerSample);
let value;
// 根据位深和数据类型解析
if (this.bitDepth === 16) {
value = dataView.getInt16(pos, this.littleEndian) / 32768;
} else {
if (this.pcmType === 'int') {
value = dataView.getInt32(pos, this.littleEndian) / 2147483648;
} else {
value = dataView.getFloat32(pos, this.littleEndian);
}
}
channelData[i] = value;
}
// 4. 消除直流偏移
let sum = 0;
for (let i = 0; i < channelData.length; i++) {
sum += channelData[i];
}
const dcOffset = sum / channelData.length;
for (let i = 0; i < channelData.length; i++) {
channelData[i] -= dcOffset;
}
// 5. 块内淡入淡出(减少边界突变)
const fadeLength = Math.min(100, channelData.length);
for (let i = 0; i < fadeLength; i++) {
channelData[i] *= (i / fadeLength);
}
const startFadeOut = Math.max(0, channelData.length - fadeLength);
for (let i = startFadeOut; i < channelData.length; i++) {
const factor = 1 - (i - startFadeOut) / fadeLength;
channelData[i] *= factor;
}
}
// 6. 块间交叉淡入淡出(解决重音关键点)
const crossfadeLength = 50;
if (this.lastBlockTail && this.lastBlockTail.length === this.numChannels) {
for (let ch = 0; ch < this.numChannels; ch++) {
const channelData = audioBuffer.getChannelData(ch);
const prevTail = this.lastBlockTail[ch];
// 只处理有足够长度交叉的情况
if (prevTail.length >= crossfadeLength && channelData.length >= crossfadeLength) {
for (let i = 0; i < crossfadeLength; i++) {
const prevWeight = (crossfadeLength - i) / crossfadeLength;
const currWeight = i / crossfadeLength;
channelData[i] = channelData[i] * currWeight + prevTail[i] * prevWeight;
}
}
}
}
// 7. 保存当前块尾部用于下一次交叉
this.lastBlockTail = [];
for (let ch = 0; ch < this.numChannels; ch++) {
const channelData = audioBuffer.getChannelData(ch);
const tailStart = Math.max(0, channelData.length - crossfadeLength);
this.lastBlockTail[ch] = channelData.slice(tailStart);
}
return audioBuffer;
}
_schedulePlay(audioBuffer) {
return new Promise(resolve => {
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
const plannedStartTime = this.playbackEndTime;
const now = this.audioContext.currentTime;
let startTime = Math.max(plannedStartTime, now);
// 动态速率调整(示例,需根据实际测试调整阈值)
const timeDrift = plannedStartTime - now;
if (timeDrift < -0.02) {
source.playbackRate.value = Math.min(1.04, 1 - (timeDrift / audioBuffer.duration));
} else if (timeDrift > 0.02) {
source.playbackRate.value = Math.max(0.96, 1 - (timeDrift / audioBuffer.duration));
} else {
source.playbackRate.value = 1.0;
}
source.start(startTime);
this.playbackEndTime = startTime + (audioBuffer.duration / source.playbackRate.value);
source.onended = () => {
this.currentSource = null;
resolve();
};
this.currentSource = source;
});
}
// 合并多个 ArrayBuffer 的辅助函数
_mergeArrayBuffers(buffers) {
let totalLength = 0;
for (let buffer of buffers) {
totalLength += buffer.byteLength;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (let buffer of buffers) {
const view = new Uint8Array(buffer);
result.set(view, offset);
offset += view.length;
}
return result.buffer;
}
}