194 lines
5.8 KiB
JavaScript
194 lines
5.8 KiB
JavaScript
import { createMp3Encoder } from 'wasm-media-encoders'
|
|
|
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value))
|
|
|
|
const normalizeStereo = (left, right, targetDb) => {
|
|
const targetGain = Math.pow(10, targetDb / 20)
|
|
let maxPeak = 0
|
|
for (let i = 0; i < left.length; i++) {
|
|
const l = Math.abs(left[i])
|
|
const r = Math.abs(right[i])
|
|
if (l > maxPeak) maxPeak = l
|
|
if (r > maxPeak) maxPeak = r
|
|
}
|
|
if (maxPeak === 0) return
|
|
const gain = targetGain / maxPeak
|
|
for (let i = 0; i < left.length; i++) {
|
|
left[i] *= gain
|
|
right[i] *= gain
|
|
}
|
|
}
|
|
|
|
const resampleLinear = (source, sourceRate, targetRate) => {
|
|
if (sourceRate === targetRate) return source
|
|
const ratio = targetRate / sourceRate
|
|
const targetLength = Math.max(1, Math.floor(source.length * ratio))
|
|
const output = new Float32Array(targetLength)
|
|
for (let i = 0; i < targetLength; i++) {
|
|
const origin = i / ratio
|
|
const left = Math.floor(origin)
|
|
const right = Math.min(source.length - 1, left + 1)
|
|
const weight = origin - left
|
|
output[i] = source[left] * (1 - weight) + source[right] * weight
|
|
}
|
|
return output
|
|
}
|
|
|
|
const interleave = (left, right) => {
|
|
const result = new Float32Array(left.length + right.length)
|
|
let index = 0
|
|
for (let i = 0; i < left.length; i++) {
|
|
result[index++] = left[i]
|
|
result[index++] = right[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
const writeString = (view, offset, text) => {
|
|
for (let i = 0; i < text.length; i++) {
|
|
view.setUint8(offset + i, text.charCodeAt(i))
|
|
}
|
|
}
|
|
|
|
const floatTo16BitPCM = (view, offset, input) => {
|
|
for (let i = 0; i < input.length; i++, offset += 2) {
|
|
const s = clamp(input[i], -1, 1)
|
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
|
|
}
|
|
}
|
|
|
|
const encodeWavBlob = (left, right, sampleRate) => {
|
|
const interleaved = interleave(left, right)
|
|
const numChannels = 2
|
|
const bitDepth = 16
|
|
const bytesPerSample = bitDepth / 8
|
|
const blockAlign = numChannels * bytesPerSample
|
|
const buffer = new ArrayBuffer(44 + interleaved.length * bytesPerSample)
|
|
const view = new DataView(buffer)
|
|
writeString(view, 0, 'RIFF')
|
|
view.setUint32(4, 36 + interleaved.length * bytesPerSample, true)
|
|
writeString(view, 8, 'WAVE')
|
|
writeString(view, 12, 'fmt ')
|
|
view.setUint32(16, 16, true)
|
|
view.setUint16(20, 1, true)
|
|
view.setUint16(22, numChannels, true)
|
|
view.setUint32(24, sampleRate, true)
|
|
view.setUint32(28, sampleRate * blockAlign, true)
|
|
view.setUint16(32, blockAlign, true)
|
|
view.setUint16(34, bitDepth, true)
|
|
writeString(view, 36, 'data')
|
|
view.setUint32(40, interleaved.length * bytesPerSample, true)
|
|
floatTo16BitPCM(view, 44, interleaved)
|
|
return new Blob([view], { type: 'audio/wav' })
|
|
}
|
|
|
|
const encodeMp3Blob = async (left, right, sampleRate) => {
|
|
const encoder = await createMp3Encoder()
|
|
encoder.configure({
|
|
sampleRate,
|
|
channels: 2,
|
|
bitrate: 192,
|
|
outputSampleRate: sampleRate
|
|
})
|
|
const chunkSize = sampleRate * 2
|
|
let processed = 0
|
|
const total = left.length
|
|
const chunks = []
|
|
while (processed < total) {
|
|
const end = Math.min(processed + chunkSize, total)
|
|
const leftChunk = left.subarray(processed, end)
|
|
const rightChunk = right.subarray(processed, end)
|
|
const chunk = encoder.encode([leftChunk, rightChunk])
|
|
if (chunk.length > 0) {
|
|
const copy = new Uint8Array(chunk.length)
|
|
copy.set(chunk)
|
|
chunks.push(copy)
|
|
}
|
|
processed = end
|
|
postMessage({
|
|
type: 'progress',
|
|
stage: 'encode',
|
|
value: Math.round((processed / total) * 100)
|
|
})
|
|
}
|
|
const finalized = encoder.finalize()
|
|
if (finalized.length > 0) {
|
|
const copy = new Uint8Array(finalized.length)
|
|
copy.set(finalized)
|
|
chunks.push(copy)
|
|
}
|
|
return new Blob(chunks, { type: 'audio/mp3' })
|
|
}
|
|
|
|
self.onmessage = async (event) => {
|
|
const payload = event.data
|
|
if (payload?.cmd !== 'encode' || !payload.tracks?.length) return
|
|
try {
|
|
let baseSampleRate = Math.max(...payload.tracks.map((track) => track.sampleRate))
|
|
if (payload.format === 'mp3' && baseSampleRate > 48000) {
|
|
baseSampleRate = 48000
|
|
}
|
|
const normalizedTracks = payload.tracks.map((track, index) => {
|
|
let left = new Float32Array(track.left)
|
|
let right = new Float32Array(track.right)
|
|
if (payload.normalize) {
|
|
normalizeStereo(left, right, payload.targetDb)
|
|
}
|
|
if (track.sampleRate !== baseSampleRate) {
|
|
left = resampleLinear(left, track.sampleRate, baseSampleRate)
|
|
right = resampleLinear(right, track.sampleRate, baseSampleRate)
|
|
}
|
|
postMessage({
|
|
type: 'progress',
|
|
stage: 'prepare',
|
|
value: Math.round(((index + 1) / payload.tracks.length) * 100)
|
|
})
|
|
return {
|
|
left,
|
|
right
|
|
}
|
|
})
|
|
const totalLength = normalizedTracks.reduce((sum, item) => sum + item.left.length, 0)
|
|
const mergedLeft = new Float32Array(totalLength)
|
|
const mergedRight = new Float32Array(totalLength)
|
|
let offset = 0
|
|
for (let i = 0; i < normalizedTracks.length; i++) {
|
|
const item = normalizedTracks[i]
|
|
mergedLeft.set(item.left, offset)
|
|
mergedRight.set(item.right, offset)
|
|
offset += item.left.length
|
|
postMessage({
|
|
type: 'progress',
|
|
stage: 'merge',
|
|
value: Math.round(((i + 1) / normalizedTracks.length) * 100)
|
|
})
|
|
}
|
|
let blob
|
|
if (payload.format === 'wav') {
|
|
postMessage({
|
|
type: 'progress',
|
|
stage: 'encode',
|
|
value: 20
|
|
})
|
|
blob = encodeWavBlob(mergedLeft, mergedRight, baseSampleRate)
|
|
postMessage({
|
|
type: 'progress',
|
|
stage: 'encode',
|
|
value: 100
|
|
})
|
|
} else {
|
|
blob = await encodeMp3Blob(mergedLeft, mergedRight, baseSampleRate)
|
|
}
|
|
postMessage({
|
|
type: 'done',
|
|
blob,
|
|
sampleRate: baseSampleRate
|
|
})
|
|
} catch (error) {
|
|
postMessage({
|
|
type: 'error',
|
|
message: error?.message || '编码失败'
|
|
})
|
|
}
|
|
}
|