544 lines
19 KiB
JavaScript
544 lines
19 KiB
JavaScript
var __renderjsModules={};
|
||
|
||
__renderjsModules["49b84592"] = (() => {
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
var __async = (__this, __arguments, generator) => {
|
||
return new Promise((resolve, reject) => {
|
||
var fulfilled = (value) => {
|
||
try {
|
||
step(generator.next(value));
|
||
} catch (e) {
|
||
reject(e);
|
||
}
|
||
};
|
||
var rejected = (value) => {
|
||
try {
|
||
step(generator.throw(value));
|
||
} catch (e) {
|
||
reject(e);
|
||
}
|
||
};
|
||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||
step((generator = generator.apply(__this, __arguments)).next());
|
||
});
|
||
};
|
||
|
||
// <stdin>
|
||
var stdin_exports = {};
|
||
__export(stdin_exports, {
|
||
default: () => stdin_default
|
||
});
|
||
|
||
// C:/Users/1/Desktop/testAudio/测试流式传输uniapp/pages/index/StreamPlayer.js
|
||
var StreamPlayer = class {
|
||
constructor({
|
||
inputSampleRate = 16e3,
|
||
numChannels = 1,
|
||
bitDepth = 16,
|
||
littleEndian = true,
|
||
pcmType = "int",
|
||
callback = () => {
|
||
}
|
||
} = {}) {
|
||
if (![16, 32].includes(bitDepth))
|
||
throw new Error("bitDepth \u5FC5\u987B\u662F 16 \u6216 32");
|
||
if (inputSampleRate <= 0)
|
||
throw new Error("\u91C7\u6837\u7387\u5FC5\u987B\u5927\u4E8E 0");
|
||
if (!["int", "float"].includes(pcmType))
|
||
throw new Error("pcmType \u5FC5\u987B\u662F int \u6216 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;
|
||
}
|
||
}
|
||
_processQueue() {
|
||
return __async(this, null, function* () {
|
||
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);
|
||
yield this._schedulePlay(audioBuffer);
|
||
this._processQueue();
|
||
} catch (err) {
|
||
console.error("\u97F3\u9891\u5904\u7406\u5931\u8D25:", err.name, err.message, err.stack);
|
||
}
|
||
});
|
||
}
|
||
_convertPCM(buffer) {
|
||
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;
|
||
const audioBuffer = this.audioContext.createBuffer(
|
||
this.numChannels,
|
||
numSamples,
|
||
this.inputSampleRate
|
||
);
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
};
|
||
|
||
// <stdin>
|
||
var player = null;
|
||
var stdin_default = {
|
||
mounted() {
|
||
const _this = this;
|
||
player = new StreamPlayer({
|
||
inputSampleRate: 44100,
|
||
// 后端PCM采样率(如8000/24000)
|
||
numChannels: 1,
|
||
// 声道数(单声道/双声道)
|
||
bitDepth: 16,
|
||
// 位深(16/32bit)
|
||
littleEndian: true,
|
||
// 端序(和后端一致)
|
||
pcmType: "int",
|
||
// 类型(int/float)
|
||
callback: _this.callback
|
||
});
|
||
},
|
||
methods: {
|
||
appendPCMChunk(numArray) {
|
||
if (!numArray || numArray.length === 0)
|
||
return;
|
||
const uint8 = new Uint8Array(numArray);
|
||
const arrayBuffer = uint8.buffer;
|
||
player.appendChunk(arrayBuffer);
|
||
},
|
||
// 停止播放(销毁播放器)
|
||
stop() {
|
||
if (player) {
|
||
player.destroy();
|
||
player = null;
|
||
const _this = this;
|
||
player = new StreamPlayer({
|
||
inputSampleRate: 16e3,
|
||
numChannels: 1,
|
||
bitDepth: 16,
|
||
littleEndian: true,
|
||
pcmType: "int",
|
||
callback: _this.callback
|
||
});
|
||
}
|
||
},
|
||
// 播放结束回调(通知主线程)
|
||
callback(e) {
|
||
if (e === "ended") {
|
||
this.$ownerInstance.callMethod("changeStreamPlaying", { type: false });
|
||
}
|
||
}
|
||
}
|
||
};
|
||
return __toCommonJS(stdin_exports);
|
||
})();
|
||
|
||
|
||
__renderjsModules["1e54c19a"] = (() => {
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
var __async = (__this, __arguments, generator) => {
|
||
return new Promise((resolve, reject) => {
|
||
var fulfilled = (value) => {
|
||
try {
|
||
step(generator.next(value));
|
||
} catch (e) {
|
||
reject(e);
|
||
}
|
||
};
|
||
var rejected = (value) => {
|
||
try {
|
||
step(generator.throw(value));
|
||
} catch (e) {
|
||
reject(e);
|
||
}
|
||
};
|
||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||
step((generator = generator.apply(__this, __arguments)).next());
|
||
});
|
||
};
|
||
|
||
// <stdin>
|
||
var stdin_exports = {};
|
||
__export(stdin_exports, {
|
||
default: () => stdin_default
|
||
});
|
||
var mediaStream;
|
||
var audioContext;
|
||
var processor;
|
||
var recordedChunks = [];
|
||
var decibelHistory = [];
|
||
var DB_CONFIG = {
|
||
minDecibels: -80,
|
||
// 最小可测分贝
|
||
maxDecibels: -30,
|
||
// 最大可测分贝
|
||
smoothingTimeConstant: 0.8
|
||
// 平滑系数,使分贝变化更平缓
|
||
};
|
||
var stdin_default = {
|
||
data() {
|
||
return {};
|
||
},
|
||
methods: {
|
||
startRecord(options) {
|
||
return __async(this, null, function* () {
|
||
if (options == null)
|
||
return;
|
||
if (audioContext)
|
||
return;
|
||
try {
|
||
var a, i = options.sampleRate, s = options.frameSize;
|
||
if (options.gain < 1) {
|
||
options.gain = 1;
|
||
}
|
||
if (options.gain > 20) {
|
||
options.gain = 20;
|
||
}
|
||
audioContext = new AudioContext();
|
||
yield audioContext.audioWorklet.addModule("static/dist/processor.worklet.js");
|
||
mediaStream = yield navigator.mediaDevices.getUserMedia({
|
||
audio: {
|
||
sampleRate: options.sampleRate,
|
||
channelCount: 1
|
||
}
|
||
});
|
||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||
processor = new AudioWorkletNode(audioContext, "processor-worklet");
|
||
processor.port.postMessage({
|
||
type: "init",
|
||
data: {
|
||
frameSize: options.frameSize,
|
||
// 样本数 (1280字节 / 2字节每样本)
|
||
fromSampleRate: 48e3,
|
||
// 输入采样率
|
||
toSampleRate: options.sampleRate,
|
||
// 输出采样率 (1/3)
|
||
arrayBufferType: "short16",
|
||
gain: options.gain
|
||
}
|
||
});
|
||
processor.port.onmessage = (t) => {
|
||
var r = t.data, o = r.frameBuffer, n = r.isLastFrame;
|
||
if (o && o.byteLength > 0) {
|
||
const decibels = this.calculateDecibels(o);
|
||
this.onDecibelsCalculated(decibels);
|
||
decibelHistory.push(decibels);
|
||
if (decibelHistory.length > 100) {
|
||
decibelHistory.shift();
|
||
}
|
||
}
|
||
if (null == o ? void 0 : o.byteLength)
|
||
for (var a2 = 0; a2 < o.byteLength; ) {
|
||
const frameData = {
|
||
isLastFrame: n && a2 + s >= o.byteLength,
|
||
frameBuffer: t.data.frameBuffer.slice(a2, a2 + s)
|
||
};
|
||
this.onFrameRecorded(frameData);
|
||
recordedChunks.push(frameData.frameBuffer);
|
||
a2 += s;
|
||
}
|
||
else
|
||
this.onFrameRecorded(t.data);
|
||
};
|
||
source.connect(processor);
|
||
processor.connect(audioContext.destination);
|
||
} catch (err) {
|
||
this.$ownerInstance.callMethod("toShowToast");
|
||
}
|
||
});
|
||
},
|
||
onFrameRecorded({
|
||
isLastFrame,
|
||
frameBuffer
|
||
}) {
|
||
this.$ownerInstance.callMethod("frameRecorded", {
|
||
isLastFrame,
|
||
frameBuffer: this.toBase64(frameBuffer)
|
||
});
|
||
},
|
||
calculateDecibels(frameBuffer) {
|
||
try {
|
||
const samples = new Int16Array(frameBuffer);
|
||
let sum = 0;
|
||
for (let i = 0; i < samples.length; i++) {
|
||
const value = samples[i] / 32768;
|
||
sum += value * value;
|
||
}
|
||
const rms = Math.sqrt(sum / samples.length);
|
||
if (rms < 1e-5) {
|
||
return DB_CONFIG.minDecibels;
|
||
}
|
||
let db = 20 * Math.log10(rms);
|
||
if (decibelHistory.length > 0) {
|
||
const lastDb = decibelHistory[decibelHistory.length - 1];
|
||
db = lastDb * DB_CONFIG.smoothingTimeConstant + db * (1 - DB_CONFIG.smoothingTimeConstant);
|
||
}
|
||
return Math.max(DB_CONFIG.minDecibels, Math.min(DB_CONFIG.maxDecibels, db));
|
||
} catch (e) {
|
||
console.error("\u8BA1\u7B97\u5206\u8D1D\u65F6\u51FA\u9519:", e);
|
||
return DB_CONFIG.minDecibels;
|
||
}
|
||
},
|
||
onDecibelsCalculated(decibels) {
|
||
this.$ownerInstance.callMethod("decibels", decibels.toFixed(1));
|
||
},
|
||
toBase64(buffer) {
|
||
let binary = "";
|
||
const bytes = new Uint8Array(buffer);
|
||
const len = bytes.byteLength;
|
||
for (let i = 0; i < len; i++) {
|
||
binary += String.fromCharCode(bytes[i]);
|
||
}
|
||
return window.btoa(binary);
|
||
},
|
||
onRecordedChunks(chunks) {
|
||
return __async(this, null, function* () {
|
||
var mergedBuffer = this.mergeAudioBuffers(chunks);
|
||
const wavBlob = this.createWavBlob(mergedBuffer, 1, 16e3);
|
||
const base64 = yield this.blobToBase64(wavBlob);
|
||
this.$ownerInstance.callMethod("recordedChunks", base64);
|
||
});
|
||
},
|
||
onStop(value) {
|
||
if (value !== "stop")
|
||
return;
|
||
this.onFrameRecorded({
|
||
isLastFrame: true,
|
||
frameBuffer: ""
|
||
});
|
||
this.onRecordedChunks(recordedChunks);
|
||
recordedChunks = [];
|
||
if (mediaStream) {
|
||
mediaStream.getTracks().forEach((track) => track.stop());
|
||
mediaStream = null;
|
||
}
|
||
if (processor) {
|
||
processor.disconnect();
|
||
processor = null;
|
||
}
|
||
if (audioContext) {
|
||
audioContext.close().then(() => {
|
||
audioContext = null;
|
||
});
|
||
}
|
||
},
|
||
//合并所有buffer
|
||
mergeAudioBuffers(buffers) {
|
||
let totalLength = buffers.reduce((acc, buf) => acc + buf.byteLength, 0);
|
||
const result = new Uint8Array(totalLength);
|
||
let offset = 0;
|
||
buffers.forEach((buffer) => {
|
||
result.set(new Uint8Array(buffer), offset);
|
||
offset += buffer.byteLength;
|
||
});
|
||
if (offset !== totalLength)
|
||
console.error("\u5408\u5E76\u540E\u7684\u957F\u5EA6\u4E0D\u7B26\uFF01");
|
||
return result.buffer;
|
||
},
|
||
// 新增WAV封装函数(基于前序回答的createWavBlob)
|
||
createWavBlob(pcmData, numChannels, sampleRate) {
|
||
const bytesPerSample = 2;
|
||
const blockAlign = numChannels * bytesPerSample;
|
||
const byteRate = sampleRate * blockAlign;
|
||
const bufferLength = pcmData.byteLength;
|
||
const totalLength = 44 + bufferLength;
|
||
const buffer = new ArrayBuffer(totalLength);
|
||
const view = new DataView(buffer);
|
||
this.writeString(view, 0, "RIFF");
|
||
view.setUint32(4, totalLength - 8, true);
|
||
this.writeString(view, 8, "WAVE");
|
||
this.writeString(view, 12, "fmt ");
|
||
view.setUint32(16, 16, true);
|
||
view.setUint16(20, 1, true);
|
||
view.setUint16(22, numChannels, true);
|
||
view.setUint32(24, sampleRate, true);
|
||
view.setUint32(28, byteRate, true);
|
||
view.setUint16(32, blockAlign, true);
|
||
view.setUint16(34, 16, true);
|
||
this.writeString(view, 36, "data");
|
||
view.setUint32(40, bufferLength, true);
|
||
const pcm16 = new Int16Array(pcmData);
|
||
for (let i = 0; i < pcm16.length; i++) {
|
||
view.setInt16(44 + i * 2, pcm16[i], true);
|
||
}
|
||
return new Blob([buffer], {
|
||
type: "audio/wav"
|
||
});
|
||
},
|
||
// 辅助函数:Blob转Base64
|
||
blobToBase64(blob) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onloadend = () => resolve(reader.result);
|
||
reader.onerror = reject;
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
},
|
||
// 辅助函数:字符串写入DataView
|
||
writeString(view, offset, string) {
|
||
for (let i = 0; i < string.length; i++) {
|
||
view.setUint8(offset + i, string.charCodeAt(i));
|
||
}
|
||
}
|
||
}
|
||
};
|
||
return __toCommonJS(stdin_exports);
|
||
})();
|