【小程序】播放 pcm 流
1月1日 / 1月21日
实时播放 pcm 流
1export class PCMPlayer { 2 constructor(option) { 3 this.init(option); 4 } 5 6 init(option) { 7 const defaultOption = { 8 inputCodec: 'Int16', // 传入的数据是采用多少位编码,默认16位 9 channels: 1, // 声道数 10 sampleRate: 8000, // 采样率 单位Hz 11 flushTime: 1000, // 缓存时间 单位 ms 12 fftSize: 2048 // analyserNode fftSize 13 }; 14 15 this.option = Object.assign({}, defaultOption, option); // 实例最终配置参数 16 this.samples = new Float32Array(); // 样本存放区域 17 this.interval = setInterval(this.flush.bind(this), this.option.flushTime); 18 this.convertValue = this.getConvertValue(); 19 this.typedArray = this.getTypedArray(); 20 this.initAudioContext(); 21 this.bindAudioContextEvent(); 22 } 23 24 getConvertValue() { 25 // 根据传入的目标编码位数 26 // 选定转换数据所需要的基本值 27 const inputCodecs = { 28 'Int8': 128, 29 'Int16': 32768, 30 'Int32': 2147483648, 31 'Float32': 1 32 }; 33 if (!inputCodecs[this.option.inputCodec]) throw new Error('wrong codec.please input one of these codecs:Int8,Int16,Int32,Float32'); 34 return inputCodecs[this.option.inputCodec]; 35 } 36 37 getTypedArray() { 38 // 根据传入的目标编码位数 39 // 选定前端的所需要的保存的二进制数据格式 40 // 完整TypedArray请看文档 41 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray 42 const typedArrays = { 43 'Int8': Int8Array, 44 'Int16': Int16Array, 45 'Int32': Int32Array, 46 'Float32': Float32Array 47 }; 48 if (!typedArrays[this.option.inputCodec]) throw new Error('wrong codec.please input one of these codecs:Int8,Int16,Int32,Float32'); 49 return typedArrays[this.option.inputCodec]; 50 } 51 52 initAudioContext() { 53 // 初始化音频上下文的东西 54 this.audioCtx = createWebAudioContext(); 55 // 控制音量的 GainNode 56 // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createGain 57 this.gainNode = this.audioCtx.createGain(); 58 this.gainNode.gain.value = 1; 59 this.gainNode.connect(this.audioCtx.destination); 60 this.startTime = this.audioCtx.currentTime; 61 this.analyserNode = this.audioCtx.createAnalyser(); 62 this.analyserNode.fftSize = this.option.fftSize; 63 } 64 65 static isTypedArray(data) { 66 // 检测输入的数据是否为 TypedArray 类型或 ArrayBuffer 类型 67 return (data.byteLength && data.buffer && data.buffer instanceof ArrayBuffer) || data instanceof ArrayBuffer; 68 } 69 70 isSupported(data) { 71 // 数据类型是否支持 72 // 目前支持 ArrayBuffer 或者 TypedArray 73 if (!PCMPlayer.isTypedArray(data)) throw new Error('请传入ArrayBuffer或者任意TypedArray'); 74 return true; 75 } 76 77 feed(data) { 78 this.isSupported(data); 79 80 // 获取格式化后的buffer 81 data = this.getFormattedValue(data); 82 // 开始拷贝buffer数据 83 // 新建一个Float32Array的空间 84 const tmp = new Float32Array(this.samples.length + data.length); 85 // console.log(data, this.samples, this.samples.length) 86 // 复制当前的实例的buffer值(历史buff) 87 // 从头(0)开始复制 88 tmp.set(this.samples, 0); 89 // 复制传入的新数据 90 // 从历史buff位置开始 91 tmp.set(data, this.samples.length); 92 // 将新的完整buff数据赋值给samples 93 // interval定时器也会从samples里面播放数据 94 this.samples = tmp; 95 // console.log('this.samples', this.samples) 96 } 97 98 getFormattedValue(data) { 99 if (data instanceof ArrayBuffer) { 100 data = new this.typedArray(data); 101 } else { 102 data = new this.typedArray(data.buffer); 103 } 104 105 let float32 = new Float32Array(data.length); 106 107 for (let i = 0; i < data.length; i++) { 108 // buffer 缓冲区的数据,需要是IEEE754 里32位的线性PCM,范围从-1到+1 109 // 所以对数据进行除法 110 // 除以对应的位数范围,得到-1到+1的数据 111 // float32[i] = data[i] / 0x8000; 112 float32[i] = data[i] / this.convertValue; 113 } 114 return float32; 115 } 116 117 volume(volume) { 118 this.gainNode.gain.value = volume; 119 } 120 121 destroy() { 122 if (this.interval) { 123 clearInterval(this.interval); 124 } 125 this.samples = null; 126 this.audioCtx.close().then(); 127 this.audioCtx = null; 128 } 129 130 flush() { 131 if (!this.samples.length) return; 132 const self = this; 133 const bufferSource = this.audioCtx.createBufferSource(); 134 if (typeof this.option.onended === 'function') { 135 bufferSource.onended = function (event) { 136 self.option.onended(this, event); 137 }; 138 } 139 const length = this.samples.length / this.option.channels; 140 const audioBuffer = this.audioCtx.createBuffer(this.option.channels, length, this.option.sampleRate); 141 142 for (let channel = 0; channel < this.option.channels; channel++) { 143 const audioData = audioBuffer.getChannelData(channel); 144 let offset = channel; 145 let decrement = 50; 146 for (let i = 0; i < length; i++) { 147 audioData[i] = this.samples[offset]; 148 /* fadein */ 149 if (i < 50) { 150 audioData[i] = (audioData[i] * i) / 50; 151 } 152 /* fadeout*/ 153 if (i >= (length - 51)) { 154 audioData[i] = (audioData[i] * decrement--) / 50; 155 } 156 offset += this.option.channels; 157 } 158 } 159 160 if (this.startTime < this.audioCtx.currentTime) { 161 this.startTime = this.audioCtx.currentTime; 162 } 163 // console.log('start vs current ' + this.startTime + ' vs ' + this.audioCtx.currentTime + ' duration: ' + audioBuffer.duration); 164 // console.log('start', this.startTime); 165 // console.log('duration', audioBuffer.duration); 166 bufferSource.buffer = audioBuffer; 167 bufferSource.connect(this.gainNode); 168 bufferSource.connect(this.analyserNode); // bufferSource连接到analyser 169 bufferSource.start(this.startTime); 170 this.option?.onstart(audioBuffer.duration); 171 this.startTime += audioBuffer.duration; 172 this.samples = new Float32Array(); 173 } 174 175 async pause() { 176 await this.audioCtx.suspend(); 177 } 178 179 async continue() { 180 await this.audioCtx.resume(); 181 } 182 183 bindAudioContextEvent() { 184 const self = this; 185 if (typeof self.option.onstatechange === 'function') { 186 this.audioCtx.onstatechange = function (event) { 187 self.audioCtx && self.option.onstatechange(this, event, self.audioCtx.state); 188 }; 189 } 190 } 191 192} 193 194export default PCMPlayer;
cd ..