初次提交
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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 || '编码失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user