背景: 临近端午想着没事周末加个班换几天调休,领导就给我调到一个比较忙的项目中写一个录音组件,直接 996
需求:整体需求 做一个录音组件,可以录音,可以播放完整录音(我的活),可以针对某个问题进行标记(我的活),可以播放标记录音(我的活)
录音的功能我猜想就还是 webRtc 然后组内架构提供了一个 npm依赖 方法都有 主要写写样式
https://www.npmjs.com/package/recorder-core
录音播放不用说了就是一个 audio标签 分段合并播放怎么搞呢?
两种方案 1.使用webaudio API 合并 一个音频 2.通过数据seek 到一个播放器的多个不同位置播放
一.web audio API
- 请求录音文件数据
/**
*请求录音文件
*@param url 录音文件地址
*/
export function getAudioArraybuffer(url: string): Promise<ArrayBuffer> {
return request.get(url, { responseType: 'arraybuffer' }).then((response) => response.data);
}
这里使用axios 封装的请求 返回的数据是 arraybuffer 格式
2.创建音频上下文 想操作音频需使用这个 这里没做兼容处理
const audioContext = new window.AudioContext();
//解码变成audioBuffer
function getDecodedAudioData(): Promise<AudioBuffer> {
return audioContext.decodeAudioData(audioData);
}
3.获取录音片段 声道处理那块看着没什么用 必须要有 不然放不出声
function getSegment(decodedData: AudioBuffer): AudioBuffer {
const duration = endTime - startTime;
const slicedBuffer: AudioBuffer = audioContext.createBuffer(
decodedData.numberOfChannels,
duration * decodedData.sampleRate,
decodedData.sampleRate,
);
const startOffset = startTime;
const endOffset = endTime;
for (let channel = 0; channel < decodedData.numberOfChannels; channel++) {
// 声道处理
const channelData = decodedData.getChannelData(channel);
const slicedChannelData = slicedBuffer.getChannelData(channel);
const startSample = Math.floor(startOffset * decodedData.sampleRate);
const endSample = Math.min(Math.ceil(endOffset * decodedData.sampleRate), decodedData.length);
for (let i = startSample; i < endSample; i++) {
slicedChannelData[i - startSample] = channelData[i];
}
}
return slicedBuffer;
}
到这里基本就可以放 网上搜的 可以直接用 sourceNode 再 链接 设备 可以直接放 但是api 配合样式比较复杂 想着是否可以 直接把这个资源给audio 标签帮我播放 我直接用 audio 标签的api。
结果发现audio 标签 必须是url 的 不能是 流格式的
那就转格式
4.AudioBuffer 转 Blob
/**
* audioBuffer转成Blob
* @param audioBuffer
* @returns Blob
*/
export function audioBufferToBlob(audioBuffer: AudioBuffer): Blob {
const { numberOfChannels } = audioBuffer;
const { sampleRate } = audioBuffer;
const channelData: Float32Array[] = [];
for (let channel = 0; channel < numberOfChannels; channel++) {
channelData.push(audioBuffer.getChannelData(channel));
}
const interleavedData = interleaveChannels(channelData);
const buffer = createWavBuffer(interleavedData, sampleRate, numberOfChannels);
const blob = new Blob([buffer], { type: 'audio/wav' });
return blob;
}
/**
* 转 Float32Array
* @param channels
* @returns
*/
function interleaveChannels(channels: Float32Array[]): Float32Array {
const channelCount = channels.length;
const frameCount = channels[0].length;
const result = new Float32Array(frameCount * channelCount);
for (let i = 0; i < frameCount; i++) {
for (let channel = 0; channel < channelCount; channel++) {
result[i * channelCount + channel] = channels[channel][i];
}
}
return result;
}
/**
* 转WAV文件类型 基本支持web audio api 都支持WAV
* @param audioData
* @param sampleRate
* @param numChannels
* @returns
*/
export function createWavBuffer(audioData: Float32Array, sampleRate: number, numChannels: number): ArrayBuffer {
const buffer = new ArrayBuffer(44 + audioData.length * 2);
const view = new DataView(buffer);
// WAV header
writeString(view, 0, 'RIFF'); // ChunkID
view.setUint32(4, 36 + audioData.length * 2, true); // ChunkSize
writeString(view, 8, 'WAVE'); // Format
writeString(view, 12, 'fmt '); // Subchunk1ID
view.setUint32(16, 16, true); // Subchunk1Size
view.setUint16(20, 1, true); // AudioFormat (PCM)
view.setUint16(22, numChannels, true); // NumChannels
view.setUint32(24, sampleRate, true); // SampleRate
view.setUint32(28, sampleRate * numChannels * 2, true); // ByteRate
view.setUint16(32, numChannels * 2, true); // BlockAlign
view.setUint16(34, 16, true); // BitsPerSample
writeString(view, 36, 'data'); // Subchunk2ID
view.setUint32(40, audioData.length * 2, true); // Subchunk2Size
// Audio data
let offset = 44;
// eslint-disable-next-line no-bitwise
const volume = 1 << 15;
for (let i = 0; i < audioData.length; i++, offset += 2) {
view.setInt16(offset, audioData[i] * volume, true);
}
return buffer;
}
export function writeString(view: DataView, offset: number, string: string): void {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
最后 通过 URL.createObjectURL(audioBlob) 创建一个url 就可以 给audio 标签 使用
但是 合并 之后音频 音质 不行 但是也能用
二. seek 到播放器指定时间段
正常的标签播放 直接调用 audio.currentTime = 第几秒;