由于最近接到一个需求,需要通过前端技术提取视频内音乐,并且不需要上传视频资源到服务器中。因此需要使用到js将mp4格式转为mp3。
需要实现这个操作,我将它分为以下三步
- 提取mp4文件blob arrayBuffer
- 将mp4格式文件转高清wav格式音乐
- 将wav格式音乐压缩为mp3格式(wav格式音乐文件过大)
提取mp4文件blob arrayBuffer
提取mp4文件arrayBuffer,这里分为两种,通过上传文件获取,或通过下载链接获取,这里我们讲述网络下载的方式
fetch(fileUrl)
.then((response) => {
return response.arrayBuffer()
})
.then((buffer) => {
// arrayBuffer转wav音频格式
blobToAwv(buffer, fileName, this.maxConcurrentDownloads, cd, this.numActiveDownloads)
// this.downloadFileMp3(buffer, fileName)
})
.catch((error) => {
console.error("Error downloading file:", error);
})
.finally(() => {
// 这里为下载队列的属性,不需要多文件转换下载,请忽略
this.numActiveDownloads--;
this.processDownloadQueue(cd);
});
将mp4格式文件转高清wav格式音乐
我们需要提取mp4内音频文件,需要使用到AudioContext音频对象,将mp4内音频文件用wav格式导出。(前提是获取到视频的blob对象,blob对象可通过下载链接获取或直接上传文件提取)
export const bufferToWavBlob = (buffer: any, fileName: string, total: number, cd: any) => {
// 创建音频上下文
const audioCtx = new AudioContext();
// arrayBuffer转audioBuffer
audioCtx.decodeAudioData(buffer, async function (audioBuffer) {
// 提取音乐内的wavblob
const blob = bufferToWav(audioBuffer, audioBuffer.sampleRate * audioBuffer.duration);
});
}
function bufferToWav(abuffer, len) {
var numOfChan = abuffer.numberOfChannels,
length = len * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [], i, sample,
offset = 0,
pos = 0;
// write WAVE header
// "RIFF"
setUint32(0x46464952);
// file length - 8
setUint32(length - 8);
// "WAVE"
setUint32(0x45564157);
// "fmt " chunk
setUint32(0x20746d66);
// length = 16
setUint32(16);
// PCM (uncompressed)
setUint16(1);
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
// avg. bytes/sec
setUint32(abuffer.sampleRate * 2 * numOfChan);
// block-align
setUint16(numOfChan * 2);
// 16-bit (hardcoded in this demo)
setUint16(16);
// "data" - chunk
setUint32(0x61746164);
// chunk length
setUint32(length - pos - 4);
// write interleaved data
for(i = 0; i < abuffer.numberOfChannels; i++)
channels.push(abuffer.getChannelData(i));
while(pos < length) {
// interleave channels
for(i = 0; i < numOfChan; i++) {
// 这里需要对channels[i][offset]做非undefied判断
// clamp
sample = Math.max(-1, Math.min(1, channels[i][offset]));
// scale to 16-bit signed int
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0;
// write 16-bit sample
view.setInt16(pos, sample, true);
pos += 2;
}
// next source sample
offset++
}
// create Blob
return new Blob([buffer], {type: "audio/wav"});
function setUint16(data) {
view.setUint16(pos, data, true);
pos += 2;
}
function setUint32(data) {
view.setUint32(pos, data, true);
pos += 4;
}
将wav格式音乐压缩为mp3格式(wav格式音乐文件过大)
在这里就能获取到wav格式音频文件,但是这个音频文件过大。考虑到节省成本,后面就需要将wav格式音频转mp3.这里使用过lamejs但是效果很差。
这里我们用到了一个js录音库。极力推荐yyds。 xiangyuecn/**Recorder.感谢该库作者提供wav转mp3格式。
https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?idf=self_base_demo
import Recorder from "recorder-core"; //注意如果未引用Recorder变量,可能编译时会被优化删除(如vue3 tree-shaking),请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//import './你clone的目录/src/recorder-core.js' //clone源码可以按这个方式引入,下同
//require('./你clone的目录/src/recorder-core.js') //clone源码可以按这个方式引入,下同
//<script src="你clone的目录/src/recorder-core.js"> //这是html中script方式引入,下同
//按需引入你需要的录音格式支持文件,如果需要多个格式支持,把这些格式的编码引擎js文件统统引入进来即可
import "recorder-core/src/engine/mp3";
import "recorder-core/src/engine/mp3-engine";
import "recorder-core/src/engine/wav";
Recorder.Wav2Other = function (newSet, wavBlob, True, False) {
const reader: any = new FileReader();
reader.onloadend = function () {
//检测wav文件头
const wavView = new Uint8Array(reader.result);
const eq = function (p, s) {
for (var i = 0; i < s.length; i++) {
if (wavView[p + i] != s.charCodeAt(i)) {
return false;
}
}
return true;
};
let pcm;
if (eq(0, "RIFF") && eq(8, "WAVEfmt ")) {
var numCh = wavView[22];
if (wavView[20] == 1 && (numCh == 1 || numCh == 2)) {
//raw pcm 单或双声道
var sampleRate =
wavView[24] +
(wavView[25] << 8) +
(wavView[26] << 16) +
(wavView[27] << 24);
var bitRate = wavView[34] + (wavView[35] << 8);
//搜索data块的位置
var dataPos = 0; // 44 或有更多块
for (var i = 12, iL = wavView.length - 8; i < iL; ) {
if (
wavView[i] == 100 &&
wavView[i + 1] == 97 &&
wavView[i + 2] == 116 &&
wavView[i + 3] == 97
) {
//eq(i,"data")
dataPos = i + 8;
break;
}
i += 4;
i +=
4 +
wavView[i] +
(wavView[i + 1] << 8) +
(wavView[i + 2] << 16) +
(wavView[i + 3] << 24);
}
console.log("wav info", sampleRate, bitRate, numCh, dataPos);
if (dataPos) {
if (bitRate == 16) {
pcm = new Int16Array(wavView.buffer.slice(dataPos));
} else if (bitRate == 8) {
pcm = new Int16Array(wavView.length - dataPos);
//8位转成16位
for (var j = dataPos, d = 0; j < wavView.length; j++, d++) {
var b = wavView[j];
pcm[d] = (b - 128) << 8;
}
}
}
if (pcm && numCh == 2) {
//双声道简单转单声道
var pcm1 = new Int16Array(pcm.length / 2);
for (var i = 0; i < pcm1.length; i++) {
pcm1[i] = (pcm[i * 2] + pcm[i * 2 + 1]) / 2;
}
pcm = pcm1;
}
}
}
if (!pcm) {
False && False("非单或双声道wav raw pcm格式音频,无法转码");
return;
}
var rec = Recorder(newSet).mock(pcm, sampleRate);
rec.stop(function (blob, duration) {
True(blob, duration, rec);
}, False);
};
reader.readAsArrayBuffer(wavBlob);
};
export const transWavBlob: any = async (wavBlob: any, fileName: string, total: number, cd) => {
if (!wavBlob) {
return;
}
var set = {
type: "mp3",
sampleRate: 48000,
bitRate: 96,
};
// let resultBlob =null
//数据格式一 Blob
Recorder.Wav2Other(
set,
wavBlob,
function (blob, duration, rec) {
console.log(
blob,
(window.URL || webkitURL).createObjectURL(blob),
"log——audio", total + "process"
);
// 下载进度计数
window.toAudioCount ++
// 下载文件
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName + ".mp3");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
if (window.toAudioCount === total - 1) {
cd();
}
// resultBlob = blob
},
function (msg) {}
);
};