amr-recorder
Version:
amr 音频录制与播放(微信语音所用的格式)
706 lines (637 loc) • 17.9 kB
JavaScript
/**
* @file AMR 录音、转换、播放器
* @author BenzLeung(https://github.com/BenzLeung)
* @date 2017/11/12
* Created by JetBrains PhpStorm.
*
* 每位工程师都有保持代码优雅的义务
* each engineer has a duty to keep the code elegant
*/
import RecorderControl from "./RecorderControl";
import amrWorker from "./amrnb";
const amrWorkerStr = amrWorker.toString()
.replace(/^\s*function.*?\(\)\s*{/, '')
.replace(/}\s*$/, '');
const amrWorkerURLObj = (window.URL || window.webkitURL).createObjectURL(new Blob([amrWorkerStr], {type:"text/javascript"}));
export default class BenzAMRRecorder {
/**
* @type {boolean}
* @private
*/
_isInit = false;
/**
* @type {boolean}
* @private
*/
_isInitRecorder = false;
/**
* @type {RecorderControl | null}
* @private
*/
_recorderControl = new RecorderControl();
/**
* @type {Float32Array | null}
* @private
*/
_samples = new Float32Array(0);
/**
* @type {Uint8Array | null}
* @private
*/
_rawData = new Uint8Array(0);
/**
* @type {Blob | null}
* @private
*/
_blob = null;
/**
* @type {Function | null}
* @private
*/
_onEnded = null;
/**
* @type {Function | null}
* @private
*/
_onAutoEnded = null;
/**
* @type {Function | null}
* @private
*/
_onPlay = null;
/**
* @type {Function | null}
* @private
*/
_onPause = null;
/**
* @type {Function | null}
* @private
*/
_onResume = null;
/**
* @type {Function | null}
* @private
*/
_onStop = null;
/**
* @type {Function | null}
* @private
*/
_onStartRecord = null;
/**
* @type {Function | null}
* @private
*/
_onCancelRecord = null;
/**
* @type {Function | null}
* @private
*/
_onFinishRecord = null;
/**
* @type {boolean}
* @private
*/
_isPlaying = false;
/**
* @type {boolean}
* @private
*/
_isPaused = false;
/**
* @type {number}
* @private
*/
_startCtxTime = 0.0;
/**
* @type {number}
* @private
*/
_pauseTime = 0.0;
constructor() {
}
/**
* 是否已经初始化
* @return {boolean}
*/
isInit() {
return this._isInit;
}
/**
* 使用浮点数据初始化
* @param {Float32Array} array
* @return {Promise}
*/
initWithArrayBuffer(array) {
if (this._isInit || this._isInitRecorder) {
BenzAMRRecorder.throwAlreadyInitialized();
}
this._playEmpty();
return new Promise((resolve, reject) => {
let u8Array = new Uint8Array(array);
this.decodeAMRAsync(u8Array).then((samples) => {
this._samples = samples;
this._isInit = true;
if (!this._samples) {
RecorderControl.decodeAudioArrayBufferByContext(array).then((data) => {
this._isInit = true;
return this.encodeAMRAsync(data, RecorderControl.getCtxSampleRate());
}).then((rawData) => {
this._rawData = rawData;
this._blob = BenzAMRRecorder.rawAMRData2Blob(rawData);
return this.decodeAMRAsync(rawData);
}).then((sample) => {
this._samples = sample;
resolve();
}).catch(() => {
reject(new Error('Failed to decode.'));
});
} else {
this._rawData = u8Array;
resolve();
}
});
});
}
/**
* 使用 Blob 对象初始化( <input type="file">)
* @param {Blob} blob
* @return {Promise}
*/
initWithBlob(blob) {
if (this._isInit || this._isInitRecorder) {
BenzAMRRecorder.throwAlreadyInitialized();
}
this._playEmpty();
this._blob = blob;
const p = new Promise((resolve) => {
let reader = new FileReader();
reader.onload = function(e) {
resolve(e.target.result);
};
reader.readAsArrayBuffer(blob);
});
return p.then((data) => {
return this.initWithArrayBuffer(data);
});
}
/**
* 使用 url 初始化
* @param {string} url
* @return {Promise}
*/
initWithUrl(url) {
if (this._isInit || this._isInitRecorder) {
BenzAMRRecorder.throwAlreadyInitialized();
}
this._playEmpty();
const p = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
resolve(this.response);
};
xhr.onerror = function() {
reject(new Error('Failed to fetch ' + url));
};
xhr.send();
});
return p.then((array) => {
return this.initWithArrayBuffer(array);
});
}
/**
* 初始化录音
* @return {Promise}
*/
initWithRecord() {
if (this._isInit || this._isInitRecorder) {
BenzAMRRecorder.throwAlreadyInitialized();
}
this._playEmpty();
return new Promise((resolve, reject) => {
this._recorderControl.initRecorder().then(() => {
this._isInitRecorder = true;
resolve();
}).catch((e) => {
reject(e);
});
});
}
/**
* init 之前先播放一个空音频。
* 因为有些环境(如iOS)播放首个音频时禁止自动、异步播放,
* 播放空音频防止加载后立即播放的功能失效。
* 但即使如此,initWith* 仍然须放入一个用户事件中
* @private
*/
_playEmpty = () => {
this._recorderControl.playPcm(new Float32Array(10), 24000);
};
on(action, fn) {
if (typeof fn === 'function' || fn === null) {
switch (action) {
case 'play':
this._onPlay = fn;
break;
case 'stop':
this._onStop = fn;
break;
case 'pause':
this._onPause = fn;
break;
case 'resume':
this._onResume = fn;
break;
case 'ended':
this._onEnded = fn;
break;
case 'autoEnded':
this._onAutoEnded = fn;
break;
case 'startRecord':
this._onStartRecord = fn;
break;
case 'cancelRecord':
this._onCancelRecord = fn;
break;
case 'finishRecord':
this._onFinishRecord = fn;
break;
case '*':
case 'all':
this._onEnded = fn;
this._onAutoEnded = fn;
this._onPlay = fn;
this._onPause = fn;
this._onResume = fn;
this._onStop = fn;
this._onStartRecord = fn;
this._onCancelRecord = fn;
this._onFinishRecord = fn;
break;
default:
}
}
}
off(action) {
this.on(action, null);
}
/**
* 播放事件
* @param {Function | null} fn
*/
onPlay(fn) {
this.on('play', fn);
}
/**
* 停止事件(包括播放结束)
* @param {Function | null} fn
*/
onStop(fn) {
this.on('stop', fn);
}
/**
* 暂停事件
* @param {Function | null} fn
*/
onPause(fn) {
this.on('pause', fn);
}
/**
* 继续播放事件
* @param {Function | null} fn
*/
onResume(fn) {
this.on('resume', fn);
}
/**
* 播放结束事件
* @param {Function | null} fn
*/
onEnded(fn) {
this.on('ended', fn);
}
/**
* 播放完毕自动结束事件
* @param {Function | null} fn
*/
onAutoEnded(fn) {
this.on('autoEnded', fn);
}
/**
* 开始录音事件
* @param {Function | null} fn
*/
onStartRecord(fn) {
this.on('startRecord', fn);
}
/**
* 结束录音事件
* @param {Function | null} fn
*/
onFinishRecord(fn) {
this.on('finishRecord', fn);
}
/**
* 放弃录音事件
* @param {Function | null} fn
*/
onCancelRecord(fn) {
this.on('cancelRecord', fn);
}
_onEndCallback = () => {
if (this._isPlaying) {
this._isPlaying = false;
if (this._onStop) {
this._onStop();
}
if (this._onAutoEnded) {
this._onAutoEnded();
}
}
if (!this._isPaused) {
if (this._onEnded) {
this._onEnded();
}
}
};
/**
* 播放(重新开始,无视暂停状态)
* @param {number|string?} startTime 可指定开始位置
*/
play(startTime) {
const _startTime = (startTime && startTime < this.getDuration()) ? parseFloat(startTime) : 0;
if (!this._isInit) {
throw new Error('Please init AMR first.');
}
if (this._onPlay) {
this._onPlay();
}
this._isPlaying = true;
this._isPaused = false;
this._startCtxTime = RecorderControl.getCtxTime() - _startTime;
this._recorderControl.playPcm(
this._samples,
this._isInitRecorder ? RecorderControl.getCtxSampleRate() : 8000,
this._onEndCallback.bind(this),
_startTime
);
}
/**
* 停止
*/
stop() {
this._recorderControl.stopPcm();
this._isPlaying = false;
this._isPaused = false;
if (this._onStop) {
this._onStop();
}
}
/**
* 暂停
*/
pause() {
if (!this._isPlaying) {
return;
}
this._isPlaying = false;
this._isPaused = true;
this._pauseTime = RecorderControl.getCtxTime() - this._startCtxTime;
this._recorderControl.stopPcm();
if (this._onPause) {
this._onPause();
}
}
/**
* 从暂停处继续
*/
resume() {
if (!this._isPaused) {
return;
}
this._isPlaying = true;
this._isPaused = false;
this._startCtxTime = RecorderControl.getCtxTime() - this._pauseTime;
this._recorderControl.playPcm(
this._samples,
this._isInitRecorder ? RecorderControl.getCtxSampleRate() : 8000,
this._onEndCallback.bind(this),
this._pauseTime,
);
if (this._onResume) {
this._onResume();
}
}
/**
* 整合 play() 和 resume(),若在暂停状态则继续,否则从头播放
*/
playOrResume() {
if (this._isPaused) {
this.resume();
} else {
this.play();
}
}
/**
* 整合 resume() 和 pause()
*/
pauseOrResume() {
if (this._isPaused) {
this.resume();
} else {
this.pause();
}
}
/**
* 整合 play() 和 resume() 和 pause()
*/
playOrPauseOrResume() {
if (this._isPaused) {
this.resume();
} else if (this._isPlaying) {
this.pause();
} else {
this.play();
}
}
/**
* 跳转到音频指定位置,不改变播放状态
* @param {number|string} time 指定位置(秒,浮点数)
*/
setPosition(time) {
const _time = parseFloat(time);
if (_time > this.getDuration()) {
this.stop();
} else if (this._isPaused) {
this._pauseTime = _time;
} else if (this._isPlaying) {
this._recorderControl.stopPcmSilently();
this._startCtxTime = RecorderControl.getCtxTime() - _time;
this._recorderControl.playPcm(
this._samples,
this._isInitRecorder ? RecorderControl.getCtxSampleRate() : 8000,
this._onEndCallback.bind(this),
_time,
);
} else {
this.play(_time);
}
}
/**
* 获取当前播放位置(秒)
* @return {Number} 位置,秒,浮点数
*/
getCurrentPosition() {
if (this._isPaused) {
return this._pauseTime;
} else if (this._isPlaying) {
return RecorderControl.getCtxTime() - this._startCtxTime;
}
return 0;
}
/**
* 是否正在播放
* @return {boolean}
*/
isPlaying() {
return this._isPlaying;
}
/**
* 是否暂停中
* @return {boolean}
*/
isPaused() {
return this._isPaused;
}
/**
* 开始录音
*/
startRecord() {
this._recorderControl.startRecord();
if (this._onStartRecord) {
this._onStartRecord();
}
}
/**
* 结束录音,并把录制的音频转换成 AMR
* @return {Promise}
*/
finishRecord() {
return new Promise((resolve) => {
this._recorderControl.stopRecord();
this._recorderControl.generateRecordSamples().then((samples) => {
this._samples = samples;
return this.encodeAMRAsync(samples, RecorderControl.getCtxSampleRate());
}).then((rawData) => {
this._rawData = rawData;
this._blob = BenzAMRRecorder.rawAMRData2Blob(this._rawData);
this._isInit = true;
if (this._onFinishRecord) {
this._onFinishRecord();
}
this._recorderControl.releaseRecord();
resolve();
});
});
}
/**
* 放弃录音
*/
cancelRecord() {
this._recorderControl.stopRecord();
this._recorderControl.releaseRecord();
if (this._onCancelRecord) {
this._onCancelRecord();
}
}
/**
* 是否正在录音
* @return {boolean}
*/
isRecording() {
return this._recorderControl.isRecording();
}
/**
* 获取音频的时间长度(单位:秒)
* @return {number}
*/
getDuration() {
let rate = this._isInitRecorder ? RecorderControl.getCtxSampleRate() : 8000;
return this._samples.length / rate;
}
/**
* 获取 AMR 文件的 Blob 对象
* @returns {Blob|null}
*/
getBlob() {
return this._blob;
}
/**
* 注销,清理内部存储
*/
destroy() {
this._recorderControl.stopPcmSilently();
this._recorderControl.stopRecord();
this._recorderControl.releaseRecord();
this.off('*');
this._recorderControl = null;
this._samples = null;
this._rawData = null;
this._blob = null;
}
/*
static encodeAMR(samples, sampleRate) {
sampleRate = sampleRate || 8000;
return AMR.encode(samples, sampleRate, 7);
}
*/
_runAMRWorker = (msg, resolve) => {
const amrWorker = new Worker(amrWorkerURLObj);
amrWorker.postMessage(msg);
amrWorker.onmessage = (e) => {
resolve(e.data.amr);
amrWorker.terminate();
};
};
encodeAMRAsync(samples, sampleRate) {
return new Promise(resolve => {
this._runAMRWorker({
command: 'encode',
samples: samples,
sampleRate: sampleRate
}, resolve);
});
}
decodeAMRAsync(u8Array) {
return new Promise(resolve => {
this._runAMRWorker({
command: 'decode',
buffer: u8Array
}, resolve);
})
}
static rawAMRData2Blob(data) {
return new Blob([data.buffer], {type: 'audio/amr'});
}
static throwAlreadyInitialized() {
throw new Error('AMR has been initialized. For a new AMR, please generate a new BenzAMRRecorder().');
}
/**
* 判断浏览器是否支持播放
* @return {boolean}
*/
static isPlaySupported() {
return RecorderControl.isPlaySupported();
}
/**
* 判断浏览器是否支持录音
* @return {boolean}
*/
static isRecordSupported() {
return RecorderControl.isRecordSupported();
}
}