Files
BYW-VoiceHub/app/workers/audioEncoderWorker.js
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

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 || '编码失败'
})
}
}