|
| 1 | +// Adapted from https://stackoverflow.com/a/42632646 |
| 2 | + |
| 3 | +/* eslint-disable no-restricted-globals */ |
| 4 | +/* eslint-disable no-bitwise */ |
| 5 | + |
| 6 | +class WavePCM |
| 7 | +{ |
| 8 | + sampleRate: number; |
| 9 | + bitDepth: number; |
| 10 | + recordedBuffers: Uint8Array[]; |
| 11 | + bytesPerSample: number; |
| 12 | + numberOfChannels: number|null = null; |
| 13 | + |
| 14 | + constructor(config: {sampleRate: number, bitDepth: number} = { |
| 15 | + sampleRate: 48000, |
| 16 | + bitDepth: 16, |
| 17 | + }) { |
| 18 | + this.sampleRate = config.sampleRate; |
| 19 | + this.bitDepth = config.bitDepth; |
| 20 | + this.recordedBuffers = []; |
| 21 | + this.bytesPerSample = this.bitDepth / 8; |
| 22 | + } |
| 23 | + |
| 24 | + record(buffers: number[][]) { |
| 25 | + this.numberOfChannels = this.numberOfChannels ?? buffers.length; |
| 26 | + const bufferLength = buffers[0].length; |
| 27 | + const reducedData = new Uint8Array(bufferLength * this.numberOfChannels * this.bytesPerSample); |
| 28 | + |
| 29 | + // Interleave |
| 30 | + for (let i = 0; i < bufferLength; i++) { |
| 31 | + for (let channel = 0; channel < this.numberOfChannels; channel++) { |
| 32 | + const outputIndex = (i * this.numberOfChannels + channel) * this.bytesPerSample; |
| 33 | + let sample = buffers[channel][i]; |
| 34 | + |
| 35 | + // Check for clipping |
| 36 | + if (sample > 1) { |
| 37 | + sample = 1; |
| 38 | + } else if (sample < -1) { |
| 39 | + sample = -1; |
| 40 | + } |
| 41 | + |
| 42 | + // bit reduce and convert to uInt |
| 43 | + switch (this.bytesPerSample) { |
| 44 | + case 4: |
| 45 | + sample *= 2147483648; |
| 46 | + reducedData[outputIndex] = sample; |
| 47 | + reducedData[outputIndex + 1] = sample >> 8; |
| 48 | + reducedData[outputIndex + 2] = sample >> 16; |
| 49 | + reducedData[outputIndex + 3] = sample >> 24; |
| 50 | + break; |
| 51 | + |
| 52 | + case 3: |
| 53 | + sample *= 8388608; |
| 54 | + reducedData[outputIndex] = sample; |
| 55 | + reducedData[outputIndex + 1] = sample >> 8; |
| 56 | + reducedData[outputIndex + 2] = sample >> 16; |
| 57 | + break; |
| 58 | + |
| 59 | + case 2: |
| 60 | + sample *= 32768; |
| 61 | + reducedData[outputIndex] = sample; |
| 62 | + reducedData[outputIndex + 1] = sample >> 8; |
| 63 | + break; |
| 64 | + |
| 65 | + case 1: |
| 66 | + reducedData[outputIndex] = (sample + 1) * 128; |
| 67 | + break; |
| 68 | + |
| 69 | + default: |
| 70 | + throw Error('Only 8, 16, 24 and 32 bits per sample are supported'); |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + this.recordedBuffers.push(reducedData); |
| 76 | + } |
| 77 | + |
| 78 | + requestData () { |
| 79 | + const bufferLength = this.recordedBuffers[0].length; |
| 80 | + const dataLength = this.recordedBuffers.length * bufferLength; |
| 81 | + const headerLength = 44; |
| 82 | + const wav = new Uint8Array(headerLength + dataLength); |
| 83 | + const view = new DataView(wav.buffer); |
| 84 | + |
| 85 | + view.setUint32(0, 1380533830, false); // RIFF identifier 'RIFF' |
| 86 | + view.setUint32(4, 36 + dataLength, true); // file length minus RIFF identifier length and file description length |
| 87 | + view.setUint32(8, 1463899717, false); // RIFF type 'WAVE' |
| 88 | + view.setUint32(12, 1718449184, false); // format chunk identifier 'fmt ' |
| 89 | + view.setUint32(16, 16, true); // format chunk length |
| 90 | + view.setUint16(20, 1, true); // sample format (raw) |
| 91 | + view.setUint16(22, (this.numberOfChannels ?? 2), true); // channel count |
| 92 | + view.setUint32(24, this.sampleRate, true); // sample rate |
| 93 | + view.setUint32(28, this.sampleRate * this.bytesPerSample * (this.numberOfChannels ?? 2), true); // byte rate (sample rate * block align) |
| 94 | + view.setUint16(32, this.bytesPerSample * (this.numberOfChannels ?? 2), true); // block align (channel count * bytes per sample) |
| 95 | + view.setUint16(34, this.bitDepth, true); // bits per sample |
| 96 | + view.setUint32(36, 1684108385, false); // data chunk identifier 'data' |
| 97 | + view.setUint32(40, dataLength, true); // data chunk length |
| 98 | + |
| 99 | + for (let i = 0; i < this.recordedBuffers.length; i++) { |
| 100 | + wav.set(this.recordedBuffers[i], i * bufferLength + headerLength); |
| 101 | + } |
| 102 | + |
| 103 | + // @ts-ignore |
| 104 | + self.postMessage(wav, [wav.buffer]); |
| 105 | + self.close(); |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +self.onmessage = function (e) { |
| 110 | + const wavPCM = new WavePCM(e.data.config); |
| 111 | + wavPCM.record(e.data.pcmArrays ?? [[]]); |
| 112 | + wavPCM.requestData(); |
| 113 | +}; |
| 114 | + |
| 115 | +export default WavePCM; |
0 commit comments