crunker.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. interface CrunkerConstructorOptions {
  2. sampleRate: number
  3. concurrentNetworkRequests: number
  4. }
  5. type CrunkerInputTypes = string | File | Blob | undefined
  6. export default class Crunker {
  7. private readonly _sampleRate: number
  8. private readonly _concurrentNetworkRequests: number
  9. private readonly _context: AudioContext
  10. constructor({ sampleRate, concurrentNetworkRequests = 200 }: Partial<CrunkerConstructorOptions> = {}) {
  11. this._context = this._createContext(sampleRate)
  12. sampleRate ||= this._context.sampleRate
  13. this._sampleRate = sampleRate
  14. this._concurrentNetworkRequests = concurrentNetworkRequests
  15. }
  16. private _createContext(sampleRate = 22050): AudioContext {
  17. window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext
  18. return new AudioContext({ sampleRate })
  19. }
  20. /**
  21. *转换url等类型为buffer
  22. */
  23. async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
  24. const buffers: (AudioBuffer | undefined)[] = []
  25. const groups = Math.ceil(filepaths.length / this._concurrentNetworkRequests)
  26. for (let i = 0; i < groups; i++) {
  27. const group = filepaths.slice(i * this._concurrentNetworkRequests, (i + 1) * this._concurrentNetworkRequests)
  28. buffers.push(...(await this._fetchAudio(...group)))
  29. }
  30. return buffers
  31. }
  32. private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
  33. return await Promise.all(
  34. filepaths.map(async filepath => {
  35. if (!filepath) {
  36. return Promise.resolve(undefined)
  37. }
  38. let buffer: ArrayBuffer
  39. if (filepath instanceof File || filepath instanceof Blob) {
  40. buffer = await filepath.arrayBuffer()
  41. } else {
  42. buffer = await fetch(filepath).then(response => {
  43. if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")!.includes("audio/")) {
  44. console.warn(
  45. `Crunker: Attempted to fetch an audio file, but its MIME type is \`${
  46. response.headers.get("Content-Type")!.split(";")[0]
  47. }\`. We'll try and continue anyway. (file: "${filepath}")`
  48. )
  49. }
  50. return response.arrayBuffer()
  51. })
  52. }
  53. /* 这里有个坑 safa浏览器老一点的版本不支持decodeAudioData返回promise 所以用这种老式写法 */
  54. return await new Promise((res, rej) => {
  55. this._context.decodeAudioData(
  56. buffer,
  57. buffer => {
  58. res(buffer)
  59. },
  60. err => {
  61. rej(err)
  62. }
  63. )
  64. })
  65. })
  66. )
  67. }
  68. /**
  69. * 根据时间合并音频
  70. */
  71. mergeAudioBuffers(buffers: AudioBuffer[], times: number[]): AudioBuffer {
  72. if (buffers.length !== times.length) {
  73. throw new Error("buffer数量和times数量必须一致")
  74. }
  75. const output = this._context.createBuffer(this._maxNumberOfChannels(buffers), this._sampleRate * this._maxDuration(buffers), this._sampleRate)
  76. buffers.forEach((buffer, index) => {
  77. const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
  78. for (let channelNumber = 0; channelNumber < output.numberOfChannels; channelNumber++) {
  79. const outputData = output.getChannelData(channelNumber)
  80. // buffers 有可能是单声道,当单声道的时候 取第一个声道的值
  81. const bufferData = buffer.getChannelData(buffer.numberOfChannels < 2 ? 0 : channelNumber)
  82. for (let i = bufferData.length - 1; i >= 0; i--) {
  83. // 当合并大于1或者小于-1的时候可能会爆音 所以这里取最大值和最小值
  84. const combinedValue = outputData[i + offsetNum] + bufferData[i]
  85. outputData[i + offsetNum] = Math.max(-1, Math.min(1, combinedValue))
  86. }
  87. }
  88. })
  89. return output
  90. }
  91. /**
  92. * 根据buffer导出audio标签
  93. */
  94. exportAudioElement(buffer: AudioBuffer, type = "audio/mp3"): HTMLAudioElement {
  95. const recorded = this._interleave(buffer)
  96. const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate)
  97. const audioBlob = new Blob([dataview], { type })
  98. return this._renderAudioElement(audioBlob)
  99. }
  100. /**
  101. * 计算音频前面的空白
  102. */
  103. calculateSilenceDuration(buffer: AudioBuffer) {
  104. const threshold = 0.01 // 静音阈值,低于此值的部分认为是静音
  105. const sampleRate = buffer.sampleRate
  106. const channelData = buffer.getChannelData(0) // 只处理单声道数据
  107. let silenceDuration = 0
  108. for (let i = 0; i < channelData.length; i++) {
  109. if (Math.abs(channelData[i]) > threshold) {
  110. break
  111. }
  112. silenceDuration++
  113. }
  114. // 将样本数转换为秒
  115. silenceDuration = silenceDuration / sampleRate
  116. return silenceDuration
  117. }
  118. private _maxNumberOfChannels(buffers: AudioBuffer[]): number {
  119. return Math.max(...buffers.map(buffer => buffer.numberOfChannels))
  120. }
  121. private _maxDuration(buffers: AudioBuffer[]): number {
  122. return Math.max(...buffers.map(buffer => buffer.duration))
  123. }
  124. private _interleave(input: AudioBuffer): Float32Array {
  125. if (input.numberOfChannels === 1) {
  126. return input.getChannelData(0)
  127. }
  128. const channels = []
  129. for (let i = 0; i < input.numberOfChannels; i++) {
  130. channels.push(input.getChannelData(i))
  131. }
  132. const length = channels.reduce((prev, channelData) => prev + channelData.length, 0)
  133. const result = new Float32Array(length)
  134. let index = 0
  135. let inputIndex = 0
  136. while (index < length) {
  137. channels.forEach(channelData => {
  138. result[index++] = channelData[inputIndex]
  139. })
  140. inputIndex++
  141. }
  142. return result
  143. }
  144. private _renderAudioElement(blob: Blob): HTMLAudioElement {
  145. const audio = document.createElement("audio")
  146. audio.src = this._renderURL(blob)
  147. audio.load()
  148. return audio
  149. }
  150. private _renderURL(blob: Blob): string {
  151. return (window.URL || window.webkitURL).createObjectURL(blob)
  152. }
  153. private _writeHeaders(buffer: Float32Array, numOfChannels: number, sampleRate: number): DataView {
  154. const bitDepth = 16
  155. const bytesPerSample = bitDepth / 8
  156. const sampleSize = numOfChannels * bytesPerSample
  157. const fileHeaderSize = 8
  158. const chunkHeaderSize = 36
  159. const chunkDataSize = buffer.length * bytesPerSample
  160. const chunkTotalSize = chunkHeaderSize + chunkDataSize
  161. const arrayBuffer = new ArrayBuffer(fileHeaderSize + chunkTotalSize)
  162. const view = new DataView(arrayBuffer)
  163. this._writeString(view, 0, "RIFF")
  164. view.setUint32(4, chunkTotalSize, true)
  165. this._writeString(view, 8, "WAVE")
  166. this._writeString(view, 12, "fmt ")
  167. view.setUint32(16, 16, true)
  168. view.setUint16(20, 1, true)
  169. view.setUint16(22, numOfChannels, true)
  170. view.setUint32(24, sampleRate, true)
  171. view.setUint32(28, sampleRate * sampleSize, true)
  172. view.setUint16(32, sampleSize, true)
  173. view.setUint16(34, bitDepth, true)
  174. this._writeString(view, 36, "data")
  175. view.setUint32(40, chunkDataSize, true)
  176. return this._floatTo16BitPCM(view, buffer, fileHeaderSize + chunkHeaderSize)
  177. }
  178. private _floatTo16BitPCM(dataview: DataView, buffer: Float32Array, offset: number): DataView {
  179. for (let i = 0; i < buffer.length; i++, offset += 2) {
  180. const tmp = Math.max(-1, Math.min(1, buffer[i]))
  181. dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7fff, true)
  182. }
  183. return dataview
  184. }
  185. private _writeString(dataview: DataView, offset: number, header: string): void {
  186. for (let i = 0; i < header.length; i++) {
  187. dataview.setUint8(offset + i, header.charCodeAt(i))
  188. }
  189. }
  190. }